这篇和上篇的不同点在于,这里手动构造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 |
最后,执行的结果如下: