先看一个代码:
#include <stdio.h>
#include <stdlib.h>
// 下面的dummy_libc_part1和dummy_libc_part2假设是GLIBC库里的任意两段函数
void dummy_libc_part1()
{
// ... 这里可能会有别的指令
__asm("mov 0(%rsp), %rdi");
__asm("popq %r13");
__asm("call *%r14");
__asm("ret");
// ... 这里可能会有别的指令
}
void dummy_libc_part2()
{
// ... 这里可能会有别的指令
__asm("popq %r14");
__asm("ret");
// ... 这里可能会有别的指令
}
int main(int argc, char **argv)
{
__asm("pushq $0x400545");
__asm("pushq $0x62");
__asm("pushq $0x400521");
__asm("pushq $0x400400");
__asm("pushq $0x40052f");
__asm("ret");
printf(".");
}
猜猜会是什么结果?不要试图去编译运行,它在你的机器上不一定和我们的机器上表现一致。
我们假设dummy_libc_part1/dummy_libc_part2均是已经存在的序列,类似GLIBC中那样的,那么利用这些指令序列,不需要写任何指令,只需要在stack上堆砌数据,就可以实现程序逻辑的任意跳转。这就是ROP!利用现成的ret指令,精心布置stack上的内容,实现代码注入。
在一个程序的地址空间中,我们最容易touch的地方,也就是stack了,如此一来,ROP的一个首要问题就是如何巧妙在stack布置数据。
GLIBC是个指令序列的宝库,在里面你可以找到几乎任何可以利用的序列。但问题就在于你如何把一些零散的序列拼接成基于push/call和return/pop的逻辑,这并不是一件容易的事,就好像李白的诗中的每一个字都在字典中,但给你一本字典你却写不出李白级别的诗。
还是看下本文最初代码中stack的堆砌逻辑吧:
在ROP实践中,肯定不会像上述代码一样采用push的方式进行stack构建,而是采用缓冲区溢出的方式。拥有缓冲区溢出漏洞的最臭名昭著的比方说字符串函数。
不过在x86_64体系结构中,字符串函数终于被洗白了,因为进程地址空间的高16bit强制为0,这意味着使用见0截断的字符串函数来进行溢出时,最多只能覆盖一个地址。类似下面的stack frame,用字符串函数是万万不能构建的:
0x00007fffffffe498
0x00007fffffffe3b0
0x00007fffffffdcb8
0x00007ffff7aa39aa
因此,ROP的编写是极具技巧性的手艺,你的任务就是在GLIBC或者被注入进程所link的LIB中寻找可以利用的指令序列。原理只是让你理解字词句段篇章,而真正的断章取义,则是一种艺术。
如今的系统均自带了ASLR(Address space layout randomization)保护,因此,即便你成功hack了一个程序,hack代码也是不可重用,你不得不每次做一遍相同的艺术性的工作,当这种工作不断重复后,它也就不再是一种艺术了,实施者也随即退化成了产线工人。
而不是经理。
浙江温州皮鞋湿,下雨进水不会胖。
来源:oschina
链接:https://my.oschina.net/u/4274358/blog/4604201