这篇和上篇的不同点在于,这里手动构造ROP链。
在寻找ROP gadget的时候,一般会用到rp++,但是在GitHub上编译好的是64位的,所以这里需要自己手动编译。因为我已经安装好VS2019 x86版本,所以想着自带的cmake能够直接用,结果出现与win10 21h2 32位版本不兼容的情况。解决办法就是下载cmake-3.22.2-i386版本(这里我选择了一个当前稳定版里面的最高版,其他版本未测),安装覆盖VS2019自己安装的cmake文件即可。
这里,还有一点需要注意的。必须关闭系统的ASLR。以Win7为例,修改注册表,找到项[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management],创建一个新的值:名字为:MoveImages,类型为:DWORD,值为:00000000。
不同系统关闭ASLR的办法参照:https://rehex.ninja/posts/disable-aslr/
win10 21h2 x86下面,rp-win-x86获取的rop gadget地址不对,和mona获取的ROP链对应模块的地址差别很大。rp 2.0以上版本增加了--va参数,可以加上这个参数获取ROP Gadget相对模块基地址的偏移。然后在immunity debugger中获取模块加载时的基地址,两者相加得到ROP Gadget的地址。或者,查看mona生成ROP链的时候,会同时生成一个rop.txt文件,看里面的ROP Gadget也可,地址和immunity debugger中是一一对应的。
ROP链的理解:通过对栈指针和寄存器的操作,不断改变执行流,达到对特定函数的调用、shellcode的执行。
在win10 21h2 32构造的不顺利,也想开始有两个寄存器来保存ESP的值,后续方便操作。但是在所有ROP Gadget里面没有找到合适的。其实有个方便的方式,参考mona生成的基于VirtualProtect函数的ROP链模板,自己在ROP Gadget里面寻找合适的即可。
1 | *** [ Python ] *** |
以上是mona自动生成的ROP链,注意其中的0x62504c95, # &Writable location [essfunc.dll]。查看模块的地址可以发现,这个地址是essfunc.dll中bss段(可读可写)所在位置。

仔细看这个1000h的空间,找一块没有使用的空间存放shellcode即可。所以可写地址不一定需要是mona生成的那样,可以自己指定其他的,如0x62504210。
在win 7 sp1 32位和win10 21h2 32位都进行了手动构造ROP链的实验。win7 sp1 32位构造的还算顺利。为加深印象,一步一步来构造整个ROP 链。
构造VirtualProtect函数及参数:首先,获取VirtualProtect的调用地址,在这里调用地址为0x77E22E1D。(查找方法见最后的参考)最终如下所示:
1 | struct.pack('<L', 0x77E22E1D) + # kernel32.VirtualProtect() essfunc.dll |
上面每个参数的值目前是随机设置的,后续会用正确的值覆盖掉。接下来的主要目标就是寻找合适的POP Gadget组成链,用正确的值覆盖上述VirtualProtect函数5个参数中对应的值。
VirtualProtect函数及变量入栈之后,在其后面可以填充一些\x90。在这里填充4个。
保存ESP的值到多个寄存器中:为了方便对栈空间的操作,可以保存ESP的值到多个寄存器中。这里根据POP Gadget可以保存到EAX和ECX中。
1 | 0x77bf58fe: push esp ; pop ecx ; ret ; (1 found) |
保存ESP的值:

将ESP的值存入EAX和ECX中:

调整ESP的值,让其指向VirtualProtect参数之后的地址:为了防止VirtualProtect的参数被后续的指令覆盖,需要调整ESP的值,让其跳过VirtualProtect变量所在的区域。这里找到的POP Gadget为:
1 | 0x6ff821d5: add esp, 0x1C ; ret ; (1 found) |
此时,栈空间如下:

确定不同寄存器指向栈布局上VirtualProtect不同的参数地址:查看栈空间目前的布局,为了让ECX指向return变量所在的栈地址,需要对ECX目前的值进行增加。可以使用如下POP Gadget:
1 | 0x6ff59f14: inc ecx ; ret ; (1 found) |
保存在ECX的ESP初始值(0x019DF9E4)距离return所在栈地址(0x019DF9F0)的距离为C,所以需要重复12次上面的指令。
栈布局中,lpaddress(0x019DF9F4)紧跟在return之后,两者地址相差4,所以可以先把ECX保存到EDX,然后对EDX目前的值进行增加。
ECX的值保存到EDX的POP Gadget如下:
1 | 0x6ffb615a: mov edx, ecx ; pop ebp ; ret ; (1 found) |
注意上面后续有个pop ebp的指令,为了不影响我们栈的布局,主要指不会破坏后续的POP Gadget,我们可以在上面指令之后增加4字节的无用数据,这里用\x41\x41\x41\x41代替。
EDX自加的POP Gadget如下:
1 | 0x77f226c5: inc edx ; ret ; (1 found) |
根据前面的分析,需要执行4次上面这个指令。
接下来,让EAX指向shellcode所在地址区域(可为shellcode开头,或shellcode之前的\x90),找到的POP Gadget如下:
1 | 0x6ff7e29a: add eax, 0x00000100 ; pop ebp ; ret ; (1 found) |
这里存在一个pop ebp,同样需要添加一个4字节无用数据到栈上,\x41\x41\x41\x41。此时EAX的值为0x019DFAE4。
查看栈布局:

到目前为止:
已经让ECX指向之前布局的栈中,return变量所在的栈地址。
已经让EDX指向之前布局的栈中,lpAddress变量所在的栈地址。
已经让EAX指向最终执行的shellcode所在的地址,可以为shellcode前面填充的\x90所在地址。
覆盖return和lpAddress在栈布局中所保存的值:现在,可以用正确的值覆盖return和lpAddress在栈布局中的所保存的值,也就是之前定义VirtualProtect参数时,随便写的几个值。对应的POP Gadget如下:
覆盖return在栈布局中所在的值:
1 | 0x6ff63bdb: mov [ecx], eax ; pop ebp ; ret ; (1 found) |
这里存在一个pop ebp,同样需要添加4字节无用数据到栈上,\x41\x41\x41\x41。
此时,return所在栈地址的值:

覆盖lpAddress在栈布局中所在的值:
1 | 0x77e9431b: mov [edx], eax ; pop esi ; pop ebp ; retn 0x000C ; (1 found) |
此时,lpAddress所在栈地址的值:

这里存在pop esi和pop ebp两条指令,需要添加8字节无用数据到栈上,\x41\x41\x41\x41。但是这里有个特别注意的指令:retn 0x000C。这个指令的意思是:先把ESP所指向地址的值存入EIP,再将ESP的值加上0x000C。特别之处在于后续的POP Gadget所在位置为8字节无用数据之后,再接12字节的无用数据,才不会破坏后续POP Gadget的正常指向,如果后续POP Gadget有POP指令,所添加的无用数据放在12字节无用数据之后即可。
指定需要修改权限的内存空间大小:这个值需要参考shellcode的大小,还需要参考填充。这里有个需要注意的地方,空间大小不能太大,看有的说不能超过一个页(4096byte),我这里试了一下,在一个页之内也会出现内存拒绝访问的问题。所以这里需要试几次,一般200h可行。
到目前为止,EAX的值已经不再需要,可以先让EAX清零,然后将内存空间大小的值存入EAX。清零EAX的POP Gadget如下所示:
1 | #0x77e0b94b: xor eax, eax ; ret ; (1 found) |
现在可以将EAX的值增加到我们需要的大小,POP Gadget如下:
1 | #0x6ff7e29a: add eax, 0x00000100 ; pop ebp ; ret ; (1 found) |
这里存在一个pop ebp,同样需要添加4字节无用数据到栈上,\x41\x41\x41\x41。本地测试发现在这里EAX为600h的时候,会出现指定Access violation when executing [01ABFAE4]的错误。其实,只需要计算shellcode的大小,加上填充的NOP大小,然后给EAX一个比它们之和大的值就行。我这里给了500h,其实200h也可。
根据之前VirtualProtect的栈布局,需要修改EDX的值(019DF9F4),让其指向dwSize所在栈地址(019DF9F8)。这里只需要EDX的值增加4即可,选择的ROP Gadget如下:
1 | #0x77f226c5: inc edx ; ret ; (1 found) |
执行4次,然后利用如下ROP Gadget完成dwSize值得覆盖:
1 | #0x77f5335f: mov [edx], eax ; xor eax, eax ; ret ; (1 found) |
此时,dwSize所在栈地址的值:

可以发现这里还完成了EAX的归零操作,正好为下一步提供了一些便利。
指定内存空间修改权限的值:这个值为固定值0x40。EAX已经归零,现在需要把0x40赋值给EAX,可以使用一下ROP Gadget:
1 | #0x77f17bb2: add eax, 0x20 ; ret ; (1 found) |
需要执行两次这条指令。
紧接着,和上一步类似,EDX再次加4,让其指向栈布局的flNewProtect所在栈地址(019DF9FC),ROP Gadget如下所示:
1 | #0x77f226c5: inc edx ; ret ; (1 found) |
需要执行4次。
然后用EAX的值(0x40)覆盖它所指向的值,ROP Gadget如下所示:
1 | #0x77f5335f: mov [edx], eax ; xor eax, eax ; ret ; (1 found) |
此时,flNewProtect所在栈地址的值:

到目前为止,VirtualProtect的变量值已经全部用正确的值进行了替换,这里lpflOldProtect在最开始定义的时候,选择一个可写的地址即可,后续一系列的ROP Gadget操作不涉及到它。
ESP指向到VirtualProtect函数所在地址:让EAX寄存器存入栈布局中VirtualProtect函数所在地址(0x019DF9EC),然后,让ESP指向该地址。
这里观察到ECX的值(0x019DF9F0)距离VirtualProtect所在栈布局中的地址仅差4,这里可以让ECX减掉4。我们这里选择先把ECX的值赋值给EAX,ROP Gadget如下所示:
1 | #0x77f42705: mov eax, ecx ; ret ; (1 found) |
然后,将EAX的值减掉4,ROP Gadget如下所示:
1 | #0x41ac80db: dec eax ; dec eax ; ret ; (1 found) |
需要执行两遍。
最终,把EAX的值赋值给ESP,ROP Gadget如下所示:
1 | #0x77d3104a: xchg eax, esp ; ret ; (1 found) |
此时,ESP的值为:

到目前为止,所有的ROP Gadget已经构造和链接完毕。
最后,自己构造的最终ROP链利用脚本如下所示:
1 | #!/usr/bin/env python3 |
最后,执行的结果如下:
