Talos Vulnerability Report

TALOS-2022-1592

Asus RT-AX82U cfg_server cm_processConnDiagPktList denial of service vulnerability

January 10, 2023
CVE Number

CVE-2022-38393

SUMMARY

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.

CONFIRMED VULNERABLE VERSIONS

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

PRODUCT URLS

RT-AX82U - https://www.asus.com/us/Networking-IoT-Servers/WiFi-Routers/ASUS-Gaming-Routers/RT-AX82U/

CVSSv3 SCORE

7.5 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

CWE

CWE-125 - Out-of-bounds Read

DETAILS

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.

Crash Information

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)
TIMELINE

2022-08-24 - Vendor Disclosure
2022-11-16 - Vendor Patch Release
2023-01-10 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.