Talos Vulnerability Report

TALOS-2018-0701

Novatek NT9665X HFS Recv buffer overflow code execution vulnerability

May 13, 2019
CVE Number

CVE-2018-4029

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

Credit

Discovered by Lilith [<_<] of Cisco Talos.