write-up: Misc/Accela_Signal/README.md
This commit is contained in:
parent
188d3d7f93
commit
5d14fe51df
@ -1,65 +1,124 @@
|
||||
# Accela Signal -- Solution
|
||||
# Accela Signal
|
||||
|
||||
## Overview
|
||||
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
|
||||
demodulation from scratch to decode the frames.
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Category | Misc |
|
||||
| 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
|
||||
nc HOST 9002 > capture.raw
|
||||
# Or use the solve script
|
||||
nc <HOST> 9002 > capture.raw
|
||||
```
|
||||
|
||||
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
|
||||
GNU Radio). You'll see:
|
||||
- Characteristic **chirp** patterns: frequency sweeps from low to high
|
||||
- Repeating preambles (identical chirps)
|
||||
- Gaps of noise between transmissions
|
||||
```
|
||||
IQ baseband, 8000 sps, int16 LE interleaved
|
||||
Chirp Spread Spectrum detected. N=128.
|
||||
```
|
||||
|
||||
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**
|
||||
- Since N = 2^SF → **SF = 7** (spreading factor)
|
||||
- Bandwidth = sample rate = 8000 Hz (baseband at Nyquist)
|
||||
- 7 bits per symbol
|
||||
- `N = 2^SF` → **SF = 7** (spreading factor)
|
||||
- Each symbol encodes 7 bits → 128 possible symbols
|
||||
- Baseband sample rate = 8000 Hz
|
||||
|
||||
### 4. Implement Dechirping
|
||||
The key to CSS demodulation is **dechirping**:
|
||||
### Step 4 — Implement dechirping
|
||||
|
||||
1. Generate the base upchirp (symbol 0):
|
||||
```
|
||||
x0[n] = exp(j * π * n²/N) for n = 0..127
|
||||
```
|
||||
```python
|
||||
import numpy as np
|
||||
|
||||
2. To decode a received chirp, multiply by the **conjugate** of the base chirp:
|
||||
```
|
||||
dechirped[n] = received[n] * conj(x0[n])
|
||||
```
|
||||
# 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]
|
||||
|
||||
3. Take the **DFT/FFT** of the dechirped signal. The peak bin = symbol value.
|
||||
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
|
||||
|
||||
### 5. Detect Frames
|
||||
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
|
||||
- **Sync**: 2 downchirps (conjugate of upchirps)
|
||||
- **Header**: 1 symbol = payload length in bytes (Gray-coded)
|
||||
- **Payload**: L symbols encoding the data bytes
|
||||
Scan the IQ stream for runs of 8 consecutive symbols decoding to 0.
|
||||
|
||||
### Step 6 — Gray decode and symbol-to-byte unpacking
|
||||
|
||||
### 6. Gray Decoding
|
||||
Symbol values are **Gray-coded** (like real LoRa). After finding the FFT peak
|
||||
bin, apply inverse Gray code:
|
||||
```python
|
||||
def gray_decode(val):
|
||||
mask = val
|
||||
@ -67,36 +126,51 @@ def gray_decode(val):
|
||||
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))
|
||||
```
|
||||
|
||||
### 7. Symbol-to-Byte Unpacking
|
||||
Each symbol carries 7 bits (SF=7). Concatenate all bits from decoded symbols,
|
||||
then group into 8-bit bytes.
|
||||
> 📸 `[screenshot: Python decoder output showing decoded symbols and CRC16 validation pass]`
|
||||
|
||||
### Step 7 — Parse frame payload and decrypt
|
||||
|
||||
### 8. Parse Frame Payload
|
||||
Payload format: `[type:1] [data:L] [crc16:2]`
|
||||
|
||||
- Type 0x01 = beacon (ASCII text, for verification)
|
||||
- Type 0x02 = data (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).
|
||||
- Type `0x01` = beacon (cleartext ASCII, for verification)
|
||||
- Type `0x02` = data frame (XOR-encrypted flag)
|
||||
|
||||
```python
|
||||
xor_key = b"L41N"
|
||||
flag = bytes(b ^ xor_key[i % 4] for i, b in enumerate(encrypted_data))
|
||||
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())
|
||||
```
|
||||
|
||||
## Key Insights
|
||||
- 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
|
||||
- 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
|
||||
- The banner hints at CSS ("Chirp Spread Spectrum detected") to point players in the right direction
|
||||
> 📸 `[screenshot: script printing the decrypted flag from the data frame]`
|
||||
|
||||
### 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}`
|
||||
|
||||
## Author
|
||||
Eun0us
|
||||
`ESPILON{4cc3l4_ch1rp_spr34d_w1r3d}`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user