之前写过两篇TRUN bypas DEP
的文章,最终都选择了Win7
作为实验的操作系统。这里,选择Win10 21H2 x32
作为实验的操作系统。
最简单的办法,直接mona
自动生成ROP Gadgets
。但是,有一个问题,mona
会去选择开启ASLR
的DLL
中的ROP Gadget
,也就意味着系统重启之后,ROP Gadgets
就会变得不可用。
windbg
中mona
插件生成的ROP Gadgets
,和之前在immunity debugger
中使用mona
产生的ROP Gadgets
略有不同,并且产生速度也比较慢,因为我的windbg
安装的位置,如果启动windbg
不用管理员,后续mona
生成的几个txt
文件都没法保存。单纯在essfunc.dll
中是找不到符合要求的ROP Gadgets
,需要使用如下命令:
1 | !mona rop -m *.dll -n |
retn
指令的地址,查找方式:
1 | !mona find -type instr -s "retn" -p 10 -o |
看一下mona
自动生成的POP Gadgets
:
1 | def create_rop_chain(): |
最终的利用脚本如下:
1 | #!/usr/bin/env python3 |
反弹shell
成功:
自动化产生ROP
的方式,还是太过简单,来看看手动构造。基础知识就不提了,前面几篇DEP
相关的文章都写了。
首先,在IDA
中查找导入表,看能否找到VirtualProtect
或者VirtualAlloc
的地址。这里我选择从essfunc.dll
中查看,因为从vulnserver.exe
中查看,发现找到的地址包含00
,不便于后续处理,还有注意最后的DLL
是需要程序运行时加载的:
可以看到VirutalProtect
的导入表地址为6250609C
。
注意VirtualProtect
的函数结构:
1 | BOOL VirtualProtect( |
在编写利用脚本时,VirtualProtect
函数需要写成如下所示:
1 | func = struct.pack("<L",0x45454545) # dummy VirutalAlloc Address |
可以看见,目前这里都是随机填写的,后续需要通过ROP Gadget
去修改为正确的值。有一点需要注意,最后的lpflOldProtect
参数,可以选择一个可读可写的内存地址,后续ROP Gadget
也不需要进行修改。这里我选择essfunc.dll
中的未使用空间,并且地址不包含00
。
查看essfunc.dll
的地址:
查看对应地址的属性:(!vprot
或者!address
都可)
查看选择的地址0x62502610
:
接下来开始组织ROP Gadgets
,首先需要保存ESP
的值,最好是保存到两个寄存器中:
1 | # PUSH ESP # POP ESI # RETN ** [KERNEL32.DLL] ** | asciiprint,ascii {PAGE_EXECUTE_READ} |
寻找合适的ROP Gadget
是一个非常繁琐的过程,上面所示就是本例中适合保存ESP
的POP Gadget
,并且方便后续的操作。
接下来看,如何把IAT
中VirtualProtect
的地址存放到之前45454545
占位符所在的位置,下面是对应的ROP Gadgets
:
1 | eip = struct.pack("<L",0x62501022) # retn essfunc.dll |
注意上面这段POP Gadgets
的关键点是需要找到45454545
占位符的位置,这需要在windbg
中不断调试。
接下来是shellcode
的地址覆盖到占位符46464646
和47474747
的位置。这里有点意思:因为目前你不知道shellcode
的具体位置,还有一个点就是ROP Gadget
是占空间的,随着ROP Gadget
越来越多,前期找到的shellcode
地址可能就偏移了,就会导致执行的地址在shellcode
中间,导致shellcode
无法执行。有一个办法就是在shellcode
之前加一段较大范围的\x90
,这样来回小范围移动,并不会影响最终shellcode
的执行。看一下这段ROP Gadgets
:
1 | rop1 += struct.pack("<L",0x74ea4480) # INC EAX # RETN ** [mswsock.dll] ** | {PAGE_EXECUTE_READ} |
接下来覆盖46464646
占位符所在地址的内容。按照我们之前编写的VirtualProtect
函数模版,每个参数之间的地址差都是4bytes
,所以这里只需要对上一步中EAX
加4
,就能执行46464646
占位符的地址,然后修改其中的值即可。ROP Gadgets
如下:
1 | rop1 += struct.pack("<L",0x74ea4480) # INC EAX # RETN ** [mswsock.dll] ** | {PAGE_EXECUTE_READ} |
接下来,有一些小技巧:
为了保存0x01
,可以使用如下ROP Gadget
:
1 | # POP EAX # RETN ** [mswsock.dll] ** | {PAGE_EXECUTE_READ} |
其中,-1
按照之前的技巧,等于ffffffff
,然后用NEG
指令即可得到1
。
这段的ROP Gadgets
如下所示:
1 | rop1 += struct.pack("<L",0x74ea4480) # INC EAX # RETN ** [mswsock.dll] ** | {PAGE_EXECUTE_READ} |
接下来继续看如何保存0x40
,继续使用之前介绍的小技巧:
直接看这段的ROP Gadgets
:
1 | rop1 += struct.pack("<L",0x74ea4480) # INC EAX # RETN ** [mswsock.dll] ** | {PAGE_EXECUTE_READ} |
因为EAX
和EDX
都被使用到,注意EDX
在使用之前,需要将其中保存的ESP
原始值保存到其他寄存器中,这里找到符合要求的EBX
,注意# MOV EBX,EDX # RETN ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}
这条ROP Gadget
。
接下来,就是执行VirutalProtect
函数,这里需要ESP
指向VirualProtect
在栈空间里面的位置,最终的ROP Gadgets
如下:
1 | rop1 += struct.pack("<L",0x76535eb8) # MOV EAX,EBX # POP EBX # RETN ** [RPCRT4.dll] ** | {PAGE_EXECUTE_READ} |
以上就是关键步骤的ROP Gadgets
。最终的利用代码如下:
1 | #!/usr/bin/env python3 |
成功反弹shell
:
上面使用的是VirtualProtect
绕过DEP
,如果是VirtualAlloc
的话,最终略有不同。因为essfunc.dll
的导入表里面没有VirutalAlloc
,这里仅做一些解释。看一下VirtualAlloc
函数的原型:
1 | LPVOID VirtualAlloc( |
利用脚本里面,VirtualAlloc
的模版如下:
1 | LPVOID VirtualAlloc( |
1 | func = pack("<L", (0x45454545)) # dummy VirutalAlloc Address |
在windbg
中看一下vulnserver
加载的DLL
有哪些:
在RPCRT4.DLL
的导入表中,可以找到VirtualAlloc
函数:
可以看到VirtualAlloc
函数在导入表的地址为4F0340D0
。需要注意的地方:api-ms-win-core-memory-l1-1-0.DLL
库在程序运行的时候并没有加载,如果使用这个值,会出现类似如下这样的问题:
我的理解:查找VirtualAlloc
或者VirutalProtect
函数在IAT
中的地址时,需要注意关联的DLL
,如果该DLL
在程序运行时加载了,那可以使用通过IAT
中函数地址找到该函数,如果DLL
在程序运行时没有加载,那么通过IAT
中函数地址无法找到该函数运行时地址。
参考:
1.https://www.nirsoft.net/articles/windows_7_kernel_architecture_changes.html