Skip to content

Commit 8fc7fc9

Browse files
authored
dot15d4: fix aux_sec_header incorrect parsing (#4969)
* dot15d4: fix aux_sec_header incorrect parsing (#4928) Fix two bugs preventing correct parsing of 802.15.4 frames with the security bit set: 1. Replace `is True` identity check with truthiness check in ConditionalField lambdas for Dot15d4Data, Dot15d4Beacon, and Dot15d4Cmd. In Python 3, `1 is True` is False because `is` checks object identity, not equality, so aux_sec_header was never parsed. 2. Add extract_padding() to Dot15d4AuxSecurityHeader so that remaining bytes after the header fields are returned to the parent packet instead of being consumed as payload. * test: wrap encrypted beacon test with no_debug_dissector The encrypted beacon payload cannot be parsed as ZigBeeBeacon, which raises an exception when conf.debug_dissector is True (as in CI). * dot15d4: prevent dissection of encrypted payloads as upper layers When sec_sc_seclevel >= 4 (ENC, ENC-MIC-*), the payload after aux_sec_header is encrypted and must not be passed to upper layer dissectors (SixLoWPAN, ZigBee, etc.). Add encrypted payload checks to guess_payload_class() in Dot15d4Data, Dot15d4Beacon, and Dot15d4Cmd.
1 parent bfab325 commit 8fc7fc9

2 files changed

Lines changed: 95 additions & 3 deletions

File tree

scapy/layers/dot15d4.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ class Dot15d4AuxSecurityHeader(Packet):
221221
lambda pkt: pkt.getfieldval("sec_sc_keyidmode") != 0),
222222
]
223223

224+
def extract_padding(self, s):
225+
return b"", s
226+
224227

225228
class Dot15d4Data(Packet):
226229
name = "802.15.4 Data"
@@ -233,10 +236,14 @@ class Dot15d4Data(Packet):
233236
lambda pkt:pkt.underlayer.getfieldval("fcf_srcaddrmode") != 0), # noqa: E501
234237
# Security field present if fcf_security == True
235238
ConditionalField(PacketField("aux_sec_header", Dot15d4AuxSecurityHeader(), Dot15d4AuxSecurityHeader), # noqa: E501
236-
lambda pkt:pkt.underlayer.getfieldval("fcf_security") is True), # noqa: E501
239+
lambda pkt:pkt.underlayer.getfieldval("fcf_security")), # noqa: E501
237240
]
238241

239242
def guess_payload_class(self, payload):
243+
# Encrypted payloads (sec_sc_seclevel >= 4) cannot be dissected further
244+
if self.aux_sec_header is not None and \
245+
self.aux_sec_header.sec_sc_seclevel >= 4:
246+
return conf.raw_layer
240247
# TODO: See how it's done in wireshark:
241248
# https://github.com/wireshark/wireshark/blob/93c60b3b7c801dddd11d8c7f2a0ea4b7d02d700a/epan/dissectors/packet-ieee802154.c#L2061 # noqa: E501
242249
# it's too magic to me
@@ -268,7 +275,7 @@ class Dot15d4Beacon(Packet):
268275
dot15d4AddressField("src_addr", None, length_of="fcf_srcaddrmode"),
269276
# Security field present if fcf_security == True
270277
ConditionalField(PacketField("aux_sec_header", Dot15d4AuxSecurityHeader(), Dot15d4AuxSecurityHeader), # noqa: E501
271-
lambda pkt:pkt.underlayer.getfieldval("fcf_security") is True), # noqa: E501
278+
lambda pkt:pkt.underlayer.getfieldval("fcf_security")), # noqa: E501
272279

273280
# Superframe spec field:
274281
BitField("sf_sforder", 15, 4), # not used by ZigBee
@@ -306,6 +313,13 @@ class Dot15d4Beacon(Packet):
306313
# TODO beacon payload
307314
]
308315

316+
def guess_payload_class(self, payload):
317+
# Encrypted payloads (sec_sc_seclevel >= 4) cannot be dissected further
318+
if self.aux_sec_header is not None and \
319+
self.aux_sec_header.sec_sc_seclevel >= 4:
320+
return conf.raw_layer
321+
return Packet.guess_payload_class(self, payload)
322+
309323
def mysummary(self):
310324
return self.sprintf("802.15.4 Beacon ( %Dot15d4Beacon.src_panid%:%Dot15d4Beacon.src_addr% ) assocPermit(%Dot15d4Beacon.sf_assocpermit%) panCoord(%Dot15d4Beacon.sf_pancoord%)") # noqa: E501
311325

@@ -323,7 +337,7 @@ class Dot15d4Cmd(Packet):
323337
lambda pkt:pkt.underlayer.getfieldval("fcf_srcaddrmode") != 0), # noqa: E501
324338
# Security field present if fcf_security == True
325339
ConditionalField(PacketField("aux_sec_header", Dot15d4AuxSecurityHeader(), Dot15d4AuxSecurityHeader), # noqa: E501
326-
lambda pkt:pkt.underlayer.getfieldval("fcf_security") is True), # noqa: E501
340+
lambda pkt:pkt.underlayer.getfieldval("fcf_security")), # noqa: E501
327341
ByteEnumField("cmd_id", 0, {
328342
1: "AssocReq", # Association request
329343
2: "AssocResp", # Association response
@@ -345,6 +359,10 @@ def mysummary(self):
345359
# command frame payloads are complete: DataReq, PANIDConflictNotify, OrphanNotify, BeaconReq don't have any payload # noqa: E501
346360
# Although BeaconReq can have an optional ZigBee Beacon payload (implemented in ZigBeeBeacon) # noqa: E501
347361
def guess_payload_class(self, payload):
362+
# Encrypted payloads (sec_sc_seclevel >= 4) cannot be dissected further
363+
if self.aux_sec_header is not None and \
364+
self.aux_sec_header.sec_sc_seclevel >= 4:
365+
return conf.raw_layer
348366
if self.cmd_id == 1:
349367
return Dot15d4CmdAssocReq
350368
elif self.cmd_id == 2:

test/scapy/layers/dot15d4.uts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,80 @@ p = Dot15d4AuxSecurityHeader(b"\x18\x05\x00\x00\x00\xff\xee\xdd\xcc\xbb\xaa\x00\
325325
assert p.sec_sc_keyidmode == 3
326326
assert p.sec_keyid_keysource == 11024999611375677183
327327

328+
= Dot15d4AuxSecurityHeader - extract_padding does not consume trailing bytes
329+
330+
p = Dot15d4AuxSecurityHeader(b"\x04\x05\x00\x00\x00\xAA\xBB")
331+
assert p.sec_sc_seclevel == 4
332+
assert p.sec_framecounter == 0x5
333+
assert Raw not in p
334+
assert Padding in p
335+
assert p[Padding].load == b"\xAA\xBB"
336+
337+
= Dot15d4 Beacon with aux_sec_header (issue #4928)
338+
339+
# Given: raw bytes for a Dot15d4 Beacon frame with fcf_security=1
340+
# Note: remaining bytes are encrypted data, so ZigBeeBeacon dissection is expected to fail
341+
with no_debug_dissector():
342+
pkt = Dot15d4(b'\x08\xD0\x84\x21\x43\x01\x00\x00\x00\x00\x48\xDE\xAC\x02\x05\x00\x00\x00\x55\xCF\x00\x00\x51\x52\x53\x54\x22\x3B\xC1\xEC\x84\x1A\xB5\x53')
343+
344+
assert pkt.fcf_frametype == 0
345+
assert pkt.fcf_security == 1
346+
assert Dot15d4Beacon in pkt
347+
assert pkt[Dot15d4Beacon].aux_sec_header is not None
348+
assert pkt[Dot15d4Beacon].aux_sec_header.sec_sc_seclevel == 2
349+
assert pkt[Dot15d4Beacon].aux_sec_header.sec_sc_keyidmode == 0
350+
assert pkt[Dot15d4Beacon].aux_sec_header.sec_framecounter == 0x5
351+
assert Raw in pkt
352+
353+
= Dot15d4 Data with aux_sec_header - build & dissect round-trip
354+
355+
# Given: a Dot15d4 Data frame with fcf_security=1
356+
pkt = Dot15d4(fcf_frametype=1, fcf_security=1, fcf_destaddrmode=2, fcf_srcaddrmode=2) / Dot15d4Data(dest_panid=0x1234, dest_addr=0xFFFF, src_panid=0x1234, src_addr=0x0001, aux_sec_header=Dot15d4AuxSecurityHeader(sec_sc_seclevel=5, sec_sc_keyidmode=1, sec_keyid_keyindex=0x01))
357+
# When: packet is serialized and re-dissected
358+
pkt2 = Dot15d4(raw(pkt))
359+
# Then: aux_sec_header is correctly parsed
360+
assert pkt2.fcf_security == 1
361+
assert Dot15d4Data in pkt2
362+
assert pkt2[Dot15d4Data].aux_sec_header is not None
363+
assert pkt2[Dot15d4Data].aux_sec_header.sec_sc_seclevel == 5
364+
assert pkt2[Dot15d4Data].aux_sec_header.sec_sc_keyidmode == 1
365+
assert pkt2[Dot15d4Data].aux_sec_header.sec_keyid_keyindex == 0x01
366+
367+
= Dot15d4 Cmd with aux_sec_header - build & dissect round-trip
368+
369+
# Given: a Dot15d4 Command frame with fcf_security=1
370+
pkt = Dot15d4(fcf_frametype=3, fcf_security=1, fcf_destaddrmode=2, fcf_srcaddrmode=2) / Dot15d4Cmd(dest_panid=0x1234, dest_addr=0xFFFF, src_panid=0x1234, src_addr=0x0001, aux_sec_header=Dot15d4AuxSecurityHeader(sec_sc_seclevel=5, sec_sc_keyidmode=1, sec_keyid_keyindex=0x01), cmd_id=4)
371+
# When: packet is serialized and re-dissected
372+
pkt2 = Dot15d4(raw(pkt))
373+
# Then: aux_sec_header is correctly parsed
374+
assert pkt2.fcf_security == 1
375+
assert Dot15d4Cmd in pkt2
376+
assert pkt2[Dot15d4Cmd].aux_sec_header is not None
377+
assert pkt2[Dot15d4Cmd].aux_sec_header.sec_sc_seclevel == 5
378+
assert pkt2[Dot15d4Cmd].aux_sec_header.sec_sc_keyidmode == 1
379+
380+
= Dot15d4 Data with encrypted payload (seclevel >= 4) stays Raw
381+
382+
# Given: a Data frame with seclevel=4 (ENC) and trailing encrypted bytes
383+
pkt = Dot15d4(fcf_frametype=1, fcf_security=1, fcf_destaddrmode=2, fcf_srcaddrmode=2) / Dot15d4Data(dest_panid=0x1234, dest_addr=0xFFFF, src_panid=0x1234, src_addr=0x0001, aux_sec_header=Dot15d4AuxSecurityHeader(sec_sc_seclevel=4, sec_sc_keyidmode=1, sec_keyid_keyindex=0x01)) / Raw(b'\xaa\xbb\xcc\xdd')
384+
# When: packet is serialized and re-dissected
385+
pkt2 = Dot15d4(raw(pkt))
386+
# Then: encrypted payload must not be dissected as SixLoWPAN/ZigBee
387+
assert pkt2[Dot15d4Data].aux_sec_header.sec_sc_seclevel == 4
388+
assert Raw in pkt2
389+
from scapy.layers.sixlowpan import SixLoWPAN
390+
assert SixLoWPAN not in pkt2
391+
392+
= Dot15d4 Beacon without aux_sec_header (fcf_security=0)
393+
394+
# Given: raw bytes for a Dot15d4 Beacon frame with fcf_security=0
395+
pkt = Dot15d4FCS(b'\x00\x80\x89\xaa\x99\x00\x00\xff\xcf\x00\x00\x00"\x84\xfe\xca\xef\xbe\xed\xfe\xce\xfa\xff\xff\xff\x00X\xa4')
396+
# When: packet is dissected
397+
# Then: fcf_security is not set and aux_sec_header is None
398+
assert pkt.fcf_security == 0
399+
assert Dot15d4Beacon in pkt
400+
assert pkt[Dot15d4Beacon].aux_sec_header is None
401+
328402
# RPL: unimplemented
329403
#p = SixLoWPAN(b"\x7b\x3b\x3a\x1a\x9b\x02\xae\x30\x21\x00\x00\xef\x05\x12\x00\x80\x20\x02\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x00\x33\x44\x09\x04\x00\x00\x00\x00\x06\x04\x00\x01\xef\xff")
330404
#p.show2()

0 commit comments

Comments
 (0)