Skip to content
shaostassen.com

← home

2026 · Solo · in progress

Emulation-Based Fault Injection Harness

A campaign harness that glitches bare-metal firmware in emulation to measure which countermeasures actually stop a secure-boot bypass — across two instruction sets, with every result cross-checked against a second emulator.

exhaustive search:
95.9M fault pairs
cross-backend agreement:
100% outside CRASH
architectures:
Cortex-M3 + RV32I
Fault injectionEmbedded securityFirmwareCortex-MRISC-VEmulationPython

repo ↗

Problem

A voltage or clock glitch can make a CPU skip an instruction. One skipped branch turns a rejected firmware image into an accepted one. The countermeasures against this — duplicated checks, control-flow integrity, redundant computation — are usually defended by argument. I wanted them defended by measurement.

Constraints

  • Emulation, no glitch rig: instruction skip stands in for the physical effect, and that abstraction gets stated, not hidden.
  • Campaigns must be exhaustive and reproducible. A harness that returns different answers on different runs is worse than no harness, because its output still looks authoritative.
  • The double-fault space is ~96M runs, so throughput is a design constraint, not an optimization.

Approach

Two targets — a secure-boot verifier and a motor safety supervisor — built across a {base, hardened} × {-O0, -O2, -Os} matrix and run under Unicorn. Faults trigger on instruction count rather than PC: counts are totally ordered, PCs are ambiguous inside a loop. A 16-rung snapshot ladder turns an O(T) replay per fault into O(T/16). The firmware never judges itself — it reports only what it did, and the harness, which knows what input it supplied, owns the verdict.

Why it's technically hard

Running the campaigns is the easy part. The hard part is that the harness lies to you silently, and usually in the direction of a more exciting result. Six such bugs surfaced: writable flash leaking state between runs, fork() inheriting emulator state, and twice a telemetry field that a fault could corrupt into looking like a genuine bypass. That last one had produced a tidy, plausible conclusion — unoptimized hardened builds are the weakest — which was entirely an artifact. The real answer is the opposite: they are the strongest, fully closed. Retracting it meant re-running every number in the writeup. Every "exploitable" result is now checked against the firmware's own verdict field before it counts.

Result

Hardening closes the rollback and bad-magic vectors completely on both instruction sets. The two surviving -O2 bypasses trace to one specific weakness: redundant checks that only verify agreement with each other, which a forged image satisfies for free. An exhaustive 95.9M-pair double-fault search found 1,635 bypasses whose apparent variety collapsed into a single instruction-count drift effect. Cross-validating against QEMU on the identical binary gave 100% agreement on every outcome class carrying a security conclusion; the one divergence localized a real fidelity gap — Unicorn does not model Cortex-M exception entry, so the firmware's HardFault handler is dead code under it. Ported to RV32I, the same countermeasures measure roughly 3× weaker, and break entirely at three faults where Cortex-M holds.

Architecture
Build
Test vector

← instruction index in the golden trace → · rows are skip width k = 1…4

OKCRASHHANGSDCExploitable

Outcome counts for the Cortex-M3 base build against the forged vector
golden trace3,890 instructions · 15,560 experiments
OK14,604
CRASH456
HANG400
SDC78
SEC_BYPASS22
SAFETY_VIOLATION0
same build on RV32I23 exploitable
Each column aggregates ~8 instructions and takes the most severe outcome in the range, so a single exploitable instruction paints a whole column. That keeps rare findings visible at this width, but it over-states how much of the trace is dangerous — the counts above are exact.

What I'd do next

Hardware validation. Simulation establishes necessary conditions for exploitability, never sufficient ones — and the cross-check already found one place where a CPU emulator and a full machine model disagree, so a real part will find more. The open engineering thread is the safety oracle: an independent watchdog closes 42.2% of violations, and the other 57.8% is firmware that never stops responding. That half needs a mitigation which does not depend on liveness at all — a hardware trip watching gate-driver current, not a software state variable.