183 lines
4.3 KiB
Markdown
183 lines
4.3 KiB
Markdown
# Lain_Br34kC0r3 V2
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Category | IoT |
|
|
| Difficulty | Hard |
|
|
| Points | 500 |
|
|
| Author | Eun0us |
|
|
| CTF | Espilon 2026 |
|
|
|
|
---
|
|
|
|
## Description
|
|
|
|
**Chapter 2 — Core Analysis**
|
|
|
|
After exploring the WIRED-MED therapy module through its UART interface, you successfully
|
|
dumped the complete flash of the device. This dump contains the bootloader, the partition
|
|
table, the NVS and the application firmware.
|
|
|
|
Your mission: **reverse engineer the ESP32 firmware** to extract the encryption keys used
|
|
to protect patient data, then decrypt the flag stored in the NVS partition.
|
|
|
|
*"Close your eyes. Open The Wired. Analyze the core."*
|
|
|
|
- **TX (read):** `nc CHALLENGE_HOST 1111`
|
|
- **RX (write):** `nc CHALLENGE_HOST 2222`
|
|
|
|
---
|
|
|
|
## TL;DR
|
|
|
|
Issue `dump_flash` over the RX UART port to receive a base64-encoded full flash dump on TX.
|
|
Decode and use `binwalk` to locate the app firmware at offset 0x10000. Run `strings` on the
|
|
binary to find the AES-256-CBC key (`W1R3D_M3D_TH3R4PY_K3Y_2024_L41N!`) and IV (`L41N_WIRED_IV_01`).
|
|
Get the encrypted flag via the `encrypted_data` command, then AES-256-CBC decrypt it.
|
|
|
|
---
|
|
|
|
## Tools
|
|
|
|
| Tool | Purpose |
|
|
|------|---------|
|
|
| `nc` | Split UART connection |
|
|
| Python 3 | Base64 decode, data parsing |
|
|
| `binwalk` / `esptool.py` | Parse the flash dump |
|
|
| `strings` | Extract AES key and IV |
|
|
| Ghidra (Xtensa) | Full reverse engineering if needed |
|
|
| `pycryptodome` | AES-256-CBC decryption |
|
|
|
|
---
|
|
|
|
## Solution
|
|
|
|
### Step 1 — Dump the flash
|
|
|
|
```bash
|
|
# Terminal 1 — TX (read)
|
|
nc <host> 1111 | tee flash_output.txt
|
|
|
|
# Terminal 2 — RX (write)
|
|
echo "dump_flash" | nc <host> 2222
|
|
```
|
|
|
|
The dump is sent as base64 between markers:
|
|
|
|
```
|
|
=== BEGIN FLASH DUMP ===
|
|
<base64 data>
|
|
=== END FLASH DUMP ===
|
|
```
|
|
|
|
Decode:
|
|
|
|
```python
|
|
import base64
|
|
|
|
with open("flash_output.txt") as f:
|
|
lines = f.readlines()
|
|
|
|
b64_data = ""
|
|
capture = False
|
|
for line in lines:
|
|
if "BEGIN FLASH DUMP" in line:
|
|
capture = True
|
|
continue
|
|
if "END FLASH DUMP" in line:
|
|
break
|
|
if capture:
|
|
b64_data += line.strip()
|
|
|
|
flash = base64.b64decode(b64_data)
|
|
with open("flash_dump.bin", "wb") as f:
|
|
f.write(flash)
|
|
```
|
|
|
|
> 📸 `[screenshot: TX terminal showing the base64 flash dump streaming out]`
|
|
|
|
### Step 2 — Identify the flash structure
|
|
|
|
```bash
|
|
binwalk flash_dump.bin
|
|
```
|
|
|
|
Expected structure:
|
|
|
|
```
|
|
0x0000 Padding (0xFF)
|
|
0x1000 ESP32 bootloader (magic 0xE9)
|
|
0x8000 Partition table
|
|
0x9000 NVS partition (24 KiB)
|
|
0xF000 PHY init data
|
|
0x10000 Application firmware (magic 0xE9)
|
|
```
|
|
|
|
### Step 3 — Extract and analyze the application firmware
|
|
|
|
```bash
|
|
dd if=flash_dump.bin of=app_firmware.bin bs=1 skip=$((0x10000))
|
|
strings -n 10 app_firmware.bin | grep -i "key\|aes\|iv\|wired\|therapy"
|
|
```
|
|
|
|
Expected results:
|
|
|
|
```
|
|
W1R3D_M3D_TH3R4PY_K3Y_2024_L41N! # AES-256 key (32 bytes)
|
|
L41N_WIRED_IV_01 # AES IV (16 bytes)
|
|
WIRED-MED Therapy Module
|
|
```
|
|
|
|
> 📸 `[screenshot: strings output identifying the AES key and IV]`
|
|
|
|
For full confirmation: open in Ghidra with **Xtensa:LE:32:default** architecture,
|
|
find `app_main()` → `wired_med_crypto_init()` → `mbedtls_aes_setkey_enc()`. The
|
|
key and IV are passed as arguments pointing into `.rodata`.
|
|
|
|
### Step 4 — Get the encrypted flag ciphertext
|
|
|
|
On the RX port:
|
|
|
|
```text
|
|
encrypted_data
|
|
```
|
|
|
|
Returns the ciphertext as a hex string on TX.
|
|
|
|
Alternatively, extract from the NVS partition (namespace `wired_med`, key `encrypted_flag`, blob type) using `nvs_tool.py` from ESP-IDF.
|
|
|
|
> 📸 `[screenshot: encrypted_data command returning the hex ciphertext]`
|
|
|
|
### Step 5 — Decrypt AES-256-CBC
|
|
|
|
```python
|
|
from Crypto.Cipher import AES
|
|
from Crypto.Util.Padding import unpad
|
|
|
|
key = b"W1R3D_M3D_TH3R4PY_K3Y_2024_L41N!" # 32 bytes
|
|
iv = b"L41N_WIRED_IV_01" # 16 bytes
|
|
|
|
ciphertext = bytes.fromhex("...") # from encrypted_data output
|
|
|
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
|
|
print(plaintext.decode())
|
|
# ESPILON{3sp32_fl4sh_dump_r3v3rs3d}
|
|
```
|
|
|
|
> 📸 `[screenshot: Python decryption script printing the flag]`
|
|
|
|
### Attack chain summary
|
|
|
|
```
|
|
dump_flash → base64 decode → binwalk → extract app @ 0x10000
|
|
→ strings/Ghidra (Xtensa RE) → find AES key + IV in .rodata
|
|
→ encrypted_data (or NVS parse) → AES-256-CBC decrypt → FLAG
|
|
```
|
|
|
|
---
|
|
|
|
## Flag
|
|
|
|
`ESPILON{3sp32_fl4sh_dump_r3v3rs3d}`
|