write-up: IoT/Lain_Br34kC0r3_V2/README.md
This commit is contained in:
parent
81798e6dbd
commit
fcea330c51
@ -1,22 +1,76 @@
|
||||
# LAIN_Br34kC0r3 V2 — Solution
|
||||
# Lain_Br34kC0r3 V2
|
||||
|
||||
**Chapitre 2 : Core Analysis** | Difficulté : Hard | Flag : `ESPILON{3sp32_fl4sh_dump_r3v3rs3d}`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Category | IoT |
|
||||
| Difficulty | Hard |
|
||||
| Points | 500 |
|
||||
| Author | Eun0us |
|
||||
| CTF | Espilon 2026 |
|
||||
|
||||
## Overview
|
||||
---
|
||||
|
||||
Ce challenge fournit un dump flash complet d'un ESP32 (bootloader + partition table + NVS + firmware applicatif). Le joueur doit extraire le firmware, le reverse engineer pour trouver les clés AES-256-CBC, puis déchiffrer le flag stocké dans la NVS.
|
||||
## Description
|
||||
|
||||
## Étape 1 — Récupérer le dump flash
|
||||
**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 (lecture)
|
||||
# Terminal 1 — TX (read)
|
||||
nc <host> 1111 | tee flash_output.txt
|
||||
|
||||
# Terminal 2 : RX (écriture)
|
||||
# Terminal 2 — RX (write)
|
||||
echo "dump_flash" | nc <host> 2222
|
||||
```
|
||||
|
||||
Le dump est envoyé en base64. Extraire et décoder :
|
||||
The dump is sent as base64 between markers:
|
||||
|
||||
```
|
||||
=== BEGIN FLASH DUMP ===
|
||||
<base64 data>
|
||||
=== END FLASH DUMP ===
|
||||
```
|
||||
|
||||
Decode:
|
||||
|
||||
```python
|
||||
import base64
|
||||
@ -24,7 +78,6 @@ import base64
|
||||
with open("flash_output.txt") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Extract base64 lines between markers
|
||||
b64_data = ""
|
||||
capture = False
|
||||
for line in lines:
|
||||
@ -41,17 +94,16 @@ with open("flash_dump.bin", "wb") as f:
|
||||
f.write(flash)
|
||||
```
|
||||
|
||||
## Étape 2 — Analyse du dump flash
|
||||
> 📸 `[screenshot: TX terminal showing the base64 flash dump streaming out]`
|
||||
|
||||
### Step 2 — Identify the flash structure
|
||||
|
||||
```bash
|
||||
# Identifier les composants
|
||||
binwalk flash_dump.bin
|
||||
|
||||
# Ou utiliser esptool
|
||||
esptool.py image_info --version 2 flash_dump.bin
|
||||
```
|
||||
|
||||
Structure identifiée :
|
||||
Expected structure:
|
||||
|
||||
```
|
||||
0x0000 Padding (0xFF)
|
||||
0x1000 ESP32 bootloader (magic 0xE9)
|
||||
@ -61,64 +113,42 @@ Structure identifiée :
|
||||
0x10000 Application firmware (magic 0xE9)
|
||||
```
|
||||
|
||||
## Étape 3 — Extraire le firmware applicatif
|
||||
### Step 3 — Extract and analyze the application firmware
|
||||
|
||||
```bash
|
||||
# Extraire l'app à partir de l'offset 0x10000
|
||||
dd if=flash_dump.bin of=app_firmware.bin bs=1 skip=$((0x10000))
|
||||
|
||||
# Vérifier
|
||||
file app_firmware.bin
|
||||
# Devrait montrer un binaire ESP32 ou "data"
|
||||
|
||||
hexdump -C app_firmware.bin | head -5
|
||||
# Premier byte devrait être 0xE9 (ESP image magic)
|
||||
```
|
||||
|
||||
## Étape 4 — Reverse Engineering
|
||||
|
||||
### Méthode rapide : strings
|
||||
|
||||
```bash
|
||||
strings -n 10 app_firmware.bin | grep -i "key\|aes\|iv\|wired\|therapy"
|
||||
```
|
||||
|
||||
Résultats attendus :
|
||||
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
|
||||
```
|
||||
|
||||
### Méthode complète : Ghidra
|
||||
> 📸 `[screenshot: strings output identifying the AES key and IV]`
|
||||
|
||||
1. Ouvrir Ghidra, importer `app_firmware.bin` (ou mieux, l'ELF si disponible)
|
||||
2. Architecture : **Xtensa:LE:32:default**
|
||||
3. Analyser → chercher `app_main` dans les symboles
|
||||
4. Suivre les appels : `app_main()` → `wired_med_crypto_init()` → `mbedtls_aes_setkey_enc()`
|
||||
5. Les arguments de `mbedtls_aes_setkey_enc()` pointent vers `therapy_aes_key` dans `.rodata`
|
||||
6. Extraire les 32 bytes de la clé et les 16 bytes de l'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`.
|
||||
|
||||
## Étape 5 — Récupérer le flag chiffré
|
||||
### Step 4 — Get the encrypted flag ciphertext
|
||||
|
||||
### Option A : Commande directe
|
||||
```
|
||||
On the RX port:
|
||||
|
||||
```text
|
||||
encrypted_data
|
||||
```
|
||||
→ Retourne le ciphertext en hex
|
||||
|
||||
### Option B : Parser la NVS
|
||||
```bash
|
||||
# Extraire la partition NVS
|
||||
dd if=flash_dump.bin of=nvs_dump.bin bs=1 skip=$((0x9000)) count=$((0x6000))
|
||||
Returns the ciphertext as a hex string on TX.
|
||||
|
||||
# Utiliser nvs_tool.py de ESP-IDF (si disponible)
|
||||
python3 $IDF_PATH/components/nvs_flash/nvs_partition_tool/nvs_tool.py --dump nvs_dump.bin
|
||||
```
|
||||
Alternatively, extract from the NVS partition (namespace `wired_med`, key `encrypted_flag`, blob type) using `nvs_tool.py` from ESP-IDF.
|
||||
|
||||
Chercher l'entrée : namespace `wired_med`, key `encrypted_flag`, type blob
|
||||
> 📸 `[screenshot: encrypted_data command returning the hex ciphertext]`
|
||||
|
||||
## Étape 6 — Déchiffrement AES-256-CBC
|
||||
### Step 5 — Decrypt AES-256-CBC
|
||||
|
||||
```python
|
||||
from Crypto.Cipher import AES
|
||||
@ -127,8 +157,7 @@ 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 from encrypted_data command or NVS
|
||||
ciphertext = bytes.fromhex("...")
|
||||
ciphertext = bytes.fromhex("...") # from encrypted_data output
|
||||
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
|
||||
@ -136,7 +165,9 @@ print(plaintext.decode())
|
||||
# ESPILON{3sp32_fl4sh_dump_r3v3rs3d}
|
||||
```
|
||||
|
||||
## Résumé de la chaîne d'attaque
|
||||
> 📸 `[screenshot: Python decryption script printing the flag]`
|
||||
|
||||
### Attack chain summary
|
||||
|
||||
```
|
||||
dump_flash → base64 decode → binwalk → extract app @ 0x10000
|
||||
@ -144,6 +175,8 @@ dump_flash → base64 decode → binwalk → extract app @ 0x10000
|
||||
→ encrypted_data (or NVS parse) → AES-256-CBC decrypt → FLAG
|
||||
```
|
||||
|
||||
## Script de solve complet
|
||||
---
|
||||
|
||||
Voir `solve/solve.py`
|
||||
## Flag
|
||||
|
||||
`ESPILON{3sp32_fl4sh_dump_r3v3rs3d}`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user