翻译:Kr0net
稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
译者的话
这篇翻译自SkullSecurity——ropasaurusrex: a primer on return-oriented programming。此文章对ROP进行了非常详尽的介绍。这也是译者个人非常喜欢和强力推荐的文章。文章的篇幅较长,毕竟算是一个完整的教程,翻译时我将文章分为了三个部分。
ropasaurusrex:ROP入门教程——STACK
ropasaurusrex:ROP入门教程——DEP
ropasaurusrex:ROP入门教程——ASLR
前言
CTF比赛之后对一些问题的回顾往往让人感觉很糟糕。可能你花了几个小时在问题上在一个题目,但不会像比我在cont上花费这么多时间,不是因为分数,而是意识到它真的很简单。但也是一个脑洞题,ROP也就是这样子。
无论如何,尽管我花了很多时间在一个错误的思路上(具体的说,我没想过绕过ASLR会花费很多时间)。这篇文章的流程:我们解题先解决没有ASLR的难度,之后再加上ASLR,这个是理解ROP的好办法。
在我开始这篇文章之前,我要感谢HIkingPeter对我的帮助。在他的帮助下我们很快地解决了这个疑惑。
巧合的是,我想写一篇关于ROP的文章也有一些时间了,基于此我甚至写了一个相关的demo。但是PlaidCTF给我们这个挑战,我认为它会是一个更好的谈论素材。这篇文章不仅仅是一个WP,同时也是一个初阶的ROP编写教程!
是什么是ROP?
ROP——返回导向——编程,是“return to libc”的EXP编程的别名。当你在一个程序中发现一个溢出或者其他形式的漏洞,但是你没有明确的办法让你的代码进入到可执行的内存空间中(DEP,或者说数据执行保护,意思是说你不能在任意地方运行代码了),ROP可以帮助你控制这个程序。
ROP里你可以选择已经在可执行内存区块的,带着return的代码块,有时候这些代码块有的很简单,但有时候很复杂,庆幸的是在这次的练习中,我们只需要了解简单的。
但是在正式开始学习ROP之前,首先我们需要更多有关栈的知识。我将会花一些时间在介绍栈上面。
栈
我相信你之前听说过栈,那么栈溢出呢?粉碎堆栈呢?它们的确切意思是什么呢?如果你已经知道,你可以可以把下面的内容当作一个快速入门,或者直接跳过进入下一节。
这是一个简单的例子,一个函数functionA()调用了functionB(1,2),接着函数functionB()调用了functionC(3,4),现在栈的情况如下:
如果你不深呼吸静下心来,是不是没办法一下子讲清楚这个栈结构?好的,让我来解释一下,每当你调用一个函数,一个新的栈帧就会被建立。一个帧的意思是一个函数为它自己在栈上分配一些内存空间。实际上,它甚至没有分配,它只是在栈中增加一些东西,更新ESP寄存器来让函数知道哪里是它应该是栈帧开始运行的(ESP,栈指针,也是一个变量)
一个栈帧保存了当前函数的上下文。这可以让你很轻松地为新调用的函数建立新的栈帧,或者返会前一帧(返回上一个函数)(通过esp的增加或者减少,esp始终在栈的顶部,也就是在当前栈的最低地址)。
你有没有想过当你调用其它函数的时候,当前函数的局部变量到哪里去了(或者说你再次递归调用一个函数的时候)?如果你没有思考过,或者不清楚,你现在应该明白:这些参数保存在旧的栈帧中。
现在,让我们看看栈中储存了什么,栈中的数据是按照顺序存放的(如果对本篇文章的栈有疑问的话,你可以重新画一个栈,在这篇文章里,栈从高地址向低地址延伸,调用者(旧函数)在顶部,被调用者(新函数)在底部):
a.参数:这些参数被调用者传递进函数里,这些是对ROP来说是十分重要的;
b.返回地址:每一个函数都应该知道当他结束的时候应该到哪儿去,当你调用一个函数的时候,下一条指令的地址在实现调用函数之前就先被压入保存在栈中。当你返回的时候,这些地址弹出栈并且跳转到对应的地址上。这对理解ROP来说也是十分重要的。
c.保存的栈指针:让我们完全无视它。严格来说这项工作是编译器做的事情,除非它不,不然我们不会再提起它
d.局部变量:函数可以根据它的需要来分配相应的内存来存储局部变量。在这里对ROP来说他们并不重要,我们可以忽视它们。
我们来做一个总结:当一个函数被调用,相应的参数带着返回地址会被压入栈中,当一个函数返回时,它再栈中抓取返回地址并跳转到相应的位置上。除非在不被清除的情况下,被压入栈中的参数将会被调用函数清除。我将假设被调用函数不会清理自己的参数(译者:这也是ROP编写中要处理的,作者在下文会具体说明),而是被调用者清理,现在理解它是怎样工作的是一个挑战(这种情况大多数发生在linux)。
天堂地狱和栈帧
要理解ROP你要知道的东西是:一个函数的“整个世界”是它的栈帧。栈是它的神,参数是它的命令,局部变量是它的罪恶(对于局部变量,译者不了解基督文化,不能理解作者这样比喻),保存的帧指针(EBP等)是它的圣经,返回地址是它的天堂(好吧,也有可能是地狱)。
假设你调用了sleep()函数,并且到了这一行,它的栈帧如下所示:
当sleep()开始时,栈帧如下。它保存栈帧指针并且通过减少esp(让esp指向更低的地址)为局部变量分配栈空间。它也可以调用其它的函数,通过控制esp来创建新的栈帧,它可以做很多不同的事情。不管怎么样,当sleep()开始时,栈帧构成了它的整个世界(译者:这个栈帧的存在时间,就是函数中参数的生命周期)。
当sleep()返回时,它的栈空间最终变成这个样子:
还有,在sleep()返回时,调用者将会通过将esp+4来清除[second](稍后,我们将会讨论如何用pop/pop/ret结构来完成同样的事情)。
在一个正常运转的系统中,这是这个函数的一个工作流程,当然这是在工作环境安全的情况下做的考虑。这个[second]值压栈之后就只会在栈中,然后返回地址将会指向调用它的地方。那么,它可以返回到别的地方吗?
控制栈
……好的,如果你这样问我(如何控制栈的话),就让我来告诉你。我们都听说过栈溢出,就是在栈中覆盖一个变量。它在运用中是怎么回事呢?,让我们来看下面的栈帧:
buf变量的长度为16字节。当向buf输入一个17字节的数会怎么样呢?向它输入的最后一个字节会覆盖[return adress],再输入一个字节的话会覆盖[second],以此类推。因此我们可以通过修改返回地址使其指向我们想的任何地方。好的,当一个函数返回时我们应该让它返回哪里呢?我觉得那里应该是一个完美的世界。在这个例子里,它确实会返回到攻击者想要的地方。如果攻击者说跳到0,那么它会跳到0然后崩溃,如果攻击者说跳到-0x414141(“AAAA”),它会跳到那儿然后崩溃。如果攻击者说。。。,好吧,接下来就让我们把这个过程变得复杂一些。
(译者的话:到这里文章的很详细地介绍了栈,其内部函数调用栈中数据所发生的变化,以及基本溢出的原理,接下来作者开始介绍DEP,也就是译者译文的第二部分)