From 7b668cdb4f986f61af4680d52a3b6ac1209a969b Mon Sep 17 00:00:00 2001 From: rmspeers Date: Thu, 1 Nov 2018 09:16:10 -0400 Subject: [PATCH 01/14] Change sent by Chandra via email. --- killerbee/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/killerbee/__init__.py b/killerbee/__init__.py index fd5ef94e..97d3467c 100644 --- a/killerbee/__init__.py +++ b/killerbee/__init__.py @@ -222,7 +222,7 @@ def check_capability(self, capab): ''' return self.driver.capabilities.check(capab) - def is_valid_channel(self, channel, page): + def is_valid_channel(self, channel, page=0): ''' Use the driver's capabilities class to determine if a requested channel number is within the capabilities of that device. From bee323171a321cd758b5b0b41a5a4ff874aa71c8 Mon Sep 17 00:00:00 2001 From: rmspeers Date: Mon, 5 Nov 2018 16:51:31 -0500 Subject: [PATCH 02/14] Updated for 2.7.1 packaging. Resolved #152. --- killerbee/__init__.py | 15 ++++----- killerbee/dev_cc253x.py | 74 ++++++++++++++++++++--------------------- killerbee/kbutils.py | 35 ++++++++++++++----- setup.py | 21 +++++++----- 4 files changed, 83 insertions(+), 62 deletions(-) diff --git a/killerbee/__init__.py b/killerbee/__init__.py index fd5ef94e..5cba58a2 100644 --- a/killerbee/__init__.py +++ b/killerbee/__init__.py @@ -24,7 +24,7 @@ def getKillerBee(channel, page= 0): raise Exception("Failed to create a KillerBee instance.") try: kb.set_channel(channel, page) - except Exception, e: + except Exception as e: raise Exception('Error: Failed to set channel to %d/%d' % (channel, page), e) return kb @@ -53,14 +53,13 @@ def __init__(self, device=None, datasource=None, gps=None): Instantiates the KillerBee class. @type device: String - @param device: Device identifier, either USB vendor:product, serial device node, or IP address + @param device: Device identifier, either USB :, serial device path, or IP address @type datasource: String - @param datasource: A known datasource type that is used - by dblog to record how the data was captured. + @param datasource: A known data-source type that is used by dblog to record how the data was captured. @type gps: String - @param gps: Optional serial device identifier for an attached GPS - unit. If provided, or if global variable has previously been set, - KillerBee skips that device in initalization process. + @param gps: Optional serial device identifier for an attached GPS unit. + If provided, or if global variable has previously been set, + KillerBee skips that device in initialization process. @return: None @rtype: None ''' @@ -80,7 +79,7 @@ def __init__(self, device=None, datasource=None, gps=None): from dev_sewio import isSewio if isSewio(device): from dev_sewio import SEWIO - self.driver = SEWIO(dev=device) #give it the ip address + self.driver = SEWIO(dev=device) # give it the ip address else: del isSewio # Figure out a device is one is not set, trying USB devices next diff --git a/killerbee/dev_cc253x.py b/killerbee/dev_cc253x.py index aa84151c..5462f95e 100644 --- a/killerbee/dev_cc253x.py +++ b/killerbee/dev_cc253x.py @@ -1,22 +1,24 @@ -''' +""" CC253x support is contributed by Scytmo. -''' +""" + +from __future__ import print_function +import sys +import struct +import time +from datetime import datetime +from kbutils import KBCapabilities, makeFCS # Import USB support depending on version of pyUSB try: import usb.core import usb.util import sys - print >>sys.stderr, "Warning: You are using pyUSB 1.x, support is in beta." + print("Warning: You are using pyUSB 1.x, support is in beta.", file=sys.stderr) except ImportError: import usb - print >>sys.stderr, "Error: You are using pyUSB 0.x, not supported for CC253x." - sys.exit() - -import struct -import time -from datetime import datetime -from kbutils import KBCapabilities, makeFCS + print("Error: You are using pyUSB 0.x, not supported for CC253x.", file=sys.stderr) + sys.exit(-1) class CC253x: @@ -35,13 +37,13 @@ class CC253x: def __init__(self, dev, bus, variant): #TODO deprecate bus param, and dev becomes a usb.core.Device object, not a string in pyUSB 1.x use - ''' + """ Instantiates the KillerBee class for Zigduino running GoodFET firmware. @type dev: String @param dev: PyUSB device @return: None @rtype: None - ''' + """ if variant == CC253x.VARIANT_CC2530: self._data_ep = CC253x.USB_CC2530_DATA_EP else: @@ -80,40 +82,36 @@ def get_capabilities(self): return self.capabilities.getlist() def __set_capabilities(self): - ''' + """ Sets the capability information appropriate for CC253x. @rtype: None @return: None - ''' + """ self.capabilities.setcapab(KBCapabilities.FREQ_2400, True) self.capabilities.setcapab(KBCapabilities.SNIFF, True) self.capabilities.setcapab(KBCapabilities.SETCHAN, True) - #self.capabilities.setcapab(KBCapabilities.INJECT, True) - #self.capabilities.setcapab(KBCapabilities.PHYJAM_REFLEX, True) - #self.capabilities.setcapab(KBCapabilities.SET_SYNC, True) # KillerBee expects the driver to implement this function def get_dev_info(self): - ''' + """ Returns device information in a list identifying the device. @rtype: List @return: List of 3 strings identifying device. - ''' + """ # TODO Determine if there is a way to get a unique ID from the device return [self.name, "CC253x", ""] # KillerBee expects the driver to implement this function def sniffer_on(self, channel=None, page=0): - ''' - Turns the sniffer on such that pnext() will start returning observed - data. Will set the command mode to Air Capture if it is not already - set. + """ + Turns the sniffer on such that pnext() will start returning observed data. + Will set the command mode to Air Capture if it is not already set. @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @rtype: None - ''' + """ self.capabilities.require(KBCapabilities.SNIFF) if channel != None: @@ -137,12 +135,12 @@ def sniffer_on(self, channel=None, page=0): # KillerBee expects the driver to implement this function def sniffer_off(self): - ''' + """ Turns the sniffer off, freeing the hardware for other functions. It is not necessary to call this function before closing the interface with close(). @rtype: None - ''' + """ if self.__stream_open == True: # TODO Here, and in other places, add error handling for ctrl_transfer failure self.dev.ctrl_transfer(CC253x.USB_DIR_OUT, CC253x.USB_XFER_STOP) @@ -156,14 +154,14 @@ def _do_set_channel(self): # KillerBee expects the driver to implement this function def set_channel(self, channel, page=0): - ''' + """ Sets the radio interface to the specifid channel (limited to 2.4 GHz channels 11-26) @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @rtype: None - ''' + """ self.capabilities.require(KBCapabilities.SETCHAN) if channel >= 11 or channel <= 26: @@ -179,7 +177,7 @@ def set_channel(self, channel, page=0): # KillerBee expects the driver to implement this function def inject(self, packet, channel=None, count=1, delay=0, page=0): - ''' + """ Injects the specified packet contents. @type packet: String @param packet: Packet contents to transmit, without FCS. @@ -192,18 +190,18 @@ def inject(self, packet, channel=None, count=1, delay=0, page=0): @type delay: Float @param delay: Delay between each frame, def=1 @rtype: None - ''' + """ raise Exception('Not yet implemented') # KillerBee expects the driver to implement this function def pnext(self, timeout=100): - ''' + """ Returns a dictionary containing packet data, else None. @type timeout: Integer @param timeout: Timeout to wait for packet reception in usec @rtype: List @return: Returns None is timeout expires and no packet received. When a packet is received, a dictionary is returned with the keys bytes (string of packet bytes), validcrc (boolean if a vaid CRC), rssi (unscaled RSSI), and location (may be set to None). For backwards compatibility, keys for 0,1,2 are provided such that it can be treated as if a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] - ''' + """ if self.__stream_open == False: self.sniffer_on() #start sniffing @@ -276,25 +274,27 @@ def pnext(self, timeout=100): def jammer_on(self, channel=None, page=0): - ''' + """ Not yet implemented. @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @rtype: None - ''' + """ raise Exception('Not yet implemented') def set_sync(self, sync=0xA7): - '''Set the register controlling the 802.15.4 PHY sync byte.''' + """ + Set the register controlling the 802.15.4 PHY sync byte. + """ raise Exception('Not yet implemented') def jammer_off(self, channel=None, page=0): - ''' + """ Not yet implemented. @return: None @rtype: None - ''' + """ raise Exception('Not yet implemented') diff --git a/killerbee/kbutils.py b/killerbee/kbutils.py index b9e15859..8f20a23c 100644 --- a/killerbee/kbutils.py +++ b/killerbee/kbutils.py @@ -1,3 +1,6 @@ +from __future__ import print_function +import sys + # Import USB support depending on version of pyUSB try: import usb.core @@ -11,7 +14,8 @@ USBVER=0 import serial -import os, glob +import os +import glob import time import random import inspect @@ -39,10 +43,11 @@ # Global variables gps_devstring = None + class KBCapabilities: - ''' + """ Class to store and report on the capabilities of a specific KillerBee device. - ''' + """ NONE = 0x00 #: Capabilities Flag: No Capabilities SNIFF = 0x01 #: Capabilities Flag: Can Sniff SETCHAN = 0x02 #: Capabilities Flag: Can Set the Channel @@ -58,6 +63,7 @@ class KBCapabilities: FREQ_868 = 0x0c #: Capabilities Flag: Can perform 868-876 MHz sniffing (ch 0-8) FREQ_870 = 0x0d #: Capabilities Flag: Can perform 870-876 MHz sniffing (ch 0-26) FREQ_915 = 0x0e #: Capabilities Flag: Can perform 915-917 MHz sniffing (ch 0-26) + def __init__(self): self._capabilities = { self.NONE : False, @@ -75,19 +81,24 @@ def __init__(self): self.FREQ_870: False, self.FREQ_915: False, self.BOOT: False } + def check(self, capab): if capab in self._capabilities: return self._capabilities[capab] else: return False + def getlist(self): return self._capabilities + def setcapab(self, capab, value): self._capabilities[capab] = value + def require(self, capab): if self.check(capab) != True: raise Exception('Selected hardware does not support required capability (%d).' % capab) - def frequency(self, channel= None, page= None): + + def frequency(self, channel=None, page=None): ''' Return actual frequency of channel/page in KHz ''' @@ -495,13 +506,13 @@ def isfreakduino(serialdev): return (version is not None) def search_usb(device): - ''' + """ Takes either None, specifying that any USB device in the global vendor and product lists are acceptable, or takes a string that identifies a device in the format :, and returns the pyUSB objects for bus and device that correspond to the identifier string. - ''' + """ if device == None: busNum = None devNum = None @@ -515,12 +526,13 @@ def search_usb(device): dev = search_usb_bus_v0x(bus, busNum, devNum) if dev != None: return (bus, dev) - return None #Note, can't expect a tuple returned + return None # NOTE: can't expect a tuple returned elif USBVER == 1: return usb.core.find(custom_match=findFromListAndBusDevId(busNum, devNum, usbVendorList, usbProductList)) #backend=backend, else: raise Exception("USB version expected to be 0.x or 1.x.") + def search_usb_bus_v0x(bus, busNum, devNum): '''Helper function for USB enumeration in pyUSB 0.x enviroments.''' devices = bus.devices @@ -534,6 +546,7 @@ def search_usb_bus_v0x(bus, busNum, devNum): return dev return None + def hexdump(src, length=16): ''' Creates a tcpdump-style hex dump string output. @@ -552,6 +565,7 @@ def hexdump(src, length=16): result.append("%04x: %-*s %s\n" % (i, length*3, hex, printable)) return ''.join(result) + def randbytes(size): ''' Returns a random string of size bytes. Not cryptographically safe. @@ -561,6 +575,7 @@ def randbytes(size): ''' return ''.join(chr(random.randrange(0,256)) for i in xrange(size)) + def randmac(length=8): ''' Returns a random MAC address using a list valid OUI's from ZigBee device @@ -590,6 +605,7 @@ def randmac(length=8): # Reverse the address for use in a packet return ''.join([prefix, suffix])[::-1] + def makeFCS(data): ''' Do a CRC-CCITT Kermit 16bit on the data given @@ -609,10 +625,12 @@ def makeFCS(data): crc = (crc // 16) ^ (q * 4225) return pack('>sys.stderr, """ + print(""" Library requirements not met. Install the following libraries, then re-run the setup script. - """, err +{} + """.format(err), file=sys.stderr) sys.exit(1) if warn != "": - print >>sys.stderr, """ + print(""" Library recommendations not met. For full support, install the following libraries, then re-run the setup script. - """, warn -# TODO: Offer the user to type y/n to continue or cancel at this point +{} + """.format(warn), file=sys.stderr) + zigbee_crypt = Extension('zigbee_crypt', sources = ['zigbee_crypt/zigbee_crypt.c'], @@ -80,7 +83,7 @@ ) setup (name = 'killerbee', - version = '2.7.0', + version = '2.7.1', description = 'ZigBee and IEEE 802.15.4 Attack Framework and Tools', author = 'Joshua Wright, Ryan Speers', author_email = 'jwright@willhackforsushi.com, ryan@riverloopsecurity.com', From 747be20d19bdb1477491c95b4ab8cea1c1defc9f Mon Sep 17 00:00:00 2001 From: rmspeers Date: Wed, 7 Nov 2018 08:05:02 -0500 Subject: [PATCH 03/14] Updated docs per suggestion in #152 --- killerbee/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/killerbee/__init__.py b/killerbee/__init__.py index 5cba58a2..964e1d8c 100644 --- a/killerbee/__init__.py +++ b/killerbee/__init__.py @@ -53,7 +53,9 @@ def __init__(self, device=None, datasource=None, gps=None): Instantiates the KillerBee class. @type device: String - @param device: Device identifier, either USB :, serial device path, or IP address + @param device: Device identifier, which is either USB `:`, + serial device path (e.g., `/dev/ttyUSB0`), or IP address. + The format needed depends on the device's firmware and connectivity to the host system. @type datasource: String @param datasource: A known data-source type that is used by dblog to record how the data was captured. @type gps: String From 37d2942def473b8cd0e3cb31b96adb1253512421 Mon Sep 17 00:00:00 2001 From: Steve Martin Date: Tue, 4 Dec 2018 20:52:13 +0000 Subject: [PATCH 04/14] Saved attempt to decrypt network key. Decrypt with fixed key and nonce. Compute nonce. Bring to production quality --- .gitignore | 2 + killerbee/dot154decode.py | 66 ++++++------- killerbee/zigbeedecode.py | 2 + tools/zbdsniff | 187 +++++++++++++++++++++-------------- tools/zbstumbler | 27 +++-- zigbee_crypt/zigbee_crypt.c | 192 ++++++++++++++++++++++++++++++++---- 6 files changed, 341 insertions(+), 135 deletions(-) diff --git a/.gitignore b/.gitignore index 173760a2..b5fb8351 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.pyc +.vscode/ build/ dist/ +pdf/ .DS_Store *.egg-info/ diff --git a/killerbee/dot154decode.py b/killerbee/dot154decode.py index 2e289446..ce25e7b8 100644 --- a/killerbee/dot154decode.py +++ b/killerbee/dot154decode.py @@ -62,7 +62,7 @@ def decrypt(self, packet, key): ''' Decrypts the specified packet. Returns empty string if the packet is not encrypted, or if decryption MIC validation fails. - + @type packet: String @param packet: Packet contents. @type key: String @@ -96,14 +96,14 @@ def decrypt(self, packet, key): # remaining bytes (representing encrypted packet payload content) C = c[0:-8] U = c[-8:] - + # 2. Form cipherText by padding C to a block size cipherText = C + ("\x00" * (16 - len(C)%16)) - - # 3. Form 1-byte flags field = 01 + + # 3. Form 1-byte flags field = 01 # XXX will vary when L changes flags = "\x01" - + # 4. Define 16-octet A_i consisting of: # Flags || Nonce || 2-byte counter i for i=0,1,2, ... # A[0] is for authenticity check, A[1] is for the first block of data, @@ -111,45 +111,45 @@ def decrypt(self, packet, key): self.__crypt_A_i = [] for i in xrange(0, (1+1+(len(C)/16))): self.__crypt_A_i.append(flags + nonce + struct.pack(">H",i)) - + # 5. Decrypt cipherText producing plainText (observed) self.__crypt_blockcntr = 1 # Start at A[1] to decrypt crypt = AES.new(key, AES.MODE_CTR, counter=self.__crypt_counter) plainText = crypt.decrypt(cipherText)[0:len(C)] - + # 6. Compute S_0 as E(Key, A[0]) crypt = AES.new(key, AES.MODE_CBC, "\x00"*16) S_0 = crypt.encrypt(self.__crypt_A_i[0]) - + # 7. Compute MIC (T) observed as S_0 XOR U T_obs = [] for i in xrange(0,len(S_0[0:8])): T_obs.append((ord(S_0[i]) ^ ord(U[i]))) - + # Convert T_obs back into a string (please, I need Python help) T_obs = ''.join(struct.pack("B",i) for i in T_obs) - + # 8. Compute a over packet contents before ciphertext payload # This is the 802.15.4 header,plus the security level, frame # counter and flags byte (01) hdrlen = self.hdrlen(packet) a = packet[0:hdrlen] + packet[hdrlen:hdrlen+6] - + # 9. Concatenate L(a) of 2-byte length a with a addAuthData = struct.pack(">H",len(a)) + a - + # 10. Pad addAuthData to an even block size addAuthData += ("\x00" * (16 - len(addAuthData)%16)) - + # 11. Form AuthData by concatenating addAuthData and PlaintextData # Pad plainText to an even block size plainTextPadded = plainText + ("\x00" * (16 - len(plainText)%16)) - + authData = addAuthData + plainTextPadded - + # 12. Perform authData transformation into B[0], B[1], ..., B[i] B = "\x59" + nonce + "\x00\x01" + authData - + # 13. Calculate the MIC (T) calculated with CBC-MAC iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" for i in xrange(0, len(B)/16): @@ -157,8 +157,8 @@ def decrypt(self, packet, key): Bn = B[i*16:(i*16)+16] iv = crypt.encrypt(Bn) T_calc = iv[0:8] - - # 14. Compare + + # 14. Compare if T_obs == T_calc: return plainText else: @@ -176,7 +176,7 @@ def pktchop(self, packet): If the packet is a beacon frame, the Beacon Data field will be populated as a list element in the format: - + Superframe Spec | GTS Fields | Pending Addr Counts | Proto ID | Stack Profile/Profile Version | Device Capabilities | Ext PAN ID | TX Offset | Update ID An exception is raised if the packet contents are too short to @@ -212,35 +212,35 @@ def pktchop(self, packet): # Superframe specification beacondata[0] = packet[offset:offset+2] offset+=2 - + # GTS data beacondata[1] = packet[offset] offset+=1 - + # Pending address count beacondata[2] = packet[offset] offset+=1 - + # Protocol ID beacondata[3] = packet[offset] offset+=1 - + # Stack Profile version beacondata[4] = packet[offset] offset+=1 - + # Capability information beacondata[5] = packet[offset] offset+=1 - + # Extended PAN ID beacondata[6] = packet[offset:offset+8] offset+=8 - + # TX Offset beacondata[7] = packet[offset:offset+3] offset+=3 - + # Update ID beacondata[8] = packet[offset] offset+=1 @@ -256,7 +256,7 @@ def pktchop(self, packet): # DPAN pktchop[2] = packet[3:5] offset = 5 - + # Examine the destination addressing mode daddr_mask = (fcf & DOT154_FCF_DADDR_MASK) >> 10 if daddr_mask == DOT154_FCF_ADDR_EXT: @@ -265,12 +265,12 @@ def pktchop(self, packet): elif daddr_mask == DOT154_FCF_ADDR_SHORT: pktchop[3] = packet[offset:offset+2] offset+=2 - + # Examine the Intra-PAN flag if (fcf & DOT154_FCF_INTRA_PAN) == 0: pktchop[4] = packet[offset:offset+2] offset+=2 - + # Examine the source addressing mode saddr_mask = (fcf & DOT154_FCF_SADDR_MASK) >> 14 if daddr_mask == DOT154_FCF_ADDR_EXT: @@ -283,7 +283,7 @@ def pktchop(self, packet): # Append remaining payload if offset < len(packet): pktchop[7] = packet[offset:] - + return pktchop @@ -344,7 +344,7 @@ def nonce(self, packet): # Byte swap fcf = struct.unpack(">sys.stderr, """ @@ -17,7 +42,7 @@ process libpcap or Daintree SNA capture files. jwright@willhackforsushi.com Usage: zbdsniff [capturefiles ...] """ -def getnetworkkey(packet): +def getnetworkkey(packet, key, verbose): """ Look for the presence of the APS Transport Key command, revealing the network key value. @@ -27,98 +52,113 @@ def getnetworkkey(packet): zmac = Dot154PacketParser() znwk = ZigBeeNWKPacketParser() zaps = ZigBeeAPSPacketParser() - + # Process MAC layer details - zmacpayload = zmac.pktchop(packet)[-1] - if zmacpayload == None: + macLayer = zmac.pktchop(packet) + if macLayer[-1] == None: + # No MAC Layer data + if verbose: + print("[-] Skipping - No MAC Layer data") return # Process NWK layer details - znwkpayload = znwk.pktchop(zmacpayload)[-1] - if znwkpayload == None: - return + znwkLayer = znwk.pktchop(macLayer[-1]) + fcf = struct.unpack("> 2 - if apsdeliverymode != 0: + apsfc = ord(zapsLayer[0]) + source = struct.unpack(" 1: - files.append(sys.argv.pop(1)) +if options.directory: + files += glob.glob(options.directory + os.sep + "*.pcap") -while len(files) != 0: - file = files.pop() +filecount = 0 + +for file in files: print "Processing %s"%file - if not os.path.exists(file): + if not os.path.exists(str(file)): print >>sys.stderr, "ERROR: Input file \"%s\" does not exist." % file - continue + sys.exit(1) filecount += 1 @@ -130,20 +170,19 @@ while len(files) != 0: if e.args == ('Unsupported pcap header format or version',): # Input file was not pcap, open it as SNA cap = DainTreeReader(file) - + # Following exception if cap == None: cap = pr - - packetcount = 0 - while 1: + + while True: packet = cap.pnext() if packet[1] == None: # End of capture break # Add additional key/password/interesting-stuff here - getnetworkkey(packet[1]) - + getnetworkkey(packet[1], options.transportKey, options.verbose) + cap.close() -print "Processed %d capture files." % filecount +print "[+] Processed %d capture files." % filecount diff --git a/tools/zbstumbler b/tools/zbstumbler index 54c8a35a..4eba9653 100644 --- a/tools/zbstumbler +++ b/tools/zbstumbler @@ -70,9 +70,6 @@ def response_handler(stumbled, packet, channel): # Check if this is a beacon frame if (fcf & DOT154_FCF_TYPE_MASK) == DOT154_FCF_TYPE_BEACON: - if args.verbose: - print "Received frame is a beacon." - # The 6th element offset in the Dot154PacketParser.pktchop() method # contains the beacon data in its own list. Extract the Ext PAN ID. spanid = pktdecode[4][::-1] @@ -80,16 +77,26 @@ def response_handler(stumbled, packet, channel): beacondata = pktdecode[6] extpanid = beacondata[6][::-1] stackprofilever = beacondata[4] + assocPermit = struct.unpack("<", beacondata[0])[0] & 0x8000 key = ''.join([spanid, source]) value = [spanid, source, extpanid, stackprofilever, channel] if not key in stumbled: if args.verbose: - print("Beacon represents new network.") + if assocPermit: + print("Beacon represents new network - ### Permitting new associations ###.") + else: + print("Beacon represents new network - not accepting new associations.") #print hexdump(packet) #print pktdecode stumbled[key] = value display_details(value) + else: + if args.verbose: + if assocPermit: + print("Received frame is a beacon - ### Permitting new associations ###.") + else: + print("Received frame is a beacon - not accepting new associations.") return value if args.verbose: @@ -129,7 +136,7 @@ if __name__ == '__main__': except Exception as e: print("Issue opening CSV output file: {0}.".format(e)) csvfile.write("panid,source,extpanid,stackprofile,stackversion,channel\n") - + # Beacon frame beacon = "\x03\x08\x00\xff\xff\xff\xff\x07" # Immutable strings - split beacon around sequence number field @@ -144,7 +151,7 @@ if __name__ == '__main__': signal.signal(signal.SIGINT, interrupt) print("zbstumbler: Transmitting and receiving on interface \'{0}\'".format(kb.get_dev_info()[0])) - + # Sequence number of beacon request frame seqnum = 0 if args.channel: @@ -160,7 +167,7 @@ if __name__ == '__main__': if seqnum > 255: seqnum = 0 - + if not args.channel: if args.verbose: print("Setting channel to {0}.".format(channel)) @@ -169,7 +176,7 @@ if __name__ == '__main__': except Exception, e: print("ERROR: Failed to set channel to {0}. ({1})".format(channel, e)) sys.exit(-1) - + if args.verbose: print("Transmitting beacon request.") @@ -194,9 +201,9 @@ if __name__ == '__main__': rxcount += 1 if args.verbose: print("Received frame.")#, time.time()-start - networkdata = response_handler(stumbled, recvpkt[0], channel) + networkdata = response_handler(stumbled, recvpkt[0], channel) - kb.sniffer_off() + kb.sniffer_off() seqnum += 1 if not args.channel: channel += 1 diff --git a/zigbee_crypt/zigbee_crypt.c b/zigbee_crypt/zigbee_crypt.c index 9b46b723..6d1df2fc 100644 --- a/zigbee_crypt/zigbee_crypt.c +++ b/zigbee_crypt/zigbee_crypt.c @@ -2,12 +2,12 @@ * zigbee_crypt.c * Copyright 2011 steiner * zigbee convenience functions - * + * * alot of this code was "borrowed" from wireshark * packet-zbee-security.c & pzcket-zbee-security.h * function: zbee_sec_ccm_decrypt */ - + // Explaination of Python Build Values http://docs.python.org/c-api/arg.html#Py_BuildValue #include @@ -35,7 +35,7 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { char cipher_out[ZBEE_SEC_CONST_BLOCKSIZE]; /* Cipher Instance. */ gcry_cipher_hd_t cipher_hd; - + if (!PyArg_ParseTuple(args, "s#s#is#s#", &pZkey, &sizeZkey, &pNonce, &sizeNonce, @@ -48,12 +48,12 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { PyErr_SetString(PyExc_ValueError, "incorrect key size (must be 16)"); return NULL; } - + if (sizeNonce != ZBEE_SEC_CONST_NONCE_LEN) { PyErr_SetString(PyExc_ValueError, "incorrect nonce size (must be 13)"); return NULL; } - + if ((sizeMIC != 0) && (sizeMIC != 4) && (sizeMIC != 8) && (sizeMIC != 16)) { PyErr_SetString(PyExc_ValueError, "incorrect mic size (must be 0, 4, 8, or 16 bytes)"); return NULL; @@ -63,7 +63,7 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { memset(pEncMIC, 0, ZBEE_SEC_CONST_MICSIZE); pEncrypted = malloc(sizeUnencryptedData); memset(pEncrypted, 0, sizeUnencryptedData); - + /* Open the cipher in ECB mode. */ if (gcry_cipher_open(&cipher_hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_ECB, 0)) { PyErr_SetString(PyExc_Exception, "gcrypt open AES-128 ECB cipher failed"); @@ -87,7 +87,7 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { gcry_cipher_close(cipher_hd); return NULL; } - + j = 0; if (sizeZigbeeData > 0) { /* Process L(a) into the cipher block. */ @@ -139,10 +139,10 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { gcry_cipher_close(cipher_hd); return NULL; } - + gcry_cipher_close(cipher_hd); memcpy(pMIC, cipher_out, sizeMIC); - + /* Create the CCM* counter block A0 */ memset(cipher_in, 0, ZBEE_SEC_CONST_BLOCKSIZE); cipher_in[0] = ZBEE_SEC_CCM_FLAG_L; @@ -187,7 +187,7 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { } /* Done with the CTR Cipher. */ gcry_cipher_close(cipher_hd); - + return Py_BuildValue("(s#s#)", pEncrypted, sizeUnencryptedData, pEncMIC, sizeMIC); }; @@ -220,7 +220,7 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { PyErr_SetString(PyExc_ValueError, "incorrect key size (must be 16)"); return NULL; } - + if (sizeNonce != ZBEE_SEC_CONST_NONCE_LEN) { PyErr_SetString(PyExc_ValueError, "incorrect nonce size (must be 13)"); return NULL; @@ -233,12 +233,12 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { memset(pMIC, 0, ZBEE_SEC_CONST_MICSIZE); memcpy(pMIC, pOldMIC, sizeMIC); - + memset(pUnencMIC, 0, ZBEE_SEC_CONST_MICSIZE); - + pUnencrypted = malloc(sizeEncryptedData); memset(pUnencrypted, 0, sizeEncryptedData); - + /* Create the CCM* counter block A0 */ memset(cipher_in, 0, ZBEE_SEC_CONST_BLOCKSIZE); cipher_in[0] = ZBEE_SEC_CCM_FLAG_L; @@ -287,7 +287,7 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { } /* Done with the CTR Cipher. */ gcry_cipher_close(cipher_hd); - + int i, j; /* Re-open the cipher in ECB mode. */ if (gcry_cipher_open(&cipher_hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_ECB, 0)) { @@ -382,21 +382,177 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { gcry_cipher_close(cipher_hd); return NULL; } - + gcry_cipher_close(cipher_hd); - + // now use j to indicate whether the MICs match j = 0; if (memcmp(cipher_out, pUnencMIC, sizeMIC) == 0) { j = 1; } - + return Py_BuildValue("(s#i)", pUnencrypted, sizeEncryptedData, j); }; +/*FUNCTION:------------------------------------------------------ + * NAME + * zbee_sec_hash + * DESCRIPTION + * ZigBee Cryptographic Hash Function, described in ZigBee + * specification sections B.1.3 and B.6. + * + * This is a Matyas-Meyer-Oseas hash function using the AES-128 + * cipher. We use the ECB mode of libgcrypt to get a raw block + * cipher. + * + * Input may be any length, and the output must be exactly 1-block in length. + * + * Implements the function: + * Hash(text) = Hash[t]; + * Hash[0] = 0^(blocksize). + * Hash[i] = E(Hash[i-1], M[i]) XOR M[j]; + * M[i] = i'th block of text, with some padding and flags concatenated. + * PARAMETERS + * char * input - Hash Input (any length). + * int input_len - Hash Input Length. + * char * output - Hash Output (exactly one block in length). + * RETURNS + * void + *--------------------------------------------------------------- + */ +static void +zbee_sec_hash(char *input, int input_len, char *output) +{ + char cipher_in[ZBEE_SEC_CONST_BLOCKSIZE]; + int i, j; + /* Cipher Instance. */ + gcry_cipher_hd_t cipher_hd; + + /* Clear the first hash block (Hash0). */ + memset(output, 0, ZBEE_SEC_CONST_BLOCKSIZE); + /* Create the cipher instance in ECB mode. */ + if (gcry_cipher_open(&cipher_hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_ECB, 0)) { + return; /* Failed. */ + } + /* Create the subsequent hash blocks using the formula: Hash[i] = E(Hash[i-1], M[i]) XOR M[i] + * + * because we can't guarantee that M will be exactly a multiple of the + * block size, we will need to copy it into local buffers and pad it. + * + * Note that we check for the next cipher block at the end of the loop + * rather than the start. This is so that if the input happens to end + * on a block boundary, the next cipher block will be generated for the + * start of the padding to be placed into. + */ + i = 0; + j = 0; + while (i= ZBEE_SEC_CONST_BLOCKSIZE) { + /* We have reached the end of this block. Process it with the + * cipher, note that the Key input to the cipher is actually + * the previous hash block, which we are keeping in output. + */ + (void)gcry_cipher_setkey(cipher_hd, output, ZBEE_SEC_CONST_BLOCKSIZE); + (void)gcry_cipher_encrypt(cipher_hd, output, ZBEE_SEC_CONST_BLOCKSIZE, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE); + /* Now we have to XOR the input into the hash block. */ + for (j=0;j= 7-bits, and we can just + * append the byte 0x80. + */ + cipher_in[j++] = 0x80; + /* Pad with '0' until the the current block is exactly 'n' bits from the + * end. + */ + while (j!=(ZBEE_SEC_CONST_BLOCKSIZE-2)) { + if (j >= ZBEE_SEC_CONST_BLOCKSIZE) { + /* We have reached the end of this block. Process it with the + * cipher, note that the Key input to the cipher is actually + * the previous hash block, which we are keeping in output. + */ + (void)gcry_cipher_setkey(cipher_hd, output, ZBEE_SEC_CONST_BLOCKSIZE); + (void)gcry_cipher_encrypt(cipher_hd, output, ZBEE_SEC_CONST_BLOCKSIZE, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE); + /* Now we have to XOR the input into the hash block. */ + for (j=0;j> 8) & 0xff; + cipher_in[j] = ((input_len * 8) >> 0) & 0xff; + /* Process the last cipher block. */ + (void)gcry_cipher_setkey(cipher_hd, output, ZBEE_SEC_CONST_BLOCKSIZE); + (void)gcry_cipher_encrypt(cipher_hd, output, ZBEE_SEC_CONST_BLOCKSIZE, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE); + /* XOR the last input block back into the cipher output to get the hash. */ + for (j=0;j Date: Sat, 22 Dec 2018 22:23:33 +0000 Subject: [PATCH 05/14] Code review comments. --- tools/zbdsniff | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/zbdsniff b/tools/zbdsniff index 6852bf8b..865681a1 100644 --- a/tools/zbdsniff +++ b/tools/zbdsniff @@ -150,7 +150,8 @@ if options.filename: files.append(options.filename) if options.directory: - files += glob.glob(options.directory + os.sep + "*.pcap") + files += glob.glob(os.path.join(options.directory, "*.pcap")) + filecount = 0 From 61fbf5f3bf104132c665c1aeec34598e1c7d2ed3 Mon Sep 17 00:00:00 2001 From: Steve Martin Date: Wed, 6 Feb 2019 08:20:57 +0000 Subject: [PATCH 06/14] Reimplement zbsniff using Scapy. --- tools/zbdsniff | 235 ++++++++++++++++--------------------------------- 1 file changed, 78 insertions(+), 157 deletions(-) diff --git a/tools/zbdsniff b/tools/zbdsniff index 865681a1..cf906bd5 100644 --- a/tools/zbdsniff +++ b/tools/zbdsniff @@ -1,18 +1,10 @@ #!/usr/bin/env python - -''' -Decode plaintext key ZigBee delivery from a capture file. Will -process libpcap or Daintree SNA capture files. -''' - -import sys -import os -import glob -from killerbee import * +from killerbee.scapy_extensions import * +from killerbee.kbutils import * from zigbee_crypt import * from optparse import OptionParser -sourceMap = dict() +ZBEE_SEC_ENC_MIC_32 = 0x5 #: ZigBee Seecurity Level id 5; Encrypted, 4 byte MIC APS_CMD = [ "Unknown", @@ -33,157 +25,86 @@ KEY_TYPE = [ "High Security Network" ] +def sniffNetworkKey(pkts, key, verbose): + addrMap = dict() + keyHash = sec_key_hash(key, '\0') + + for p in pkts: + if (p.haslayer("ZigbeeNWK")): + nwk = p.getlayer("ZigbeeNWK") + if nwk.source is not None and nwk.ext_src is not None: + if not nwk.source in addrMap and verbose: + print("[+] Extended Source: " + ":".join("{:02x}".format(ord(ch)) for ch in raw(nwk)[8:16]) + " mapped to " + hex(nwk.source)) + addrMap[nwk.source] = raw(nwk)[8:16] + if (p.haslayer("ZigbeeSecurityHeader") and p.haslayer("ZigbeeAppDataPayload")): + sec = p.getlayer("ZigbeeSecurityHeader") + if sec.key_type < 6: + if nwk.source in addrMap: + data = raw(p.getlayer("ZigbeeAppDataPayload")) + scf = (ord(data[2]) & ~ ZBEE_SEC_ENC_MIC_32) | ZBEE_SEC_ENC_MIC_32 + a = data[0:2] + chr(scf) + data[3:7] + c = data[7:-4] + mic = data[-4:] + nonce = addrMap[nwk.source] + data[3:7] + chr(scf) + decrypted, success = decrypt_ccm(keyHash, nonce, mic, c, a) + if success: + print("[+] Decrypted:") + if ord(decrypted[0]) < len(APS_CMD): + print(" APS Command: " + APS_CMD[ord(decrypted[0])]) + if ord(decrypted[1]) < len(KEY_TYPE): + print(" Key Type: " + KEY_TYPE[ord(decrypted[1])]) + print(" Value: " + ":".join("{:02x}".format(ord(ch)) for ch in decrypted[2:18])) + else: + print("[-] Decrypt failed - Wrong Key ???") + else: + print("[-] There was no ext_src mapping for: ", str(nwk.source)) + else: + print("[-] Unexpected key_type", str(sec.key_type)) + + +if __name__ == '__main__': + # Define the command line options. + parser = OptionParser(description="zbdsniff: Decode plaintext Zigbee Nwtwork key from a " + + "capture file. Will process libpcap or Daintree SNA capture files. Original concept: " + + "jwright@willhackforsushi.com, re-implemented using Scapy by Steve Martin.") + parser.add_option("-f", "--file", dest="filename", + help="PCap file to process", metavar="FILE") + parser.add_option("-d", "--dir", dest="directory", + help="Directory of PCap files to process", metavar="DIR") + parser.add_option("-k", "--transport-key", dest="transportKey", + help="Transport Key for decryption") + parser.add_option("-v", "--verbose", + action="store_true", dest="verbose", default=False, + help="Print detailed status messages to stdout") + + (options, args) = parser.parse_args() + + if (not options.filename and not options.directory): + print("A packet capture file or directory must be specified") + sys.exit(1) -def usage(): - print >>sys.stderr, """ -zbdsniff: Decode plaintext key ZigBee delivery from a capture file. Will -process libpcap or Daintree SNA capture files. jwright@willhackforsushi.com - -Usage: zbdsniff [capturefiles ...] - """ - -def getnetworkkey(packet, key, verbose): - """ - Look for the presence of the APS Transport Key command, revealing the - network key value. - """ - - try: - zmac = Dot154PacketParser() - znwk = ZigBeeNWKPacketParser() - zaps = ZigBeeAPSPacketParser() - - # Process MAC layer details - macLayer = zmac.pktchop(packet) - if macLayer[-1] == None: - # No MAC Layer data - if verbose: - print("[-] Skipping - No MAC Layer data") - return - - # Process NWK layer details - znwkLayer = znwk.pktchop(macLayer[-1]) - fcf = struct.unpack(">sys.stderr, "ERROR: Input file \"%s\" does not exist." % file + if (not options.transportKey): + print("A transport key value must be specified") sys.exit(1) - filecount += 1 + files = [] + if options.filename: + files.append(options.filename) + + if options.directory: + files += glob.glob(os.path.join(options.directory, "*.pcap")) - # Check if the input file is libpcap; if not, assume SNA. - cap = None - try: - pr = PcapReader(file) - except Exception, e: - if e.args == ('Unsupported pcap header format or version',): - # Input file was not pcap, open it as SNA - cap = DainTreeReader(file) + filecount = 0 - # Following exception - if cap == None: - cap = pr + for file in files: + print "Processing %s"%file + if not os.path.exists(str(file)): + print >>sys.stderr, "ERROR: Input file \"%s\" does not exist." % file + sys.exit(1) - while True: - packet = cap.pnext() - if packet[1] == None: - # End of capture - break - # Add additional key/password/interesting-stuff here - getnetworkkey(packet[1], options.transportKey, options.verbose) + filecount += 1 + pkts = kbrdpcap(file) + sniffNetworkKey(pkts, options.transportKey, options.verbose) - cap.close() -print "[+] Processed %d capture files." % filecount + print "[+] Processed %d capture files." % filecount From cfda5159f5526ad30a003003daac3ae399f6bd1a Mon Sep 17 00:00:00 2001 From: SrEdeu Date: Mon, 18 Mar 2019 14:02:26 +0100 Subject: [PATCH 07/14] fixing datetime method call --- tools/zbfind | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/zbfind b/tools/zbfind index 884c5ed5..065cd9b4 100644 --- a/tools/zbfind +++ b/tools/zbfind @@ -1020,7 +1020,7 @@ class ZBPoller(Thread): idev = dev details = idev[1] # Update last seen detail - details[0][1] = "Last seen: " + str(datetime.datetime.now()) + details[0][1] = "Last seen: " + str(datetime.now()) if (fc_secenabled != 0): details[0][2] = "Security: Enabled" details[0][3] = "Last Seq Num: " + seq @@ -1036,7 +1036,7 @@ class ZBPoller(Thread): if match == False: # Add this entry to the list - nowstr = str(datetime.datetime.now()) + nowstr = str(datetime.now()) details = [["First seen: " + nowstr]] details[0].append("Last Seen: " + nowstr) if (fc_secenabled != 0): From b1c3ed41f5200a59c3f3f4816cc48ee442ab8594 Mon Sep 17 00:00:00 2001 From: rmspeers Date: Sun, 24 Mar 2019 11:32:05 -0400 Subject: [PATCH 08/14] Reverting changes in PR request https://github.com/riverloopsec/killerbee/pull/165 from datetime, which do not test correct on Python 2.7, updated dates in README --- README.md | 9 +++++---- tools/zbfind | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a8aa660c..4e7250e3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ All Rights Reserved. The main toolkit was/is authored by: + 2009, Joshua Wright -+ 2010-2017, Ryan Speers ++ 2010-2019, Ryan Speers + 2010-2011, Ricky Melgares We appreciate the many contributers to the framework, including the following who have contributed capabilities: @@ -285,8 +285,9 @@ QUESTIONS/COMMENTS/CONCERNS ============== Please use the ticketing system at https://github.com/riverloopsec/killerbee/issues. -The original version was written by: jwright@willhackforsushi.com -The current version, fixes, etc are handled by: ryan@riverloopsecurity.com -Additional Tools/Fixes by: bryanhalf@gmail.com +The original version was written by: jwright@willhackforsushi.com. +The current version, fixes, etc are handled by: ryan@riverloopsecurity.com. +(See the list above for all contributors/credits.) For contributors/developers, see DEVELOPMENT.md for details and guidance. + diff --git a/tools/zbfind b/tools/zbfind index 065cd9b4..884c5ed5 100644 --- a/tools/zbfind +++ b/tools/zbfind @@ -1020,7 +1020,7 @@ class ZBPoller(Thread): idev = dev details = idev[1] # Update last seen detail - details[0][1] = "Last seen: " + str(datetime.now()) + details[0][1] = "Last seen: " + str(datetime.datetime.now()) if (fc_secenabled != 0): details[0][2] = "Security: Enabled" details[0][3] = "Last Seq Num: " + seq @@ -1036,7 +1036,7 @@ class ZBPoller(Thread): if match == False: # Add this entry to the list - nowstr = str(datetime.now()) + nowstr = str(datetime.datetime.now()) details = [["First seen: " + nowstr]] details[0].append("Last Seen: " + nowstr) if (fc_secenabled != 0): From aa0209d29f740b6c817b0f91fa438077f95e95a5 Mon Sep 17 00:00:00 2001 From: Steve Martin Date: Sun, 24 Mar 2019 17:24:27 +0000 Subject: [PATCH 09/14] Fix memory leaks on error exits, ensure allocation is successful before referencing. --- .gitignore | 2 ++ zigbee_crypt/zigbee_crypt.c | 39 ++++++++++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 173760a2..ee445b89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.pyc build/ dist/ +pdf/ +.vscode/ .DS_Store *.egg-info/ diff --git a/zigbee_crypt/zigbee_crypt.c b/zigbee_crypt/zigbee_crypt.c index 7a24e938..49808aa2 100644 --- a/zigbee_crypt/zigbee_crypt.c +++ b/zigbee_crypt/zigbee_crypt.c @@ -62,18 +62,26 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { memset(pMIC, 0, ZBEE_SEC_CONST_MICSIZE); // set both mics to 0 memset(pEncMIC, 0, ZBEE_SEC_CONST_MICSIZE); + pEncrypted = malloc(sizeUnencryptedData); + if (pEncrypted == NULL) { + PyErr_SetString(PyExc_MemoryError, "could not allocate memory"); + return NULL; + } + memset(pEncrypted, 0, sizeUnencryptedData); /* Open the cipher in ECB mode. */ if (gcry_cipher_open(&cipher_hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_ECB, 0)) { PyErr_SetString(PyExc_Exception, "gcrypt open AES-128 ECB cipher failed"); + free(pEncrypted); return NULL; } /* Load the key. */ if (gcry_cipher_setkey(cipher_hd, pZkey, ZBEE_SEC_CONST_KEYSIZE)) { PyErr_SetString(PyExc_Exception, "setting the key failed"); gcry_cipher_close(cipher_hd); + free(pEncrypted); return NULL; } /* Generate the first cipher block B0. */ @@ -86,6 +94,7 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { if (gcry_cipher_encrypt(cipher_hd, cipher_out, ZBEE_SEC_CONST_BLOCKSIZE, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE)) { PyErr_SetString(PyExc_Exception, "mic creation failed"); gcry_cipher_close(cipher_hd); + free(pEncrypted); return NULL; } @@ -103,6 +112,7 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { if (gcry_cipher_encrypt(cipher_hd, cipher_out, ZBEE_SEC_CONST_BLOCKSIZE, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE)) { PyErr_SetString(PyExc_Exception, "mic creation failed"); gcry_cipher_close(cipher_hd); + free(pEncrypted); return NULL; } /* Reset j to point back to the start of the new cipher block. */ @@ -122,6 +132,7 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { if (gcry_cipher_encrypt(cipher_hd, cipher_out, ZBEE_SEC_CONST_BLOCKSIZE, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE)) { PyErr_SetString(PyExc_Exception, "mic creation failed"); gcry_cipher_close(cipher_hd); + free(pEncrypted); return NULL; } /* Reset j to point back to the start of the new cipher block. */ @@ -138,6 +149,7 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { if (gcry_cipher_encrypt(cipher_hd, cipher_out, ZBEE_SEC_CONST_BLOCKSIZE, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE)) { PyErr_SetString(PyExc_Exception, "mic creation failed"); gcry_cipher_close(cipher_hd); + free(pEncrypted); return NULL; } @@ -151,6 +163,7 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { if (pEncrypted == NULL) { PyErr_SetString(PyExc_MemoryError, "could not allocate memory"); + free(pEncrypted); return NULL; } /* @@ -160,30 +173,35 @@ static PyObject *zigbee_crypt_encrypt_ccm(PyObject *self, PyObject *args) { */ if (gcry_cipher_open(&cipher_hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_CTR, 0)) { PyErr_SetString(PyExc_Exception, "gcrypt open AES-128 CTR cipher failed"); + free(pEncrypted); return NULL; } /* Re-load the Key. */ if (gcry_cipher_setkey(cipher_hd, pZkey, ZBEE_SEC_CONST_KEYSIZE)) { PyErr_SetString(PyExc_Exception, "setting the key failed"); gcry_cipher_close(cipher_hd); + free(pEncrypted); return NULL; } /* Set the counter. */ if (gcry_cipher_setctr(cipher_hd, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE)) { PyErr_SetString(PyExc_Exception, "setting the counter failed"); gcry_cipher_close(cipher_hd); + free(pEncrypted); return NULL; } /* Encrypt/Decrypt the payload. */ if (gcry_cipher_encrypt(cipher_hd, pEncMIC, ZBEE_SEC_CONST_MICSIZE, pMIC, ZBEE_SEC_CONST_MICSIZE)) { PyErr_SetString(PyExc_Exception, "encryption of the mic failed"); gcry_cipher_close(cipher_hd); + free(pEncrypted); return NULL; } /* Encrypt/Decrypt the payload. */ if (gcry_cipher_encrypt(cipher_hd, pEncrypted, sizeUnencryptedData, pUnencryptedData, sizeUnencryptedData)) { PyErr_SetString(PyExc_Exception, "encryption of the payload failed"); gcry_cipher_close(cipher_hd); + free(pEncrypted); return NULL; } /* Done with the CTR Cipher. */ @@ -241,6 +259,11 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { memset(pUnencMIC, 0, ZBEE_SEC_CONST_MICSIZE); pUnencrypted = malloc(sizeEncryptedData); + if (pUnencrypted == NULL) { + PyErr_SetString(PyExc_MemoryError, "could not allocate memory"); + return NULL; + } + memset(pUnencrypted, 0, sizeEncryptedData); /* Create the CCM* counter block A0 */ @@ -248,11 +271,6 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { cipher_in[0] = ZBEE_SEC_CCM_FLAG_L; memcpy(cipher_in + 1, pNonce, ZBEE_SEC_CONST_NONCE_LEN); - if (pUnencrypted == NULL) { - PyErr_SetString(PyExc_MemoryError, "could not allocate memory"); - return NULL; - } - /* Cipher Instance. */ gcry_cipher_hd_t cipher_hd; @@ -263,30 +281,35 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { */ if (gcry_cipher_open(&cipher_hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_CTR, 0)) { PyErr_SetString(PyExc_Exception, "gcrypt open AES-128 CTR cipher failed"); + free(pUnencrypted); return NULL; } /* Set the Key. */ if (gcry_cipher_setkey(cipher_hd, pZkey, ZBEE_SEC_CONST_KEYSIZE)) { PyErr_SetString(PyExc_Exception, "setting the key failed"); gcry_cipher_close(cipher_hd); + free(pUnencrypted); return NULL; } /* Set the counter. */ if (gcry_cipher_setctr(cipher_hd, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE)) { PyErr_SetString(PyExc_Exception, "setting the counter failed"); gcry_cipher_close(cipher_hd); + free(pUnencrypted); return NULL; } /* Encrypt/Decrypt the payload. */ if (gcry_cipher_encrypt(cipher_hd, pUnencMIC, ZBEE_SEC_CONST_MICSIZE, pMIC, ZBEE_SEC_CONST_MICSIZE)) { PyErr_SetString(PyExc_Exception, "decryption of the mic failed"); gcry_cipher_close(cipher_hd); + free(pUnencrypted); return NULL; } /* Encrypt/Decrypt the payload. */ if (gcry_cipher_encrypt(cipher_hd, pUnencrypted, sizeEncryptedData, pEncryptedData, sizeEncryptedData)) { PyErr_SetString(PyExc_Exception, "decryption of the payload failed"); gcry_cipher_close(cipher_hd); + free(pUnencrypted); return NULL; } /* Done with the CTR Cipher. */ @@ -296,12 +319,14 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { /* Re-open the cipher in ECB mode. */ if (gcry_cipher_open(&cipher_hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_ECB, 0)) { PyErr_SetString(PyExc_Exception, "gcrypt open AES-128 ECB cipher failed"); + free(pUnencrypted); return NULL; } /* Re-load the key. */ if (gcry_cipher_setkey(cipher_hd, pZkey, ZBEE_SEC_CONST_KEYSIZE)) { PyErr_SetString(PyExc_Exception, "setting the key failed"); gcry_cipher_close(cipher_hd); + free(pUnencrypted); return NULL; } /* Generate the first cipher block B0. */ @@ -314,6 +339,7 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { if (gcry_cipher_encrypt(cipher_hd, cipher_out, ZBEE_SEC_CONST_BLOCKSIZE, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE)) { PyErr_SetString(PyExc_Exception, "mic verification failed"); gcry_cipher_close(cipher_hd); + free(pUnencrypted); return NULL; } /* @@ -349,6 +375,7 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { if (gcry_cipher_encrypt(cipher_hd, cipher_out, ZBEE_SEC_CONST_BLOCKSIZE, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE)) { PyErr_SetString(PyExc_Exception, "mic verification failed"); gcry_cipher_close(cipher_hd); + free(pUnencrypted); return NULL; } /* Reset j to point back to the start of the new cipher block. */ @@ -368,6 +395,7 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { if (gcry_cipher_encrypt(cipher_hd, cipher_out, ZBEE_SEC_CONST_BLOCKSIZE, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE)) { PyErr_SetString(PyExc_Exception, "mic verification failed"); gcry_cipher_close(cipher_hd); + free(pUnencrypted); return NULL; } /* Reset j to point back to the start of the new cipher block. */ @@ -384,6 +412,7 @@ static PyObject *zigbee_crypt_decrypt_ccm(PyObject *self, PyObject *args) { if (gcry_cipher_encrypt(cipher_hd, cipher_out, ZBEE_SEC_CONST_BLOCKSIZE, cipher_in, ZBEE_SEC_CONST_BLOCKSIZE)) { PyErr_SetString(PyExc_Exception, "mic verification failed"); gcry_cipher_close(cipher_hd); + free(pUnencrypted); return NULL; } From 08b32f8ac0083dc50e240e8b7e1eb4f957c791fd Mon Sep 17 00:00:00 2001 From: rmspeers Date: Sun, 24 Mar 2019 13:25:01 -0400 Subject: [PATCH 10/14] Updated zbdsniff to handle removal of Daintree, made py3 forwards compatible --- tools/zbdsniff | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/tools/zbdsniff b/tools/zbdsniff index cf906bd5..c70a7dc1 100644 --- a/tools/zbdsniff +++ b/tools/zbdsniff @@ -1,8 +1,11 @@ #!/usr/bin/env python + +from __future__ import print_function +from optparse import OptionParser + from killerbee.scapy_extensions import * from killerbee.kbutils import * from zigbee_crypt import * -from optparse import OptionParser ZBEE_SEC_ENC_MIC_32 = 0x5 #: ZigBee Seecurity Level id 5; Encrypted, 4 byte MIC @@ -25,6 +28,7 @@ KEY_TYPE = [ "High Security Network" ] + def sniffNetworkKey(pkts, key, verbose): addrMap = dict() keyHash = sec_key_hash(key, '\0') @@ -34,7 +38,7 @@ def sniffNetworkKey(pkts, key, verbose): nwk = p.getlayer("ZigbeeNWK") if nwk.source is not None and nwk.ext_src is not None: if not nwk.source in addrMap and verbose: - print("[+] Extended Source: " + ":".join("{:02x}".format(ord(ch)) for ch in raw(nwk)[8:16]) + " mapped to " + hex(nwk.source)) + print(("[+] Extended Source: " + ":".join("{:02x}".format(ord(ch)) for ch in raw(nwk)[8:16]) + " mapped to " + hex(nwk.source))) addrMap[nwk.source] = raw(nwk)[8:16] if (p.haslayer("ZigbeeSecurityHeader") and p.haslayer("ZigbeeAppDataPayload")): sec = p.getlayer("ZigbeeSecurityHeader") @@ -50,32 +54,29 @@ def sniffNetworkKey(pkts, key, verbose): if success: print("[+] Decrypted:") if ord(decrypted[0]) < len(APS_CMD): - print(" APS Command: " + APS_CMD[ord(decrypted[0])]) + print(" APS Command: {}".format(APS_CMD[ord(decrypted[0])])) if ord(decrypted[1]) < len(KEY_TYPE): - print(" Key Type: " + KEY_TYPE[ord(decrypted[1])]) + print(" Key Type: {}".format(KEY_TYPE[ord(decrypted[1])])) print(" Value: " + ":".join("{:02x}".format(ord(ch)) for ch in decrypted[2:18])) else: print("[-] Decrypt failed - Wrong Key ???") else: - print("[-] There was no ext_src mapping for: ", str(nwk.source)) + print("[-] There was no ext_src mapping for: {}".format(nwk.source)) else: - print("[-] Unexpected key_type", str(sec.key_type)) + print("[-] Unexpected key_type {}".format(sec.key_type)) if __name__ == '__main__': # Define the command line options. parser = OptionParser(description="zbdsniff: Decode plaintext Zigbee Nwtwork key from a " + - "capture file. Will process libpcap or Daintree SNA capture files. Original concept: " + + "capture file. Will process libpcap capture files. Original concept: " + "jwright@willhackforsushi.com, re-implemented using Scapy by Steve Martin.") - parser.add_option("-f", "--file", dest="filename", - help="PCap file to process", metavar="FILE") + parser.add_option("-f", "--file", dest="filename", help="PCap file to process", metavar="FILE") parser.add_option("-d", "--dir", dest="directory", - help="Directory of PCap files to process", metavar="DIR") - parser.add_option("-k", "--transport-key", dest="transportKey", - help="Transport Key for decryption") - parser.add_option("-v", "--verbose", - action="store_true", dest="verbose", default=False, - help="Print detailed status messages to stdout") + help="Directory of PCap files to process", metavar="DIR") + parser.add_option("-k", "--transport-key", dest="transportKey", help="Transport Key for decryption") + parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, + help="Print detailed status messages to stdout") (options, args) = parser.parse_args() @@ -90,21 +91,23 @@ if __name__ == '__main__': files = [] if options.filename: files.append(options.filename) - if options.directory: files += glob.glob(os.path.join(options.directory, "*.pcap")) filecount = 0 - - for file in files: - print "Processing %s"%file - if not os.path.exists(str(file)): - print >>sys.stderr, "ERROR: Input file \"%s\" does not exist." % file + for fname in files: + print("Processing {}".format(fname)) + if not os.path.exists(str(fname)): + print("ERROR: Input file \"{}\" does not exist.".format(fname), file=sys.stderr) sys.exit(1) filecount += 1 - pkts = kbrdpcap(file) + try: + pkts = kbrdpcap(fname) + except Exception as e: + print("ERROR: Input file \"{}\" is not able to be loaded. Is it a PCAP file? Daintree support was removed in KillerBee 2.7.1".format(fname), file=sys.stderr) + continue sniffNetworkKey(pkts, options.transportKey, options.verbose) + print("[+] Processed {} capture files.".format(filecount)) - print "[+] Processed %d capture files." % filecount From 60b74164a3f90f420e5984d3b123ef63188c3915 Mon Sep 17 00:00:00 2001 From: rmspeers Date: Tue, 28 May 2019 07:43:39 -0500 Subject: [PATCH 11/14] Add FAQs, general cleanup of tools --- FAQ.md | 104 +++++++++++++++++++++++++++++++++++ README.md | 28 ++++------ killerbee/dev_cc253x.py | 2 +- setup.py | 108 ++++++++++++++++++------------------- tools/zbassocflood | 20 ++++--- tools/zbcat | 5 +- tools/zbid | 5 +- tools/zbkey | 16 +++--- tools/zborphannotify | 13 ++--- tools/zbpanidconflictflood | 31 +++++------ tools/zbrealign | 1 - 11 files changed, 214 insertions(+), 119 deletions(-) create mode 100644 FAQ.md diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 00000000..ddf541a7 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,104 @@ +# Frequently Asked Questions + +We often receive the same questions via email, and include this to answer some of the most common ones. + +## Installation + +#### Failed install due to Python.h missing + +- Appears as: + ```bash + ... + zigbee_crypt/zigbee_crypt.c:13:10: fatal error: Python.h: No such file or directory + #include + ^~~~~~~~~~ + compilation terminated. + error: command 'x86_64-linux-gnu-gcc' failed with exit status 1 + ``` + +- Cause: +The requirements were not installed per `README.md`, specifically the Python development package. + +- Fix: +Install the requirement, such as `sudo apt-get install -y python-dev` (or `python3-dev`). + +#### Failed install due to gcrypt.h missing + +- Appears as: + ```bash + ... + zigbee_crypt/zigbee_crypt.c:15:10: fatal error: gcrypt.h: No such file or directory + #include + ^~~~~~~~~~ + compilation terminated. + error: command 'x86_64-linux-gnu-gcc' failed with exit status 1 + ``` + +- Cause: +The requirements were not installed per `README.md`, specifically the gcrypt development package. + +- Fix: +Install the requirement, such as `sudo apt-get install -y libgcrypt-dev`. + +## Device Usage + +### Atmel RZUSBSTICK + +#### Flashing + +See `firmware/README.md` for details. + +#### ValueError device has no langid + +- Appears as: + ```bash + zbid + ... + Traceback (most recent call last): + File "/usr/local/bin/zbid", line 23, in + show_dev(gps=arg_gpsdev, include=args.include) + File "/usr/local/lib/python2.7/dist-packages/killerbee/__init__.py", line 46, in show_dev + for dev in kbutils.devlist(vendor=vendor, product=product, gps=gps, include=include): + File "/usr/local/lib/python2.7/dist-packages/killerbee/kbutils.py", line 285, in devlist + devlist = devlist_usb_v1x(vendor, product) + File "/usr/local/lib/python2.7/dist-packages/killerbee/kbutils.py", line 215, in devlist_usb_v1x + usb.util.get_string(dev, dev.iProduct), \ + File "/usr/lib/python2.7/dist-packages/usb/util.py", line 314, in get_string + raise ValueError("The device has no langid") + ValueError: The device has no langid + ``` + +- Cause: USB permissions + +- Fix: Run as sudo, or change the permissions to the USB device so your user can query it + +### Apimote v4beta + +#### Does not enumerate reliably + +- Appears as: + - Device doesn't show up in `zbid` sometimes + - "Serial timeout" message printed to console during running commands + +- Cause: +As detailed on the page for this device, it is in beta due to instability observed with it establishing +a serial sync with some hosts. + +- Fix: + - Help improve it, likely by working on the settings for the FTDI chip on the PCB + - Specify the device using `-i` when you run commands, so enumeration doesn't need to be run each time + - Unplug and replug the device as needed + +#### Does not get frames received + +- Appears as: Missing frames that you expect and see with other devices +- Cause: Often we find that users are not attaching the antenna as required. +- Fix: As detailed in the product documentation, you must either: + - have an appropriate antenna attached to the RP-SMA port + - or, move the component on the PCB to select the internal antenna + +#### Shows v2 when it enumerates + +- Appears as: `zbid` lists the device as 'v2' +- Cause: expected behavior, as from the software side only v1 is different than v2-v4, and thus it doesn't see a difference +- Fix: N/A diff --git a/README.md b/README.md index 4e7250e3..d20ffd0d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ We appreciate the many contributers to the framework, including the following wh + Jeff Spielberg + Scytmo (bug fixes and CC2530/1 EMK board support) + Adam Laurie/rfidiot (APS crypto implementation, firmware, DFU & BOOTLOADER, SubGHZ, SiLabs NodeTest) ++ Steve Martin REQUIREMENTS ================ @@ -36,14 +37,8 @@ KillerBee is developed and tested on Linux systems. MacOS usage is possible but not supported. We have striven to use a minimum number of software dependencies, however, it -is necessary to install the following Python modules before installation: - -+ serial -+ usb -+ crypto (for some functions) -+ pygtk (for use of tools that have GUIs) -+ cairo (for use of tools that have GUIs) -+ scapy (for some tools which utilize 802.15.4 Scapy extensions) +is necessary to install the following Python modules before installation. +The install will detect and prompt you for what is needed. On Ubuntu systems, you can install the needed dependencies with the following commands: @@ -98,8 +93,7 @@ You must enable these to be searched for in `killerbee/config.py` and then reins ApiMote v4beta (and v3): ---------------- -The devices typically come preloaded and do not need to be reflashed for basic -use. +The devices typically come preloaded and do not need to be reflashed for basic use. The hardware is open-source at https://github.com/riverloopsec/apimote. It is available assembled by contacting team at riverloopsecurity dot com. @@ -174,8 +168,6 @@ networks, built using the KillerBee framework. Each tool has its own usage instructions documented by running the tool with the "-h" argument, and summarized below. - -+ kbbootloader - Switches device into DFU/BOOTLOADER mode (if device is capable) + zbid - Identifies available interfaces that can be used by KillerBee and associated tools. + zbwireshark - Similar to zbdump but exposes a named pipe for real-time @@ -218,17 +210,15 @@ summarized below. + zbdsniff - Captures ZigBee traffic, looking for NWK frames and over-the-air key provisioning. When a key is found, zbdsniff prints the key to stdout. The sample packet capture - sample/zigbee-network-key-ota.dcf can be used to demonstrate + `sample/zigbee-network-key-ota.dcf` can be used to demonstrate this functionality. + zbfind - A GTK GUI application for tracking the location of an IEEE - 802.15.4 transmitter by measuring RSSI. Zbfind can be passive + 802.15.4 transmitter by measuring RSSI. zbfind can be passive in discovery (only listen for packets) or it can be active by sending Beacon Request frames and recording the responses from ZigBee routers and coordinators. If you get a bunch of errors after starting this tool, make - sure your DISPLAY variable is set properly. If you know how - to catch these errors to display a reasonable error message, - please drop me a note. + sure your `DISPLAY` variable is set properly. + zbgoodfind - Implements a key search function using an encrypted packet capture and memory dump from a legitimate ZigBee or IEEE 802.15.4 device. This tool accompanies Travis Goodspeed's @@ -245,6 +235,7 @@ summarized below. installed to run this. + zbscapy - Provides an interactive Scapy shell for interacting via a KillerBee interface. Scapy must be installed to run this. ++ kbbootloader - Switches device into DFU/BOOTLOADER mode (if device is capable) Additional tools, that are for special cases or are not stable, are stored in the Api-Do project repository: http://code.google.com/p/zigbee-security/ @@ -289,5 +280,4 @@ The original version was written by: jwright@willhackforsushi.com. The current version, fixes, etc are handled by: ryan@riverloopsecurity.com. (See the list above for all contributors/credits.) -For contributors/developers, see DEVELOPMENT.md for details and guidance. - +For contributors/developers, see `DEVELOPMENT.md` for details and guidance. diff --git a/killerbee/dev_cc253x.py b/killerbee/dev_cc253x.py index 5462f95e..1f33f1e0 100644 --- a/killerbee/dev_cc253x.py +++ b/killerbee/dev_cc253x.py @@ -215,7 +215,7 @@ def pnext(self, timeout=100): pdata = self.dev.read(self._data_ep, self._maxPacketSize, timeout=timeout) except usb.core.USBError as e: if e.errno != 110: #Operation timed out - print "Error args:", e.args + print("Error args: {}".format(e.args)) raise e #TODO error handling enhancements for USB 1.0 else: diff --git a/setup.py b/setup.py index ae03f71a..f07bfd07 100644 --- a/setup.py +++ b/setup.py @@ -9,93 +9,91 @@ print("No setuptools found, attempting to use distutils instead.") from distutils.core import setup, Extension -err = "" -warn = "" +err = [] +warn = [] +apt_get_pkgs = [] +pip_pkgs = [] # We have made gtk, cairo, scapy-com into optional libraries try: import gtk except ImportError: - warn += "gtk (apt-get install python-gtk2)\n" + warn.append("gtk") + apt_get_pkgs.append("python-gtk2") try: import cairo except ImportError: - warn += "cairo (apt-get install python-cairo)\n" - -try: - import Crypto -except ImportError: - err += "crypto (apt-get install python-crypto)\n" + warn.append("cairo") + apt_get_pkgs.append("python-cairo") # Ensure we have either pyUSB 0.x or pyUSB 1.x, but we now # prefer pyUSB 1.x moving forward. Support for 0.x may be deprecated. try: import usb except ImportError: - err += "usb (apt-get install python-usb)\n" + err.append("usb") + apt_get_pkgs.append("python-usb") try: import usb.core #print("Warning: You are using pyUSB 1.x, support is in beta.") except ImportError: - warn += "You are using pyUSB 0.x. Consider upgrading to pyUSB 1.x." + warn.append("You are using pyUSB 0.x. Consider upgrading to pyUSB 1.x.") -try: - import serial -except ImportError: - err += "serial (apt-get install python-serial)\n" - -try: - import rangeparser -except ImportError: - err += "rangeparser (pip install rangeparser)\n" +# 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 += "Scapy-com 802.15.4 (git clone https://bitbucket.org/secdev/scapy-com)" + warn.append("Scapy 802.15.4 (see README.md)") + pip_pkgs.append("git+https://github.com/secdev/scapy.git#egg=scapy") -if err != "": +if len(err) > 0: print(""" -Library requirements not met. Install the following libraries, then re-run -the setup script. +Library requirements not met. Install the following libraries, then re-run the setup script. -{} - """.format(err), file=sys.stderr) - sys.exit(1) +{}\n""".format('\n'.join(err)), file=sys.stderr) -if warn != "": +if len(warn) > 0: print(""" -Library recommendations not met. For full support, install the following libraries, -then re-run the setup script. +Library recommendations not met. For full support, install the following libraries, then re-run the setup script. -{} - """.format(warn), file=sys.stderr) +{}\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) zigbee_crypt = Extension('zigbee_crypt', - sources = ['zigbee_crypt/zigbee_crypt.c'], - libraries = ['gcrypt'], - include_dirs = ['/usr/local/include', '/usr/include', '/sw/include/', 'zigbee_crypt'], - library_dirs = ['/usr/local/lib', '/usr/lib','/sw/var/lib/'] - ) - -setup (name = 'killerbee', - version = '2.7.1', - description = 'ZigBee and IEEE 802.15.4 Attack Framework and Tools', - author = 'Joshua Wright, Ryan Speers', - author_email = 'jwright@willhackforsushi.com, ryan@riverloopsecurity.com', - license = 'LICENSE.txt', - packages = ['killerbee', 'killerbee.openear', 'killerbee.zbwardrive'], - requires = ['Crypto', 'usb', 'gtk', 'cairo', 'rangeparser'], # Not causing setup to fail, not sure why - scripts = ['tools/zbdump', 'tools/zbgoodfind', 'tools/zbid', 'tools/zbreplay', - 'tools/zbconvert', 'tools/zbdsniff', 'tools/zbstumbler', 'tools/zbassocflood', - 'tools/zbfind', 'tools/zbscapy', 'tools/zbwireshark', 'tools/zbkey', - 'tools/zbwardrive', 'tools/zbopenear', 'tools/zbfakebeacon', - 'tools/zborphannotify', 'tools/zbpanidconflictflood', 'tools/zbrealign', 'tools/zbcat', - 'tools/zbjammer', 'tools/kbbootloader'], - install_requires=['pyserial>=2.0', 'pyusb', 'crypto'], - ext_modules = [ zigbee_crypt ], - ) + sources = ['zigbee_crypt/zigbee_crypt.c'], + libraries = ['gcrypt'], + include_dirs = ['/usr/local/include', '/usr/include', '/sw/include/', 'zigbee_crypt'], + library_dirs = ['/usr/local/lib', '/usr/lib','/sw/var/lib/'] + ) + +setup(name = 'killerbee', + version = '2.7.1', + description = 'ZigBee and IEEE 802.15.4 Attack Framework and Tools', + author = 'Joshua Wright, Ryan Speers', + author_email = 'jwright@willhackforsushi.com, ryan@riverloopsecurity.com', + license = 'LICENSE.txt', + packages = ['killerbee', 'killerbee.openear', 'killerbee.zbwardrive'], + scripts = ['tools/zbdump', 'tools/zbgoodfind', 'tools/zbid', 'tools/zbreplay', + 'tools/zbconvert', 'tools/zbdsniff', 'tools/zbstumbler', 'tools/zbassocflood', + 'tools/zbfind', 'tools/zbscapy', 'tools/zbwireshark', 'tools/zbkey', + 'tools/zbwardrive', 'tools/zbopenear', 'tools/zbfakebeacon', + 'tools/zborphannotify', 'tools/zbpanidconflictflood', 'tools/zbrealign', 'tools/zbcat', + 'tools/zbjammer', 'tools/kbbootloader'], + install_requires=['pyserial>=2.0', 'pyusb', 'pycrypto', 'rangeparser', + 'git+https://github.com/secdev/scapy.git#egg=scapy'], + # NOTE: pygtk doesn't install via distutils on non-Windows hosts + ext_modules = [zigbee_crypt], + ) diff --git a/tools/zbassocflood b/tools/zbassocflood index a6314d1c..dd6caf3a 100644 --- a/tools/zbassocflood +++ b/tools/zbassocflood @@ -52,8 +52,8 @@ def watchforaresp(kb, timeout=1): kb.sniffer_off() return None -if __name__ == '__main__': +if __name__ == '__main__': # Command-line arguments arg_panid = None arg_devstring = None @@ -65,10 +65,10 @@ if __name__ == '__main__': # Assoc Request assocreq = "\x23\xc8\x41\x0b\xc7\x00\x00\xff" \ - "\xff\x00\x00\x00\x00\x00\x00\x00\x00\x01\x8e" + "\xff\x00\x00\x00\x00\x00\x00\x00\x00\x01\x8e" # Association Request Frame in list form, split where we need to modify - assocreqp = [ "\x23\xc8", + assocreqp = [ "\x23\xc8", "", # Seq num "", # Dest PANID "\x00\x00", # Destination (coordinator) @@ -78,7 +78,7 @@ if __name__ == '__main__': ] # Data Request Frame in list form, split where we need to modify - datareqp = [ "\x63\xc8", # FC (intra PAN set) + datareqp = [ "\x63\xc8", # FC (intra PAN set) "", # Seq num "", # Dest PANID "\x00\x00", # Destination (coordinator) @@ -87,10 +87,9 @@ if __name__ == '__main__': ] # ACK Frame in list form, split where we need to modify - ackp = [ "\x02\x00", # FC - "" # ACK'd seq num - ] - + ackp = ["\x02\x00", # FC + "" # ACK'd seq num + ] while len(sys.argv) > 1: op = sys.argv.pop(1) @@ -136,7 +135,7 @@ if __name__ == '__main__': seqnum = 0 ffdswitch = False while 1: - if seqnum > 254: # 254 to accommodate datareq + if seqnum > 254: # 254 to accommodate datareq seqnum = 0 mac = randmac() @@ -161,7 +160,7 @@ if __name__ == '__main__': try: # Send the associate request frame kb.inject(assocreqinj) - time.sleep(0.05) # Delay between assoc and data requests + time.sleep(0.05) # Delay between assoc and data requests # Send the data request frame kb.inject(datareqinj) @@ -174,7 +173,6 @@ if __name__ == '__main__': # Listen for the ACK response seq = watchforaresp(kb, arg_framedelay*10) except Exception, e: - print e print "ERROR: Unable to handle response processing." print e sys.exit(-1) diff --git a/tools/zbcat b/tools/zbcat index 2b53f42d..7d95db93 100644 --- a/tools/zbcat +++ b/tools/zbcat @@ -1,6 +1,6 @@ #!/usr/bin/env python - # Contributed by Bryan Halfpap , Copyright 2016 + import argparse from time import sleep @@ -15,9 +15,10 @@ Can be configured to respond to DATA REQUEST packets (--respond) or it can just Written to help facilitate these situations due to a Wireless Village Wireless Capture-the-Flag at Shmoocon 2016 by @redbarondefcoin NOTE: This script will not handle all scenarios with longer addressing or different frame versions. A future version - could use scapy-com dot15d4 layer to construct/parse packets. [-rmspeers] + could use the scapy dot15d4 layer to construct/parse packets. [-rmspeers] """ + def create_packet(panid, source, dest, data, extsource=False, ack=False, shortaddress=True, seq=0): if extsource == False: extsource = '' diff --git a/tools/zbid b/tools/zbid index b885318d..9cd42478 100644 --- a/tools/zbid +++ b/tools/zbid @@ -1,11 +1,11 @@ #!/usr/bin/env python -''' +""" Print a list of the attached KillerBee recognized devices to stdout. The -g flag may be provided to ignore a serial device, such as an attached GPS serial device which should be ignored by KillerBee. -''' +""" import sys import argparse @@ -21,4 +21,3 @@ if __name__ == '__main__': arg_gpsdev = args.ignore[0] if args.ignore is not None else None #arg_include = args.include if len(args.include)>0 else None show_dev(gps=arg_gpsdev, include=args.include) - diff --git a/tools/zbkey b/tools/zbkey index 2d645d94..cd372fdb 100644 --- a/tools/zbkey +++ b/tools/zbkey @@ -1,4 +1,7 @@ #!/usr/bin/env python +# Contributed by: mandar.satam@gmail.com +# Edited by: ryan@riverloopsecurity.com +# Version: BETA import sys import signal @@ -8,18 +11,16 @@ import time from killerbee import * + def usage(): print >>sys.stderr, """ zbkey: Attempts to retrieve a key by sending the associate request followed by the data request after association response Example usage: ./zbkey -f 14 -s 0.1 -p aa1a -a 0fc8071c08c25100 -i [deviceid] -Contributed by: mandar.satam@gmail.com -Edited by: ryan@riverloopsecurity.com -Version: BETA - Usage: ./zbkey -f [channel] -s 0.1 -p [PANID] -a [IEEE64bitaddress] -i [deviceid] """ + def show_dev(): '''Prints the list of connected devices to stdout.''' kb = KillerBee() @@ -27,6 +28,7 @@ def show_dev(): for dev in kb.dev_list(): print "%s\t%s\t%s" % (dev[0], dev[1], dev[2]) + def interrupt(signum, frame): '''Handles shutdown when a signal is received.''' global kb @@ -34,6 +36,7 @@ def interrupt(signum, frame): print "Exiting..." sys.exit(2) + def associate_response_handle(packet): #TODO link to the correct part of the specification for this frame ''' @@ -75,6 +78,7 @@ def transport_response_handle(packet): return key return None + def isAckFor(packet, lastseq): ''' Determine if the packet is an acknowledgement frame corresponding to our @@ -84,7 +88,8 @@ def isAckFor(packet, lastseq): if packet[2] == lastseq: return True return False - + + def getAckByte(packet): return packet[2] @@ -224,4 +229,3 @@ if value != None: kb.close() sys.exit(1) - diff --git a/tools/zborphannotify b/tools/zborphannotify index 8266d27d..56a2a8a7 100644 --- a/tools/zborphannotify +++ b/tools/zborphannotify @@ -8,15 +8,16 @@ import argparse from time import sleep import logging -logging.getLogger("scapy.runtime").setLevel(logging.ERROR) # suppress annoying Scapy IPv6 warning +logging.getLogger("scapy.runtime").setLevel(logging.ERROR) o# suppress annoying Scapy IPv6 warning from killerbee import * try: - from scapy.all import * + from scapy.all import * except ImportError: - print 'This tool requires Scapy to be installed (specifically scapy-com edition).' - from sys import exit - exit(-1) + print 'This tool requires Scapy to be installed (specifically scapy-com edition).' + from sys import exit + exit(-1) + def create_orphan_mac(panid, devleave, coordinator, device, seqnum): '''802.15.4 Disassociation Frame''' @@ -33,6 +34,7 @@ def create_orphan_mac(panid, devleave, coordinator, device, seqnum): b.src_addr = device return b + if __name__ == '__main__': tohex = lambda s: int(s.replace(':', ''), 16) # Command-line arguments @@ -68,4 +70,3 @@ if __name__ == '__main__': print sp.summary() kb.inject(str(sp)) sleep(0.005) - diff --git a/tools/zbpanidconflictflood b/tools/zbpanidconflictflood index 02a3983b..0768009c 100644 --- a/tools/zbpanidconflictflood +++ b/tools/zbpanidconflictflood @@ -13,22 +13,23 @@ from time import sleep from killerbee import * + def create_beacon(panid, coordinator, epanid): '''Raw creation of beacon packet.''' #print hex(panid), beacon = [ # FRAME CONTROL - "\x00", # FCF for beacon 0x00 - "\x80", # src addressing mode: short/16-bit 0x0002 (this is the only FCF flag marked) - "\xd8", # Sequence number for 802.15.4 layer - str(struct.pack("H", panid)), # 2-byte shortcode of panid + "\x00", # FCF for beacon 0x00 + "\x80", # src addressing mode: short/16-bit 0x0002 (this is the only FCF flag marked) + "\xd8", # Sequence number for 802.15.4 layer + str(struct.pack("H", panid)), # 2-byte shortcode of panid #str(struct.pack("H", coordinator)[0]), # only some implementations make it 0000 "\x00\x00", # Source address 0 "\x00\x00", # SUPERFRAME - "\xff\xcf", # beacon interval, superframe interval, final cap slot, jbattery extension, pan coordinator (true!), association permit (true) + "\xff\xcf", # beacon interval, superframe interval, final cap slot, jbattery extension, pan coordinator (true!), association permit (true) # GTS "\x00", - #Pending addresses + # Pending addresses "\x00", # bullshit zigbee layer packet "\x00\x22\x8c", @@ -38,6 +39,7 @@ def create_beacon(panid, coordinator, epanid): ] return ''.join(beacon) + # Thread 1: Sniffs and extracts Source PAN ID def get_spanids(): spanid = threading.spanid @@ -48,7 +50,7 @@ def get_spanids(): recvpkt = listen.pnext() except: print "Warning: Issue recieving packet." - pass #TODO: Should this be continue instead? + pass # TODO: Should this be continue instead? # Check for empty packet (timeout) and valid FCS if recvpkt != None and recvpkt['validcrc']: restart_threshold = 0 @@ -63,8 +65,8 @@ def get_spanids(): spanid = canidate # Now start sending beacons on the gathered Source PAN ID by informing the other thread: threading.spanid = spanid - # BUG: There's an instance where the Killerbee Framework reads a packet generated by - # the smartthings motion sensor and *crashes* - we handle that here by assuming + # BUG: There's an instance where (at least at the time of this tool writing) we read a packet generated by + # the smartthings motion sensor and *crash* - we handle that here by assuming # that if we didn't get a packet that the interface is busted and we should restart it. # TODO: Isolate the issue and file a bug. restart_threshold += 1 @@ -73,11 +75,12 @@ def get_spanids(): listen.sniffer_off() listen.sniffer_on() restart_threshold = 0 - + + # Thread 2: Injects beacons def inject(): while not continue_execution.isSet(): - sleep(args.sleep) # Added sleep as it seems we were overwhelming USB lib. + sleep(args.sleep) # Added sleep as it seems we were overwhelming USB lib. sp = create_beacon(threading.spanid, args.coordinator, args.epanid) #TODO: Is there any need for: args.devleave, args.coordinator, args.device) try: @@ -87,9 +90,9 @@ def inject(): pass pass + if __name__ == '__main__': tohex = lambda s: int(s.replace(':', ''), 16) - # Command-line arguments parser = argparse.ArgumentParser() parser.add_argument('-f', '--channel', '-c', action='store', dest='channel', required=True, type=int, default=11) @@ -102,7 +105,7 @@ if __name__ == '__main__': #parser.add_argument('--numloops', action='store', default=1, type=int) args = parser.parse_args() - # Can't get the device to like context switching between listen and inject, so we have to have a workaround....... + # Can't get the device to like context switching between listen and inject, so we have to have a workaround... kb = KillerBee(device=args.devstring) listen = KillerBee(device=args.listeninterface) @@ -126,8 +129,6 @@ if __name__ == '__main__': getem2.start() getem1.start() - #import pdb;pdb.set_trace() - #def signal_handler(signal, frame): # continue_execution.set() # import pdb;pdb.set_trace() diff --git a/tools/zbrealign b/tools/zbrealign index d2dfec2c..6188806f 100644 --- a/tools/zbrealign +++ b/tools/zbrealign @@ -78,4 +78,3 @@ if __name__ == '__main__': seqnummac = 0 kb.inject(sp2) # already made it a string to get around limitation in dot14d4 sleep(0.005) - From ae58ca5546a1b86bf084449edc77927be780697e Mon Sep 17 00:00:00 2001 From: rmspeers Date: Tue, 28 May 2019 13:57:55 -0400 Subject: [PATCH 12/14] Fix issue with assocPermit unpack, remove scapy git+ install from setup.py as saw sometimes issues with this syntax --- setup.py | 4 ++-- tools/zborphannotify | 2 +- tools/zbstumbler | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index f07bfd07..dee68b5c 100644 --- a/setup.py +++ b/setup.py @@ -92,8 +92,8 @@ 'tools/zbwardrive', 'tools/zbopenear', 'tools/zbfakebeacon', 'tools/zborphannotify', 'tools/zbpanidconflictflood', 'tools/zbrealign', 'tools/zbcat', 'tools/zbjammer', 'tools/kbbootloader'], - install_requires=['pyserial>=2.0', 'pyusb', 'pycrypto', 'rangeparser', - 'git+https://github.com/secdev/scapy.git#egg=scapy'], + install_requires=['pyserial>=2.0', 'pyusb', 'pycrypto', 'rangeparser'], + #'git+https://github.com/secdev/scapy.git#egg=scapy'], # NOTE: pygtk doesn't install via distutils on non-Windows hosts ext_modules = [zigbee_crypt], ) diff --git a/tools/zborphannotify b/tools/zborphannotify index 56a2a8a7..65d483de 100644 --- a/tools/zborphannotify +++ b/tools/zborphannotify @@ -8,7 +8,7 @@ import argparse from time import sleep import logging -logging.getLogger("scapy.runtime").setLevel(logging.ERROR) o# suppress annoying Scapy IPv6 warning +logging.getLogger("scapy.runtime").setLevel(logging.ERROR) # suppress annoying Scapy IPv6 warning from killerbee import * try: diff --git a/tools/zbstumbler b/tools/zbstumbler index 4eba9653..9450f278 100644 --- a/tools/zbstumbler +++ b/tools/zbstumbler @@ -59,6 +59,7 @@ def display_details(routerdata): if args.csvfile is not None: csvfile.write("0x%02X%02X,0x%02X%02X,%s,%s,%s,%d\n"%(ord(spanid[0]), ord(spanid[1]), ord(source[0]), ord(source[1]), extpanidstr, stackprofilestr, stackverstr, channel)) + def response_handler(stumbled, packet, channel): global args d154 = Dot154PacketParser() @@ -77,7 +78,7 @@ def response_handler(stumbled, packet, channel): beacondata = pktdecode[6] extpanid = beacondata[6][::-1] stackprofilever = beacondata[4] - assocPermit = struct.unpack("<", beacondata[0])[0] & 0x8000 + assocPermit = struct.unpack(" Date: Tue, 4 Jun 2019 07:48:58 -0400 Subject: [PATCH 13/14] Added some subghz page to some tools --- killerbee/__init__.py | 2 +- tools/zbjammer | 7 ++++--- tools/zbrealign | 3 ++- tools/zbreplay | 7 ++++--- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/killerbee/__init__.py b/killerbee/__init__.py index ebaec2c0..2295139d 100644 --- a/killerbee/__init__.py +++ b/killerbee/__init__.py @@ -315,7 +315,7 @@ def page(self): # Driver must have this variable name set in its set_channel function return self.driver._page - def set_channel(self, channel, page= 0): + def set_channel(self, channel, page=0): ''' Sets the radio interface to the specifid channel & page (subghz) @type channel: Integer diff --git a/tools/zbjammer b/tools/zbjammer index bca6f61f..7da8e603 100755 --- a/tools/zbjammer +++ b/tools/zbjammer @@ -24,6 +24,7 @@ def interrupt(signum, frame): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('-i', '--iface', '--dev', action='store', dest='devstring') parser.add_argument('-c', '-f', '--channel', action='store', type=int, default=None) +parser.add_argument('-z', '--subghz_page', action='store', type=int, default=0) parser.add_argument('-D', action='store_true', dest='showdev') args = parser.parse_args() @@ -37,11 +38,11 @@ if args.channel == None: kb = KillerBee(device=args.devstring) signal.signal(signal.SIGINT, interrupt) -if not kb.is_valid_channel(args.channel): - print >>sys.stderr, "ERROR: Must specify a valid IEEE 802.15.4 channel for the selected device." +if not kb.is_valid_channel(args.channel, args.subghz_page): + print >>sys.stderr, "ERROR: Must specify a valid IEEE 802.15.4 channel/page for the selected device." kb.close() sys.exit(1) -kb.set_channel(args.channel) +kb.set_channel(args.channel, page=args.subghz_page) print "zbjammer: jamming channel", args.channel print "*** WARNING: this may not actually work on your hardware! Check with spectrum analyser!" diff --git a/tools/zbrealign b/tools/zbrealign index 6188806f..5c26f3a9 100644 --- a/tools/zbrealign +++ b/tools/zbrealign @@ -49,6 +49,7 @@ if __name__ == '__main__': # Command-line arguments parser = argparse.ArgumentParser() parser.add_argument('-f', '--channel', '-c', action='store', dest='channel', required=True, type=int, default=11) + parser.add_argument('-z', '--subghz_page', action='store', type=int, default=0) parser.add_argument('-i', '--interface', action='store', dest='devstring', default=None) parser.add_argument('-p', '--panid', help="The current PAN ID of the victim PAN", action='store', required=True, type=tohex) parser.add_argument('-s', '--coordinator', help="Realignment packet coordinator short address", action='store', required=True, type=tohex) @@ -66,7 +67,7 @@ if __name__ == '__main__': #TODO: seqnums for each direction kb = KillerBee(device=args.devstring) - kb.set_channel(args.channel) + kb.set_channel(args.channel, page=args.subghz_page) seqnummac = args.srcseq diff --git a/tools/zbreplay b/tools/zbreplay index 895e3a75..a9fce49c 100644 --- a/tools/zbreplay +++ b/tools/zbreplay @@ -44,6 +44,7 @@ parser.add_argument('-r', '--pcapfile', action='store', default=None) parser.add_argument('-R', '--dsnafile', action='store', default=None) #parser.add_argument('-g', '--gps', '--ignore', action='append', dest='ignore') parser.add_argument('-c', '-f', '--channel', action='store', type=int, default=None) +parser.add_argument('-z', '--subghz_page', action='store', type=int, default=0) parser.add_argument('-n', '--count', action='store', type=int, default=-1) parser.add_argument('-s', '--sleep', action='store', type=float, default=1.0) parser.add_argument('-D', action='store_true', dest='showdev') @@ -69,11 +70,11 @@ elif args.dsnafile is not None: kb = KillerBee(device=args.devstring) signal.signal(signal.SIGINT, interrupt) -if not kb.is_valid_channel(args.channel): - print >>sys.stderr, "ERROR: Must specify a valid IEEE 802.15.4 channel for the selected device." +if not kb.is_valid_channel(args.channel, args.subghz_page): + print >>sys.stderr, "ERROR: Must specify a valid IEEE 802.15.4 channel/page for the selected device." kb.close() sys.exit(1) -kb.set_channel(args.channel) +kb.set_channel(args.channel, page=args.subghz_page) print("zbreplay: retransmitting frames from \'{0}\' on interface \'{1}\' with a delay of {2} seconds.".format(savefile, kb.get_dev_info()[0], args.sleep)) From 352e84410bdc261f0253361b838f90972f715596 Mon Sep 17 00:00:00 2001 From: rmspeers Date: Tue, 4 Jun 2019 07:49:56 -0400 Subject: [PATCH 14/14] Specify C501 for antenna selector --- FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FAQ.md b/FAQ.md index ddf541a7..679164df 100644 --- a/FAQ.md +++ b/FAQ.md @@ -95,7 +95,7 @@ a serial sync with some hosts. - Cause: Often we find that users are not attaching the antenna as required. - Fix: As detailed in the product documentation, you must either: - have an appropriate antenna attached to the RP-SMA port - - or, move the component on the PCB to select the internal antenna + - or, move the component C501 on the PCB to select the internal antenna #### Shows v2 when it enumerates