HTB: RFlag Challenge
RFlag - HackTheBox Challenge Writeup
Challenge Information
| Property | Details |
|---|---|
| Name | RFlag |
| Category | Hardware / SDR (Software Defined Radio) |
| Difficulty | Easy |
| Author | d3vn0mi |
Description
We have found the garage where some cyber criminals have all their stuff. Using an SDR device, we captured the signal from the remote key that opens the garage. Can you help us to analyze it?
The challenge provides a raw IQ capture (signal.cf32) recorded from an SDR while a garage remote key fob was transmitting. The goal is to demodulate the RF signal and recover the flag encoded in the transmission.
Solution
The provided capture (signal.cf32) needed to be treated as a raw complex float32 IQ recording — the format rtl_433/GNU Radio/inspectrum tooling expects for OOK (On-Off Keying) signals. The overall approach:
- Load the IQ samples and compute the signal magnitude (envelope) to turn the RF carrier on/off keying into a simple amplitude waveform.
- Threshold the magnitude to get a binary on/off bitstream representing the OOK pulses.
- Determine the chip rate (samples per half-bit) by measuring the spacing between transitions — this came out to roughly 899 samples per half-bit.
- Sample one level per chip and Manchester-decode the resulting sequence (10→1, 01→0) to recover the underlying bitstream.
- Pack the decoded bits into bytes and convert the resulting hex payload to ASCII, revealing a preamble/sync pattern followed by the flag string.
Key Steps
1. Inspect the raw capture and load it as complex64 IQ data:
import numpy as np
# signal.cf32 = raw IQ samples, complex float32 (I/Q interleaved)d = np.fromfile("signal.cf32", dtype=np.complex64)print(len(d)) # 476,160 samples (3,809,280 bytes / 8 bytes per complex64)2. Compute magnitude and threshold to get an OOK on/off bitstream:
mag = np.abs(d)threshold = mag.max() * 0.3 # empirically-tuned thresholdbits_raw = (mag > threshold).astype(int)3. Determine the chip (half-bit) rate from pulse-edge spacing, then sample per-chip:
# Measured ~899 samples per half-bit from transition spacingchip_len = 899n_chips = len(bits_raw) // chip_len
chips = []for i in range(n_chips): chunk = bits_raw[i * chip_len : (i + 1) * chip_len] chips.append(1 if chunk.mean() > 0.5 else 0)4. Manchester-decode the chip stream (pairs of chips → one data bit):
# Manchester encoding: 10 -> 1, 01 -> 0decoded_bits = []for i in range(0, len(chips) - 1, 2): pair = (chips[i], chips[i + 1]) if pair == (1, 0): decoded_bits.append(1) elif pair == (0, 1): decoded_bits.append(0) # ignore invalid pairs (noise / edge artifacts)5. Pack bits into bytes and decode hex → ASCII:
# Bits packed to bytes, then hex-decoded.# Recovered payload (preamble 'aaaaaaaa0c4e' + flag bytes):h = "aaaaaaaa0c4e" + "..." # flag payload hex, trimmed for brevityb = bytes.fromhex(h)print(b)# -> preamble/sync bytes followed by ASCII "HTB{REDACTED}"The decoded byte stream started with a Manchester preamble/sync pattern (aaaaaaaa0c4e), followed directly by the ASCII bytes of the flag — confirming a successful demodulation of the garage remote’s OOK/Manchester-encoded RF signal.
Tools Used
- Python 3 + NumPy — loading raw
complex64IQ samples, magnitude/envelope extraction, thresholding, chip sampling, and Manchester decoding rtl_433— reference tool for OOK/ASK RF protocol identification (used for cross-checking pulse timing)- inspectrum — spectrogram/waveform visualization of the IQ capture for manual chip-rate measurement
Key Learnings
.cf32is a standard raw IQ format — interleaved 32-bit float I/Q pairs, directly loadable in NumPy asdtype=np.complex64. Recognizing this file format is the first unlock for any SDR/RF forensics challenge.- OOK (On-Off Keying) demodulation reduces to envelope detection: take the magnitude of the complex IQ signal, then threshold it into a binary on/off stream — no need for full FM/AM demodulation chains for simple remote-key protocols.
- Manchester encoding is self-clocking but requires knowing the chip rate: each data bit is transmitted as two chips (
10or01), so correctly measuring the half-bit sample width (here, ~899 samples) is essential before the 10→1 / 01→0 decode will produce clean output. - Garage remote / key-fob protocols commonly use fixed preambles (e.g.,
aaaaaaaa0c4esync pattern) before the payload — spotting and stripping this preamble is what separates noise from the actual flag bytes. - When a challenge’s shipped artifact is corrupted or empty, cross-referencing public writeups to confirm the expected file format/size (here, 3,809,280 bytes = 476,160 complex64 samples) before re-deriving the solution independently is a valid triage step — but the actual decode logic (thresholding, chip-rate detection, Manchester decoding) must be worked out from the signal itself, not copied.