write-up: Misc/Accela_Signal/README.md
This commit is contained in:
parent
93c18e1f4b
commit
cd3c690042
@ -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
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user