Field report / IoT

Dumping a Bricked CP Plus E31A Camera and Pulling Its Firmware Apart

A practical walkthrough of dumping my own bricked CP Plus E31A IP camera with a CH341A, extracting its SquashFS and JFFS2 partitions, and mapping the boot flow, Tuya storage, and sensitive config areas.

#firmware#iot-security#hardware-hacking#reverse-engineering#ch341a#tuya#squashfs#jffs2
Technical recordCP-PLUS-E31A-CAMERA-FIRMWARE-DUMP
Device
CP Plus E31A / EzyKam+ Wi-Fi IP camera
Flash
Winbond 8 MB SPI NOR
Programmer
CH341A with SOIC clip
Firmware
MIPS32 little-endian Linux, uClibc
Tuya SDK
TuyaOS 2.3.2 / SDK 5.3.37
Status
Dump verified, filesystems mapped, secrets redacted

This started with a camera that was already gone.

My CP Plus E31A, also sold around the EzyKam+ ecosystem, was permanently bricked. It was not booting, it was not joining Wi-Fi, and there was no normal web UI or app path left to recover it. At that point the interesting question changed from “can I fix it?” to “what is actually inside this thing?”

So I took the direct route: clip onto the SPI flash, dump the firmware, and pull the image apart locally.

This post is the cleaned-up version of that process. It is not a remote exploit story, and it is not a dump of someone else’s device. It is a hardware-level look at my own bricked camera, with private credentials and device values redacted.

Getting a trustworthy flash dump

The camera uses an 8 MB Winbond SPI NOR flash chip. I used a CH341A USB SPI programmer and a SOIC clip to read the chip in-circuit.

The basic setup was:

  • CP Plus E31A camera board;
  • Winbond 8 MB SPI NOR flash;
  • CH341A USB SPI programmer;
  • SOIC clip on the flash chip;
  • Linux host running flashrom.

I did not want to build the whole analysis on a flaky clip read, so I read the chip more than once and compared the dumps.

flashrom -p ch341a_spi -r backup1.bin
flashrom -p ch341a_spi -r backup2.bin
sha256sum backup1.bin backup2.bin

Both images were exactly 8 MB and produced the same SHA-256:

142c6f614b63aa8bc5f0b1731c5b1aff2a91511628ba0044fc01fc97b2555655

That matching hash mattered more than the tool output. A successful-looking flashrom run can still be a bad read if the clip shifts or the board is being powered in a messy way. Two matching full reads gave me a stable starting point.

Pulling apart the image

With a clean dump in hand, I used binwalk to inspect and extract the firmware:

binwalk backup1.bin
binwalk -e backup1.bin

The image separated into three areas that explained most of the device layout:

Area Offset Type Runtime mount
Root filesystem 0x200000 SquashFS /
Application partition 0x2f0000 SquashFS /app
Writable configuration 0x7a0000 JFFS2 /conf
0x000000  ── earlier firmware regions
0x200000  ── SquashFS ── /
0x2f0000  ── SquashFS ── /app
0x7a0000  ── JFFS2   ── /conf
                            ├── tuya.json
                            ├── tuya_user.db
                            └── tuya_enckey.db

The first SquashFS is the embedded Linux root filesystem. The second SquashFS is the camera application partition. The JFFS2 area is where the device keeps writable configuration.

In short: the extracted root filesystem was SquashFS, the extracted application filesystem was SquashFS, and the writable configuration area was JFFS2.

That split is common in small embedded devices: keep the OS and application read-only, then put user/device state in a writable flash partition.

The root filesystem was small and familiar

The root filesystem looked like a compact BusyBox-based Linux system:

/
├── bin
├── etc
├── lib
├── sbin
├── usr
└── var

The first files I checked were the usual suspects:

/etc/inittab
/etc/init.d/rcS
/etc/passwd
/etc/shadow

/etc/passwd showed a root account:

root:x:0:0:root:/:/bin/sh

/etc/shadow contained a root password hash. I am not publishing the hash, but its presence is important because the firmware also starts a serial login prompt:

console::respawn:/sbin/getty -L console 115200 vt100

That tells us the camera is configured to bring up a login prompt on the UART console. It does not mean there is an unauthenticated shell. It does mean that the UART path is part of the device’s recovery/debug surface.

The boot chain

The boot path is simple once the scripts are laid out:

  1. BusyBox init starts.
  2. /etc/inittab runs /etc/init.d/rcS.
  3. rcS sets PATH and LD_LIBRARY_PATH.
  4. rcS brings loopback up and reads the hardware clock.
  5. rcS mounts /dev/mtdblock3 as /app.
  6. rcS mounts /dev/mtdblock4 as /conf.
  7. rcS starts /app/init/app_init.sh.
  8. app_init.sh runs /app/jz/load.sh.
  9. Drivers and platform setup scripts run.
  10. /app/bin/daemon is copied to /tmp/daemon.
  11. /tmp/daemon @ipc starts the main application path.

The key lines from rcS are:

mount -t squashfs /dev/mtdblock3 /app
mount -t jffs2 -o sync /dev/mtdblock4 /conf
/app/init/app_init.sh &

And the important part of app_init.sh:

cp /app/bin/daemon /tmp/
chmod +x /tmp/daemon
insmod /app/drivers/gv-gpio.ko
/tmp/daemon @ipc

There is also a commented telnet line in the startup scripts:

# telnetd &

That is just an observation. I did not find the boot scripts enabling telnet by default.

The application partition

The second SquashFS becomes /app at runtime. This is where the camera-specific pieces live:

/app
├── bin
├── drivers
├── init
├── jz
├── lib
├── music
├── osd
├── sound
└── web

The two binaries that matter most are:

/app/bin/daemon
/app/bin/ipc

daemon is launched first. It imports process-control and watchdog-flavored functions like fork, execvp, waitpid, kill, signal, setsid, gv_swdg_check, and gv_swdg_rmpid. That makes it look like a supervisor for the main camera process.

ipc is the big one. It is the main camera application and contains references to Tuya startup, MQTT, ONVIF, RTSP, P2P, WebRTC, BLE, Wi-Fi provisioning, OTA, cloud activation, and device identity handling.

Both are stripped MIPS32 little-endian ELF binaries using uClibc:

ipc:    ELF 32-bit LSB executable, MIPS, MIPS32 rel2, dynamically linked, stripped
daemon: ELF 32-bit LSB executable, MIPS, MIPS32 rel2, dynamically linked, stripped

The app libraries also tell a lot about the design:

libgv_core.so
libgv_ipc.so
libgv_rtsp.so
libgv_onvif_rtsp.so
libgv_device_id_decryption.so

That last one, libgv_device_id_decryption.so, immediately went onto the list for deeper analysis because the name points toward device identity handling somewhere in the stack.

Tuya SDK fingerprints

Strings in ipc made the Tuya side visible:

Firmware version: 2.132.982
Tuya SDK version: 5.3.37

The longer SDK string was:

TuyaOS2.3.2_ty_ipc_wr_wl_linux_sdk_5.3.37-beta.16_mips-gcc472-uclibc216-64bit-r233_wifi_wire_ble_no_qrcode_0.0.1-beta.3

Some useful function anchors from the stripped binary:

0x004d2720  tuya_ipc_init_sdk
0x004d29dc  tuya_ipc_start_sdk_generic
0x004d2bf4  tuya_ipc_start_sdk
0x004d8a48  tuya_ipc_mqtt_register_cb_init
0x0055e788  create_mqtt_hand
0x0055cbe4  mqtt_client_start
0x0059c2d4  mqtt_connect
0x00439ed8  gv_onvif_handler_init
0x0043a088  gv_onvif_handler_start
0x004eaa14  tuya_ipc_get_p2p_auth
0x004ea218  tuya_ipc_p2p_get_auth_param
0x004ea65c  tuya_ipc_p2p_get_pw
0x004ea8f4  tuya_ipc_p2p_get_lk
0x0056ac9c  GetEncryptorInstance

Those names were enough to map the broad architecture before fully decompiling everything: daemon starts/supervises, ipc runs the camera, and Tuya SDK code handles cloud, MQTT, P2P, and device configuration.

The writable configuration partition

The JFFS2 partition mounted as /conf was the most sensitive part of the dump. It contained:

/conf/factory.json
/conf/gv.json
/conf/static_ip.conf
/conf/tuya.json
/conf/tuya/tuya_user.db
/conf/tuya/tuya_user.db_bak
/conf/tuya/tuya_enckey.db

This area included device configuration and private values. I recovered my own old Wi-Fi configuration, ONVIF configuration, MAC addresses, static network settings, and Tuya-related data. Those values are not included here.

The categories looked like this:

previous Wi-Fi credentials: ********
ONVIF password:              ********
root password hash:          ********
MAC addresses:               ********
static network config:       ********
Tuya cloud/P2P/BLE records:  ********
local key-related data:      ********

No plaintext email addresses showed up in the recovered files.

The Tuya database was not a normal database

tuya_user.db was not SQLite, JSON, or XML. It was a custom binary key-value store.

The same directory also contained tuya_user.db_bak, the same size as the main database file. I treated it as a backup copy, not a separate format.

The file started with recognizable little-endian headers:

file_header_u32_le:
  0x10bc562d, 0x2, 0x1f, 0x701c

db_header_u32_le:
  0x55aa55aa, 0x200, 0x13, 0x1

The directory contained entries such as:

gw_bi
gw_di
cloud_enc_key
p2p_auth_info
p2p_pwd
ble_network_key
ble_beaconkey

The companion file tuya_enckey.db was exactly 16 bytes. I am not publishing the value, but its size and location make it interesting for the storage encryption path.

tuya_enckey.db: ********************************

At this stage I am comfortable saying the database stores sensitive Tuya configuration. I am not claiming a parser vulnerability or broken cryptography from the file names alone.

Following the config read path

The most useful early reverse-engineering anchor was GetEncryptorInstance(). Tracing around it showed this path:

GetEncryptorInstance()
  -> iot_wd_common_read()
  -> wd_common_read()
  -> kvs_read()
  -> simplekv_read()
  -> internal decode/read helper

GetEncryptorInstance() reads cloud_enc_key, checks that it has at least 16 bytes, copies 16 bytes into an internal buffer, and wires up AES-CBC-related functions. It also calls MD5 helper routines.

That was the point where cloud_enc_key became more than a string hit. It was part of a traced code path in the application.

What I can say from this dump

The strongest finding from this pass is simple:

physical access to the SPI flash can recover sensitive local configuration and Tuya/P2P/BLE-related records from this camera.

The evidence I used was:

  • /conf/tuya.json contains ONVIF password configuration;
  • /etc/shadow contains a root password hash;
  • /conf/tuya/tuya_user.db contains cloud, P2P, BLE, and identity-related records;
  • /conf/tuya/tuya_enckey.db is a 16-byte file associated with the device’s local Tuya storage and warrants further analysis;
  • ipc contains the Tuya SDK code paths that read and use these values.

This is a physical-access finding, not a remote exploitation claim. It matters for resale, repair, disposal, and anyone doing hardware-level threat modeling.

Ending where the evidence ends

The dump gave me a solid map of the camera:

  • how it boots;
  • where /app and /conf come from;
  • how daemon and ipc fit together;
  • which Tuya SDK version is present;
  • where the writable configuration lives;
  • which local files contain sensitive device state.

The most valuable part was not one magic string or one scary-looking filename. It was the chain: physical flash dump, verified hash, extracted filesystems, startup scripts, binaries, config database, and code paths that read those values.

That is the kind of firmware work I trust. Slow, a little messy, and backed by evidence instead of vibes.

End of field report

Published Aug 09, 2026#firmware · #iot-security · #hardware-hacking · #reverse-engineering · #ch341a · #tuya · #squashfs · #jffs2