3

I am working with a fork of scapy (a Python packet manipulation tool) called scapy-com. This implements 802.15.4 and Zigbee parsing/manipulation, amongst other protocols.

A quirk of the Zigbee protcol is found in the network level security header. Initially, the security level (which defines the encryption and length of message integrity code) is set correctly, but is then set to 0 (no encryption) before it is sent. From the spec:

The security level sub-field of the security control field shall be over-written by the 3-bit all-zero string '000'

The spec can be found here. The relevant section is "4.3.1.1 Security Processing of Outgoing Frames".

This means that packet captures indicate that no encryption or message integrity code is in use. The security level must be communicated out-of-band.

scapy-com doesn't deal with this. It naively parses the security level and sets the length of the MIC to 0. The code that does this is:

def util_mic_len(pkt):
    ''' Calculate the length of the attribute value field '''
    # NWK security level 0 seems to implicitly be same as 5
    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

The project that uses scapy-com attempts to deal with this by setting the security level to 5:

#TODO: Investigate and issue a different fix:
# https://code.google.com/p/killerbee/issues/detail?id=30
# This function destroys the packet, therefore work on a copy - @cutaway
pkt = pkt.copy()   #this is hack to fix the below line
pkt.nwk_seclevel=5 #the issue appears to be when this is set

mic = pkt.mic

However, this doesn't work - the message integrity code has already been set. I have worked around this by simply altering the util_mic_len function to set the mic length correctly.

The question is, how should the Zigbee parser be changed so that altering the nwk_seclevel after the initial dissection causes the mic length to be updated?

I can see two solutions:

  1. Change the scapy-com code so that changing nwk_seclevel automatically changes the mic length.
  2. Re-dissect the packets from outside scapy-com as they are changed.

The issue with 1 is I have no idea about how to go about it.

The issue with 2 is that I have some idea but can't get it to work - I can't work out how to call dissect on a packet after it has been loaded. Calling pkt.dissect(pkt) seems to not work and looks odd.

What is the best or recommended solution here?

Cybergibbons
  • 464
  • 3
  • 9

2 Answers2

2

Fixing scapy sounds right solution. scapy-com is quite old. Zigbee specific code in scapy-com is 1244 lines of code, which in large part are enumerations and field lists. So, it should not be too hard to migrate it to scapy-python3. If you would assist in migrating it to scapy-python3 http://github.com/phaethon/scapy , I could help with fixing the issue.

Eriks Dobelis
  • 913
  • 7
  • 16
1

The project you are referring to is KillerBee and I had this exact problem with decryption. I simply "fixed" the code thusly:

from struct import pack
f = pkt.getlayer(ZigbeeSecurityHeader).fields

pkt.nwk_seclevel = 5
nwk_mic = pkt.mic
nwk_encrypted = f['data'][:-6]

ext_source = f['ext_source']

nwk_sec_ctrl_byte = str(pkt.getlayer(ZigbeeSecurityHeader))[0]
nwk_nonce = struct.pack('Q',ext_source) + struct.pack('I',f['fc']) + nwk_sec_ctrl_byte

nwk_crop_size = 4 + 2 + len(pkt.getlayer(ZigbeeSecurityHeader).fields['data'])  # The length of the encrypted data, mic and FCS

# the Security Control Field flags have to be adjusted before this is calculated, so we store their original values so we can reset them later
zigbeeData = pkt.getlayer(ZigbeeNWK).do_build()
zigbeeData = zigbeeData[:-nwk_crop_size]

(nwk_payload, nwk_micCheck) = zigbee_crypt.decrypt_ccm(nkey, nwk_nonce, nwk_mic, nwk_encrypted, zigbeeData)