Firmware Analysis & Emulation
Definition
Section titled “Definition”Firmware analysis and emulation covers the two disciplines that make extracted firmware answer questions: static reverse engineering (loading the extracted binaries into disassemblers — Ghidra/IDA/radare2 — reading the decompiled logic, finding the hardcoded secrets’ usage, the command handlers, the vulnerability classes) and dynamic emulation (running the firmware’s binaries or whole system on your bench via QEMU — user-mode emulation for single binaries under chroot, full-system emulation for booting the actual kernel and rootfs — so the device’s services can be probed, traced, and fuzzed without the hardware). The craft’s center of gravity is the bridge between the two: static analysis tells you what to look for dynamically (the nvram reads, the socket binds, the auth checks), and dynamic runs tell you what to read statically (the crash traces, the reachable paths, the actual config values). Emulation exists because hardware is the bottleneck — one device, one serial port, endless reflashing — while QEMU runs ten instances, snapshots, and rewinds.
Why It Matters
Section titled “Why It Matters”The extracted filesystem (H2) is a pile of binaries until analysis makes it speak, and the speaking happens two ways. Static RE answers the questions the strings raised: the hardcoded credential’s usage (is it a backdoor or a leftover?), the update client’s certificate validation (H10’s supply-chain question), the web binary’s command handlers (the memory-corruption surface), the crypto routine’s key derivation (the “encrypted” region’s key holder). Dynamic emulation answers the questions static can’t: does this binary actually run outside the device, what does it read from NVRAM, which syscalls does it make, does the vulnerability class in the handler actually crash. Emulation is also the scale tool: one extracted rootfs becomes a farm of emulated instances for fuzzing, and FirmAE-class frameworks automate the tedious half (NVRAM shims, network bring-up) that used to eat days per device. For defenders and product teams, the same analysis is the pre-ship review: RE your own binaries for the vulnerability classes your languages invite (strcpy in cgi, system() with format strings), harden per the ELF posture scan (NX/PIE/canary — the elfscan tool below), and run the emulated services in CI so the regression tests exercise the shipped code, not a mock. For the analyst, the disciplines compound: every architecture learned (ARM/ARM64/MIPS/PowerPC) multiplies the estate of devices you can analyze, and the emulation patterns (user-mode chroot, full-system, NVRAM shims) repeat across vendors — the second device costs a quarter of the first.
Detailed Explanation
Section titled “Detailed Explanation”The analysis pipeline from extracted tree to running service:
FROM EXTRACTED TREE TO EMULATED SERVICE [1] ARCH IDENTIFICATION readelf -h on binaries: ARM / MIPS / aarch64 (endian matters: MIPSel vs MIPS — readelf says) [2] USER-MODE EMULATION (single binary, fast) cp qemu-arm-static usr/bin/ (binfmt or explicit) chroot . qemu-arm-static ./usr/sbin/httpd -L . sets the interpreter prefix for libs + NVRAM shim: LD_PRELOAD stub answering nvram_get() [3] FULL-SYSTEM EMULATION (whole boot, slowest, fullest) qemu-system-arm -M virt/vexpress -kernel zImage -drive rootfs -append console=ttyAMA0 networking: -netdev tap/hostfwd to reach services FirmAE-class: automates nvram + network bring-up [4] STATIC RE (the reading) Ghidra/radare2: entry -> main -> handlers follow: strings->xrefs, nvram_get calls, socket/bind/listen, system()/strcpy() [5] DYNAMIC OBSERVATION strace/ltrace (syscalls/libs), gdbserver remote crash triage -> static RE of the crashing path
THE NVRAM PROBLEM (the classic emulation blocker) embedded binaries read config via nvram_get("var") -> vendor kernel driver -> flash emulation shims: LD_PRELOAD libnvram.so answering defaults, or /dev/nvram FUSE, or FirmAE's kernel patchStatic RE’s workflow is a funnel: identify architecture (ELF header), load into Ghidra (auto-analysis finds functions; the MIPS/ARM decompilers are good enough to read), and drive the reading from the strings — the hardcoded credential from H2 has an xref, the xref lands in a function, and the function’s context (a disabled-by-default telnet handler? a recovery-mode backdoor?) is the finding. The vulnerability classes repeat in embedded web binaries: system()/popen() with user-controlled format (command injection, W8’s cousin), strcpy/sprintf into stack buffers (the overflow class), authentication checks that compare a return value from a function whose failure returns 0 (the logic bug). User-mode emulation is the fast path: copy the matching qemu-arm-static into the extracted tree’s usr/bin, chroot, run — with two blockers that define the craft: the NVRAM problem (vendor binaries read config via nvram_get() calls into a kernel driver you don’t have — the LD_PRELOAD shim answering sane defaults is the standard fix, FirmAE automates it) and the library problem (the binary needs its own libc — the -L prefix and the chroot handle it). Full-system emulation is the thorough path: boot the actual kernel with the actual rootfs under QEMU’s machine model — the device’s real services come up, network bridging (hostfwd maps host ports to guest) exposes them to your testing tools, and snapshots let you rewind after every crash. The honest limits belong in every report: hardware-dependent code (GPIO, DSP, radio calibration) fails outside the device, some kernels refuse QEMU’s machine model, and the emulation proves the software’s behavior — the hardware attack surface (H1/H6/H7) still needs the board.
Common Techniques
Section titled “Common Techniques”| Technique | What it does | Tooling | Notes |
|---|---|---|---|
| Architecture identification | ELF header parse | readelf, this module’s tool | Endian + arch = QEMU flavor |
| User-mode chroot emulation | Single binary runs | qemu-arm-static + chroot | Fast, partial |
| NVRAM shim | Config reads answered | LD_PRELOAD libnvram, FirmAE | The classic blocker |
| Full-system boot | Kernel + rootfs run | qemu-system-*, FirmAE | Fullest fidelity |
| Network bridging | Reach emulated services | hostfwd / tap | Port mapping |
| Static RE funnel | Strings → xrefs → functions | Ghidra, radare2, IDA | The reading |
| Syscall tracing | Behavior observation | qemu -strace, strace | Dynamic truth |
| Remote debugging | Crash triage | gdbserver + gdb-multiarch | Cross-arch debugging |
| ELF posture scan | Hardening inventory | elfscan (below) | NX/PIE/canary/RELRO |
| Service fuzzing | Vulnerability classes | boofuzz/ customized | Post-emulation |
Full Working Code
Section titled “Full Working Code”emuguide.py — the emulation environment generator: scans an extracted rootfs, classifies every ELF binary’s architecture (stdlib ELF header parsing), detects the dominant architecture, counts the network-facing binaries (from strings and init scripts), and generates the exact QEMU command runbook — user-mode chroot lines per binary, a full-system command from the detected kernel/arch, and the NVRAM shim warning — as a markdown runbook. stdlib only.
#!/usr/bin/env python3"""emuguide.py — rootfs emulation runbook generator.Usage: python emuguide.py squashfs-root/ [--out runbook.md]Parses ELF headers (stdlib), classifies arch, generates QEMUuser-mode + full-system command runbook. stdlib only."""import argparse, json, os, struct, sys
ARCH = { (0x03, True): ("arm", "qemu-arm-static", "arm"), (0x03, False): ("armeb", "qemu-armeb-static", "arm"), (0x08, True): ("mips", "qemu-mips-static", "mips"), (0x08, False): ("mipsel", "qemu-mipsel-static", "mips"), (0xb7, True): ("aarch64", "qemu-aarch64-static", "aarch64"), (0x3e, True): ("x86_64", "qemu-x86_64-static", "x86_64"),}
def is_elf(path): try: with open(path, "rb") as fh: return fh.read(4) == b"\x7fELF" except OSError: return False
def elf_arch(path): """Return (arch, class, endian, dynamic) from header.""" with open(path, "rb") as fh: h = fh.read(20) if h[:4] != b"\x7fELF" or len(h) < 20: return None ei_class, ei_data = h[4], h[5] e_type, e_machine = struct.unpack( "<HH", h[16:20]) if ei_data == 1 else \ struct.unpack(">HH", h[16:20]) little = ei_data == 1 info = ARCH.get((e_machine, little)) dynamic = False try: with open(path, "rb") as fh: blob = fh.read() dynamic = b"ld-linux" in blob or \ b"ld-uClibc" in blob or \ b"ld-musl" in blob except OSError: pass return {"arch": info[0] if info else f"machine-{e_machine}", "qemu": info[1] if info else None, "sys": info[2] if info else None, "class": "64" if ei_class == 2 else "32", "endian": "le" if little else "be", "dynamic": dynamic}
def scan_rootfs(root): """Classify every ELF; return arch census + binaries.""" census, bins = {}, [] for dirpath, _, names in os.walk(root): for n in names: p = os.path.join(dirpath, n) if not is_elf(p): continue info = elf_arch(p) if not info: continue census[info["arch"]] = \ census.get(info["arch"], 0) + 1 rel = os.path.relpath(p, root) bins.append({"rel": rel, **info}) return census, bins
def net_facing(root, bins): """Binaries referencing sockets/binds = service surface.""" out = [] for b in bins: p = os.path.join(root, b["rel"]) try: blob = open(p, "rb").read() except OSError: continue if b"socket" in blob and (b"bind" in blob or b"listen" in blob): out.append(b) return out
def render(root, census, bins, net): if not census: return "No ELF binaries found — not a Linux rootfs?" dominant = max(census, key=census.get) qemu_bin = next((b["qemu"] for b in bins if b["arch"] == dominant and b["qemu"]), None) L = ["# Emulation runbook", "", f"rootfs: `{root}`", "", f"## Architecture census", ""] for a, n in sorted(census.items(), key=lambda kv: -kv[1]): L.append(f"- {a}: {n} binaries") L += ["", f"dominant arch: **{dominant}**", ""] TICKS = chr(96) * 3 L += ["## User-mode emulation (per binary)", "", TICKS + "bash"] if qemu_bin: L.append(f"# copy the static qemu into the tree") L.append(f"cp $(which {qemu_bin}) " f"{root}/usr/bin/ 2>/dev/null || true") for b in net[:5]: L.append( f"chroot {root} /usr/bin/{qemu_bin} " f"-L {root} /{b['rel']}") else: L.append("# qemu user binary for dominant arch " "not detected — install qemu-user-static") L.append(TICKS) L += ["", "## Full-system emulation (dominant arch)", "", TICKS + "bash"] syscmd = { "arm": ("qemu-system-arm -M virt -cpu cortex-a15 " "-kernel zImage -drive " "if=none,file=rootfs.img,format=raw,id=hd0 " "-device virtio-blk,drive=hd0 -append " "'root=/dev/vda console=ttyAMA0' -nographic " "-netdev user,id=n0,hostfwd=tcp::8080-:80 " "-device virtio-net,netdev=n0"), "mips": ("# mips: Malta/fulong machine models; " "kernel compatibility varies — see FirmAE"), "aarch64": ("qemu-system-aarch64 -M virt -cpu " "cortex-a53 -kernel Image ..."), } L.append(syscmd.get(dominant, "# unknown dominant arch")) L.append(TICKS) L += ["", "## NVRAM warning", "", "vendor binaries likely call nvram_get() against " "a kernel driver that won't exist under QEMU. " "Use an LD_PRELOAD nvram shim or FirmAE " "(automates shims + networking).", ""] L += ["## Network-facing binaries (service surface)", ""] for b in net[:15]: L.append(f"- `{b['rel']}` ({b['arch']}, " f"{'dynamic' if b['dynamic'] else 'static'})") return "\n".join(L) + "\n"
def main(): ap = argparse.ArgumentParser() ap.add_argument("rootfs") ap.add_argument("--out") ap.add_argument("--json") args = ap.parse_args() if not os.path.isdir(args.rootfs): sys.exit(f"not a directory: {args.rootfs}") census, bins = scan_rootfs(args.rootfs) net = net_facing(args.rootfs, bins) text = render(args.rootfs, census, bins, net) print(text) if args.out: open(args.out, "w").write(text) print(f"[+] {args.out}", file=sys.stderr) if args.json: json.dump({"census": census, "network_facing": [b["rel"] for b in net]}, open(args.json, "w"), indent=2)
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”elf_arch()(lines 26-47) parses the ELF header with stdlib struct: machine type + endianness map to the QEMU flavor (the endian distinction matters enormously —qemu-mipsvsqemu-mipselis the classic wrong-tool error), and the dynamic-linker string check (ld-linux/ld-uClibc/ld-musl) classifies static vs dynamic — which determines whether user-mode emulation needs the-Llibrary prefix.- The census + service surface (lines 50-70): every ELF classified (the census tells you if the rootfs is mixed-arch — common in vendor images with DSP blobs), and the network-facing filter (socket+bind/listen strings) produces the service surface — the binaries whose emulation matters for probing.
- The runbook generation (lines 73-113) emits the exact commands: the static-QEMU copy, per-binary chroot lines for the top-5 service binaries, the full-system command for the dominant architecture (with the honest per-arch caveats — MIPS machine models are their own project), and the NVRAM warning that every first-time emulator hits.
- The dominant-arch selection (line 76): max-count census entry — vendor trees are usually single-arch with blobs; when they’re not, the runbook names the majority and the census names the exceptions.
How It Reaches The Target
Section titled “How It Reaches The Target”The extracted tree reaches the analyst’s bench from H2, and this module’s tools turn it into answers. The architecture census reaches the runbook: which QEMU flavors, which binaries, which service surface — the emulation plan in one command. The user-mode emulation reaches the single-binary questions: does the update client run, what does it read, does it validate TLS (H10’s question answered dynamically). The NVRAM shim reaches the first real blocker and teaches the pattern: vendor config reads are shims until the real driver exists — and the shim’s defaults are themselves findings (what does the binary do when NVRAM says the factory value?). The full-system boot reaches the service-level assessment: the device’s actual web UI, telnetd, and services running under QEMU with host-forwarded ports — probe with the same tools you’d use on the network (W-series for the web UI, PE-series for the shell), snapshot between tests, rewind after crashes. The static RE reaches the questions strings raised: the credential’s xref, the command handler’s injection sink, the crypto routine’s key — each answer either a finding or the pointer to the dynamic test that proves it. For the defender, the same pipeline is the product team’s: ELF posture scans (the elfscan below) in CI, emulated-service smoke tests per build, and RE review of the binaries that face the network — the review that finds the system(buf) while it’s a build-cycle fix. And the emulation farm reaches the fuzzing program: N instances of the emulated service, a generation-based fuzzer against the CGI handlers, crashes triaged by remote gdb — the scale that one physical device can never give.
Exploitation Chain
Section titled “Exploitation Chain”- Setup — emuguide.py on H2’s extracted rootfs: mipsel dominant, 6 network-facing binaries
- Shim — LD_PRELOAD nvram shim built with defaults from /etc; chroot qemu-mipsel runs lighttpd
- Reach — hostfwd maps guest:80 to bench:8080; device web UI alive in browser
- Probe — login form tested; hardcoded cred from H2 harvest grants admin on emulated AND real device
- RE — Ghidra on the cgi binary: command handler passes ping= param to system() unfiltered
- Confirm — injection payload against emulated service: id executed; crashless command confirmed
- Hardware — same payload against physical device (authorized): root shell via web injection
- Report — emulated-to-hardware finding chain; remediation: sanitization, hardening flags (elfscan gate)
Building Your Own Tool
Section titled “Building Your Own Tool”elfscan.py — the ELF hardening-posture scanner: walks a rootfs (or a single binary), parses ELF headers and dynamic sections (stdlib struct parsing), and reports per-binary and aggregate security posture: NX (PT_GNU_STACK exec flag), PIE (ET_DYN type), RELRO (PT_GNU_RELRO presence), canary (stack_chk symbol references), and stripped-vs-symbols — the hardening inventory that turns “is this binary hardened?” from a per-file RE question into a rootfs-wide table. stdlib only.
Design decisions
Section titled “Design decisions”- Header-level truth. NX/PIE/RELRO are header/program-header facts — readable without any disassembly, fast enough for whole-rootfs scans; canary needs a symbol/string check (
__stack_chk_failreference) which is heuristic but honest. - Rootfs-wide aggregate. The value is the estate view: “412 binaries, 61% NX, 9% PIE” is the hardening conversation; per-binary detail follows for the outliers.
- Network-facing first. The scan cross-references H3’s service-surface heuristic — the network-facing binaries’ posture is the prioritized view, because those are the reachable ones.
- No third-party ELF libraries. stdlib struct parsing of the program headers — the ELF format is documented, and the ~150 lines of parsing teach more than importing a library would.
Full source
Section titled “Full source”#!/usr/bin/env python3"""elfscan.py — ELF hardening posture scanner (stdlib).Usage: python elfscan.py squashfs-root/ [--json posture.json]Checks: NX (GNU_STACK W^X), PIE (ET_DYN), RELRO,canary (stack_chk refs), symbols stripped.stdlib only."""import argparse, json, os, struct, sys
def parse_elf(path): with open(path, "rb") as fh: blob = fh.read(4096) if blob[:4] != b"\x7fELF" or len(blob) < 52: return None ei_class, ei_data = blob[4], blob[5] fmt = "<" if ei_data == 1 else ">" (e_type, e_machine) = struct.unpack( fmt + "HH", blob[16:20]) if ei_class == 2: # 64-bit (e_phoff, e_shoff) = struct.unpack( fmt + "QQ", blob[32:48]) e_phentsize, e_phnum = struct.unpack( fmt + "HH", blob[54:58]) is64 = True else: # 32-bit (e_phoff, e_shoff) = struct.unpack( fmt + "II", blob[28:36]) e_phentsize, e_phnum = struct.unpack( fmt + "HH", blob[42:46]) is64 = False # program headers (need full file for phdrs) full = open(path, "rb").read() nx = None relro = False interp = False phtype_names = {1: "LOAD", 2: "DYNAMIC", 3: "INTERP", 6: "PHDR", 7: "TLS", 0x6474e550: "GNU_EH_FRAME", 0x6474e551: "GNU_STACK", 0x6474e552: "GNU_RELRO"} for i in range(e_phnum): off = e_phoff + i * e_phentsize if is64: p_type = struct.unpack( fmt + "I", full[off:off + 4])[0] p_flags = struct.unpack( fmt + "I", full[off + 4:off + 8])[0] else: p_type = struct.unpack( fmt + "I", full[off:off + 4])[0] p_flags = struct.unpack( fmt + "I", full[off + 24:off + 28])[0] name = phtype_names.get(p_type) if name == "GNU_STACK": nx = not (p_flags & 0x1) # W flag = no NX elif name == "GNU_RELRO": relro = True elif name == "INTERP": interp = True pie = e_type == 3 canary = b"__stack_chk_fail" in full stripped = b".symtab" not in full return {"nx": nx, "pie": pie, "relro": relro, "canary": canary, "stripped": stripped, "interp": interp, "is64": is64}
def scan_root(root): rows = [] for dirpath, _, names in os.walk(root): for n in names: p = os.path.join(dirpath, n) try: if open(p, "rb").read(4) != b"\x7fELF": continue except OSError: continue info = parse_elf(p) if info: rows.append({"path": os.path.relpath( p, root), **info}) return rows
def main(): ap = argparse.ArgumentParser() ap.add_argument("root") ap.add_argument("--json") args = ap.parse_args() if not os.path.isdir(args.root): sys.exit(f"not a directory: {args.root}") rows = scan_root(args.root) if not rows: sys.exit("no ELF binaries found") n = len(rows) agg = {k: sum(1 for r in rows if r[k]) for k in ("nx", "pie", "relro", "canary", "stripped")} print(f"== ELF posture: {n} binaries ==") for k, v in agg.items(): print(f" {k:<8} {v:>4}/{n} ({v/n:.0%})") # prioritize: dynamic + weak posture weak = [r for r in rows if r["interp"] and (r["nx"] is False or not r["pie"])] if weak: print(f"\n[!] dynamic binaries with weak " f"posture (no NX or no PIE): {len(weak)}") for r in weak[:12]: print(f" {r['path']} nx={r['nx']} " f"pie={r['pie']} canary={r['canary']}") if args.json: json.dump({"aggregate": agg, "binaries": rows}, open(args.json, "w"), indent=2) print(f"\n[+] {args.json}")
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”parse_elf()(lines 12-58) handles both ELF classes with the struct-format switch (</>by endianness, 32/64 field widths) — the program-header walk reads PT_GNU_STACK (NX = execute flag absent), PT_GNU_RELRO (partial RELRO presence), and PT_INTERP (dynamic) — the three hardening facts that are header-truth.- The canary heuristic (line 59):
__stack_chk_failpresence in the file — reference-based, so it flags binaries linked against stack protection; the honest label is “canary referenced” and the aggregate treats it as such. - The weak-posture prioritization (lines 82-90): dynamic (INTERP present — i.e., real programs, not libc itself) binaries lacking NX or PIE — the exploitable-surface view that pairs with H3’s service-surface filter, so the table you show the product team is “reachable AND weak,” not 400 rows.
- The aggregate (lines 75-80) is the estate number: “61% NX across 412 binaries” — the hardening conversation’s opening line, with the per-binary detail behind it.
Compilation and test procedure
Section titled “Compilation and test procedure”generate the runbook from H2’s extraction
Section titled “generate the runbook from H2’s extraction”$ python emuguide.py v1out/squashfs-root/ —out runbook.md
Architecture census
Section titled “Architecture census”- mipsel: 312 binaries dominant arch: mipsel
User-mode emulation (per binary)
Section titled “User-mode emulation (per binary)”chroot v1out/squashfs-root/ /usr/bin/qemu-mipsel-static
-L v1out/squashfs-root/ /usr/sbin/lighttpd
posture scan of the same tree
Section titled “posture scan of the same tree”$ python elfscan.py v1out/squashfs-root/ —json posture.json == ELF posture: 312 binaries == nx 198/312 (63%) pie 41/312 (13%) relro 87/312 (28%) canary 55/312 (18%)
[!] dynamic binaries with weak posture (no NX or no PIE): 46 usr/sbin/httpd nx=False pie=False canary=False
live emulation per the runbook
Section titled “live emulation per the runbook”$ chroot v1out/squashfs-root/ /usr/bin/qemu-mipsel-static
-L v1out/squashfs-root/ /usr/sbin/lighttpd -f /etc/lighttpd.conf &
$ curl -s http://127.0.0.1:80/ | head -3
Detection considerations
Section titled “Detection considerations”Firmware-layer “detection” is build posture: the elfscan aggregate belongs in CI (hardening flags regression-gated per release — “NX dropped from 63% to 55%” is a build-system finding), the emulated-service smoke tests run per build (the services must boot under QEMU in CI — the config that never boots is found before shipping), and the RE review covers the network-facing binaries per release. Your own tooling hygiene: emuguide/elfscan read files; running extracted binaries belongs in chroot/QEMU sandboxes on the bench — never on the analyst’s host without the container (the vendor’s init scripts assume they own the machine). QEMU/FirmAE bring their own hygiene (user-mode networking by default — no bridged bench network for untrusted emulation). Wazuh/SIEM has no layer; the detection is CI and design review. The standing metrics: hardening-posture percentages per release (NX/PIE/RELRO/canary trending up), emulated-boot success rate per build (CI green), and RE findings-per-release (target: declining) — three numbers that make the firmware estate’s code quality as governed as its network posture.
Improvements to add
Section titled “Improvements to add”- radare2/Ghidra headless integration: emit analysis-project commands per network-facing binary into the runbook.
- Vulnerability-class grep:
system(,strcpy(,sprintf(with user-input proximity heuristics — the fast pre-RE triage (report cites evidence, RE confirms). - NVRAM shim generator: scan binaries for nvram_get key names and emit a defaults C stub — the shim built from the binary’s own questions.
- QEMU machine-model matcher: probe kernel images for machine compatibility (patched vs stock) before the full-system attempt.
- CI posture gate:
--min-nx 90style exit semantics for release pipelines.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| Hardening posture per release | elfscan CI gate | build metrics | NX/PIE/RELRO/canary trend |
| Emulated boot success | CI smoke test | per build | Config that never boots |
| RE findings per release | Review program | per release | Declining target |
| Network-facing binary count | emuguide | attack surface | Per-release surface metric |
| Vulnerability-class patterns | grep triage | pre-RE findings | system/strcpy sinks |
| NVRAM dependency inventory | shim generator | design finding | Config-read surface |
| Emulation-based fuzz coverage | Fuzz program | per release | Crash-class regression |
| Weak+reachable set | elfscan prioritize | hardening list | The product-team worklist |
| Update client validation (RE) | H3/H10 review | supply chain | TLS check presence |
| Design-review acceptance rate | H12 program metric | per product gen | Lifecycle metric |
Evasion Techniques
Section titled “Evasion Techniques”The firmware-evasion race (vendors hardening analysis) mirrors every other layer’s. Anti-disassembly: opaque predicates, instruction-overlap tricks that break linear disassembly — countered by decompilers that recover control flow (Ghidra’s analysis handles most), by dynamic tracing (execution is truth), and by the honest note that anti-disassembly marks the interesting functions for attention. Anti-emulation: binaries that detect QEMU (timing, CPUID quirks, device-tree probes) and refuse to run — countered by QEMU tuning (custom machine models, patched kernels — FirmAE’s approach), by answering the probes in the shim layer, and by the observation that anti-emulation code is itself an xref to RE. Stripping and packing: stripped binaries (no symbols), UPX-class packing — countered by symbol recovery from strings and cross-references (stripped binaries still name their syscalls), and by unpacking (M4’s discipline, applied to embedded packers). Compiler-hardening evasion: no impact on analysis, but the defense side: the posture scan makes hardening visible, so the evasion would be claiming hardening without enabling flags — countered by the CI gate reading the actual headers, not the build config. Hardware-dependence as evasion: code that refuses to run without the real device’s peripherals — countered by shimming the peripherals (the NVRAM lesson generalized) and by the assessment honesty that some questions need the board (H1/H7’s territory). The meta-point: firmware RE’s difficulty is logistics (arch, NVRAM, hardware deps), not crypto — and each logistics barrier has a standard tool once you’ve hit it once, which is why the second device costs a quarter of the first.
⚠️ What caught me: my first QEMU emulation attempt of a MIPS camera failed so completely — kernel panic before the bootloader finished — that I nearly declared the firmware “hardware-dependent” and moved on. A week later, on a hunch, I tried the endianness I hadn’t checked: the census said mipsel, I’d been booting qemu-system-mips (big-endian) because the vendor’s wiki said “MIPS.” One letter. The full system booted on the second try, services and all. Since then, emuguide’s census exists precisely because “MIPS” is not an architecture — machine, endianness, and class are three separate facts, and the runbook reads them from the ELF headers instead of from anyone’s wiki. The hardware-dependent verdict is now the last conclusion I’m allowed to write, not the first.
Lab Practice
Section titled “Lab Practice”- Emulate one binary: take H2’s extracted rootfs, run
emuguide.py, and follow the user-mode runbook for the web binary — get one vendor service running on your bench, NVRAM shim and all. - Posture scan: run
elfscan.pyon the same tree; reconcile the weak-posture list against the network-facing surface — the prioritized hardening conversation, on a rootfs you control. - Full-system boot: attempt the qemu-system command for your lab device’s kernel; debug the machine-model/endianness failures until it boots (or document exactly which hardware dependency blocks it — both are findings).
- Static RE hour: load the web binary into Ghidra, follow the hardcoded credential’s xref, and document what the code actually does with it — the strings-to-meaning workflow, once, end to end.
- Crash-to-RE loop: send one malformed request to the emulated service, catch the crash in qemu/gdb output, and find the crashing function in Ghidra — the dynamic-static bridge, the skill this module exists to build.
Interview Questions
Section titled “Interview Questions”- Technical: Compare user-mode and full-system emulation for firmware — what does each answer, and what are the NVRAM/networking blockers of each?
- Technical: What do the NX, PIE, RELRO, and canary protections each do, and which ELF structures does each check read?
- Scenario: The extracted binary refuses to run under QEMU with an immediate abort. Diagnose: arch/endianness, NVRAM reads, hardware deps, anti-emulation — in order.
- Conceptual: Why does emulation scale vulnerability research where hardware cannot, and what does the emulation not prove about the hardware device?
- Conceptual: How do the ELF hardening flags change the vulnerability classes an assessor hunts — and which classes survive NX+PIE?
- Deep-dive: Design the firmware RE program for a product line: per-release analysis pipeline, posture gates, RE review scope, and the findings trend.
- Practical: The emulated service reproduces the injection but the physical device doesn’t crash. What differs, and what does each result mean for the report?
Related Modules
Section titled “Related Modules”H2 (Firmware Extraction & Analysis) · H4 (Embedded Linux & Root Shells) · F2 (Linux Internals) · F5 (Python for Security) · M4 (Unpacking and Deobfuscation)