ESPILON-CTF-2026-Writeups/Misc/Accela_Signal/README.md
Eun0us 6a0877384d [+] Writeups v2 — sync solves, real points, scoreboard stats, cleanup
- Remove undeployed challenges: Phantom_Byte, Cr4cK_w1f1, Lain_Br34kC0r3 V1,
  Lain_VS_Knights, Lets_All_Love_UART, AETHER_NET, Last_Train_451, Web3/
- Sync 24 solve/ files from main CTF-Espilon repo
- Update all READMEs with real CTFd final scores at freeze
- Add git-header.png banner
- Rewrite README: scoreboard top 10, edition stats (1410 users, 264 boards,
  1344 solves), correct freeze date March 26 2026
2026-03-27 21:27:45 +01:00

177 lines
4.9 KiB
Markdown

# Accela Signal
| Field | Value |
|-------|-------|
| Category | Misc |
| Difficulty | Hard |
| Points | 536 |
| Author | Eun0us |
| CTF | Espilon 2026 |
---
## Description
During the KIDS experiment, Tachibana Labs deployed a covert LoRa-like mesh network
throughout the hospital infrastructure. Code-named "Accela Signal", these transmissions
use Chirp Spread Spectrum modulation to carry data fragments below the detection threshold
of conventional receivers.
Your NAVI has intercepted the baseband IQ stream from one of these nodes. The signal
contains chirp-modulated frames with encoded data. Analyze the modulation, demodulate
the chirps, decode the protocol, and extract the hidden message.
Hint: The stream banner tells you the basics. The rest is signal processing.
- IQ Stream: `tcp/<host>:9002`
Format: **ESPILON{flag}**
---
## TL;DR
Capture an 8 kSps int16 LE IQ stream. Identify Chirp Spread Spectrum (LoRa-like) modulation
(N=128, SF=7). Implement dechirp + FFT demodulation. Detect frames by preamble (8x symbol-0
chirps) and sync (2x downchirps). Gray-decode symbols. Unpack 7-bit symbols to bytes. Find
the data frame (type=0x02), XOR with key `L41N`, decode the flag.
---
## Tools
| Tool | Purpose |
|------|---------|
| `nc` | Capture IQ stream |
| Python 3 + numpy + scipy | CSS demodulation pipeline |
| `inspectrum` / matplotlib | Visual spectrogram analysis |
---
## Solution
### Step 1 — Capture and read the banner
```bash
nc <HOST> 9002 > capture.raw
```
First bytes are a text banner before the binary IQ data:
```
IQ baseband, 8000 sps, int16 LE interleaved
Chirp Spread Spectrum detected. N=128.
```
![nc output showing the IQ stream banner](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/accela_nc.png)
### Step 2 — Analyze the spectrogram
Load the IQ data in inspectrum or plot with matplotlib. You see:
- Characteristic **chirps**: frequency sweeps from low to high
- Repeating groups of identical chirps (preamble)
- Occasional downchirps (sync)
This is **Chirp Spread Spectrum (CSS)**, identical in principle to LoRa.
![spectrogram showing upchirp preamble and downchirp sync patterns](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/accela_spectrogram.png)
### Step 3 — Determine parameters
- Each chirp spans 128 samples → **N = 128**
- `N = 2^SF`**SF = 7** (spreading factor)
- Each symbol encodes 7 bits → 128 possible symbols
- Baseband sample rate = 8000 Hz
### Step 4 — Implement dechirping
```python
import numpy as np
# Read raw int16 LE interleaved IQ
raw = np.fromfile("capture.raw", dtype="<i2").astype(float) / 32768.0
iq = raw[0::2] + 1j * raw[1::2]
N = 128
# Base upchirp (symbol 0): exp(j * pi * n^2 / N)
n = np.arange(N)
upchirp0 = np.exp(1j * np.pi * n**2 / N)
downchirp = np.conj(upchirp0)
def decode_symbol(chirp_samples):
"""Dechirp then find FFT peak = symbol value"""
dechirped = chirp_samples * np.conj(upchirp0)
spectrum = np.abs(np.fft.fft(dechirped))
return int(np.argmax(spectrum))
```
### Step 5 — Detect frames
Frame structure:
```
[Preamble: 8x symbol 0] [Sync: 2x downchirp] [Header: 1 symbol = length] [Payload: L symbols]
```
Scan the IQ stream for runs of 8 consecutive symbols decoding to 0.
### Step 6 — Gray decode and symbol-to-byte unpacking
```python
def gray_decode(val):
mask = val
while mask:
mask >>= 1
val ^= mask
return val
def symbols_to_bytes(symbols):
"""Pack 7-bit symbols (SF=7) into 8-bit bytes"""
bits = ""
for s in symbols:
bits += f"{s:07b}"
return bytes(int(bits[i:i+8], 2) for i in range(0, len(bits) - 7, 8))
```
![Python decoder output showing decoded symbols and CRC16 validation pass](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/accela_decoder.png)
### Step 7 — Parse frame payload and decrypt
Payload format: `[type:1] [data:L] [crc16:2]`
- Type `0x01` = beacon (cleartext ASCII, for verification)
- Type `0x02` = data frame (XOR-encrypted flag)
```python
import crcmod
crc16_fn = crcmod.predefined.mkCrcFun('crc-ccitt-false')
for frame_type, data, crc in decoded_frames:
if crc16_fn(bytes([frame_type]) + data) != crc:
continue # invalid frame
if frame_type == 0x02:
key = b"L41N"
flag = bytes(b ^ key[i % 4] for i, b in enumerate(data))
print(flag.rstrip(b'\x00').decode())
```
![script printing the decrypted flag from the data frame](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/accela_flag.png)
### Key insights
- CSS encodes data as a cyclic frequency shift of a chirp signal
- The dechirp + FFT converts frequency offset into a bin index (symbol value)
- Gray coding ensures adjacent symbols differ by 1 bit, reducing BER on noisy channels
- The beacon frame (type 0x01) provides a known-plaintext verification step
- The stream banner hint `"N=128"` directly gives the spreading factor
---
## Flag
`ESPILON{4cc3l4_ch1rp_spr34d_w1r3d}`