Classroom Glossary Public page

WIR-101 Lab 9 — IQ Demodulation, PSK 3-Way Handshake, and Rolling Code Analysis

885 words

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

  1. Plot: amplitude envelope + binary threshold + sampling points (matplotlib figure with 3 subplots)
  2. Decoded byte sequence (first 10 bytes in hex)
  3. 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:

  1. Sends SYN (Station A)
  2. Receives SYN, sends SYN-ACK (Station B)
  3. Receives SYN-ACK, sends ACK (Station A)
  4. 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

  1. Screenshot of GNU Radio Companion flowgraph
  2. Output of frame verification: decoded bytes from received_bits.iq match the original SYN frame
  3. 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

  1. The frame table (5 transmissions with counter values)
  2. counter_progression.png
  3. 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

  1. 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?
  2. 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?
  3. 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?
  4. 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.py
  • deliverable_A.png (3-subplot figure) + deliverable_A.md
  • GNU Radio flowgraph screenshot (deliverable_B_flowgraph.png)
  • deliverable_B.md
  • deliverable_C_table.md
  • counter_progression.png
  • deliverable_C_analysis.md
  • writeup.md