Why Encrypted Firmware Is Not Enough

Encrypting a firmware image stops the casual download-and-read attack, and that is worth something. But encryption is not a wall. The device has to decrypt its own firmware to run it, which means the key is on the device, and a key on the device is a key an attacker can chase.
The Appeal and the Limit
Encryption is satisfying because it is visible. You ship an image that is unreadable noise, and the firmware that used to give up its secrets to anyone with a chip clip now gives up nothing. For a casual attacker, that is the end of the road.
The limit is structural. A locked box that has to open itself every time it boots is not really locked, it is delayed. The questions that decide whether the encryption matters are where the key lives, how the device gets at it, and what the plaintext does once the device is running.
Confirm It Is Actually Encrypted
Before assuming a dump is encrypted, measure its entropy. Encrypted and compressed data both look like high-entropy noise, so binwalk -E is the first check. A flat line near the maximum across the whole image points at encryption rather than a normal mix of code, strings, and a compressed filesystem.
binwalk -E firmware.bin
DECIMAL HEXADECIMAL ENTROPY -------------------------------------------------------- 0 0x0 0.998121 Rising entropy edge (possibly encrypted or compressed) 1024 0x400 0.997930 524288 0x80000 0.998004 # entropy ~1.0 across the entire image, no low-entropy headers
A normal firmware image shows low-entropy regions where headers, padding, and configuration live. An image that is uniformly near 1.0 from start to finish, with no plaintext bootloader strings, is encrypted. That uniformity is itself the clue that tells you to go looking for the thing that decrypts it.
Tell Encryption From Compression
High entropy alone does not prove encryption, because gzip and LZMA also look random. Compression leaves headers, so a quick scan for known magic bytes rules it out before you assume the harder problem.
binwalk firmware.bin | grep -iE 'lzma|gzip|xz|zlib|squashfs'
(no output) # no compression or filesystem signatures anywhere -> not a packed image
No compression signature, no readable strings, and flat entropy together make the case for encryption. Now the work shifts to the one thing that cannot be encrypted: the code that does the decrypting.
Find the Decryption Stub
Something has to decrypt the image at boot, and that something cannot itself be encrypted, or the chip could never get started. There is almost always a small plaintext loader, a ROM routine, or a first-stage bootloader that holds or fetches the key. On many SoCs you can read that early stage separately, and its strings give the algorithm away.
strings bootrom_dump.bin | grep -iE 'aes|cbc|ctr|rsa|sha|key|otp'
aes-128-cbc key_from_otp load_and_decrypt image signature invalid
That tells you the scheme in one line: AES-128 in CBC mode, with a key read from one-time-programmable memory. Now the question is how well that key is protected, and the answer is often not very well.
The Key Is Rarely Well Hidden
Keys turn up in places that are easier to reach than the designers assumed: one-time-programmable fuses readable through a debug interface, a plaintext region of flash the loader reads at boot, or a constant compiled straight into the bootloader. The location decides everything about how hard the encryption really is.
The worst case is a key that is the same on every unit. If one key decrypts the whole product line, recovering it once breaks the fleet. Per-device keys derived at manufacturing are far better, because compromising one device then buys an attacker exactly one device.
Reading the Key Out of Fuses
When the key lives in OTP or eFuses and the debug port is open, reading it can be a single command. The device exposes the fuse bank as a memory region, and a halted core will read it out as readily as any other address.
# halt the core over SWD and read the OTP/fuse region named in the datasheet
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg \
-c 'init; halt; mdw 0x1FFF7000 4; shutdown'0x1fff7000: a1b2c3d4 e5f60718 90a1b2c3 d4e5f607 # the AES key, read straight out of one-time-programmable memory
When the key falls to a four-word memory read, the encryption was never the obstacle. The open debug port was the real flaw, and the encryption only ever looked like protection.
Decryption at Runtime Leaks the Plaintext
Even with a perfectly protected key, the firmware has to decrypt itself to run, and the plaintext sits in RAM while the device operates. An open debug port turns that into a one-step bypass: halt the core after boot and read the decrypted image straight out of memory.
# dump decrypted code from RAM after the device has booted
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
-c 'init; halt; dump_image plain.bin 0x20000000 0x20000; shutdown'target halted due to debug-request, current mode: Thread dumped 131072 bytes in 1.84s # plain.bin now holds the decrypted firmware lifted from RAM
Encryption protected the image at rest. It did nothing for the image the running device was actively using, and the debug port handed it over in plaintext.
When the Debug Port Is Closed
Suppose the team did close debug. The plaintext is still in RAM, and a fault-injection glitch at the right moment can dump it or skip the check that would have stopped you. Glitching is more work than attaching a debugger, but it is well within reach for a determined attacker with the device on a bench.
The point is not that every device falls in ten minutes. It is that encryption moves the problem to the key and to the running device’s memory, and unless those are protected too, the encryption is buying less time than the team thinks.
Prove the Key Is Shared Across Units
The single most damaging finding is one key for the whole fleet. It is also easy to test if you have two units: extract the key from each and compare. Identical keys mean one extraction breaks every device the product ever shipped.
# extract the key region from two units and compare them byte for byte
cmp <(dd if=unitA_otp.bin bs=1 skip=0 count=16 2>/dev/null) <(dd if=unitB_otp.bin bs=1 skip=0 count=16 2>/dev/null) && echo "SAME KEY"SAME KEY # both units decrypt with the identical AES key -> fleet-wide break from one device
When the comparison prints SAME KEY, the severity jumps from “one device compromised” to “every device compromised,” and the fix, per-device keys provisioned at manufacturing, is a manufacturing-line change rather than a firmware patch. That is worth knowing before the line spins up, not after.
Where the Decryption Actually Happens
Modern SoCs sometimes decrypt inside a separate security core or a trusted execution environment, so the plaintext never reaches the main CPU’s RAM. That is a meaningfully stronger design, and it is worth confirming which case you are in before you assume the runtime-leak attack works.
# does the boot path mention a TEE / secure monitor handling the image?
strings bootrom_dump.bin | grep -iE 'tee|optee|secure monitor|smc|trustzone'optee_os 3.16 secure_world_load smc_handler # decryption runs in the secure world; main-CPU RAM dump will not reveal plaintext
If the decryption lives behind a TrustZone boundary, dumping normal-world RAM gets you nothing, and the attack shifts to the secure world or to fault injection. The presence of optee or smc strings tells you the design took this seriously, which changes the rest of the assessment.
What Encryption Actually Buys
None of this means encryption is worthless. It stops the casual download-and-read attack, raises the cost for opportunistic attackers, and protects the image in transit and at rest. The mistake is treating it as the finish line rather than one layer among several.
Priced correctly, encryption is a speed bump for a determined attacker with the device in hand, and a real obstacle for everyone else. The trouble starts when a team believes the encryption alone makes the firmware secret, and skips the controls that actually keep it that way.
Layering the Defense
Encryption earns its place alongside other controls, and the combination is what makes any of it meaningful. The short list I look for is a key locked in hardware, a secure element or fused OTP that the CPU can use but not export, debug interfaces disabled or authenticated on production units, and verified boot so a tampered image will not run even if an attacker writes flash.
With those in place, encryption stops being decorative and becomes a genuine layer. Without them, it is a label on a box that an attacker opens with a debugger.
A Quick Self-Test for Your Own Image
Before you ship, run the attacker’s first moves against your own firmware. If any of these come back the easy way, the encryption is not carrying the weight you assigned it.
# 1. is it uniformly high entropy (encrypted) or are there plaintext gaps? binwalk -E firmware.bin # 2. can the debug port read RAM or the fuse bank on a production unit? openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c 'init; halt; mdw 0x20000000 4; shutdown' # 3. is the same key present on two different units? cmp <(extract_key unitA.bin) <(extract_key unitB.bin)
A production unit that lets a debugger halt it and read memory has already lost, encrypted firmware or not. The same key on two units means a fleet-wide break is one extraction away. Those two checks tell you most of what you need to know.
Where This Fits
Evaluating firmware protection as a system, not a checkbox, is central to a product security assessment. If you want your firmware confidentiality pressure-tested, including how fast the key falls and what the running device leaks, that is the kind of work we do at Berkner Tech.