Introduction
Password prompts in bootloaders are a strange thing. Everybody who ships one knows it only buys time, and yet it keeps showing up in the threat model as if it were a real boundary. During a recent hardware assessment I spent a few evenings finding out exactly how much time one of them buys. The answer, on this particular board, was about forty microseconds.
The target is an embedded control unit built around an NXP i.MX6UL, running a
vendor build of U-Boot 2025.04. Secure Boot is enabled in production, the SPL
authenticates the next stage, measured boot extends PCRs 11 through 13, and the
interactive U-Boot shell is locked behind a password. On paper that is a decent
stack. The password itself is stored as a SHA-512 crypt hash in the
bootstopkeycrypt environment variable, which means there is nothing useful to
read out of the flash and nothing worth brute forcing.
So I did not attack the password. I attacked the instruction that acts on the result of comparing it.
The target instruction
Getting to the prompt is the boring part. You interrupt the autoboot countdown with three presses of ESC:
U-Boot SPL 2025.04-<redacted> (<redacted>)
DRAM: 512 MiB
Trying to boot from MMC1
Authenticate image from DDR location 0x877fffc0...
U-Boot 2025.04-<redacted> (<redacted>)
CPU: i.MX6UL rev1.2 at 696MHz
CPU: Automotive temperature grade (-40C to 125C) at 55C
Reset cause: POR
DRAM: 512 MiB
Core: 58 devices, 23 uclasses, devicetree: fit
WDT: Started watchdog@20bc000 with servicing every 1000ms (120s timeout)
MMC: FSL_SDHC: 0
SEC0: RNG instantiated
Sec: Measured boot: PCR 11/12/13 extended
Net: eth0: ethernet@2188000
Normal Boot
Hit 3 * ESC key to stop autoboot (bootdelay=5sec)...
What happens after you type a password is the interesting part. U-Boot feeds your
input to crypt() with the salt from bootstopkeycrypt, SHA-512 with 5000 rounds,
then hands both strings to strcmp(). Non-zero means you branch to the
“*** Invalid password ***” path. Zero means you get a shell.
That is one branch. One conditional, taken once, on a value held in one register. Decades of fault injection literature exist because of code shaped exactly like this, and the vendor handed me a very convenient property on top: the 5000 rounds of SHA-512 before the comparison take roughly 90 milliseconds. A long, loud, easily recognisable chunk of work sitting immediately in front of the instruction I want to corrupt. If you ever wondered why people put the expensive hash after nothing in particular, this is why it matters.
Hardware
Nothing exotic here, and that is sort of the point:
- Glitch controller: a Digilent Arty S7-50. The 100 MHz system clock gives 10 ns resolution, which is at least an order of magnitude finer than I ended up needing.
- Crowbar: a 2N3904 NPN, collector on the i.MX6UL
VCC_ARMrail, emitter to ground. When the FPGA pulls the base high, the core supply goes to the floor. - UART: a plain USB-to-UART bridge on the debug header.
The only part that takes actual effort is getting onto VCC_ARM. I found the rail
with a multimeter (1.35 V nominal), picked a decoupling capacitor sitting close to
the SoC, lifted it with a heat gun, and soldered a 0.1 mm copper wire to the
now-free pad. That wire is the glitch injection point.

Removing the capacitor is not optional. Decoupling capacitors exist precisely to keep the supply stable across sudden current demand, which is another way of saying they exist to absorb your glitch. Leave them in place and the crowbar fights the entire local capacitance of the board.
For the trigger I needed something that happens at a fixed distance from the comparison. The password itself is perfect: the falling edge of the first UART start bit of the test password is T=0 for everything that follows. I picked that signal off the TX pin of the MAX13223E RS-232 transceiver with a PCBite SP10 clip and fed it to the FPGA.

Finding the window
I did not have a debugger on this target, so the timing model had to come from the UART output alone. The trick is to use the serial console as a very coarse oscilloscope: glitch at offset X, see what the board says, and work backwards.
The one landmark that is directly observable is printf(). At an offset of
91880 µs with a 50 µs pulse, the device emitted a single * and then stopped,
which means the glitch landed just as the first character of
*** Invalid password *** was being pushed out. That is the upper bound. The
comparison has to be before it.
With that anchor, a few hundred runs of scanning gave me this:
| Event | Offset from T=0 | Notes |
|---|---|---|
crypt() entered |
~1.3 ms | SHA-512, 5000 rounds |
crypt() returns |
89.5 to 94.7 ms | 5.2 ms of jitter between boots |
strcmp() |
90.65 to 95.97 ms | tracks the crypt() duration |
first printf() |
~91.88 ms | first * observable here |
The 5.2 ms of jitter is the whole difficulty of this attack. It comes from the boot path itself (clock ramping, watchdog servicing, environment loading from MMC) and it means that there is no single offset that is always correct. There is only an offset that is correct often enough. This is why the campaign script fires thousands of attempts instead of one well-aimed shot.
Pulse width turned out to be far more sensitive than I expected, with a very hard threshold:
| Pulse width | Result |
|---|---|
| ≤ 20 µs | nothing. The CPU does not notice. |
| 25 to 35 µs | first effects, 12 to 19% hang rate. Undefined state, no usable bypass. |
| 40 µs | ~40% effect rate, and the successful bypasses live here. |
| 50 to 100 µs | ~50% effect rate but almost entirely data aborts and prefetch aborts. Too violent to be useful. |
That 40 µs band is narrow enough to be frustrating and wide enough to be reproducible. Below it the core rides out the dip, above it you are not corrupting one instruction any more, you are resetting the processor in slow motion.
Final configuration:
# FPGA clock: 100 MHz -> 1 cycle = 10 ns
TARGET_OFFSET = 9_178_000 # 91780 us after the UART trigger
GLITCH_WIDTH = 4_000 # 40 us (confirmed window: 91760-91790 us)
Here is what the rail actually does. VCC_ARM leaves 1.35 V, sits near zero for the
duration of the crowbar pulse, and then recovers on the RC of whatever decoupling
is left on the board:

The bypass
The campaign script sends wrongpassword every single time, watches the console,
and classifies the outcome. Most attempts are resets or hangs. Then, on attempt 27
of a 5000 attempt run:
[23:36:58] [0022/5000] off=91770.0µs → ⚡ reset
[23:37:03] [0023/5000] off=91785.0µs → 🔴 hang
[23:37:08] [0024/5000] off=91780.0µs → ⚡ reset
[23:37:13] [0025/5000] off=91790.0µs → ⚡ reset
[23:37:18] [0026/5000] off=91780.0µs → 🔴 hang
[23:37:23] *** BYPASS! off=91785.0µs wid=40µs ***
[23:37:23] >> version
version
U-Boot 2025.04-<redacted> (<redacted>)
arm-linux-gnueabi-gcc (GCC) 14.3.0
GNU ld (GNU Binutils) 2.44.0.20250715
=>
Twenty-seven attempts at roughly five seconds each. Call it two and a half minutes of wall clock time, after the setup work. The password was wrong in every single one of those attempts, including the one that worked.
help confirms it is not a degraded shell. Everything is there, including fuse,
hab_auth_img, mmc, tpm2 and a vendor-specific firmware update command.
What this actually gets you
This is the part worth being precise about, because “bootloader bypass” means very different things depending on what else is enabled.
Secure Boot is still doing its job. I cannot load an unsigned kernel, because the glitch lands in the password routine and not in the image verification path. HAB remains perfectly happy and completely irrelevant.
What I get instead:
- The full environment, including
bootstopkeycryptitself, the partition layout, network configuration and every boot script. - Raw eMMC access through
mmc read. A sector level dump of the entire device without ever booting Linux, which walks straight around every filesystem permission on it. - eFuse readout through
fuse read. - EEPROM access through a vendor-specific EEPROM command, which is where the device-specific calibration and configuration data lives.
- Persistent boot argument manipulation through
setenvandsaveenv.
That last one deserves its own paragraph. The U-Boot environment is not covered by
signature verification. So I can append init=/bin/sh to bootargs, save it, and
the next boot hands me a root shell on a fully signed, fully verified kernel. Secure
Boot verified exactly what it promised to verify and the thing it did not cover was
the configuration telling it what to do afterwards.
The signed image is authentic. It is just doing what I told it to.
Fixing it
The core problem is structural: the authentication decision collapses into a single branch on a single return value, which makes it a single instruction target. No amount of hashing in front of that branch helps, and arguably the expensive hash makes it worse by providing a free, highly visible timing landmark.
The fix is the standard glitch-resistant pattern. Evaluate the comparison result more than once, in independent locations in the code, store it in several registers using different encodings (not 0 and 1, which are both one bit flip apart from everything), and require all of the checks to agree before granting the shell. One glitch corrupts one branch. It does not corrupt three of them in different places, encoded differently, in the same run.
While you are at it, protect the environment. If bootargs can be rewritten from an
unauthenticated prompt and survives a reboot, then Secure Boot only covers half the
thing it appears to cover.
Potting the area around the SoC in epoxy is worth mentioning but belongs firmly in
the “raises the cost” category rather than the “fixes it” category. It makes finding
VCC_ARM and lifting that decoupling capacitor genuinely annoying. It does not make
it impossible, and it is not a substitute for the redundant check.
Conclusion
None of this is novel. Crowbar glitching a core supply rail to skip a branch is well-trodden ground, the hardware costs less than a decent multimeter, and the technique is on the syllabus of every hardware security course worth attending.
What I find worth repeating is how the pieces lined up. A password prompt that looks like a boundary, a 90 millisecond hash that very helpfully announces when the decision is about to be made, a single branch carrying the whole decision, and an environment outside the chain of trust that converts shell access into persistent root on a signed system. Every one of those is defensible on its own. Together they turn forty microseconds of missing supply voltage into the device.
If your threat model includes someone holding your hardware, assume the bootloader password is a speed bump and design the next layer accordingly.