Skip to content

Firmware Extraction & Analysis

Firmware extraction and analysis is the discipline of turning a device’s flash image into an analyzable filesystem: acquisition (SPI dump from H1’s clip, OTA interception, vendor download, console dd), identification (what is this image — magic signatures, header parsing, entropy profiling), extraction (unpacking the embedded filesystems — squashfs, jffs2, cramfs, UBI, plus the compression layers around them), and triage (what’s inside worth finding: credentials, private keys, backdoor endpoints, update URLs, hardcoded secrets). The firmware image is the device’s source of truth — not its documentation, not its marketing — and analysis at the image level finds what no network assessment can: the hardcoded key every shipped unit shares, the debug endpoint nobody disabled, the default credential the manual doesn’t mention but the binary does.

Firmware analysis is the highest-leverage static discipline in hardware security because one extraction serves everything: the credentials it yields are estate-wide (every shipped unit shares the image), the backdoors it reveals are pre-installation, the crypto keys it exposes defeat the protocols they protect, and the update URLs it leaks become supply-chain pivot points (H10). The extraction itself matters as much as the analysis: an image you can’t unpack (encrypted, custom-packed) is the engagement’s first real puzzle, and the tools for solving it — entropy analysis to locate the encrypted regions, bootloader analysis to find the decryption routine, glitching to make the device decrypt for you (H7’s bridge) — are the difference between “we got the squashfs” and “we got nothing.” For defenders and product teams, firmware analysis is the pre-ship acceptance test: run your own extraction pipeline on your own image before an assessor does, and the findings (default creds, included private keys, debug services in the image, world-readable shadow files) are the design-review evidence that costs a build cycle pre-ship and a CVE post-ship. For the analyst, the craft compounds across devices: the filesystem types, the packing conventions, and the secret patterns repeat across vendors, so each extraction makes the next one faster — and the library of images (yours, versioned, diffed) becomes the assessment’s memory (the fwdiff workflow below).

The firmware image’s anatomy and the extraction pipeline:

FIRMWARE IMAGE ANATOMY (typical embedded Linux)
[bootloader: U-Boot] magic visible, config env at tail
[kernel: uImage/zImage] gzip/lzma-compressed vmlinux
[rootfs: squashfs/jffs2/ubifs/cramfs]
magic: hsqs (LE sqsh), jffs2 0x1985, ubi magic
[data partitions: configs, NVRAM, resources]
[padding/alignment, sometimes update headers/签名]
EXTRACTION PIPELINE
1. identify: binwalk / magic scan / entropy profile
2. extract: dd out the filesystem region
unsquashfs / jefferson / ubireader
3. triage: /etc (passwd, shadow, configs), certs,
keys, www/cgi binaries, init scripts
4. secrets: strings + regex for high-value patterns
5. cross-ref: keys found -> which protocol? (WI/H links)
ENTROPY PROFILE (the encryption detector)
sliding-window entropy across the image:
filesystem text ~4-6 bits/byte
compressed data ~7.5-7.99
ENCRYPTED data ~7.99 flat, no structure
flat 7.99 mid-image = encrypted region (find the key
holder: bootloader? external chip? H7 glitch path)

Acquisition ranks by cost: vendor download is free (update packages often contain full images, sometimes less-packed than the flash), OTA interception needs a MITM position on the update channel (H10’s surface, and often TLS — check the binary’s update client for its validation), console dd needs the root shell H1 found, and the SPI dump (H1) is the universal fallback that needs nothing but a clip. Identification starts with binwalk (the signature scanner — filesystem magics, compression headers, kernel images) and the entropy profile, which is the honest detector of encrypted regions: filesystem text sits at 4-6 bits/byte, compression at 7.5-7.99 with block structure, and encryption at flat 7.99 — the flat high-entropy region mid-image is the finding “encrypted rootfs”, and the next question is who holds the key. Extraction is per-filesystem: squashfs (unsquashfs, watching for non-standard compression options that vendor builds love — the classic “unsquashfs fails” puzzle), jffs2 (jefferson, which understands the NAND-specific structures), UBI (ubireader), and the plain cpio/tar layers inside update packages. Triage has a fixed checklist: /etc/passwd+shadow (hash classes present?), /etc/init.d (what starts at boot), web roots (cgi binaries = memory-corruption surface), SSL certs/keys (the device’s own identity — and private keys shipped in the image are the estate-wide finding), and the config systems (NVRAM defaults compiled into the binary). The secret-harvest regexes are the analyst’s fast wins: private key headers, password, secret, apikey, URLs with credentials, and the vendor’s own crypto constants (a hardcoded AES key found in strings and then observed in the binary’s crypto init is the finding that decrypts the device’s own protocol).

TechniqueWhat it doesToolingNotes
Vendor package downloadFree image acquisitionVendor support sitesOften less obfuscated
OTA interceptionUpdate-in-transit captureMITM + client analysisCheck update client’s TLS
SPI flash dumpUniversal acquisitionflashrom + CH341A (H1)Works with device off
Console ddDump from running systemRoot shell (H1/H4)Live-state snapshot
binwalk signature scanLocate embedded contentbinwalk -MeFirst pass, every image
Entropy profilingFind encrypted regionsThis module’s toolThe encryption detector
squashfs extractionRootfs unpackunsquashfsWatch custom compression
jffs2/ubi extractionNAND filesystemsjefferson/ubireaderNAND-specific structures
Secret regex harvestCreds/keys/URLsThis module’s toolThe fast wins
Firmware diffingVersion-over-version changefwdiff (below)Update analysis

fwanalyze.py — a firmware image triage analyzer (stdlib only): computes the sliding-window entropy profile (locating encrypted/compressed regions), scans for embedded-content magics (filesystems, compression, archives), harvests high-value strings with confidence-scored patterns (keys, creds, URLs), and emits the triage report that routes the image into extraction or crypto-analysis.

#!/usr/bin/env python3
"""fwanalyze.py — firmware image triage (entropy + magic + strings).
Usage:
python fwanalyze.py image.bin [--window 4096] [--json report.json]
stdlib only.
"""
import argparse, json, math, re, sys
from collections import Counter
MAGICS = [
(b"hsqs", "squashfs (LE)"), (b"sqsh", "squashfs (BE)"),
(b"\x19\x85", "jffs2"), (b"UBI#", "ubi"),
(b"\x1f\x8b", "gzip"), (b"\xfd7zXZ", "xz"),
(b"BZh", "bzip2"), (b"\x5d\x00\x00", "lzma"),
(b"PK\x03\x04", "zip"), (b"ustar", "tar"),
(b"\xd0\xcf\x11\xe0", "ole"),
(b"\x27\x05\x19\x56", "u-boot legacy image"),
(b"\xed\x26\x49\x42", "FIT image"),
(b"\x55\xaa", "mbr"), (b"\x89PNG", "png"),
]
SECRET_PATTERNS = [
(re.compile(rb"-----BEGIN (RSA |EC |DSA |OPENSSH )?"
rb"PRIVATE KEY-----"), 10, "private key"),
(re.compile(rb"\b(AKIA[0-9A-Z]{16})\b"), 10,
"aws access key"),
(re.compile(rb"(?i)\b(password|passwd|pwd)\s*[=:]\s*"
rb"['\"]?([!-~]{4,32})"), 6,
"password assignment"),
(re.compile(rb"(?i)\b(api[_-]?key|token|secret)\s*[=:]"
rb"\s*['\"]?([!-~]{8,48})"), 7,
"api key/secret"),
(re.compile(rb"https?://[!-~]{8,120}"), 2, "url"),
(re.compile(rb"\b(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?"
rb"\b"), 2, "ip:port"),
(re.compile(rb"-----BEGIN CERTIFICATE-----"), 3,
"certificate"),
(re.compile(rb"(?i)\broot:.*?:[0-9]*:[0-9]*:"), 8,
"shadow-like entry"),
]
def entropy(data):
if not data:
return 0.0
c = Counter(data)
n = len(data)
return -sum((v / n) * math.log2(v / n)
for v in c.values())
def entropy_profile(data, window):
"""Sliding-window entropy; classify regions."""
regions = []
step = window
for off in range(0, len(data) - window, step):
e = entropy(data[off:off + window])
cls = ("encrypted?" if e > 7.9 else
"compressed" if e > 7.2 else
"mixed" if e > 5.5 else "text-ish")
regions.append({"offset": off, "entropy":
round(e, 2), "class": cls})
return regions
def scan_magics(data):
hits = []
for magic, name in MAGICS:
off = 0
while True:
off = data.find(magic, off)
if off < 0:
break
hits.append({"offset": off, "what": name})
off += 1
if len(hits) > 200:
return hits
return hits
def harvest_strings(data, min_len=6):
"""Printable runs + pattern scoring."""
strs = re.findall(rb"[ -~]{%d,}" % min_len, data)
findings = []
for s in strs:
for rx, score, label in SECRET_PATTERNS:
m = rx.search(s)
if m:
snippet = m.group()[:80]
findings.append({"score": score,
"label": label,
"match": snippet.decode(
errors="replace")})
# dedupe by (label, match)
seen = set()
out = []
for f in sorted(findings, key=lambda f: -f["score"]):
key = (f["label"], f["match"])
if key not in seen:
seen.add(key)
out.append(f)
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("image")
ap.add_argument("--window", type=int, default=4096)
ap.add_argument("--json")
args = ap.parse_args()
try:
data = open(args.image, "rb").read()
except OSError as e:
sys.exit(f"read error: {e}")
print(f"== {args.image}: {len(data)} bytes ==")
hits = scan_magics(data)
print(f"\n[embedded content] {len(hits)} magic hits:")
shown = set()
for h in hits:
key = (h["what"])
if key in shown and h["what"] not in (
"gzip", "zip"):
continue
shown.add(key)
print(f" 0x{h['offset']:08x} {h['what']}")
regions = entropy_profile(data, args.window)
enc = [r for r in regions if r["class"] == "encrypted?"]
comp = [r for r in regions if r["class"] == "compressed"]
print(f"\n[entropy] window={args.window}: "
f"{len(enc)} encrypted-class windows, "
f"{len(comp)} compressed")
if enc:
runs = []
start = enc[0]["offset"]
prev = start
for r in enc[1:]:
if r["offset"] - prev > args.window * 2:
runs.append((start, prev))
start = r["offset"]
prev = r["offset"]
runs.append((start, prev))
for s, e in runs[:6]:
print(f" encrypted region ~0x{s:08x}-"
f"0x{e:08x} ({(e-s)//1024} KB) "
f"— find the key holder")
findings = harvest_strings(data)
print(f"\n[secret harvest] {len(findings)} scored "
f"matches (top 15):")
for f in findings[:15]:
print(f" [{f['score']:>2}] {f['label']:<18} "
f"{f['match'][:60]}")
if args.json:
json.dump({"size": len(data), "magics": hits[:50],
"regions": regions[:200],
"findings": findings[:100]},
open(args.json, "w"), indent=2)
print(f"[+] {args.json}")
if __name__ == "__main__":
main()
  • entropy_profile() (lines 40-52) is the encryption detector: non-overlapping windows classified by threshold — >7.9 flags “encrypted?” (flat randomness), 7.2-7.9 “compressed” (high entropy but the block structure differs in practice), 5.5-7.2 “mixed”. The thresholds are heuristics with a report that says so; the runs aggregation in main (lines 100-112) merges adjacent encrypted windows into regions — the readable output that says “here is the ciphertext, go find the key holder.”
  • scan_magics() (lines 55-68) walks the magic table with overlap (offset+1 rescan catches nested content — gzip inside uImage inside header), capped at 200 hits. The dedup in main (lines 88-93) keeps the human output readable while the JSON keeps everything.
  • harvest_strings() (lines 71-92) scores printable runs against the secret-pattern table: private keys and AWS keys score 10, api-token shapes 7, password assignments 6, shadow-like entries 8, URLs/IPs 2 (context, not secrets). The dedupe (lines 88-92) is by (label, match) — one finding per unique match, sorted by score.
  • The pattern table (lines 24-36) is the analyst’s maintained asset: every engagement adds patterns (vendor-specific key shapes, custom config markers), and the table’s position at module top makes that edit the one-line change it should be.

The image reaches the analyst through the four acquisition paths ranked by friction — vendor download (a support page, often the whole rootfs in an update package), OTA interception (a MITM position and the update client’s validation review), console dd (H1’s shell), and the SPI clip (H1’s universal fallback). The analysis reaches the assessment as the image’s truth: the entropy profile says where the secrets hide (flat-7.99 region = encrypted rootfs → the bootloader holds the key → the bootlog string or the glitch path), the magic scan says what’s unpackable, and the secret harvest yields the first findings in minutes — hardcoded credentials, private keys shipped in the image, update endpoints with embedded basic-auth. Those findings reach the estate widely: every shipped unit shares the image, so a hardcoded key is every device’s key, a backdoor endpoint is every device’s backdoor — the firmware finding is never “one device’s problem.” The extracted filesystem reaches H3/H4: static RE of the web binaries (memory-corruption surface), the init scripts (what starts, what listens), and the emulation plan (run the rootfs in QEMU on the bench). The update-package path reaches H10: the vendor’s signing posture, the update client’s TLS validation, and the supply-chain surface all live in the same analysis. For the defender, the same pipeline is the pre-ship acceptance test: run it on your own image and the findings are the release checklist — no private keys in images, no default creds, no debug services, entropy profile matching the design (if you claim encrypted rootfs, the profile must show it), each fix verifiable by re-running the analyzer on the next build.

  1. Acquire — vendor support site: firmware zip for the target model; or SPI clip dump from H1 bench
  2. Identify — fwanalyze.py: squashfs magic at 0x1A0000, gzip kernel, one flat-7.99 region at 0x3F0000
  3. Extract — dd the squashfs region; unsquashfs fails (custom compaction); binwalk -Me succeeds via alternate offset
  4. Triage — /etc/shadow has one root hash; /etc/init.d starts telnetd on LAN; web root has 4 cgi binaries
  5. Harvest — secret harvest: RSA private key in /etc/certs/, hardcoded admin:secret123 in www binary
  6. Correlate — private key = the device’s HTTPS identity: estate-wide impersonation possible
  7. Validate — key + cred confirmed on lab device: admin session established (H4 chain)
  8. Report — image-level findings (estate-wide scope), remediation: secrets out of image, per-unit identity, telnetd removal

fwdiff.py — the firmware version differ: extracts the filesystems from two images (delegating to system unsquashfs/jefferson once fwanalyze located them), diffs the extracted trees (new/changed/removed files with hashes), highlights config and binary changes, and emits the update-analysis report — the tool that answers “what did this firmware update actually change?”

  • Extract once, diff many. The tool consumes extracted directories (or extracts via system tools into a cache dir keyed by image hash) — re-extraction on every diff wastes the expensive step and cache keys make version chains cheap.
  • Hash-tree diff, not text diff. Per-file SHA-256 comparison finds changed binaries; text diffing is offered per-file for configs only — binaries lie to text diffs.
  • Risk-ranked output. New SUID files, changed init scripts, new setuid binaries, modified auth files rank first — the update-diff report’s headline is the security-relevant subset, with the full diff attached.
  • Vendor-package aware. Update packages often carry deltas or re-packed images; the tool accepts either extracted trees or raw images and normalizes — the assessor’s input flexibility.
#!/usr/bin/env python3
"""fwdiff.py — firmware filesystem differ (two extracted trees).
Usage:
python fwdiff.py old_root/ new_root/ [--out diff.md]
python fwdiff.py --extract old.bin new.bin --cachedir cache/
Trees: extracted rootfs dirs. stdlib only (extraction shells
out to system unsquashfs/jefferson/ubireader when --extract).
"""
import argparse, hashlib, json, os, subprocess, sys
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def scan_tree(root):
"""path -> sha256 (+ size, mode bits)."""
files = {}
for dirpath, _, names in os.walk(root):
for n in names:
p = os.path.join(dirpath, n)
rel = os.path.relpath(p, root)
try:
st = os.stat(p)
files[rel] = {
"sha": sha256(p) if st.st_size < 32<<20
else "big",
"size": st.st_size,
"mode": oct(st.st_mode & 0o7777)}
except OSError:
files[rel] = {"sha": "unreadable",
"size": 0, "mode": "?"}
return files
def extract_if_image(path, cachedir):
"""If path is a file (image), extract to cache dir."""
if os.path.isdir(path):
return path
os.makedirs(cachedir, exist_ok=True)
key = sha256(path)[:16]
out = os.path.join(cachedir, key)
if os.path.isdir(out):
return out
# try binwalk -Me as the universal extractor
r = subprocess.run(["binwalk", "-Me", "--directory",
out, path],
capture_output=True, text=True)
if r.returncode != 0:
sys.exit(f"extraction failed for {path}: "
f"{(r.stderr or r.stdout)[-300:]}")
# find the largest extracted dir = the rootfs candidate
best, best_n = None, -1
for dirpath, dirnames, _ in os.walk(out):
n = sum(len(f) for _, _, f in os.walk(dirpath))
if n > best_n:
best, best_n = dirpath, n
return best or out
def diff_trees(old, new):
fo, fn = scan_tree(old), scan_tree(new)
added = sorted(set(fn) - set(fo))
removed = sorted(set(fo) - set(fn))
changed = sorted(k for k in set(fo) & set(fn)
if fo[k]["sha"] != fn[k]["sha"])
risky = []
for k in added:
if "0400" < fn[k]["mode"].replace("o", "") or \
int(fn[k]["mode"], 8) & 0o4000:
risky.append(("new-suid", k))
for k in changed:
base = os.path.basename(k)
if base in ("shadow", "passwd", "sudoers") or \
k.startswith("etc/init.d/"):
risky.append(("auth-changed", k))
return {"added": added, "removed": removed,
"changed": changed, "risky": risky}
def render(d, old, new):
lines = [f"# Firmware diff: {os.path.basename(old)} "
f"-> {os.path.basename(new)}", "",
f"- added: {len(d['added'])}, "
f"removed: {len(d['removed'])}, "
f"changed: {len(d['changed'])}", ""]
if d["risky"]:
lines += ["## Security-relevant changes", ""]
for kind, k in d["risky"][:25]:
lines.append(f"- [{kind}] {k}")
lines.append("")
lines += ["## Added (first 30)", ""]
for k in d["added"][:30]:
lines.append(f"- {k}")
lines += ["", "## Changed (first 30)", ""]
for k in d["changed"][:30]:
lines.append(f"- {k}")
lines += ["", "## Removed (first 30)", ""]
for k in d["removed"][:30]:
lines.append(f"- {k}")
return "\n".join(lines) + "\n"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("old")
ap.add_argument("new")
ap.add_argument("--extract", action="store_true",
help="inputs are images; extract first")
ap.add_argument("--cachedir", default="fw_cache")
ap.add_argument("--out")
args = ap.parse_args()
if args.extract:
old = extract_if_image(args.old, args.cachedir)
new = extract_if_image(args.new, args.cachedir)
else:
old, new = args.old, args.new
for t in (old, new):
if not os.path.isdir(t):
sys.exit(f"not a directory: {t}")
d = diff_trees(old, new)
text = render(d, old, new)
print(text)
if args.out:
open(args.out, "w").write(text)
print(f"[+] {args.out}")
if __name__ == "__main__":
main()
  • extract_if_image() (lines 26-42) implements the cache discipline: image hash → cache key → extract once via binwalk -Me (the universal extractor), then pick the largest extracted tree as the rootfs candidate — the heuristic that works because rootfs dominates every image, stated in the comment rather than pretended.
  • diff_trees() (lines 45-66) computes the three sets (added/removed/changed) from hash comparison, then the risk-ranked overlay: new SUID-bearing files (mode 0o4000/0o2000 check), changed auth files and init scripts — the security-relevant subset that headlines the report.
  • The scan_tree size guard (line 38): files above 32 MB hash as "big" — a pragmatic bound that keeps the diff fast on giant binaries while still comparing existence and metadata.
  • The render split (lines 69-95): risky changes first, then the three raw sets capped at 30 rows — the report a reviewer reads in two minutes with the full data in the JSON.

triage the vendor image from the H1 SPI dump

Section titled “triage the vendor image from the H1 SPI dump”

$ python fwanalyze.py fw_dump.bin —window 4096 —json report.json == fw_dump.bin: 16777216 bytes ==

[embedded content] 41 magic hits: 0x00110000 u-boot legacy image 0x00120000 gzip 0x001a0000 squashfs (LE) 0x003f0000 jffs2

[entropy] window=4096: 2 encrypted-class windows, 41 compressed encrypted region ~0x003f0000-0x0044a000 (352 KB) — find the key holder

[secret harvest] 23 scored matches (top 15): [10] private key -----BEGIN RSA PRIVATE KEY----- [ 8] shadow-like entry root:$1$abcdefg…:0:0:99999:7::: [ 7] api key/secret upgrade_token=9f2ab1c4d5e6f7081122 [ 6] password assign. admin_passwd=secret123

$ binwalk -Me fw_v1.bin -C v1out && binwalk -Me fw_v2.bin -C v2out $ python fwdiff.py v1out/squashfs-root v2out/squashfs-root
—out diff.md

  • added: 14, removed: 2, changed: 31
  • [auth-changed] etc/init.d/rcS
  • [new-suid] usr/bin/upgraded

Firmware-layer defense is build-time: secrets out of the image (per-unit provisioning at manufacture or first-boot generation), filesystems encrypted (so the dump yields ciphertext — and the entropy profile becomes the acceptance evidence), signed updates (H10’s supply-chain control), and the extraction pipeline run against every release build as CI — the binwalk/fwanalyze pass gating the release the same way unit tests gate code. The assessment of your own image pre-ship is the acceptance test that catches the eternal findings (default creds, shipped private keys, telnetd) while they cost a build cycle instead of a CVE. Your own tooling hygiene: everything here reads files and writes reports — the only risky step is running extracted binaries, which belongs to H3’s emulation sandbox, never to the analysis bench. Wazuh/SIEM has no layer here; the “detection” is the CI gate and the design review. The standing metric for product teams: findings-per-image trend across releases (target: zero secret-class findings for N consecutive releases), and the image’s entropy profile matching the documented design — two checks that keep the firmware estate honest as it evolves.

  1. Kernel config extraction: parse IKCONFIG from kernel images (if embedded) — hardening facts (module signing? kptr restrict?) without booting.
  2. NVRAM default extraction: parse vendor NVRAM-default blobs compiled into binaries (the credentials live there as often as in /etc). 3.entropy-region key-holder tracing: correlate encrypted regions with bootloader disassembly references (bridge to H3/H7).
  3. UBI/UBIFS deep support: full volume-table parsing rather than delegating to ubireader.
  4. CI gate mode: --fail-on secret exit-code semantics for release pipelines.
SignalSourceIDNotes
Secret-class findings in imagefwanalyze CI gaterelease checklistPrivate keys, creds, tokens
Entropy profile vs design claimfwanalyzeacceptance test“Encrypted rootfs” verified
Extraction success of release buildbinwalk gaterelease checklistShould require keys
Default creds in imagefwanalyze harvesteternal findingEstate-wide scope
Debug services in rootfsTree triagedesign reviewtelnetd/classic
SUID inventory changesfwdiffupdate reviewUpdate-diff governance
Update package signature posturePackage analysissupply chainH10 bridge
OTA client TLS validationClient binary reviewdesign reviewInterception surface
Findings-per-image trendProgram metricper releaseZero-secret streak target
Design-review acceptance rateH12 program metricper product genThe lifecycle metric

The firmware-hiding race runs between vendor packing and analyst extraction. Custom filesystem packing: non-standard squashfs compression options, custom headers, XOR-mangled magics — defeats binwalk’s signature pass; countered by entropy profiling (the structure shows regardless of magic), by header-diffing between images (the vendor’s own updates reveal their packing), and by binwalk’s heuristic depth. Encryption of the rootfs: the real defense — countered by key-holder analysis (the bootloader must decrypt; its code or its eFuses hold the key), by RAM dumping post-boot (decrypted content in memory, via JTAG per H1), and by glitching the boot-time check (H7 — the escalation the encryption justifies). Update-package obfuscation: encrypted deltas, vendor-proprietary containers — countered by analyzing the updater binary (it must parse the package; its parsing code is the format documentation), and by the OTA interception that captures the post-decryption image. Secret-splitting: credentials computed from device-unique values at boot (MAC-derived, serial-derived) — countered by RE of the derivation (H3) and by the honest classification that this is better (per-unit secrets) but only as strong as the derivation’s inputs. Image-signing without encryption: signatures verify integrity but not secrecy — countered by recognizing that signing protects the supply chain, not the analysis, and auditing which claim each control actually makes. The meta-pattern: every hiding layer must be undone by the device itself at boot, so the device is always its own decryption oracle — and the analysis question is never “is it decryptable” but “where does the decryption happen and can I reach that place” (bench RAM dump, glitch, or the honest report that it can’t be reached non-destructively).

⚠️ What caught me: I extracted a vendor’s rootfs and found the squashfs “encrypted” — flat 7.99 entropy, no magic, nothing. I spent two full days on bootloader RE looking for the decryption routine before an offhand line in the vendor’s changelog (“improved compression”) made me re-check: it wasn’t encryption at all, it was a double-compressed squashfs (xz inside gzip inside a custom header) whose entropy profile mimicked encryption perfectly. Two days for one changelog line. Since then, the encrypted-region finding carries a mandatory pre-check — try decompressing the region with every common algorithm at every offset before crying “encrypted” — and the report distinguishes “encrypted (verified)” from “high-entropy (unexplained)”. The difference between those two phrases is two days of somebody’s life.

  1. Dump and triage: take your H1 dev board’s SPI dump through fwanalyze.py — entropy profile, magic scan, secret harvest — and reconcile every finding against the board’s actual config (ground truth you control).
  2. Extraction ladder: extract the rootfs (unsquashfs/binwalk), then walk the triage checklist (/etc, init.d, www, certs) and list every finding that a network assessment could not have found.
  3. Encrypted-region drill: create a deliberately-encrypted partition on your lab image, run the analyzer, and confirm the flat-7.99 detection — then implement the decompression pre-check and verify it doesn’t misfire on compressed-but-unencrypted data.
  4. Version diff: “upgrade” your lab image (change a config, add a binary), rebuild, and run fwdiff.py — the update-diff workflow validated against known changes.
  5. Secret-cleanup drill: take your findings list, remove each secret from the image (regenerate per-unit, move to provisioned storage), rebuild, re-run the analyzer, and confirm zero secret-class findings — the CI gate, exercised by hand.
  • Technical: Walk the firmware extraction pipeline — acquisition options, identification methods, extraction per filesystem type, and the triage checklist.
  • Technical: How does the sliding-window entropy profile distinguish text, compression, and encryption — and what false positives does compression create?
  • Scenario: Your analyzer flags a 352 KB flat-entropy region mid-image. What is the diagnostic sequence before you call it encrypted?
  • Conceptual: Why is a hardcoded key a more severe finding than a hardcoded password, and how does estate-wide scope change the remediation math?
  • Conceptual: How do image signing and image encryption differ in the claims they make — and which supply-chain risk does each actually address?
  • Deep-dive: Design the firmware CI gate: which checks, which fail conditions, how findings route to engineering, and what trend the release program tracks.
  • Practical: The image yields the device’s private HTTPS key. What is the immediate risk story, the estate-wide scope, and the remediation path?

H1 (Hardware Hacking Fundamentals (UART/JTAG/SPI/I2C)) · H3 (Firmware Analysis & Emulation) · H4 (Embedded Linux & Root Shells) · F2 (Linux Internals) · M4 (Unpacking and Deobfuscation)