Prerequisites: Weeks 9-10 lectures; GNU Radio Companion installed; Python NumPy/SciPy; URH installed Duration: ~3 hours (split across two lab sessions or one extended session) Points: 100 (three exercises, each scored separately)
Authorization
- Lab Authorization Form signed
- Exercise B transmission: inside Faraday cage only; ANTSDR E200 or HackRF One; 433.92 MHz
- Exercise C: observe instructor demo device only; no independent replay or attack against real consumer devices
- Virtual path students: no live transmission required
Overview
This lab has three exercises:
| Exercise | Topic | Time | Hardware Required |
|---|---|---|---|
| A | ASK IQ demodulation in Python | 45 min | None (virtual path file) |
| B | BPSK 3-way handshake (GNU Radio) | 75 min | ANTSDR E200 or HackRF One (or virtual path) |
| C | Rolling-code observation and analysis | 60 min | RTL-SDR + instructor demo device (or virtual path) |
Exercise A — ASK IQ Demodulation (45 min)
All students complete this exercise.
Download lab9_ask_capture.sigmf-data and lab9_ask_capture.sigmf-meta from the course portal.
The SigMF metadata file contains:
{
"global": {
"core:datatype": "cf32_le",
"core:sample_rate": 250000,
"core:version": "1.0.0"
},
"captures": [{"core:frequency": 433920000, "core:sample_start": 0}]
}
A1 — Load and inspect
import numpy as np
import json, matplotlib.pyplot as plt
# Load metadata
with open('lab9_ask_capture.sigmf-meta') as f:
meta = json.load(f)
sample_rate = meta['global']['core:sample_rate']
center_freq = meta['captures'][0]['core:frequency']
# Load IQ data
samples = np.fromfile('lab9_ask_capture.sigmf-data', dtype=np.complex64)
print(f"Sample rate: {sample_rate} Hz")
print(f"Center freq: {center_freq/1e6} MHz")
print(f"Duration: {len(samples)/sample_rate:.2f} seconds")
A2 — Demodulate ASK to bitstream
from scipy.signal import butter, filtfilt
import numpy as np
# Step 1: amplitude envelope
amplitude = np.abs(samples)
# Step 2: low-pass filter (5 kHz cutoff -- adjust for signal's bit rate)
b, a = butter(4, 5000 / (sample_rate / 2), btype='low')
smoothed = filtfilt(b, a, amplitude)
# Step 3: normalize to [0, 1]
normalized = (smoothed - smoothed.min()) / (smoothed.max() - smoothed.min())
# Step 4: threshold at 0.5
binary = (normalized > 0.5).astype(int)
# Step 5: estimate bit rate (find transition spacing)
transitions = np.where(np.diff(binary) != 0)[0]
if len(transitions) > 2:
avg_half_period = np.median(np.diff(transitions)) / 2
bit_rate_est = sample_rate / (avg_half_period * 2)
print(f"Estimated bit rate: {bit_rate_est:.0f} bps")
samples_per_bit = int(sample_rate / bit_rate_est)
else:
print("No transitions found -- check filter cutoff or signal window")
samples_per_bit = None
A3 — Sample at symbol centers and decode
# Sample at the center of each bit period
if samples_per_bit:
# Find first transition (start of preamble)
first_transition = transitions[0] if len(transitions) > 0 else 0
start = first_transition + samples_per_bit // 2 # offset to symbol center
# Sample every samples_per_bit points
sample_points = range(start, len(binary) - samples_per_bit, samples_per_bit)
decoded_bits = [binary[i] for i in sample_points]
# Convert bits to hex (group in bytes)
decoded_bytes = []
for i in range(0, len(decoded_bits) - 7, 8):
byte_bits = decoded_bits[i:i+8]
byte_val = int(''.join(str(b) for b in byte_bits), 2)
decoded_bytes.append(byte_val)
print("Decoded bytes:", [hex(b) for b in decoded_bytes[:20]])
A Deliverable
- Plot: amplitude envelope + binary threshold + sampling points (matplotlib figure with 3 subplots)
- Decoded byte sequence (first 10 bytes in hex)
- Identify: can you find a preamble pattern? A repeating structure? What is the approximate bit rate?
Expected output checksum: the first 6 bytes of the decoded sequence should be aa aa aa d5 7f 3c (standard 433 MHz remote preamble format). If you get a different result, check your bit-rate estimation and threshold.
Exercise B — BPSK 3-Way Handshake (75 min)
In-lab students: use ANTSDR E200 or HackRF One inside the Faraday cage. Virtual path students: use GNU Radio with Channel Model block (no live TX).
B1 — Frame format implementation
# handshake_frames.py
import numpy as np
import struct
import binascii
PREAMBLE = bytes([0xAA, 0xAA, 0xAA, 0xAA])
TYPE_SYN = 0x01
TYPE_ACK = 0x02
TYPE_SYNACK = 0x03
def make_frame(ftype, seq, payload=b'\x00'*8):
body = bytes([ftype, seq]) + payload
crc = binascii.crc_hqx(body, 0xFFFF)
frame = PREAMBLE + body + struct.pack('>H', crc)
return frame
def verify_frame(frame):
if frame[:4] != PREAMBLE:
return None, "bad preamble"
body = frame[4:-2]
crc_received = struct.unpack('>H', frame[-2:])[0]
crc_expected = binascii.crc_hqx(body, 0xFFFF)
if crc_received != crc_expected:
return None, f"CRC mismatch: got {crc_received:04x} expected {crc_expected:04x}"
ftype, seq = body[0], body[1]
return {'type': ftype, 'seq': seq, 'payload': body[2:]}, "ok"
# Generate test frames
syn = make_frame(TYPE_SYN, 0)
synack = make_frame(TYPE_SYNACK, 0)
ack = make_frame(TYPE_ACK, 0)
print("SYN frame bytes:", syn.hex())
print("SYN-ACK frame bytes:", synack.hex())
print("ACK frame bytes:", ack.hex())
# Verify round-trip
result, status = verify_frame(syn)
print(f"SYN verify: {status}, type={result['type']:#04x}" if result else f"SYN verify failed: {status}")
B2 — Convert frames to BPSK symbols
# bpsk_encode.py
import numpy as np
def frame_to_bpsk_symbols(frame_bytes, samples_per_symbol=8):
bits = np.unpackbits(np.frombuffer(frame_bytes, dtype=np.uint8))
bpsk = (2*bits.astype(np.float32) - 1).astype(np.complex64) # 0->-1, 1->+1
return np.repeat(bpsk, samples_per_symbol)
syn_frame = make_frame(0x01, 0x00) # from above
syn_symbols = frame_to_bpsk_symbols(syn_frame)
syn_symbols.tofile('syn_frame.iq')
print(f"SYN symbols: {len(syn_symbols)} samples ({len(syn_symbols)/1e6*1e3:.2f} ms at 1 Msps)")
B3 — GNU Radio flowgraph
In-lab: Open GNU Radio Companion. Build the following TX flowgraph:
[File Source: syn_frame.iq, dtype=complex] → [Throttle: 1M] → [BPSK Mod: sps=8, alpha=0.35] → [osmocom Sink: 433.92 MHz, 1M sps, gain=20]
And the RX flowgraph on the second SDR port:
[osmocom Source: 433.92 MHz, 1M sps] → [LPF: cutoff 50kHz] → [BPSK Demod: sps=8] → [File Sink: received.iq]
Virtual path: Use this loopback flowgraph (no hardware):
[File Source: syn_frame.iq] → [Throttle: 1M] → [BPSK Mod: sps=8] → [Channel Model: noise 0.01] → [BPSK Demod: sps=8] → [File Sink: received_bits.iq]
B4 — 3-way handshake exchange
Implement a simple state machine that:
- Sends SYN (Station A)
- Receives SYN, sends SYN-ACK (Station B)
- Receives SYN-ACK, sends ACK (Station A)
- Both sides log handshake complete
For the lab submission, use the flowgraph in loopback mode and demonstrate that your SYN frame survives the encode → BPSK Mod → Channel Model → BPSK Demod → decode pipeline intact.
B Deliverable
- Screenshot of GNU Radio Companion flowgraph
- Output of frame verification: decoded bytes from
received_bits.iqmatch the original SYN frame - 1 paragraph: what happens to the decoded bits when you increase the Channel Model noise voltage from 0.01 to 0.5? At what noise level does the CRC start failing?
Exercise C — Rolling Code Observation (60 min)
In-lab students: use RTL-SDR + instructor demo device at the observation station.
Virtual path students: use lab9_rolling_codes_5x.sigmf-data from the course portal.
C1 — Capture 5 consecutive transmissions
In-lab:
# Record 30 seconds of transmissions from the instructor demo device
rtl_sdr -f 433920000 -s 250000 -g 40 -n $((250000 * 30)) rolling_code_capture.iq
Open the capture in URH (Universal Radio Hacker):
urh rolling_code_capture.iq
Set: modulation = ASK, sample rate = 250000, center frequency = 433920000.
C2 — Identify and extract frames
In URH's Analysis tab, let URH auto-detect the bit rate. Switch to the Protocol view. You should see 5 frames corresponding to the 5 button presses.
For each frame, identify:
- Preamble: which bits are the same in all 5 frames? (This is the sync preamble)
- Serial/Device ID: which bits are the same and non-preamble? (Identifies the device)
- Counter field: which bits change with each transmission? (The rolling counter)
Build a table:
| Transmission # | Full bit sequence (hex) | Counter field (hex) |
|---|---|---|
| 1 | ||
| 2 | ||
| 3 | ||
| 4 | ||
| 5 |
C3 — Counter analysis
Plot the counter field values (y-axis) vs. transmission number (x-axis). The counter should increment by 1 each time.
counters = [0x1234, 0x1235, 0x1236, 0x1237, 0x1238] # replace with your actual values
import matplotlib.pyplot as plt
plt.plot(range(1,6), counters, 'o-')
plt.xlabel('Transmission #')
plt.ylabel('Counter Value (hex)')
plt.title('Rolling Code Counter Progression')
plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: hex(int(x))))
plt.grid(True)
plt.savefig('counter_progression.png', dpi=150)
C Deliverable
- The frame table (5 transmissions with counter values)
counter_progression.png- 2-paragraph analysis:
- Paragraph 1: Given this counter progression, why does replaying Transmission 1 fail after Transmission 2 has been accepted by the receiver? Relate to the synchronization window concept.
- Paragraph 2: If you captured Transmissions 1 and 2 using the Rolljam technique (jam + record both, replay 1 to open, retain 2), what would you do with Transmission 2? Why does the attack succeed without breaking any cryptography?
Write-up Questions
- Exercise A: your ASK demodulator used a fixed threshold (0.5 normalized). What happens when the signal fades (lower RSSI) and the normalized amplitude drops to 0.3? How would you make the threshold adaptive?
- Exercise B: your BPSK flowgraph uses 8 samples per symbol at 1 Msps, so the symbol rate is 125,000 symbols/second. At BPSK (1 bit/symbol), what is the data throughput? How does this compare to 802.11b at 11 Mbps? What would you need to change to increase throughput while keeping the same hardware?
- Exercise C: the instructor demo device's PRNG is an LFSR. Given two consecutive counter values (C1 and C2), the Berlekamp-Massey algorithm can recover the LFSR polynomial in O(n^2) time. Why does this not apply to a rolling code system that uses AES-CTR as its PRNG?
- Reflect on Exercise B vs C: in Exercise B, you built a communication channel that established authenticated mutual presence (SYN/SYN-ACK/ACK). In Exercise C, you observed a one-way authentication system (remote proves identity by knowing the counter sequence). What does the rolling code system lack that your 3-way handshake has?
Submission
Zip into lab9_YOURNAME.zip:
handshake_frames.py,bpsk_encode.pydeliverable_A.png(3-subplot figure) +deliverable_A.md- GNU Radio flowgraph screenshot (
deliverable_B_flowgraph.png) deliverable_B.mddeliverable_C_table.mdcounter_progression.pngdeliverable_C_analysis.mdwriteup.md