write-up: IoT/Wired_Airwave_013/README.md

This commit is contained in:
Eun0us 2026-03-26 17:33:39 +00:00
parent 9844d09683
commit 188d3d7f93

View File

@ -1,60 +1,154 @@
# Wired Airwave 013 -- Solution
# Wired Airwave 013
## Overview
| Field | Value |
|-------|-------|
| Category | IoT |
| Difficulty | Medium |
| Points | 500 |
| Author | Eun0us |
| CTF | Espilon 2026 |
The challenge exposes:
---
- `tcp/9001`: raw interleaved int8 IQ stream (2-FSK bursts)
- `tcp/31337`: maintenance console
## Description
Goal:
Clinique Sainte-Mika uses a wireless maintenance channel for Room 013 monitors.
The RF backend exposes raw baseband IQ over TCP.
1. Demodulate valid RF frames from IQ.
2. Recover the maintenance token hidden in maintenance frames.
3. Submit it with `unlock <token>` on the console.
Your objective:
1. Decode the FSK bursts from the IQ stream.
2. Recover the maintenance token hidden in service frames.
3. Submit the token on the control console.
## Packet format
- IQ Stream: `tcp/<host>:9001`
- Maintenance Console: `tcp/<host>:31337`
After preamble and sync, each frame carries 20 obfuscated bytes:
Format: **ESPILON{flag}**
- `type` (1 byte)
- `counter` (1 byte)
- `data` (16 bytes, text)
- `crc16-ccitt` (2 bytes, big endian)
---
The 20-byte payload is XOR-obfuscated with repeating key `WIREDMED13`.
## TL;DR
## Decode path
Capture the raw int8 IQ stream (interleaved I/Q). Implement differential FSK demodulation.
Locate frames using preamble + sync markers. XOR-deobfuscate with key `WIREDMED13`.
Verify CRC16-CCITT. Reassemble maintenance frame parts (`P1:0BS3RV3` + `P2:-L41N-868`)
into token `0BS3RV3-L41N-868`. Submit to the console.
1. Convert stream to complex IQ (`int8` interleaved).
2. Differential FSK demod:
- sign of `imag(s[n] * conj(s[n-1]))`
3. Symbol slicing with `40` samples/symbol.
4. Find `preamble + sync` marker.
5. Parse payload, XOR-deobfuscate, verify CRC16.
---
## Maintenance token
## Tools
Valid decoded maintenance frames include:
| Tool | Purpose |
|------|---------|
| `nc` | Capture IQ stream and connect to console |
| Python 3 + numpy | FSK demodulation and frame parsing |
| CRC16-CCITT library | Frame validation |
- `P1:0BS3RV3`
- `P2:-L41N-868`
---
Token is:
## Solution
`0BS3RV3-L41N-868`
### Step 1 — Capture the IQ stream
## Unlock
```bash
nc <host> 9001 > capture.raw
# Wait a few seconds, then Ctrl+C
```
The stream begins with a text banner:
```
IQ stream — int8 interleaved, samplerate=200000, encoding=2-FSK
```
After the banner, raw binary IQ data follows. Save after the newline.
> 📸 `[screenshot: nc output showing the IQ stream banner before binary data]`
### Step 2 — Demodulate the 2-FSK signal
```python
import numpy as np
with open("capture.raw", "rb") as f:
raw = np.frombuffer(f.read(), dtype=np.int8).astype(float)
# Reconstruct complex samples from interleaved I/Q
samples = raw[0::2] + 1j * raw[1::2]
# Differential FSK demodulation: sign of imag(s[n] * conj(s[n-1]))
diff = samples[1:] * np.conj(samples[:-1])
bits_raw = (np.imag(diff) > 0).astype(int)
# Symbol slicing at 40 samples per symbol
SAMPLES_PER_SYMBOL = 40
symbols = []
for i in range(0, len(bits_raw) - SAMPLES_PER_SYMBOL, SAMPLES_PER_SYMBOL):
chunk = bits_raw[i:i+SAMPLES_PER_SYMBOL]
symbols.append(int(np.mean(chunk) > 0.5))
```
### Step 3 — Find and parse frames
Look for the preamble pattern (eight `1`s then a sync marker).
Once found, read the 20-byte obfuscated payload.
> 📸 `[screenshot: spectrogram of IQ data showing FSK burst patterns]`
### Step 4 — XOR-deobfuscate and verify CRC
```python
import crcmod
crc16 = crcmod.predefined.mkCrcFun('crc-ccitt-false')
KEY = b"WIREDMED13"
for frame in detected_frames:
payload = bytes(frame[:20])
deobf = bytes(b ^ KEY[i % len(KEY)] for i, b in enumerate(payload))
frame_type = deobf[0]
counter = deobf[1]
data = deobf[2:18]
crc = (deobf[18] << 8) | deobf[19]
calculated = crc16(deobf[:18])
if calculated == crc:
print(f"type={frame_type:02x} data={data}")
```
### Step 5 — Collect maintenance frame parts
Valid decoded maintenance frames produce:
```
type=0x10 data=P1:0BS3RV3
type=0x10 data=P2:-L41N-868
```
Telemetry frames (type=0x01) are noise for this challenge.
Token = `0BS3RV3-L41N-868`
> 📸 `[screenshot: decoded frame output showing the two token parts]`
### Step 6 — Submit to the console
```bash
nc <host> 31337
```
```text
unlock 0BS3RV3-L41N-868
```
Server returns the flag.
The server returns the flag.
## Automated solver
> 📸 `[screenshot: maintenance console returning the flag after unlock]`
```bash
python3 solve.py --host <host>
```
---
## Flag
`ESPILON{sdr_fsk_w1r3d_m3d_013}`