Two devices, same corner of the shop. Both are the kind of thing you bolt to a wall, point at a driveway, and forget about for three years. One is a cheap PTZ WiFi camera off a local marketplace. The other is a TP-Link VIGI NVR. I pulled both apart at the firmware level expecting the usual budget-versus-brand gap, a bit more polish on the expensive one.

That is not what I found. I found two completely different ideas about what firmware security even is.

On the cheap camera the key was in the binary. On the TP-Link the key is in the silicon. One is a locked door with the key taped to the frame. The other is a lock welded into the wall, and you can stand there reading the brand name on it for as long as you like.

The two devices

The cheap one is an EasyTao / Tris Home PTZ WiFi camera. Inside: a GOKE GK7201V300 SoC, a Fudan FM25Q64 8MB SPI flash, a Sony IMX307 sensor. Firmware build dated 2025-05-10. It runs the XiongMai / XMEye platform, which is the same family lineage that fed Mirai. If you have read anything about IoT botnets, you have read about this platform’s relatives. The pedigree matters, because none of what follows is new. It has been documented for years and shipped anyway.

The real one is a TP-Link VIGI NVR1008H-8P(UN), hardware v1.20, firmware build 1.5.1 from March 2026. Inside: a SigmaStar SSD621, part of the Infinity series, which is rebranded MStar silicon, running a Linux 4.9.84 vendor kernel. Not exotic hardware. Mid-tier surveillance gear. That is the point. This is not a hardened military appliance, it is a normal product where someone made normal good decisions.

Side by side

Dimension EasyTao (XiongMai) TP-Link VIGI
SoC GOKE GK7201V300 SigmaStar SSD621 (Infinity / MStar)
Flash dump Full dump, unprotected SPI Full dump, but the payload is encrypted
Firmware signing None, anywhere in the boot chain RSA-2048 signed envelope
Rootfs protection None, plain SquashFS Hardware-AES encrypted, key in eFuse
Key extractability In the binary In the silicon, never in the file
UART Auto-login root shell, no auth Present, but no free root
Root password MD5crypt, cracked in ~27 seconds Not in anything I could pull statically
Static unpack binwalk and walk away Impossible, plaintext only lives on-device

Every row on the left is a door left open. Every row on the right is a door that holds. Now the walk-through.

What crap looks like

The EasyTao does not fail at one layer. It fails as a philosophy. Each thing you would normally have to work for is just handed to you, and the failures chain.

Start at the serial console. Solder onto the UART pads, set 115200, and you do not get a login prompt. You get a root shell. No password, no delay.

Auto login as root

~ # id
uid=0(root) gid=0(root) groups=0(root)

That “Auto login as root” is not me being cute, that is the device’s own banner. Pull the rootfs and look at inittab and it is spelled out: the getty line carries -n and drops you straight in as root. This is a deliberate configuration choice. Nobody leaves -n root and an auto-login banner in a shipping build by accident. Someone wanted a console they could fall into during manufacturing and never took it out.

You do not even need the UART, though. The SPI flash is completely unprotected. Clip a CH341A onto the FM25Q64 and read it with flashrom. I pulled it three times to be sure I was getting a clean, stable image and not bus noise.

flashrom -p ch341a_spi -r dump1.bin
flashrom -p ch341a_spi -r dump2.bin
flashrom -p ch341a_spi -r dump3.bin
sha256sum dump*.bin

Three identical SHA-256 hashes. Clean dump, no surprises, no read protection to fight. binwalk it and the SquashFS rootfs falls right out.

Inside that rootfs, the root password hash is MD5crypt, the $1$ prefix. In 2025. hashcat mode 500 against a standard wordlist ate it in about 27 seconds.

hashcat -m 500 shadow.hash rockyou.txt
# cracked in ~27s

So now I have the plaintext root password, on a device that already gave me a free root shell over UART. The password almost does not matter at that point, but the fact that it cracks in under half a minute tells you exactly how much thought went into the rest.

Then there is no firmware signing. None. Anywhere in the boot chain. rcS mounts what it finds and runs it. There is no verification step that could say “this image is not ours, stop.” That means a reflash attack is wide open. Write a modified image back to the flash you just dumped and the device will boot it without complaint, because nothing in the chain is checking.

And if opening the case is too much effort, the SD card path will do it for you. The platform supports an autorun script. Drop a file named xm_autorun.sh onto a FAT-formatted card, insert it, and the contents run as root on mount.

# xm_autorun.sh, sitting in the root of the SD card
#!/bin/sh
# whatever is in here executes as root when the card goes in

No soldering, no flash clip, no password. Physical access to the SD slot is full code execution as root. That is the entire attack.

The part that actually annoyed me was the “encryption.” The config layer uses a format they call XMFV1001, a JSON-ish blob that looks encrypted at a glance. It is obfuscation. The keys to undo it are embedded in the same binaries that ship on the device. It exists to make a casual look harder, not to stop anyone, and there is a real cost to that: it lets the vendor say the word “encrypted” while providing none of the property. That is security theatre in the literal sense.

For context, none of this stays on the device either. Registration to the cloud goes out in plaintext carrying admin tokens, certificate validation is broken, and the thing phones home constantly to cloud infrastructure you have no visibility into or control over. The local security being absent is bad. The local security being absent on something that chats to a remote service all day is worse.

Add it up and there is no single bug to report. The threat model is the bug. Every layer assumes whoever is touching the device is allowed to, which is the exact assumption an attacker is counting on.

What real looks like

The VIGI was a different afternoon entirely. Everything that was free on the camera, I had to earn here, and most of it I did not get.

First read of the firmware file through binwalk and the kernel comes out clean. A plain ARM zImage, no encryption on it at all.

binwalk firmware.bin
# ... Linux kernel ARM boot executable zImage ...

That single fact told me something useful before I had done any real work: the encryption was rootfs-only, not whole-payload. They left the kernel readable on purpose, because the kernel is not where the interesting secrets live. The rootfs is. So I went at the rootfs blob.

It read dead flat. I ran a sliding-window entropy pass across the whole thing, roughly 27MB, and it sat at 7.99 bits per byte the entire way with no structural boundaries anywhere.

binwalk -E rootfs.bin
# ~7.99 bits/byte, flat across the entire blob, no dips, no sections

That flatness is a fingerprint. Compressed data is high entropy too, but it has shape: headers, block boundaries, little dips where structure leaks through. This had none. A perfectly flat 7.99 across tens of megabytes is what stream-cipher output looks like, not what a compressed filesystem looks like.

Before I let myself say “AES,” I did the cheap check. A lot of budget gear hides behind a single-byte XOR and calls it encryption, and you feel pretty silly writing “strong crypto” in a report about something you could have undone with one pass. So I ran a single-byte XOR sweep looking for any known headers or any drop in entropy across all 256 keys. Zero hits. Nothing fell out. It was not XOR, and it was not anything that simple.

The kernel I had left readable is what gave the rest away. Strings out of it:

strings kernel.bin | grep -iE 'aes|sstar|infinity|efuse'
AESDMA
!sstar,infinity-aes
read eFuse timeout

There it is. The SigmaStar SSD621 has a hardware AES engine. At boot, the rootfs is decrypted in hardware by the AESDMA block, and the key is fed to it straight from an eFuse. The CPU never sees the key, and neither do I. The “read eFuse timeout” warning string is the tell that there is a one-time-programmable fuse bank in the loop, burned at the factory.

This is the whole difference in one design decision. The key is not stored in a file I can pull. It is not stored in flash I can clip onto. It is burned into the silicon and wired directly into the crypto engine. There is nothing in the firmware file to extract, because the plaintext rootfs does not exist in the firmware file. It only ever exists on the running device, in memory and at /dev/mtdblock6, after the hardware has done the decrypt. Static unpacking is not hard here, it is impossible. The artifact you would unpack is not in your hands.

And the payload is wrapped properly on top of that. RSA-2048 signed envelope, a 1024-byte TP-Link header, the actual payload starting at offset 0x400. So even the reflash trick that the camera waved through does not apply: a modified image fails the signature check before it ever boots.

Which left me with one realistic way forward: stop trying to crack the file and pivot to the device itself. Get on the UART, get far enough into the running system, and read the already-decrypted rootfs off /dev/mtdblock6 where the hardware has kindly done the work for me. That is a live-device attack with all the access requirements that implies. It is a completely different proposition from “clip a CH341A on and walk away,” which is exactly the point.

The takeaway

The cheap camera fails because somebody chose convenience and shipped it. The auto-login, the unprotected flash, the MD5crypt, the missing signing, the SD autorun, the fake encryption: every one of those is a decision that made the factory’s life a little easier and the attacker’s life a lot easier. There is no lock to pick because nobody fitted one.

The VIGI fails you slowly, and it keeps its secrets even after you escalate. It lets you read the kernel, then makes you prove the rootfs is really encrypted, then makes you rule out the lazy answer, then tells you the key is somewhere you fundamentally cannot reach, then forces you onto the live device, and even then the key stays in the fuse. Same product category. Same price-sensitive market. Wildly different respect for the idea that the person holding the device might not be a friend.

When people ask what “good firmware security” actually looks like in practice, this is the honest answer. It is not one clever feature. It is every layer assuming you are hostile and acting accordingly. One device taped the key to the frame. The other welded the lock to the wall and let me admire it.

The triage tooling I leaned on here, the sliding-window entropy check, the single-byte XOR sweep, and the crypto-string fingerprinting, is on GitHub: taffy210/fw-triage. No firmware in the repo, bring your own dump.