firmware rehosting haxx

every firmware rehosting tool i have used lies to me about whether it worked. not on purpose. the tool boots something, gets an HTTP 200 back, prints a green line, and moves on. then you open it and it is a static login page with a dead CGI backend behind it. nothing from the vendor is actually running.

i spent a good chunk of this year rehosting real embedded devices in QEMU, one SoC family at a time. the single most useful idea i picked up reorganised how i think about the whole problem. it is from a DEF CON 33 talk by S. Polke (CrowdStrike), and it is called LightTouch.

the one idea

you want to run a router’s firmware without the router. the reason why you would want to inflict this (coughs) joy upon yourself is wholly yours. there are several reasons why you’d want that, whether it is to replay CVEs, build a lab, or whatever else. in my case, we’re building a product at $work that needs us to play with lots of different devices. we don’t always have the space or budget to keep the real hardware around. however, your big problem is that the SoC is some Broadcom or Realtek or HiSilicon part that QEMU does not model and never will. so you have two obvious moves.

move one: emulate around the kernel. boot a stock QEMU machine into a normal distro, unpack the firmware, chroot into the vendor root, and LD_PRELOAD a pile of shims (libnvram and friends) to paper over every peripheral the binary reaches for. this is known as Firmadyne / FirmAE. it is great for poking at one binary. but it falls apart the moment the whole system has to come up. real firmware is a dozen processes that orchestrate each other through devices that are not there. (you can think of network cards, antennas, etc. those are doing things, printing values, returning data to the custom programs from your firmware. so, these programs usually expect something)

move two: emulate under the userspace. aka the LightTouch trick:

do not emulate the SoC. build and boot a stock kernel that matches the firmware’s kernel version and CPU, on a QEMU machine that actually exists (-M malta, -M virt, …), and run the unmodified vendor userspace on top of it, as the real PID 1.

simple, right ? the vendor userspace can be a busybox with uClibc, and a web server. it does not care what board it is on. it cares about the syscall ABI. give it a kernel of the right version and the right architecture and it comes up, because from userspace’s point of view nothing is fake. the kernel is real. the faking actually happens below all of that, in the kernel and the machine, where you forge the handful of char devices and ioctls the vendor code expects.

two dimensions are non-negotiable:

  1. kernel version. build the vendor’s exact version if you can (Linux 2.4.20, 4.9-ndm-5, whatever). a modern gcc will not even build Linux 2.4.20, so you should go find the relevant toolchain if you can.
  2. CPU architecture. arch, endianness, word size, float ABI, all matched to the vendor ELF. a wrong float ABI loads fine and then dies with SIGILL on the first floating-point instruction. good luck finding the root cause of the error.

get those two right and the userspace runs. everything else (the peripheral fakes, the init shim, the networking) is per-device and you work it out from the firmware (a memory dump is best).

everyone else boots something else

what surprised me, once i read the literature properly: almost nobody does what LightTouch does. they all solve a different problem and report a number for that problem.

approach kernel that runs userspace that runs headline number
LightTouch stock kernel, same version, patched per SoC unmodified vendor userspace, real PID 1 ~95% functional on one router
ECMO vendor kernel binary a researcher’s ramfs, not the vendor rootfs 710/815 kernels reach a shell (87.1%)
FirmGuide vendor kernel binary not the focus >96% of 6,188 images
Firmadyne / FirmAE one generic prebuilt kernel vendor userspace + LD_PRELOAD heuristics FirmAE 79.36% (self-reported)
Greenhouse host kernel (qemu-user) one service in a chroot 2841/7140 web servers (39.7%)
FIRMWELL host kernel (qemu-user) init, then the target service 6490/14049 services (46%)

the numbers are all lies !!

look at the “headline number” column, and understand that not one of those percentages measures the same thing.

  • ECMO counts a kernel that reaches a shell.
  • FirmAE counts any HTTP response. a 502 counts. Index of / counts.
  • Greenhouse counts a web server that serves its own pages. in doing so it showed that almost half of FirmAE’s “successes” cannot serve their service.
  • FIRMWELL re-measured FirmAE at 25% on 14,049 images, not 79%.

so when a tool tells you 79%, what they mean is “79% of the time something replied.” a process that answers a socket is not necessarily a working device.

the payoff

all of this costs more than running FirmAE. you build a kernel, match a toolchain, and forge the char devices the vendor code reaches for. but a matched kernel gives you the one thing qemu-user and generic-kernel setups cannot: the vendor’s actual kernel code runs. .ko modules load. real ioctls return real answers instead of an LD_PRELOAD educated guess. the whole userspace comes up together the way it does on the device. so when i tested CVE-2025-55423 against a rehosted ipTIME n702bcm, it validated end to end into a root shell against the live vendor binaries. no on-device debugging, no guessing whether the bug was real. the thing answering the socket was the vendor’s own code, simple as that.

so: match the kernel, match the arch, run the vendor userspace as PID 1, and refuse to call anything “working” until a vendor process does a vendor thing.

limits

none of this replaces the real device, and i want to be honest about that. you are matching a kernel version, not the vendor’s exact .config, so the drivers and options compiled in can still differ from the real thing. the peripherals you forge are approximations. you guess what a char device should return. you guess the kernel config the vendor .ko was built against. you guess which ioctl the daemon actually cares about.

most of the time a bad guess just means it does not boot, and you fix it and try again. the bad case is when it boots and behaves slightly differently from the real hardware. now the rehost lies the other way: it says a bug is reachable when on the real board it is not, or the other way round. so it is a great lab, but it is not the real device. before you fully confirm a vulnerability, you should run it against the physical hardware at least once.

Until next time :)~

djnn.sh

offensive security & software engineering


rehosting firmware without lying to yourself

2026-11-10