Headline
CVE-2018-4029: TALOS-2018-0701 || Cisco Talos Intelligence Group
An exploitable code execution vulnerability exists in the HTTP request-parsing function of the NT9665X Chipset firmware running on the Anker Roav A1 Dashcam, version RoavA1SWV1.9. A specially crafted packet can cause an unlimited and arbitrary write to memory, resulting in code execution.
Summary
An exploitable code execution vulnerability exists in the HTTP request-parsing function of the NT9665X Chipset firmware running on the Anker Roav A1 Dashcam, version “RoavA1_SW_V1.9.” A specially crafted packet can cause an unlimited and arbitrary write to memory, resulting in code execution.
Tested Versions
Anker Roav A1 Dashcam RoavA1_SW_V1.9
Product URLs
https://goroav.com/products/roav-dash-cam-a1
CVSSv3 Score
10.0 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE
CWE-120: Buffer Copy without Checking Size of Input (‘Classic Buffer Overflow’)
Details
The Novatek NT9665X SOC is a chipset used in an large number of consumer camera devices, particularly in dashboard cameras. The chip provides default firmware that is a fork of the Embedded Configurable Operating System (eCOS) project, which is found within the Roav A1 Dashcam,the product we are focusing on in this advisory.
The Roav A1 Dashcam by Anker is a dashboard camera that allows users to connect using the Roav app for Android and iOS so that they can control the camera remotely. In order to do this, users must first enable the “Wi-Fi AP” setting manually on the dashcam, and then connect to the “Roav_A1_” SSID, with the default password of “goroavcam”.
From here, the app interacts mainly with the dashboard camera via an eCOS web server running on port 80 that requires no authentication. The standard HTTP POST, GET and DELETE requests can be used to upload, download or delete videos and pictures from the dashcam, but there’s also a separate interface used for configuration. Regardless of the request, when reading any HTTP request, a function dubbed parse_http is used to start parsing out the individual pieces of the request.
Before the parsing even occurs though, the function will set some socket options on the client socket, turning it into a non-blocking socket. After this, it will start receiving the HTTP request in 0x800 bytes chunks. The code that handles this is as such:
803CBBB0 recv_block: # CODE XREF: parse_http_0:recv_loop↓j
803CBBB0 addiu $s2, $s4, 0x6268 //[0]
803CBBB4 addu $s2, $s0, $s2 //[1]
803CBBB8 move $a0, $s0 //[2]
803CBBBC move $a1, $s2
803CBBC0 li $a2, 0x800 //[3]
803CBBC4 jal recv_or_select
;(http_sess,dst,len,0x1,???,???,???)
803CBBC8 li $a3, 1
803CBBCC beqzl $v0, path_to_ret
803CBBD0 lui $s1, 1
803CBBD4 beq $v0, $s6, error__
803CBBD8 addiu $s2, 1
803CBBDC j find_0x0a0d0a0d //[4]
803CBBE0 move $v1, $v0
At [0], the destination write address of the recv_or_select call is calculated from the $a0 HTTP session struct passed into parse_http function, which is now in $s0 (at [1]), and adding 0x6268, presumably to point to an empty buffer. At [2], the source of the recv_or_select call is calculated by just passing in the HTTP session struct itself, which gets further parsed inside. Finally at [3], we can see the length of the read is always 0x800 bytes. The function will return the amount of bytes read, which in our case will always be 0x800, and since it is not returning less than 0x0, we end up taking the branch at [4] to the find_0x0a0d0a0d portion of the code:
803CBBE4 inc_search_ptr:
803CBBE4 # parse_http_0+148↓j ...
803CBBE4 beqz $v1, recv_loop # (diff => ~0x6a31)
803CBBE8 addiu $s2, 1 # $s2 => 0x80d43829
803CBBEC find_0x0a0d0a0d:
803CBBEC lbu $a0, -1($s2) # find \x0d
803CBBF0 bne $a0, $s1, inc_seach_ptr
803CBBF4 addiu $v1, -1
803CBBF8 lbu $a0, 0($s2) # find \x0a
803CBBFC bne $a0, $s3, inc_seach_ptr
803CBC00 nop
803CBC04 lbu $a0, 1($s2) # find \x0d
803CBC08 bne $a0, $s1, inc_seach_ptr
803CBC0C nop
803CBC10 lbu $a0, 2($s2) # find \x0a
803CBC14 bne $a0, $s3, inc_seach_ptr
803CBC18 move $a1, $zero
In short, the above code will search only for the bytes “\x0a\x0d\x0a\x0d” or more familiarly, “\r\n\r\n”, denoting the end of the HTTP request (assuming there’s no POST data). Upon finding this delimiter, the program will start to parse the request for the different HTTP verbs. However, if there isn’t a “\r\n\r\n” within the length 0x800 buffer that we read, we go to the recv_loop basic block and the following occurs instead:
803CBD7C recv_loop: # CODE XREF: parse_http_0:loc_803CBBE4↑j
803CBD7C j recv_block
803CBD80 addu $s4, $v0 # $v0 => 0x800 //[0]
As $v0 still contains the return value from the recv_or_select function, $s4 gets incremented by the number of bytes read, in this case, 0x800. This causes the destination of the write to be increased by 0x800, and the process repeats again:
803CBBB0 recv_block: # CODE XREF: parse_http_0:recv_loop↓j
803CBBB0 addiu $s2, $s4, 0x6268
803CBBB4 addu $s2, $s0, $s2
803CBBB8 move $a0, $s0
803CBBBC move $a1, $s2
803CBBC0 li $a2, 0x800
803CBBC4 jal recv_or_select
;(http_sess,dst,len,0x1,???,???,???)
803CBBC8 li $a3, 1
Note that there is no exit condition for this recv_loop, unless 0x0 bytes are read or a “\r\n\r\n” string is found, which leads to a traditional buffer overflow. In this situation, there’s an unusual circumstance, as for the first iteration of this loop, the parameters to recv_or_select are:
$a0 : 0x80d3cdc0
$a1 : 0x80d43028
$a2 : 0x00000800
$a3 : 0x00000001
But, if the HTTP buffer is greater than ~0x11044 bytes, a crash will occur inside of the recv_or_select function, as the source address of a memcpy call inside of the recv_or_select function is a user-controlled value, and the destination buffer is 0x11044 bytes further along than the first iteration, sitting at 0x80d5406c. To understand why the source address gets overwritten, it is necessary to delve into the crashing memcpy inside of the recv_or_select function:
803CAB0C loc_803CAB0C: # CODE XREF: recv_or_select+A8↑j
803CAB0C lw $a1, 0x6F60($v1) //[0]
803CAB10 lw $v1, 0x88+offset($sp) //[1]
803CAB14 lw $a2, 0x88+memcpy_len($sp) //[2]
803CAB18 jal memcpy # (dst,src,len)
803CAB1C addu $a0, $v0, $v1 # //[3]
At [0], the user-controlled source address is loaded into $a1 from an offset into the http_struct that was passed into the function as $a0.
At [1], $v1 is set to 0x0, and really isn’t important, and at [2], the length of the memcpy is the value that was passed in as $a2 to this function (in our case, this is once again 0x800). At [3], we add the offset (0x0) to the destination of the write, which was the $a1 parameter passed into this function.
With all this in mind, the current running theory is that the recv_loop will eventually overwrite the pointer to the input buffer of the http_session, causing the subsequent memcpy to read from a user-controlled address. It should be noted that this vulnerability was researched without a debugger due to time constraints, so this might not be completely accurate. Regardless of the accuracy of the root cause, the end result is still an unlimited and arbitrary write to memory.
Crash Output
*** CPU Exception!!! cause 0x02: TLB exception (load or instruction fetch)
epc - 0x800b5e10
$ra - 0x803cab20
$sp - 0x80d424d0
$fp - 0x80d424ec
general registers:
$zero : 0x00000000 $at : 0x00000001 $v0 : 0x80d5406c $v1 : 0x00000000
$a0 : 0x80d5406c $a1 : 0x61616161 $a2 : 0x00000000 $a3 : 0x61616961
$t0 : 0x00000008 $t1 : 0x01010101 $t2 : 0x00000000 $t3 : 0x61616765
$t4 : 0x80aed924 $t5 : 0x800a0000 $t6 : 0x00000000 $t7 : 0x807d0000
$s0 : 0x80d424f4 $s1 : 0x61616161 $s2 : 0x80d424e8 $s3 : 0x80d42514
$s4 : 0x80afe8b8 $s5 : 0x80d3cd60 $s6 : 0x00000001 $s7 : 0x0007a120
$t8 : 0x8089ad00 $t9 : 0x8060f540 null : 0x00000800 null : 0x80d5406c
gp : 0x8060f540 sp : 0x80d424d0 fp : 0x80d424ec ra : 0x803cab20
co-processor registers:
entrylo : 0x00000001 status : 0x00000008 vector : 0x0100c403 epc : 0x800b5e10
cause : 0x00000000 badvaddr : 0x00800008 hwrena : 0x00000400 prid : 0x00019655
entrylo : 0x01605792
Thread(id) :
Hfs Session(260)
stack :
range(0x80d3ce94 - 0x80d42e94)
call stack :
0 frame(0x80d424d0 - 0x80d424e8) ............................ $pc : 0x800b5e10
+ 0x80d424d0 : 0x80d53e6a 0x0000000a 0x000108a4 0x00000008
+ 0x80d424e0 : 0xffffffff 0x05040017
:
abort ($pc 0504000f is invalid address!)
*** CPU Exception in Task[]! cause=0x00000002, addr=0x800b5e10
Timeline
2018-10-29 - Talos contacts vendor
2018-11-02 - Report disclosed to vendor
2018-12-04 - 30 day follow up
2019-01-18 - 60 day follow up - Talos reaches out to TWNCERT for assistance reaching vendor (Novatek)>br> 2019-01-22 - TWNCERT contacted Novatek and advised Novatek will check emails for reports
2019-03-06 - 90+ day follow up - Talos asks TWNCERT for direct point of contact for Novatek
2019-03-27 - Talos sends follow up to TWNCERT
2019-04-02 - Talos sends copies of email correspondence and reports to TWNCERT
2019-04-18 - Suggested pubic disclosure date of 2019-05-13 (171 days after initial disclosure)
2019-04-19 - Vendor fixed issue and provided patch to their IDH
2019-05-13 - Public disclosure
Discovered by Lilith [<_<] of Cisco Talos.