ESPILON-CTF-2026-Writeups/Misc/Accela_Signal
2026-03-26 17:33:40 +00:00
..
README.md write-up: Misc/Accela_Signal/README.md 2026-03-26 17:33:40 +00:00

Accela Signal

Field Value
Category Misc
Difficulty Hard
Points 500
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

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.

📸 [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

  • Each chirp spans 128 samples → N = 128
  • N = 2^SFSF = 7 (spreading factor)
  • Each symbol encodes 7 bits → 128 possible symbols
  • Baseband sample rate = 8000 Hz

Step 4 — Implement dechirping

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

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))

📸 [screenshot: Python decoder output showing decoded symbols and CRC16 validation pass]

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)
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())

📸 [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}