一步一步学ROP之linux_x86篇

最近在学蒸米的《一步一步学ROP之linux_x86篇》,内容写的很详细,个人学到了很多,但同时学的过程中也有很多疑惑,然后文章有些地方也有些小错误,所以在这里自己再补充并修正一些方便自己日后回顾学习。如果有冒犯到博主在这里说声抱歉。(如侵权删)

原文章地址:https://yq.aliyun.com/articles/58699


一、序

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

小编备注:文中涉及代码可在文章最后的github链接找到。


二、Control Flow Hijack 程序流劫持

比较常见的程序流劫持就是栈溢出,格式化字符串攻击和堆溢出了。通过程序流劫持,攻击者可以控制PC指针从而执行目标代码。为了应对这种攻击,系统防御者也提出了各种防御方法,最常见的方法有DEP,NX(堆栈不可执行),ASLR(内存地址随机化),Stack Protector(栈保护)等。可以在linux下使用命令

checksec + 目标文件

查看文件所使用的防御

没有开任何防御

但是如果上来就部署全部的防御,初学者可能会觉得无从下手,所以我们先从最简单的没有任何保护的程序开始,随后再一步步增加各种防御措施,接着再学习绕过的方法,循序渐进。

首先来看这个有明显缓冲区溢出的程序:

这里我们用

gcc -fno-stack-protector -z execstack -o level1 level1.c

这个命令编译程序。-fno-stack-protector和-z execstack这两个参数会分别关掉Stack Protector和NX(linux下开启了NX的以及windows下开启了DEP的程序堆栈是不可执行的,linux下的程序默认编译时是开启了NX的)。同时我们在shell中执行:

这几个指令。执行完后我们就关掉整个linux系统的ASLR保护。

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

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

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

我们可以得到内存出错的地址为0x37654136。随后我们使用命令:

就可以非常容易的计算出PC返回值的覆盖点为140个字节。我们只要构造一个”A”*140+ret字符串,就可以让pc执行ret地址上的代码了。

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

这里我们使用一段最简单的执行execve ("/bin/sh")命令的语句作为shellcode。

溢出点有了,shellcode有了,下一步就是控制PC跳转到shellcode的地址上:

[shellcode][“AAAAAAAAAAAAAA”….][ret]
^------------------------------------------------|

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

最简单的方法就是开启core dump这个功能。

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

因为溢出点是140个字节,再加上4个字节的ret地址,我们可以计算出buffer的地址为$esp-144。通过gdb的命令 “x/10s $esp-144”,我们可以得到buf的地址为0xbffff290。

OK,现在溢出点,shellcode和返回值地址都有了,可以开始写exp了。写exp的话,我强烈推荐pwntools这个工具,因为它可以非常方便的做到本地调试和远程攻击的转换。本地测试成功后只需要简单的修改一条语句就可以马上进行远程攻击。

最终本地测试代码如下:

执行exp:

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

随后这个程序的IO就被重定向到10001这个端口上了(ctrl+c关闭),并且可以使用 nc 127.0.0.1 10001来访问我们的目标程序服务了。

因为现在目标程序是跑在socat的环境中,exp脚本除了要把p = process('./level1')换成p = remote('127.0.0.1',10001) 之外,ret的地址还会发生改变。解决方法还是采用生成core dump的方案,然后用gdb调试core文件获取返回地址。然后我们就可以使用exp进行远程溢出啦!


三、Ret2libc – Bypass NX 通过ret2libc绕过NX防护

现在我们把NX打开,依然关闭stack protector和ASLR。编译方法如下:

gcc -fno-stack-protector  -o level1 level1.c

这时候我们如果使用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”这个字符串,并且这个字符串的地址也是固定的。那么接下来我们就来找一下这个函数的地址。这时候我们可以使用gdb进行调试。然后通过print和find命令来查找system和”/bin/sh”字符串(查找/bin/sh的时候直接用find "/bin/sh"命令)的地址。

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

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

OK。测试成功。


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

接下来我们打开ASLR保护。

sudo -s
echo 2 > /proc/sys/kernel/randomize_va_space
exit

现在我们再回头测试一下level2的exp,发现已经不好用了。

如果你通过sudo cat /proc/[pid]/maps或者ldd查看,你会发现level2的libc.so地址每次都是变化的。

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

Linux内存随机化分布图

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

我们发现除了程序本身的实现的函数之外,我们还可以使用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中的offset(相对地址)是不变的,所以如果我们得到了write()的地址并且拥有目标服务器上的libc.so就可以计算出system()在内存中的地址了。然后我们再将pc指针return回vulnerable_function()函数,就可以进行ret2libc溢出攻击,并且这一次我们知道了system()在内存中的地址,就可以调用system()函数来获取我们的shell了。

使用ldd命令可以查看目标程序调用的so库。随后我们把libc.so拷贝到当前目录,因为我们的exp需要这个so文件来计算相对地址:

最后exp如下:

这里对payload1解释一下

payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) + p32(got_write) + p32(4)

#p32(1) + p32(got_write) + p32(4)其实在调用write函数

#write用法:ssize_t write(int fd, const void *buf, size_t nbyte);
#fd:[文件描述符] fd=0时为输入,fd=1时为输出,fd=2为报错
#buf:指定的缓冲区,即[指针],指向一段内存单元;
#nbyte:要写入文件指定的字节数;返回值:写入文档的字节数(成功);-1(出错)
#p32(1) + p32(got_write) + p32(4)相当于传进去fd=1(输出),buf= p32(got_write) ,nbyte=4 最终就是为了打印出write()的地址

接着我们使用socat把level2绑定到10003端口:

最后执行我们的exp:


五、小结

本章简单介绍了ROP攻击的基本原理,由于篇幅原因,我们会在随后的文章中会介绍更多的攻击技巧:如何利用工具寻找gadgets,如何在不知道对方libc.so版本的情况下计算offset;如何绕过Stack Protector等。欢迎大家到时继续学习。另外本文提到的所有源代码和工具都可以从作者的github下载:https://github.com/zhengmin1989/ROP_STEP_BY_STEP


六、参考文献

The geometry of innocent flesh on the bone: return-into-libc without function calls (on the x86)
picoCTF 2013: https://github.com/picoCTF/2013-Problems
Smashing The Stack For Fun And Profit: http://phrack.org/issues/49/14.html
程序员的自我修养
ROP轻松谈

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容