courses

Zigbee

Why this matters

Zigbee is the oldest of the modern low-power mesh protocols and the one with the largest installed base of insecure devices. It is in bulbs, locks, blinds, smoke detectors, industrial sensors and half the products sold under a hub vendor’s own brand.

Its security architecture makes one decision that determines everything else: the entire mesh shares a single network key. Compromise any device — the cheapest bulb, the one nobody thought about — and you hold the key that decrypts and forges traffic for the whole network. Every Zigbee finding you will write is, in the end, about how someone obtained that key.

The radio layer

Zigbee does not define a radio. It sits on IEEE 802.15.4, which it shares with Thread, and which supplies the PHY and MAC.

  2.4 GHz (channels 11–26) 915 MHz (channels 1–10) 868 MHz (channel 0)
Modulation O-QPSK with DSSS BPSK / O-QPSK BPSK / O-QPSK
Rate 250 kb/s 40–250 kb/s 20–100 kb/s
Channel width 2 MHz, 5 MHz spacing 2 MHz —
Usage Effectively all Zigbee Rare Rare

In practice Zigbee means 2.4 GHz, channels 11 through 26, and a handful of those dominate: channels 11, 15, 20 and 25 are the conventional choices because they fall in the gaps between WiFi channels 1, 6 and 11. When you go hunting, start there.

Range per hop is modest — tens of metres indoors — and the mesh is what gives it building coverage. Routers (mains-powered devices, typically bulbs and plugs) forward for battery-powered end devices that spend their lives asleep.

⚠️ Coexistence is a real operational effect. 802.15.4’s 2 MHz channels sit inside WiFi’s 20 MHz ones. A busy WiFi network on channel 6 will degrade Zigbee channels 15–19 measurably. If a mesh behaves strangely during a lab, check the WiFi before you blame your capture.

Capturing it

Unlike Bluetooth, 802.15.4 does not hop. Pick the channel and you see everything on it — which makes Zigbee one of the easiest radios in this course to observe, and a good first mesh to study.

Hardware Notes
TI CC2531 USB dongle Cheapest option, widely available, KillerBee support
TI CC1352/CC2652 LaunchPad Current-generation, multiprotocol
Atmel RZUSBstick The classic KillerBee target; long discontinued
ApiMote Purpose-built for 802.15.4 security work
nRF52840 dongle With 802.15.4 sniffer firmware; also does Thread

Wireshark dissects 802.15.4 and Zigbee natively, and will decrypt the payloads if you give it the network key — which is the point of everything below.

The stack above the radio

Layer Provides Key used
ZCL / ZDO Clusters: on/off, level, lock, OTA —
APS End-to-end addressing, binding Link key (pairwise)
NWK Mesh routing, broadcast Network key (shared)
802.15.4 MAC/PHY Framing, CSMA-CA, addressing (optionally MAC key)

Three device roles: exactly one coordinator (which is also the Trust Center), any number of routers, and end devices that sleep and do not forward.

The Trust Center decides who joins and distributes keys. In a home system it is the hub; compromise it and there is nothing left to attack.

The security model, and its single point of failure

All Zigbee cryptography is AES-128 in CCM* — the primitive is not the problem. The problem is key distribution.

The network key is shared by every node. It protects NWK-layer frames, so any device holding it can decrypt all mesh traffic and inject frames that every other node accepts. There is no per-device isolation at this layer and no forward secrecy: recover the key and historical captures decrypt too.

Link keys are pairwise, between a device and the Trust Center, protecting APS-layer messages. Their main job is protecting the network key in transit, during joining.

And that is where it has always gone wrong.

For a device to join, the Trust Center sends it the network key, encrypted with a link key. But the joining device does not have a pairwise link key yet — it has just arrived. Something has to bootstrap it.

Pre-3.0 Zigbee bootstrapped with a global, publicly documented default link key, the ASCII string ZigBeeAlliance09. It was specified to make testing and interoperability easy, and vendors shipped it enabled.

The consequence is direct: capture a join, decrypt the key transport with a key everyone knows, and you have the network key. No cryptanalysis, no implementation bug. The design told you the answer.

⚠️ You do not have to wait for a join. Deauthenticating a device forces a rejoin, and a rejoin re-transports the key. This is the Zigbee analogue of deauth-to-capture-a-handshake on WiFi — the same idea with a different acronym.

What Zigbee 3.0 changed

Zigbee 3.0 addressed this in two ways:

Both are real improvements. Neither helps if the network still accepts legacy joins — and networks do, because the hub vendor wants your ten-year-old sensors to keep working. A Zigbee 3.0 hub in backward-compatible mode has Zigbee 1.x’s key transport problem, and backward-compatible mode is the shipping default.

That is the assessment question to ask, and it is not answered by the version number on the box.

Zigbee Light Link’s Touchlink commissioning lets you pair a bulb by being physically near it. Proximity is enforced by transmit power alone, and it is authenticated by a global master key shared across all ZLL products — which leaked years ago.

The practical result: from a distance considerably greater than “touching”, using an amplifier, an attacker can factory-reset bulbs, steal them onto their own network, or command them. Ronen et al.’s IoT Goes Nuclear combined Touchlink range abuse with a flaw in Philips Hue’s OTA image verification to demonstrate a worm propagating bulb-to-bulb across a city — the canonical demonstration that a “harmless” device class is a routable attack surface.

Proximity is not authentication. It is a recurring theme: Bluetooth needed Channel Sounding for the same reason.

Replay and frame counters

NWK and APS frames carry monotonic frame counters, and receivers reject counters they have already seen. The protection is real and it has the same weakness as everywhere else in this course: the counter must survive a reboot, so it lives in flash, and flash wears out.

Implementers write it lazily, or skip forward in blocks, or reset it. A node that resets its counter to zero must either be rejected forever or be allowed to re-announce itself — and vendors overwhelmingly choose availability. See LoRa for the identical failure in a completely different protocol.

Worked example: recovering the network key from a join

zbdsniff exists precisely because the default-link-key problem is so mechanical. Identify the interface, find the network, capture, then decrypt.

$ sudo zbid
    Dev      Product String       Serial Number
  1:6        CC2531 USB Dongle    __01F5C4E2

Survey for networks. zbstumbler transmits beacon requests, so it is active — use it in lab, not in the wild:

$ sudo zbstumbler -i 1:6
zbstumbler: Transmitting and receiving on interface '1:6'
New Network: PANID 0x8A1C  Source 0x0000
        Ext PANID: 00:12:4b:00:1c:a3:f1:22
        Stack Profile: ZigBee Enterprise
        Stack Version: ZigBee 2006/2007
        Channel: 15

Lock to channel 15 and capture. Then force the target to rejoin — power-cycling the device in lab is the honest way to do it:

$ sudo zbdump -i 1:6 -f 15 -w join.pcap
zbdump: listening on '1:6', channel 15, page 0 (2425 MHz), link-type DLT_IEEE802_15_4, capture size 127 bytes
^C
42 packets captured

Now decrypt the key transport frame. zbdsniff takes the capture with -f and the link key to try with -k, and the link key is the mandatory argument — which is the whole lesson stated as a command-line flag. Here it is ZigBeeAlliance09 as hex:

$ zbdsniff -f join.pcap -k 5a6967426565416c6c69616e63653039
Processing join.pcap
[+] Extended Source: 00:12:4b:00:1c:a3:f1:22 mapped to 0x0000
[+] Decrypted:
    APS Command: APS_CMD_TRANSPORT_KEY
    Key Type: KEY_TYPE_STANDARD_NWK
    Value: 41:6c:32:31:30:30:4c:61:62:4e:65:74:4b:65:79:21
[+] Processed 1 capture files.

That value is ASCII: Al2100LabNetKey!.

⚠️ Older write-ups show zbdsniff <file> printing NETWORK KEY FOUND:. That is KillerBee 2.x. Current zbdsniff was reimplemented on Scapy, takes -f/-d for input, requires -k, and prints the format above; invoked the old way it exits with “A packet capture file or directory must be specified”.

Hand that key to Wireshark and the whole capture becomes readable — Preferences → Protocols → ZigBee → Pre-configured Keys. From the command line you cannot pass it with -o uat:…: the ZigBee NWK table and the Zigbee Direct table are both registered under the display name Pre-configured Keys, so the lookup resolves to the wrong one. Write the table file instead — ~/.config/wireshark/zigbee_pc_keys, one record per line:

"41:6c:32:31:30:30:4c:61:62:4e:65:74:4b:65:79:21","Normal","Lab"

The key field also accepts a quoted 16-character string, so "Al2100LabNetKey!" works and is easier to check by eye. Then:

$ tshark -r join.pcap \
    -Y zbee_zcl -T fields -e zbee_zcl.cmd.id -e zbee_zcl_general.onoff.cmd.srv_rx.id

If you want to force the security level as well, the preference is zbee_nwk.seclevel and it takes the level’s name, not its number: -o "zbee_nwk.seclevel: AES-128 Encryption, 32-bit Integrity Protection".

The point of the exercise is not the tooling. It is that the network key was protected by a key printed in a public specification, and everything else followed from that one decision.

Key takeaways

References


Related course pages: Radio Protocols · Thread and Matter · Z-Wave · WiFi · Attacks · Tools of the Trade

🛠️ Maintenance note: KillerBee’s hardware support is the fragile part — the RZUSBstick it was built around has been discontinued for years and CC2531 dongles are increasingly scarce, so re-verify the lab’s capture hardware and the zbid output before each offering. The zbdsniff transcript assumes a network still using the default link key; if the lab hub is upgraded to a Zigbee 3.0 stack with install codes, that example needs rebuilding around a deliberately legacy-configured network. KillerBee itself is lightly maintained: at the time of writing zbstumbler on upstream master still contains Python 2 print statements and will not run under Python 3 without patching, though zbdump and zbdsniff are fine — run the whole chain before lab, and be ready to fall back to a CC2531 running zigbee2mqtt’s sniffer firmware or an nRF52840 with Nordic’s 802.15.4 sniffer feeding Wireshark directly. Wireshark’s ZigBee key UAT is reachable only by writing ~/.config/wireshark/zigbee_pc_keys — its display name collides with Zigbee Direct’s table, so -o uat: cannot address it. Re-check that against the installed version.