WINTERN 2020: IOT FIRMWARE ANALYSIS — Margin Research
WINTERN 2020: IOT FIRMWARE ANALYSIS

WINTERN 2020: IOT FIRMWARE ANALYSIS

Chase Kanipe
by Chase Kanipe
May 13, 2021

For this project I did a partial analysis of the Wyze V2 Camera. It's a small camera that allows one to live stream video in 1080p to your phone from anywhere. It also supports motion/sound recording with cloud storage via AWS. This data can also be stored to an SD card.

Extracting the firmware

Extracting the firmware proved to be the most time-consuming challenge of this project. Initially, I downloaded several versions of the firmware from their website and extracted them using binwalk. These images turned out to be diffs though, so they weren't sufficient for analysis.
My second attempt at extracting the firmware was to read it over SPI from a small chip inside the camera. Unfortunately the equipment I had available was very limited. While I did have a bus pirate, the soldering iron I had avaliable's tip was much too large to solder onto the tiny chip legs, and the jumper cables I used were as thick as the legs themselves. These hardware limitations made it difficult to solder the last two jumper cables to the legs, and the connection was never stable enough for the bus pirate to read a complete firmware image.
Eventually, Matt from Margin Research found a public repo called WyzeHacks, which had various tools for augmenting the Wyze firmware. He used this to install a backdoor into the camera so that binaries could be pulled off the device. Below is my analysis of a file he sent me.

Manual Reverse Engineering

strcpy

I began my analysis process with a manual analysis of the hl_client binary using Radare2. Hoping to find some low-hanging fruit, I started by looking for cross references to commonly vulnerable library calls. Here I get a list of imports, filtering to those containing the string "str".

[0x0041112c]> ii~str
4   0x004015dc GLOBAL FUNC       strcpy
37  0x004017bc GLOBAL FUNC       strnlen
51  0x0040185c GLOBAL FUNC       strstr
54  0x0040188c GLOBAL FUNC       strncmp
57  0x004018bc GLOBAL FUNC       strncpy
76  0x0040199c GLOBAL FUNC       strcmp
96  0x00401aac GLOBAL FUNC       strlen
97  0x00401abc GLOBAL FUNC       strchr


This binary imports strcpy, a potentially dangerous function.

[0x0041112c]> axt strcpy
sym.read_IP_file 0x401ba8 [CALL] jal strcpy
sym.msgarrv 0x40370c [CALL] jal strcpy
sym.msgarrv 0x4039ac [CALL] jal strcpy
sym.print_string_ptr 0x413d60 [CALL] jal strcpy
sym.print_object 0x416060 [CALL] jal strcpy
sym.iot_get_log_handler 0x418a3c [CALL] jal strcpy
sym.iot_delete_sensor_handler 0x419000 [CALL] jal strcpy
sym.iot_bind_cancel_handler 0x41952c [CALL] jal strcpy

Manual analysis revealed that most of the uses are innocuous. For example, copying static strings embedded in the binary to an appropriately sized buffer. However, at least one of them appears to be vulnerable to a stack buffer overflow. We'll start by disassembling the block containing the relevant cross reference.

:> pdb 0x401b64
¦           0x00401b64      b800c48f       lw a0, 0xb8(fp)
¦           0x00401b68      4200023c       lui v0, 0x42
¦           0x00401b6c      689b4524       addiu a1, v0, -0x6498
¦           0x00401b70      9470100c       jal fopen           ; Opens file
¦           0x00401b74      00000000       nop
¦           0x00401b78      1800c2af       sw v0, 0x18(fp)
¦           0x00401b7c      2400c227       addiu v0, fp, 0x24
¦           0x00401b80      21204000       move a0, v0
¦           0x00401b84      01000524       addiu a1, zero, 1
¦           0x00401b88      80000624       addiu a2, zero, 0x80
¦           0x00401b8c      1800c78f       lw a3, 0x18(fp)
¦           0x00401b90      8470100c       jal fread           ; Reads data from stream
¦           0x00401b94      00000000       nop
¦           0x00401b98      2000c2af       sw v0, 0x20(fp)
¦           0x00401b9c      2400c227       addiu v0, fp, 0x24
¦           0x00401ba0      b000c48f       lw a0, 0xb0(fp)
¦           0x00401ba4      21284000       move a1, v0
¦           0x00401ba8      bc6f100c       jal strcpy          ; Copies data until \0
¦           0x00401bac      00000000       nop


It appears that this code opens a file and copies the data, regardless of size, to a buffer. We can get cross references to the function:

sym.getServerStr 0x401ce8 [CALL] jal sym.read_IP_file

Disassembly of getServerStr is shown below:

[0x00401ca4]> pdb
            ; CALL XREF from sym.client_ParamInit @ 0x403dc4
+ 124: sym.getServerStr (int32_t arg1, int32_t arg3, int32_t arg_18h);
¦           0x00401ca4      d8ffbd27       addiu sp, sp, -0x28
¦           0x00401ca8      2400bfaf       sw ra, 0x24(sp)
¦           0x00401cac      2000beaf       sw fp, 0x20(sp)
¦           0x00401cb0      21f0a003       move fp, sp
¦           0x00401cb4      1800c0af       sw zero, 0x18(fp)
¦           0x00401cb8      4300023c       lui v0, 0x43
¦           0x00401cbc      e0e24424       addiu a0, v0, -0x1d20
¦
¦           0x00401cc0      21280000       move a1, zero
¦           0x00401cc4      80000624       addiu a2, zero, 0x80
¦           0x00401cc8      9870100c       jal memset
¦           0x00401ccc      00000000       nop
¦           0x00401cd0      4300023c       lui v0, 0x43
¦           0x00401cd4      e0e24424       addiu a0, v0, -0x1d20
¦
¦           0x00401cd8      1800c227       addiu v0, fp, 0x18
¦           0x00401cdc      21284000       move a1, v0
¦           0x00401ce0      4200023c       lui v0, 0x42
¦           0x00401ce4      949b4624       addiu a2, v0, -0x646c
¦           0x00401ce8      b306100c       jal sym.read_IP_file
¦           0x00401cec      00000000       nop
¦       +-< 0x00401cf0      04004014       bnez v0, 0x401d04
¦       ¦   0x00401cf4      00000000       nop

It appears that there's a buffer overflow from reading and copying the file /configs/IOT_server.txt. The data is read into a fixed size buffer, but its length is unbounded due to the use of strcpy. This could lead to a stack buffer overflow.

printf

We can also look around for any format string vulnerabilities. We'll start by getting cross references to printf:

[0x0041bf00]> axt
sym.read_IP_file 0x401af8 [CALL] jal printf
sym.common_process_data 0x401d8c [CALL] jal printf
sym.parse_action 0x401fb4 [CALL] jal printf
sym.parse_action 0x402188 [CALL] jal printf
sym.parse_action 0x4021cc [CALL] jal printf
sym.parse_action 0x402210 [CALL] jal printf
sym.parse_action 0x402254 [CALL] jal printf
sym.parse_action 0x402298 [CALL] jal printf
sym.msgsend 0x4028c8 [CALL] jal printf
sym.msgsend 0x402928 [CALL] jal printf
sym.delta_handler 0x40298c [CALL] jal printf
sym.delta_handler 0x402b28 [CALL] jal printf
sym.delta_handler 0x402b9c [CALL] jal printf
sym.delta_handler 0x402c74 [CALL] jal printf
sym.get_shadow_handler 0x402cf4 [CALL] jal printf
sym.get_shadow_handler 0x402e54 [CALL] jal printf
sym.get_shadow_handler 0x402ec8 [CALL] jal printf
sym.get_shadow_handler 0x402fa0 [CALL] jal printf
sym.msgarrv 0x403038 [CALL] jal printf
sym.msgarrv 0x4030d4 [CALL] jal printf
sym.msgarrv 0x403178 [CALL] jal printf
sym.msgarrv 0x403334 [CALL] jal printf
sym.msgarrv 0x403438 [CALL] jal printf
sym.setTopic 0x403b28 [CALL] jal printf
sym.setTopic 0x403b5c [CALL] jal printf
sym.setTopic 0x403b90 [CALL] jal printf
sym.setTopic 0x403bc4 [CALL] jal printf
sym.disconnectCallbackFunc 0x403d68 [CALL] jal printf
sym.client_ParamInit 0x403ddc [CALL] jal printf
sym.client_ParamInit 0x403ec0 [CALL] jal printf
sym.clientMqttConnect 0x403f8c [CALL] jal printf
sym.clientMqttConnect 0x403fb0 [CALL] jal printf
sym.clientMqttConnect 0x4040ac [CALL] jal printf
sym.clientMqttConnect 0x4040cc [CALL] jal printf
sym.aws_iot_subscribe_publish 0x404608 [CALL] jal printf
sym.get_message_handler 0x4047e0 [CALL] jal printf
...truncated...  

The full list is rather long. We'll filter it down by constructing a command to see if each use is preceded by a format string. We can get the full list of cross references with the axt printf command. We can filter this list to just the offsets by getting the column using ~[1] and we'll write it to a file using > offsets.txt. The full command is as follows.

[0x0041067c]> axt printf~[1] > offsets.txt

We can cat this list (truncated).

[0x0041067c]> !cat offsets.txt
0x401af8
0x401d8c
0x401fb4
0x402188
0x4021cc
0x402210
0x402254
0x402298
0x4028c8
0x402928
0x40298c
0x402b28
0x402b9c
0x402c74
0x402cf4
0x402e54
0x402ec8
0x402fa0
0x403038
0x4030d4
0x403178
0x403334
0x403438
0x403b28
0x403b5c
0x403b90
...truncated...  

We can print the full block for each offset using the pdb command. We can iterate this command over the list using @@, giving us the full command below.

[0x0041067c]> pdb @@.offsets.txt

We can then scan through this output, making sure each use of printf has a corresponding static format string. All uses of printf appear to be secure, except for possibly one.

:> pdb
¦           ; CODE XREF from sym._iot_tls_verify_cert @ 0x410644
¦           0x0041068c      4200023c       lui v0, 0x42
¦           0x00410690      60b24424       addiu a0, v0, -0x4da0
¦           0x00410694      4200023c       lui v0, 0x42
¦           0x00410698      38b94524       addiu a1, v0, -0x46c8
¦           0x0041069c      3a000624       addiu a2, zero, 0x3a
¦           0x004106a0      c06f100c       jal printf
¦           0x004106a4      00000000       nop
¦           0x004106a8      2c04c28f       lw v0, 0x42c(fp)
¦           0x004106ac      0000428c       lw v0, (v0)
¦           0x004106b0      1800c327       addiu v1, fp, 0x18
¦           0x004106b4      21206000       move a0, v1
¦           0x004106b8      00040524       addiu a1, zero, 0x400
¦           0x004106bc      4200033c       lui v1, 0x42
¦           0x004106c0      c0b26624       addiu a2, v1, -0x4d40
¦           0x004106c4      21384000       move a3, v0
¦           0x004106c8      c06f100c       jal printf
¦           0x004106cc      00000000       nop
¦           0x004106d0      0a000424       addiu a0, zero, 0xa
¦           0x004106d4      b46f100c       jal fcn.0041bed0
¦           0x004106d8      00000000       nop
¦           0x004106dc      4200023c       lui v0, 0x42
¦           0x004106e0      60b24424       addiu a0, v0, -0x4da0
¦           0x004106e4      4200023c       lui v0, 0x42
¦           0x004106e8      38b94524       addiu a1, v0, -0x46c8
¦           0x004106ec      3b000624       addiu a2, zero, 0x3b
¦           0x004106f0      c06f100c       jal printf
¦           0x004106f4      00000000       nop
¦           0x004106f8      1800c227       addiu v0, fp, 0x18
¦           0x004106fc      21204000       move a0, v0
¦           0x00410700      0470100c       jal fcn.0041c010
¦           0x00410704      00000000       nop
¦           0x00410708      0a000424       addiu a0, zero, 0xa
¦           0x0041070c      b46f100c       jal fcn.0041bed0
¦           0x00410710      00000000       nop 

We can see that the second printf in this basic block uses a buffer passed into the function as the format string. When manually tracing this back, we can see this is tainted by a AWS related network call. It's hard to determine through manual static analysis if this is exploitable. There's a lot of code between the network call and the use, so it would be best to test this dynamically.

Heap Issues

We can also look for heap related issues like use-after-frees, double frees, unchecked malloc returns, etc. By searching through the functions that call free, we can build a list of potentially interesting functions which may have heap bugs.

[0x00419818]> axt sym.imp.free
(nofunc) 0x400c18 [UNKNOWN] invalid
sym.msgarrv 0x403a50 [CALL] jal sym.imp.free
sym.upload_resend_list_again 0x405510 [CALL] jal sym.imp.free
sym.upload_resend_list_again 0x405544 [CALL] jal sym.imp.free
sym.cJSON_InitHooks; sym.imp.free 0x4123a4 [DATA] addiu v1, v1, -sym.imp.free
sym.cJSON_InitHooks; sym.imp.free 0x412408 [DATA] addiu v0, v0, -sym.imp.free
sym.cJSON_Delete 0x41252c [CALL] jalr t9
sym.cJSON_Delete 0x412570 [CALL] jalr t9
sym.cJSON_Delete 0x412588 [CALL] jalr t9
sym.ensure 0x412be0 [CALL] jalr t9
sym.ensure 0x412c4c [CALL] jalr t9
sym.print_array 0x4150a8 [CALL] jalr t9
sym.print_array 0x4150e0 [CALL] jalr t9
sym.print_array 0x4151f8 [CALL] jalr t9
sym.print_array 0x415230 [CALL] jalr t9
sym.print_object 0x415c38 [CALL] jalr t9
sym.print_object 0x415e60 [CALL] jalr t9
sym.print_object 0x415ea8 [CALL] jalr t9
sym.print_object 0x415ee0 [CALL] jalr t9
sym.print_object 0x415ef8 [CALL] jalr t9
sym.print_object 0x416110 [CALL] jalr t9
sym.print_object 0x41613c [CALL] jalr t9
sym.print_object 0x416174 [CALL] jalr t9
sym.print_object 0x41618c [CALL] jalr t9
sym.cJSON_AddItemToObject 0x416560 [CALL] jalr t9
sym.cJSON_AddItemToObjectCS 0x416618 [CALL] jalr t9
sym.xxtea_ubyte_encrypt 0x4185c4 [CALL] jal sym.imp.free
sym.xxtea_ubyte_encrypt 0x418618 [CALL] jal sym.imp.free
sym.xxtea_ubyte_encrypt 0x418624 [CALL] jal sym.imp.free
sym.xxtea_ubyte_decrypt 0x4186e8 [CALL] jal sym.imp.free
sym.xxtea_ubyte_decrypt 0x41873c [CALL] jal sym.imp.free
sym.xxtea_ubyte_decrypt 0x418748 [CALL] jal sym.imp.free
sym.is_resend_action_func 0x4196c4 [CALL] jal sym.imp.free
sym.delete_guid_from_linklist 0x41981c [CALL] jal sym.imp.free 


After scanning through these manually, there is one potential issue here.

:> pdb
¦           ; CODE XREF from sym.xxtea_to_ubyte_array @ 0x417f98
¦           0x00418008      1800c28f       lw v0, (var_18h)
¦           0x0041800c      01004224       addiu v0, v0, 1
¦           0x00418010      21204000       move a0, v0
¦           0x00418014      0c70100c       jal sym.imp.malloc
¦           0x00418018      00000000       nop
¦           0x0041801c      2000c2af       sw v0, (var_20h)
¦           0x00418020      2000c48f       lw a0, (var_20h)
¦           0x00418024      3000c58f       lw a1, (arg_30h)
¦           0x00418028      1800c68f       lw a2, (var_18h)
¦           0x0041802c      fc6f100c       jal memcpy      ; Null pointer dereference
¦           0x00418030      00000000       nop
¦           0x00418034      2000c38f       lw v1, (var_20h)
¦           0x00418038      1800c28f       lw v0, (var_18h)
¦           0x0041803c      21106200       addu v0, v1, v0                                                                     
¦           0x00418040      000040a0       sb zero, (v0)       


It looks like they forgot to check the return of the malloc call and it is immediately freed. This could lead to a null pointer dereference if the malloc call fails (there are a variety of methods for inducing this) and the null pointer is passed to memcpy. Exploiting this kind of vulnerability, however, is generally difficult.

Summary

The summary of my manual results is:

  • 1 simple stack buffer overflow but you'd need to overwrite a local file
  • 6 would-be buffer overflows (some stack, some heap) if the function containing the library call was used incorrectly, but none of those appear to be exploitable
  • 2 format strings, exploitability not yet determined
  • 1 null pointer dereference
  • sscanf uses appear to be safe
  • A bunch of safe uses of strcpy

Preliminary Emulation

In the interest of proving the exploitability of some of these vulnerabilities, I started the process of emulating the binary. Unfortunately due to the timeframe of this project, the emulation wasn’t able to be completed. This appears to be an ideal use case for Qiling, a binary emulation framework built on unicorn and written in Python.

Qiling

I wanted to be able to emulate the binary and visualize the coverage. I decided to do this by adding a basic block trace to the Qiling script and dumping these addresses to a file. Then, by reading this file in Binary Ninjaand highlighting the traced basic blocks, we can visualize the coverage. An example emulation script is below.

from qiling import *
import os

def hook_block(ql, address, size):
    if "0x4" in hex(address):
        print("At address: 0x%x" % address)
        with open("output.txt", "a+") as f:
            f.write(hex(address) + "\n")

def go():
    ql = Qiling(["/home/oem/margin2/hl_client"],
                 "/home/oem/software/qiling/examples/rootfs/mips32_linux")
    ql.hook_block(hook_block)
    os.system("rm output.txt; touch output.txt")
    ql.run()

go() 

After running the script, we can then build a small Binary Ninjascript to visualize the contents of output.txt.

from binaryninja.interaction import ( 
    show_message_box,
    get_int_input,
    get_choice_input
)

from binaryninjaui import (
    DockHandler,
    DockContextHandler,
    getMonospaceFont,
    UIActionHandler
)

from PySide2 import QtCore
from PySide2.QtCore import Qt, QMimeData
from PySide2.QtGui import QBrush, QColor
from PySide2.QtWidgets import (
    QApplication,
    QVBoxLayout,
    QWidget,
    QComboBox,
    QTableWidget,
    QTableWidgetItem,
    QMenu
)

import pyqtgraph as pg


class RegisterView(QWidget, DockContextHandler):
    def __init__(self, parent, name, data):
        print(" ---------- Initializing visualization view ----------")

        QWidget.__init__(self, parent)
        DockContextHandler.__init__(self, self, name)

        self.parent = parent

        self.actionHandler = UIActionHandler()
        self.actionHandler.setupActionHandler(self)
        self._layout = QVBoxLayout()

        pg.plot([5, 6, 7, 8, 9])

        print(" ---------- Initialized visualization view  ---------- ")


class PlotView(QWidget, DockContextHandler):
    def __init__(self, parent, name, data):
        print(" ---------- Initializing visualization view ----------")

        QWidget.__init__(self, parent)
        DockContextHandler.__init__(self, self, name)
        self.parent = parent
        self.actionHandler = UIActionHandler()
        self.actionHandler.setupActionHandler(self)
        self.layout = QVBoxLayout()
        plt1 = pg.PlotWidget(name="Plot 1", clickable=False)
        plt1.setLabel("left", "Value", units="V")

        #plt = QLineSeries()

        self.layout.addWidget(plt1)
        #self.layout.addWidget(plt2)

        plt1.plot([5, 6, 7, 8], [9, 10, 11, 12])
        # plt2.plot([5, 6, 7, 8], [9, 10, 11, 12])

        self.setLayout(self.layout)

        # pg.plot([5, 6, 7, 8, 9])

        print(" ---------- Initialized visualization view  ---------- ")


def get_window(name, parent, data):
    window = PlotView(parent, name, data)
    window.setWindowTitle("Here is a new title")
    window.setEnabled(False)
    return window


dock_handler = DockHandler.getActiveDockHandler()

dock_handler.addDockWidget(
    "SENinja Registers",
    get_window,
    Qt.RightDockWidgetArea,
    Qt.Vertical,
    False
)

print("Done running test plugin") 

Some examples of the highlighted coverage in Binary Ninja shown below.

Here is more coverage from another function.

Conclusion

In summary, through manual analysis I found:

  • 1 simple stack buffer overflow but you'd need to overwrite a local file
  • 6 would-be buffer overflows (some stack, some heap) if the function containing the library call was used incorrectly, but none of those appear to be exploitable
  • 2 format strings, exploitability not yet determined
  • 1 potential null pointer dereference
  • sscanf uses appear to be safe
  • A bunch of safe uses of strcpy
  • The small qiling script is contained in hl_client.py. The Binary Ninja plugin is contained in plugin.py.


Chase can be found on GitHub here: https://github.com/0xchase


Share this article:

arrow-up icon