# 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 """ Wireless MAC according to IEEE 802.15.4. """ import struct from scapy.compat import orb, chb from scapy.error import warning from scapy.config import conf from scapy.data import DLT_IEEE802_15_4_WITHFCS, DLT_IEEE802_15_4_NOFCS from scapy.packet import Packet, bind_layers from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ ConditionalField, Field, LELongField, PacketField, XByteField, \ XLEIntField, XLEShortField, FCSField, Emph # Fields # class dot15d4AddressField(Field): __slots__ = ["adjust", "length_of"] def __init__(self, name, default, length_of=None, fmt=" %Dot15d4.fcf_srcaddrmode% ) Seq#%Dot15d4.seqnum%") # noqa: E501 def guess_payload_class(self, payload): if self.fcf_frametype == 0x00: return Dot15d4Beacon elif self.fcf_frametype == 0x01: return Dot15d4Data elif self.fcf_frametype == 0x02: return Dot15d4Ack elif self.fcf_frametype == 0x03: return Dot15d4Cmd else: return Packet.guess_payload_class(self, payload) def answers(self, other): if isinstance(other, Dot15d4): if self.fcf_frametype == 2: # ack if self.seqnum != other.seqnum: # check for seqnum matching return 0 elif other.fcf_ackreq == 1: # check that an ack was indeed requested # noqa: E501 return 1 return 0 def post_build(self, p, pay): # This just forces destaddrmode to None for Ack frames. if self.fcf_frametype == 2 and self.fcf_destaddrmode != 0: self.fcf_destaddrmode = 0 return p[:1] + \ chb((self.fcf_srcaddrmode << 6) + (self.fcf_framever << 4)) \ + p[2:] + pay else: return p + pay class Dot15d4FCS(Dot15d4): ''' This class is a drop-in replacement for the Dot15d4 class above, except it expects a FCS/checksum in the input, and produces one in the output. This provides the user flexibility, as many 802.15.4 interfaces will have an AUTO_CRC setting # noqa: E501 that will validate the FCS/CRC in firmware, and add it automatically when transmitting. # noqa: E501 ''' name = "802.15.4 - FCS" match_subclass = True fields_desc = Dot15d4.fields_desc + [FCSField("fcs", None, fmt=" %Dot15d4Data.dest_panid%:%Dot15d4Data.dest_addr% )") # noqa: E501 class Dot15d4Beacon(Packet): name = "802.15.4 Beacon" fields_desc = [ XLEShortField("src_panid", 0x0), dot15d4AddressField("src_addr", None, length_of="fcf_srcaddrmode"), # Security field present if fcf_security == True ConditionalField(PacketField("aux_sec_header", Dot15d4AuxSecurityHeader(), Dot15d4AuxSecurityHeader), # noqa: E501 lambda pkt:pkt.underlayer.getfieldval("fcf_security") is True), # noqa: E501 # Superframe spec field: BitField("sf_sforder", 15, 4), # not used by ZigBee BitField("sf_beaconorder", 15, 4), # not used by ZigBee BitEnumField("sf_assocpermit", 0, 1, [False, True]), BitEnumField("sf_pancoord", 0, 1, [False, True]), BitField("sf_reserved", 0, 1), # not used by ZigBee BitEnumField("sf_battlifeextend", 0, 1, [False, True]), # not used by ZigBee # noqa: E501 BitField("sf_finalcapslot", 15, 4), # not used by ZigBee # GTS Fields # GTS Specification (1 byte) BitEnumField("gts_spec_permit", 1, 1, [False, True]), # GTS spec bit 7, true=1 iff PAN cord is accepting GTS requests # noqa: E501 BitField("gts_spec_reserved", 0, 4), # GTS spec bits 3-6 BitField("gts_spec_desccount", 0, 3), # GTS spec bits 0-2 # GTS Directions (0 or 1 byte) ConditionalField(BitField("gts_dir_reserved", 0, 1), lambda pkt:pkt.getfieldval("gts_spec_desccount") != 0), # noqa: E501 ConditionalField(BitField("gts_dir_mask", 0, 7), lambda pkt:pkt.getfieldval("gts_spec_desccount") != 0), # noqa: E501 # GTS List (variable size) # TODO add a Packet/FieldListField tied to 3bytes per count in gts_spec_desccount # noqa: E501 # Pending Address Fields: # Pending Address Specification (1 byte) BitField("pa_num_short", 0, 3), # number of short addresses pending BitField("pa_reserved_1", 0, 1), BitField("pa_num_long", 0, 3), # number of long addresses pending BitField("pa_reserved_2", 0, 1), # Address List (var length) # TODO add a FieldListField of the pending short addresses, followed by the pending long addresses, with max 7 addresses # noqa: E501 # TODO beacon payload ] def mysummary(self): return self.sprintf("802.15.4 Beacon ( %Dot15d4Beacon.src_panid%:%Dot15d4Beacon.src_addr% ) assocPermit(%Dot15d4Beacon.sf_assocpermit%) panCoord(%Dot15d4Beacon.sf_pancoord%)") # noqa: E501 class Dot15d4Cmd(Packet): name = "802.15.4 Command" fields_desc = [ XLEShortField("dest_panid", 0xFFFF), # Users should correctly set the dest_addr field. By default is 0x0 for construction to work. # noqa: E501 dot15d4AddressField("dest_addr", 0x0, length_of="fcf_destaddrmode"), ConditionalField(XLEShortField("src_panid", 0x0), \ lambda pkt:util_srcpanid_present(pkt)), ConditionalField(dot15d4AddressField("src_addr", None, length_of="fcf_srcaddrmode"), lambda pkt:pkt.underlayer.getfieldval("fcf_srcaddrmode") != 0), # noqa: E501 # Security field present if fcf_security == True ConditionalField(PacketField("aux_sec_header", Dot15d4AuxSecurityHeader(), Dot15d4AuxSecurityHeader), # noqa: E501 lambda pkt:pkt.underlayer.getfieldval("fcf_security") is True), # noqa: E501 ByteEnumField("cmd_id", 0, { 1: "AssocReq", # Association request 2: "AssocResp", # Association response 3: "DisassocNotify", # Disassociation notification 4: "DataReq", # Data request 5: "PANIDConflictNotify", # PAN ID conflict notification 6: "OrphanNotify", # Orphan notification 7: "BeaconReq", # Beacon request 8: "CoordRealign", # coordinator realignment 9: "GTSReq" # GTS request # 0x0a - 0xff reserved }), # TODO command payload ] def mysummary(self): return self.sprintf("802.15.4 Command %Dot15d4Cmd.cmd_id% ( %Dot15dCmd.src_panid%:%Dot15d4Cmd.src_addr% -> %Dot15d4Cmd.dest_panid%:%Dot15d4Cmd.dest_addr% )") # noqa: E501 # command frame payloads are complete: DataReq, PANIDConflictNotify, OrphanNotify, BeaconReq don't have any payload # noqa: E501 # Although BeaconReq can have an optional ZigBee Beacon payload (implemented in ZigBeeBeacon) # noqa: E501 def guess_payload_class(self, payload): if self.cmd_id == 1: return Dot15d4CmdAssocReq elif self.cmd_id == 2: return Dot15d4CmdAssocResp elif self.cmd_id == 3: return Dot15d4CmdDisassociation elif self.cmd_id == 8: return Dot15d4CmdCoordRealign elif self.cmd_id == 9: return Dot15d4CmdGTSReq else: return Packet.guess_payload_class(self, payload) class Dot15d4CmdCoordRealign(Packet): name = "802.15.4 Coordinator Realign Command" fields_desc = [ # PAN Identifier (2 octets) XLEShortField("panid", 0xFFFF), # Coordinator Short Address (2 octets) XLEShortField("coord_address", 0x0000), # Logical Channel (1 octet): the logical channel that the coordinator intends to use for all future communications # noqa: E501 ByteField("channel", 0), # Short Address (2 octets) XLEShortField("dev_address", 0xFFFF), # Channel page (0/1 octet) TODO optional # ByteField("channel_page", 0), ] def mysummary(self): return self.sprintf("802.15.4 Coordinator Realign Payload ( PAN ID: %Dot15dCmdCoordRealign.pan_id% : channel %Dot15d4CmdCoordRealign.channel% )") # noqa: E501 # Utility Functions # def util_srcpanid_present(pkt): '''A source PAN ID is included if and only if both src addr mode != 0 and PAN ID Compression in FCF == 0''' # noqa: E501 if (pkt.underlayer.getfieldval("fcf_srcaddrmode") != 0) and (pkt.underlayer.getfieldval("fcf_panidcompress") == 0): # noqa: E501 return True else: return False class Dot15d4CmdAssocReq(Packet): name = "802.15.4 Association Request Payload" fields_desc = [ BitField("allocate_address", 0, 1), # Allocate Address BitField("security_capability", 0, 1), # Security Capability BitField("reserved2", 0, 1), # bit 5 is reserved BitField("reserved1", 0, 1), # bit 4 is reserved BitField("receiver_on_when_idle", 0, 1), # Receiver On When Idle BitField("power_source", 0, 1), # Power Source BitField("device_type", 0, 1), # Device Type BitField("alternate_pan_coordinator", 0, 1), # Alternate PAN Coordinator # noqa: E501 ] def mysummary(self): return self.sprintf("802.15.4 Association Request Payload ( Alt PAN Coord: %Dot15d4CmdAssocReq.alternate_pan_coordinator% Device Type: %Dot15d4CmdAssocReq.device_type% )") # noqa: E501 class Dot15d4CmdAssocResp(Packet): name = "802.15.4 Association Response Payload" fields_desc = [ XLEShortField("short_address", 0xFFFF), # Address assigned to device from coordinator (0xFFFF == none) # noqa: E501 # Association Status # 0x00 == successful # 0x01 == PAN at capacity # 0x02 == PAN access denied # 0x03 - 0x7f == Reserved # 0x80 - 0xff == Reserved for MAC primitive enumeration values ByteEnumField("association_status", 0x00, {0: 'successful', 1: 'PAN_at_capacity', 2: 'PAN_access_denied'}), # noqa: E501 ] def mysummary(self): return self.sprintf("802.15.4 Association Response Payload ( Association Status: %Dot15d4CmdAssocResp.association_status% Assigned Address: %Dot15d4CmdAssocResp.short_address% )") # noqa: E501 class Dot15d4CmdDisassociation(Packet): name = "802.15.4 Disassociation Notification Payload" fields_desc = [ # Disassociation Reason # 0x00 == Reserved # 0x01 == The coordinator wishes the device to leave the PAN # 0x02 == The device wishes to leave the PAN # 0x03 - 0x7f == Reserved # 0x80 - 0xff == Reserved for MAC primitive enumeration values ByteEnumField("disassociation_reason", 0x02, {1: 'coord_wishes_device_to_leave', 2: 'device_wishes_to_leave'}), # noqa: E501 ] def mysummary(self): return self.sprintf("802.15.4 Disassociation Notification Payload ( Disassociation Reason %Dot15d4CmdDisassociation.disassociation_reason% )") # noqa: E501 class Dot15d4CmdGTSReq(Packet): name = "802.15.4 GTS request command" fields_desc = [ # GTS Characteristics field (1 octet) # Reserved (bits 6-7) BitField("reserved", 0, 2), # Characteristics Type (bit 5) BitField("charact_type", 0, 1), # GTS Direction (bit 4) BitField("gts_dir", 0, 1), # GTS Length (bits 0-3) BitField("gts_len", 0, 4), ] def mysummary(self): return self.sprintf("802.15.4 GTS Request Command ( %Dot15d4CmdGTSReq.gts_len% : %Dot15d4CmdGTSReq.gts_dir% )") # noqa: E501 # PAN ID conflict notification command frame is not necessary, only Dot15d4Cmd with cmd_id = 5 ("PANIDConflictNotify") # noqa: E501 # Orphan notification command not necessary, only Dot15d4Cmd with cmd_id = 6 ("OrphanNotify") # noqa: E501 # Bindings # bind_layers(Dot15d4, Dot15d4Beacon, fcf_frametype=0) bind_layers(Dot15d4, Dot15d4Data, fcf_frametype=1) bind_layers(Dot15d4, Dot15d4Ack, fcf_frametype=2) bind_layers(Dot15d4, Dot15d4Cmd, fcf_frametype=3) # DLT Types # conf.l2types.register(DLT_IEEE802_15_4_WITHFCS, Dot15d4FCS) conf.l2types.register(DLT_IEEE802_15_4_NOFCS, Dot15d4)