Wiki / Concepts

Firmware Reverse Engineering

Reverse engineering applied to an embedded image, where there is often no file format, no symbols, no operating system, and no correct load address until you work it out.

Firmware reverse engineering is ordinary reverse engineering with most of the scaffolding removed. A desktop binary hands you a file format, a symbol table, an entry point and a standard library. A firmware image frequently hands you none of those: it is a flat array of bytes that some CPU was told to start executing at an address you have to deduce.

Two very different targets

Before anything else, decide which kind of image you have. Everything downstream depends on it.

  • Linux-class firmware. A bootloader, a compressed kernel and a filesystem packed into one image. binwalk finds the boundaries, the rootfs unpacks into a normal directory tree, and the interesting binaries inside it are ordinary ELF files with a dynamic linker and libc. From that point the work is standard reverse engineering.
  • Bare-metal firmware. A single blob with no filesystem, no ELF header and no libc. There is no main, only a reset vector; there are no syscalls, only writes to peripheral registers. This is the case that needs the techniques below.

binwalk finding nothing is itself the answer: no signatures usually means bare-metal, or an encrypted image.

Getting the load address right

Everything hinges on this. Disassemble a bare-metal blob at the wrong base and every absolute pointer lands in the wrong place, cross-references collapse, and strings never resolve. Three ways to recover it:

  • The vector table. On ARM Cortex-M the image starts with the initial stack pointer followed by the reset vector and the exception handlers, a run of word-aligned values that all point into the same region and mostly have bit 0 set (Thumb). Their common high bits are the load address.
  • Pointer clustering. Take the values that look like pointers, histogram their upper bits, and the modal region is where the image believes it lives.
  • The datasheet. The memory map of the chip tells you where flash is mapped. An ESP32 executes XIP from a known window; an STM32 maps flash at 0x08000000. Identify the silicon first and the address is usually a lookup, not a puzzle.

Load it wrong, and the tell is immediate: no coherent function starts, strings that are visible in strings output but referenced by nothing.

Recognising the architecture

binwalk -A, cpu_rec, or simply trying each disassembler and seeing which produces sane prologues. Watch for ARM versus Thumb (Thumb is 2-byte aligned and far more common on microcontrollers), MIPS endianness (both exist in the wild and the wrong one produces plausible-looking garbage), Xtensa on ESP32 and RISC-V on newer parts.

Reading bare-metal code

There is no libc to anchor on, so the anchors change:

  • Peripheral registers. Writes to fixed high addresses are memory-mapped I/O. Match the address against the chip's memory map and a nameless function becomes "configure UART" or "start the SPI transfer".
  • The vector table. Every interrupt handler is an entry point. The handler list tells you which peripherals the firmware actually uses.
  • Strings, still. Even bare-metal images carry version banners, AT command tables and error text.
  • Chip SDK signatures. Vendor SDKs (ESP-IDF, Nordic nRF, STM32 HAL) have recognisable function shapes and constants; identifying the SDK gives you documentation for half the binary at once.

Pitfalls

  • Assuming the whole image is code. Firmware images mix code, constant tables, configuration blocks, calibration data and padding. Disassembling data produces convincing nonsense.
  • Missing that the image is encrypted. A flat entropy plateau near 8.0 with no signatures means the bytes are not the firmware; find the decryption path instead of reversing noise.
  • Forgetting the secret has to be inside. If a self-contained image decrypts something at runtime, the key is in the image by necessity. That cuts both ways: it is often the shortest path to the answer, and it is why obfuscation alone never protects a shipped artifact.
  • Reversing when emulation is cheaper. A static binary that reads its inputs from files can often be run under qemu user mode with those paths faked, and made to produce the secret itself with no disassembly at all.

What it pairs with

binwalk splits the image, ghidra and radare2 read it, qemu runs it. Upstream, the bytes come off the device via esptool, flashrom or a soic-clip. The general discipline and its working loop are in reverse-engineering.

Further reading