Firmware / deep
UEFI firmware inspection, modification and recovery
Read the SPI chip, write one region back, and recover a board that will not POST
Revised 2026-08-28 · 16 min · risk: can brick
A desktop firmware image is four regions with three owners, and only one is yours to write. This note reads the SPI NOR chip, inspects the image in UEFITool, writes back a single region, and recovers a board that no longer POSTs. The bench ends with two matching dumps and a board booting from a write you verified.
Bench and scope§
Two boards, open bench, 21 °C ambient. Primary: Z790 ATX, BIOS 2103, a Winbond W25Q128JVSIQ in SOIC-8 at U19, PSU unplugged and standby drained before every clip. Secondary: X670E on AGESA 1.2.0.3c, to show where AMD differs. Programmers: a black-board CH341A modified to 3.3 V and meter-verified, and a Raspberry Pi 4 on Bookworm driving /dev/spidev0.0. Tools: flashrom 1.4.0, UEFITool NE A68, UEFITool 0.28.0 for edits, UEFIExtract, iucode_tool 2.3.1.
Assume throughout that a bad write leaves a board that does not POST and cannot be recovered from software. That is the working condition, not a footnote.
Four regions, three owners§
On an Intel platform the chip is not one image. The first 4 KB is the flash descriptor: the Flash Region Base Address table (FRBA) gives each region's start and end, and the Flash Master Base Address table (FMBA) gives every bus master a read and a write bitmask over them.
- FLMSTR1 — host CPU / BIOS
- What the CPU reaches through the PCH SPI controller. It decides whether
flashrom -p internalcan do anything at all. - FLMSTR2 — CSME
- The management engine. On a production board it owns its region and nobody else reads it.
- FLMSTR3 — GbE controller
- The integrated NIC's MAC address and PHY configuration. Small, and locked to everyone but itself.
A production descriptor grants FLMSTR1 read and write on BIOS, read-only on the descriptor, and nothing on ME or GbE. flashrom prints this before doing anything:
FREG0: Flash Descriptor region (0x00000000-0x00000fff) is read-only.
FREG1: BIOS region (0x00500000-0x00ffffff) is read-write.
FREG2: Management Engine region (0x00003000-0x004fffff) is locked.
FREG3: Gigabit Ethernet region (0x00001000-0x00002fff) is locked.
Read that table first. A region marked locked cannot be read or written from the host at any privilege level — chipset enforcement, not a check that root defeats. The PCH also carries Protected Range registers PR0 to PR4, which firmware sets at boot to fence off part of the BIOS region on top of the descriptor mask; both must be clear for a host write to land.
AMD has no descriptor. The image opens with a firmware entry table pointing at the PSP directory ($PSP) and the BIOS directory ($BHD); enforcement lives in the PSP, not in a mask you can read off sector zero. Expect the host path closed and plan for the clip.
Reading the image in UEFITool§
Open a dump in UEFITool NE and you get a tree. Under the image root sit the descriptor and the four regions. Inside the BIOS region are firmware volumes, each with a filesystem GUID; inside each, FFS files typed as PEI module, DXE driver, freeform, raw or volume image. Each file holds sections: PE32 for the executable, user interface for the readable name, DXE dependency, often a compression or GUID-defined section wrapping the rest.
Ctrl+F searches decompressed section bodies, so a driver's user interface string — PchSmiDispatcher, AmdCpmOemInitDxe — hits even inside a compressed volume. Searching by GUID finds the file itself; the status bar gives the offset.
Variable stores form their own subtree: AMI Aptio V images carry an NVAR store, EDK2-derived images a VSS store signed $VSS or $SVS. UEFITool NE parses both and lists variables by name, confirming a setup variable exists before you hunt for it at runtime.
Padding is whatever sits outside every volume. Empty padding is 0xFF or 0x00 alignment filler and is uninteresting. Non-empty padding is data the parser could not classify — an embedded controller image, a boot logo, a vendor table — worth a look, usually mundane.
One version fact that costs people an afternoon: UEFITool NE (the A-series) is read-only — no replace, insert or rebuild. The last release that modifies an image is 0.28.0, with an older parser. Keep both. For scripted work use UEFIExtract, whose CSV report sorted by GUID diffs two BIOS versions cleanly:
UEFIExtract dump1.bin all # full tree to dump1.bin.dump/
UEFIExtract dump1.bin report # CSV inventory of every file and section
UEFIExtract -h # remaining modes, including GUID extraction
The microcode region§
Microcode updates sit in the BIOS region as raw data, referenced by the Firmware Interface Table. The FIT pointer is at physical 0xFFFFFFC0 near the reset vector; entries of type 0x01 point at updates. Each begins with the 48-byte Intel header: header version, update revision, date in BCD, processor signature, checksum, loader revision, processor flags and sizes.
A BIOS ships several revisions because one board takes several CPUs. A Z790 image carries an update per Alder Lake and Raptor Lake stepping the socket accepts; the CPU loads the one whose processor signature and platform flags match and ignores the rest. There is no single "microcode version".
The legitimate reason to look is verification — release notes say a BIOS updates microcode, the image says which revision it actually carries. Parse the container, then compare against the running CPU:
iucode_tool -tb -l extracted-microcode.bin
/sys/devices/system/cpu/cpu0/microcode/version
cat /sys/devices/system/cpu/cpu0/microcode/version
dmesg | grep -i microcode
If the loaded revision exceeds anything in the image, the distribution is late-loading from /lib/firmware/intel-ucode/ and masking what the board provides; boot with dis_ucode_ldr to see the board alone.
Revision changes platform behaviour, and downgrading is how some of it comes back — boost and voltage/frequency algorithms ride in microcode, which is why a curve stable before an update may not be after; see per-core curve optimizer. Vendors increasingly block it: a CPU will not load a revision older than the one already applied in the same boot, and Linux refuses late-load downgrades. On a board with Boot Guard in verified mode the initial boot block is signed against a key hashed into PCH fuses — edit what the IBB covers and it will not execute. Establish whether your board fuses Boot Guard before planning a modification.
Reading and writing from the host§
Ask flashrom what it sees before touching anything:
flashrom -p internal --flash-name
flashrom -p ch341a_spi --flash-size
Detection is by JEDEC ID, and IDs are shared across die revisions with different erase and protection behaviour. When flashrom cannot disambiguate it stops:
Found Winbond flash chip "W25Q128.V" (16384 kB, SPI) on ch341a_spi.
Found Winbond flash chip "W25Q128.W" (16384 kB, SPI) on ch341a_spi.
Multiple flash chip definitions match the detected chip(s): "W25Q128.V", "W25Q128.W"
Please specify which chip definition to use with the -c <chipname> option.
Read the part number off the package with a loupe and pass -c. Guessing between a 3.3 V and a 1.8 V part is how chips die. Then dump twice and compare before believing either:
flashrom -p ch341a_spi -c W25Q128.V -r dump1.bin
flashrom -p ch341a_spi -c W25Q128.V -r dump2.bin
cmp dump1.bin dump2.bin && sha256sum dump1.bin dump2.bin
A difference from cmp does not mean a marginal dump; it means no dump. Fix the connection first.
Scope writes to one region. On the host, --ifd reads the layout out of the image's own descriptor:
flashrom -p internal --ifd -i bios -r bios-only.bin
flashrom -p internal --ifd -i bios -w vendor-full.bin
Through a programmer there is no host descriptor to consult, so supply the layout yourself:
regions.txt
00000000:00000fff fd
00001000:00002fff gbe
00003000:004fffff me
00500000:00ffffff bios
flashrom -p ch341a_spi -c W25Q128.V -l regions.txt -i bios -w vendor-full.bin
Confining the write to bios keeps your descriptor, ME configuration and GbE MAC in place. A full unverified image overwrites all four regions with someone else's, including a MAC that is not yours. Never use -n; the verify pass is the only automatic confirmation you get.
Clipping the chip§
SOIC-8 SPI NOR pinout, pin 1 at the dot with the marking upright, numbering counter-clockwise:
- 1 CS#, 6 CLK, 5 DI, 2 DO
- The bus. DI is the chip's input and goes to the programmer's MOSI; DO is its output, to MISO.
- 3 WP#, 7 HOLD# (or RESET#)
- Active low, both held high for single-SPI access. Most clip adapters tie them to VCC; if yours does not, tie them yourself.
- 8 VCC, 4 GND
- 3.3 V on a W25Q128JV. Check the suffix — 1.8 V variants share the package and will not survive 3.3 V.
The hard part is contention, not wiring. The PCH's SPI master sits on the same four signals and drives them whenever +3.3 V standby is present — any time the PSU is plugged in, powered or not. Unplug at the wall, hold the power button ten seconds, confirm the standby LED is out. Even then, powering VCC from the programmer back-feeds the standby rail through pin 8: the regulator ends up powering part of the motherboard, the rail sags, reads come back inconsistent. That is the mechanism behind most mismatched dumps.
In-circuit with the programmer supplying VCC works on many boards; try it first. If the rail sags, isolate — lift pin 8, or desolder the chip onto a SOIC-8 ZIF adapter. Feeding the standby rail from a bench supply while the programmer drives only signals sounds clever and reintroduces the contention you were removing.
A Raspberry Pi avoids this structurally: its GPIO is natively 3.3 V. Set dtparam=spi=on in /boot/firmware/config.txt, reboot, and wire header pin 17 (3V3) to chip 8, 25 (GND) to 4, 24 (CE0) to 1, 23 (SCLK) to 6, 19 (MOSI) to 5, 21 (MISO) to 2.
flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=2000 -c W25Q128.V -r dump1.bin
spispeed is in kHz. Start at 2000, drop to 512 if dumps disagree; clip leads are unshielded and long ones will not hold a clean edge.
Vendor recovery paths§
Before reaching for a clip, check whether the board reflashes itself with no CPU and no memory installed: ASUS BIOS FlashBack, Gigabyte Q-Flash Plus, MSI Flash BIOS Button, ASRock BIOS Flashback. A controller on standby power reads a file from one specific USB port and programs the chip directly.
The filename is exact and vendor-specific. ASUS ships a BIOSRenamer utility inside the download archive that produces the model-specific name; run it rather than guessing. Gigabyte wants the extracted image renamed as the manual specifies, GIGABYTE.bin on most recent boards. MSI wants MSI.ROM, ASRock creative.rom. Case matters on some implementations. These paths fail silently more often than loudly, for dull reasons:
- the archive was never extracted, so the controller sees a
.zip; - the stick is exFAT, NTFS or GPT — it must be FAT32 on MBR, file in the root;
- a USB 3.x-only stick the controller cannot enumerate; plain USB 2.0 usually works;
- the wrong port — only the marked one reaches the flash controller;
- 24-pin connected but the EPS 8-pin left off, which many boards require;
- a downgrade below the board's floor, common on AM4 across AGESA generations.
If the LED never blinks, the controller did not accept the file; work that list rather than repeating the button press. ASUS CrashFree BIOS 3 runs on the main CPU from an intact recovery block, so it needs a board that still executes code — which a botched write rarely leaves you.
What to measure and record§
Measure the electrical side before trusting a byte. With the clip on and the board de-powered, meter pin 8 to pin 4: 3.3 V nominal, and under 3.0 V means the programmer is trying to power the board. Sag under load during a read is the same fault. With a scope, look at CLK on pin 6 — rounded edges at 2000 kHz means shorter leads or a lower spispeed.
Record per session: the flashrom detection line with chip name and size; the FREG table if reading from the host; sha256sum of every dump; board model and BIOS version from dmidecode -t 0. A good dump is the right size to the byte and hashes identically across two independent reads. All 0xFF, all 0x00, or a repeating pattern is a wiring result, not a firmware one.
After a write, flashrom's verify pass must pass; then read the chip into a third file and compare it against what you meant to write. Confirm at the board: it POSTs, the version string matches, and the settings you rely on elsewhere — the lane split in PCIe bifurcation and lane topology, the load-line behaviour in VRM load-line and thermal analysis — survived rather than silently reverting to vendor defaults.
Failure table§
| Symptom | Root cause | Corrective action |
|---|---|---|
| Dump is all 0xFF | Chip unpowered, or CS# never asserted | Meter pin 8 to pin 4 for 3.3 V; reseat on pin 1 |
| Dump is all 0x00 | MISO held low, or chip in reset | Tie WP# and HOLD# to VCC; check for a shorted jaw |
| Two dumps differ | Marginal signal integrity or rail sag | Drop spispeed to 512, shorten leads, isolate pin 8 |
| "Multiple flash chip definitions match" | Shared JEDEC ID across die revisions | Read the package marking, pass the exact -c |
| Host write refused, region locked | Descriptor FLMSTR1 mask | Not defeatable from the host; use an external programmer |
| Host write refused, range read-only | PCH Protected Range register set at boot | External programmer, or firmware leaving PR0 to PR4 clear |
| No POST, no debug LEDs after a write | Wrong image, or an interrupted erase | Reflash the verified original dump externally |
| POSTs, halts after a modification | Boot Guard verification of the boot block | Restore the original; the IBB is fixed once fused |
| FlashBack LED never blinks | Filename, filesystem, port, or downgrade floor | Vendor renamer onto FAT32 MBR USB 2.0, marked port |