Talos Vulnerability Report

TALOS-2018-0567

Yi Technology Home Camera 27US TimeSync Code Execution Vulnerability

October 31, 2018
CVE Number

CVE-2018-3892

Summary

An exploitable firmware downgrade vulnerability exists in the time syncing functionality of Yi Home Camera 27US 1.8.7.0D. A specially crafted packet can cause a buffer overflow, resulting in code execution. An attacker can intercept and alter network traffic to trigger this vulnerability.

Tested Versions

Yi Technology Home Camera 27US 1.8.7.0D

Product URLs

https://www.yitechnology.com

CVSSv3 Score

9.6 - CVSS:3.0/AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-121: Stack-based Buffer Overflow

Details

Yi Home Camera is an IoT home camera sold globally. The 27US version is one of the newer models sold in the U.S., and is the most basic model out of the Yi Technology camera lineup. It still, however, includes all the functionality that one would expect from an IoT device: the ability to view video from anywhere, offline and subscription-based cloud storage, and ease of setup.

After initial pairing to a network has occurred and the camera has network connectivity, it immediately reaches out to api.us.xiaoyi.com to perform a time synchronization. The request disassembly is shown below:

//cloud.c (~0x148fc)
LDR     R2, =aHttpS     ; "http://%s"
ADD     R3, R3, #0x14
ADD     R0, SP, #0xCB0+var_C68 ; s
BL      snprintf
ADD     R3, SP, #0xCB0+var_C68
LDR     R1, =aSC136UrlSV2Ipc ; "%s -c 136 -url %s/v2/ipc/sync_time "
LDR     R2, =aHomeAppCloudap ; "/home/app/cloudAPI"
MOV     R0, R6          ; stack_addr //[0]
BL      sprintf
[…]
popen_cmd:
MOV     R1, R4         //[1]
MOV     R2, #0x800 //[2]
MOV     R3, #0xA
MOV     R0, R6 
BL      popen_cmd_withret_timeout

A command is built inside of the address pointed to by R6 at [0], which is located on the stack, and then this is eventually passed as R0 to a call to popen, and the first 0x800 [1] bytes of the results are stored in the address pointed to by R4 [2], which is a size 0x800 buffer on the stack. This results in the following request and response being sent:

[o.o] Sent 114 bytes to remote (10.10.69.23:38351→api.us.xiaoyi.com:80)

GET /v2/ipc/sync_time?hmac=BlDqGUkYNp8%3D&seq=9 HTTP/1.1
Host: api.us.xiaoyi.com
Accept: */*

[o.o] Sent 343 bytes to local (10.10.69.23:38351←api.us.xiaoyi.com:80)

HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: text/html;charset=UTF-8
Date: Wed, 28 Mar 2018 19:09:26 GMT
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: No-cache
Server: Apache-Coyote/1.1
X-Vcap-Request-Id: 1d642545-a6fd-429e-7f36-0587378a5eda
Content-Length: 37
Connection: keep-alive

{"code":"20000","time":1522264167222}

It should be explicitly noted that this occurs over HTTP and not HTTPS. Thus, if there’s any sort of MITM attack going on, the attacker has complete control over the response (with the exception of the 0x800 size limit). Keeping that in mind, let's move on to what is done with the response:

ADD     R0, SP, #0xCB0+nptr   //[1]
STR     R7, [R3],#4
LDR     R1, =aCode      ; "\"code\"" //[2]
MOV     R2, R4                          //[3]
STR     R7, [R3]                       
BL      trans_json      ; (output,needle,haystack)

A new function trans_json is called that takes in three parameters. Somewhat similar to strstr, it searches for the needle “code” [2] inside of the haystack [3] which is the POST data read from the server, and then stores the output inside of R0, which is an address further up on the stack.

This setup, under normal circumstances would be somewhat safe, considering the length restriction (0x800) of the server’s response. But it is most unfortunate that the trans_json function is not the most securely implemented:

MOV     R0, R2          ; haystack
MOV     R5, R1          ; needle
BL      strstr
SUBS    R6, R0, #0
BEQ   ret_0

As one might expect, it first searches for the needle within the haystack. If the needle is not found, it returns 0. If a strstr match is found, the resulting address is stored in R6, and then the length of the needle is found and added to the address in R6, which results in a pointer to the parameter’s value, which is stored in R1. Continuing on, it weirdly loops 12 times, incrementing R1 until it finds a character that’s not a quote (0x27), double quote (0x22) or colon char (0x3a). After this, the new pointer is stored in LR and we resume with disassembly:

loc_15A48                               ; CODE XREF: trans_json+9C↓j
       MOV        R0, LR          // [1]
       MOV        R2, R4          // [2]
       LDR         R1, =asc_1AD7C  ; "%[^\"',}]"
       BL            sscanf
       MOV        R0, #1
       LDMFD   SP!, {R4-R6,PC}

The program reads in all the characters that are not do not match the set of chars at 3 from the value of the JSON parameter at [1], and then outputs it to R4 at [2], which is the original R0/output address passed in. Not surprisingly, there’s a couple issues here. First, there’s no length checks anywhere in the function, the only restriction is that our haystack is 0x800 bytes long max, due to the previous popen_cmd_withret_timeout. The second issue is that the output buffer that gets written to is most definitely not 0x800 bytes long. A quick glance at the stack setup:

EXPORT yi_sync_time
yi_sync_time

var_CB0= -0xCB0
var_CA4= -0xCA4
var_CA0= -0xCA0
var_C9C= -0xC9C
code_param_dst= -0xC98  // [1]
var_C94= -0xC94
time_param_dst= -0xC88   // [2]
var_C84= -0xC84
var_C68= -0xC68
cmd_sprintf_dst= -0xC28   // [3] 
command_response= -0x830  // [4]

The output of our trans_json(stack_addr,”code”,cmd_resp) call is going to go to [1]. It should also be noted that the time parameter is treated the same way, and read with trans_json into the stack address at [2]. Since the overflow occurs higher up on the stack, and the variable at [4] is so large, we cannot actually overflow into the stored return address, but thankfully, we can overflow into the cmd_sprintf_dst variable. Which, if atoi(code) does not return 0x4e20, ends up getting passed back into popen_cmd_withret_timeout:

LDR     R1, =aCode      ; "\"code\""
MOV     R2, R4
STR      R7, [R3]
BL        trans_json      ; (output,needle,haystack)
CMP     R0, #1
ADD     R0, SP, #0xCB0+code_param_dst 
BNE     sleep_retry

BL      atoi
LDR     R3, =0x4E20
CMP     R0, R3
BEQ     parse_time

sleep_retry:
LDR     R0, =0xBB8
BL      ms_sleep
SUBS    R5, R5, #1
BNE     popen_cmd

Thus resulting in whatever string we overflow with getting evaluated as a command, and code execution:

Breakpoint 1, 0x0001499c in yi_sync_time ()
(gdb) info reg r0 r1 r2 
r0 0xbeffeec0 3204443840
r1 0x19504 103684 
r2 0xbefff330 3204444976
(gdb) x/1s $r1
0x19504: "\"code\""
(gdb) x/1s $r2
0xbefff330: "req_info=http://api.us.xiaoyi.com/v2/ipc/sync_time?hmac=GUkYNp8%3D&seq=9\n{\"code\":\"", 'Z' <repeats 98 times>...

(gdb) c 
Continuing.

sh: ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ: not found

Timeline

2018-05-01 - Vendor disclosure
2018-09-03 - Vendor submitted build to Talos for testing
2018-09-05 - Talos confirmed issue patched
2018-10-22 - Vendor released new firmware
2018-10-31 - Public release

Credit

Discovered by Lilith (>_>) of Cisco Talos.