ROP(Return Oriented Programming)原理解析

左心房为你撑大大i 提交于 2020-09-28 09:37:25

先看一个代码:

#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代码也是不可重用,你不得不每次做一遍相同的艺术性的工作,当这种工作不断重复后,它也就不再是一种艺术了,实施者也随即退化成了产线工人。

而不是经理。


浙江温州皮鞋湿,下雨进水不会胖。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!