Firmware Bring-Up Is the Hard Part: AI-Assisted Router Emulation and 3 0-Days
A disclosure-safe look at using rootless Podman, QEMU user-mode, proot, and AI-assisted triage to build an evidence-preserving firmware lab before trusting router vulnerability findings.
Router firmware research usually breaks before the glamorous part.
The hard stop is often not a missing decompiler trick or a clever parser bug. It is the basic fact that the firmware expects to be running on a real board, with real devices, real boot ordering, and years of vendor assumptions packed into shell, Lua, init scripts, and native binaries.
The load-bearing concept for this post is the fidelity boundary: the line between what an emulated lab faithfully represents and what it deliberately does not claim to model. My goal was not to pretend a container was a router. My goal was to make the management plane run repeatably enough that real code paths, emulation noise, and candidate vulnerabilities could be separated without guessing.
That boundary is also what made AI useful. AI did not autonomously find router 0-days for me. It helped compress the triage loop around logs, diffs, shell scripts, Lua paths, service dependencies, and regression checks once I had constrained the problem enough for the suggestions to be testable.
Disclosure note: I reported three previously unreported findings to the vendor. This post intentionally stays at the vendor/category level and avoids the model, firmware version, packet formats, vulnerable endpoint details, exploit steps, and weaponized reproduction material. I use generic service-family language where needed because the point here is the lab and the research workflow, not a public exploit recipe.
The Useful Story
The headline is simple: three previously unreported findings came out of a consumer-router research project.
The useful engineering story is the infrastructure around them:
- bringing vendor firmware up without a full hardware lab
- keeping stock firmware and emulation patches separate
- deciding what the emulation needed to prove, and what it could not prove
- turning repeated boot failures into small, testable fixes
- using AI as bounded triage acceleration instead of treating it as an autonomous reverser
One service family involved TP-Link Device Debug Protocol (TDDP), a vendor diagnostic and test protocol plane. The web-management path involved LuCI, the Lua Configuration Interface commonly used in OpenWrt-derived firmware, and ubus, OpenWrt’s lightweight interprocess communication and remote procedure call bus.
Those names matter because they define the shape of the lab. They do not define the vulnerabilities in this post. The exploit mechanics belong in separate coordinated-disclosure writeups.
Why Bring-Up Fails
At a distance, embedded userspace emulation sounds mechanical:
- extract the firmware
- run the root filesystem under an emulator
- browse to the management UI
- start testing
Real firmware does not behave that politely.
The image expected a system-on-chip (SoC), meaning the CPU and board-specific peripherals that a router firmware treats as its normal hardware world. That world includes switch hardware, wireless interfaces, LEDs, thermal paths, hotplug state, kernel devices, boot-time side effects, and configuration artifacts created by services that may never start in a lab.
When those assumptions break, the failures look unrelated:
- symlinks lost during extraction
- shell scripts exiting on missing devices
- Lua code dereferencing absent
ubusobjects - init scripts waiting forever for board services
- web routes failing because a platform daemon never created expected state
- runtime differences between Linux-native storage and Windows-backed mounts
Manual triage turns into a swamp quickly. You stop asking whether a parser, command path, or authentication boundary is interesting and start asking why a daemon for hardware you do not have blocked the web server.
I wanted a lab that could reset cleanly, preserve the original root filesystem, and bring up enough of the management plane to make those questions separable.
The Lab Boundary
The working setup used Windows Subsystem for Linux (WSL), Microsoft’s Linux environment on Windows, as the host-side control plane. Inside WSL, rootless Podman provided a daemonless container runtime that can run without a privileged Docker-style service. QEMU user-mode ran the vendor ARM Linux binaries by translating user-space execution and syscalls rather than emulating an entire router board. proot, a user-space chroot and bind-mount emulator, presented the extracted firmware tree as the process root without requiring root privileges inside the lab.
That combination was intentionally less ambitious than full-system emulation.
| The lab models | The lab does not model |
|---|---|
| Extracted vendor userspace | The complete router board |
| Web-management startup | Real switch or wireless hardware |
LuCI and ubus failure behavior | Kernel drivers or SoC registers |
| Service ordering and config assumptions | Electrical timing or RF behavior |
| Repeatable endpoint reachability checks | Production exploit reliability |
The boundary kept the research honest. A management-plane crash in this lab was worth investigating. A missing wireless device was usually noise. A candidate vulnerability still needed manual analysis, source or binary review where available, and disclosure-safe validation before I treated it as real.
Rebuilding The Firmware Lab
The orchestration was built so a fresh WSL environment could recreate the lab from a sanitized project directory:
cd ~/router-firmware-lab
chmod +x ./setup-wsl-podman.sh
./setup-wsl-podman.sh
The bootstrap installed the pieces needed for repeated bring-up:
podmanpodman-composepodman-dockerqemu-user-staticprootsquashfs-toolsuidmapslirp4netnsfuse-overlayfs
The runtime then came up through a wrapper:
cd ~/router-firmware-lab
./run.sh up -d
The first validation step was deliberately boring: confirm that benign management-plane routes were reachable. I am showing redacted request shapes here, not exploit traffic or protocol details:
curl -fsS 'http://127.0.0.1:8080/<login-page>' >/dev/null
curl -fsS 'http://127.0.0.1:8080/<management-rpc>' \
-X POST \
-d '<benign-read-request>'
Those checks were not proof of vulnerability. They were proof that the lab had crossed the minimum threshold for useful research: the web stack was alive enough to test intentionally.
The Fixes That Mattered
The fastest progress came from removing failures that were outside the research question.
Re-extract Inside Linux
Firmware extracted on the wrong filesystem can lose the details that matter most: symlinks, permissions, and SquashFS semantics. The first rule became simple: extract the firmware inside Linux, then treat that stock extraction as evidence.
Once the original tree is unreliable, every downstream conclusion gets weaker.
Keep Stock And Emulated Trees Separate
The lab kept two root filesystems:
- the stock extraction
- the patched emulation tree
That separation made every lab-only change inspectable. If I changed an init script, stubbed a hardware utility, or guarded a Lua path, the diff showed exactly where the lab diverged from the vendor image.
That mattered for discipline. Emulation patches should make the firmware observable. They should not blur into the vulnerability analysis.
Stub Board-Only Services
Some binaries were never going to behave correctly without the real board. Wireless, switch, thermal, LED, and platform-glue paths often existed to talk to hardware the lab did not have.
For those paths, safe stubs were better than repeated crashes. The goal was not to make every daemon correct. The goal was to stop irrelevant board failures from destroying the management plane.
Harden Missing LuCI And ubus State
Router web code often assumes the rest of the system already created the objects it needs. LuCI templates, Lua controllers, and ubus calls can fail hard when a service did not start or a config artifact does not exist.
The emulation tree added guards around missing state where the management plane could clearly degrade cleanly. That meant nil checks, synthetic fallbacks, and non-fatal handling for absent objects in places where the original firmware expected a complete board boot.
Those patches were lab-only. They made the UI testable; they were not evidence that production firmware behaved safely or unsafely.
Move Runtime State Off Windows-Backed Storage
One subtle failure came from using a Windows-backed mount as the live emulated root filesystem inside a rootless Podman container. The source tree could live there, but the active firmware root needed Linux-native filesystem behavior.
The fix was to bind-mount the project for code and scripts, then put the extracted and patched runtime rootfs in a Podman-managed Linux-native volume.
That removed a class of runtime errors that looked like firmware instability but were really lab instability.
Time-Bound Hung Init Scripts
Even after obvious hardware paths were masked, some init scripts could still hang long enough to block the handoff to the web server.
The runtime strategy became:
- start a broad userspace stack by default
- mask known hardware- or kernel-dependent services
- enforce timeouts on service startup
- keep failures non-fatal unless they directly block the management plane
That produced a profile realistic enough for management-plane work without pretending to be the complete appliance.
Runtime Knobs
Firmware bring-up is not all-or-nothing. The useful question is usually narrower: what is the smallest believable runtime that still exercises the behavior I care about?
For a smaller debugging boot:
TPLINK_PROFILE=core ./run.sh up
To move closer to the original stack:
TPLINK_UNMASK_SERVICES=network,firewall,nat ./run.sh up
To force more services off while isolating a crash:
TPLINK_MASK_SERVICES=miniupnpd,minidlna ./run.sh up
To reset generated state:
./run.sh clean
Those knobs gave the lab a controlled way to move along the fidelity boundary instead of arguing about whether emulation was either perfect or useless.
Compact Evidence Trail
I am not publishing raw exploit material, packet formats, or full local transcripts in this post. The evidence that belongs here is the evidence for the lab.
| Evidence | What it established |
|---|---|
| Linux-side SquashFS extraction | The stock tree preserved firmware filesystem semantics |
| Stock vs emulated rootfs diff | Lab-only patches stayed reviewable |
| Clean Podman volume rebuild | Runtime state could be reset and reproduced |
| Service logs with startup timeouts | Hung or missing services were isolated without blocking all testing |
| Benign HTTP reachability checks | The management plane was alive enough for targeted validation |
| Manual notes tying findings to code paths | Candidate vulnerabilities were separated from emulation failures |
The table is intentionally compact. It gives the shape of the proof without turning a disclosure-safe infrastructure post into a vulnerability walkthrough.
Where AI Helped
AI was useful because the tasks were bounded.
I fed it concrete artifacts: boot logs, shell scripts, Lua fragments, service lists, diffs, and failing commands. Then I used it to propose the next small experiment, explain a likely dependency chain, or turn a one-off failure into a repeatable check.
The best uses were operational:
- compare boot attempts and highlight the next likely blocker
- trace service startup through shell, Lua, and config paths
- draft safe stubs for obviously hardware-only utilities
- suggest guards around missing
ubusand config dependencies - keep wrapper scripts and regression checks aligned as the lab changed
- summarize diffs so I could review the actual changes faster
The human work stayed in the places where judgment matters:
- deciding whether a failure was a real code path or emulation noise
- reading native parser and command-execution logic
- choosing which services to stub, patch, unmask, or leave alone
- checking whether a candidate issue had enough evidence for disclosure
- refusing to turn an uncertain observation into a stronger claim than the lab supported
That distinction is important. AI accelerated triage. It did not replace reverse engineering, vulnerability validation, or disclosure judgment.
Why This Stack Worked
Rootless Podman gave the lab a clean reset boundary. QEMU user-mode made vendor ARM userspace fast enough to iterate on. proot let the extracted rootfs behave like a process root without requiring a full machine model. WSL made it practical to keep Windows-side tooling while running the active firmware state on Linux-native storage.
The result was not a perfect virtual router.
The result was better: a repeatable management-plane lab with a clearly stated fidelity boundary. That is the difference between “the firmware almost boots” and “I can run a controlled experiment, inspect the evidence, reset the state, and do it again.”
What Comes Next
The vulnerability-specific writeups will wait for coordinated disclosure. Those posts can carry the details this one intentionally omits.
The more reusable lesson is already clear. Router research gets much better when the lab stops being a pile of one-off fixes and becomes a controlled system. AI fits into that system when it is constrained to triage, explanation, patch review, and regression generation.
The bugs got my attention.
The bounded, reproducible lab is what made the research compound.