← Back to Writeups
CUSTOMMediumHardware

FM Hunting

XESXOR9/2/20264 min read
#hardware#custom#medium#baud-rate-detection-from-signa#envelope-detection-via-hilbert#fsk-demodulation-with-bandpass#xor-decryption-with-extracted-#linux

FM Hunting

Platform: ASIS CTF | Category: Hardware | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2025-12-27 | Status: Solved Techniques: Baud rate detection from signal analysis, Envelope detection via Hilbert transform, FSK demodulation with bandpass filtering, XOR decryption with extracted key

Summary

Task: ASIS Hardware "FM Hunting" — radio hunting. Solution: FM radio signal analysis, frequency identification.

FM Hunting - ASIS CTF

Description

A wayward field node on 65.109.194.34:7356 spills raw baseband IQ at 48 kHz. It's a simple 2-FSK beacon. Follow the payload to get the flag!

Analysis

Step 1: Data Capture

Connected to the server and captured raw IQ data:

bash
nc 65.109.194.34 7356 > raw_iq.bin

The data was 16-bit audio samples at 48 kHz sample rate.

Step 2: Signal Analysis

Analyzed the captured data using spectrogram and frequency analysis:

  • Sample rate: 48 kHz
  • Modulation: 2-FSK (Binary Frequency Shift Keying)
  • Tone frequencies:
    • Low tone: ~500 Hz (represents bit 0)
    • High tone: ~23500 Hz (represents bit 1)
  • Baud rate: 100 baud (480 samples per bit) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3: FSK Demodulation

Used bandpass filters to isolate each tone and envelope detection to extract the binary signal:

python
import numpy as np
from scipy.signal import butter, filtfilt, hilbert
from scipy.io import wavfile

# Read audio data
sample_rate, data = wavfile.read('audio_16bit.wav')

# Bandpass filter parameters
def bandpass_filter(data, lowcut, highcut, fs, order=5):
    nyq = 0.5 * fs
    low = lowcut / nyq
    high = highcut / nyq
    b, a = butter(order, [low, high], btype='band')
    return filtfilt(b, a, data)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Filter for each tone
low_tone = bandpass_filter(data, 400, 600, sample_rate)   # ~500 Hz
high_tone = bandpass_filter(data, 23000, 24000, sample_rate)  # ~23500 Hz

# Envelope detection using Hilbert transform
low_envelope = np.abs(hilbert(low_tone))
high_envelope = np.abs(hilbert(high_tone))

# Determine bits based on which envelope is stronger
samples_per_bit = 480  # 48000 Hz / 100 baud
bits = []

for i in range(0, len(data) - samples_per_bit, samples_per_bit):
    low_power = np.mean(low_envelope[i:i+samples_per_bit])
    high_power = np.mean(high_envelope[i:i+samples_per_bit])
    bits.append(1 if high_power > low_power else 0)

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Bit to Byte Conversion

Converted bits to bytes using LSB-first ordering:

python
def bits_to_bytes_lsb(bits):
    result = []
    for i in range(0, len(bits) - 7, 8):
        byte = 0
        for j in range(8):
            byte |= (bits[i + j] << j)  # LSB first
        result.append(byte)
    return bytes(result)

decoded = bits_to_bytes_lsb(bits)

Step 5: Message Analysis

The decoded message contained:

  • Text: "This file contains a message encrypted with XOR"
  • KEY:{0lymp1c} at offset 347
  • Encrypted FLAG at offset 659 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 6: XOR Decryption

python
key = b'0lymp1c'
encrypted_flag = decoded[659:659+24]  # FLAG: section

def xor_decrypt(data, key):
    return bytes([data[i] ^ key[i % len(key)] for i in range(len(data))])

flag = xor_decrypt(encrypted_flag, key)
print(flag.decode())  # ASIS{REDACTED}

Solution

Complete demodulation and decryption script:

python
#!/usr/bin/env python3
"""
ASIS CTF - FM Hunting
2-FSK Demodulation and XOR Decryption
"""

import numpy as np
from scipy.signal import butter, filtfilt, hilbert
from scipy.io import wavfile
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def bandpass_filter(data, lowcut, highcut, fs, order=5):
    """Apply bandpass filter to isolate frequency band"""
    nyq = 0.5 * fs
    low = lowcut / nyq
    high = highcut / nyq
    b, a = butter(order, [low, high], btype='band')
    return filtfilt(b, a, data)

def demodulate_fsk(audio_file, low_freq=500, high_freq=23500, baud_rate=100):
    """Demodulate 2-FSK signal"""
    sample_rate, data = wavfile.read(audio_file)
    
    # Normalize if stereo
    if len(data.shape) > 1:
        data = data[:, 0]
    
    # Bandpass filter for each tone
    low_tone = bandpass_filter(data, low_freq - 100, low_freq + 100, sample_rate)
    high_tone = bandpass_filter(data, high_freq - 500, high_freq + 500, sample_rate)
    
    # Envelope detection
    low_envelope = np.abs(hilbert(low_tone))
    high_envelope = np.abs(hilbert(high_tone))
    
    # Extract bits
    samples_per_bit = sample_rate // baud_rate
    bits = []
    
    for i in range(0, len(data) - samples_per_bit, samples_per_bit):
        low_power = np.mean(low_envelope[i:i+samples_per_bit])
        high_power = np.mean(high_envelope[i:i+samples_per_bit])
        bits.append(1 if high_power > low_power else 0)
    
    return bits
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def bits_to_bytes_lsb(bits):
    """Convert bits to bytes (LSB first)"""
    result = []
    for i in range(0, len(bits) - 7, 8):
        byte = 0
        for j in range(8):
            byte |= (bits[i + j] << j)
        result.append(byte)
    return bytes(result)

def xor_decrypt(data, key):
    """XOR decrypt with repeating key"""
    return bytes([data[i] ^ key[i % len(key)] for i in range(len(data))])

def main():
    # Demodulate FSK signal
    bits = demodulate_fsk('audio_16bit.wav')
    decoded = bits_to_bytes_lsb(bits)
    
    # Save decoded data
    with open('decoded.bin', 'wb') as f:
        f.write(decoded)
    
    # Find and extract key
    key_marker = b'KEY:{'
    key_start = decoded.find(key_marker)
    if key_start != -1:
        key_end = decoded.find(b'}', key_start)
        key = decoded[key_start + len(key_marker):key_end]
        print(f"Found key: {key.decode()}")
    
    # Find and decrypt flag
    flag_marker = b'FLAG:'
    flag_start = decoded.find(flag_marker)
    if flag_start != -1:
        # Extract encrypted flag bytes after marker
        encrypted = decoded[flag_start + len(flag_marker):flag_start + len(flag_marker) + 50]
        # Remove any padding/null bytes
        encrypted = encrypted.split(b'\x00')[0]
        
        decrypted = xor_decrypt(encrypted, key)
        print(f"Decrypted flag: {decrypted.decode()}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

if __name__ == '__main__':
    main()

Notes

  • Flag meaning: "XOR ALL THE THINGS" in leet-speak (X0R = XOR, A11 = ALL, 7H3 = THE, 7H1NG5 = THINGS)
  • 2-FSK: Binary Frequency Shift Keying uses two frequencies to represent 0 and 1
  • Hilbert transform: Used for envelope detection to extract amplitude of filtered signals
  • LSB-first: Least Significant Bit first ordering for byte reconstruction
  • Baud rate detection: 48000 Hz / 480 samples per bit = 100 baud

References