Skip to content

Commit 123db03

Browse files
author
Dimitrios-Georgios Akestoridis
committed
Add LoWPAN enhancements
1 parent e0c96c5 commit 123db03

File tree

6 files changed

+929
-60
lines changed

6 files changed

+929
-60
lines changed

scapy/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,7 @@ class Conf(ConfClass):
832832
'llmnr',
833833
'lltd',
834834
'mgcp',
835+
'mle',
835836
'mobileip',
836837
'netbios',
837838
'netflow',

scapy/layers/dot15d4.py

Lines changed: 191 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Copyright (C) Ryan Speers <[email protected]> 2011-2012
55
# Copyright (C) Roger Meyer <[email protected]>: 2012-03-10 Added frames
66
# Copyright (C) Gabriel Potter <[email protected]>: 2018
7-
# Copyright (C) 2020 Dimitrios-Georgios Akestoridis <[email protected]>
7+
# Copyright (C) 2020, 2022 Dimitrios-Georgios Akestoridis <[email protected]>
88
# This program is published under a GPLv2 license
99

1010
"""
@@ -36,6 +36,7 @@
3636
XByteField,
3737
XLEIntField,
3838
XLEShortField,
39+
XStrField,
3940
)
4041

4142
# Fields #
@@ -233,9 +234,36 @@ class Dot15d4Data(Packet):
233234
lambda pkt:pkt.underlayer.getfieldval("fcf_srcaddrmode") != 0), # noqa: E501
234235
# Security field present if fcf_security == True
235236
ConditionalField(PacketField("aux_sec_header", Dot15d4AuxSecurityHeader(), Dot15d4AuxSecurityHeader), # noqa: E501
236-
lambda pkt:pkt.underlayer.getfieldval("fcf_security") is True), # noqa: E501
237+
lambda pkt:pkt.underlayer.getfieldval("fcf_security")), # noqa: E501
238+
# Secured Payload (variable length)
239+
ConditionalField(
240+
XStrField("sec_payload", ""),
241+
lambda pkt:pkt.underlayer.getfieldval("fcf_security"),
242+
),
243+
# Message Integrity Code (variable length)
244+
ConditionalField(
245+
XStrField("mic", ""),
246+
lambda pkt:pkt.underlayer.getfieldval("fcf_security"),
247+
),
237248
]
238249

250+
def post_dissect(self, s):
251+
if self.underlayer.getfieldval("fcf_security"):
252+
if self.aux_sec_header.sec_sc_seclevel in {1, 5}:
253+
mic_length = 4
254+
elif self.aux_sec_header.sec_sc_seclevel in {2, 6}:
255+
mic_length = 8
256+
elif self.aux_sec_header.sec_sc_seclevel in {3, 7}:
257+
mic_length = 16
258+
else:
259+
mic_length = 0
260+
self.sec_payload = bytes(self.aux_sec_header.payload)
261+
self.aux_sec_header.remove_payload()
262+
if mic_length > 0 and mic_length <= len(self.sec_payload):
263+
self.mic = self.sec_payload[-mic_length:]
264+
self.sec_payload = self.sec_payload[:-mic_length]
265+
return s
266+
239267
def guess_payload_class(self, payload):
240268
# TODO: See how it's done in wireshark:
241269
# https://github.com/wireshark/wireshark/blob/93c60b3b7c801dddd11d8c7f2a0ea4b7d02d700a/epan/dissectors/packet-ieee802154.c#L2061 # noqa: E501
@@ -268,7 +296,7 @@ class Dot15d4Beacon(Packet):
268296
dot15d4AddressField("src_addr", None, length_of="fcf_srcaddrmode"),
269297
# Security field present if fcf_security == True
270298
ConditionalField(PacketField("aux_sec_header", Dot15d4AuxSecurityHeader(), Dot15d4AuxSecurityHeader), # noqa: E501
271-
lambda pkt:pkt.underlayer.getfieldval("fcf_security") is True), # noqa: E501
299+
lambda pkt:pkt.underlayer.getfieldval("fcf_security")), # noqa: E501
272300

273301
# Superframe spec field:
274302
BitField("sf_sforder", 15, 4), # not used by ZigBee
@@ -303,12 +331,137 @@ class Dot15d4Beacon(Packet):
303331
FieldListField("pa_long_addresses", [],
304332
dot15d4AddressField("", 0, adjust=lambda pkt, x: 8),
305333
count_from=lambda pkt: pkt.pa_num_long),
306-
# TODO beacon payload
334+
# Secured Payload (variable length)
335+
ConditionalField(
336+
XStrField("sec_payload", ""),
337+
lambda pkt:pkt.underlayer.getfieldval("fcf_security"),
338+
),
339+
# Message Integrity Code (variable length)
340+
ConditionalField(
341+
XStrField("mic", ""),
342+
lambda pkt:pkt.underlayer.getfieldval("fcf_security"),
343+
),
307344
]
308345

346+
def post_dissect(self, s):
347+
if self.underlayer.getfieldval("fcf_security"):
348+
if self.aux_sec_header.sec_sc_seclevel in {1, 5}:
349+
mic_length = 4
350+
elif self.aux_sec_header.sec_sc_seclevel in {2, 6}:
351+
mic_length = 8
352+
elif self.aux_sec_header.sec_sc_seclevel in {3, 7}:
353+
mic_length = 16
354+
else:
355+
mic_length = 0
356+
self.sec_payload = bytes(self.aux_sec_header.payload)
357+
self.aux_sec_header.remove_payload()
358+
if mic_length > 0 and mic_length <= len(self.sec_payload):
359+
self.mic = self.sec_payload[-mic_length:]
360+
self.sec_payload = self.sec_payload[:-mic_length]
361+
i = 0
362+
if len(self.sec_payload) > i:
363+
if isinstance(self.sec_payload[i], str):
364+
tmp_val = struct.unpack(">B", self.sec_payload[i])[0]
365+
else:
366+
tmp_val = self.sec_payload[i]
367+
self.sf_sforder = (tmp_val >> 4) & 0b1111
368+
self.sf_beaconorder = tmp_val & 0b1111
369+
i += 1
370+
if len(self.sec_payload) > i:
371+
if isinstance(self.sec_payload[i], str):
372+
tmp_val = struct.unpack(">B", self.sec_payload[i])[0]
373+
else:
374+
tmp_val = self.sec_payload[i]
375+
self.sf_assocpermit = (tmp_val >> 7) & 0b1
376+
self.sf_pancoord = (tmp_val >> 6) & 0b1
377+
self.sf_reserved = (tmp_val >> 5) & 0b1
378+
self.sf_battlifeextend = (tmp_val >> 4) & 0b1
379+
self.sf_finalcapslot = tmp_val & 0b1111
380+
i += 1
381+
if len(self.sec_payload) > i:
382+
if isinstance(self.sec_payload[i], str):
383+
tmp_val = struct.unpack(">B", self.sec_payload[i])[0]
384+
else:
385+
tmp_val = self.sec_payload[i]
386+
self.gts_spec_permit = (tmp_val >> 7) & 0b1
387+
self.gts_spec_reserved = (tmp_val >> 3) & 0b1111
388+
self.gts_spec_desccount = tmp_val & 0b111
389+
i += 1
390+
if self.gts_spec_desccount != 0 and len(self.sec_payload) > i:
391+
if isinstance(self.sec_payload[i], str):
392+
tmp_val = struct.unpack(">B", self.sec_payload[i])[0]
393+
else:
394+
tmp_val = self.sec_payload[i]
395+
self.gts_dir_reserved = (tmp_val >> 7) & 0b1
396+
self.gts_dir_mask = tmp_val & 0b1111111
397+
i += 1
398+
# TODO: Update the GTS List field
399+
if len(self.sec_payload) > i:
400+
if isinstance(self.sec_payload[i], str):
401+
tmp_val = struct.unpack(">B", self.sec_payload[i])[0]
402+
else:
403+
tmp_val = self.sec_payload[i]
404+
self.pa_reserved_1 = (tmp_val >> 7) & 0b1
405+
self.pa_num_long = (tmp_val >> 4) & 0b111
406+
self.pa_reserved_2 = (tmp_val >> 3) & 0b1
407+
self.pa_num_short = tmp_val & 0b111
408+
i += 1
409+
for _ in range(self.pa_num_short):
410+
if len(self.sec_payload) > i + 1:
411+
if isinstance(self.sec_payload[i], str):
412+
tmp_val = (
413+
struct.unpack("<H", self.sec_payload[i:i + 2])[0]
414+
)
415+
else:
416+
tmp_val = (
417+
self.sec_payload[i] +
418+
(self.sec_payload[i + 1] << 8)
419+
)
420+
self.pa_short_addresses.append(tmp_val)
421+
i += 2
422+
for _ in range(self.pa_num_long):
423+
if len(self.sec_payload) > i + 7:
424+
if isinstance(self.sec_payload[i], str):
425+
tmp_val = (
426+
struct.unpack("<Q", self.sec_payload[i:i + 8])[0]
427+
)
428+
else:
429+
tmp_val = (
430+
self.sec_payload[i] +
431+
(self.sec_payload[i + 1] << 8) +
432+
(self.sec_payload[i + 2] << 16) +
433+
(self.sec_payload[i + 3] << 24) +
434+
(self.sec_payload[i + 4] << 32) +
435+
(self.sec_payload[i + 5] << 40) +
436+
(self.sec_payload[i + 6] << 48) +
437+
(self.sec_payload[i + 7] << 56)
438+
)
439+
self.pa_long_addresses.append(tmp_val)
440+
i += 8
441+
self.sec_payload = self.sec_payload[i:]
442+
return s
443+
309444
def mysummary(self):
310445
return self.sprintf("802.15.4 Beacon ( %Dot15d4Beacon.src_panid%:%Dot15d4Beacon.src_addr% ) assocPermit(%Dot15d4Beacon.sf_assocpermit%) panCoord(%Dot15d4Beacon.sf_pancoord%)") # noqa: E501
311446

447+
def guess_payload_class(self, payload):
448+
from scapy.layers.sixlowpan import ThreadBeacon
449+
from scapy.layers.zigbee import ZigBeeBeacon
450+
if len(payload) > 0:
451+
if (
452+
(isinstance(payload[0], int) and payload[0] == 0x03) or
453+
(isinstance(payload[0], bytes) and payload[0] == b'\x03')
454+
):
455+
# https://gitlab.com/wireshark/wireshark/-/blob/5ecb57cb9026cebf0cfa4918c4a86942620c5ecf/epan/dissectors/packet-thread.c#L2223-2225 # noqa: E501
456+
return ThreadBeacon
457+
elif (
458+
(isinstance(payload[0], int) and payload[0] == 0x00) or
459+
(isinstance(payload[0], bytes) and payload[0] == b'\x00')
460+
):
461+
# https://gitlab.com/wireshark/wireshark/-/blob/5ecb57cb9026cebf0cfa4918c4a86942620c5ecf/epan/dissectors/packet-zbee-nwk.c#L1507-1509 # noqa: E501
462+
return ZigBeeBeacon
463+
return Packet.guess_payload_class(self, payload)
464+
312465

313466
class Dot15d4Cmd(Packet):
314467
name = "802.15.4 Command"
@@ -323,7 +476,7 @@ class Dot15d4Cmd(Packet):
323476
lambda pkt:pkt.underlayer.getfieldval("fcf_srcaddrmode") != 0), # noqa: E501
324477
# Security field present if fcf_security == True
325478
ConditionalField(PacketField("aux_sec_header", Dot15d4AuxSecurityHeader(), Dot15d4AuxSecurityHeader), # noqa: E501
326-
lambda pkt:pkt.underlayer.getfieldval("fcf_security") is True), # noqa: E501
479+
lambda pkt:pkt.underlayer.getfieldval("fcf_security")), # noqa: E501
327480
ByteEnumField("cmd_id", 0, {
328481
1: "AssocReq", # Association request
329482
2: "AssocResp", # Association response
@@ -336,9 +489,41 @@ class Dot15d4Cmd(Packet):
336489
9: "GTSReq" # GTS request
337490
# 0x0a - 0xff reserved
338491
}),
339-
# TODO command payload
492+
# Secured Payload (variable length)
493+
ConditionalField(
494+
XStrField("sec_payload", ""),
495+
lambda pkt: pkt.underlayer.getfieldval("fcf_security"),
496+
),
497+
# Message Integrity Code (variable length)
498+
ConditionalField(
499+
XStrField("mic", ""),
500+
lambda pkt: pkt.underlayer.getfieldval("fcf_security"),
501+
),
340502
]
341503

504+
def post_dissect(self, s):
505+
if self.underlayer.getfieldval("fcf_security"):
506+
if self.aux_sec_header.sec_sc_seclevel in {1, 5}:
507+
mic_length = 4
508+
elif self.aux_sec_header.sec_sc_seclevel in {2, 6}:
509+
mic_length = 8
510+
elif self.aux_sec_header.sec_sc_seclevel in {3, 7}:
511+
mic_length = 16
512+
else:
513+
mic_length = 0
514+
self.sec_payload = bytes(self.aux_sec_header.payload)
515+
self.aux_sec_header.remove_payload()
516+
if mic_length > 0 and mic_length <= len(self.sec_payload):
517+
self.mic = self.sec_payload[-mic_length:]
518+
self.sec_payload = self.sec_payload[:-mic_length]
519+
if len(self.sec_payload) > 0:
520+
if isinstance(self.sec_payload[0], str):
521+
self.cmd_id = struct.unpack(">B", self.sec_payload[0])[0]
522+
else:
523+
self.cmd_id = self.sec_payload[0]
524+
self.sec_payload = self.sec_payload[1:]
525+
return s
526+
342527
def mysummary(self):
343528
return self.sprintf("802.15.4 Command %Dot15d4Cmd.cmd_id% ( %Dot15dCmd.src_panid%:%Dot15d4Cmd.src_addr% -> %Dot15d4Cmd.dest_panid%:%Dot15d4Cmd.dest_addr% )") # noqa: E501
344529

scapy/layers/mle.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# This file is part of Scapy
2+
# See http://www.secdev.org/projects/scapy for more information
3+
# Copyright (C) 2022 Dimitrios-Georgios Akestoridis <[email protected]>
4+
# This program is published under a GPLv2 license
5+
6+
"""
7+
MLE (Mesh Link Establishment)
8+
=============================
9+
10+
For more information, please refer to the following files:
11+
12+
* https://datatracker.ietf.org/doc/html/draft-ietf-6lo-mesh-link-establishment-00
13+
14+
* https://doi.org/10.1109/IEEESTD.2006.232110
15+
16+
* https://gitlab.com/wireshark/wireshark/-/blob/5ecb57cb9026cebf0cfa4918c4a86942620c5ecf/epan/dissectors/packet-mle.c
17+
18+
""" # noqa: E501
19+
20+
from scapy.fields import (
21+
ByteEnumField,
22+
ConditionalField,
23+
PacketField,
24+
XStrField,
25+
)
26+
from scapy.layers.dot15d4 import Dot15d4AuxSecurityHeader
27+
from scapy.layers.inet import UDP
28+
from scapy.packet import (
29+
Packet,
30+
bind_layers,
31+
)
32+
33+
34+
class MLE(Packet):
35+
name = "Mesh Link Establishment"
36+
fields_desc = [
37+
ByteEnumField(
38+
"sec_suite",
39+
0,
40+
{
41+
0: "IEEE 802.15.4 Security (0)",
42+
255: "No Security (255)",
43+
},
44+
),
45+
ConditionalField(
46+
PacketField(
47+
"aux_sec_header",
48+
Dot15d4AuxSecurityHeader(),
49+
Dot15d4AuxSecurityHeader,
50+
),
51+
lambda pkt: pkt.sec_suite == 0,
52+
),
53+
ConditionalField(
54+
XStrField("sec_payload", ""),
55+
lambda pkt: pkt.sec_suite == 0,
56+
),
57+
ConditionalField(
58+
XStrField("mic", ""),
59+
lambda pkt: pkt.sec_suite == 0,
60+
),
61+
]
62+
63+
def post_dissect(self, s):
64+
if self.sec_suite == 0:
65+
if self.aux_sec_header.sec_sc_seclevel in {1, 5}:
66+
mic_length = 4
67+
elif self.aux_sec_header.sec_sc_seclevel in {2, 6}:
68+
mic_length = 8
69+
elif self.aux_sec_header.sec_sc_seclevel in {3, 7}:
70+
mic_length = 16
71+
else:
72+
mic_length = 0
73+
self.sec_payload = bytes(self.aux_sec_header.payload)
74+
self.aux_sec_header.remove_payload()
75+
if mic_length > 0 and mic_length <= len(self.sec_payload):
76+
self.mic = self.sec_payload[-mic_length:]
77+
self.sec_payload = self.sec_payload[:-mic_length]
78+
return s
79+
80+
def guess_payload_class(self, payload):
81+
if self.sec_suite == 255:
82+
return MLECmd
83+
return Packet.guess_payload_class(self, payload)
84+
85+
86+
class MLECmd(Packet):
87+
name = "Mesh Link Establishment Command"
88+
fields_desc = [
89+
ByteEnumField(
90+
"cmd_type",
91+
0,
92+
{
93+
0: "Link Request (0)",
94+
1: "Link Accept (1)",
95+
2: "Link Accept and Request (2)",
96+
3: "Link Reject (3)",
97+
4: "Advertisement (4)",
98+
5: "Update (5)",
99+
6: "Update Request (6)",
100+
7: "Data Request (7)",
101+
8: "Data Response (8)",
102+
9: "Parent Request (9)",
103+
10: "Parent Response (10)",
104+
11: "Child ID Request (11)",
105+
12: "Child ID Response (12)",
106+
13: "Child Update Request (13)",
107+
14: "Child Update Response (14)",
108+
15: "Announce (15)",
109+
16: "Discovery Request (16)",
110+
17: "Discovery Response (17)",
111+
},
112+
),
113+
# TODO: Dissect the command payload
114+
]
115+
116+
117+
bind_layers(UDP, MLE, sport=19788, dport=19788)

0 commit comments

Comments
 (0)