Neven Marić
Hidden Investigations CTF 2026

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.

  • hardware
  • esp32
  • flash-encryption
  • nvs
  • efuse
  • hmac
  • tls
  • reverse-engineering
  • risc-v

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.txt for 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 = 7b915b80a7d169eb49edbe733e5a97540720264227dd39b872244ed1de0c1d86
  • tky = 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-gw
  • gateway/device_id = ee0315f3953aad298d7f3f43973ad618 (16-byte blob)
  • gateway/generation = 7 (u32)
  • vault_v2/seed = 826e4e9e65c0ce89f413b7c291bb7e84185c64d4ec8769805227253b985083c5
  • vault_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 cryptography for NVS AES-XTS-256; capstone (RISC-V32 + compressed) for disasm
  • Python stdlib hmac/hashlib for the HMAC-SHA256 auth
The flagRevealhiCTF{efus3_cl34r_jt4g_0p3n_nv5_d8cde9fcc3}

This CTF was possibly solved, and the writeup most likely written by an LLM. Apologies for inconsistencies.