HardwareHidden Investigations CTF 2026
C3 Vault
A retired ESP32-C3 gateway's SPI flash (flash-encrypted) and eFuse read-register window were provided. The goal: recover the enrollment credentials and prove to a live TCP(+TLS) verifier that we are the gateway.
What this one teaches
- ESP32-C3 XTS-AES-128 flash encryption keys are stored byte-reversed in eFuse — espsecure expects the reversed order.
- NVS encryption is independent of flash encryption: the NVS partition must be read from the original (encrypted) flash image, not the flash-decrypted one.
- Reversing HMAC auth from a RISC-V binary: identify mbedtls calls, reconstruct the message layout, and replay against a live TLS verifier.
Task description
A retired ESP32-C3 gateway still has access to a remote vault. The recovery lab preserved its SPI flash and eFuse read-register window. Recover its enrollment credentials and convince the live service that you are the gateway.
The gateway runs a custom enrollment application built with ESP-IDF. The supplied artifacts are a reproducible lab acquisition; see
recovery.txtfor their exact format. The endpoint is a host-side authentication verifier, not an emulated shell.No brute force or physical board is required. The event flag exists only on the live instance, not in the downloadable evidence.
Challenge ID: 37ad0ee5-5532-475b-ac7b-00a49ba6c7bd · Submission result: goodFlag
1. eFuse parsing (efuse_read.bin)
336 bytes = 84 little-endian words, laid out BLOCK0(6) BLOCK1(6) BLOCK2..10(8 each).
Only BLOCK4 / KEY0 is non-zero. BLOCK0’s key-purpose word = 0x0404 → purpose
4 = XTS_AES_128_KEY (flash encryption). Base MAC (BLOCK1) = 02:63:c3:00:00:01.
Flash-encryption key in eFuse physical order:
a60e11f7e712272a9cdf3d656ad89282e861dc0ffd2fc127f7f74da513654d7f
Gotcha: espefuse burns XTS keys byte-reversed, so the key espsecure
expects is the reverse:
7f4d6513a54df7f727c12ffd0fdc61e88292d86a653ddf9c2a2712e7f7110ea6
2. Flash decryption
espsecure.py decrypt_flash_data --aes_xts --keyfile <reversed_key> --address 0x0 \
--output flash_dec.bin flash_image.bin
Round-trip re-encryption reproduces the original byte-for-byte. Bootloader magic
e9 @0x0, app magic e9 @0x10000.
Partition table (@0x8000):
| label | type | offset | size | flags |
|---|---|---|---|---|
| nvs | data/nvs | 0x9000 | 0x6000 | - |
| nvs_keys | data/nvs_keys | 0xf000 | 0x1000 | encrypted |
| factory | app | 0x10000 | 0x100000 | - |
3. NVS keys (nvs_keys @0xf000, flash-encrypted)
From the flash-decrypted image:
eky=7b915b80a7d169eb49edbe733e5a97540720264227dd39b872244ed1de0c1d86tky=7b160cece5a3c8ef9a4e4b47636c5abbccde5838828603f845cc6640a838de76
Confirmed via esp_rom_crc32_le(0xffffffff, eky||tky) == stored 0xba65fb57.
4. NVS decryption
Key gotcha: the nvs partition is not flash-encrypted (only NVS-encrypted),
so it must be read from the original flash_image.bin, not the flash-decrypted
image. NVS encryption = AES-XTS-256, decrypting each 32-byte entry independently
with tweak = the entry’s partition-relative byte offset (LE32 padded to 16 bytes),
key = eky||tky. Page headers and the entry-state bitmap are plaintext.
Recovered credentials (NVS page 0):
gateway/hostname=c3-vault-gwgateway/device_id=ee0315f3953aad298d7f3f43973ad618(16-byte blob)gateway/generation=7(u32)vault_v2/seed=826e4e9e65c0ce89f413b7c291bb7e84185c64d4ec8769805227253b985083c5vault_v1/seed=d857200f87a1728b5220a4d77c31d7d9ee9bf8992ad16dbb398d05844676aa4f(the “stale namespace that survived migration” — a decoy; v2 is the live one)
5. Auth protocol (reversed from main/vault.c, factory app, RISC-V)
HMAC wrapper @0x4200c0c4 = mbedtls md_setup/hmac_starts/update/finish;
md_info_from_type(9) → output size 0x20 = SHA-256.
device_key = HMAC-SHA256(seed_v2,
"c3-vault/device-key/v2\0" + device_id(16) + generation_u32_LE) # 43-byte msg
= 3783807e8cd41e83a331f5cfe6050e8b5b21d70cd6ddcd26c405816fb094f444
response = HMAC-SHA256(device_key,
"C3VAULT-AUTH-V2\0" + challenge(32) + device_id(16)) # 64-byte msg
6. Live protocol (TLS, port 1337)
C3VAULT/2 READY
DEVICE ee0315f3953aad298d7f3f43973ad618
CHALLENGE <64 hex>
Commands: help, status, auth <64 hex>, quit (one attempt per connection)
> auth <response hex>
VAULT OPENED
hiCTF{efus3_cl34r_jt4g_0p3n_nv5_d8cde9fcc3}
Transport is TLS; command is auth <64 hex> (not RESPONSE).
Tools
espsecure(esptool 4.8.1) via Docker for AES-XTS flash decrypt- Python
cryptographyfor NVS AES-XTS-256;capstone(RISC-V32 + compressed) for disasm - Python stdlib
hmac/hashlibfor the HMAC-SHA256 auth
The flagReveal
hiCTF{efus3_cl34r_jt4g_0p3n_nv5_d8cde9fcc3}This CTF was possibly solved, and the writeup most likely written by an LLM. Apologies for inconsistencies.