ESPILON-CTF-2026-Writeups/Misc/Accela_Signal
2026-03-22 19:18:58 +01:00
..
README.md ESPILON CTF 2026 — Write-ups édition 1 (33 challenges) 2026-03-22 19:18:58 +01:00

Accela Signal -- Solution

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.

Steps

1. Capture IQ Stream

Connect to TCP port 9002. A text banner appears first, followed by raw IQ data.

nc HOST 9002 > capture.raw
# Or use the solve script

The banner tells you: IQ baseband, 8000 sps, int16 LE interleaved.

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

This is Chirp Spread Spectrum (CSS), the modulation used by LoRa.

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

4. Implement Dechirping

The key to CSS demodulation is dechirping:

  1. Generate the base upchirp (symbol 0):

    x0[n] = exp(j * π * n²/N)    for n = 0..127
    
  2. To decode a received chirp, multiply by the conjugate of the base chirp:

    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:

[Preamble: 8× symbol 0] [Sync: 2× downchirp] [Header: 1 symbol] [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

6. Gray Decoding

Symbol values are Gray-coded (like real LoRa). After finding the FFT peak bin, apply inverse Gray code:

def gray_decode(val):
    mask = val
    while mask:
        mask >>= 1
        val ^= mask
    return val

7. Symbol-to-Byte Unpacking

Each symbol carries 7 bits (SF=7). Concatenate all bits from decoded symbols, then group into 8-bit bytes.

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

xor_key = b"L41N"
flag = bytes(b ^ xor_key[i % 4] for i, b in enumerate(encrypted_data))

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

Flag

ESPILON{4cc3l4_ch1rp_spr34d_w1r3d}

Author

Eun0us