Project Overview: Why Dump SPI Flash?

When we talk about hacking projects with Raspberry Pi, most tutorials stop at software-level network scanning. True hardware hacking begins at the silicon level. Extracting (dumping) firmware directly from an SPI flash chip is the foundational skill for IoT security research, recovering bricked smart home devices, and reverse-engineering proprietary protocols.

In this guide, we will wire a Raspberry Pi directly to a Winbond SPI flash chip, write a robust Python script to read the memory contents, and debug the inevitable hardware-level failures. This project targets the Raspberry Pi 4 Model B (4GB or 8GB) running Raspberry Pi OS Bookworm, utilizing the modern gpiozero and spidev libraries.

Difficulty Rating: Intermediate (Requires basic soldering/clipping, Linux CLI comfort, and Python fundamentals).
Estimated Time: 45 minutes for wiring and first successful dump.
Estimated Cost: $55 (Assuming you already own the Raspberry Pi).

Hardware Bill of Materials (BOM)

Hardware hacking fails when you use the wrong physical interfaces. Do not substitute the test clip with fly-wires if you are working on a populated PCB; the signal integrity at 2MHz will degrade, resulting in bit-flips in your dump.

Component Exact Model / Variant Purpose & Notes Approx. Cost
Microcomputer Raspberry Pi 4 Model B (Bookworm OS) Host controller. Pi 5 works but requires rpi-lgpio backend. $35 - $55
Target Chip Winbond W25Q32JV (SOIC-8) 32Mbit (4MB) 3.3V SPI Flash. Common in IoT routers and smart plugs. $2.50
Test Clip Pomona 5250 SOIC8 Clip Gold-plated contacts. Generic clones often slip off the chip legs. $8.00
Decoupling Cap 100µF Ceramic (0805 or through-hole) Critical: Prevents 3.3V rail brownouts during read spikes. $0.10
Wiring 28 AWG Silicone Jumper Wires (F-F) Silicone insulation prevents melting if you accidentally short VCC to GND. $5.00

Pin Mapping & Wiring the SOIC8 Clip

The W25Q32JV operates at 3.3V, which perfectly matches the Raspberry Pi's GPIO logic levels. Warning: If you are hacking a device with a 5V flash chip (like an older AT45DB series), you must use a BSS138 bi-directional logic level converter between the Pi and the chip, or you will fry the Pi's GPIO pins.

GPIO to SPI Flash Pinout

Flash Pin (SOIC8) Pin Name Raspberry Pi GPIO (Physical Pin) Wire Color Recommendation
1/CS (Chip Select)GPIO 8 (Pin 24)Yellow
2DO (MISO)GPIO 9 (Pin 21)Blue
3/WP (Write Protect)3.3V (Pin 1) - Tie HighRed
4GNDGND (Pin 25)Black
5DI (MOSI)GPIO 10 (Pin 19)Green
6CLK (SCK)GPIO 11 (Pin 23)Orange
7/HOLD3.3V (Pin 17) - Tie HighRed
8VCC3.3V (Pin 1)Red
Pro-Tip: The Decoupling Capacitor Rule
The Raspberry Pi's 3.3V GPIO header rail is limited to roughly 50mA. When an SPI flash chip transitions from standby to active read, it can spike to 30mA. If your wire length exceeds 4 inches, the inductance of the wire will cause a momentary voltage sag, resetting the chip mid-dump. Solder a 100µF capacitor directly across the VCC and GND wires at the clip, not at the Pi header.

The Python SPI Dump Script

This script targets Raspberry Pi OS Bookworm. It uses spidev for the SPI bus and gpiozero for the Chip Select line. We avoid the deprecated RPi.GPIO library, which throws warnings on modern kernels.

Prerequisite: Enable the SPI interface via sudo raspi-config (Interface Options > SPI) and reboot.

import spidev
import time
from gpiozero import OutputDevice
import sys
import os

# --- Configuration ---
CS_PIN = 8  # GPIO 8 (SPI0 CE0)
CHIP_SIZE_BYTES = 4 * 1024 * 1024  # 4MB for W25Q32
CHUNK_SIZE = 4096  # 4KB read chunks
OUTPUT_FILE = "firmware_dump.bin"

# --- Hardware Initialization ---
spi = spidev.SpiDev()
try:
    spi.open(0, 0)  # Bus 0, Device 0
    spi.max_speed_hz = 2000000  # 2MHz is safe for clipped connections
    spi.mode = 0
except FileNotFoundError as e:
    print(f"Fatal: {e}")
    print("Fix: SPI is not enabled. Run 'sudo raspi-config' and enable SPI.")
    sys.exit(1)
except PermissionError as e:
    print(f"Fatal: {e}")
    print("Fix: Run script with sudo, or add your user to the 'spi' group.")
    sys.exit(1)

# active_high=False means cs.on() drives the pin LOW (asserting CS)
cs = OutputDevice(CS_PIN, active_high=False)

def read_jedec_id():
    """Reads the Manufacturer and Device ID to verify connection."""
    cs.on()  # Assert CS (Drive LOW)
    # 0x9F is the standard JEDEC ID command, followed by 3 dummy bytes
    resp = spi.xfer2([0x9F, 0x00, 0x00, 0x00])
    cs.off() # Deassert CS (Drive HIGH)
    return resp[1:]

def dump_flash():
    """Dumps the entire flash memory to a binary file."""
    jedec = read_jedec_id()
    if jedec == [0x00, 0x00, 0x00] or jedec == [0xFF, 0xFF, 0xFF]:
        print("Error: Read all 0x00 or 0xFF. Check wiring and clip seating.")
        sys.exit(1)
    
    print(f"Detected JEDEC ID: {jedec[0]:02X} {jedec[1]:02X} {jedec[2]:02X}")
    print(f"Starting dump of {CHIP_SIZE_BYTES // (1024*1024)}MB to {OUTPUT_FILE}...")
    
    with open(OUTPUT_FILE, "wb") as f:
        for address in range(0, CHIP_SIZE_BYTES, CHUNK_SIZE):
            # Standard Read Command (0x03) + 3-byte address
            cmd = [
                0x03,
                (address >> 16) & 0xFF,
                (address >> 8) & 0xFF,
                address & 0xFF
            ]
            # Append dummy bytes to clock out the data
            cmd.extend([0x00] * CHUNK_SIZE)
            
            cs.on()
            resp = spi.xfer2(cmd)
            cs.off()
            
            # Write only the data payload (strip the 4 command/address bytes)
            f.write(bytes(resp[4:]))
            
            # Progress indicator
            progress = (address + CHUNK_SIZE) / CHIP_SIZE_BYTES * 100
            print(f"\rProgress: {progress:.1f}%", end="", flush=True)
            
            # Small delay to prevent watchdog/brownout on marginal power supplies
            time.sleep(0.001)
            
    print("\nDump complete!")

if __name__ == "__main__":
    try:
        dump_flash()
    except KeyboardInterrupt:
        print("\nDump aborted by user.")
    finally:
        spi.close()
        cs.close()

Debugging: When the Dump Fails

Hardware hacking rarely works on the first try. If your script fails, do not blindly rewrite the code. The issue is almost always physical. Here is how to interpret the errors and the first three things to check.

Exact Error: FileNotFoundError: [Errno 2] No such file or directory: '/dev/spidev0.0'

Ranked Causes:

  1. SPI Overlay Missing: You forgot to enable SPI in raspi-config, or the dtparam=spi=on line is commented out in /boot/firmware/config.txt.
  2. Kernel Module Not Loaded: Run lsmod | grep spi_bcm2835. If it returns nothing, run sudo modprobe spi_bcm2835.

Exact Error: JEDEC ID: FF FF FF (or 00 00 00)

This isn't a Python exception; it's a logical failure. The Pi is talking, but the chip isn't answering, or the MISO line is floating.

The First 3 Things to Check When Connection Fails:
  1. Clip Seating & Continuity: Use a multimeter in continuity mode. Probe the back of the SOIC8 clip where the wires insert, and verify it beeps when you touch the corresponding leg on the chip. Pomona clips can occasionally fail to bite through oxidation on older PCBs.
  2. The Decoupling Brownout: Hook your multimeter up to the VCC and GND wires at the clip. Set it to Min/Max mode. Run the script. If the voltage dips below 2.9V during the read attempt, your Pi's 3.3V rail is browning out. Add a larger capacitor or power the chip from an external 3.3V bench supply (sharing a common GND with the Pi).
  3. Chip Select (CS) Logic: Verify with an oscilloscope or logic analyzer that GPIO 8 is actually pulling LOW when the script runs. If you wired CS to the wrong pin, the chip will ignore the clock.

Extending the Hack: Firmware Analysis

Once you have firmware_dump.bin, the hardware hack is complete, and the software reverse-engineering begins.

How to Extend: Install Binwalk (sudo apt install binwalk). Run binwalk -e firmware_dump.bin. Binwalk will scan the binary for file signatures, extract embedded SquashFS filesystems, and allow you to grep through the extracted files for hardcoded MQTT broker credentials, SSH keys, or backdoor admin passwords.

How to Simplify: If you don't want to deal with Python scripts and GPIO wiring, buy a CH341A USB SPI Programmer ($10 on Amazon). It comes with its own SOIC8 clip and Windows GUI software. It is less flexible than the Pi for automated exploitation, but much faster for a one-off backup.

FAQ: Hardware Hacking Projects with Raspberry Pi

What are the best hacking projects with Raspberry Pi for beginners?

Before attempting SPI flash dumping, beginners should start with I2C bus scanning and UART console extraction. Connecting to a device's UART (TX/RX) pins using a $5 USB-to-TTL serial adapter (like the CP2102) and interrupting the bootloader (often by pressing 'Enter' or 'Ctrl+C' during boot) to drop into a root U-Boot shell is the highest-yield, lowest-difficulty hardware hack available.

Can I use Raspberry Pi 5 for hardware hacking projects?

Yes, but with a major caveat. The Raspberry Pi 5 uses the RP1 southbridge chip, which completely changed how GPIO is handled at the kernel level. The legacy RPi.GPIO Python library does not work reliably on the Pi 5. You must use gpiozero (which uses the rpi-lgpio backend in Bookworm) or the lgpio C/Python bindings directly. The SPI hardware itself works perfectly, provided you use the updated libraries shown in this guide.

Why is my SPI flash dump returning all 0xFF bytes?

A dump of pure 0xFF means the flash memory is either completely erased, or the MISO (Master In Slave Out) line is disconnected/floating high. If you are hacking a populated PCB, the system's main microcontroller might be fighting you for control of the SPI bus. To fix this, you often need to hold the main MCU in a reset state (by pulling its NRST pin to GND) so it doesn't interfere with your Pi's read commands.