# Wired Airwave 013 | Field | Value | |-------|-------| | Category | IoT | | Difficulty | Medium | | Points | 500 | | Author | Eun0us | | CTF | Espilon 2026 | --- ## Description Clinique Sainte-Mika uses a wireless maintenance channel for Room 013 monitors. The RF backend exposes raw baseband IQ over TCP. 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. - IQ Stream: `tcp/:9001` - Maintenance Console: `tcp/:31337` Format: **ESPILON{flag}** --- ## TL;DR 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. --- ## Tools | Tool | Purpose | |------|---------| | `nc` | Capture IQ stream and connect to console | | Python 3 + numpy | FSK demodulation and frame parsing | | CRC16-CCITT library | Frame validation | --- ## Solution ### Step 1 — Capture the IQ stream ```bash nc 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 31337 ``` ```text unlock 0BS3RV3-L41N-868 ``` The server returns the flag. > 📸 `[screenshot: maintenance console returning the flag after unlock]` --- ## Flag `ESPILON{sdr_fsk_w1r3d_m3d_013}`