write-up: IoT/Wired_Airwave_013/README.md
This commit is contained in:
parent
de91ac689c
commit
93c18e1f4b
@ -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)
|
## Description
|
||||||
- `tcp/31337`: maintenance console
|
|
||||||
|
|
||||||
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.
|
Your objective:
|
||||||
2. Recover the maintenance token hidden in maintenance frames.
|
1. Decode the FSK bursts from the IQ stream.
|
||||||
3. Submit it with `unlock <token>` on the console.
|
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
|
```bash
|
||||||
nc <host> 31337
|
nc <host> 31337
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
unlock 0BS3RV3-L41N-868
|
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}`
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user