---
title: "This firmware was not dumped. It was rebuilt from what the CPU read"
locale: "en"
url: "https://irz.fr/en/articles/firmware-spi-read-reconstruction-en"
markdown_url: "https://irz.fr/en/articles/firmware-spi-read-reconstruction-en.md"
category: "tech"
tags: ["firmware", "SPI", "Quad-SPI", "reverse engineering", "Scapy", "logic analyzer"]
published_at: "2026-09-24T19:45:00.000Z"
author: "Manon Girard"
translation: "https://irz.fr/fr/articles/firmware-spi-read-reconstruction-fr.md"
---

# This firmware was not dumped. It was rebuilt from what the CPU read

Matthew Alt reconstructs a firmware image by observing SPI reads during boot. Each transaction reveals an address and bytes, while any region the CPU never requests remains unknown.

A flash dump makes a simple promise: read every address and leave with a copy of the chip.

Matthew “wrongbaud” Alt did not have that option. During a black-box assessment, his team had one specimen of the device. Useful debug interfaces were disabled, reading the flash in circuit had failed, and desoldering the chip risked damaging a target they could not replace.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

Unable to query the memory cleanly, he listened to **the processor doing it for him**.

A logic analyzer attached to the SPI bus captures the commands sent during startup and the responses from a 16 MiB Winbond W25Q128JV flash. Each read transaction already carries the two pieces needed for reconstruction: an address requested by the processor and the bytes returned from that address.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

The resulting image can then be opened in Binwalk like an ordinary firmware file.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)[5](https://binwalk.readthedocs.io/en/latest/)

The result looks like a dump without carrying its guarantee of completeness. The whole method lives in that gap.

## One transaction is a small piece of evidence

There is no large `firmware.bin` object moving over the bus. The controller selects flash, sends a command and address, then lets the chip answer.

In Alt's first capture, the clock runs at about **16 MHz**. Another line drops to mark `Chip Select`. Then `0x03` appears, the W25Q128JV's standard `READ` opcode.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

The observed transaction reads address zero. The returned bytes begin `03 00 00 08`, which form `0x08000003` when interpreted as a little-endian 32-bit word.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

At that point, an electrical trace regains a location in memory.

A `READ` at `0x001000` followed by 256 bytes therefore gives a fragment with a known home. Those bytes can go back at `0x001000` in the 16 MiB image.

Alt therefore initializes the reconstructed image to `0xFF`, the erased state of NOR flash, and fills only offsets backed by captured reads. A second array records which bytes are genuinely covered.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

> **A capture does not copy the flash: it accumulates addressed evidence**
> Diagram showing SPI address and data transactions placed into a 16 MiB image with unknown gaps
> - READ(address) + response → fragment placed at its real offset
> - CAPTURED SPI
0x03 · address
→ returned bytes
> - yellow = observed · black = unknown
> - A region the CPU never reads remains unknown even when the reconstructed file is exactly 16 MiB long.
> The final file preserves flash addresses, but its coverage is strictly limited to transactions that were observed.

## Scapy is doing something other than networking

The raw capture comes from Saleae Logic. Its SPI analyzer can export decoded bytes as CSV, including transaction timing and MOSI/MISO values.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

The remaining job is turning those columns back into usable transactions.

Alt uses **Scapy**, the Python tool best known for constructing and dissecting network packets. Scapy is more general than its most visible use: its documentation explains how new protocols can be defined from fields and layers.[3](https://scapy.readthedocs.io/en/latest/build_dissect.html)[4](https://scapy.readthedocs.io/en/latest/)

He defines an SPI transaction and a `SPIFlashCmd` layer. The opcode becomes an enumerated field; commands such as `0x03`, `0x0B` and `0x6B` can carry a 24-bit address; fast-read variants add their dummy cycles. A read response is represented as a length-delimited byte blob.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

Once transactions have structure, rebuilding the image becomes mechanical:

1. read a transaction;
2. keep read responses;
3. retrieve the address;
4. copy returned bytes at that offset;
5. mark the region as covered.

This is where Scapy earns its keep: downstream code gets **logical transactions**, not CSV columns that still resemble the wires.

That becomes important when the physical bus changes shape later in the same boot.

## The first firmware has a hole

The file reconstructed from standard SPI is good enough for Binwalk to find familiar structures: gzip data, an LZMA block, startup strings and DDR initialization. Alt identifies the bootloader and Linux kernel image.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

The root filesystem is missing.

This is where a `.bin` file can lie by appearance. Unobserved regions were filled with `0xFF`, making them visually identical to genuinely erased flash.

But “nothing was observed here” is not the same claim as “the flash really contains 0xFF here.”

The coverage map prevents those states from being confused.

Alt considers two possibilities: the capture did not cover the full boot, or the controller changed how it accessed the flash.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

The second answer is correct.

## The bus widens during boot

Early in boot, IO0 and IO1 carry ordinary SPI data while IO2 and IO3 remain in their control-pin roles.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

Then those pins start switching too.

At the same time, the clock rises from about **16 MHz to 50 MHz**. The kernel has reconfigured its flash controller for **Quad-SPI**.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

Later reads use opcode `0x6B`, `FAST_READ_QUAD_OUT`. It is a **1-1-4** command: opcode and address still travel on one line, eight dummy clocks follow, then returned data uses all four lanes in parallel.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

With a higher clock and four data lanes, Alt estimates read throughput at roughly twelve times the early-boot rate.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

Saleae's default SPI decoder cannot follow this mode. Ignore IO2 and IO3 and the capture loses exactly the reads that begin loading the filesystem.

> **The boot changes bus width**
> Diagram comparing 16 MHz standard SPI on one data lane with 50 MHz Quad-SPI on four lanes when loading the filesystem
> - SAME FLASH · TWO READ MODES DURING ONE BOOT
> - STANDARD SPI · ~16 MHz
> - READ 0x03
IO0 / IO1
bootloader + kernel
> - QUAD-SPI · ~50 MHz
> - FAST_READ 0x6B
IO0–IO3
rootfs + later regions
> - One decoder cannot see everything once the physical protocol changes after kernel loading.
> The hole in the first file was not necessarily erased flash: the target had started reading over four lanes.

## 67.3 percent is already a lot

Alt captures again with a Quad-SPI analyzer and adapts the import to its format.

This time the tool counts **36,839 `FAST_READ_QUAD_OUT` transactions**. Out of the W25Q128JV's 16,777,216 bytes, it places **11,288,528 bytes**, or **67.3 percent** coverage.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

Binwalk now finds a SquashFS at `0x2D0000`, around 3.16 MiB, and a JFFS2 filesystem at `0xDE0000`.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

The beginning of the Quad-SPI image is still `0xFF`.

Again, that does not prove the start of flash is empty. The Quad capture begins after the mode switch. Bootloader and kernel had already been read over single-lane SPI.

The captures complement each other in different address ranges:

- standard SPI capture: early data, bootloader and kernel;
- Quad-SPI capture: filesystem and large regions read later;
- final image: fragments layered at their actual addresses.

Alt shows a `dd` command copying everything from `0x2D0000` upward from the quad image into the first image. His `spidump` tool can also merge multiple captures by layering observed reads.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

At this point it is no longer much like copying a file. Each capture documents another region of the address space, and the fragments are layered together.

## What the processor never reads stays invisible

This limitation is not an implementation detail. It follows directly from the method.

An external programmer can arbitrarily request every flash address. A passive logic analyzer commands nothing. It only sees what the system chooses to read while observation is active.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)[2](https://hackaday.com/2026/09/24/reconstructing-device-firmware-from-spi-reads)

A recovery partition that is never mounted can remain absent. A key stored in a region untouched during boot remains unknown. Configuration tied to another product feature remains absent unless that feature runs during a capture.

More behaviours can still be provoked: several boots, recovery, configuration, networking, updates. Each scenario may add new regions to the map without desoldering the chip.

This is why the **coverage map** matters as much as the `.bin` file. Without it, `0xFF` conflates two states: a value that was genuinely observed and missing data filled with a default.

## Observe before extracting

The technique does not beat a physical dump in every situation.

If the flash can be read reliably with a programmer, a complete dump is more direct. Bus sniffing adds a logic analyzer, protocol decoding, signal-integrity problems, multiple captures and perhaps multiple bus modes.[1](https://voidstarsec.com/blog/scapy-spi-reconstruction)

It has one valuable property, though: **it is passive at the protocol level**. It does not need to persuade the flash to talk to a second master while remaining soldered on the board. It watches a conversation that already works between processor and memory.

With a one-off specimen or a board that refuses in-circuit reads, that passive property becomes a serious reason to use the method.

It does offer a useful hardware reverse-engineering rule.

When a device will not let you read its state directly, watch **the decisions it makes from that state**.

Here, every boot-time address request reveals a few more bytes. Not all of flash. Only what the processor needed to know.

That can still be enough to rebuild a useful firmware image.

## References

1. [Matthew Alt — Extending Scapy for Hardware Reverse Engineering, VoidStar Security, September 23, 2026](https://voidstarsec.com/blog/scapy-spi-reconstruction)
2. [Hackaday — Reconstructing Device Firmware From SPI Reads, September 24, 2026](https://hackaday.com/2026/09/24/reconstructing-device-firmware-from-spi-reads)
3. [Scapy — Adding new protocols](https://scapy.readthedocs.io/en/latest/build_dissect.html)
4. [Scapy 2.7.1 documentation](https://scapy.readthedocs.io/en/latest/)
5. [Binwalk documentation](https://binwalk.readthedocs.io/en/latest/)
