比如return-to-libc攻击作为ROP的一类特例,就需要攻击者首先在自己的机器上使用GDB一类工具从本地libc库中寻找所需指令的返回地址,然后构造为数据包后准备提交给受害者的机器上的某个有栈溢出漏洞的进程,这个方法成立的前提应当是攻击者自己的机器和受害者的机器的进程内存空间中同样的返回地址下有著同样指令吧?而为了便于猜测返回地址,很多实验还会关闭地址随机化设置。

实际上,假如不是libc这样的基本库函数已经公开而广为通用了,ROP攻击者是否其实不一定知道受害者的机器上一个进程的内存空间内的各类指令分别放在哪些地址下呢?

那么假如使用某种不影响程序正常运行的花指令技术,对每一台机器内哪怕最基本的标准库文件都进行相应处理,使得不同设备上的库文件内部的具体实现不完全相同,从而使各个指令的返回地址也不完全相同或者无法预估,这样能否防御住ROP攻击呢?


攻击者本机上的libc跟目标机器上本来就不一定一样。就算你不加混淆,libc也有不同的版本啊。不同版本的libc的不同函数偏移本来就有差别。

而且还有个问题是,『花指令』其实是逆向的对抗技术,让基于静态扫描的反汇编无法得到正确的结果。你设想的东西我不知道学界有没有专用名词,让我自己称呼的话,我会叫它『地址混淆』。

然后说问题的关键:一个攻击可以很复杂,ROP往往只是其中的一环。在ROP之前,攻击者完全可以先通过某个任意读的漏洞扫一圈地址空间。如果他能这样做,那你对libc的函数做地址混淆并没有什么意义,反正他还是能获得libc。

而由于ASLR的存在,真实攻击中的ROP通常都是要以某个内存泄露的漏洞为前提的……不然连基地址都拿不到,根本没法做ROP。

综上所述,你设想的东西不是说没有意义,但对防护能力的提升也非常有限,这大概就是它没有被广泛应用的原因吧。


操作系统防ROP不是靠花指令,而是靠库载入地址的随机性。另外如果有些固定相对偏移的表内可能包含地址的情况下,比如_ _exit__list里面的清理回调函数,都使用一个叫做PTR_ MANGLE的宏进行编码,使用时需要使用PTR_ DEMANGLE宏解码,这个宏原理如下:

# define PTR_MANGLE(var) asm ("xor %%fs:%c2, %0
"
"rol $2*" LP_SIZE "+1, %0"
: "=r" (var)
: "0" (var),
"i" (offsetof (tcbhead_t,
# define PTR_DEMANGLE(var) asm ("ror $2*" LP_SIZE "+1, %0
"
"xor %%fs:%c2, %0"
: "=r" (var)
: "0" (var),
"i" (offsetof (tcbhead_t,
pointer_guard)))

而tcbhead_t结构如下:

typedef struct
{
dtv_t *dtv;
uintptr_t pointer_guard;
} tcbhead_t;

可以看出来,这两个宏实际上就是把指针跟tcbhead里面的pointer_guard异或了以下,所以如果在非特权态即使取到了地址,但是拿不到pointer_ guard的话,也是没办法知道指针地址的。这样攻击者就没办法了。

但是这里面是有脆弱性的,如果已经知道一个libc公共函数的地址,然后又知道了mangle过的地址,是可以反推pointer _guard的,这样就可以解密指针了。这个脆弱性直到现在最新的libc(2020-6-15)里面也有,已经有大神写出来过相关的脚本进行反推了,自己可以google一下。


不行。从题干描述来看,你的目的是让目标机器与攻击者的机器上的 libc 文件内容不同。但问题在于,目标机器上每次进程启动所载入的 libc 都是相同的。只要攻击可以重复(比如服务崩溃后自动重启),攻击者只需要知道一个大致范围就可以了,大不了上下 1K 位元组范围挨个试,这个防御手段就变成了摆设。

除非可以做到进程每次载入的 libc 加的花都不一样。这样的话就把动态链接库最大的一个优点弄没了:我的机器上一共有 90 个进程(这就是我现在 Windows 10 刚开机的状态),但内存里只需要一份 libc。如果每个进程的 libc 都加不同的花,就变成了大家的 libc 都是独一份,2M 的 libc 乘以 90 就变成了 180M 的内存占用。这还只是 libc 一个文件,那么多基础的动态链接库全搞一遍,进程再多一点,10 个 G 都不一定够用?。

地址空间随机化没有上面的两个问题,好用 ?


(当然更有可能的情况是,你还没溢出完,就被 Security Cookie 给逮住了。Security Cookie,好用 ?


答案是不能。

ROP是扫码代码空间找出合适代码,这个代码不一定是原代码。

比如

L1:

mov $0x12345678, %rax

这条指令从L1开始执行,是条mov指令,但是从L1+1,开始执行呢,就不是mov指令了。所以加花指令是没有用的,攻击者扫码的返回指令之前的代码。而返回指令就一个位元组c3,加了花指令代码空间也会有很多0xc3。只要c3之前能拼凑出有用的代码,就能被利用。因为栈是被攻击者控制的,找出需要的代码还是很简单。尤其是x86,指令长度不固定的,因此没法逆序反汇编,或者说逆序反汇编有很多种可能。在0xc3之前的代码进行反汇编,如选地址-1,-2,-3,-4,-5,-6,-7,-8等进行反汇编,会得到各种不同的指令。其实代码加密,很多用的也是这个方法。反汇编看到的代码是杂乱无章的,就是执行的时候改了执行地址。

比如:

jmp L1+1:

L1:

.db 0x48

...

以上代码执行的时候没有问题,但是反汇编出来,jmp L1+1之后会接著反汇编,会反汇编到0x48(mov指令),而实际上0x48是跳过的,达到了反汇编代码乱码的效果。

操作系统为了保护代码,代码段是只读的,ROP利用栈跟现有代码片段组合,达到了改写代码段的效果。有人已经证明ROP是图灵完备的,即可以利用现有代码片段+控制的栈内容可以构造出任意逻辑代码。


可以啊,有一些论文就是这么干的。思想还是随机化,ASLR是地址随机化。你说的就是局部指令随机化,还有的更激进的有指令集随机化。只是实用的目前只有ASLR。


推荐阅读:
相关文章