CVE-2022-38393
A denial of service vulnerability exists in the cfg_server cm_processConnDiagPktList opcode of Asus RT-AX82U 3.0.0.4.386_49674-ge182230 router’s configuration service. A specially-crafted network packet can lead to denial of service. An attacker can send a malicious packet to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
Asus RT-AX82U 3.0.0.4.386_49674-ge182230
RT-AX82U - https://www.asus.com/us/Networking-IoT-Servers/WiFi-Routers/ASUS-Gaming-Routers/RT-AX82U/
7.5 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE-125 - Out-of-bounds Read
The Asus RT-AX82U router is one of the newer Wi-Fi 6 (802.11ax)-enabled routers that also supports mesh networking with other Asus routers. Like basically every other router, it is configurable via a HTTP server running on the local network. However, it can also be configured to support remote administration and monitoring in a more IOT style.
The cfg_server
and cfg_client
binaries living on the Asus RT-AX82U are both used for easy configuration of a mesh network setup, which can be done with multiple Asus routers via their GUI. Interestingly though, the cfg_server
binary is bound to TCP and UDP port 7788 by default, exposing some basic functionality. The TCP port and UDP ports have different opcodes, but for our sake, we’re only dealing with a particular set of ConnDiag opcodes which look like such:
struct tlv_holder connDiagPacketHandlers =
{
uint32_t type = 0x5
tlv_func *tfunc = cm_processREQ_CHKSTA
}
struct tlv_holder connDiagPacketHandlers[1] =
{
uint32_t type = 0x6
tlv_func *tfunc = cm_processRSP_CHKSTA
}
The above TLVs are accessible from the cm_recvUDPHandler
thread in a particular codeflow:
0001ed90 cm_recvUdpHandler()
// [...]
0001edf8 int32_t bytes_read = recvfrom(sfd: cm_ctrlBlock.udp_sock, buf: &readbuf, len: 0x7ff, flags: 0, srcaddr: &sockadd, addrlen: &sockaddsize) // [1]
// [...]
0001ee00 if (bytes_read == 0xffffffff)
// [...]
0001ee98 else if (sockadd.sa_data[2].d != cm_ctrlBlock.self_address)
// [...]
0001f0e0 char* malloc_824 = malloc(bytes: 0x824) // [2]
0001f0e4 struct udp_resp* inp = malloc_824
0001f0e8 if (malloc_824 != 0)
0001f184 memset(malloc_824, 0, 0x824) // [3]
0001f194 memcpy(inp, &readbuf, bytes_read)
0001f198 int32_t ipaddr = sockadd.sa_data[2].d
0001f19c inp->bytes_read = bytes_read
0001f1a4 int32_t ip = ipaddr u>> 0x18 | (ipaddr u>> 0x10 & 0xff) << 8 | (ipaddr u>> 8 & 0xff) << 0x10 | (ipaddr & 0xff) << 0x18
0001f1d4 snprintf(s: &inp->ip_addr_str, maxlen: 0x20, format: "%d.%d.%d.%d", ip u>> 0x18, ip u>> 0x10 & 0xff, ip u>> 8 & 0xff, ror.d(ip, 0) & 0xff, var_864, var_860, var_85c, var_858, var_854)
0001f1dc int32_t var_838_1 = readbuf[4].d
0001f1dc int32_t var_834_1 = readbuf[8].d
0001f1e8 if (readbuf[0].d == 0x6000000) // [4]
0001f1f0 r0_6 = cm_addConnDiagPktToList(inp: inp)
At [1], the server reads in 0x7ff bytes from its UDP 7788 port, and at [2] and [3], the data is then copied from the stack over to a cleared-out heap allocation of size 0x824. Assuming the first four bytes of the input packet are “\x00\x00\x00\x06”, then the packet gets added to a particular linked list structure, the connDiagUdpList
.
Before we continue on though, it’s appropriate to list out the structure of the input packet:
struct tlv_pkt {
uint32_t type;
uint32_t datalen;
uint32_t crc;
uint8_t data[];
}
Continuing on, another thread is constantly polling the connDiagUdpList
, and if a packet is seen, then we jump over to cm_processConnDiagPktList()
:
00053ca8 int32_t cm_processConnDiagPktList()
00053cc8 pthread_mutex_lock(mutex: &connDiagLock)
00053cd8 struct list* connDiagUdp = connDiagUdpList
00053ce8 if (connDiagUdp->entry_count s> 0)
00053d2c for (struct listitem* item = connDiagUdp->tail; item != 0; item = item->next)
00053d30 struct udp_resp* input_pkt = item->inp
00053d38 if (input_pkt != 0)
00053d44 uint32_t null = terminateConnDiagPktList
00053d4c if (null != 0)
00053d4c break
00053d50 uint32_t hex_6000000 = input_pkt->req_type_le
00053d58 uint32_t dlen = input_pkt->datalen_le
00053d68 int32_t dlenle = input_pkt->bytes_read - 0xc // [5]
00053d6c uint32_t crcle = input_pkt->crcle
// [...]
00053d80 if (dlenle == (dlen u>> 0x18 | (dlen u>> 0x10 & 0xff) << 8 | (dlen u>> 8 & 0xff) << 0x10 | (dlen & 0xff) << 0x18)) //[6]
00053e0c char* buf = &input_pkt->readbuf
00053e18 crc = do_crc32(IV: null, buf: buf, bufsize: dlenle) // [7]
At [5], the actual length of the input packet minus twelve is compared against the length field inside the packet itself [6]. Assuming they match, the CRC is then checked, another field provided in the packet itself. A flaw is present in this function, however, in that there is a check missing in this code path that can be seen in both the TCP and UDP handlers: the code needs to verify that the size of the received packet is >= 0xC bytes. Thus, if a packet is received that is less than 0xC bytes, the dlenle
field at [5] underflows to somewhere between 0xFFFFFFFC
and 0xFFFFFFFF
. The check against the length field [6] can be easily bypassed by just correctly putting the underflowed length inside the packet. The CRC check at [7] isn’t an issue, since if the bufsize
parameter is less than zero, it automatically skips CRC calculation. Since a CRC skip results in a return value of 0x0, we need to make sure that the crc
field is “\x00\x00\x00\x00”. Conveniently, this is handled already for us if our packet is only 8 bytes long, since the buffer that the packet lives in was memset
to 0x0 beforehand.
While we can pass all the above checks with an 8-byte packet, it does prevent us from having any control over what occurs after. We end up hitting cm_processConnDiagPkt(uint32_t tlv_type, uint32_t datalen, uint32_t crc, char *databuf, char *ipaddr)
which just passes us off to the appropriate TLV handler. Since our opcode has to be “\x00\x00\x00\x06”, we always hit cm_processRSP_CHKSTA(char *pktbuf, uint32_t pktlen, uint32_t ipaddr)
:
00052f20 int32_t cm_processRSP_CHKSTA(char* pktbuf, uint32_t pktlen, int32_t ipaddr)
00052f50 char jsonbuf[0x800]
00052f50 memset(&jsonbuf, 0, 0x800)
// [...]
00052f64 if (cm_ctrlBlock.group_key_ready != 0)
00053004 char* groupkey = cm_selectGroupKey(which_key: 1)
0005300c if (groupkey == 0)
// [...]
00053098 goto label_530a0
000530c0 char* r0_11 = do_decrypt(sesskey1: groupkey, sesskey2: cm_selectGroupKey(which_key: 0), pktbuf: pktbuf, pktlen: pktlen) //[8]
Assuming there is a group key (which there should always be, even if the AImesh setting is not configured), then we end up hitting the do_decrypt
function at [8], which decrypts the data of our input packet with one of the groupkeys. The do_decrypt
function ends up hitting aes_decrypt
as shown below:
0001db18 void* aes_decrypt(char* sesskey1, char* pktbuf, char* pktlen, int32_t* outlen)
0001db30 int32_t ctx = EVP_CIPHER_CTX_new()
0001db38 int32_t outl = 0
0001db3c void* ctx = ctx
0001db40 void* ret
0001db40 if (ctx == 0)
// [...]
0001db6c else
0001db6c char* bytesleft = nullptr
0001db7c int32_t r0_2 = EVP_DecryptInit_ex(ctx, EVP_aes_256_ecb(), 0, sesskey1, 0)
// [...]
0001db84 if (r0_2 != 0)
0001dba0 *outlen = 0
0001dbac void* alloc_size = EVP_CIPHER_CTX_block_size(ctx) + pktlen
0001dbb4 maloced = malloc(bytes: alloc_size) // 0xc...
0001dbbc if (maloced == 0)
//[...]
0001dbe4 else
0001dbe4 memset(maloced, 0, alloc_size)
0001dbec void* mbuf = maloced
0001dbf0 char* pktiter = pktlen
0001dc00 void* inpbuf
0001dc00 void* r3_2
0001dc00 while (true)
0001dc00 inpbuf = &pktbuf[pktlen - pktiter]
0001dc04 if (pktiter u<= 0x10)
0001dc04 break
0001dc10 bytesleft = 0x10
0001dc1c int32_t r0_8 = EVP_DecryptUpdate(ctx, mbuf, &outl, inpbuf, 0x10) //[9]
0001dc20 r3_2 = r0_8
0001dc24 if (r0_8 == 0)
0001dc24 break
0001dc60 int32_t outl_len = outl
0001dc64 pktiter = pktiter - 0x10
0001dc6c mbuf = mbuf + outl_len
0001dc74 *outlen = *outlen + outl_len
For brevity’s sake, we can skip all the way to [9], where EVP_DecryptUpdate
is called repeatedly in a loop over the input buffer. Since the pktlen
argument has been underflowed to atleast 0xFFFFFFFC, it suffices to say that we have a wild read, resulting in a crash when reading unmapped memory.
potentially unexpected fatal signal 11.
CPU: 1 PID: 12452 Comm: cfg_server Tainted: P O 4.1.52 #2
Hardware name: Generic DT based system
task: d04cd800 ti: d0632000 task.ti: d0632000
PC is at 0xb6c7f460
LR is at 0xb6d3ca04
pc : [<b6c7f460>] lr : [<b6d3ca04>] psr: 60070010
sp : b677c46c ip : 00ff4ff4 fp : b6600670
r10: b6c7ef40 r9 : 00000000 r8 : beec0b82
r7 : b6600670 r6 : 00000010 r5 : b6620c38 r4 : 00ff5004
r3 : b6c7f440 r2 : 00000000 r1 : 00000000 r0 : 00000000
Flags: nZCv IRQs on FIQs on Mode USER_32 ISA ARM Segment user
Control: 10c5387d Table: 1048c04a DAC: 00000015
CPU: 1 PID: 12452 Comm: cfg_server Tainted: P O 4.1.52 #2
Hardware name: Generic DT based system
[<c0026fe0>] (unwind_backtrace) from [<c0022c38>] (show_stack+0x10/0x14)
[<c0022c38>] (show_stack) from [<c047f89c>] (dump_stack+0x8c/0xa0)
[<c047f89c>] (dump_stack) from [<c003ac30>] (get_signal+0x490/0x558)
[<c003ac30>] (get_signal) from [<c00221d0>] (do_signal+0xc8/0x3ac)
[<c00221d0>] (do_signal) from [<c0022658>] (do_work_pending+0x94/0xa4)
[<c0022658>] (do_work_pending) from [<c001f4cc>] (work_pending+0xc/0x20)
2022-08-24 - Vendor Disclosure
2022-11-16 - Vendor Patch Release
2023-01-10 - Public Release
Discovered by Lilith >_> of Cisco Talos.