Writeup: Jnouned Routeur - EspilonCTF (ESP)
Challenge Description
The CERT-CORP intercepted a strange firmware from an unknown router model built by the shady Jnouned Company. Analysts found it targets an ESP32-based prototype, but the system is protected by a locked UART console. Your mission: flash the firmware, break into the system, and unlock admin access.
Category: Reverse Engineering Flag: ESPILON{Jn0un3d_4dM1N}
Files Provided
bootloader.bin(26,752 bytes) - ESP32 2nd stage bootloaderjnouned_router.bin(873,056 bytes) - Main firmware imagepartition-table (1).bin(3,072 bytes) - Partition table
Solution
Step 1: Identify the Firmware Format
Running file and strings on the binaries reveals this is an ESP-IDF v5.3.2 firmware for ESP32 (Xtensa LX6 architecture). The project name is jnouned_router.
strings jnouned_router.bin | grep -i "esp-idf\|project\|admin\|flag"
Key strings found:
admin_login <password>- UART console expects a passwordflag- A console command to "Show hidden flag"FLAG CAPTURED: %s- Flag output formatCTF_FLAGS- NVS namespace for encrypted flagsFailed to decrypt flag %d- Multiple encrypted flags exist
Step 2: Parse the ESP32 Image Segments
The ESP32 image header reveals 5 segments:
| Segment | Virtual Address | Size | Purpose |
|---|---|---|---|
| 0 | 0x3f400020 | 124 KB | DROM (read-only data) |
| 1 | 0x3ffb0000 | 6 KB | DRAM (initialized data) |
| 2 | 0x400d0020 | 617 KB | IROM (code, flash-mapped) |
| 3 | 0x3ffb1930 | 10 KB | DRAM (BSS-adjacent) |
| 4 | 0x40080000 | 98 KB | IRAM (fast code) |
Step 3: Disassemble the Password Construction
Using the Capstone disassembly framework with Xtensa support, I located the password construction function at 0x400daa14:
from capstone import *
md = Cs(CS_ARCH_XTENSA, CS_MODE_LITTLE_ENDIAN)
The disassembled function:
entry a1, 0x20
mov.n a10, a2 ; buffer pointer
mov.n a11, a3 ; buffer size
l32r a15, "2022" ; literal pool
l32r a14, "admin-"
l32r a13, "jnoun-"
l32r a12, "%s%s%s"
l32r a8, snprintf ; 0x401539cc
callx8 a8 ; snprintf(buf, size, "%s%s%s", "jnoun-", "admin-", "2022")
retw.n
Understanding Xtensa's windowed ABI: callx8 rotates the register window by 8, so the caller's a12-a15 become the callee's a4-a7. This means:
a4="%s%s%s"(format string)a5="jnoun-"a6="admin-"a7="2022"
Password: jnoun-admin-2022
Step 4: Locate Encrypted Flag Data
By tracing string references in the IROM literal pools, I found the flag decryption infrastructure:
- 8 small encrypted blocks (4 bytes each) at DROM addresses
0x3f412e38-0x3f412e54 - 8 large encrypted blocks (alternating 16 and 32 bytes) at
0x3f412d78-0x3f412e28 - An XOR decryption function at
0x400de16c
Step 5: Reverse the XOR Key Derivation
The function at 0x400de16c XOR-decrypts the 8 small blocks with hardcoded per-block keys:
; Block 0: data from 0x3f412e54, XOR key 0x38
l32r a9, [0x3f412e54]
l8ui a9, a9, 0
movi.n a10, 0x38
xor a9, a9, a10
s8i a9, a2, 0 ; write to output buffer
; Block 1: data from 0x3f412e50, XOR key 0xe7
; ... (same pattern for all 8 blocks)
| Block | Source Address | XOR Key |
|---|---|---|
| 0 | 0x3f412e54 | 0x38 |
| 1 | 0x3f412e50 | 0xe7 |
| 2 | 0x3f412e4c | 0x91 |
| 3 | 0x3f412e48 | 0xf7 |
| 4 | 0x3f412e44 | 0x10 |
| 5 | 0x3f412e40 | 0x6b |
| 6 | 0x3f412e3c | 0x74 |
| 7 | 0x3f412e38 | 0x96 |
XOR-decrypting these 8 blocks produces a 32-byte AES-256 key.
Step 6: AES-256-CBC Decryption
The large encrypted blocks are structured as alternating (16-byte IV, 32-byte ciphertext) pairs. Using the derived AES key with CBC mode:
from Crypto.Cipher import AES
# Derived 32-byte key from XOR step
key = bytes([...]) # 32 bytes
# For each flag: (IV_address, CT_address)
flag_pairs = [
(0x3f412e28, 0x3f412e08), # Flag 0
(0x3f412df8, 0x3f412dd8), # Flag 1
(0x3f412dc8, 0x3f412da8), # Flag 2
(0x3f412d98, 0x3f412d78), # Flag 3
]
for iv_addr, ct_addr in flag_pairs:
iv = read_drom(iv_addr, 16)
ct = read_drom(ct_addr, 32)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
flag = cipher.decrypt(ct) # Remove PKCS7 padding
print(flag)
Decrypted Flags
FLAG 0: ESPILON{Jn0un3d_4dM1N}
FLAG 1: ESPILON{802_11_tx_jnned}
FLAG 2: ESPILON{Adm1n_4r3_jn0uned}
FLAG 3: ESPILON{Jn0un3d_UDP_Pr0t0c0l}
Flag
ESPILON{Jn0un3d_4dM1N}
Tools Used
- Python 3 with Capstone (Xtensa disassembly)
- PyCryptodome (AES decryption)
strings,xxd,file(initial recon)
Key Takeaways
- ESP32 uses Xtensa LX6 with a windowed register ABI —
call8shifts registers by 8, so function arguments map differently than in conventional calling conventions. l32ris the primary way Xtensa loads constants — it reads from a literal pool at a negative PC-relative offset. Following these pointers is essential for resolving string and data references.- The firmware used a two-layer encryption scheme: XOR to derive an AES key, then AES-256-CBC with per-flag IVs to protect the actual flag strings.