x86linux write函数,一步一步学ROP之linux_x86篇

一、序

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术,可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。虽然现在大家都在用64位的操作系统,但是想要扎实学好ROP还是得从基础的x86系统开始。在随后的教程中我们还会带来linux_x64以及android(arm)方面的ROP利用方法,欢迎大家继续学习。

源代码和工具

二、Control Flow Hijack程序流劫持

比较常见的程序流劫持就是堆栈溢出和格式化字符串攻击了。通过程序流劫持,攻击者可以控制PC指针从而执行目标代码。为了应对这种攻击,系统防御者也提出了各种防御方法,最常见的方法有DEP(堆栈不可执行),ASLR(内存地址随机化),Stack Protector(栈保护)等。如果上来就部署全部的防御,初学者可能会觉得无从下手,所以我们先从最简单的没有任何保护的程序开始,随后再一步步增加各种防御措施,接着再学习绕过的方法,循序渐进。首先来看这个有明显缓冲区溢出的程序。

0818b9ca8b590ca3270a3433284dd417.png

用gcc -fno-stack-protector -z execstack -o level1 level1.c这个命令编译程序。-fno-stack-protector和-z execstack这两个参数会分别关掉Stack Protector和DEP,同时在shell中执行下面这几个指令关掉整个linux系统的ASLR保护。

0818b9ca8b590ca3270a3433284dd417.png

接下来开始对目标程序进行分析。先来确定溢出点的位置,这里我推荐使用pattern.py这个脚本来进行计算。使用如下命令。

0818b9ca8b590ca3270a3433284dd417.png

生成一串测试用的150个字节的字符串。

0818b9ca8b590ca3270a3433284dd417.png

随后使用gdb ./level1调试程序。

0818b9ca8b590ca3270a3433284dd417.png

内存出错的地址为0x37654136,随后使用下面的命令就可以计算出PC返回值的覆盖点为140个字节。只要构造一个A*140+ret字符串就可以让PC执行ret地址上的代码了。

0818b9ca8b590ca3270a3433284dd417.png

接下来我们需要一段shellcode,可以用msf生成,或者自己反编译一下。

0818b9ca8b590ca3270a3433284dd417.png

这里使用一段最简单的执行execve("/bin/sh")命令的语句作为shellcode。溢出点和shellcode有了,下一步就是控制PC跳转到shellcode的地址上。

0818b9ca8b590ca3270a3433284dd417.png

对初学者来说这个shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置,虽然关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,但当直接执行./level1的时候,buf的位置会固定在别的地址上。怎么解决这个问题呢?最简单的方法就是开启core dump这个功能。

0818b9ca8b590ca3270a3433284dd417.png

开启之后,当出现内存错误时,系统会生成一个core dump文件在tmp目录下。然后再用gdb查看这个core文件就可以获取到buf真正的地址了。

0818b9ca8b590ca3270a3433284dd417.png

因为溢出点是140个字节,再加上4个字节的ret地址,可以计算出buffer的地址为$esp-144。通过gdb的命令x/10s $esp-144,可以得到buf的地址为0xbffff290。现在溢出点、shellcode和返回值地址都有了,可以开始写exp了。写exp的话,我强烈推荐pwntools这个工具,因为它可以非常方便的做到本地调试和远程攻击的转换。本地测试成功后只需要简单的修改一条语句就可以马上进行远程攻击。

0818b9ca8b590ca3270a3433284dd417.png

最终本地测试代码如下。

0818b9ca8b590ca3270a3433284dd417.png

执行exp。

0818b9ca8b590ca3270a3433284dd417.png

接下来把这个目标程序作为一个服务绑定到服务器的某个端口上,这里可以使用socat这个工具来完成,命令如下。

0818b9ca8b590ca3270a3433284dd417.png

随后这个程序的IO就被重定向到10001这个端口上了,并且可以使用nc 127.0.0.1 10001来访问目标程序服务了。因为现在目标程序是跑在socat的环境中,exp脚本除了要把p = process('./level1')换成p = remote('127.0.0.1',10001)之外,ret的地址还会发生改变。解决方法还是采用生成core dump的方案,然后用gdb调试core文件获取返回地址。然后就可以使用exp进行远程溢出啦!

0818b9ca8b590ca3270a3433284dd417.png

三、Ret2libc Bypass DEP通过Ret2libc绕过DEP防护

现在把DEP打开,依然关闭stack protector和ASLR。如果使用level1的exp来进行测试的话,系统会拒绝执行我们的shellcode。如果你通过sudo cat /proc/[pid]/maps查看,你会发现level1的stack是rwx的,但是level2的stack却是rw的。

level1: bffdf000-c0000000 rw-p 00000000 00:00 0 [stack]

level2: bffdf000-c0000000 rwxp 00000000 00:00 0 [stack]

那么如何执行shellcode呢?我们知道level2调用了libc.so,并且libc.so里保存了大量可利用的函数,如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。既然思路有了,那么接下来的问题就是如何得到system()这个函数的地址以及/bin/sh这个字符串的地址。如果关掉了ASLR的话,system()函数在内存中的地址是不会变化的,并且libc.so中也包含/bin/sh这个字符串,地址也是固定的。

0818b9ca8b590ca3270a3433284dd417.png

首先在main函数上下一个断点,然后执行程序,这样的话程序会加载libc.so到内存中,然后可以通过print system命令来获取system函数在内存中的位置,随后可以通过print __libc_start_main命令来获取libc.so在内存中的起始位置,接下来通过find命令来查找/bin/sh这个字符串。这样就得到了system的地址0xb7e5f460以及/bin/sh的地址0xb7f81ff8。下面开始写exp。

0818b9ca8b590ca3270a3433284dd417.png

要注意的是system()后面跟的是执行完system函数后的返回地址,接下来才是/bin/sh字符串的地址。因为执行完后也不打算干别的什么事,所以就随便写了一个0xdeadbeef作为返回地址。下面我们测试一下exp。

0818b9ca8b590ca3270a3433284dd417.png

OK。测试成功。

四、ROP Bypass DEP and ASLR通过ROP绕过DEP和ASLR防护

接下来我们打开ASLR保护。

0818b9ca8b590ca3270a3433284dd417.png

现在再回头测试一下level2的exp,发现已经不好用了。如果你通过sudo cat /proc/[pid]/maps或者ldd查看,你会发现level2的libc.so地址每次都是变化的。

0818b9ca8b590ca3270a3433284dd417.png

那么如何解决地址随机化的问题呢?我们需要先泄漏出libc.so某些函数在内存中的地址,然后再利用泄漏出的函数地址根据偏移量计算出system()函数和/bin/sh字符串在内存中的地址,然后再执行我们的Ret2libc的shellcode。既然栈、libc和heap的地址都是随机的,怎么才能泄露出libc.so的地址呢?方法还是有的,因为程序本身在内存中的地址并不是随机的,如图所示。

0818b9ca8b590ca3270a3433284dd417.png

只要把返回值设置到程序本身就能执行期望的指令了。首先利用objdump来查看可以利用的plt函数和对应的got表。

0818b9ca8b590ca3270a3433284dd417.png

我们发现除了程序本身的实现的函数之外,还可以使用read@plt()和write@plt()函数。但因为程序本身并没有调用system()函数,所以并不能直接调用system()来获取shell。但其实有write@plt()函数就够了,因为可以通过write@plt()函数把write()函数在内存中的地址也就是write.got给打印出来。既然write()函数实现是在libc.so当中,那调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当调用write@plit()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got跳转到真正的write()函数上去。因为system()函数和write()在libc.so中的相对地址是不变的,所以如果得到了write()的地址并且拥有目标服务器上的libc.so就可以计算出system()在内存中的地址了,然后再将PC指针return回vulnerable_function()函数,就可以进行Ret2libc溢出攻击,并且这一次我们知道了system()在内存中的地址,就可以调用system()函数来获取我们的shell了。使用ldd命令可以查看目标程序调用的so库。随后把libc.so拷贝到当前目录,因为exp需要这个so文件来计算相对地址。

0818b9ca8b590ca3270a3433284dd417.png

最后exp如下。

0818b9ca8b590ca3270a3433284dd417.png

接着使用socat把level2绑定到10003端口。

0818b9ca8b590ca3270a3433284dd417.png

最后执行exp。

0818b9ca8b590ca3270a3433284dd417.png

五、小结

本文简单介绍了ROP攻击的基本原理,由于篇幅原因,我们会在随后的文章中会介绍更多的攻击技巧:如何利用工具寻找gadgets;如何在不知道对方libc.so版本的情况下计算offset;如何绕过Stack Protector等。欢迎大家到时继续学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值