Berkner Tech

Identifying the Bootloader in a Flash Dump

Identifying the bootloader inside a raw embedded flash dump, from binwalk scan to U-Boot environment

The bootloader runs first, sets up the hardware, and decides what code the device is allowed to run next. Find it in a raw flash dump and you learn exactly how a device defends itself at startup, or whether it defends itself at all. Here is the workflow I use to locate and read it on real hardware.

Why the Bootloader Is the First Thing I Check

Secure boot, if it exists, lives in the bootloader. So does the answer to the question that shapes the rest of an assessment: does this device verify the next stage before running it, or does it trust whatever sits in flash? Before I reverse engineer the application, I want the bootloader, because it tells me where the root of trust is and whether it actually holds.

On Linux-class devices that usually means U-Boot. On bare-metal microcontrollers it means a small first-stage loader sitting at the reset vector. The two look nothing alike in a dump, so the first job is figuring out which one you are holding before you spend an hour analyzing the wrong bytes.

Start With a Map of the Image

Confirm what you have, then let binwalk lay out the structure. A single scan usually shows where the bootloader ends and the kernel begins, and it does it in seconds.

# what kind of file is this
file firmware.bin

# scan for known signatures and offsets
binwalk firmware.bin
Example output
firmware.bin: data

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             uImage header, header size: 64 bytes, header CRC: 0x2A1F
64            0x40            U-Boot 2021.04 (ARM), load addr: 0x80000000
197632        0x3040          LZMA compressed data
1248304       0x130C30        Squashfs filesystem, little endian, version 4.0

That first block is the bootloader. Note the offset (0x40) and the version string U-Boot 2021.04. The rest of the image, the compressed kernel and the SquashFS root filesystem, comes after it. You now know the layout before touching anything else, and that map guides every later step.

Confirm the Architecture Before You Disassemble

If binwalk does not name the CPU, a quick opcode scan settles it. Pointing binwalk -A at the image reports the instruction sets it recognizes, which tells your disassembler how to read the bytes.

# scan for executable opcodes and report the architecture
binwalk -A firmware.bin | head
Example output
DECIMAL    HEXADECIMAL   DESCRIPTION
----------------------------------------------------------
68         0x44          ARM instructions, function prologue
512        0x200         ARM instructions, function epilogue
2364       0x93C         ARM instructions, function prologue
# dominant match: ARM (32-bit, little endian)

Guessing the architecture wrong is the most common reason a bootloader looks like garbage in a disassembler. Confirm it here, in seconds, rather than after an hour of staring at nonsense that was never meant to be read as the wrong instruction set.

Spotting U-Boot

U-Boot is loud. It is full of recognizable strings, so strings confirms it instantly and often reveals the exact build and the date it was compiled.

# pull printable strings, keep ones 8+ chars, grep for the banner
strings -n 8 firmware.bin | grep -i 'u-boot'
Example output
U-Boot 2021.04 (May 18 2023 - 11:42:07 +0000)
U-Boot SPL 2021.04
uboot_version
Hit any key to stop autoboot
U-Boot> 

A dated banner like that is worth a quick search against public CVEs, because plenty of shipped devices run a U-Boot that is years behind. The Hit any key to stop autoboot and U-Boot> strings are also a strong hint that an interactive console may be reachable over the serial port, which is a separate way into the same device.

Carving the Bootloader Out for a Disassembler

To analyze the loader on its own, carve it out with dd using the offset and size binwalk reported. Working on the isolated block keeps your disassembler from choking on the compressed kernel and filesystem that follow it.

# extract the bootloader region starting at offset 0x40 (64 decimal)
dd if=firmware.bin of=uboot.bin bs=1 skip=64 count=197568

Load uboot.bin into Ghidra or radare2 at the load address from the header, which was 0x80000000 here. Setting the correct base address is what makes the cross references resolve, so the boot command parser and the verification routines line up where the code actually expects to find them.

Reading the U-Boot Environment

The environment block is where the interesting decisions live. It holds the boot command, the kernel arguments, and the autoboot delay. Pull it straight out of the image with a targeted grep before you do anything heavier.

strings firmware.bin | grep -E '^(bootcmd|bootargs|bootdelay|baudrate)='
Example output
bootdelay=2
baudrate=115200
bootcmd=sf probe; sf read 0x80000000 0x40000 0x400000; bootm 0x80000000
bootargs=console=ttyS0,115200 root=/dev/mtdblock3 rootfstype=squashfs

Three things jump out. bootdelay=2 means a two second window to interrupt boot and drop to a U-Boot prompt over the serial console. console=ttyS0,115200 tells you the console is live and at what rate. And bootcmd loads the kernel with bootm and no signature check anywhere in sight.

Walking the Boot Command

The boot command is worth reading slowly, because it is the device describing its own startup in one line. Here it probes the SPI flash, copies a region into RAM at 0x80000000, and jumps into it with bootm. There is no bootm variant that verifies a signature, no FIT image, and no key in the path.

That absence is the headline finding. A device whose boot command loads and runs whatever is at a fixed flash offset will run modified firmware just as happily as the original. Compare that to a secure design, where the command would invoke a verified boot step and refuse to continue if the signature did not match.

When It Is a Bare-Metal MCU Bootloader

A microcontroller image is quieter. There is no banner, so you fall back to structure. On an ARM Cortex-M, the first eight bytes are the initial stack pointer followed by the reset vector, which points at the first instruction the chip runs after power-up.

# dump the first 16 bytes of an MCU image
hexdump -C -n 16 mcu_firmware.bin
Example output
00000000  00 80 00 20 c1 01 00 08  09 02 00 08 0d 02 00 08  |... ............|
# initial SP = 0x20008000   reset vector = 0x080001c1

A stack pointer in RAM, the 0x20000000 range, and a reset vector in flash, the 0x08000000 range, confirm a valid vector table and tell you the load address you need before the image makes any sense. From there the bootloader is the early code the reset vector leads to, usually clock and memory setup before it jumps to the application.

Following the Reset Vector Into Code

Once you know the load address, disassemble from the reset vector and read the first few hundred instructions. The first-stage loader is short and recognizable: it sets the clock tree, configures RAM, and copies the next stage into place before branching to it. Whatever validation exists happens right there, in the handful of instructions before that branch.

# disassemble from the reset vector with the correct base address
arm-none-eabi-objdump -D -b binary -m arm \
  --adjust-vma=0x08000000 mcu_firmware.bin | less

If the branch to the application is unconditional, the loader trusts whatever sits at the application’s address. If there is a comparison or a call into a hash or signature routine just before it, that is the check you have to understand, and later the check an attacker will try to skip with a glitch.

What the Bootloader Tells You About Security

Once located, the bootloader answers one question above all: does it verify the next stage. Search it for the language of verification before you commit to a deeper read.

strings uboot.bin | grep -iE 'verify|signature|rsa|sha256|secure boot|fit'
Example output
## Booting kernel from Legacy Image at 80000000 ...
   Verifying Checksum ... OK
Bad Magic Number

A CRC checksum is integrity, not authenticity. It catches corruption, not an attacker who recomputes it after editing the image. Seeing only Verifying Checksum and no signature, RSA, or FIT verification strings is the tell that secure boot is absent.

What a Signed Boot Chain Looks Like Instead

For contrast, a device with verified boot leaves very different fingerprints. The strings include FIT, an algorithm name like sha256,rsa2048, and often an embedded public key or its hash. The boot command invokes bootm on a FIT image with a configuration node, and the loader refuses to continue when verification fails.

strings uboot.bin | grep -iE 'fit|rsa2048|sha256|required.*conf|signature ok'
Example output
## Loading kernel from FIT Image at 82000000 ...
   Verifying Hash Integrity ... sha256,rsa2048:dev+ OK
   Verifying Signature ... OK
## checking conf required: yes

That difference, Verifying Signature ... OK versus a bare checksum, is the whole finding in two lines. It is also why I run this grep early: it sorts devices into “runs unsigned code” and “verifies its chain” before I invest any deeper effort.

Confirming the Finding on the Live Device

Static analysis tells you what the boot path looks like, and a few minutes on the live device proves it. If the autoboot delay left a window, interrupt it over the serial console and ask the loader directly. The environment you read out of the dump should match what the running bootloader reports, and any difference is itself worth understanding.

# interrupt autoboot, then confirm the boot path from the live prompt
=> printenv bootcmd
=> printenv bootargs
Example output
bootcmd=sf probe; sf read 0x80000000 0x40000 0x400000; bootm 0x80000000
bootargs=console=ttyS0,115200 root=/dev/mtdblock3 rootfstype=squashfs

When the live values match the dump and the loader boots without a single verification step, the unsigned-boot finding is no longer a theory from a static read, it is demonstrated on the hardware in front of you. That is the version of a finding that a product team cannot argue with, and the version that gets a fix prioritized.

A Repeatable Triage Checklist

On every new image I run the same short sequence so nothing gets skipped under time pressure. It takes a couple of minutes and almost always points at the next move.

# 1. structure
binwalk firmware.bin

# 2. loader identity
strings -n 8 firmware.bin | grep -iE 'u-boot|barebox|coreboot'

# 3. boot decisions
strings firmware.bin | grep -E '^(bootcmd|bootargs|bootdelay)='

# 4. verification language
strings firmware.bin | grep -iE 'verify|signature|rsa|fit'

If step four comes back empty, you have your answer before you open a disassembler: the device most likely runs unsigned code, and the rest of the assessment is about how cheaply an attacker can replace what is in flash.

Turning the Finding Into a Fix

Locating the bootloader is reconnaissance, but the value to a product team is the recommendation that follows. If the loader runs unsigned code, the fix is a verified boot chain anchored in a hardware root of trust, where each stage checks the signature of the next before handing off control.

If verification exists but the keys or the critical check are reachable, the fix is locking the debug interfaces and moving the root-of-trust key into fused or secure-element storage. Either way, the dump told you precisely which of those problems you have, which is why I always start here rather than in the application.

Where This Fits

Identifying and reading the bootloader is early reconnaissance, and it sets the agenda for everything after: firmware extraction, secure-boot analysis, and tracing each weakness back to the design decision behind it. If you are building a connected product and want to know what your boot chain actually verifies, that is the kind of work we do at Berkner Tech.


References and Further Reading