CVE-2018-3892
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.
Yi Technology Home Camera 27US 1.8.7.0D
9.6 - CVSS:3.0/AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE-121: Stack-based Buffer Overflow
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
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
Discovered by Lilith (>_>) of Cisco Talos.