write-up: Misc/Accela_Signal/README.md

This commit is contained in:
Eun0us 2026-03-26 17:33:40 +00:00
parent 188d3d7f93
commit 5d14fe51df

View File

@ -1,65 +1,124 @@
# Accela Signal -- Solution # Accela Signal
## Overview | Field | Value |
A LoRa-like Chirp Spread Spectrum (CSS) IQ stream containing two types of frames: |-------|-------|
beacon (cleartext) and data (XOR-encrypted flag). Players must implement CSS | Category | Misc |
demodulation from scratch to decode the frames. | Difficulty | Hard |
| Points | 500 |
| Author | Eun0us |
| CTF | Espilon 2026 |
## Steps ---
## 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
### 1. Capture IQ Stream
Connect to TCP port 9002. A text banner appears first, followed by raw IQ data.
```bash ```bash
nc HOST 9002 > capture.raw nc <HOST> 9002 > capture.raw
# Or use the solve script
``` ```
The banner tells you: `IQ baseband, 8000 sps, int16 LE interleaved`. First bytes are a text banner before the binary IQ data:
### 2. Analyze the Signal ```
Open the IQ data in a spectrogram tool (e.g., inspectrum, Python matplotlib, or IQ baseband, 8000 sps, int16 LE interleaved
GNU Radio). You'll see: Chirp Spread Spectrum detected. N=128.
- Characteristic **chirp** patterns: frequency sweeps from low to high ```
- Repeating preambles (identical chirps)
- Gaps of noise between transmissions
This is **Chirp Spread Spectrum (CSS)**, the modulation used by LoRa. > 📸 `[screenshot: nc output showing the IQ stream banner]`
### 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.
> 📸 `[screenshot: spectrogram showing upchirp preamble and downchirp sync patterns]`
### Step 3 — Determine parameters
### 3. Determine Parameters
- Each chirp spans 128 samples → **N = 128** - Each chirp spans 128 samples → **N = 128**
- Since N = 2^SF → **SF = 7** (spreading factor) - `N = 2^SF`**SF = 7** (spreading factor)
- Bandwidth = sample rate = 8000 Hz (baseband at Nyquist) - Each symbol encodes 7 bits → 128 possible symbols
- 7 bits per symbol - Baseband sample rate = 8000 Hz
### 4. Implement Dechirping ### Step 4 — Implement dechirping
The key to CSS demodulation is **dechirping**:
1. Generate the base upchirp (symbol 0): ```python
``` import numpy as np
x0[n] = exp(j * π * n²/N) for n = 0..127
# 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))
``` ```
2. To decode a received chirp, multiply by the **conjugate** of the base chirp: ### Step 5 — Detect frames
```
dechirped[n] = received[n] * conj(x0[n])
```
3. Take the **DFT/FFT** of the dechirped signal. The peak bin = symbol value.
### 5. Detect Frames
Frame structure: Frame structure:
``` ```
[Preamble: 8× symbol 0] [Sync: 2× downchirp] [Header: 1 symbol] [Payload: L symbols] [Preamble: 8x symbol 0] [Sync: 2x downchirp] [Header: 1 symbol = length] [Payload: L symbols]
``` ```
- **Preamble**: 8 consecutive chirps all decoding to symbol 0 Scan the IQ stream for runs of 8 consecutive symbols decoding to 0.
- **Sync**: 2 downchirps (conjugate of upchirps)
- **Header**: 1 symbol = payload length in bytes (Gray-coded) ### Step 6 — Gray decode and symbol-to-byte unpacking
- **Payload**: L symbols encoding the data bytes
### 6. Gray Decoding
Symbol values are **Gray-coded** (like real LoRa). After finding the FFT peak
bin, apply inverse Gray code:
```python ```python
def gray_decode(val): def gray_decode(val):
mask = val mask = val
@ -67,36 +126,51 @@ def gray_decode(val):
mask >>= 1 mask >>= 1
val ^= mask val ^= mask
return val 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))
``` ```
### 7. Symbol-to-Byte Unpacking > 📸 `[screenshot: Python decoder output showing decoded symbols and CRC16 validation pass]`
Each symbol carries 7 bits (SF=7). Concatenate all bits from decoded symbols,
then group into 8-bit bytes. ### Step 7 — Parse frame payload and decrypt
### 8. Parse Frame Payload
Payload format: `[type:1] [data:L] [crc16:2]` Payload format: `[type:1] [data:L] [crc16:2]`
- Type 0x01 = beacon (ASCII text, for verification) - Type `0x01` = beacon (cleartext ASCII, for verification)
- Type 0x02 = data (XOR-encrypted flag) - Type `0x02` = data frame (XOR-encrypted flag)
- CRC-16 CCITT validates the payload
### 9. Decrypt Flag
The data frame's content is XOR'd with the repeating key `"L41N"` (4 bytes).
```python ```python
xor_key = b"L41N" import crcmod
flag = bytes(b ^ xor_key[i % 4] for i, b in enumerate(encrypted_data))
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())
``` ```
## Key Insights > 📸 `[screenshot: script printing the decrypted flag from the data frame]`
- CSS/LoRa modulation encodes data as cyclic frequency shifts of a chirp signal
- The dechirp + FFT technique converts the frequency-domain problem into a simple peak detection ### Key insights
- Gray coding ensures that adjacent symbols (close FFT bins) differ by only 1 bit, reducing errors
- The 7-bit symbol → 8-bit byte packing is standard for non-byte-aligned symbol sizes - CSS encodes data as a cyclic frequency shift of a chirp signal
- The banner hints at CSS ("Chirp Spread Spectrum detected") to point players in the right direction - 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 ## Flag
`ESPILON{4cc3l4_ch1rp_spr34d_w1r3d}`
## Author `ESPILON{4cc3l4_ch1rp_spr34d_w1r3d}`
Eun0us