From 7b4e34e149f09b4e588919e58ac2472ae4777277 Mon Sep 17 00:00:00 2001 From: Pierre Lalet Date: Sun, 14 May 2023 22:08:19 +0200 Subject: [PATCH 001/122] 802.1q: rename .id field to .dei --- scapy/layers/l2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py index c0da4e4d324..e7973a1cda8 100644 --- a/scapy/layers/l2.py +++ b/scapy/layers/l2.py @@ -371,9 +371,12 @@ class Dot1Q(Packet): name = "802.1Q" aliastypes = [Ether] fields_desc = [BitField("prio", 0, 3), - BitField("id", 0, 1), + BitField("dei", 0, 1), BitField("vlan", 1, 12), XShortEnumField("type", 0x0000, ETHER_TYPES)] + deprecated_fields = { + "id": ("dei", "2.5.0"), + } def answers(self, other): # type: (Packet) -> int From bed85cbb8d020dc4ceb70571f3457bebec5154fb Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Sat, 20 May 2023 16:47:43 +0300 Subject: [PATCH 002/122] Recognize DNS cookies (#4014) It's mostly prompted by relatively new dig and delv sending cookies by default (unless `+nocookie`/`+noedns`/... is specified explicitly). https://datatracker.ietf.org/doc/html/rfc7873#section-4 --- scapy/layers/dns.py | 2 +- test/scapy/layers/dns_edns0.uts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 9dc1a19d9ff..1244ca130a7 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -541,7 +541,7 @@ def pre_dissect(self, s): # RFC 2671 - Extension Mechanisms for DNS (EDNS0) edns0types = {0: "Reserved", 1: "LLQ", 2: "UL", 3: "NSID", 4: "Reserved", - 5: "PING", 8: "edns-client-subnet"} + 5: "PING", 8: "edns-client-subnet", 10: "COOKIE"} class EDNS0TLV(Packet): diff --git a/test/scapy/layers/dns_edns0.uts b/test/scapy/layers/dns_edns0.uts index 143957db38e..d4893aac0ab 100644 --- a/test/scapy/layers/dns_edns0.uts +++ b/test/scapy/layers/dns_edns0.uts @@ -54,6 +54,13 @@ raw(tlv) == b'\x00\x05\x00\x04\x00\x11"3' #conf.debug_dissector = old_debug_dissector #len(r.ar) and r.ar.rdata[0].optcode == 4 # XXX: should be 5 ++ Test EDNS-COOKIE + += EDNS-COOKIE - basic instantiation +tlv = EDNS0TLV(optcode="COOKIE", optdata=b"\x01" * 8) +assert tlv.optcode == 10 +assert raw(tlv) == b"\x00\x0A\x00\x08\x01\x01\x01\x01\x01\x01\x01\x01" + + Test DNS Name Server Identifier (NSID) Option = NSID- basic instantiation From 7f89ce51c09417c2aa9d78d3e5c75091cacd4bff Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Mon, 22 May 2023 15:57:47 +0300 Subject: [PATCH 003/122] Tolerate non-UTF-8 NetBIOS names in NetBIOS answering machines (#4007) NetBIOS names encoded using non-UTF-8 encodings can't be decoded without specifying them explicitly (and for that to work they have to be inferred based on bytes). It's kind of possible but instead of guessing encodings `is_request` uses raw question names and `server_name` is converted to bytes instead. To make it possible to spoof all sorts of hosts NBNS_am also accepts bytes as server_names directly (but the patch is backward-compatible with the previous versions accepting strings only). Empty strings and Nones are still equivalent and keep working too. Fixes: ``` File "scapy/ansmachine.py", line 57, in File "scapy/ansmachine.py", line 211, in __call__ File "scapy/ansmachine.py", line 217, in sniff File "scapy/sendrecv.py", line 1311, in sniff File "scapy/sendrecv.py", line 1254, in _run File "scapy/sessions.py", line 109, in on_packet_received File "scapy/ansmachine.py", line 167, in reply File "scapy/layers/netbios.py", line 381, in is_request req[NBNSQueryRequest].QUESTION_NAME.decode().strip() == ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf0 in position 0: invalid continuation byte ``` --- scapy/layers/netbios.py | 6 +++--- test/answering_machines.uts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/scapy/layers/netbios.py b/scapy/layers/netbios.py index 5986528784a..678fa6c4f7c 100644 --- a/scapy/layers/netbios.py +++ b/scapy/layers/netbios.py @@ -13,6 +13,7 @@ from scapy.arch import get_if_addr from scapy.base_classes import Net from scapy.ansmachine import AnsweringMachine +from scapy.compat import bytes_encode from scapy.config import conf from scapy.packet import Packet, bind_bottom_up, bind_layers, bind_top_down @@ -366,7 +367,7 @@ def parse_options(self, server_name=None, from_ip=None, ip=None): :param from_ip: an IP (can have a netmask) to filter on :param ip: the IP to answer with """ - self.ServerName = server_name + self.ServerName = bytes_encode(server_name or "") self.ip = ip if isinstance(from_ip, str): self.from_ip = Net(from_ip) @@ -378,8 +379,7 @@ def is_request(self, req): return False return NBNSQueryRequest in req and ( not self.ServerName or - req[NBNSQueryRequest].QUESTION_NAME.decode().strip() == - self.ServerName + req[NBNSQueryRequest].QUESTION_NAME.strip() == self.ServerName ) def make_reply(self, req): diff --git a/test/answering_machines.uts b/test/answering_machines.uts index 22a0b4a4b81..54eee4a705c 100644 --- a/test/answering_machines.uts +++ b/test/answering_machines.uts @@ -133,3 +133,21 @@ def check_WiFi_am_reply(packet): test_WiFi_am(Dot11(FCfield="to-DS")/IP()/TCP()/"Scapy", check_WiFi_am_reply, iffrom="scapy0", ifto="scapy1", replace="5c4pY", pattern="Scapy") + + += NBNS_am +def check_NBNS_am_reply(name): + def check(packet): + assert NBNSQueryResponse in packet and packet[NBNSQueryResponse].RR_NAME == bytes_encode(name) + return check + +for server_name in (None, "", b"test", "test"): + test_am(NBNS_am, + Ether()/IP()/UDP()/NBNSHeader()/NBNSQueryRequest(QUESTION_NAME="test"), + check_NBNS_am_reply("test"), + server_name=server_name) + +test_am(NBNS_am, + Ether()/IP()/UDP()/NBNSHeader()/NBNSQueryRequest(QUESTION_NAME=b"\x85"), + check_NBNS_am_reply(b"\x85"), + server_name=b"\x85") From 8538f5082d4de200c2f3c934fba79caab3253d32 Mon Sep 17 00:00:00 2001 From: Guillaume Valadon Date: Tue, 30 May 2023 00:59:50 +0200 Subject: [PATCH 004/122] Force the source MAC address in neighsol() (#4020) --- scapy/layers/inet6.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py index 67393b78b26..125e57ec272 100644 --- a/scapy/layers/inet6.py +++ b/scapy/layers/inet6.py @@ -79,7 +79,9 @@ def neighsol(addr, src, iface, timeout=1, chainCC=0): This function sends an ICMPv6 Neighbor Solicitation message to get the MAC address of the neighbor with specified IPv6 address address. - 'src' address is used as source of the message. Message is sent on iface. + 'src' address is used as the source IPv6 address of the message. Message + is sent on 'iface'. The source MAC address is retrieved accordingly. + By default, timeout waiting for an answer is 1 second. If no answer is gathered, None is returned. Else, the answer is @@ -89,9 +91,10 @@ def neighsol(addr, src, iface, timeout=1, chainCC=0): nsma = in6_getnsma(inet_pton(socket.AF_INET6, addr)) d = inet_ntop(socket.AF_INET6, nsma) dm = in6_getnsmac(nsma) - p = Ether(dst=dm) / IPv6(dst=d, src=src, hlim=255) + sm = get_if_hwaddr(iface) + p = Ether(dst=dm, src=sm) / IPv6(dst=d, src=src, hlim=255) p /= ICMPv6ND_NS(tgt=addr) - p /= ICMPv6NDOptSrcLLAddr(lladdr=get_if_hwaddr(iface)) + p /= ICMPv6NDOptSrcLLAddr(lladdr=sm) res = srp1(p, type=ETH_P_IPV6, iface=iface, timeout=1, verbose=0, chainCC=chainCC) From 5b90dbd0c6b8b9956518b989aff392b09b75a80b Mon Sep 17 00:00:00 2001 From: Luke Valenta Date: Tue, 23 May 2023 08:40:07 -0400 Subject: [PATCH 005/122] Add crypto_validator decorator to TLS13_HKDF methods to fix #4005 Trigger an ImportError on calls to any TLS13_HKDF methods if the 'cryptography' module is missing. --- scapy/layers/tls/crypto/hkdf.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scapy/layers/tls/crypto/hkdf.py b/scapy/layers/tls/crypto/hkdf.py index 28cb002ce43..a4a3edfa9d0 100644 --- a/scapy/layers/tls/crypto/hkdf.py +++ b/scapy/layers/tls/crypto/hkdf.py @@ -9,7 +9,7 @@ import struct -from scapy.config import conf +from scapy.config import conf, crypto_validator from scapy.layers.tls.crypto.pkcs1 import _get_hash if conf.crypto_valid: @@ -20,9 +20,11 @@ class TLS13_HKDF(object): + @crypto_validator def __init__(self, hash_name="sha256"): self.hash = _get_hash(hash_name) + @crypto_validator def extract(self, salt, ikm): h = self.hash hkdf = HKDF(h, h.digest_size, salt, None, default_backend()) @@ -30,11 +32,13 @@ def extract(self, salt, ikm): ikm = b"\x00" * h.digest_size return hkdf._extract(ikm) + @crypto_validator def expand(self, prk, info, L): h = self.hash hkdf = HKDFExpand(h, L, info, default_backend()) return hkdf.derive(prk) + @crypto_validator def expand_label(self, secret, label, hash_value, length): hkdf_label = struct.pack("!H", length) hkdf_label += struct.pack("B", 6 + len(label)) @@ -44,6 +48,7 @@ def expand_label(self, secret, label, hash_value, length): hkdf_label += hash_value return self.expand(secret, hkdf_label, length) + @crypto_validator def derive_secret(self, secret, label, messages): h = Hash(self.hash, backend=default_backend()) h.update(messages) @@ -51,6 +56,7 @@ def derive_secret(self, secret, label, messages): hash_len = self.hash.digest_size return self.expand_label(secret, label, hash_messages, hash_len) + @crypto_validator def compute_verify_data(self, basekey, handshake_context): hash_len = self.hash.digest_size finished_key = self.expand_label(basekey, b"finished", b"", hash_len) From 3da71cb275721292131a3ea5196bfa98883741f2 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 25 May 2023 20:52:44 +0200 Subject: [PATCH 006/122] fix depricated argument in python-can --- scapy/contrib/cansocket_python_can.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scapy/contrib/cansocket_python_can.py b/scapy/contrib/cansocket_python_can.py index 89dbb6bb0cf..baf4a6cbd74 100644 --- a/scapy/contrib/cansocket_python_can.py +++ b/scapy/contrib/cansocket_python_can.py @@ -151,8 +151,12 @@ def register(self, socket, *args, **kwargs): :param args: Arguments for the python-can Bus object :param kwargs: Keyword arguments for the python-can Bus object """ - k = str(kwargs.get("bustype", "unknown_bustype")) + "_" + \ - str(kwargs.get("channel", "unknown_channel")) + if "interface" in kwargs.keys(): + k = str(kwargs.get("interface", "unknown_interface")) + "_" + \ + str(kwargs.get("channel", "unknown_channel")) + else: + k = str(kwargs.get("bustype", "unknown_bustype")) + "_" + \ + str(kwargs.get("channel", "unknown_channel")) with self.pool_mutex: if k in self.pool: t = self.pool[k] From a45836f97ef61baa2f17590d19c710414af6b9ef Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 4 Jun 2023 22:21:32 -0400 Subject: [PATCH 007/122] Import InvalidSignature from the correct location --- scapy/layers/tls/cert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index 5eab98fb04a..ed934e8b17f 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -45,10 +45,10 @@ _EncryptAndVerifyRSA, _DecryptAndSignRSA from scapy.compat import raw, bytes_encode if conf.crypto_valid: + from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa, ec - from cryptography.hazmat.backends.openssl.ec import InvalidSignature # Maximum allowed size in bytes for a certificate file, to avoid From cabf209b08b3efce0ca9d5cca0bb68c83ac470a6 Mon Sep 17 00:00:00 2001 From: "Matthias St. Pierre" Date: Wed, 3 May 2023 00:04:38 +0200 Subject: [PATCH 008/122] IKEv2: fix length calculation for IKEv2_Notify payloads with SPI As a testcase, add a decrypted CREATE_CHILD_SA request for an ESP child sa, which contains a REKEY_SA Notify payload. --- scapy/contrib/ikev2.py | 3 +- test/contrib/ikev2.uts | 115 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/scapy/contrib/ikev2.py b/scapy/contrib/ikev2.py index 0912d6b741d..d4fc1e03863 100644 --- a/scapy/contrib/ikev2.py +++ b/scapy/contrib/ikev2.py @@ -748,7 +748,8 @@ class IKEv2_Notify(IKEv2_Payload): ShortEnumField("type", 0, IKEv2NotifyMessageTypes), XStrLenField("SPI", "", length_from=lambda pkt: pkt.SPIsize), ConditionalField( - XStrLenField("notify", "", length_from=lambda pkt: pkt.length - 8), + XStrLenField("notify", "", + length_from=lambda pkt: pkt.length - 8 - pkt.SPIsize), lambda pkt: pkt.type not in (16407, 16408) ), ConditionalField( diff --git a/test/contrib/ikev2.uts b/test/contrib/ikev2.uts index 7257ece2741..db503867fc1 100644 --- a/test/contrib/ikev2.uts +++ b/test/contrib/ikev2.uts @@ -1320,6 +1320,121 @@ frames = [ type='MOBIKE_SUPPORTED' ) ), + # CREATE_CHILD_SA request, decrypted + ( + # i: frame number + -4, + # title: + "CREATE_CHILD_SA request, decrypted", + binascii.unhexlify(''.join(""" + 00 50 56 99 bf d5 00 50 56 99 69 93 08 00 45 00 + 01 38 60 32 40 00 40 11 c1 0f 0a 05 02 36 0a 05 + 02 34 b8 99 11 94 01 24 19 a9 00 00 00 00 46 b3 + f6 88 4d 37 5f 9a f5 38 82 35 ea 87 5e 8a 29 20 + 24 00 00 00 00 00 00 00 01 18 + + 21 00 00 0c 03 04 40 09 5f c7 ff 5a 28 00 00 2c + 00 00 00 28 01 03 04 03 6b 21 88 20 03 00 00 0c + 01 00 00 14 80 0e 00 80 03 00 00 08 04 00 00 1c + 00 00 00 08 05 00 00 00 22 00 00 2c ea 7e 88 57 + 4a 36 64 cd 67 e3 3c 42 46 66 59 4d df 70 25 03 + b2 00 a3 3f 87 82 f2 3c 94 c0 60 0e ae 7e d9 50 + d7 67 e9 6e 2c 00 00 48 00 1c 00 00 8e 15 b1 f4 + 9a cc 04 ff 12 e3 2f bc 3a f0 57 14 81 f3 b9 6c + 21 1a f7 36 97 6d c2 23 80 74 ef 75 59 d1 99 65 + 5a a5 80 00 87 4a bf 1f 13 f7 e1 6f de 34 80 94 + 28 1c 93 cb 5a ee 30 24 d9 3e b9 55 2d 00 00 18 + 01 00 00 00 07 00 00 10 00 00 ff ff c0 a8 e1 0b + c0 a8 e1 0b 00 00 00 18 01 00 00 00 07 00 00 10 + 00 00 ff ff c0 a8 e1 00 c0 a8 e1 ff + """.split())), + Ether(dst='00:50:56:99:bf:d5', src='00:50:56:99:69:93', type=2048) /\ + IP(version=4, ihl=5, tos=0, len=312, id=24626, flags=2, frag=0, ttl=64, proto=17, chksum=49423, src='10.5.2.54', dst='10.5.2.52') /\ + UDP(sport=47257, dport=4500, len=292, chksum=6569) /\ + NON_ESP(non_esp=0) /\ + IKEv2( + init_SPI=b'F\xb3\xf6\x88M7_\x9a', + resp_SPI=b'\xf58\x825\xea\x87^\x8a', + next_payload=41, + version=32, + exch_type=36, + flags=0, + id=0, + length=280 + ) /\ + IKEv2_Notify( + next_payload=33, + flags=0, + length=12, + proto=3, + SPIsize=4, + type=16393, + SPI=b'_\xc7\xffZ', + notify=b'' + ) /\ + IKEv2_SA( + prop=IKEv2_Proposal( + trans=( + IKEv2_Transform(next_payload=3, flags=0, length=12, transform_type=1, res2=0, transform_id=20, key_length=128) /\ + IKEv2_Transform(next_payload=3, flags=0, length=8, transform_type=4, res2=0, transform_id=28) /\ + IKEv2_Transform(next_payload=0, flags=0, length=8, transform_type=5, res2=0, transform_id=0) + ), + next_payload=0, flags=0, length=40, proposal=1, proto=3, SPIsize=4, trans_nb=3, SPI=b'k!\x88 '), + next_payload=40, + flags=0, + length=44 + ) /\ + IKEv2_Nonce( + next_payload=34, + flags=0, + length=44, + nonce=b'\xea~\x88WJ6d\xcdg\xe3\xb9U' + ) /\ + IKEv2_TSi( + traffic_selector=[ + IPv4TrafficSelector( + TS_type=7, + IP_protocol_ID=0, + length=16, + start_port=0, + end_port=65535, + starting_address_v4='192.168.225.11', + ending_address_v4='192.168.225.11' + ) + ], + next_payload=45, + flags=0, + length=24, + number_of_TSs=1, + res2=0 + ) /\ + IKEv2_TSr( + traffic_selector=[ + IPv4TrafficSelector( + TS_type=7, + IP_protocol_ID=0, + length=16, + start_port=0, + end_port=65535, + starting_address_v4='192.168.225.0', + ending_address_v4='192.168.225.255' + ) + ], + next_payload=0, + flags=0, + length=24, + number_of_TSs=1, + res2=0 + ) + ), ] From 2dcae1bdc95c70a72e442653de6417b554a7935a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Sun, 11 Jun 2023 17:13:16 +0200 Subject: [PATCH 009/122] Implement ISOTP packets for CANFD layer (#4022) * Implement ISOTP packets for CANFD layer * fix flake --------- Co-authored-by: Nils Weiss --- scapy/contrib/isotp/__init__.py | 7 +- scapy/contrib/isotp/isotp_packet.py | 128 ++++++++++++++++++++++++++-- test/contrib/isotp_packet.uts | 81 +++++++++++++++--- 3 files changed, 197 insertions(+), 19 deletions(-) diff --git a/scapy/contrib/isotp/__init__.py b/scapy/contrib/isotp/__init__.py index d22b84cc764..142b381ed27 100644 --- a/scapy/contrib/isotp/__init__.py +++ b/scapy/contrib/isotp/__init__.py @@ -13,14 +13,17 @@ from scapy.error import log_loading from scapy.contrib.isotp.isotp_packet import ISOTP, ISOTPHeader, \ - ISOTPHeaderEA, ISOTP_SF, ISOTP_FF, ISOTP_CF, ISOTP_FC + ISOTPHeaderEA, ISOTP_SF, ISOTP_FF, ISOTP_CF, ISOTP_FC, \ + ISOTP_FF_FD, ISOTP_SF_FD, ISOTPHeaderEA_FD, ISOTPHeader_FD from scapy.contrib.isotp.isotp_utils import ISOTPSession, \ ISOTPMessageBuilder from scapy.contrib.isotp.isotp_soft_socket import ISOTPSoftSocket from scapy.contrib.isotp.isotp_scanner import isotp_scan __all__ = ["ISOTP", "ISOTPHeader", "ISOTPHeaderEA", "ISOTP_SF", "ISOTP_FF", - "ISOTP_CF", "ISOTP_FC", "ISOTPSoftSocket", "ISOTPSession", + "ISOTP_CF", "ISOTP_FC", "ISOTP_FF_FD", "ISOTP_SF_FD", + "ISOTPSoftSocket", "ISOTPSession", "ISOTPHeader_FD", + "ISOTPHeaderEA_FD", "ISOTPSocket", "ISOTPMessageBuilder", "isotp_scan", "USE_CAN_ISOTP_KERNEL_MODULE", "log_isotp"] diff --git a/scapy/contrib/isotp/isotp_packet.py b/scapy/contrib/isotp/isotp_packet.py index 391b62ec28c..2eb8a6ff149 100644 --- a/scapy/contrib/isotp/isotp_packet.py +++ b/scapy/contrib/isotp/isotp_packet.py @@ -9,10 +9,12 @@ import struct import logging +from scapy.config import conf from scapy.packet import Packet from scapy.fields import BitField, FlagsField, StrLenField, \ ThreeBytesField, XBitField, ConditionalField, \ - BitEnumField, ByteField, XByteField, BitFieldLenField, StrField + BitEnumField, ByteField, XByteField, BitFieldLenField, StrField, \ + FieldLenField, IntField, ShortField from scapy.compat import chb, orb from scapy.layers.can import CAN from scapy.error import Scapy_Exception @@ -217,6 +219,10 @@ def post_build(self, pkt, pay): """ if self.length is None: pkt = pkt[:4] + chb(len(pay)) + pkt[5:] + + if conf.contribs['CAN']['swap-bytes']: + data = CAN.inv_endianness(pkt) # type: bytes + return data + pay return pkt + pay def guess_payload_class(self, payload): @@ -227,17 +233,65 @@ def guess_payload_class(self, payload): :param payload: payload bytes string :return: Type of payload class """ + if len(payload) < 1: + return self.default_payload_class(payload) + t = (orb(payload[0]) & 0xf0) >> 4 if t == 0: - return ISOTP_SF + length = (orb(payload[0]) & 0x0f) + if length == 0: + return ISOTP_SF_FD + else: + return ISOTP_SF elif t == 1: - return ISOTP_FF + if len(payload) < 2: + return self.default_payload_class(payload) + length = ((orb(payload[0]) & 0x0f) << 12) + orb(payload[1]) + if length == 0: + return ISOTP_FF_FD + else: + return ISOTP_FF elif t == 2: return ISOTP_CF else: return ISOTP_FC +class ISOTPHeader_FD(ISOTPHeader): + name = 'ISOTPHeaderFD' + fields_desc = [ + FlagsField('flags', 0, 3, ['error', + 'remote_transmission_request', + 'extended']), + XBitField('identifier', 0, 29), + ByteField('length', None), + FlagsField('fd_flags', 4, 8, ['bit_rate_switch', + 'error_state_indicator', + 'fd_frame']), + ShortField('reserved', 0), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + + data = super().post_build(pkt, pay) + + length = data[4] + + if 8 < length <= 24: + wire_length = length + (-length) % 4 + elif 24 < length <= 64: + wire_length = length + (-length) % 8 + elif length > 64: + raise NotImplementedError + else: + wire_length = length + + pad = b"\x00" * (wire_length - length) + + return data[0:4] + chb(wire_length) + data[5:] + pad + + class ISOTPHeaderEA(ISOTPHeader): name = 'ISOTPHeaderExtendedAddress' fields_desc = [ @@ -250,7 +304,7 @@ class ISOTPHeaderEA(ISOTPHeader): XByteField('extended_address', 0) ] - def post_build(self, p, pay): + def post_build(self, pkt, pay): # type: (bytes, bytes) -> bytes """ This will set the ByteField 'length' to the correct value. @@ -258,8 +312,48 @@ def post_build(self, p, pay): is counted as payload on the CAN layer """ if self.length is None: - p = p[:4] + chb(len(pay) + 1) + p[5:] - return p + pay + pkt = pkt[:4] + chb(len(pay) + 1) + pkt[5:] + + if conf.contribs['CAN']['swap-bytes']: + data = CAN.inv_endianness(pkt) # type: bytes + return data + pay + return pkt + pay + + +class ISOTPHeaderEA_FD(ISOTPHeaderEA): + name = 'ISOTPHeaderExtendedAddressFD' + fields_desc = [ + FlagsField('flags', 0, 3, ['error', + 'remote_transmission_request', + 'extended']), + XBitField('identifier', 0, 29), + ByteField('length', None), + FlagsField('fd_flags', 4, 8, ['bit_rate_switch', + 'error_state_indicator', + 'fd_frame']), + ShortField('reserved', 0), + XByteField('extended_address', 0) + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + + data = super().post_build(pkt, pay) + + length = data[4] + + if 8 < length <= 24: + wire_length = length + (-length) % 4 + elif 24 < length <= 64: + wire_length = length + (-length) % 8 + elif length > 64: + raise NotImplementedError + else: + wire_length = length + + pad = b"\x00" * (wire_length - length) + + return data[0:4] + chb(wire_length) + data[5:] + pad ISOTP_TYPE = {0: 'single', @@ -277,17 +371,37 @@ class ISOTP_SF(Packet): ] +class ISOTP_SF_FD(Packet): + name = 'ISOTPSingleFrameFD' + fields_desc = [ + BitEnumField('type', 0, 4, ISOTP_TYPE), + BitField('zero_field', 0, 4), + FieldLenField('message_size', None, length_of='data', fmt="B"), + StrLenField('data', b'', length_from=lambda pkt: pkt.message_size) + ] + + class ISOTP_FF(Packet): name = 'ISOTPFirstFrame' fields_desc = [ BitEnumField('type', 1, 4, ISOTP_TYPE), BitField('message_size', 0, 12), - ConditionalField(BitField('extended_message_size', 0, 32), + ConditionalField(IntField('extended_message_size', 0), lambda pkt: pkt.message_size == 0), StrField('data', b'', fmt="B") ] +class ISOTP_FF_FD(Packet): + name = 'ISOTPFirstFrame' + fields_desc = [ + BitEnumField('type', 1, 4, ISOTP_TYPE), + BitField('zero_field', 0, 12), + IntField('message_size', 0), + StrField('data', b'', fmt="B") + ] + + class ISOTP_CF(Packet): name = 'ISOTPConsecutiveFrame' fields_desc = [ diff --git a/test/contrib/isotp_packet.uts b/test/contrib/isotp_packet.uts index ca490ed1b58..e0225eb09ef 100644 --- a/test/contrib/isotp_packet.uts +++ b/test/contrib/isotp_packet.uts @@ -195,25 +195,19 @@ assert p.type == 1 assert p.identifier == 0 = Build FF frame EA, extended size, with constructor, check for correct length assignments -p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_FF(message_size=0, - extended_message_size=2000, - data=b'\xad'))) +p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_FF_FD(message_size=2000, data=b'\xad'))) assert p.extended_address == 0 assert p.length == 8 -assert p.message_size == 0 -assert p.extended_message_size == 2000 +assert p.message_size == 2000 assert len(p.data) == 1 assert p.data == b'\xad' assert p.type == 1 assert p.identifier == 0 = Build FF frame, extended size, with constructor, check for correct length assignments -p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_FF(message_size=0, - extended_message_size=2000, - data=b'\xad'))) +p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_FF_FD(message_size=2000, data=b'\xad'))) assert p.length == 7 -assert p.message_size == 0 -assert p.extended_message_size == 2000 +assert p.message_size == 2000 assert len(p.data) == 1 assert p.data == b'\xad' assert p.type == 1 @@ -451,3 +445,70 @@ isotpex.show() assert isotpex.data == dhex("AA") assert isotpex.rx_ext_address == 0x02 += Build ISOTP_FF_FD + +pkt = ISOTP_FF_FD(message_size=0xffff0000) +assert bytes(pkt) == bytes.fromhex("1000ffff0000") + += Build ISOTP_SF_FD + +pkt = ISOTP_SF_FD(message_size=0xff) +assert bytes(pkt) == bytes.fromhex("00ff") + += Build ISOTP_FF_FD 2 + +pkt = ISOTPHeaderEA_FD(identifier=0x7ff, extended_address=0xaf)/ISOTP_FF_FD(message_size=0xffff0000) +assert bytes(pkt) == bytes.fromhex("000007ff 07 04 00 00 af 1000ffff0000") + += Build ISOTP_SF_FD 2 + +pkt = ISOTPHeaderEA_FD(identifier=0x7ff, extended_address=0xaf)/ISOTP_SF_FD(message_size=0xff) +assert bytes(pkt) == bytes.fromhex("000007ff 03 04 00 00 af 00ff") + += Build ISOTP_FF_FD 3 + +pkt = ISOTPHeader_FD(identifier=0x7ff)/ISOTP_FF_FD(message_size=0xffff0000) +assert bytes(pkt) == bytes.fromhex("000007ff 06 04 00 00 1000ffff0000") + += Build ISOTP_SF_FD 3 + +pkt = ISOTPHeader_FD(identifier=0x7ff)/ISOTP_SF_FD(message_size=0xff) +assert bytes(pkt) == bytes.fromhex("000007ff 02 04 00 00 00ff") + += Dissect ISOTPFD 1 +pkt = ISOTPHeaderEA_FD(bytes.fromhex("000007ff 07 04 00 00 af 1000ffff0000")) +pkt.show() +sub_pkt = pkt[ISOTP_FF_FD] +assert pkt.identifier == 0x7ff +assert pkt.length == 0x7 +assert pkt.fd_flags == 0x4 +assert pkt.extended_address == 0xaf +assert sub_pkt.message_size == 0xffff0000 + += Dissect ISOTPFD 2 +pkt = ISOTPHeaderEA_FD(bytes.fromhex("000007ff 07 04 00 00 af 00ff00000000")) +pkt.show() +sub_pkt = pkt[ISOTP_SF_FD] +assert pkt.identifier == 0x7ff +assert pkt.length == 0x7 +assert pkt.fd_flags == 0x4 +assert pkt.extended_address == 0xaf +assert sub_pkt.message_size == 0xff + += Dissect ISOTPFD 3 +pkt = ISOTPHeader_FD(bytes.fromhex("000007ff 06 04 00 00 1000ffff0000")) +pkt.show() +sub_pkt = pkt[ISOTP_FF_FD] +assert pkt.identifier == 0x7ff +assert pkt.length == 0x6 +assert pkt.fd_flags == 0x4 +assert sub_pkt.message_size == 0xffff0000 + += Dissect ISOTPFD 4 +pkt = ISOTPHeader_FD(bytes.fromhex("000007ff 06 04 00 00 00ff00000000")) +pkt.show() +sub_pkt = pkt[ISOTP_SF_FD] +assert pkt.identifier == 0x7ff +assert pkt.length == 0x6 +assert pkt.fd_flags == 0x4 +assert sub_pkt.message_size == 0xff \ No newline at end of file From 577ab1f3b2e1f81d1926a0b3dc7b5a0f085ebc1a Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Sun, 11 Jun 2023 18:27:37 +0300 Subject: [PATCH 010/122] Fix FixedPointField.i2h to accept None (#3999) to make it possible for its derived classes to accept None too. For example ntp.TimeStampField has to be able to handle None to calculate timestamps when packets are assembled. Fixes: ```sh orig : TimeStampField (64 bits) = Traceback (most recent call last): File "", line 2, in File "scapy/packet.py", line 2379, in ls print("%-15r" % (getattr(obj, fname),), end=' ') ^^^^^^^^^^^^^^^^^^^ File "scapy/packet.py", line 469, in __getattr__ return v if isinstance(v, RawVal) else fld.i2h(self, v) ^^^^^^^^^^^^^^^^ File "scapy/fields.py", line 3209, in i2h int_part = val >> self.frac_bits ~~~~^^~~~~~~~~~~~~~~~ TypeError: unsupported operand type(s) for >>: 'NoneType' and 'int' ``` Also closes https://github.com/secdev/scapy/issues/3872 --- scapy/fields.py | 4 +++- test/scapy/layers/ntp.uts | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/scapy/fields.py b/scapy/fields.py index d6f632c8dc7..82a8abd7b07 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -3204,8 +3204,10 @@ def any2i(self, pkt, val): return (ival << self.frac_bits) | fract def i2h(self, pkt, val): - # type: (Optional[Packet], int) -> EDecimal + # type: (Optional[Packet], Optional[int]) -> Optional[EDecimal] # A bit of trickery to get precise floats + if val is None: + return val int_part = val >> self.frac_bits pw = 2.0**self.frac_bits frac_part = EDecimal(val & (1 << self.frac_bits) - 1) diff --git a/test/scapy/layers/ntp.uts b/test/scapy/layers/ntp.uts index a76ce4fe3a9..0f9f58a5c9d 100644 --- a/test/scapy/layers/ntp.uts +++ b/test/scapy/layers/ntp.uts @@ -25,18 +25,23 @@ assert not NTPControl in p assert not NTPPrivate in p assert NTP in p assert p.mysummary() == "NTP v4, client" +ls(p) + p = NTPControl() assert not NTPHeader in p assert NTPControl in p assert not NTPPrivate in p assert NTP in p assert p.mysummary() == "NTP v2, NTP control message" +ls(p) + p = NTPPrivate() assert not NTPHeader in p assert not NTPControl in p assert NTPPrivate in p assert NTP in p assert p.mysummary() == "NTP v2, reserved for private use" +ls(p) = NTP - Layers (2) p = NTPHeader() From 92c18f9e60c0959b633589953ce0c53063185ff7 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 17 Jun 2023 01:39:52 +0200 Subject: [PATCH 011/122] Fix grammar for codespell 2.2.5 (#4032) --- .config/codespell_ignore.txt | 4 +++- scapy/pipetool.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt index 0ca5b425fbf..f571d4a6568 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -1,4 +1,3 @@ -Ether aci ans archtypes @@ -11,6 +10,7 @@ delt doas doubleclick ether +ether eventtypes fo gost @@ -20,9 +20,11 @@ microsof mitre nd negociate +optiona ot potatoe referer +requestor ro ser singl diff --git a/scapy/pipetool.py b/scapy/pipetool.py index f3278d2a3d4..0034b66fc68 100644 --- a/scapy/pipetool.py +++ b/scapy/pipetool.py @@ -606,7 +606,7 @@ def send(self, msg): class PeriodicSource(ThreadGenSource): - """Generage messages periodically on low exit: + """Generate messages periodically on low exit: .. code:: From 1fb14adfc16f84ac3d891ae7b1b63111a3427582 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 21 Jun 2023 09:07:19 +0200 Subject: [PATCH 012/122] Added myself to the maintainers --- .github/FUNDING.yml | 2 +- doc/scapy/backmatter.rst | 2 +- pyproject.toml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 70258bbec6e..ade501c1b5e 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [gpotter2, guedou, p-l-] +github: [gpotter2, guedou, p-l-, polybassa] diff --git a/doc/scapy/backmatter.rst b/doc/scapy/backmatter.rst index 326083045e4..5c8df340f32 100644 --- a/doc/scapy/backmatter.rst +++ b/doc/scapy/backmatter.rst @@ -4,7 +4,7 @@ Credits ********* - Philippe Biondi is Scapy's author. He has also written most of the documentation. -- Pierre Lalet, Gabriel Potter, Guillaume Valadon are the current most active maintainers and contributors. +- Pierre Lalet, Gabriel Potter, Guillaume Valadon, Nils Weiss are the current most active maintainers and contributors. - Fred Raynal wrote the chapter on building and dissecting packets. - Peter Kacherginsky contributed several tutorial sections, one-liners and recipes. - Dirk Loss integrated and restructured the existing docs to make this book. diff --git a/pyproject.toml b/pyproject.toml index 1c7fe8ce74c..4550f2e9c91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ maintainers = [ { name="Pierre LALET" }, { name="Gabriel POTTER" }, { name="Guillaume VALADON" }, + { name="Nils Weiss" }, ] license = { text="GPL-2.0-only" } requires-python = ">=3.7, <4" From f359a7e01b65f8516d021f76c6ffa42800234546 Mon Sep 17 00:00:00 2001 From: Gulshan Singh Date: Wed, 21 Jun 2023 13:24:34 -0700 Subject: [PATCH 013/122] Add more HCI command and event packets (#4003) --- scapy/layers/bluetooth.py | 56 +++++++++++++++++++++++++++ test/scapy/layers/bluetooth.uts | 68 ++++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index c98661eb712..46e4458e1e5 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -32,6 +32,7 @@ LEShortField, LenField, MultipleTypeField, + NBytesField, PacketListField, PadField, SignedByteField, @@ -966,12 +967,46 @@ class HCI_Cmd_LE_Set_Scan_Enable(Packet): ByteField("filter_dups", 1), ] +class HCI_Cmd_Create_Connection(Packet): + name = "Create Connection" + fields_desc = [LEMACField("bd_addr", None), + LEShortField("packet_type", 0xcc18), + ByteField("page_scan_repetition_mode", 0x02), + ByteField("reserved", 0x0), + LEShortField("clock_offset", 0x0), + ByteField("allow_role_switch", 0x1), ] + + class HCI_Cmd_Disconnect(Packet): name = "Disconnect" fields_desc = [XLEShortField("handle", 0), ByteField("reason", 0x13), ] +class HCI_Cmd_Link_Key_Request_Reply(Packet): + name = "Link Key Request Reply" + fields_desc = [LEMACField("bd_addr", None), + NBytesField("link_key", None, 16), ] + + +class HCI_Cmd_Authentication_Requested(Packet): + name = "Authentication Requested" + fields_desc = [LEShortField("handle", 0)] + + +class HCI_Cmd_Set_Connection_Encryption(Packet): + name = "Set Connection Encryption" + fields_desc = [LEShortField("handle", 0), ByteField("encryption_enable", 0)] + + +class HCI_Cmd_Remote_Name_Request(Packet): + name = "Remote Name Request" + fields_desc = [LEMACField("bd_addr", None), + ByteField("page_scan_repetition_mode", 0x02), + ByteField("reserved", 0x0), + LEShortField("clock_offset", 0x0), ] + + class HCI_Cmd_LE_Create_Connection(Packet): name = "LE Create Connection" fields_desc = [LEShortField("interval", 96), @@ -1099,6 +1134,13 @@ def answers(self, other): return self.payload.answers(other) +class HCI_Event_Connect_Complete(Packet): + name = "Connect Complete" + fields_desc = [ByteField("status", 0), + LEShortField("handle", 0x0100), + LEMACField("bd_addr", None), ] + + class HCI_Event_Disconnection_Complete(Packet): name = "Disconnection Complete" fields_desc = [ByteEnumField("status", 0, {0: "success"}), @@ -1106,6 +1148,13 @@ class HCI_Event_Disconnection_Complete(Packet): XByteField("reason", 0), ] +class HCI_Event_Remote_Name_Request_Complete(Packet): + name = "Remote Name Request Complete" + fields_desc = [ByteField("status", 0), + LEMACField("bd_addr", None), + StrFixedLenField("remote_name", b"\x00", 248), ] + + class HCI_Event_Encryption_Change(Packet): name = "Encryption Change" fields_desc = [ByteEnumField("status", 0, {0: "change has occurred"}), @@ -1256,7 +1305,12 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertise_Enable, opcode=0x200a) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Parameters, opcode=0x200b) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Enable, opcode=0x200c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Create_Connection, opcode=0x0405) bind_layers(HCI_Command_Hdr, HCI_Cmd_Disconnect, opcode=0x406) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Link_Key_Request_Reply, opcode=0x040b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Authentication_Requested, opcode=0x0411) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Connection_Encryption, opcode=0x0413) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_Name_Request, opcode=0x0419) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection, opcode=0x200d) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection_Cancel, opcode=0x200e) # noqa: E501 bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_White_List_Size, opcode=0x200f) @@ -1274,7 +1328,9 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Reply, opcode=0x201a) # noqa: E501 bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply, opcode=0x201b) # noqa: E501 +bind_layers(HCI_Event_Hdr, HCI_Event_Connect_Complete, code=0x3) bind_layers(HCI_Event_Hdr, HCI_Event_Disconnection_Complete, code=0x5) +bind_layers(HCI_Event_Hdr, HCI_Event_Remote_Name_Request_Complete, code=0x07) bind_layers(HCI_Event_Hdr, HCI_Event_Encryption_Change, code=0x8) bind_layers(HCI_Event_Hdr, HCI_Event_Command_Complete, code=0xe) bind_layers(HCI_Event_Hdr, HCI_Event_Command_Status, code=0xf) diff --git a/test/scapy/layers/bluetooth.uts b/test/scapy/layers/bluetooth.uts index 76d1f959422..43f55ac66cc 100644 --- a/test/scapy/layers/bluetooth.uts +++ b/test/scapy/layers/bluetooth.uts @@ -4,8 +4,9 @@ = HCI layers # a huge packet with all classes in it! -pkt = HCI_ACL_Hdr()/HCI_Cmd_Complete_Read_BD_Addr()/HCI_Cmd_Connect_Accept_Timeout()/HCI_Cmd_Disconnect()/HCI_Cmd_LE_Connection_Update()/HCI_Cmd_LE_Create_Connection()/HCI_Cmd_LE_Create_Connection_Cancel()/HCI_Cmd_LE_Host_Supported()/HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply()/HCI_Cmd_LE_Long_Term_Key_Request_Reply()/HCI_Cmd_LE_Read_Buffer_Size()/HCI_Cmd_LE_Set_Advertise_Enable()/HCI_Cmd_LE_Set_Advertising_Data()/HCI_Cmd_LE_Set_Advertising_Parameters()/HCI_Cmd_LE_Set_Random_Address()/HCI_Cmd_LE_Set_Scan_Enable()/HCI_Cmd_LE_Set_Scan_Parameters()/HCI_Cmd_LE_Start_Encryption_Request()/HCI_Cmd_Read_BD_Addr()/HCI_Cmd_Reset()/HCI_Cmd_Set_Event_Filter()/HCI_Cmd_Set_Event_Mask()/HCI_Command_Hdr()/HCI_Event_Command_Complete()/HCI_Event_Command_Status()/HCI_Event_Disconnection_Complete()/HCI_Event_Encryption_Change()/HCI_Event_Hdr()/HCI_Event_LE_Meta()/HCI_Event_Number_Of_Completed_Packets()/HCI_Hdr()/HCI_LE_Meta_Advertising_Reports()/HCI_LE_Meta_Connection_Complete()/HCI_LE_Meta_Connection_Update_Complete()/HCI_LE_Meta_Long_Term_Key_Request() +pkt = HCI_ACL_Hdr()/HCI_Cmd_Create_Connection()/HCI_Cmd_Complete_Read_BD_Addr()/HCI_Cmd_Connect_Accept_Timeout()/HCI_Cmd_Disconnect()/HCI_Cmd_LE_Connection_Update()/HCI_Cmd_LE_Create_Connection()/HCI_Cmd_LE_Create_Connection_Cancel()/HCI_Cmd_LE_Host_Supported()/HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply()/HCI_Cmd_LE_Long_Term_Key_Request_Reply()/HCI_Cmd_LE_Read_Buffer_Size()/HCI_Cmd_LE_Set_Advertise_Enable()/HCI_Cmd_LE_Set_Advertising_Data()/HCI_Cmd_LE_Set_Advertising_Parameters()/HCI_Cmd_LE_Set_Random_Address()/HCI_Cmd_LE_Set_Scan_Enable()/HCI_Cmd_LE_Set_Scan_Parameters()/HCI_Cmd_LE_Start_Encryption_Request()/HCI_Cmd_Authentication_Requested()/HCI_Cmd_Link_Key_Request_Reply()/HCI_Cmd_Read_BD_Addr()/HCI_Cmd_Remote_Name_Request()/HCI_Cmd_Reset()/HCI_Cmd_Set_Connection_Encryption()/HCI_Cmd_Set_Event_Filter()/HCI_Cmd_Set_Event_Mask()/HCI_Command_Hdr()/HCI_Event_Command_Complete()/HCI_Event_Command_Status()/HCI_Event_Connect_Complete()/HCI_Event_Disconnection_Complete()/HCI_Event_Encryption_Change()/HCI_Event_Remote_Name_Request_Complete()/HCI_Event_Hdr()/HCI_Event_LE_Meta()/HCI_Event_Number_Of_Completed_Packets()/HCI_Hdr()/HCI_LE_Meta_Advertising_Reports()/HCI_LE_Meta_Connection_Complete()/HCI_LE_Meta_Connection_Update_Complete()/HCI_LE_Meta_Long_Term_Key_Request() assert HCI_ACL_Hdr in pkt.layers() +assert HCI_Cmd_Create_Connection in pkt.layers() assert HCI_Cmd_Complete_Read_BD_Addr in pkt.layers() assert HCI_Cmd_Connect_Accept_Timeout in pkt.layers() assert HCI_Cmd_Disconnect in pkt.layers() @@ -23,15 +24,21 @@ assert HCI_Cmd_LE_Set_Random_Address in pkt.layers() assert HCI_Cmd_LE_Set_Scan_Enable in pkt.layers() assert HCI_Cmd_LE_Set_Scan_Parameters in pkt.layers() assert HCI_Cmd_LE_Start_Encryption_Request in pkt.layers() +assert HCI_Cmd_Authentication_Requested in pkt.layers() +assert HCI_Cmd_Link_Key_Request_Reply in pkt.layers() assert HCI_Cmd_Read_BD_Addr in pkt.layers() +assert HCI_Cmd_Remote_Name_Request in pkt.layers() assert HCI_Cmd_Reset in pkt.layers() +assert HCI_Cmd_Set_Connection_Encryption in pkt.layers() assert HCI_Cmd_Set_Event_Filter in pkt.layers() assert HCI_Cmd_Set_Event_Mask in pkt.layers() assert HCI_Command_Hdr in pkt.layers() assert HCI_Event_Command_Complete in pkt.layers() assert HCI_Event_Command_Status in pkt.layers() +assert HCI_Event_Connect_Complete in pkt.layers() assert HCI_Event_Disconnection_Complete in pkt.layers() assert HCI_Event_Encryption_Change in pkt.layers() +assert HCI_Event_Remote_Name_Request_Complete in pkt.layers() assert HCI_Event_Hdr in pkt.layers() assert HCI_Event_LE_Meta in pkt.layers() assert HCI_Event_Number_Of_Completed_Packets in pkt.layers() @@ -53,6 +60,46 @@ assert L2CAP_InfoReq in pkt + HCI Commands += Create Connection + +cmd = HCI_Hdr(hex_bytes("0105040d76d56f95010018cc0200000001")) +assert HCI_Cmd_Create_Connection in cmd +assert cmd[HCI_Cmd_Create_Connection].bd_addr == "00:01:95:6f:d5:76" +assert cmd[HCI_Cmd_Create_Connection].packet_type == 52248 +assert cmd[HCI_Cmd_Create_Connection].page_scan_repetition_mode == 2 +assert cmd[HCI_Cmd_Create_Connection].reserved == 0 +assert cmd[HCI_Cmd_Create_Connection].clock_offset == 0 +assert cmd[HCI_Cmd_Create_Connection].allow_role_switch == 1 + += Authentication Requested + +cmd = HCI_Hdr(hex_bytes("011104020001")) +assert HCI_Cmd_Authentication_Requested in cmd +assert cmd[HCI_Cmd_Authentication_Requested].handle == 256 + += Link Key Request Reply + +cmd = HCI_Hdr(hex_bytes("010b041676d56f9501006c9016a48a009180086a39200f03d3dd")) +assert HCI_Cmd_Link_Key_Request_Reply in cmd +assert cmd[HCI_Cmd_Link_Key_Request_Reply].bd_addr == "00:01:95:6f:d5:76" +assert cmd[HCI_Cmd_Link_Key_Request_Reply].link_key == 294855023751241435024024030130491265132 + += Set Connection Encryption + +cmd = HCI_Hdr(hex_bytes("01130403000101")) +assert HCI_Cmd_Set_Connection_Encryption in cmd +assert cmd[HCI_Cmd_Set_Connection_Encryption].handle == 256 +assert cmd[HCI_Cmd_Set_Connection_Encryption].encryption_enable == 1 + += Remote Name Request + +cmd = HCI_Hdr(hex_bytes("0119040a76d56f95010002000000")) +assert HCI_Cmd_Remote_Name_Request in cmd +assert cmd[HCI_Cmd_Remote_Name_Request].bd_addr == "00:01:95:6f:d5:76" +assert cmd[HCI_Cmd_Remote_Name_Request].page_scan_repetition_mode == 2 +assert cmd[HCI_Cmd_Remote_Name_Request].reserved == 0 +assert cmd[HCI_Cmd_Remote_Name_Request].clock_offset == 0 + = LE Create Connection # Request data @@ -120,6 +167,25 @@ assert expected_cmd_raw_data == cmd_raw_data + HCI Events + += Connect Complete + +evt_raw_data = hex_bytes("04030b00000176d56f9501000100") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Connect_Complete in evt_pkt +assert evt_pkt[HCI_Event_Connect_Complete].status == 0 +assert evt_pkt[HCI_Event_Connect_Complete].handle == 256 +assert evt_pkt[HCI_Event_Connect_Complete].bd_addr == "00:01:95:6f:d5:76" + += Remote Name Request Complete + +evt_raw_data = hex_bytes("0407ff0076d56f950100746573742d6c6170746f70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Remote_Name_Request_Complete in evt_pkt +assert evt_pkt[HCI_Event_Remote_Name_Request_Complete].status == 0 +assert evt_pkt[HCI_Event_Remote_Name_Request_Complete].bd_addr == "00:01:95:6f:d5:76" +assert evt_pkt[HCI_Event_Remote_Name_Request_Complete].remote_name == b"test-laptop".ljust(248, b"\x00") + = LE Connection Update Event evt_raw_data = hex_bytes("043e0a03004800140001003c00") evt_pkt = HCI_Hdr(evt_raw_data) From 91159a701cc690769c2db85e38ebe3e89728c935 Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Thu, 22 Jun 2023 11:04:07 +0200 Subject: [PATCH 014/122] bluetooth: Reorder HCI command packets binding to follow opcode (#4037) --- scapy/layers/bluetooth.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index 46e4458e1e5..a6a10d71d67 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -1289,14 +1289,20 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): conf.l2types.register(DLT_BLUETOOTH_HCI_H4, HCI_Hdr) conf.l2types.register(DLT_BLUETOOTH_HCI_H4_WITH_PHDR, HCI_PHDR_Hdr) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Reset, opcode=0x0c03) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Create_Connection, opcode=0x0405) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Disconnect, opcode=0x0406) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Link_Key_Request_Reply, opcode=0x040b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Authentication_Requested, opcode=0x0411) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Connection_Encryption, opcode=0x0413) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_Name_Request, opcode=0x0419) bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Event_Mask, opcode=0x0c01) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Reset, opcode=0x0c03) bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Event_Filter, opcode=0x0c05) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Local_Name, opcode=0x0c13) bind_layers(HCI_Command_Hdr, HCI_Cmd_Connect_Accept_Timeout, opcode=0x0c16) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Host_Supported, opcode=0x0c6d) bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Extended_Inquiry_Response, opcode=0x0c52) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Host_Supported, opcode=0x0c6d) bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_BD_Addr, opcode=0x1009) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Local_Name, opcode=0x0c13) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Buffer_Size, opcode=0x2002) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Random_Address, opcode=0x2005) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Parameters, opcode=0x2006) # noqa: E501 @@ -1305,12 +1311,6 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertise_Enable, opcode=0x200a) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Parameters, opcode=0x200b) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Enable, opcode=0x200c) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Create_Connection, opcode=0x0405) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Disconnect, opcode=0x406) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Link_Key_Request_Reply, opcode=0x040b) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Authentication_Requested, opcode=0x0411) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Connection_Encryption, opcode=0x0413) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_Name_Request, opcode=0x0419) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection, opcode=0x200d) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection_Cancel, opcode=0x200e) # noqa: E501 bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_White_List_Size, opcode=0x200f) @@ -1319,21 +1319,16 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Remove_Device_From_White_List, opcode=0x2012) # noqa: E501 bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Connection_Update, opcode=0x2013) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Remote_Used_Features, opcode=0x2016) # noqa: E501 - - bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Start_Encryption_Request, opcode=0x2019) # noqa: E501 - -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Start_Encryption_Request, opcode=0x2019) # noqa: E501 - bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Reply, opcode=0x201a) # noqa: E501 bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply, opcode=0x201b) # noqa: E501 -bind_layers(HCI_Event_Hdr, HCI_Event_Connect_Complete, code=0x3) -bind_layers(HCI_Event_Hdr, HCI_Event_Disconnection_Complete, code=0x5) +bind_layers(HCI_Event_Hdr, HCI_Event_Connect_Complete, code=0x03) +bind_layers(HCI_Event_Hdr, HCI_Event_Disconnection_Complete, code=0x05) bind_layers(HCI_Event_Hdr, HCI_Event_Remote_Name_Request_Complete, code=0x07) -bind_layers(HCI_Event_Hdr, HCI_Event_Encryption_Change, code=0x8) -bind_layers(HCI_Event_Hdr, HCI_Event_Command_Complete, code=0xe) -bind_layers(HCI_Event_Hdr, HCI_Event_Command_Status, code=0xf) +bind_layers(HCI_Event_Hdr, HCI_Event_Encryption_Change, code=0x08) +bind_layers(HCI_Event_Hdr, HCI_Event_Command_Complete, code=0x0e) +bind_layers(HCI_Event_Hdr, HCI_Event_Command_Status, code=0x0f) bind_layers(HCI_Event_Hdr, HCI_Event_Number_Of_Completed_Packets, code=0x13) bind_layers(HCI_Event_Hdr, HCI_Event_LE_Meta, code=0x3e) From b828bdcdf50ba6a2c5396e440dfe1a7e16909c42 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Sat, 24 Jun 2023 01:33:31 +0200 Subject: [PATCH 015/122] Fix capitalization (#4040) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4550f2e9c91..11a805bef76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ maintainers = [ { name="Pierre LALET" }, { name="Gabriel POTTER" }, { name="Guillaume VALADON" }, - { name="Nils Weiss" }, + { name="Nils WEISS" }, ] license = { text="GPL-2.0-only" } requires-python = ">=3.7, <4" From 2f2bf33a252397eafd010a9bfb0370c04a19a58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20V=C3=A1zquez?= Date: Thu, 22 Jun 2023 13:18:01 +0200 Subject: [PATCH 016/122] bluetooth: Remove duplicage XLEShortField implementation --- scapy/layers/bluetooth.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index a6a10d71d67..c87a599ae63 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -43,13 +43,14 @@ XByteField, XLELongField, XStrLenField, + XLEShortField, ) from scapy.supersocket import SuperSocket from scapy.sendrecv import sndrcv from scapy.data import MTU from scapy.consts import WINDOWS from scapy.error import warning -from scapy.utils import lhex, mac2str, str2mac +from scapy.utils import mac2str, str2mac from scapy.volatile import RandMAC @@ -57,10 +58,6 @@ # Fields # ########## -class XLEShortField(LEShortField): - def i2repr(self, pkt, x): - return lhex(self.i2h(pkt, x)) - class LEMACField(Field): def __init__(self, name, default): From c1c6a586241d356a6e50becf8c80e96e2133f5f5 Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Mon, 26 Jun 2023 09:47:30 +0200 Subject: [PATCH 017/122] Move LEMACField to fields, refactor it and add tests (#4043) --- scapy/fields.py | 10 ++++++++++ scapy/layers/bluetooth.py | 36 +----------------------------------- test/fields.uts | 13 +++++++++++++ 3 files changed, 24 insertions(+), 35 deletions(-) diff --git a/scapy/fields.py b/scapy/fields.py index 82a8abd7b07..751b6e764e6 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -793,6 +793,16 @@ def randval(self): return RandMAC() +class LEMACField(MACField): + def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[str]) -> bytes + return MACField.i2m(self, pkt, x)[::-1] + + def m2i(self, pkt, x): + # type: (Optional[Packet], bytes) -> str + return MACField.m2i(self, pkt, x[::-1]) + + class IPField(Field[Union[str, Net], bytes]): def __init__(self, name, default): # type: (str, Optional[str]) -> None diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index c87a599ae63..74aa01c1434 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -23,7 +23,6 @@ BitField, ByteEnumField, ByteField, - Field, FieldLenField, FieldListField, FlagsField, @@ -44,46 +43,13 @@ XLELongField, XStrLenField, XLEShortField, + LEMACField, ) from scapy.supersocket import SuperSocket from scapy.sendrecv import sndrcv from scapy.data import MTU from scapy.consts import WINDOWS from scapy.error import warning -from scapy.utils import mac2str, str2mac -from scapy.volatile import RandMAC - - -########## -# Fields # -########## - - -class LEMACField(Field): - def __init__(self, name, default): - Field.__init__(self, name, default, "6s") - - def i2m(self, pkt, x): - if x is None: - return b"\0\0\0\0\0\0" - return mac2str(x)[::-1] - - def m2i(self, pkt, x): - return str2mac(x[::-1]) - - def any2i(self, pkt, x): - if isinstance(x, (bytes, str)) and len(x) == 6: - x = self.m2i(pkt, x) - return x - - def i2repr(self, pkt, x): - x = self.i2h(pkt, x) - if self in conf.resolve: - x = conf.manufdb._resolve_MAC(x) - return x - - def randval(self): - return RandMAC() ########## diff --git a/test/fields.uts b/test/fields.uts index fe0329f93d3..8081107b21e 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -95,6 +95,19 @@ r = m.addfield(None, b"FOO", "c0:01:be:ef:ba:be") r assert r == b"FOO\xc0\x01\xbe\xef\xba\xbe" += LEMACField class +~ core field +m = LEMACField("foo", None) +r = m.i2m(None, None) +r +assert r == b"\x00\x00\x00\x00\x00\x00" +r = m.getfield(None, b"\xbe\xba\xef\xbe\x01\xc0ABCD") +r +assert r == (b"ABCD","c0:01:be:ef:ba:be") +r = m.addfield(None, b"FOO", "be:ba:ef:be:01:c0") +r +assert r == b"FOO\xc0\x01\xbe\xef\xba\xbe" + = SourceMACField conf.route.add(net="1.2.3.4/32", dev=conf.iface) p = Ether() / ARP(pdst="1.2.3.4") From dda902e829a51cc6237e253290c6871f30d7daf3 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Mon, 26 Jun 2023 22:59:53 +0200 Subject: [PATCH 018/122] DNS rewrite + MayEnd (make undersized dissection fail) (#4012) * DNS rewrite * Build/dissect packets on answering tests * Safety check to avoid OOM * DNS backward compatibility * Add MayEnd support * Fix automative tests for MayEnd --- doc/scapy/usage.rst | 10 +- scapy/config.py | 3 + scapy/contrib/altbeacon.py | 11 +- scapy/contrib/automotive/doip.py | 24 +- scapy/contrib/automotive/gm/gmlan.py | 26 +- scapy/contrib/automotive/kwp.py | 25 +- scapy/contrib/coap.py | 6 +- scapy/contrib/erspan.py | 16 +- scapy/contrib/isotp/isotp_utils.py | 9 +- scapy/contrib/ldp.py | 17 +- scapy/contrib/loraphy2wan.py | 35 +- scapy/contrib/openflow3.py | 2 +- scapy/contrib/rtcp.py | 7 +- .../iec104/iec104_information_elements.py | 19 +- scapy/contrib/socks.py | 14 +- scapy/contrib/stamp.py | 6 +- scapy/fields.py | 63 +- scapy/layers/dns.py | 608 ++++++++++-------- scapy/layers/dot11.py | 6 +- scapy/layers/inet6.py | 63 +- scapy/layers/llmnr.py | 41 +- scapy/layers/ntp.py | 41 +- scapy/layers/tls/extensions.py | 87 +-- scapy/layers/tls/keyexchange_tls13.py | 26 +- scapy/packet.py | 8 +- test/answering_machines.uts | 12 +- test/contrib/automotive/doip.uts | 4 +- test/contrib/automotive/ecu.uts | 11 +- test/contrib/automotive/obd/obd.uts | 8 +- test/contrib/automotive/uds.uts | 4 +- test/contrib/bgp.uts | 2 +- test/contrib/eigrp.uts | 22 +- test/contrib/erspan.uts | 4 +- test/contrib/ethercat.uts | 5 + test/contrib/ldp.uts | 1 + test/contrib/modbus.uts | 2 +- test/contrib/pcom.uts | 8 +- test/contrib/pim.uts | 2 +- test/scapy/layers/dhcp6.uts | 6 +- test/scapy/layers/dns.uts | 161 +++-- test/scapy/layers/dot11.uts | 4 +- test/scapy/layers/inet.uts | 7 + test/scapy/layers/inet6.uts | 33 +- test/scapy/layers/ntp.uts | 18 +- test/tls.uts | 2 +- test/tls13.uts | 7 + 46 files changed, 913 insertions(+), 583 deletions(-) diff --git a/doc/scapy/usage.rst b/doc/scapy/usage.rst index 0bdb84e02de..1e6c1e37842 100644 --- a/doc/scapy/usage.rst +++ b/doc/scapy/usage.rst @@ -1361,21 +1361,21 @@ DNS Requests This will perform a DNS request looking for IPv4 addresses >>> ans = sr1(IP(dst="8.8.8.8")/UDP(sport=RandShort(), dport=53)/DNS(rd=1,qd=DNSQR(qname="secdev.org",qtype="A"))) - >>> ans.an.rdata + >>> ans.an[0].rdata '217.25.178.5' **SOA request:** >>> ans = sr1(IP(dst="8.8.8.8")/UDP(sport=RandShort(), dport=53)/DNS(rd=1,qd=DNSQR(qname="secdev.org",qtype="SOA"))) - >>> ans.ns.mname + >>> ans.an[0].mname b'dns.ovh.net.' - >>> ans.ns.rname + >>> ans.an[0].rname b'tech.ovh.net.' **MX request:** >>> ans = sr1(IP(dst="8.8.8.8")/UDP(sport=RandShort(), dport=53)/DNS(rd=1,qd=DNSQR(qname="google.com",qtype="MX"))) - >>> results = [x.exchange for x in ans.an.iterpayloads()] + >>> results = [x.exchange for x in ans.an] >>> results [b'alt1.aspmx.l.google.com.', b'alt4.aspmx.l.google.com.', @@ -1477,7 +1477,7 @@ LLMNR spoof See :class:`~scapy.layers.llmnr.LLMNR_am`:: >>> conf.iface = "tap0" - >>> llmnr_spoof(iface="tap0", filter_ips=Net("10.0.0.1/24")) + >>> llmnr_spoof(iface="tap0", from_ip=Net("10.0.0.1/24")) Netbios spoof ------------- diff --git a/scapy/config.py b/scapy/config.py index 2651616fa57..a3a79e87ed4 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -911,6 +911,9 @@ class Conf(ConfClass): loopback_name = "lo" if LINUX else "lo0" nmap_base = "" # type: str nmap_kdb = None # type: Optional[NmapKnowledgeBase] + #: a safety mechanism: the maximum amount of items included in a PacketListField + #: or a FieldListField + max_list_count = 100 def __getattribute__(self, attr): # type: (str) -> Any diff --git a/scapy/contrib/altbeacon.py b/scapy/contrib/altbeacon.py index eacbd8532e9..b263872b0bb 100644 --- a/scapy/contrib/altbeacon.py +++ b/scapy/contrib/altbeacon.py @@ -11,8 +11,13 @@ The AltBeacon specification can be found at: https://github.com/AltBeacon/spec """ -from scapy.fields import ByteField, ShortField, SignedByteField, \ - StrFixedLenField +from scapy.fields import ( + ByteField, + MayEnd, + ShortField, + SignedByteField, + StrFixedLenField, +) from scapy.layers.bluetooth import EIR_Hdr, EIR_Manufacturer_Specific_Data, \ UUIDField, LowEnergyBeaconHelper from scapy.packet import Packet @@ -54,7 +59,7 @@ class AltBeacon(Packet, LowEnergyBeaconHelper): ShortField("id2", None), ShortField("id3", None), - SignedByteField("tx_power", None), + MayEnd(SignedByteField("tx_power", None)), ByteField("mfg_reserved", None), ] diff --git a/scapy/contrib/automotive/doip.py b/scapy/contrib/automotive/doip.py index 139a603065c..98dde3944ed 100644 --- a/scapy/contrib/automotive/doip.py +++ b/scapy/contrib/automotive/doip.py @@ -13,9 +13,19 @@ import time from scapy.contrib.automotive import log_automotive -from scapy.fields import ByteEnumField, ConditionalField, \ - XByteField, XShortField, XIntField, XShortEnumField, XByteEnumField, \ - IntField, StrFixedLenField, XStrField +from scapy.fields import ( + ByteEnumField, + ConditionalField, + IntField, + MayEnd, + StrFixedLenField, + XByteEnumField, + XByteField, + XIntField, + XShortEnumField, + XShortField, + XStrField, +) from scapy.packet import Packet, bind_layers, bind_bottom_up from scapy.supersocket import StreamSocket from scapy.layers.inet import TCP, UDP @@ -28,6 +38,8 @@ Optional, ) +# ISO 13400-2 sect 9.2 + class DoIP(Packet): """ @@ -121,7 +133,7 @@ class DoIP(Packet): lambda p: p.payload_type in [2, 4]), ConditionalField(StrFixedLenField("gid", b"", 6), lambda p: p.payload_type in [4]), - ConditionalField(XByteEnumField("further_action", 0, { + ConditionalField(MayEnd(XByteEnumField("further_action", 0, { 0x00: "No further action required", 0x01: "Reserved by ISO 13400", 0x02: "Reserved by ISO 13400", 0x03: "Reserved by ISO 13400", 0x04: "Reserved by ISO 13400", @@ -132,7 +144,9 @@ class DoIP(Packet): 0x0d: "Reserved by ISO 13400", 0x0e: "Reserved by ISO 13400", 0x0f: "Reserved by ISO 13400", 0x10: "Routing activation required to initiate central security", - }), lambda p: p.payload_type in [4]), + })), lambda p: p.payload_type in [4]), + # VIN/GID sync. status is marked as optional, so the packet MayEnd + # on further_action ConditionalField(XByteEnumField("vin_gid_status", 0, { 0x00: "VIN and/or GID are synchronized", 0x01: "Reserved by ISO 13400", 0x02: "Reserved by ISO 13400", diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index f4a248dce92..ce88513c99d 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -10,10 +10,25 @@ import struct from scapy.contrib.automotive import log_automotive -from scapy.fields import ObservableDict, XByteEnumField, ByteEnumField, \ - ConditionalField, XByteField, StrField, XShortEnumField, XShortField, \ - X3BytesField, XIntField, ShortField, PacketField, PacketListField, \ - FieldListField, MultipleTypeField, StrFixedLenField +from scapy.fields import ( + ByteEnumField, + ConditionalField, + FieldListField, + MayEnd, + MultipleTypeField, + ObservableDict, + PacketField, + PacketListField, + ShortField, + StrField, + StrFixedLenField, + X3BytesField, + XByteEnumField, + XByteField, + XIntField, + XShortEnumField, + XShortField, +) from scapy.packet import Packet, bind_layers, NoPayload from scapy.config import conf from scapy.contrib.isotp import ISOTP @@ -725,7 +740,8 @@ class GMLAN_NR(Packet): name = 'NegativeResponse' fields_desc = [ XByteEnumField('requestServiceId', 0, GMLAN.services), - ByteEnumField('returnCode', 0, negativeResponseCodes), + MayEnd(ByteEnumField('returnCode', 0, negativeResponseCodes)), + # XXX Is this MayEnd correct? Why is the field below also 0xe3 ? ShortField('deviceControlLimitExceeded', 0) ] diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index 8914f205b78..5617c1d7a23 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -8,9 +8,19 @@ import struct -from scapy.fields import ByteEnumField, StrField, ConditionalField, \ - BitField, XByteField, X3BytesField, ByteField, \ - ObservableDict, XShortEnumField, XByteEnumField +from scapy.fields import ( + BitField, + ByteEnumField, + ByteField, + ConditionalField, + MayEnd, + ObservableDict, + StrField, + X3BytesField, + XByteEnumField, + XByteField, + XShortEnumField, +) from scapy.packet import Packet, bind_layers, NoPayload from scapy.config import conf from scapy.error import log_loading @@ -391,7 +401,8 @@ class KWP_ROE(Packet): fields_desc = [ ByteEnumField('responseRequired', 1, responseTypes), ByteEnumField('eventWindowTime', 0, eventWindowTimes), - ByteEnumField('eventType', 0, eventTypes), + MayEnd(ByteEnumField('eventType', 0, eventTypes)), + # XXX Is this MayEnd correct? ByteField('eventParameter', 0), ByteEnumField('serviceToRespond', 0, KWP.services), ByteField('serviceParameter', 0) @@ -405,7 +416,8 @@ class KWP_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' fields_desc = [ ByteField("numberOfActivatedEvents", 0), - ByteEnumField('eventWindowTime', 0, KWP_ROE.eventWindowTimes), + MayEnd(ByteEnumField('eventWindowTime', 0, KWP_ROE.eventWindowTimes)), + # XXX Is this MayEnd correct? ByteEnumField('eventType', 0, KWP_ROE.eventTypes), ] @@ -958,7 +970,8 @@ class KWP_NR(Packet): } name = 'NegativeResponse' fields_desc = [ - XByteEnumField('requestServiceId', 0, KWP.services), + MayEnd(XByteEnumField('requestServiceId', 0, KWP.services)), + # XXX Is this MayEnd correct? ByteEnumField('negativeResponseCode', 0, negativeResponseCodes) ] diff --git a/scapy/contrib/coap.py b/scapy/contrib/coap.py index 340b6a4d428..1c8eb3d7b14 100644 --- a/scapy/contrib/coap.py +++ b/scapy/contrib/coap.py @@ -120,9 +120,9 @@ def _get_opt_val_size(pkt): class _CoAPOpt(Packet): fields_desc = [BitField("delta", 0, 4), BitField("len", 0, 4), - StrLenField("delta_ext", None, length_from=_get_delta_ext_size), # noqa: E501 - StrLenField("len_ext", None, length_from=_get_len_ext_size), - StrLenField("opt_val", None, length_from=_get_opt_val_size)] + StrLenField("delta_ext", "", length_from=_get_delta_ext_size), # noqa: E501 + StrLenField("len_ext", "", length_from=_get_len_ext_size), + StrLenField("opt_val", "", length_from=_get_opt_val_size)] @staticmethod def _populate_extended(val): diff --git a/scapy/contrib/erspan.py b/scapy/contrib/erspan.py index 69c3310cc32..3ef2d6157fc 100644 --- a/scapy/contrib/erspan.py +++ b/scapy/contrib/erspan.py @@ -4,6 +4,8 @@ """ ERSPAN - Encapsulated Remote SPAN + +https://datatracker.ietf.org/doc/html/draft-foschiano-erspan-03 """ # scapy.contrib.description = ERSPAN - Encapsulated Remote SPAN @@ -19,16 +21,24 @@ class ERSPAN(Packet): """ - A generic ERSPAN packet, pointing by default to ERSPAN II + A generic ERSPAN packet """ name = "ERSPAN" fields_desc = [] @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt: + ver = _pkt[0] >> 4 + if ver == 1: + return ERSPAN_II + elif ver == 2: + return ERSPAN_III + else: + return ERSPAN_I if cls == ERSPAN: return ERSPAN_II - return Packet.dispatch_hook(cls, _pkt, *args, **kargs) + return cls class ERSPAN_I(ERSPAN): @@ -40,7 +50,7 @@ class ERSPAN_I(ERSPAN): class ERSPAN_II(ERSPAN): name = "ERSPAN II" match_subclass = True - fields_desc = [BitField("ver", 0, 4), + fields_desc = [BitField("ver", 1, 4), BitField("vlan", 0, 12), BitField("cos", 0, 3), BitField("en", 0, 2), diff --git a/scapy/contrib/isotp/isotp_utils.py b/scapy/contrib/isotp/isotp_utils.py index 9f15193aa8c..49269f20aab 100644 --- a/scapy/contrib/isotp/isotp_utils.py +++ b/scapy/contrib/isotp/isotp_utils.py @@ -10,6 +10,7 @@ import struct +from scapy.config import conf from scapy.utils import EDecimal from scapy.packet import Packet from scapy.sessions import DefaultSession @@ -201,7 +202,13 @@ def _build( # type: (...) -> ISOTP bucket = t[2] data = bucket.ready or b"" - p = basecls(data) + try: + p = basecls(data) + except Exception: + if conf.debug_dissector: + from scapy.sendrecv import debug + debug.crashed_on = (basecls, data) + raise if hasattr(p, "rx_id"): p.rx_id = t[0] if hasattr(p, "rx_ext_address"): diff --git a/scapy/contrib/ldp.py b/scapy/contrib/ldp.py index ae36c58d062..bd08ee8f58f 100644 --- a/scapy/contrib/ldp.py +++ b/scapy/contrib/ldp.py @@ -17,8 +17,15 @@ from scapy.compat import orb from scapy.packet import Packet, bind_layers, bind_bottom_up -from scapy.fields import BitField, IPField, IntField, ShortField, StrField, \ - XBitField +from scapy.fields import ( + BitField, + MayEnd, + IPField, + IntField, + ShortField, + StrField, + XBitField, +) from scapy.layers.inet import UDP from scapy.layers.inet import TCP from scapy.config import conf @@ -168,6 +175,8 @@ def size(self, s): return tmp_len def getfield(self, pkt, s): + if not s: + return s, [] tmp_len = self.size(s) return s[tmp_len:], self.m2i(pkt, s[:tmp_len]) @@ -358,7 +367,7 @@ class LDPLabelMM(_LDP_Packet): XBitField("type", 0x0400, 15), ShortField("len", None), IntField("id", 0), - FecTLVField("fec", None), + MayEnd(FecTLVField("fec", None)), LabelTLVField("label", 0)] # 3.5.8. Label Request Message @@ -393,7 +402,7 @@ class LDPLabelWM(_LDP_Packet): XBitField("type", 0x0402, 15), ShortField("len", None), IntField("id", 0), - FecTLVField("fec", None), + MayEnd(FecTLVField("fec", None)), LabelTLVField("label", 0)] # 3.5.11. Label Release Message diff --git a/scapy/contrib/loraphy2wan.py b/scapy/contrib/loraphy2wan.py index 08b570f0572..01487d457dd 100644 --- a/scapy/contrib/loraphy2wan.py +++ b/scapy/contrib/loraphy2wan.py @@ -16,12 +16,29 @@ """ from scapy.packet import Packet -from scapy.fields import BitField, ByteEnumField, ByteField, \ - ConditionalField, IntField, LEShortField, PacketListField, \ - StrFixedLenField, X3BytesField, XByteField, XIntField, \ - XShortField, BitFieldLenField, LEX3BytesField, XBitField, \ - BitEnumField, XLEIntField, StrField, PacketField, \ - MultipleTypeField +from scapy.fields import ( + BitEnumField, + BitField, + BitFieldLenField, + ByteEnumField, + ByteField, + ConditionalField, + IntField, + LEShortField, + LEX3BytesField, + MayEnd, + MultipleTypeField, + PacketField, + PacketListField, + StrField, + StrFixedLenField, + X3BytesField, + XBitField, + XByteField, + XIntField, + XLEIntField, + XShortField, +) class FCtrl_DownLink(Packet): @@ -692,9 +709,9 @@ class PHYPayload(Packet): name = "PHYPayload" fields_desc = [MHDR, MACPayload, - ConditionalField(XIntField("MIC", 0), - lambda pkt:(pkt.MType != 0b001 or - LoRa.encrypted is False))] + MayEnd(ConditionalField(XIntField("MIC", 0), + lambda pkt: (pkt.MType != 0b001 or + LoRa.encrypted is False)))] class LoRa(Packet): # default frame (unclear specs => taken from https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5677147/) # noqa: E501 diff --git a/scapy/contrib/openflow3.py b/scapy/contrib/openflow3.py index 5fe0cf6e715..1f7bc15ce2f 100755 --- a/scapy/contrib/openflow3.py +++ b/scapy/contrib/openflow3.py @@ -2248,7 +2248,7 @@ class OFPFlowStats(_ofp_header_item): LongField("byte_count", 0), MatchField("match"), PacketListField("instructions", [], OFPIT, - length_from=lambda pkt:pkt.len - 56 - pkt.match.len)] # noqa: E501 + length_from=lambda pkt:pkt.len - 52 - pkt.match.len)] # noqa: E501 def extract_padding(self, s): return b"", s diff --git a/scapy/contrib/rtcp.py b/scapy/contrib/rtcp.py index 25414182e11..fbf039432ad 100644 --- a/scapy/contrib/rtcp.py +++ b/scapy/contrib/rtcp.py @@ -94,7 +94,12 @@ class SDESChunk(Packet): name = "SDES chunk" fields_desc = [ IntField('sourcesync', None), - PacketListField('items', None, pkt_cls=SDESItem) + PacketListField( + 'items', None, + next_cls_cb=( + lambda x, y, p, z: None if (p and p.chunk_type == 0) else SDESItem + ) + ) ] diff --git a/scapy/contrib/scada/iec104/iec104_information_elements.py b/scapy/contrib/scada/iec104/iec104_information_elements.py index 9c1c07e94b1..f82813d1720 100644 --- a/scapy/contrib/scada/iec104/iec104_information_elements.py +++ b/scapy/contrib/scada/iec104/iec104_information_elements.py @@ -29,9 +29,16 @@ from scapy.contrib.scada.iec104.iec104_fields import \ IEC60870_5_4_NormalizedFixPoint, IEC104SignedSevenBitValue, \ LESignedShortField, LEIEEEFloatField -from scapy.fields import BitEnumField, ByteEnumField, ByteField, \ - ThreeBytesField, \ - BitField, LEShortField, LESignedIntField +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + LEShortField, + LESignedIntField, + MayEnd, + ThreeBytesField, +) def _generate_attributes_and_dicts(cls): @@ -613,7 +620,7 @@ class IEC104_IE_CP56TIME2A(IEC104_IE_CommonQualityFlags): BitField('reserved_2', 0, 2), BitField('hours', 0, 5), BitEnumField('weekday', 0, 3, WEEK_DAY_FLAGS), - BitField('day_of_month', 0, 5), + MayEnd(BitField('day_of_month', 0, 5)), BitField('reserved_3', 0, 4), BitField('month', 0, 4), BitField('reserved_4', 0, 1), @@ -630,7 +637,7 @@ class IEC104_IE_CP56TIME2A_START_TIME(IEC104_IE_CP56TIME2A): informantion_element_fields = [ LEShortField('start_sec_milli', 0), BitEnumField('start_iv', 0, 1, IEC104_IE_CommonQualityFlags.IV_FLAGS), - BitEnumField('start_gen', 0, 1, IEC104_IE_CP56TIME2A.GEN_FLAGS), + MayEnd(BitEnumField('start_gen', 0, 1, IEC104_IE_CP56TIME2A.GEN_FLAGS)), # only valid in monitor direction ToDo: special treatment needed? BitField('start_minutes', 0, 6), BitEnumField('start_su', 0, 1, IEC104_IE_CP56TIME2A.SU_FLAGS), @@ -655,7 +662,7 @@ class IEC104_IE_CP56TIME2A_STOP_TIME(IEC104_IE_CP56TIME2A): informantion_element_fields = [ LEShortField('stop_sec_milli', 0), BitEnumField('stop_iv', 0, 1, IEC104_IE_CommonQualityFlags.IV_FLAGS), - BitEnumField('stop_gen', 0, 1, IEC104_IE_CP56TIME2A.GEN_FLAGS), + MayEnd(BitEnumField('stop_gen', 0, 1, IEC104_IE_CP56TIME2A.GEN_FLAGS)), # only valid in monitor direction ToDo: special treatment needed? BitField('stop_minutes', 0, 6), BitEnumField('stop_su', 0, 1, IEC104_IE_CP56TIME2A.SU_FLAGS), diff --git a/scapy/contrib/socks.py b/scapy/contrib/socks.py index 182e1aff0c3..1aadb5cf2f8 100644 --- a/scapy/contrib/socks.py +++ b/scapy/contrib/socks.py @@ -16,8 +16,15 @@ from scapy.layers.dns import DNSStrField from scapy.layers.inet import TCP, UDP from scapy.layers.inet6 import IP6Field -from scapy.fields import ByteField, ByteEnumField, ShortField, IPField, \ - StrField, MultipleTypeField +from scapy.fields import ( + ByteEnumField, + ByteField, + IPField, + MultipleTypeField, + ShortField, + StrField, + StrNullField, +) from scapy.packet import Packet, bind_layers, bind_bottom_up # TODO: support the 3 different authentication exchange procedures for SOCKS5 # noqa: E501 @@ -86,8 +93,7 @@ class SOCKS4Request(Packet): ByteEnumField("cd", 1, _socks4_cd_request), ShortField("dstport", 80), IPField("dst", "0.0.0.0"), - StrField("userid", ""), - ByteField("null", 0), + StrNullField("userid", ""), ] diff --git a/scapy/contrib/stamp.py b/scapy/contrib/stamp.py index 300064994f7..e2c35389921 100644 --- a/scapy/contrib/stamp.py +++ b/scapy/contrib/stamp.py @@ -225,8 +225,7 @@ class STAMPSessionSenderTestUnauthenticated(Packet): PacketField('err_estimate', ErrorEstimate(), ErrorEstimate), ShortField('ssid', 1), NBytesField('mbz', 0, 28), # 28 bytes MBZ - PacketListField('tlv_objects', [], STAMPTestTLV, - length_from=lambda pkt: pkt.parent.len - 8 - 44), + PacketListField('tlv_objects', [], STAMPTestTLV), ] @@ -297,8 +296,7 @@ class STAMPSessionReflectorTestUnauthenticated(Packet): ShortField('mbz1', 0), ByteField('ttl_sender', 255), NBytesField('mbz2', 0, 3), # 3 bytes MBZ - PacketListField('tlv_objects', [], STAMPTestTLV, - length_from=lambda pkt: pkt.parent.len - 8 - 44), + PacketListField('tlv_objects', [], STAMPTestTLV), ] diff --git a/scapy/fields.py b/scapy/fields.py index 751b6e764e6..a07b11b01cd 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -304,6 +304,7 @@ class _FieldContainer(object): """ A field that acts as a container for another field """ + __slots__ = ["fld"] def __getattr__(self, attr): # type: (str) -> Any @@ -325,13 +326,35 @@ def __eq__(self, other): # type: (Any) -> bool return bool(self.fld == other) - def __ne__(self, other): + def __hash__(self): + # type: () -> int + return hash(self.fld) + + +class MayEnd(_FieldContainer): + """ + Allow packet dissection to end after the dissection of this field + if no bytes are left. + + A good example would be a length field that can be 0 or a set value, + and where it would be too annoying to use multiple ConditionalFields + + Important note: any field below this one MUST default + to an empty value, else the behavior will be unexpected. + """ + __slots__ = ["fld"] + + def __init__(self, fld): + # type: (Any) -> None + self.fld = fld + + def __eq__(self, other): # type: (Any) -> bool - # Python 2.7 compat - return not self == other + return bool(self.fld == other) - # mypy doesn't support __hash__ = None - __hash__ = None # type: ignore + def __hash__(self): + # type: () -> int + return hash(self.fld) class ActionField(_FieldContainer): @@ -353,7 +376,7 @@ class ConditionalField(_FieldContainer): __slots__ = ["fld", "cond"] def __init__(self, - fld, # type: Field[Any, Any] + fld, # type: AnyField cond # type: Callable[[Packet], bool] ): # type: (...) -> None @@ -1133,6 +1156,10 @@ class FieldValueRangeException(Scapy_Exception): pass +class MaximumItemsCount(Scapy_Exception): + pass + + class FieldAttributeException(Scapy_Exception): pass @@ -1577,7 +1604,7 @@ class PacketListField(_PacketField[List[BasePacket]]): (i.e. a stack of layers). All elements in PacketListField have current packet referenced in parent field. """ - __slots__ = ["count_from", "length_from", "next_cls_cb"] + __slots__ = ["count_from", "length_from", "next_cls_cb", "max_count"] islist = 1 def __init__( @@ -1588,6 +1615,7 @@ def __init__( count_from=None, # type: Optional[Callable[[Packet], int]] length_from=None, # type: Optional[Callable[[Packet], int]] next_cls_cb=None, # type: Optional[Callable[[Packet, List[BasePacket], Optional[Packet], bytes], Type[Packet]]] # noqa: E501 + max_count=None, # type: Optional[int] ): # type: (...) -> None """ @@ -1687,6 +1715,8 @@ class object defining a ``dispatch_hook`` class method :param length_from: a callback returning the number of bytes to dissect :param next_cls_cb: a callback returning either None or the type of the next Packet to dissect. + :param max_count: an int containing the max amount of results. This is + a safety mechanism, exceeding this value will raise a Scapy_Exception. """ if default is None: default = [] # Create a new list for each instance @@ -1698,6 +1728,7 @@ class object defining a ``dispatch_hook`` class method self.count_from = count_from self.length_from = length_from self.next_cls_cb = next_cls_cb + self.max_count = max_count def any2i(self, pkt, x): # type: (Optional[Packet], Any) -> List[BasePacket] @@ -1778,6 +1809,12 @@ def getfield(self, pkt, s): else: remain = b"" lst.append(p) + if len(lst) > (self.max_count or conf.max_list_count): + raise MaximumItemsCount( + "Maximum amount of items reached in PacketListField: %s " + "(defaults to conf.max_list_count)" + % (self.max_count or conf.max_list_count) + ) if isinstance(remain, tuple): remain, nb = remain @@ -1886,7 +1923,7 @@ def i2m(self, pkt, y): def m2i(self, pkt, x): # type: (Optional[Packet], bytes) -> bytes - x = x.strip(b"\x00").strip(b" ") + x = x[1:].strip(b"\x00").strip(b" ") return b"".join(map( lambda x, y: chb( (((orb(x) - 1) & 0xf) << 4) + ((orb(y) - 1) & 0xf) @@ -2008,7 +2045,7 @@ def randval(self): class FieldListField(Field[List[Any], List[Any]]): - __slots__ = ["field", "count_from", "length_from"] + __slots__ = ["field", "count_from", "length_from", "max_count"] islist = 1 def __init__( @@ -2018,6 +2055,7 @@ def __init__( field, # type: AnyField length_from=None, # type: Optional[Callable[[Packet], int]] count_from=None, # type: Optional[Callable[[Packet], int]] + max_count=None, # type: Optional[int] ): # type: (...) -> None if default is None: @@ -2026,6 +2064,7 @@ def __init__( Field.__init__(self, name, default) self.count_from = count_from self.length_from = length_from + self.max_count = max_count def i2count(self, pkt, val): # type: (Optional[Packet], List[Any]) -> int @@ -2085,6 +2124,12 @@ def getfield(self, c -= 1 s, v = self.field.getfield(pkt, s) val.append(v) + if len(val) > (self.max_count or conf.max_list_count): + raise MaximumItemsCount( + "Maximum amount of items reached in FieldListField: %s " + "(defaults to conf.max_list_count)" + % (self.max_count or conf.max_list_count) + ) if isinstance(s, tuple): s, bn = s diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 1244ca130a7..db3370eb90d 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -7,6 +7,7 @@ DNS: Domain Name System. """ +import abc import operator import socket import struct @@ -19,7 +20,7 @@ from scapy.config import conf from scapy.compat import orb, raw, chb, bytes_encode, plain_str from scapy.error import log_runtime, warning, Scapy_Exception -from scapy.packet import Packet, bind_layers, NoPayload, Raw +from scapy.packet import Packet, bind_layers, Raw from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ ConditionalField, Field, FieldLenField, FlagsField, IntField, \ PacketListField, ShortEnumField, ShortField, StrField, \ @@ -33,6 +34,7 @@ from typing import ( Any, + List, Optional, Tuple, Type, @@ -66,82 +68,81 @@ dnsclasses = {1: 'IN', 2: 'CS', 3: 'CH', 4: 'HS', 255: 'ANY'} -def dns_get_str(s, pointer=0, pkt=None, _fullpacket=False): +def dns_get_str(s, full=None, _ignore_compression=False): """This function decompresses a string s, starting from the given pointer. :param s: the string to decompress - :param pointer: first pointer on the string (default: 0) - :param pkt: (optional) an InheritOriginDNSStrPacket packet + :param full: (optional) the full packet (used for decompression) :returns: (decoded_string, end_index, left_string) """ - # The _fullpacket parameter is reserved for scapy. It indicates - # that the string provided is the full dns packet, and thus - # will be the same than pkt._orig_str. The "Cannot decompress" - # error will not be prompted if True. + # _ignore_compression is for internal use only max_length = len(s) # The result = the extracted name name = b"" # Will contain the index after the pointer, to be returned after_pointer = None processed_pointers = [] # Used to check for decompression loops - # Analyse given pkt - if pkt and hasattr(pkt, "_orig_s") and pkt._orig_s: - s_full = pkt._orig_s - else: - s_full = None bytes_left = None + _fullpacket = False # s = full packet + pointer = 0 while True: if abs(pointer) >= max_length: log_runtime.info( "DNS RR prematured end (ofs=%i, len=%i)", pointer, len(s) ) break - cur = orb(s[pointer]) # get pointer value + cur = s[pointer] # get pointer value pointer += 1 # make pointer go forward if cur & 0xc0: # Label pointer if after_pointer is None: # after_pointer points to where the remaining bytes start, # as pointer will follow the jump token after_pointer = pointer + 1 + if _ignore_compression: + # skip + pointer += 1 + continue if pointer >= max_length: log_runtime.info( "DNS incomplete jump token at (ofs=%i)", pointer ) break + if not full: + raise Scapy_Exception("DNS message can't be compressed " + + "at this point!") # Follow the pointer - pointer = ((cur & ~0xc0) << 8) + orb(s[pointer]) - 12 + pointer = ((cur & ~0xc0) << 8) + s[pointer] if pointer in processed_pointers: warning("DNS decompression loop detected") break + if len(processed_pointers) >= 20: + warning("More than 20 jumps in a single DNS decompression ! " + "Dropping (evil packet)") + break if not _fullpacket: - # Do we have access to the whole packet ? - if s_full: - # Yes -> use it to continue - bytes_left = s[after_pointer:] - s = s_full - max_length = len(s) - _fullpacket = True - else: - # No -> abort - raise Scapy_Exception("DNS message can't be compressed " + - "at this point!") + # We switch our s buffer to full, so we need to remember + # the previous context + bytes_left = s[after_pointer:] + s = full + max_length = len(s) + _fullpacket = True processed_pointers.append(pointer) continue elif cur > 0: # Label # cur = length of the string name += s[pointer:pointer + cur] + b"." pointer += cur - else: + else: # End break if after_pointer is not None: # Return the real end index (not the one we followed) pointer = after_pointer if bytes_left is None: bytes_left = s[pointer:] - # name, end_index, remaining - return name, pointer, bytes_left, len(processed_pointers) != 0 + # name, remaining + return name or b".", bytes_left def _is_ptr(x): @@ -194,19 +195,16 @@ def dns_compress(pkt): def field_gen(dns_pkt): """Iterates through all DNS strings that can be compressed""" for lay in [dns_pkt.qd, dns_pkt.an, dns_pkt.ns, dns_pkt.ar]: - if lay is None: + if not lay: continue - current = lay - while not isinstance(current, NoPayload): - if isinstance(current, InheritOriginDNSStrPacket): - for field in current.fields_desc: - if isinstance(field, DNSStrField) or \ - (isinstance(field, MultipleTypeField) and - current.type in [2, 3, 4, 5, 12, 15]): - # Get the associated data and store it accordingly # noqa: E501 - dat = current.getfieldval(field.name) - yield current, field.name, dat - current = current.payload + for current in lay: + for field in current.fields_desc: + if isinstance(field, DNSStrField) or \ + (isinstance(field, MultipleTypeField) and + current.type in [2, 3, 4, 5, 12, 15]): + # Get the associated data and store it accordingly # noqa: E501 + dat = current.getfieldval(field.name) + yield current, field.name, dat def possible_shortens(dat): """Iterates through all possible compression parts in a DNS string""" @@ -267,13 +265,13 @@ def possible_shortens(dat): return dns_pkt -class InheritOriginDNSStrPacket(Packet): - __slots__ = Packet.__slots__ + ["_orig_s", "_orig_p"] - - def __init__(self, _pkt=None, _orig_s=None, _orig_p=None, *args, **kwargs): - self._orig_s = _orig_s - self._orig_p = _orig_p - Packet.__init__(self, _pkt=_pkt, *args, **kwargs) +class DNSCompressedPacket(Packet): + """ + Class to mark that a packet contains DNSStrField and supports compression + """ + @abc.abstractmethod + def get_full(self): + pass class DNSStrField(StrLenField): @@ -282,7 +280,6 @@ class DNSStrField(StrLenField): It will also handle DNS decompression. (may be StrLenField if a length_from is passed), """ - __slots__ = ["compressed"] def h2i(self, pkt, x): if not x: @@ -297,114 +294,23 @@ def i2m(self, pkt, x): def i2len(self, pkt, x): return len(self.i2m(pkt, x)) + def get_full(self, pkt): + while pkt and not isinstance(pkt, DNSCompressedPacket): + pkt = pkt.parent or pkt.underlayer + if not pkt: + return None + return pkt.get_full() + def getfield(self, pkt, s): remain = b"" if self.length_from: remain, s = super(DNSStrField, self).getfield(pkt, s) # Decode the compressed DNS message - decoded, _, left, self.compressed = dns_get_str(s, 0, pkt) + decoded, left = dns_get_str(s, full=self.get_full(pkt)) # returns (remaining, decoded) return left + remain, decoded -class DNSRRCountField(ShortField): - __slots__ = ["rr"] - - def __init__(self, name, default, rr): - ShortField.__init__(self, name, default) - self.rr = rr - - def _countRR(self, pkt): - x = getattr(pkt, self.rr) - i = 0 - while isinstance(x, (DNSRR, DNSQR)) or isdnssecRR(x): - x = x.payload - i += 1 - return i - - def i2m(self, pkt, x): - if x is None: - x = self._countRR(pkt) - return x - - def i2h(self, pkt, x): - if x is None: - x = self._countRR(pkt) - return x - - -class DNSRRField(StrField): - __slots__ = ["countfld", "passon", "rr"] - holds_packets = 1 - - def __init__(self, name, countfld, default, passon=1): - StrField.__init__(self, name, None) - self.countfld = countfld - # Notes: - # - self.rr: used by DNSRRCountField() to compute the records count - # - self.default: used to set the default record - self.rr = self.default = default - self.passon = passon - - def i2m(self, pkt, x): - if x is None: - return b"" - return bytes_encode(x) - - def decodeRR(self, name, s, p): - ret = s[p:p + 10] - # type, cls, ttl, rdlen - typ, cls, _, rdlen = struct.unpack("!HHIH", ret) - p += 10 - cls = DNSRR_DISPATCHER.get(typ, DNSRR) - rr = cls(b"\x00" + ret + s[p:p + rdlen], _orig_s=s, _orig_p=p) - - # Reset rdlen if DNS compression was used - for fname in rr.fieldtype.keys(): - rdata_obj = rr.fieldtype[fname] - if fname == "rdata" and isinstance(rdata_obj, MultipleTypeField): - rdata_obj = rdata_obj._find_fld_pkt_val(rr, rr.type)[0] - if isinstance(rdata_obj, DNSStrField) and rdata_obj.compressed: - del rr.rdlen - break - rr.rrname = name - - p += rdlen - return rr, p - - def getfield(self, pkt, s): - if isinstance(s, tuple): - s, p = s - else: - p = 0 - ret = None - c = getattr(pkt, self.countfld) - if c > len(s): - log_runtime.info("DNS wrong value: DNS.%s=%i", self.countfld, c) - return s, b"" - while c: - c -= 1 - name, p, _, _ = dns_get_str(s, p, _fullpacket=True) - rr, p = self.decodeRR(name, s, p) - if ret is None: - ret = rr - else: - ret.add_payload(rr) - if self.passon: - return (s, p), ret - else: - return s[p:], ret - - -class DNSQRField(DNSRRField): - def decodeRR(self, name, s, p): - ret = s[p:p + 4] - p += 4 - rr = DNSQR(b"\x00" + ret, _orig_s=s, _orig_p=p) - rr.qname = name - return rr, p - - class DNSTextField(StrLenField): """ Special StrLenField that handles DNS TEXT data (16) @@ -451,93 +357,6 @@ def i2m(self, pkt, s): return ret_s -class DNSQR(InheritOriginDNSStrPacket): - name = "DNS Question Record" - show_indent = 0 - fields_desc = [DNSStrField("qname", "www.example.com"), - ShortEnumField("qtype", 1, dnsqtypes), - ShortEnumField("qclass", 1, dnsclasses)] - - -class DNS(Packet): - name = "DNS" - fields_desc = [ - ConditionalField(ShortField("length", None), - lambda p: isinstance(p.underlayer, TCP)), - ShortField("id", 0), - BitField("qr", 0, 1), - BitEnumField("opcode", 0, 4, {0: "QUERY", 1: "IQUERY", 2: "STATUS"}), - BitField("aa", 0, 1), - BitField("tc", 0, 1), - BitField("rd", 1, 1), - BitField("ra", 0, 1), - BitField("z", 0, 1), - # AD and CD bits are defined in RFC 2535 - BitField("ad", 0, 1), # Authentic Data - BitField("cd", 0, 1), # Checking Disabled - BitEnumField("rcode", 0, 4, {0: "ok", 1: "format-error", - 2: "server-failure", 3: "name-error", - 4: "not-implemented", 5: "refused"}), - DNSRRCountField("qdcount", None, "qd"), - DNSRRCountField("ancount", None, "an"), - DNSRRCountField("nscount", None, "ns"), - DNSRRCountField("arcount", None, "ar"), - DNSQRField("qd", "qdcount", DNSQR()), - DNSRRField("an", "ancount", None), - DNSRRField("ns", "nscount", None), - DNSRRField("ar", "arcount", None, 0), - ] - - def answers(self, other): - return (isinstance(other, DNS) and - self.id == other.id and - self.qr == 1 and - other.qr == 0) - - def mysummary(self): - name = "" - if self.qr: - type = "Ans" - if self.ancount > 0 and isinstance(self.an, DNSRR): - name = ' "%s"' % self.an.rdata - else: - type = "Qry" - if self.qdcount > 0 and isinstance(self.qd, DNSQR): - name = ' "%s"' % self.qd.qname - return 'DNS %s%s ' % (type, name) - - def post_build(self, pkt, pay): - if isinstance(self.underlayer, TCP) and self.length is None: - pkt = struct.pack("!H", len(pkt) - 2) + pkt[2:] - return pkt + pay - - def compress(self): - """Return the compressed DNS packet (using `dns_compress()`""" - return dns_compress(self) - - def pre_dissect(self, s): - """ - Check that a valid DNS over TCP message can be decoded - """ - if isinstance(self.underlayer, TCP): - - # Compute the length of the DNS packet - if len(s) >= 2: - dns_len = struct.unpack("!H", s[:2])[0] - else: - message = "Malformed DNS message: too small!" - log_runtime.info(message) - raise Scapy_Exception(message) - - # Check if the length is valid - if dns_len < 14 or len(s) < dns_len: - message = "Malformed DNS message: invalid length!" - log_runtime.info(message) - raise Scapy_Exception(message) - - return s - - # RFC 2671 - Extension Mechanisms for DNS (EDNS0) edns0types = {0: "Reserved", 1: "LLQ", 2: "UL", 3: "NSID", 4: "Reserved", @@ -568,7 +387,7 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): return EDNS0TLV -class DNSRROPT(InheritOriginDNSStrPacket): +class DNSRROPT(Packet): name = "DNS OPT Resource Record" fields_desc = [DNSStrField("rrname", ""), ShortEnumField("type", 41, dnstypes), @@ -782,7 +601,7 @@ def i2repr(self, pkt, x): return [dnstypes.get(rr, rr) for rr in rrlist] if rrlist else repr(x) -class _DNSRRdummy(InheritOriginDNSStrPacket): +class _DNSRRdummy(Packet): name = "Dummy class that implements post_build() for Resource Records" def post_build(self, pkt, pay): @@ -796,6 +615,9 @@ def post_build(self, pkt, pay): return tmp_pkt + pkt + pay + def default_payload_class(self, payload): + return conf.padding_layer + class DNSRRMX(_DNSRRdummy): name = "DNS MX Resource Record" @@ -1024,14 +846,8 @@ class DNSRRTSIG(_DNSRRdummy): 32769: DNSRRDLV, # RFC 4431 } -DNSSEC_CLASSES = tuple(DNSRR_DISPATCHER.values()) - - -def isdnssecRR(obj): - return isinstance(obj, DNSSEC_CLASSES) - -class DNSRR(InheritOriginDNSStrPacket): +class DNSRR(Packet): name = "DNS Resource Record" show_indent = 0 fields_desc = [DNSStrField("rrname", ""), @@ -1060,6 +876,166 @@ class DNSRR(InheritOriginDNSStrPacket): length_from=lambda pkt:pkt.rdlen) )] + def default_payload_class(self, payload): + return conf.padding_layer + + +def _DNSRR(s, **kwargs): + """ + DNSRR dispatcher func + """ + if s: + # Try to find the type of the RR using the dispatcher + _, remain = dns_get_str(s, _ignore_compression=True) + cls = DNSRR_DISPATCHER.get( + struct.unpack("!H", remain[:2])[0], + DNSRR, + ) + rrlen = ( + len(s) - len(remain) + # rrname len + 10 + + struct.unpack("!H", remain[8:10])[0] + ) + pkt = cls(s[:rrlen], **kwargs) / conf.padding_layer(s[rrlen:]) + # drop rdlen because if rdata was compressed, it will break everything + # when rebuilding + del pkt.fields["rdlen"] + return pkt + return None + + +class DNSQR(Packet): + name = "DNS Question Record" + show_indent = 0 + fields_desc = [DNSStrField("qname", "www.example.com"), + ShortEnumField("qtype", 1, dnsqtypes), + ShortEnumField("qclass", 1, dnsclasses)] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class _DNSPacketListField(PacketListField): + # A normal PacketListField with backward-compatible hacks + def any2i(self, pkt, x): + # type: (Optional[Packet], List[Any]) -> List[Any] + if x is None: + warnings.warn( + ("The DNS fields 'qd', 'an', 'ns' and 'ar' are now " + "PacketListField(s) ! " + "Setting a null default should be [] instead of None"), + DeprecationWarning + ) + x = [] + return super(_DNSPacketListField, self).any2i(pkt, x) + + def i2h(self, pkt, x): + # type: (Optional[Packet], List[Packet]) -> Any + class _list(list): + """ + Fake list object to provide compatibility with older DNS fields + """ + def __getattr__(self, attr): + try: + ret = getattr(self[0], attr) + warnings.warn( + ("The DNS fields 'qd', 'an', 'ns' and 'ar' are now " + "PacketListField(s) ! " + "To access the first element, use pkt.an[0] instead of " + "pkt.an"), + DeprecationWarning + ) + return ret + except AttributeError: + raise + return _list(x) + + +class DNS(DNSCompressedPacket): + name = "DNS" + fields_desc = [ + ConditionalField(ShortField("length", None), + lambda p: isinstance(p.underlayer, TCP)), + ShortField("id", 0), + BitField("qr", 0, 1), + BitEnumField("opcode", 0, 4, {0: "QUERY", 1: "IQUERY", 2: "STATUS"}), + BitField("aa", 0, 1), + BitField("tc", 0, 1), + BitField("rd", 1, 1), + BitField("ra", 0, 1), + BitField("z", 0, 1), + # AD and CD bits are defined in RFC 2535 + BitField("ad", 0, 1), # Authentic Data + BitField("cd", 0, 1), # Checking Disabled + BitEnumField("rcode", 0, 4, {0: "ok", 1: "format-error", + 2: "server-failure", 3: "name-error", + 4: "not-implemented", 5: "refused"}), + FieldLenField("qdcount", None, count_of="qd"), + FieldLenField("ancount", None, count_of="an"), + FieldLenField("nscount", None, count_of="ns"), + FieldLenField("arcount", None, count_of="ar"), + _DNSPacketListField("qd", [DNSQR()], DNSQR, count_from=lambda pkt: pkt.qdcount), + _DNSPacketListField("an", [], _DNSRR, count_from=lambda pkt: pkt.ancount), + _DNSPacketListField("ns", [], _DNSRR, count_from=lambda pkt: pkt.nscount), + _DNSPacketListField("ar", [], _DNSRR, count_from=lambda pkt: pkt.arcount), + ] + + def get_full(self): + # Required for DNSCompressedPacket + if isinstance(self.underlayer, TCP): + return self.original[2:] + else: + return self.original + + def answers(self, other): + return (isinstance(other, DNS) and + self.id == other.id and + self.qr == 1 and + other.qr == 0) + + def mysummary(self): + name = "" + if self.qr: + type = "Ans" + if self.an and isinstance(self.an, DNSRR): + name = ' "%s"' % self.an[0].rdata + else: + type = "Qry" + if self.qd and isinstance(self.qd, DNSQR): + name = ' "%s"' % self.qd[0].qname + return 'DNS %s%s ' % (type, name) + + def post_build(self, pkt, pay): + if isinstance(self.underlayer, TCP) and self.length is None: + pkt = struct.pack("!H", len(pkt) - 2) + pkt[2:] + return pkt + pay + + def compress(self): + """Return the compressed DNS packet (using `dns_compress()`)""" + return dns_compress(self) + + def pre_dissect(self, s): + """ + Check that a valid DNS over TCP message can be decoded + """ + if isinstance(self.underlayer, TCP): + + # Compute the length of the DNS packet + if len(s) >= 2: + dns_len = struct.unpack("!H", s[:2])[0] + else: + message = "Malformed DNS message: too small!" + log_runtime.info(message) + raise Scapy_Exception(message) + + # Check if the length is valid + if dns_len < 14 or len(s) < dns_len: + message = "Malformed DNS message: invalid length!" + log_runtime.info(message) + raise Scapy_Exception(message) + + return s + bind_layers(UDP, DNS, dport=5353) bind_layers(UDP, DNS, sport=5353) @@ -1117,63 +1093,139 @@ class DNS_am(AnsweringMachine): cls = DNS # We use this automaton for llmnr_spoof def parse_options(self, joker=None, - match=None, joker6=None, from_ip=None): + match=None, + srvmatch=None, + joker6=False, + from_ip=None, + from_ip6=None, + src_ip=None, + src_ip6=None, + ttl=10): """ :param joker: default IPv4 for unresolved domains. (Default: None) Set to False to disable, None to mirror the interface's IP. :param joker6: default IPv6 for unresolved domains (Default: False) set to False to disable, None to mirror the interface's IPv6. - :param match: a dictionary of {names: (ip, ipv6)} + :param match: a dictionary of {name: val} where name is a string representing + a domain name (A, AAAA) and val is a tuple of 2 elements, each + representing an IP or a list of IPs + :param srvmatch: a dictionary of {name: (port, target)} used for SRV :param from_ip: an source IP to filter. Can contain a netmask + :param from_ip6: an source IPv6 to filter. Can contain a netmask + :param ttl: the DNS time to live (in seconds) + :param src_ip: + :param src_ip6: + + Example: + + >>> dns_spoof(joker="192.168.0.2", iface="eth0") + >>> dns_spoof(match={ + ... "_ldap._tcp.dc._msdcs.DOMAIN.LOCAL.": (389, "srv1.domain.local") + ... }) """ if match is None: self.match = {} else: - self.match = match + assert all(isinstance(x, (tuple, list)) for x in match.values()), ( + "'match' values must be a tuple of 2 elements: ('', '')" + ". They can be None" + ) + self.match = {bytes_encode(k): v for k, v in match.items()} + if srvmatch is None: + self.srvmatch = {} + else: + assert all(isinstance(x, (tuple, list)) for x in srvmatch.values()), ( + "'srvmatch' values must be a tuple of 2 elements: (port, 'target')" + ) + self.srvmatch = {bytes_encode(k): v for k, v in srvmatch.items()} self.joker = joker self.joker6 = joker6 if isinstance(from_ip, str): self.from_ip = Net(from_ip) else: self.from_ip = from_ip + if isinstance(from_ip6, str): + self.from_ip6 = Net(from_ip6) + else: + self.from_ip6 = from_ip6 + self.src_ip = src_ip + self.src_ip6 = src_ip6 + self.ttl = ttl def is_request(self, req): from scapy.layers.inet6 import IPv6 return ( req.haslayer(self.cls) and - req.getlayer(self.cls).qr == 0 and - (not self.from_ip or ( - req[IPv6].src in req if IPv6 in req else req[IP].src - ) in self.from_ip) + req.getlayer(self.cls).qr == 0 and ( + ( + not self.from_ip6 or req[IPv6].src in self.from_ip6 + ) + if IPv6 in req else + ( + not self.from_ip or req[IP].src in self.from_ip + ) + ) ) def make_reply(self, req): - IPcls = IPv6 if IPv6 in req else IP - resp = IPcls(dst=req[IPcls].src) / UDP(sport=req.dport, dport=req.sport) - dns = req.getlayer(self.cls) - if req.qd.qtype == 28: - # AAAA - if self.joker6 is False: - return - rdata = self.match.get( - dns.qd.qname, - self.joker or get_if_addr6(self.optsniff.get("iface", conf.iface)) - ) - if isinstance(rdata, (tuple, list)): - rdata = rdata[1] - resp /= self.cls(id=dns.id, qr=1, qd=dns.qd, - an=DNSRR(rrname=dns.qd.qname, ttl=10, rdata=rdata, - type=28)) + if IPv6 in req: + resp = IPv6(dst=req[IPv6].src, src=self.src_ip6) else: - if self.joker is False: - return - rdata = self.match.get( - dns.qd.qname, - self.joker or get_if_addr(self.optsniff.get("iface", conf.iface)) - ) - if isinstance(rdata, (tuple, list)): - # Fallback - rdata = rdata[0] - resp /= self.cls(id=dns.id, qr=1, qd=dns.qd, - an=DNSRR(rrname=dns.qd.qname, ttl=10, rdata=rdata)) + resp = IP(dst=req[IP].src, src=self.src_ip) + resp /= UDP(sport=req.dport, dport=req.sport) + ans = [] + req = req.getlayer(self.cls) + for rq in req.qd: + if rq.qtype in [1, 28]: + # A or AAAA + if rq.qtype == 28: + # AAAA + try: + rdata = self.match[rq.qname][1] + except KeyError: + if self.joker6 is False: + return + rdata = self.joker6 or get_if_addr6( + self.optsniff.get("iface", conf.iface) + ) + elif rq.qtype == 1: + # A + try: + rdata = self.match[rq.qname][0] + except KeyError: + if self.joker is False: + return + rdata = self.joker or get_if_addr( + self.optsniff.get("iface", conf.iface) + ) + if rdata is None: + # Ignore None + return + # Common A and AAAA + if not isinstance(rdata, list): + rdata = [rdata] + ans.extend([ + DNSRR(rrname=rq.qname, ttl=self.ttl, rdata=x, type=rq.qtype) + for x in rdata + ]) + elif rq.qtype == 33: + # SRV + try: + port, target = self.srvmatch[rq.qname] + except KeyError: + return + ans.append(DNSRRSRV( + rrname=rq.qname, + port=port, + target=target, + weight=100, + ttl=self.ttl + )) + else: + # Not handled + continue + # Common: All + if not ans: + return + resp /= self.cls(id=req.id, qr=1, qd=req.qd, an=ans) return resp diff --git a/scapy/layers/dot11.py b/scapy/layers/dot11.py index 5eeafa0ed29..1cc38b6f8e9 100644 --- a/scapy/layers/dot11.py +++ b/scapy/layers/dot11.py @@ -38,6 +38,7 @@ LEShortEnumField, LEShortField, LESignedIntField, + MayEnd, MultipleTypeField, OUIField, PacketField, @@ -1270,14 +1271,15 @@ class Dot11EltCountry(Dot11Elt): ByteEnumField("ID", 7, _dot11_id_enum), ByteField("len", None), StrFixedLenField("country_string", b"\0\0\0", length=3), - PacketListField( + MayEnd(PacketListField( "descriptors", [], Dot11EltCountryConstraintTriplet, length_from=lambda pkt: ( pkt.len - 3 - (pkt.len % 3) ) - ), + )), + # When this extension is last, padding appears to be omitted ConditionalField( ByteField("pad", 0), lambda pkt: (len(pkt.descriptors) + 1) % 2 diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py index 125e57ec272..b54cca234c4 100644 --- a/scapy/layers/inet6.py +++ b/scapy/layers/inet6.py @@ -34,11 +34,32 @@ MTU, ) from scapy.error import log_runtime, warning -from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ - DestIP6Field, FieldLenField, FlagsField, IntField, IP6Field, \ - LongField, MACField, PacketLenField, PacketListField, ShortEnumField, \ - ShortField, SourceIP6Field, StrField, StrFixedLenField, StrLenField, \ - X3BytesField, XBitField, XIntField, XShortField +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + DestIP6Field, + FieldLenField, + FlagsField, + IntField, + IP6Field, + LongField, + MACField, + MayEnd, + PacketLenField, + PacketListField, + ShortEnumField, + ShortField, + SourceIP6Field, + StrField, + StrFixedLenField, + StrLenField, + X3BytesField, + XBitField, + XIntField, + XShortField, +) from scapy.layers.inet import IP, IPTools, TCP, TCPerror, TracerouteResult, \ UDP, UDPerror from scapy.layers.l2 import CookedLinux, Ether, GRE, Loopback, SNAP @@ -1767,44 +1788,22 @@ def mysummary(self): class TruncPktLenField(PacketLenField): - __slots__ = ["cur_shift"] - - def __init__(self, name, default, cls, cur_shift, length_from=None, shift=0): # noqa: E501 - PacketLenField.__init__(self, name, default, cls, length_from=length_from) # noqa: E501 - self.cur_shift = cur_shift - - def getfield(self, pkt, s): - tmp_len = self.length_from(pkt) - i = self.m2i(pkt, s[:tmp_len]) - return s[tmp_len:], i - - def m2i(self, pkt, m): - s = None - try: # It can happen we have sth shorter than 40 bytes - s = self.cls(m) - except Exception: - return conf.raw_layer(m) - return s - def i2m(self, pkt, x): - s = raw(x) + s = bytes(x) tmp_len = len(s) - r = (tmp_len + self.cur_shift) % 8 - tmp_len = tmp_len - r - return s[:tmp_len] + return s[:tmp_len - (tmp_len % 8)] def i2len(self, pkt, i): return len(self.i2m(pkt, i)) -# Faire un post_build pour le recalcul de la taille (en multiple de 8 octets) class ICMPv6NDOptRedirectedHdr(_ICMPv6NDGuessPayload, Packet): name = "ICMPv6 Neighbor Discovery Option - Redirected Header" fields_desc = [ByteField("type", 4), FieldLenField("len", None, length_of="pkt", fmt="B", - adjust=lambda pkt, x:(x + 8) // 8), - StrFixedLenField("res", b"\x00" * 6, 6), - TruncPktLenField("pkt", b"", IPv6, 8, + adjust=lambda pkt, x: (x + 8) // 8), + MayEnd(StrFixedLenField("res", b"\x00" * 6, 6)), + TruncPktLenField("pkt", b"", IPv6, length_from=lambda pkt: 8 * pkt.len - 8)] # See which value should be used for default MTU instead of 1280 diff --git a/scapy/layers/llmnr.py b/scapy/layers/llmnr.py index 9cce97cd8c0..c393d1f3109 100644 --- a/scapy/layers/llmnr.py +++ b/scapy/layers/llmnr.py @@ -19,10 +19,9 @@ from scapy.compat import orb from scapy.layers.inet import UDP from scapy.layers.dns import ( - DNSQRField, - DNSRRField, - DNSRRCountField, + DNSCompressedPacket, DNS_am, + DNS, ) @@ -30,37 +29,35 @@ _LLMNR_IPv4_mcast_addr = "224.0.0.252" -class LLMNRQuery(Packet): +class LLMNRQuery(DNSCompressedPacket): name = "Link Local Multicast Node Resolution - Query" - fields_desc = [ShortField("id", 0), - BitField("qr", 0, 1), - BitEnumField("opcode", 0, 4, {0: "QUERY"}), - BitField("c", 0, 1), - BitField("tc", 0, 2), - BitField("z", 0, 4), - BitEnumField("rcode", 0, 4, {0: "ok"}), - DNSRRCountField("qdcount", None, "qd"), - DNSRRCountField("ancount", None, "an"), - DNSRRCountField("nscount", None, "ns"), - DNSRRCountField("arcount", None, "ar"), - DNSQRField("qd", "qdcount", None), - DNSRRField("an", "ancount", None), - DNSRRField("ns", "nscount", None), - DNSRRField("ar", "arcount", None, 0)] + qd = [] + fields_desc = [ + ShortField("id", 0), + BitField("qr", 0, 1), + BitEnumField("opcode", 0, 4, {0: "QUERY"}), + BitField("c", 0, 1), + BitField("tc", 0, 2), + BitField("z", 0, 4) + ] + DNS.fields_desc[-9:] overload_fields = {UDP: {"sport": 5355, "dport": 5355}} + def get_full(self): + # Required for DNSCompressedPacket + return self.original + def hashret(self): return struct.pack("!H", self.id) def mysummary(self): if self.an: return "LLMNRResponse '%s' is at '%s'" % ( - self.an.rrname.decode(errors="backslashreplace"), - self.an.rdata, + self.an[0].rrname.decode(errors="backslashreplace"), + self.an[0].rdata, ), [UDP] if self.qd: return "LLMNRQuery who has '%s'" % ( - self.qd.qname.decode(errors="backslashreplace"), + self.qd[0].qname.decode(errors="backslashreplace"), ), [UDP] diff --git a/scapy/layers/ntp.py b/scapy/layers/ntp.py index e7585743294..9b0d4e4ae6b 100644 --- a/scapy/layers/ntp.py +++ b/scapy/layers/ntp.py @@ -13,12 +13,32 @@ import datetime from scapy.packet import Packet, bind_layers -from scapy.fields import BitField, BitEnumField, ByteField, ByteEnumField, \ - XByteField, SignedByteField, FlagsField, ShortField, LEShortField, \ - IntField, LEIntField, FixedPointField, IPField, StrField, \ - StrFixedLenField, StrFixedLenEnumField, XStrFixedLenField, PacketField, \ - PacketLenField, PacketListField, FieldListField, ConditionalField, \ - PadField +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + FieldListField, + FixedPointField, + FlagsField, + IPField, + IntField, + LEIntField, + LEShortField, + MayEnd, + PacketField, + PacketLenField, + PacketListField, + PadField, + ShortField, + SignedByteField, + StrField, + StrFixedLenEnumField, + StrFixedLenField, + XByteField, + XStrFixedLenField, +) from scapy.layers.inet6 import IP6Field from scapy.layers.inet import UDP from scapy.utils import lhex @@ -741,6 +761,8 @@ class NTPControlDataPacketLenField(PacketLenField): def m2i(self, pkt, m): ret = None + if not m: + return ret # op_code == CTL_OP_READSTAT if pkt.op_code == 1: @@ -808,8 +830,8 @@ class NTPControl(NTP): ShortField("association_id", 0), ShortField("offset", 0), ShortField("count", None), - NTPControlDataPacketLenField( - "data", "", Packet, length_from=lambda p: p.count), + MayEnd(NTPControlDataPacketLenField( + "data", "", Packet, length_from=lambda p: p.count)), PacketField("authenticator", "", NTPAuthenticator), ] @@ -1156,7 +1178,8 @@ class NTPInfoMemStats(Packet): "hashcount", [0.0 for i in range(0, _NTP_HASH_SIZE)], ByteField("", 0), - count_from=lambda p: _NTP_HASH_SIZE + count_from=lambda p: _NTP_HASH_SIZE, + max_count=_NTP_HASH_SIZE ) ] diff --git a/scapy/layers/tls/extensions.py b/scapy/layers/tls/extensions.py index c5d80d329c5..87ffe67219c 100644 --- a/scapy/layers/tls/extensions.py +++ b/scapy/layers/tls/extensions.py @@ -11,9 +11,22 @@ import os import struct -from scapy.fields import ByteEnumField, ByteField, EnumField, FieldLenField, \ - FieldListField, IntField, PacketField, PacketListField, ShortEnumField, \ - ShortField, StrFixedLenField, StrLenField, XStrLenField +from scapy.fields import ( + ByteEnumField, + ByteField, + EnumField, + FieldLenField, + FieldListField, + IntField, + MayEnd, + PacketField, + PacketListField, + ShortEnumField, + ShortField, + StrFixedLenField, + StrLenField, + XStrLenField, +) from scapy.packet import Packet, Raw, Padding from scapy.layers.x509 import X509_Extensions from scapy.layers.tls.basefields import _tls_version @@ -196,8 +209,8 @@ def addfield(self, pkt, s, val): class TLS_Ext_ServerName(TLS_Ext_PrettyPacketList): # RFC 4366 name = "TLS Extension - Server Name" fields_desc = [ShortEnumField("type", 0, _tls_ext), - FieldLenField("len", None, length_of="servernames", - adjust=lambda pkt, x: x + 2), + MayEnd(FieldLenField("len", None, length_of="servernames", + adjust=lambda pkt, x: x + 2)), ServerLenField("servernameslen", None, length_of="servernames"), ServerListField("servernames", [], ServerName, @@ -207,7 +220,7 @@ class TLS_Ext_ServerName(TLS_Ext_PrettyPacketList): # RFC 4366 class TLS_Ext_EncryptedServerName(TLS_Ext_PrettyPacketList): name = "TLS Extension - Encrypted Server Name" fields_desc = [ShortEnumField("type", 0xffce, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), EnumField("cipher", None, _tls_cipher_suites), ShortEnumField("key_exchange_group", None, _tls_named_groups), @@ -228,7 +241,7 @@ class TLS_Ext_EncryptedServerName(TLS_Ext_PrettyPacketList): class TLS_Ext_MaxFragLen(TLS_Ext_Unknown): # RFC 4366 name = "TLS Extension - Max Fragment Length" fields_desc = [ShortEnumField("type", 1, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ByteEnumField("maxfraglen", 4, {1: "2^9", 2: "2^10", 3: "2^11", @@ -238,7 +251,7 @@ class TLS_Ext_MaxFragLen(TLS_Ext_Unknown): # RFC 4366 class TLS_Ext_ClientCertURL(TLS_Ext_Unknown): # RFC 4366 name = "TLS Extension - Client Certificate URL" fields_desc = [ShortEnumField("type", 2, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] _tls_trusted_authority_types = {0: "pre_agreed", @@ -310,7 +323,7 @@ def m2i(self, pkt, m): class TLS_Ext_TrustedCAInd(TLS_Ext_Unknown): # RFC 4366 name = "TLS Extension - Trusted CA Indication" fields_desc = [ShortEnumField("type", 3, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("talen", None, length_of="ta"), _TAListField("ta", [], Raw, length_from=lambda pkt: pkt.talen)] @@ -319,7 +332,7 @@ class TLS_Ext_TrustedCAInd(TLS_Ext_Unknown): # RFC 4366 class TLS_Ext_TruncatedHMAC(TLS_Ext_Unknown): # RFC 4366 name = "TLS Extension - Truncated HMAC" fields_desc = [ShortEnumField("type", 4, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class ResponderID(Packet): @@ -363,7 +376,7 @@ def m2i(self, pkt, m): class TLS_Ext_CSR(TLS_Ext_Unknown): # RFC 4366 name = "TLS Extension - Certificate Status Request" fields_desc = [ShortEnumField("type", 5, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ByteEnumField("stype", None, _cert_status_type), _StatusReqField("req", [], Raw, length_from=lambda pkt: pkt.len - 1)] @@ -372,7 +385,7 @@ class TLS_Ext_CSR(TLS_Ext_Unknown): # RFC 4366 class TLS_Ext_UserMapping(TLS_Ext_Unknown): # RFC 4681 name = "TLS Extension - User Mapping" fields_desc = [ShortEnumField("type", 6, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("umlen", None, fmt="B", length_of="um"), FieldListField("um", [], ByteField("umtype", 0), @@ -383,7 +396,7 @@ class TLS_Ext_ClientAuthz(TLS_Ext_Unknown): # RFC 5878 """ XXX Unsupported """ name = "TLS Extension - Client Authz" fields_desc = [ShortEnumField("type", 7, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ] @@ -391,7 +404,7 @@ class TLS_Ext_ServerAuthz(TLS_Ext_Unknown): # RFC 5878 """ XXX Unsupported """ name = "TLS Extension - Server Authz" fields_desc = [ShortEnumField("type", 8, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ] @@ -401,7 +414,7 @@ class TLS_Ext_ServerAuthz(TLS_Ext_Unknown): # RFC 5878 class TLS_Ext_ClientCertType(TLS_Ext_Unknown): # RFC 5081 name = "TLS Extension - Certificate Type (client version)" fields_desc = [ShortEnumField("type", 9, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("ctypeslen", None, length_of="ctypes"), FieldListField("ctypes", [0, 1], ByteEnumField("certtypes", None, @@ -412,7 +425,7 @@ class TLS_Ext_ClientCertType(TLS_Ext_Unknown): # RFC 5081 class TLS_Ext_ServerCertType(TLS_Ext_Unknown): # RFC 5081 name = "TLS Extension - Certificate Type (server version)" fields_desc = [ShortEnumField("type", 9, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ByteEnumField("ctype", None, _tls_cert_types)] @@ -436,7 +449,7 @@ class TLS_Ext_SupportedGroups(TLS_Ext_Unknown): """ name = "TLS Extension - Supported Groups" fields_desc = [ShortEnumField("type", 10, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("groupslen", None, length_of="groups"), FieldListField("groups", [], ShortEnumField("ng", None, @@ -456,7 +469,7 @@ class TLS_Ext_SupportedEllipticCurves(TLS_Ext_SupportedGroups): # RFC 4492 class TLS_Ext_SupportedPointFormat(TLS_Ext_Unknown): # RFC 4492 name = "TLS Extension - Supported Point Format" fields_desc = [ShortEnumField("type", 11, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("ecpllen", None, fmt="B", length_of="ecpl"), FieldListField("ecpl", [0], ByteEnumField("nc", None, @@ -467,7 +480,7 @@ class TLS_Ext_SupportedPointFormat(TLS_Ext_Unknown): # RFC 4492 class TLS_Ext_SignatureAlgorithms(TLS_Ext_Unknown): # RFC 5246 name = "TLS Extension - Signature Algorithms" fields_desc = [ShortEnumField("type", 13, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), SigAndHashAlgsLenField("sig_algs_len", None, length_of="sig_algs"), SigAndHashAlgsField("sig_algs", [], @@ -479,7 +492,7 @@ class TLS_Ext_SignatureAlgorithms(TLS_Ext_Unknown): # RFC 5246 class TLS_Ext_Heartbeat(TLS_Ext_Unknown): # RFC 6520 name = "TLS Extension - Heartbeat" fields_desc = [ShortEnumField("type", 0x0f, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ByteEnumField("heartbeat_mode", 2, {1: "peer_allowed_to_send", 2: "peer_not_allowed_to_send"})] @@ -504,7 +517,7 @@ def i2repr(self, pkt, x): class TLS_Ext_ALPN(TLS_Ext_PrettyPacketList): # RFC 7301 name = "TLS Extension - Application Layer Protocol Negotiation" fields_desc = [ShortEnumField("type", 0x10, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("protocolslen", None, length_of="protocols"), ProtocolListField("protocols", [], ProtocolName, length_from=lambda pkt:pkt.protocolslen)] @@ -521,13 +534,13 @@ class TLS_Ext_Padding(TLS_Ext_Unknown): # RFC 7685 class TLS_Ext_EncryptThenMAC(TLS_Ext_Unknown): # RFC 7366 name = "TLS Extension - Encrypt-then-MAC" fields_desc = [ShortEnumField("type", 0x16, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_ExtendedMasterSecret(TLS_Ext_Unknown): # RFC 7627 name = "TLS Extension - Extended Master Secret" fields_desc = [ShortEnumField("type", 0x17, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_SessionTicket(TLS_Ext_Unknown): # RFC 5077 @@ -545,25 +558,25 @@ class TLS_Ext_SessionTicket(TLS_Ext_Unknown): # RFC 5077 class TLS_Ext_KeyShare(TLS_Ext_Unknown): name = "TLS Extension - Key Share (dummy class)" fields_desc = [ShortEnumField("type", 0x33, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_PreSharedKey(TLS_Ext_Unknown): name = "TLS Extension - Pre Shared Key (dummy class)" fields_desc = [ShortEnumField("type", 0x29, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_EarlyDataIndication(TLS_Ext_Unknown): name = "TLS Extension - Early Data" fields_desc = [ShortEnumField("type", 0x2a, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_EarlyDataIndicationTicket(TLS_Ext_Unknown): name = "TLS Extension - Ticket Early Data Info" fields_desc = [ShortEnumField("type", 0x2a, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), IntField("max_early_data_size", 0)] @@ -575,13 +588,13 @@ class TLS_Ext_EarlyDataIndicationTicket(TLS_Ext_Unknown): class TLS_Ext_SupportedVersions(TLS_Ext_Unknown): name = "TLS Extension - Supported Versions (dummy class)" fields_desc = [ShortEnumField("type", 0x2b, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_SupportedVersion_CH(TLS_Ext_Unknown): name = "TLS Extension - Supported Versions (for ClientHello)" fields_desc = [ShortEnumField("type", 0x2b, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("versionslen", None, fmt='B', length_of="versions"), FieldListField("versions", [], @@ -593,7 +606,7 @@ class TLS_Ext_SupportedVersion_CH(TLS_Ext_Unknown): class TLS_Ext_SupportedVersion_SH(TLS_Ext_Unknown): name = "TLS Extension - Supported Versions (for ServerHello)" fields_desc = [ShortEnumField("type", 0x2b, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ShortEnumField("version", None, _tls_version)] @@ -604,7 +617,7 @@ class TLS_Ext_SupportedVersion_SH(TLS_Ext_Unknown): class TLS_Ext_Cookie(TLS_Ext_Unknown): name = "TLS Extension - Cookie" fields_desc = [ShortEnumField("type", 0x2c, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("cookielen", None, length_of="cookie"), XStrLenField("cookie", "", length_from=lambda pkt: pkt.cookielen)] @@ -622,7 +635,7 @@ def build(self): class TLS_Ext_PSKKeyExchangeModes(TLS_Ext_Unknown): name = "TLS Extension - PSK Key Exchange Modes" fields_desc = [ShortEnumField("type", 0x2d, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("kxmodeslen", None, fmt='B', length_of="kxmodes"), FieldListField("kxmodes", [], @@ -634,7 +647,7 @@ class TLS_Ext_PSKKeyExchangeModes(TLS_Ext_Unknown): class TLS_Ext_TicketEarlyDataInfo(TLS_Ext_Unknown): name = "TLS Extension - Ticket Early Data Info" fields_desc = [ShortEnumField("type", 0x2e, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), IntField("max_early_data_size", 0)] @@ -652,13 +665,13 @@ class TLS_Ext_NPN(TLS_Ext_PrettyPacketList): class TLS_Ext_PostHandshakeAuth(TLS_Ext_Unknown): # RFC 8446 name = "TLS Extension - Post Handshake Auth" fields_desc = [ShortEnumField("type", 0x31, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_SignatureAlgorithmsCert(TLS_Ext_Unknown): # RFC 8446 name = "TLS Extension - Signature Algorithms Cert" fields_desc = [ShortEnumField("type", 0x32, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), SigAndHashAlgsLenField("sig_algs_len", None, length_of="sig_algs"), SigAndHashAlgsField("sig_algs", [], @@ -670,7 +683,7 @@ class TLS_Ext_SignatureAlgorithmsCert(TLS_Ext_Unknown): # RFC 8446 class TLS_Ext_RenegotiationInfo(TLS_Ext_Unknown): # RFC 5746 name = "TLS Extension - Renegotiation Indication" fields_desc = [ShortEnumField("type", 0xff01, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("reneg_conn_len", None, fmt='B', length_of="renegotiated_connection"), StrLenField("renegotiated_connection", "", @@ -680,7 +693,7 @@ class TLS_Ext_RenegotiationInfo(TLS_Ext_Unknown): # RFC 5746 class TLS_Ext_RecordSizeLimit(TLS_Ext_Unknown): # RFC 8449 name = "TLS Extension - Record Size Limit" fields_desc = [ShortEnumField("type", 0x1c, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ShortField("record_size_limit", None)] diff --git a/scapy/layers/tls/keyexchange_tls13.py b/scapy/layers/tls/keyexchange_tls13.py index 1df598de82f..878e88b3fc6 100644 --- a/scapy/layers/tls/keyexchange_tls13.py +++ b/scapy/layers/tls/keyexchange_tls13.py @@ -16,6 +16,7 @@ FieldLenField, IntField, PacketField, + PacketLenField, PacketListField, ShortEnumField, ShortField, @@ -23,7 +24,7 @@ StrLenField, XStrLenField, ) -from scapy.packet import Packet, Padding +from scapy.packet import Packet from scapy.layers.tls.extensions import TLS_Ext_Unknown, _tls_ext from scapy.layers.tls.crypto.groups import ( _tls_named_curves, @@ -228,27 +229,25 @@ class Ticket(Packet): StrFixedLenField("mac", None, 32)] -class TicketField(PacketField): - __slots__ = ["length_from"] - - def __init__(self, name, default, length_from=None, **kargs): - self.length_from = length_from - PacketField.__init__(self, name, default, Ticket, **kargs) - +class TicketField(PacketLenField): def m2i(self, pkt, m): - tmp_len = self.length_from(pkt) - tbd, rem = m[:tmp_len], m[tmp_len:] - return self.cls(tbd) / Padding(rem) + if len(m) < 64: + # Minimum ticket size is 64 bytes + return conf.raw_layer(m) + return self.cls(m) class PSKIdentity(Packet): name = "PSK Identity" fields_desc = [FieldLenField("identity_len", None, length_of="identity"), - TicketField("identity", "", + TicketField("identity", "", Ticket, length_from=lambda pkt: pkt.identity_len), IntField("obfuscated_ticket_age", 0)] + def default_payload_class(self, payload): + return conf.padding_layer + class PSKBinderEntry(Packet): name = "PSK Binder Entry" @@ -257,6 +256,9 @@ class PSKBinderEntry(Packet): StrLenField("binder", "", length_from=lambda pkt: pkt.binder_len)] + def default_payload_class(self, payload): + return conf.padding_layer + class TLS_Ext_PreSharedKey_CH(TLS_Ext_Unknown): # XXX define post_build and post_dissection methods diff --git a/scapy/packet.py b/scapy/packet.py index 4c35c517d28..e35b1bfc45a 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -30,6 +30,7 @@ Field, FlagsField, FlagValue, + MayEnd, MultiEnumField, MultipleTypeField, PacketListField, @@ -1005,8 +1006,6 @@ def do_dissect(self, s): _raw = s self.raw_packet_cache_fields = {} for f in self.fields_desc: - if not s: - break s, fval = f.getfield(self, s) # Skip unused ConditionalField if isinstance(f, ConditionalField) and fval is None: @@ -1017,6 +1016,11 @@ def do_dissect(self, s): self.raw_packet_cache_fields[f.name] = \ self._raw_packet_cache_field_value(f, fval, copy=True) self.fields[f.name] = fval + # Nothing left to dissect + if not s and (isinstance(f, MayEnd) or + (fval is not None and isinstance(f, ConditionalField) and + isinstance(f.fld, MayEnd))): + break self.raw_packet_cache = _raw[:-len(s)] if s else _raw self.explicit = 1 return s diff --git a/test/answering_machines.uts b/test/answering_machines.uts index 54eee4a705c..bb80f66c391 100644 --- a/test/answering_machines.uts +++ b/test/answering_machines.uts @@ -11,11 +11,12 @@ import mock @mock.patch("scapy.ansmachine.sniff") def test_am(cls_name, packet_query, check_reply, mock_sniff, **kargs): + packet_query = packet_query.__class__(bytes(packet_query)) def sniff(*args,**kargs): kargs["prn"](packet_query) mock_sniff.side_effect = sniff am = cls_name(**kargs) - am.send_reply = check_reply + am.send_reply = lambda x: check_reply(x.__class__(bytes(x))) am() @@ -32,10 +33,10 @@ test_am(BOOTP_am, = DHCP_am def check_DHCP_am_reply(packet): assert DHCP in packet and len(packet[DHCP].options) - assert ("domain", "localnet") in packet[DHCP].options + assert ("domain", b"localnet") in packet[DHCP].options test_am(DHCP_am, - Ether()/IP()/UDP()/BOOTP(op=1)/DHCP(), + Ether()/IP()/UDP()/BOOTP(op=1)/DHCP(options=[('message-type', 'request')]), check_DHCP_am_reply) @@ -64,7 +65,8 @@ test_am(ICMPEcho_am, = DNS_am def check_DNS_am_reply(packet): assert DNS in packet and packet[DNS].ancount == 1 - assert packet[DNS].an.rdata == "192.168.1.1" + assert packet[DNS].an[0].rdata == "192.168.1.1" + assert packet[DNS].qd[0].qname == b"www.secdev.org." test_am(DNS_am, IP()/UDP()/DNS(qd=DNSQR(qname="www.secdev.org")), @@ -138,7 +140,7 @@ test_WiFi_am(Dot11(FCfield="to-DS")/IP()/TCP()/"Scapy", = NBNS_am def check_NBNS_am_reply(name): def check(packet): - assert NBNSQueryResponse in packet and packet[NBNSQueryResponse].RR_NAME == bytes_encode(name) + assert NBNSQueryResponse in packet and packet[NBNSQueryResponse].RR_NAME.strip() == bytes_encode(name) return check for server_name in (None, "", b"test", "test"): diff --git a/test/contrib/automotive/doip.uts b/test/contrib/automotive/doip.uts index 33dc9793f6d..99ebef167d1 100644 --- a/test/contrib/automotive/doip.uts +++ b/test/contrib/automotive/doip.uts @@ -297,7 +297,7 @@ assert p.previous_msg == b'\x10\x03' + pcap based tests = read diag_ack pcap file -pkt = rdpcap("test/pcaps/doip_ack.pcap").res[0] +pkt = rdpcap(scapy_path("test/pcaps/doip_ack.pcap")).res[0] assert len(pkt) == 70 @@ -313,7 +313,7 @@ assert pkt.previous_msg == b'\x22\xFD\x31' = read main pcap file -pkts = rdpcap("test/pcaps/doip.pcap.gz") +pkts = rdpcap(scapy_path("test/pcaps/doip.pcap.gz")) ips = [p for p in pkts if p.proto == 6] assert len(ips) > 1 diff --git a/test/contrib/automotive/ecu.uts b/test/contrib/automotive/ecu.uts index af1d70e67db..ede976bc0f5 100644 --- a/test/contrib/automotive/ecu.uts +++ b/test/contrib/automotive/ecu.uts @@ -606,8 +606,9 @@ assert unanswered_packets[0].diagnosticSessionType == 4 = Analyze multiple UDS messages -with PcapReader(scapy_path("test/pcaps/ecu_trace.pcap.gz")) as sock: - udsmsgs = sniff(session=ISOTPSession, session_kwargs={"use_ext_address":False, "basecls":UDS}, count=50, opened_socket=sock, timeout=3) +udsmsgs = sniff(offline=scapy_path("test/pcaps/ecu_trace.pcap.gz"), + session=ISOTPSession, session_kwargs={"use_ext_address":False, "basecls":UDS}, + count=50, timeout=3) assert len(udsmsgs) == 50 @@ -701,8 +702,10 @@ session = EcuSession(verbose=False, store_supported_responses=False) conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 conf.contribs['CAN']['swap-bytes'] = True -with PcapReader(scapy_path("test/pcaps/gmlan_trace.pcap.gz")) as sock: - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=200, opened_socket=sock, timeout=6) +conf.debug_dissector = True +gmlanmsgs = sniff(offline=scapy_path("test/pcaps/gmlan_trace.pcap.gz"), session=ISOTPSession, + session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, + count=200, timeout=6) ecu = session.ecu assert len(ecu.supported_responses) == 0 diff --git a/test/contrib/automotive/obd/obd.uts b/test/contrib/automotive/obd/obd.uts index 17f3df69a82..fa65e95e447 100644 --- a/test/contrib/automotive/obd/obd.uts +++ b/test/contrib/automotive/obd/obd.uts @@ -445,7 +445,7 @@ assert p.data_records[0].turbocharger_a_turbine_inlet_temperature == \ round((0x2233 * 0.1) - 40, 3) assert p.data_records[0].turbocharger_a_turbine_outlet_temperature == \ round((0x4455 * 0.1) - 40, 3) -r = OBD(b'\x02\x75') +r = OBD(b'\x02\x75\x00') assert p.answers(r) @@ -465,7 +465,7 @@ assert p.data_records[0].sensor2 == 1707.7 assert p.data_records[0].sensor3 == 1759.1 assert p.data_records[0].sensor4 == 1810.5 -r = OBD(b'\x02\x78') +r = OBD(b'\x02\x78\x00') assert p.answers(r) = Check dissecting a response for Service 02 PID 7F @@ -485,7 +485,7 @@ assert p.data_records[0].total == 0xFFFFFFFFFFFFFFFF assert p.data_records[0].total_idle == 0x0102030405060708 assert p.data_records[0].total_with_pto_active == 0x0011223344556677 -r = OBD(b'\x02\x7F') +r = OBD(b'\x02\x7F\x00') assert p.answers(r) @@ -497,7 +497,7 @@ assert p.data_records[0].pid == 0x89 assert p.data_records[0].frame_no == 0x01 assert p.data_records[0].data == b'ABCDEFGHIKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOP' -r = OBD(b'\x02\x89') +r = OBD(b'\x02\x89\x00') assert p.answers(r) = Check dissecting a response for Service 02 PID 0C, 05, 04 diff --git a/test/contrib/automotive/uds.uts b/test/contrib/automotive/uds.uts index 221554dd9a7..ead0097accb 100644 --- a/test/contrib/automotive/uds.uts +++ b/test/contrib/automotive/uds.uts @@ -26,7 +26,7 @@ dsc.hashret() == dscpr.hashret() = Check if negative response answers dsc = UDS(b'\x10') -neg = UDS(b'\x7f\x10') +neg = UDS(b'\x7f\x10\x00') assert neg.answers(dsc) = CHECK hashret NEG @@ -35,7 +35,7 @@ dsc.hashret() == neg.hashret() = Check if negative response answers not dsc = UDS(b'\x10') -neg = UDS(b'\x7f\x11') +neg = UDS(b'\x7f\x11\x00') assert not neg.answers(dsc) = Check if positive response answers not diff --git a/test/contrib/bgp.uts b/test/contrib/bgp.uts index d9e0c5992a9..6b1edfea1f9 100644 --- a/test/contrib/bgp.uts +++ b/test/contrib/bgp.uts @@ -315,7 +315,7 @@ raw(BGPAuthenticationInformation()) == b'\x00' = BGPAuthenticationInformation - Basic dissection c = BGPAuthenticationInformation(b'\x00') -c.authentication_code == 0 and c.authentication_data == None +c.authentication_code == 0 and not c.authentication_data ################################# BGPOptParam ################################# diff --git a/test/contrib/eigrp.uts b/test/contrib/eigrp.uts index cee8a23003a..cb29649e953 100644 --- a/test/contrib/eigrp.uts +++ b/test/contrib/eigrp.uts @@ -85,37 +85,37 @@ assert inet_pton(socket.AF_INET6, f.randval()) = EIGRPGuessPayloadClass function: Return Parameters TLV from scapy.contrib.eigrp import _EIGRPGuessPayloadClass -isinstance(_EIGRPGuessPayloadClass(b"\x00\x01"), EIGRPParam) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x01" + b"\x00" * 50), EIGRPParam) = EIGRPGuessPayloadClass function: Return Authentication Data TLV -isinstance(_EIGRPGuessPayloadClass(b"\x00\x02"), EIGRPAuthData) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x02" + b"\x00" * 50), EIGRPAuthData) = EIGRPGuessPayloadClass function: Return Sequence TLV -isinstance(_EIGRPGuessPayloadClass(b"\x00\x03"), EIGRPSeq) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x03" + b"\x00" * 50), EIGRPSeq) = EIGRPGuessPayloadClass function: Return Software Version TLV -isinstance(_EIGRPGuessPayloadClass(b"\x00\x04"), EIGRPSwVer) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x04" + b"\x00" * 50), EIGRPSwVer) = EIGRPGuessPayloadClass function: Return Next Multicast Sequence TLV -isinstance(_EIGRPGuessPayloadClass(b"\x00\x05"), EIGRPNms) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x05" + b"\x00" * 50), EIGRPNms) = EIGRPGuessPayloadClass function: Return Stub Router TLV -isinstance(_EIGRPGuessPayloadClass(b"\x00\x06"), EIGRPStub) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x06" + b"\x00" * 50), EIGRPStub) = EIGRPGuessPayloadClass function: Return Internal Route TLV -isinstance(_EIGRPGuessPayloadClass(b"\x01\x02"), EIGRPIntRoute) +isinstance(_EIGRPGuessPayloadClass(b"\x01\x02" + b"\x00" * 50), EIGRPIntRoute) = EIGRPGuessPayloadClass function: Return External Route TLV -isinstance(_EIGRPGuessPayloadClass(b"\x01\x03"), EIGRPExtRoute) +isinstance(_EIGRPGuessPayloadClass(b"\x01\x03" + b"\x00" * 50), EIGRPExtRoute) = EIGRPGuessPayloadClass function: Return IPv6 Internal Route TLV -isinstance(_EIGRPGuessPayloadClass(b"\x04\x02"), EIGRPv6IntRoute) +isinstance(_EIGRPGuessPayloadClass(b"\x04\x02" + b"\x00" * 50), EIGRPv6IntRoute) = EIGRPGuessPayloadClass function: Return IPv6 External Route TLV -isinstance(_EIGRPGuessPayloadClass(b"\x04\x03"), EIGRPv6ExtRoute) +isinstance(_EIGRPGuessPayloadClass(b"\x04\x03" + b"\x00" * 100), EIGRPv6ExtRoute) = EIGRPGuessPayloadClass function: Return EIGRPGeneric -isinstance(_EIGRPGuessPayloadClass(b"\x23\x42"), EIGRPGeneric) +isinstance(_EIGRPGuessPayloadClass(b"\x23\x42" + b"\x00" * 50), EIGRPGeneric) + TLV List diff --git a/test/contrib/erspan.uts b/test/contrib/erspan.uts index cdff9e2b64b..e4465b382f1 100644 --- a/test/contrib/erspan.uts +++ b/test/contrib/erspan.uts @@ -14,14 +14,14 @@ assert pkt.seqnum_present == 0 pkt = GRE()/ERSPAN_II()/Ether(src="11:11:11:11:11:11", dst="ff:ff:ff:ff:ff:ff") b = bytes(pkt) -assert b == b'\x10\x00\x88\xbe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\x11\x11\x11\x11\x11\x11\x90\x00' +assert b == b'\x10\x00\x88\xbe\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\x11\x11\x11\x11\x11\x11\x90\x00' = Dissect ERSPAN II pkt = GRE(b) assert pkt[GRE].proto == 0x88be assert pkt[GRE].seqnum_present == 1 -assert pkt[GRE][ERSPAN].ver == 0 +assert pkt[GRE][ERSPAN].ver == 1 assert pkt[Ether].src == "11:11:11:11:11:11" + ERSPAN III diff --git a/test/contrib/ethercat.uts b/test/contrib/ethercat.uts index 20f63571759..6e1d872212e 100644 --- a/test/contrib/ethercat.uts +++ b/test/contrib/ethercat.uts @@ -183,6 +183,9 @@ assert frm[EtherCat].length == 60 nums_11_bits = [random.randint(0, 65535) & 0b11111111111 for dummy in range(0, 23)] nums_4_bits = [random.randint(0, 16) & 0b1111 for dummy in range(0, 23)] +old_max_list_count = conf.max_list_count +conf.max_list_count = 3000 + frm = Ether()/EtherCat()/EtherCatAPRD(adp=0x1234, ado=0x5678, irq=0xbad0, wkc=0xbeef, data=[1]*2035, c=1) frm = Ether(frm.do_build()) assert frm[EtherCat].length == 2047 @@ -215,6 +218,8 @@ assert frm[EtherCatAPRD].c == 0 assert frm[EtherCat]._reserved == 0 +conf.max_list_count = old_max_list_count + = EtherCat and Type12 DLPDU layers for type_id in EtherCat.ETHERCAT_TYPE12_DLPDU_TYPES: diff --git a/test/contrib/ldp.uts b/test/contrib/ldp.uts index 5e184bcdd7d..af6bcbb17d5 100644 --- a/test/contrib/ldp.uts +++ b/test/contrib/ldp.uts @@ -44,5 +44,6 @@ assert pkti.params == [180, 0, 0, 0, 0, '1.1.2.2', 0] pkta = LDPAddress(address=['1.1.2.2', '172.16.2.1'])/LDPLabelMM(fec=[('172.16.2.0', 31)])/LDPLabelMM(fec=[('1.1.2.2', 32)])/LDPLabelMM(fec=[('1.1.2.1', 32)]) = Advanced dissection - complex LDP +load_contrib("mpls") pkt = Ether(b"\xcc\x04\x04\xdc\x00\x10\xcc\x03\x04\xdc\x00\x10\x88G\x00\x01-\xfeE\xc0\x014\xfe\x84\x00\x00\xff\x06\xb5z\x01\x01\x02\x02\x01\x01\x02\x01\xe4\xe4\x02\x86\xbf\xfb'\xe4\xb9\xb3\xe4GP\x10\x0e\xb6v\x9f\x00\x00\x00\x01\x01\x08\x01\x01\x02\x02\x00\x00\x03\x00\x00\x12\x00\x00\x00\x0e\x01\x01\x00\n\x00\x01\x01\x01\x02\x02\xac\x10\x02\x01\x04\x00\x00\x18\x00\x00\x00\x0f\x01\x00\x00\x08\x02\x00\x01\x1f\xac\x10\x02\x00\x02\x00\x00\x04\x00\x00\x00\x03\x04\x00\x00\x18\x00\x00\x00\x10\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x02\x02\x02\x00\x00\x04\x00\x00\x00\x03\x04\x00\x00\x18\x00\x00\x00\x11\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x02\x01\x02\x00\x00\x04\x00\x00\x00\x12\x04\x00\x00\x18\x00\x00\x00\x12\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x01\x02\x02\x00\x00\x04\x00\x00\x00\x13\x04\x00\x00\x18\x00\x00\x00\x13\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x01\x01\x02\x00\x00\x04\x00\x00\x00\x14\x04\x00\x00\x18\x00\x00\x00\x14\x01\x00\x00\x08\x02\x00\x01\x1f\xac\x10\x01\x00\x02\x00\x00\x04\x00\x00\x00\x15\x04\x00\x00\x18\x00\x00\x00\x15\x01\x00\x00\x08\x02\x00\x01\x1f\xac\x10\x00\x00\x02\x00\x00\x04\x00\x00\x00\x16\x04\x00\x00$\x00\x00\x00\x16\x01\x00\x00\x14\x80\x80\x05\x0c\x00\x00\x00\x00\x00\x00\x00\n\x01\x04\x05\xdc\x0c\x04\x03\x02\x02\x00\x00\x04\x00\x00\x00\x10") assert pkt.getlayer(LDPLabelMM, 8).fec == [('0.0.0.0', 12), ('0.0.0.0', 0), ('5.0.0.0', 4), ('2.0.0.0', 3)] diff --git a/test/contrib/modbus.uts b/test/contrib/modbus.uts index a65b2532aaa..5a010ef73c0 100644 --- a/test/contrib/modbus.uts +++ b/test/contrib/modbus.uts @@ -105,7 +105,7 @@ assert isinstance(p.payload, ModbusPDU0BGetCommEventCounterError) p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x02\xff\x0c') assert isinstance(p.payload, ModbusPDU0CGetCommEventLogRequest) = MBAP Guess Payload ModbusPDU0CGetCommEventLogResponse -p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x02\xff\x0c') +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x02\xff\x0c\x00\x00\x00\x00\x00\x00\x00') assert isinstance(p.payload, ModbusPDU0CGetCommEventLogResponse) = MBAP Guess Payload ModbusPDU0CGetCommEventLogError p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x8c\x01') diff --git a/test/contrib/pcom.uts b/test/contrib/pcom.uts index 110a4db86cd..b5ad57e8586 100755 --- a/test/contrib/pcom.uts +++ b/test/contrib/pcom.uts @@ -16,10 +16,10 @@ r = b'\x65\x00\x04\x00\x00\x00\x00\x00' raw(PCOMResponse() / b'\x00\x00\x00\x00')[2:] == r = PCOM/TCP Guess Payload Class -assert isinstance(PCOMRequest(b'\x00\x00\x65\x00\x01\x00\x00\x00').payload, PCOMAsciiRequest) -assert isinstance(PCOMResponse(b'\x00\x00\x65\x00\x01\x00\x00\x00').payload, PCOMAsciiResponse) -assert isinstance(PCOMRequest(b'\x00\x00\x66\x00\x01\x00\x00\x00').payload, PCOMBinaryRequest) -assert isinstance(PCOMResponse(b'\x00\x00\x66\x00\x01\x00\x00\x00').payload, PCOMBinaryResponse) +assert isinstance(PCOMRequest(b'\x00\x00\x65\x00\x01\x00\x00\x00\x00\x00\x00\x00').payload, PCOMAsciiRequest) +assert isinstance(PCOMResponse(b'\x00\x00\x65\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00').payload, PCOMAsciiResponse) +assert isinstance(PCOMRequest(b'\x00\x00\x66\x00\x01\x00\x00\x00' + b'\x00' * 25).payload, PCOMBinaryRequest) +assert isinstance(PCOMResponse(b'\x00\x00\x66\x00\x01\x00\x00\x00' + b'\x00' * 25).payload, PCOMBinaryResponse) + Test PCOM/Ascii = PCOM/ASCII Default values diff --git a/test/contrib/pim.uts b/test/contrib/pim.uts index 195c645e6dc..7fbba92a565 100644 --- a/test/contrib/pim.uts +++ b/test/contrib/pim.uts @@ -7,7 +7,7 @@ = PIMv2 Hello - instantiation -hello_data = b'\x01\x00^\x00\x00\r\x00\xd0\xcb\x00\xba\xe4\x08\x00E\xc0\x00BY\xf9\x00\x00\x01gTe\x15\x15\x15\x15\xe0\x00\x00\r \x00\xa55\x00\x01\x00\x02\x00i\x00\x13\x00\x04\x00\x00\x00\x00\x00\x02\x00\x04\x01\xf4\t\xc4\x00\x14\x00\x04' +hello_data = b'\x01\x00^\x00\x00\r\x00\xd0\xcb\x00\xba\xe4\x08\x00E\xc0\x00BY\xf9\x00\x00\x01gTe\x15\x15\x15\x15\xe0\x00\x00\r \x00\xa55\x00\x01\x00\x02\x00i\x00\x13\x00\x04\x00\x00\x00\x00\x00\x02\x00\x04\x01\xf4\t\xc4\x00\x14\x00\x04\x00\x00\x00\x00' hello_pkt = Ether(hello_data) diff --git a/test/scapy/layers/dhcp6.uts b/test/scapy/layers/dhcp6.uts index b076df7d9b3..ae23482e108 100644 --- a/test/scapy/layers/dhcp6.uts +++ b/test/scapy/layers/dhcp6.uts @@ -298,7 +298,7 @@ raw(DHCP6OptOptReq(reqopts=[])) == b'\x00\x06\x00\x00' = DHCP6OptOptReq - Basic dissection a=DHCP6OptOptReq(b'\x00\x06\x00\x00') -a.optcode == 6 and a.optlen == 0 and a.reqopts == [23,24] +a.optcode == 6 and a.optlen == 0 and a.reqopts == [] = DHCP6OptOptReq - Dissection with specific value a=DHCP6OptOptReq(b'\x00\x06\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04') @@ -1022,7 +1022,7 @@ raw(DHCP6OptRelayAgentERO(reqopts=[])) == b'\x00+\x00\x00' = DHCP6OptRelayAgentERO - Basic dissection a=DHCP6OptRelayAgentERO(b'\x00+\x00\x00') -a.optcode == 43 and a.optlen == 0 and a.reqopts == [23,24] +a.optcode == 43 and a.optlen == 0 and a.reqopts == [] = DHCP6OptRelayAgentERO - Dissection with specific value a=DHCP6OptRelayAgentERO(b'\x00+\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04') @@ -1451,7 +1451,7 @@ raw(DHCP6OptRelayMsg(optcode=37)) == b'\x00%\x00\x04\x00\x00\x00\x00' = DHCP6OptRelayMsg - Basic Dissection a = DHCP6OptRelayMsg(b'\x00\r\x00\x00') -a.optcode == 13 and a.optlen == 0 and isinstance(a.message, DHCP6) +a.optcode == 13 and a.optlen == 0 and a.message is None = DHCP6OptRelayMsg - Embedded DHCP6 packet Instantiation raw(DHCP6OptRelayMsg(message=DHCP6_Solicit())) == b'\x00\t\x00\x04\x01\x00\x00\x00' diff --git a/test/scapy/layers/dns.uts b/test/scapy/layers/dns.uts index 77e600446dc..27bc8985bbe 100644 --- a/test/scapy/layers/dns.uts +++ b/test/scapy/layers/dns.uts @@ -26,11 +26,11 @@ assert query.qname == query.__class__(raw(query)).qname ~ netaccess needs_root IP UDP DNS dns_ans.show() dns_ans.show2() -dns_ans[DNS].an.show() +dns_ans[DNS].an[0].show() dns_ans2 = IP(raw(dns_ans)) DNS in dns_ans2 assert raw(dns_ans2) == raw(dns_ans) -dns_ans2.qd.qname = "www.secdev.org." +dns_ans2.qd[0].qname = "www.secdev.org." * We need to recalculate these values del dns_ans2[IP].len del dns_ans2[IP].chksum @@ -44,13 +44,13 @@ assert raw(DNSRR(type='A', rdata='1.2.3.4')) == b'\x00\x00\x01\x00\x01\x00\x00\x pkt = IP(raw(IP(src="10.0.0.1", dst="8.8.8.8")/UDP(sport=RandShort(), dport=53)/DNS(qd=DNSQR(qname="secdev.org.")))) assert UDP in pkt and isinstance(pkt[UDP].payload, DNS) assert pkt[UDP].dport == 53 and pkt[UDP].length is None -assert pkt[DNS].qdcount == 1 and pkt[DNS].qd.qname == b"secdev.org." +assert pkt[DNS].qdcount == 1 and pkt[DNS].qd[0].qname == b"secdev.org." * DNS over TCP pkt = IP(raw(IP(src="10.0.0.1", dst="8.8.8.8")/TCP(sport=RandShort(), dport=53, flags="P")/DNS(qd=DNSQR(qname="secdev.org.")))) assert TCP in pkt and isinstance(pkt[TCP].payload, DNS) assert pkt[TCP].dport == 53 and pkt[DNS].length is not None -assert pkt[DNS].qdcount == 1 and pkt[DNS].qd.qname == b"secdev.org." +assert pkt[DNS].qdcount == 1 and pkt[DNS].qd[0].qname == b"secdev.org." = DNS frame with advanced decompression ~ dns @@ -60,7 +60,9 @@ pkt = Ether(a) assert pkt.ancount == 3 assert pkt.arcount == 4 assert pkt.an[1].rdata == b'Zalmoid.local.' +assert pkt.an[1].rdlen is None assert pkt.an[2].rdata == b'Zalmoid.local.' +assert pkt.an[2].rdlen is None assert pkt.ar[1].nextname == b'1.A.9.4.7.E.A.4.B.A.F.B.2.1.4.0.0.6.E.F.7.1.F.2.5.3.E.0.1.0.A.2.ip6.arpa.' assert pkt.ar[2].nextname == b'136.0.168.192.in-addr.arpa.' pkt.show() @@ -80,25 +82,46 @@ assert b.an[6].rdata == b'24:e3:14:4d:84:c0@fe80::26e3:14ff:fe4d:84c0._apple-mob c = b'\x01\x00^\x00\x00\xfb\x14\x0cv\x8f\xfe(\x08\x00E\x00\x01C\xe3\x91@\x00\xff\x11\xf4u\xc0\xa8\x00\xfe\xe0\x00\x00\xfb\x14\xe9\x14\xe9\x01/L \x00\x00\x84\x00\x00\x00\x00\x04\x00\x00\x00\x00\x05_raop\x04_tcp\x05local\x00\x00\x0c\x00\x01\x00\x00\x11\x94\x00\x1e\x1b140C768FFE28@Freebox Server\xc0\x0c\xc0(\x00\x10\x80\x01\x00\x00\x11\x94\x00\xa0\ttxtvers=1\x08vs=190.9\x04ch=2\x08sr=44100\x05ss=16\x08pw=false\x06et=0,1\x04ek=1\ntp=TCP,UDP\x13am=FreeboxServer1,2\ncn=0,1,2,3\x06md=0,2\x07sf=0x44\x0bft=0xBF0A00\x08sv=false\x07da=true\x08vn=65537\x04vv=2\xc0(\x00!\x80\x01\x00\x00\x00x\x00\x19\x00\x00\x00\x00\x13\x88\x10Freebox-Server-3\xc0\x17\xc1\x04\x00\x01\x80\x01\x00\x00\x00x\x00\x04\xc0\xa8\x00\xfe' pkt = Ether(c) assert DNS in pkt -assert pkt.an.rdata == b'140C768FFE28@Freebox Server._raop._tcp.local.' -assert pkt.an.getlayer(DNSRR, type=1).rrname == b'Freebox-Server-3.local.' -assert pkt.an.getlayer(DNSRR, type=1).rdata == '192.168.0.254' -assert pkt.an.getlayer(DNSRR, type=16).rdata == [b'txtvers=1', b'vs=190.9', b'ch=2', b'sr=44100', b'ss=16', b'pw=false', b'et=0,1', b'ek=1', b'tp=TCP,UDP', b'am=FreeboxServer1,2', b'cn=0,1,2,3', b'md=0,2', b'sf=0x44', b'ft=0xBF0A00', b'sv=false', b'da=true', b'vn=65537', b'vv=2'] +assert pkt.an[0].rdata == b'140C768FFE28@Freebox Server._raop._tcp.local.' +assert pkt.an[1].rdata == [b'txtvers=1', b'vs=190.9', b'ch=2', b'sr=44100', b'ss=16', b'pw=false', b'et=0,1', b'ek=1', b'tp=TCP,UDP', b'am=FreeboxServer1,2', b'cn=0,1,2,3', b'md=0,2', b'sf=0x44', b'ft=0xBF0A00', b'sv=false', b'da=true', b'vn=65537', b'vv=2'] +assert pkt.an[2].rrname == b'140C768FFE28@Freebox Server._raop._tcp.local.' +assert pkt.an[2].port == 5000 +assert pkt.an[2].target == b'Freebox-Server-3.local.' +assert pkt.an[3].rrname == b'Freebox-Server-3.local.' +assert pkt.an[3].rdata == '192.168.0.254' + += Other compressed DNS +~ dns +s = b'\x00\x00\x84\x00\x00\x00\x00\x02\x00\x00\x00\x06\x0bGourmandise\x04_smb\x04_tcp\x05local\x00\x00!\x80\x01\x00\x00\x00x\x00\x14\x00\x00\x00\x00\x01\xbd\x0bGourmandise\xc0"\x0bGourmandise\x0b_afpovertcp\xc0\x1d\x00!\x80\x01\x00\x00\x00x\x00\x08\x00\x00\x00\x00\x02$\xc09\xc09\x00\x1c\x80\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\x00s#\x99\xca\xf7\xea\xdc\xc09\x00\x01\x80\x01\x00\x00\x00x\x00\x04\xc0\xa8\x01x\xc09\x00\x1c\x80\x01\x00\x00\x00x\x00\x10*\x01\xcb\x00\x0bD\x1f\x00\x18k\xb1\x99\x90\xdf\x84.\xc0\x0c\x00/\x80\x01\x00\x00\x00x\x00\t\xc0\x0c\x00\x05\x00\x00\x80\x00@\xc0G\x00/\x80\x01\x00\x00\x00x\x00\t\xc0G\x00\x05\x00\x00\x80\x00@\xc09\x00/\x80\x01\x00\x00\x00x\x00\x08\xc09\x00\x04@\x00\x00\x08' +pkt = DNS(s) +assert [x.rrname for x in pkt.ar] == [ + b'Gourmandise.local.', + b'Gourmandise.local.', + b'Gourmandise.local.', + b'Gourmandise._smb._tcp.local.', + b'Gourmandise._afpovertcp._tcp.local.', + b'Gourmandise.local.' +] = DNS advanced building ~ dns -pkt = DNS(qr=1, qd=None, aa=1, rd=1) -pkt.an = DNSRR(type=12, rrname='_raop._tcp.local.', rdata='140C768FFE28@Freebox Server._raop._tcp.local.')/DNSRR(rrname='140C768FFE28@Freebox Server._raop._tcp.local.', type=16, rdata=[b'txtvers=1', b'vs=190.9', b'ch=2', b'sr=44100', b'ss=16', b'pw=false', b'et=0,1', b'ek=1', b'tp=TCP,UDP', b'am=FreeboxServer1,2', b'cn=0,1,2,3', b'md=0,2', b'sf=0x44', b'ft=0xBF0A00', b'sv=false', b'da=true', b'vn=65537', b'vv=2'])/DNSRRSRV(rrname='140C768FFE28@Freebox Server._raop._tcp.local.', target='Freebox-Server-3.local.', port=5000, type=33, rclass=32769)/DNSRR(rrname='Freebox-Server-3.local.', rdata='192.168.0.254', rclass=32769, type=1, ttl=120) +pkt = DNS(qr=1, qd=[], aa=1, rd=1) +pkt.an = [ + DNSRR(type=12, rrname='_raop._tcp.local.', rdata='140C768FFE28@Freebox Server._raop._tcp.local.'), + DNSRR(rrname='140C768FFE28@Freebox Server._raop._tcp.local.', type=16, rdata=[b'txtvers=1', b'vs=190.9', b'ch=2', b'sr=44100', b'ss=16', b'pw=false', b'et=0,1', b'ek=1', b'tp=TCP,UDP', b'am=FreeboxServer1,2', b'cn=0,1,2,3', b'md=0,2', b'sf=0x44', b'ft=0xBF0A00', b'sv=false', b'da=true', b'vn=65537', b'vv=2']), + DNSRRSRV(rrname='140C768FFE28@Freebox Server._raop._tcp.local.', target='Freebox-Server-3.local.', port=5000, type=33, rclass=32769), + DNSRR(rrname='Freebox-Server-3.local.', rdata='192.168.0.254', rclass=32769, type=1, ttl=120), +] pkt = DNS(raw(pkt)) -assert DNSRRSRV in pkt.an -assert pkt[DNSRRSRV].target == b'Freebox-Server-3.local.' -assert pkt[DNSRRSRV].rrname == b'140C768FFE28@Freebox Server._raop._tcp.local.' -assert isinstance(pkt[DNSRRSRV].payload, DNSRR) -assert pkt[DNSRRSRV].payload.rrname == b'Freebox-Server-3.local.' -assert pkt[DNSRRSRV].payload.rdata == '192.168.0.254' +assert DNSRRSRV in pkt.an[2] +assert pkt.an[2][DNSRRSRV].target == b'Freebox-Server-3.local.' +assert pkt.an[2][DNSRRSRV].rrname == b'140C768FFE28@Freebox Server._raop._tcp.local.' + +assert pkt.an[3].rrname == b'Freebox-Server-3.local.' +assert pkt.an[3].rdata == '192.168.0.254' = Basic DNS Compression ~ dns @@ -132,44 +155,48 @@ assert raw(recompressed) == raw(pkt) frame = b'E\x00\x00\xa4\x93\x1d\x00\x00y\x11\xdc\xfc\x08\x08\x08\x08\xc0\xa8\x00w\x005\xb4\x9b\x00\x90k\x80\x00\x00\x81\x80\x00\x01\x00\x05\x00\x00\x00\x00\x06google\x03com\x00\x00\x0f\x00\x01\xc0\x0c\x00\x0f\x00\x01\x00\x00\x02B\x00\x11\x00\x1e\x04alt2\x05aspmx\x01l\xc0\x0c\xc0\x0c\x00\x0f\x00\x01\x00\x00\x02B\x00\t\x00\x14\x04alt1\xc0/\xc0\x0c\x00\x0f\x00\x01\x00\x00\x02B\x00\t\x002\x04alt4\xc0/\xc0\x0c\x00\x0f\x00\x01\x00\x00\x02B\x00\t\x00(\x04alt3\xc0/\xc0\x0c\x00\x0f\x00\x01\x00\x00\x02B\x00\x04\x00\n\xc0/' pkt = IP(frame) -results = [x.exchange for x in pkt.an.iterpayloads()] +results = [x.exchange for x in pkt.an] assert results == [b'alt2.aspmx.l.google.com.', b'alt1.aspmx.l.google.com.', b'alt4.aspmx.l.google.com.', b'alt3.aspmx.l.google.com.', b'aspmx.l.google.com.'] pkt.clear_cache() assert raw(dns_compress(pkt)) == frame -= Advanced dns_get_str tests += DNS frame with typebitmaps ~ dns -assert dns_get_str(b"\x06cheese\x00blobofdata....\x06hamand\xc0\x0c", 22, _fullpacket=True)[0] == b'hamand.cheese.' - compressed_pkt = b'\x01\x00^\x00\x00\xfb\xa0\x10\x81\xd9\xd3y\x08\x00E\x00\x01\x14\\\n@\x00\xff\x116n\xc0\xa8F\xbc\xe0\x00\x00\xfb\x14\xe9\x14\xe9\x01\x00Ho\x00\x00\x84\x00\x00\x00\x00\x04\x00\x00\x00\x03\x03188\x0270\x03168\x03192\x07in-addr\x04arpa\x00\x00\x0c\x80\x01\x00\x00\x00x\x00\x0f\x07Android\x05local\x00\x019\x017\x013\x01D\x019\x01D\x01E\x01F\x01F\x01F\x011\x018\x010\x011\x012\x01A\x010\x010\x010\x010\x010\x010\x010\x010\x010\x010\x010\x010\x010\x018\x01E\x01F\x03ip6\xc0#\x00\x0c\x80\x01\x00\x00\x00x\x00\x02\xc03\xc03\x00\x01\x80\x01\x00\x00\x00x\x00\x04\xc0\xa8F\xbc\xc03\x00\x1c\x80\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\xa2\x10\x81\xff\xfe\xd9\xd3y\xc0\x0c\x00/\x80\x01\x00\x00\x00x\x00\x06\xc0\x0c\x00\x02\x00\x08\xc0B\x00/\x80\x01\x00\x00\x00x\x00\x06\xc0B\x00\x02\x00\x08\xc03\x00/\x80\x01\x00\x00\x00x\x00\x08\xc03\x00\x04@\x00\x00\x08' +pkt = Ether(compressed_pkt) +assert pkt.ar[2].nextname == b"Android.local." +assert pkt.ar[2].sprintf("%typebitmaps%") == "['A', 'AAAA']" + += Advanced dns_get_str tests +~ dns -Ether(compressed_pkt) +full = b"\x06cheese\x00blobofdata....\x06hamand\xc0\x00" +assert dns_get_str(full[22:], full=full)[0] == b'hamand.cheese.' = Decompression loop in dns_get_str ~ dns -assert dns_get_str(b"\x04data\xc0\x0c", 0, _fullpacket=True)[0] == b"data.data." +full = b"\x04data\xc0\x00" +assert dns_get_str(full, full=full)[0] == b"data.data." = Prematured end in dns_get_str ~ dns -assert dns_get_str(b"\x06da", 0, _fullpacket=True)[0] == b"da." -assert dns_get_str(b"\x04data\xc0\x01", 0, _fullpacket=True)[0] == b"data." +assert dns_get_str(b"\x06da", 0)[0] == b"da." + +full = b"\x04data\xc0\x0f" +assert dns_get_str(full, full=full)[0] == b"data." -= Other decompression loop in dns_get_str -~ dns -s = b'\x00\x00\x84\x00\x00\x00\x00\x02\x00\x00\x00\x06\x0bGourmandise\x04_smb\x04_tcp\x05local\x00\x00!\x80\x01\x00\x00\x00x\x00\x14\x00\x00\x00\x00\x01\xbd\x0bGourmandise\xc0"\x0bGourmandise\x0b_afpovertcp\xc0\x1d\x00!\x80\x01\x00\x00\x00x\x00\x08\x00\x00\x00\x00\x02$\xc09\xc09\x00\x1c\x80\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\x00s#\x99\xca\xf7\xea\xdc\xc09\x00\x01\x80\x01\x00\x00\x00x\x00\x04\xc0\xa8\x01x\xc09\x00\x1c\x80\x01\x00\x00\x00x\x00\x10*\x01\xcb\x00\x0bD\x1f\x00\x18k\xb1\x99\x90\xdf\x84.\xc0\x0c\x00/\x80\x01\x00\x00\x00x\x00\t\xc0\x0c\x00\x05\x00\x00\x80\x00@\xc0G\x00/\x80\x01\x00\x00\x00x\x00\t\xc0G\x00\x05\x00\x00\x80\x00@\xc09\x00/\x80\x01\x00\x00\x00x\x00\x08\xc09\x00\x04@\x00\x00\x08' -DNS(s) = DNS record type 16 (TXT) -p = DNS(raw(DNS(id=1,ra=1,qd=None,an=DNSRR(rrname='scapy', type='TXT', rdata="niceday", ttl=1)))) -assert p[DNS].an.rdata == [b"niceday"] +p = DNS(raw(DNS(id=1,ra=1,qd=[],an=DNSRR(rrname='scapy', type='TXT', rdata="niceday", ttl=1)))) +assert p[DNS].an[0].rdata == [b"niceday"] -p = DNS(raw(DNS(id=1,ra=1,qd=None,an=DNSRR(rrname='secdev', type='TXT', rdata=["sweet", "celestia"], ttl=1)))) -assert p[DNS].an.rdata == [b"sweet", b"celestia"] +p = DNS(raw(DNS(id=1,ra=1,qd=[],an=DNSRR(rrname='secdev', type='TXT', rdata=["sweet", "celestia"], ttl=1)))) +assert p[DNS].an[0].rdata == [b"sweet", b"celestia"] assert raw(p) == b'\x00\x01\x01\x80\x00\x00\x00\x01\x00\x00\x00\x00\x06secdev\x00\x00\x10\x00\x01\x00\x00\x00\x01\x00\x0f\x05sweet\x08celestia' = DNS - Malformed DNS over TCP message @@ -178,13 +205,13 @@ _old_dbg = conf.debug_dissector conf.debug_dissector = True try: - p = IP(raw(IP()/TCP()/DNS(qd=None,length=28))[:-13]) + p = IP(raw(IP()/TCP()/DNS(qd=[],length=28))[:-13]) assert False except Scapy_Exception as e: assert str(e) == "Malformed DNS message: too small!" try: - p = IP(raw(IP()/TCP()/DNS(qd=None,length=28, qdcount=1))) + p = IP(raw(IP()/TCP()/DNS(qd=[],length=28, qdcount=1))) assert False except Scapy_Exception as e: assert str(e) == "Malformed DNS message: invalid length!" @@ -196,17 +223,17 @@ conf.debug_dissector = _old_dbg data = b'E\x00\x00n~\x82\x00\x00{\x11\xae\xeb\x08\x08\x08\x08\x01\x01\x01\x01\x005\x005\x00Z!\x17\x00\x00\x81\x80\x00\x01\x00\x00\x00\x01\x00\x00\x03www\x06google\x03com\x00\x00\x0f\x00\x01\xc0\x10\x00\x06\x00\x01\x00\x00\x002\x00&\x03ns1\xc0\x10\tdns-admin\xc0\x10\x14Po\x8f\x00\x00\x03\x84\x00\x00\x03\x84\x00\x00\x07\x08\x00\x00\x00<' p = IP(data) -assert p.ns.rrname == b"google.com." -assert p.ns.mname == b"ns1.google.com." -assert p.ns.rname == b"dns-admin.google.com." +assert p.ns[0].rrname == b"google.com." +assert p.ns[0].mname == b"ns1.google.com." +assert p.ns[0].rname == b"dns-admin.google.com." cp = dns_compress(p) -assert cp.ns.rrname == b'\xc0\x10' -assert cp.ns.mname == b'\x03ns1\xc0\x10' -assert cp.ns.rname == b'\tdns-admin\xc0\x10' +assert cp.ns[0].rrname == b'\xc0\x10' +assert cp.ns[0].mname == b'\x03ns1\xc0\x10' +assert cp.ns[0].rname == b'\tdns-admin\xc0\x10' p = IP(raw(cp)) -assert p.ns.rrname == b"google.com." -assert p.ns.mname == b"ns1.google.com." -assert p.ns.rname == b"dns-admin.google.com." +assert p.ns[0].rrname == b"google.com." +assert p.ns[0].mname == b"ns1.google.com." +assert p.ns[0].rname == b"dns-admin.google.com." = DNS - dns_compress on close indexes @@ -214,9 +241,9 @@ p = dns_compress(DNS(qd=DNSQR(qname=b'scapy.'), an=DNSRR(rrname=b'scapy.'), ar=D assert raw(p) == b'\x00\x00\x01\x00\x00\x01\x00\x01\x00\x00\x00\x01\x05scapy\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00)\x10\x00\x00\x00\x80\x00\x00\x00' p = DNS(raw(p)) -assert p.qd.qname == b'scapy.' -assert p.an.rrname == b'scapy.' -assert p.ar.rrname == b'.' +assert p.qd[0].qname == b'scapy.' +assert p.an[0].rrname == b'scapy.' +assert p.ar[0].rrname == b'.' = DNS - dns_encode edge cases @@ -228,14 +255,38 @@ assert dns_encode(dns_encode(b"*")) == b'\x03\x01*\x00' assert raw(DNS()) == b'\x00\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01' -= DNS - preserve rdlen when rdata is not a compressed DNS string += DNS - OOM test +% parse a DNS packet specifically crafted for OOM + +import zlib +data = zlib.decompress(b'x\x9c\xed\xce!\n\xc2`\x00\x80\xd1\x7f\x0c\xc1\xbal\xf1\x12\x1e\xc0 Vob3\xaf+\xec\x02\xbb\x82x\x83\xb1"\xa2\xc1\xac\x06\x9b\xc9\xb0"\x0b3\xccdV\x93\xa0\x0f^\xfc\xc2w^\x16U\x88\x8a\xd5n\xdf\xb6\xdd0\n\xf5q\xb1\t!M&Y>K\xae\xf9\xacw\t\x83\xe8V\xaf\x0f\xa7m\xdaV\xf78z\xad\x1f\xbf\x95=5\xd9\x071\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\x0f\xe94\xdf\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\xff\x15\x85\xe18.\xcbrZ\xce\x1f5u\n\xd1') + +# measure the time it takes +old_max_list_count = conf.max_list_count +conf.max_list_count = 10 +import time +t = time.monotonic() + +with no_debug_dissector(): + try: + dns = Ether(data) + except MaximumItemsCount: + pass + +delta = time.monotonic() - t +assert delta < 10 + +conf.max_list_count = old_max_list_count -# RR type A -dnsrr1 = Raw(b'\x01a\x01b\x01c\x00\x00\x01\x10\x00\x00\x00\x00\x01\x00\x04\x01\x02\x03\x04') -# RR type NS & plain rdata -dnsrr2 = Raw(b'\x02ns\xc0\x0e\x00\x02\x10\x00\x00\x00\x00\x01\x00\x06\x01x\x01y\x01z') -# RR type NS & compressed rdata -dnsrr3 = Raw(b'\x02ns\xc0\x0e\x00\x02\x10\x00\x00\x00\x00\x01\x00\x07\x04test\xc0\x0e') += DNS - Backward compatibility: keep deprecated behavior +~ dns + +# Get through a list (should be pkt.an[0].rdata) +c = b'\x01\x00^\x00\x00\xfb\x14\x0cv\x8f\xfe(\x08\x00E\x00\x01C\xe3\x91@\x00\xff\x11\xf4u\xc0\xa8\x00\xfe\xe0\x00\x00\xfb\x14\xe9\x14\xe9\x01/L \x00\x00\x84\x00\x00\x00\x00\x04\x00\x00\x00\x00\x05_raop\x04_tcp\x05local\x00\x00\x0c\x00\x01\x00\x00\x11\x94\x00\x1e\x1b140C768FFE28@Freebox Server\xc0\x0c\xc0(\x00\x10\x80\x01\x00\x00\x11\x94\x00\xa0\ttxtvers=1\x08vs=190.9\x04ch=2\x08sr=44100\x05ss=16\x08pw=false\x06et=0,1\x04ek=1\ntp=TCP,UDP\x13am=FreeboxServer1,2\ncn=0,1,2,3\x06md=0,2\x07sf=0x44\x0bft=0xBF0A00\x08sv=false\x07da=true\x08vn=65537\x04vv=2\xc0(\x00!\x80\x01\x00\x00\x00x\x00\x19\x00\x00\x00\x00\x13\x88\x10Freebox-Server-3\xc0\x17\xc1\x04\x00\x01\x80\x01\x00\x00\x00x\x00\x04\xc0\xa8\x00\xfe' +pkt = Ether(c) +assert pkt.an.rdata == b'140C768FFE28@Freebox Server._raop._tcp.local.' -d = DNS(raw(DNS(ancount=3, an=dnsrr1/dnsrr2/dnsrr3))) -assert d.an[0].rdlen == 4 and d.an[1].rdlen == 6 and d.an[2].rdlen is None +# Set qd to None (should be qd=[]) +pkt = DNS(qr=1, qd=None, aa=1, rd=1) +pkt = DNS(bytes(pkt)) +assert pkt.qd == [] \ No newline at end of file diff --git a/test/scapy/layers/dot11.uts b/test/scapy/layers/dot11.uts index a4e294e017b..c9860f3b8f4 100644 --- a/test/scapy/layers/dot11.uts +++ b/test/scapy/layers/dot11.uts @@ -178,8 +178,8 @@ assert pkt[Dot11EltCountry].pad == 0 assert pkt.getlayer(Dot11Elt, ID=11) * Country element: Secondary padding check -erp_payload = b'\x1e\x2a\x01\x62' -country_payload = b'\x07\x06\x55\x53\x20\x01\x0b' +erp_payload = b'\x2a\x01\x62' +country_payload = b'\x07\x06\x55\x53\x20\x01\x0b\x1e' bare_country = Dot11EltCountry(country_payload) country_nested = Dot11EltCountry(country_payload + erp_payload) diff --git a/test/scapy/layers/inet.uts b/test/scapy/layers/inet.uts index e1131e95dbd..e16c769e4bc 100644 --- a/test/scapy/layers/inet.uts +++ b/test/scapy/layers/inet.uts @@ -421,6 +421,9 @@ pkt = IP(len=28, ihl=5) / UDP() bpkt = IP(raw(pkt)) assert bpkt.chksum == 0x7cce and bpkt.payload.chksum == 0x0172 +* Invalid territory +conf.debug_dissector = False + pkt = IP() / UDP() / ("A" * 10) bpkt = IP(raw(pkt)) assert bpkt.chksum == 0x7cc4 and bpkt.payload.chksum == 0xbb17 @@ -445,6 +448,8 @@ pkt = IP(len=42, ihl=6, options=[IPOption_RR()]) / UDP() / ("A" * 10) bpkt = IP(raw(pkt)) assert bpkt.chksum == 0x70bd and bpkt.payload.chksum == 0xbb17 +conf.debug_dissector = True + = IP with forced-length 0 p = IP()/TCP() p[IP].len = 0 @@ -495,7 +500,9 @@ value == 26908070 test.i2repr("", value) == '7:28:28.70' = IPv4 - UDP null checksum +conf.debug_dissector = False IP(raw(IP()/UDP()/Raw(b"\xff\xff\x01\x6a")))[UDP].chksum == 0xFFFF +conf.debug_dissector = True = IPv4 - (IP|UDP|TCP|ICMP)Error query = IP(dst="192.168.0.1", src="192.168.0.254", ttl=1)/UDP()/DNS() diff --git a/test/scapy/layers/inet6.uts b/test/scapy/layers/inet6.uts index d140b653779..4bc7e4573d3 100644 --- a/test/scapy/layers/inet6.uts +++ b/test/scapy/layers/inet6.uts @@ -26,7 +26,9 @@ a.nh == 6 and a.plen == 20 and isinstance(a.payload, TCP) and a.payload.chksum = raw(IPv6()/TCP()/Raw(load="somedata")) == b'`\x00\x00\x00\x00\x1c\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xd5\xdd\x00\x00somedata' = IPv6 Class with TCP and TCP data - dissection -a=IPv6(raw(IPv6()/TCP(dport=1234, sport=1234)/Raw(load="somedata"))) +with no_debug_dissector(): + a=IPv6(raw(IPv6()/TCP(dport=1234, sport=1234)/Raw(load="somedata"))) + a.nh == 6 and a.plen == 28 and isinstance(a.payload, TCP) and a.payload.chksum == 0xcc9d and isinstance(a.payload.payload, Raw) and a[Raw].load == b"somedata" = IPv6 Class binding with Ethernet - build @@ -56,8 +58,9 @@ p = PseudoIPv6(src="fd00::abcd", dst="fd00::1234", uplen=64, nh=socket.IPPROTO_U raw(p) == b"\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xab\xcd\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x34\x00\x00\x00\x40\x00\x00\x00\x11" = in6_chksum is computed on UDP or TCP build -p = IPv6(raw(IPv6()/UDP()/Raw(load="somedata"))) -assert p.chksum == 0x45cb +with no_debug_dissector(): + p = IPv6(raw(IPv6()/UDP()/Raw(load="somedata"))) + assert p.chksum == 0x45cb ########### IPv6ExtHdrRouting Class ########################### @@ -874,12 +877,16 @@ assert a.pkt == b"" = ICMPv6NDOptRedirectedHdr - Disssection with specific values ~ ICMPv6NDOptRedirectedHdr -a=ICMPv6NDOptRedirectedHdr(b'\x04\xff\x11\x11\x00\x00\x00\x00somerawingthatisnotanipv6pac') +with no_debug_dissector(): + a=ICMPv6NDOptRedirectedHdr(b'\x04\xff\x11\x11\x00\x00\x00\x00somerawingthatisnotanipv6pac') + a.type == 4 and a.len == 255 and a.res == b'\x11\x11\x00\x00\x00\x00' and isinstance(a.pkt, Raw) and a.pkt.load == b"somerawingthatisnotanipv6pac" = ICMPv6NDOptRedirectedHdr - Dissection with cut IPv6 Header ~ ICMPv6NDOptRedirectedHdr -a=ICMPv6NDOptRedirectedHdr(b'\x04\x06\x00\x00\x00\x00\x00\x00`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') +with no_debug_dissector(): + a=ICMPv6NDOptRedirectedHdr(b'\x04\x06\x00\x00\x00\x00\x00\x00`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + a.type == 4 and a.len == 6 and a.res == b"\x00\x00\x00\x00\x00\x00" and isinstance(a.pkt, Raw) and a.pkt.load == b'`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' = ICMPv6NDOptRedirectedHdr - Complete dissection @@ -993,9 +1000,9 @@ a=ICMPv6NDOptSrcAddrList(b'\t\x05BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\ a.type == 9 and a.len == 5 and a.res == b'BBBBBB' and len(a.addrlist) == 2 and a.addrlist[0] == "ffff::ffff" and a.addrlist[1] == "1111::1111" = ICMPv6NDOptSrcAddrList - Dissection with specific values -conf.debug_dissector = False -a=ICMPv6NDOptSrcAddrList(b'\t\x03BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') -conf.debug_dissector = True +with no_debug_dissector(): + a=ICMPv6NDOptSrcAddrList(b'\t\x03BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') + a.type == 9 and a.len == 3 and a.res == b'BBBBBB' and len(a.addrlist) == 1 and a.addrlist[0] == "ffff::ffff" and isinstance(a.payload, Raw) and a.payload.load == b'\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' @@ -1021,9 +1028,9 @@ a=ICMPv6NDOptTgtAddrList(b'\n\x05BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\ a.type == 10 and a.len == 5 and a.res == b'BBBBBB' and len(a.addrlist) == 2 and a.addrlist[0] == "ffff::ffff" and a.addrlist[1] == "1111::1111" = ICMPv6NDOptTgtAddrList - Instantiation with specific values -conf.debug_dissector = False -a=ICMPv6NDOptTgtAddrList(b'\n\x03BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') -conf.debug_dissector = True +with no_debug_dissector(): + a=ICMPv6NDOptTgtAddrList(b'\n\x03BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') + a.type == 10 and a.len == 3 and a.res == b'BBBBBB' and len(a.addrlist) == 1 and a.addrlist[0] == "ffff::ffff" and isinstance(a.payload, Raw) and a.payload.load == b'\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' @@ -1694,7 +1701,7 @@ raw(ICMPv6NIReplyRefuse())[:8] == b'\x8c\x01\x00\x00\x00\x00\x00\x00' = ICMPv6NIReplyRefuse - basic dissection a=ICMPv6NIReplyRefuse(b'\x8c\x01\x00\x00\x00\x00\x00\x00\xf1\xe9\xab\xc9\x8c\x0by\x18') -a.type == 140 and a.code == 1 and a.cksum == 0 and a.unused == 0 and a.flags == 0 and a.nonce == b'\xf1\xe9\xab\xc9\x8c\x0by\x18' and a.data == None +a.type == 140 and a.code == 1 and a.cksum == 0 and a.unused == 0 and a.flags == 0 and a.nonce == b'\xf1\xe9\xab\xc9\x8c\x0by\x18' and a.data == b"" ############ @@ -1706,7 +1713,7 @@ raw(ICMPv6NIReplyUnknown(nonce=b'\x00'*8)) == b'\x8c\x02\x00\x00\x00\x00\x00\x00 = ICMPv6NIReplyRefuse - basic dissection a=ICMPv6NIReplyRefuse(b'\x8c\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.type == 140 and a.code == 2 and a.cksum == 0 and a.unused == 0 and a.flags == 0 and a.nonce == b'\x00'*8 and a.data == None +a.type == 140 and a.code == 2 and a.cksum == 0 and a.unused == 0 and a.flags == 0 and a.nonce == b'\x00'*8 and a.data == b"" ############ diff --git a/test/scapy/layers/ntp.uts b/test/scapy/layers/ntp.uts index 0f9f58a5c9d..eddac823816 100644 --- a/test/scapy/layers/ntp.uts +++ b/test/scapy/layers/ntp.uts @@ -129,7 +129,7 @@ assert p.status == 0 assert p.association_id == 0 assert p.offset == 0 assert p.count == 0 -assert p.data == b'' +assert p.data is None = NTP Control (mode 6) - CTL_OP_READSTAT (2) - response @@ -175,7 +175,7 @@ assert p.op_code == 2 assert p.sequence == 18 assert p.status == 0 assert p.association_id == 64655 -assert p.data == b'' +assert p.data is None = NTP Control (mode 6) - CTL_OP_READVAR (2) - response (1st packet) @@ -246,7 +246,7 @@ assert p.response == 1 assert p.err == 1 assert p.more == 0 assert p.op_code == 2 -assert len(p.data.load) == 0 +assert not p.data assert p.authenticator.key_id == 1 assert bytes_hex(p.authenticator.dgst) == b'97280249dba07338ed722860db4a580a' @@ -279,7 +279,7 @@ assert p.op_code == 3 assert hasattr(p, 'status_word') assert isinstance(p.status_word, NTPErrorStatusPacket) assert p.status_word.error_code == 5 -assert len(p.data.load) == 0 +assert not p.data assert p.authenticator.key_id == 1 assert bytes_hex(p.authenticator.dgst) == b'807a80fbafc470679853a8e57865811c' @@ -358,8 +358,8 @@ assert p.response == 0 assert p.err == 0 assert p.more == 0 assert p.op_code == 12 -assert p.data == b'' -assert p.authenticator == b'' +assert p.data is None +assert not p.authenticator = NTP Control (mode 6) - CTL_OP_REQ_NONCE (2) - response @@ -373,7 +373,7 @@ assert p.err == 0 assert p.more == 0 assert p.op_code == 12 assert p.data.load == b'nonce=db4186a2e1d9022472e24bc9\r\n' -assert p.authenticator == b'' +assert not p.authenticator = NTP Control (mode 6) - CTL_OP_READ_MRU (1) - request @@ -387,7 +387,7 @@ assert p.err == 0 assert p.op_code == 10 assert p.count == 40 assert p.data.load == b'nonce=db4186a2e1d9022472e24bc9, frags=32' -assert p.authenticator == b'' +assert not p.authenticator = NTP Control (mode 6) - CTL_OP_READ_MRU (2) - response s = b'\xd6\x8a\x00\x08\x00\x00\x00\x00\x00\x00\x00\xe9nonce=db4186a2e2073198b93c6419, addr.0=192.168.122.100:123,\r\nfirst.0=0xdb418673.323e1a89, last.0=0xdb418673.323e1a89, ct.0=1,\r\nmv.0=36, rs.0=0x0, WWQ.0=18446744073709509383, now=0xdb4186a2.e20ff8f4,\r\nlast.newest=0xdb418673.323e1a89\r\n\x00\x00\x00' @@ -400,7 +400,7 @@ assert p.err == 0 assert p.op_code == 10 assert p.count == 233 assert p.data.load == b'nonce=db4186a2e2073198b93c6419, addr.0=192.168.122.100:123,\r\nfirst.0=0xdb418673.323e1a89, last.0=0xdb418673.323e1a89, ct.0=1,\r\nmv.0=36, rs.0=0x0, WWQ.0=18446744073709509383, now=0xdb4186a2.e20ff8f4,\r\nlast.newest=0xdb418673.323e1a89\r\n\x00\x00\x00' -assert p.authenticator == b'' +assert not p.authenticator ############ diff --git a/test/tls.uts b/test/tls.uts index 474db2b3a1f..72bca4b8084 100644 --- a/test/tls.uts +++ b/test/tls.uts @@ -1522,7 +1522,7 @@ assert r2.tls_session.tls_version == 0x303 pkt = TLSServerHello(b"\x02\x00\x00\x28\x03\x03ABCDEFGHIJKLMNOPQRSTUVWXYZ012345\x00\x00\x39\x00\x00\x00") assert pkt.extlen == 0 -assert pkt.ext is None +assert pkt.ext == [] = Issue 3324 - FFDH support diff --git a/test/tls13.uts b/test/tls13.uts index 750ce979a67..c046048ed1c 100644 --- a/test/tls13.uts +++ b/test/tls13.uts @@ -1212,3 +1212,10 @@ assert ch.len == 103 assert ch.client_shares[0].kxlen == 97 assert len(ch.client_shares[0].key_exchange) == 97 += Parse TLS 1.3 Client Hello with non-rfc 5077 ticket + +ch = TLS(b'\x16\x03\x01\x01\x1a\x01\x00\x01\x16\x03\x03\xec\x9c>\xb2\x9e|B\x05\x17f\x86\xc8\x18\x0421\x87\x87\x12\xf6\xec\xa2J\x95\x84[\xf8\xab\xe9gK> \xc6%\xff&wn)\xb2\xf5\xe8_x\x96\xe9\nEsK\xda\x86o\x82f\xa5\xbadk\xf4Ar~}\x00\x08\x13\x02\x13\x03\x13\x01\x00\xff\x01\x00\x00\xc5\x00\x0b\x00\x04\x03\x00\x01\x02\x00\n\x00\x16\x00\x14\x00\x1d\x00\x17\x00\x1e\x00\x19\x00\x18\x01\x00\x01\x01\x01\x02\x01\x03\x01\x04\x00#\x00\x00\x00\x16\x00\x00\x00\x17\x00\x00\x00\r\x00\x1e\x00\x1c\x04\x03\x05\x03\x06\x03\x08\x07\x08\x08\x08\t\x08\n\x08\x0b\x08\x04\x08\x05\x08\x06\x04\x01\x05\x01\x06\x01\x00+\x00\x03\x02\x03\x04\x00-\x00\x02\x01\x01\x003\x00&\x00$\x00\x1d\x00 l\x19\xe1f1 )6\xbf\x91\x9e\xab\xd2\x06\x16\x0b|\x88\xf7,\xf1\x88\x99Z\xb6\xb3\x93\xe4\x08z\x8a\t\x00)\x00:\x00\x15\x00\x0fClient_identity\x00\x00\x00\x00\x00! m\xf3^\xc1l\xac5\xf2\xe3=\xeb\xe3\x81\xd3\xb3\xdd\xbd\xbd\x01\xc9\xdd\x01i\x8c1\xa0ye\xcd\x04\x9e\x9c') + +assert isinstance(ch.msg[0].ext[9], TLS_Ext_PreSharedKey_CH) +assert ch.msg[0].ext[9].identities[0].identity.load == b'Client_identity' +assert ch.msg[0].ext[9].identities[0].obfuscated_ticket_age == 0 From cdbdb150ce69dc10debc1d659e5303d13b15f7fa Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 27 Jun 2023 23:05:07 +0200 Subject: [PATCH 019/122] Update definitions.py (#4046) Co-authored-by: superuserx --- .../automotive/volkswagen/definitions.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scapy/contrib/automotive/volkswagen/definitions.py b/scapy/contrib/automotive/volkswagen/definitions.py index f9b09dacf6b..17854a98289 100644 --- a/scapy/contrib/automotive/volkswagen/definitions.py +++ b/scapy/contrib/automotive/volkswagen/definitions.py @@ -3155,16 +3155,16 @@ UDS_RDBI.dataIdentifiers[0xf1df] = "ECU Programming Information" -UDS_RC.routineControlTypes[0x0202] = "Check Memory" -UDS_RC.routineControlTypes[0x0203] = "Check Programming Preconditions" -UDS_RC.routineControlTypes[0x0317] = "Reset of Adaption Values" -UDS_RC.routineControlTypes[0x0366] = "Reset of all Adaptions" -UDS_RC.routineControlTypes[0x03e7] = "Reset to Factory Settings" -UDS_RC.routineControlTypes[0x045a] = "Clear user defined DTC information" -UDS_RC.routineControlTypes[0x0544] = "Verify partial software checksum" -UDS_RC.routineControlTypes[0x0594] = "Check upload preconditions" -UDS_RC.routineControlTypes[0xff00] = "Erase Memory" -UDS_RC.routineControlTypes[0xff01] = "Check Programming Dependencies" +UDS_RC.routineControlIdentifiers[0x0202] = "Check Memory" +UDS_RC.routineControlIdentifiers[0x0203] = "Check Programming Preconditions" +UDS_RC.routineControlIdentifiers[0x0317] = "Reset of Adaption Values" +UDS_RC.routineControlIdentifiers[0x0366] = "Reset of all Adaptions" +UDS_RC.routineControlIdentifiers[0x03e7] = "Reset to Factory Settings" +UDS_RC.routineControlIdentifiers[0x045a] = "Clear user defined DTC information" +UDS_RC.routineControlIdentifiers[0x0544] = "Verify partial software checksum" +UDS_RC.routineControlIdentifiers[0x0594] = "Check upload preconditions" +UDS_RC.routineControlIdentifiers[0xff00] = "Erase Memory" +UDS_RC.routineControlIdentifiers[0xff01] = "Check Programming Dependencies" UDS_RD.dataFormatIdentifiers[0x0000] = "Uncompressed" From 99db114bf7087447ab8c9b1fd5e1f92d17f88884 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 7 Jul 2023 22:12:39 +0200 Subject: [PATCH 020/122] Fix issue in Automotive Executor using make_lined_table (#4045) * Fix issue in Automotive Executor using make_lined_table * add unit test --- scapy/contrib/automotive/scanner/executor.py | 2 +- test/contrib/automotive/scanner/uds_scanner.uts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scapy/contrib/automotive/scanner/executor.py b/scapy/contrib/automotive/scanner/executor.py index eeec5bd3745..196a88b6002 100644 --- a/scapy/contrib/automotive/scanner/executor.py +++ b/scapy/contrib/automotive/scanner/executor.py @@ -413,7 +413,7 @@ def show_testcases_status(self): for t in self.configuration.test_cases: for s in self.state_graph.nodes: data += [(repr(s), t.__class__.__name__, t.has_completed(s))] - make_lined_table(data, lambda tup: (tup[0], tup[1], tup[2])) + make_lined_table(data, lambda *tup: (tup[0], tup[1], tup[2])) @property def supported_responses(self): diff --git a/test/contrib/automotive/scanner/uds_scanner.uts b/test/contrib/automotive/scanner/uds_scanner.uts index 7c72325ba19..8dc67970ae0 100644 --- a/test/contrib/automotive/scanner/uds_scanner.uts +++ b/test/contrib/automotive/scanner/uds_scanner.uts @@ -196,6 +196,7 @@ scanner = executeScannerInVirtualEnvironment( "request_length": 1}) scanner.show_testcases() +scanner.show_testcases_status() assert len(scanner.state_paths) == 5 assert scanner.scan_completed assert scanner.progress() > 0.95 From f37c4021eb191b2cf95693dc308a06d2bfb17717 Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Sat, 8 Jul 2023 00:17:27 +0300 Subject: [PATCH 021/122] [dhcp6] add Captive-Portal option (#4050) * [dhcp] rename default-url to captive-portal According to https://datatracker.ietf.org/doc/html/rfc8910#section-4.2 IANA has updated the "BOOTP Vendor Extensions and DHCP Options" registry (https://www.iana.org/assignments/bootp-dhcp-parameters) as follows: Tag: 114 Name: DHCP Captive-Portal Data Length: N Meaning: DHCP Captive-Portal Reference: RFC 8910 * [dhcp6] add Captive-Portal option https://datatracker.ietf.org/doc/html/rfc8910#section-2.2 The format of the IPv6 Captive-Portal DHCP option is shown below. 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | option-code | option-len | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ . URI (variable length) . | ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ option-code: The Captive-Portal DHCPv6 Option (103) (two octets). option-len: The unsigned 16-bit length, in octets, of the URI. URI: The URI for the captive portal API endpoint to which the user should connect. --- scapy/layers/dhcp.py | 2 +- scapy/layers/dhcp6.py | 10 ++++++++++ test/scapy/layers/dhcp.uts | 5 +++-- test/scapy/layers/dhcp6.uts | 17 +++++++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/scapy/layers/dhcp.py b/scapy/layers/dhcp.py index b3a03596455..0e1aef72679 100644 --- a/scapy/layers/dhcp.py +++ b/scapy/layers/dhcp.py @@ -320,7 +320,7 @@ def randval(self): 101: StrField("tcode", ""), 112: IPField("netinfo-server-address", "0.0.0.0"), 113: StrField("netinfo-server-tag", ""), - 114: StrField("default-url", ""), + 114: StrField("captive-portal", ""), 116: ByteField("auto-config", 0), 117: ShortField("name-service-search", 0,), 118: IPField("subnet-selection", "0.0.0.0"), diff --git a/scapy/layers/dhcp6.py b/scapy/layers/dhcp6.py index a1109efae88..776d2113c13 100644 --- a/scapy/layers/dhcp6.py +++ b/scapy/layers/dhcp6.py @@ -124,6 +124,7 @@ def _dhcp6_dispatcher(x, *args, **kargs): 66: "OPTION_RELAY_SUPPLIED_OPTIONS", # RFC6422 68: "OPTION_VSS", # RFC6607 79: "OPTION_CLIENT_LINKLAYER_ADDR", # RFC6939 + 103: "OPTION_CAPTIVE_PORTAL", # RFC8910 112: "OPTION_MUD_URL", # RFC8520 } @@ -181,6 +182,7 @@ def _dhcp6_dispatcher(x, *args, **kargs): 66: "DHCP6OptRelaySuppliedOpt", # RFC6422 68: "DHCP6OptVSS", # RFC6607 79: "DHCP6OptClientLinkLayerAddr", # RFC6939 + 103: "DHCP6OptCaptivePortal", # RFC8910 112: "DHCP6OptMudUrl", # RFC8520 } @@ -1026,6 +1028,14 @@ class DHCP6OptClientLinkLayerAddr(_DHCP6OptGuessPayload): # RFC6939 _LLAddrField("clladdr", ETHER_ANY)] +class DHCP6OptCaptivePortal(_DHCP6OptGuessPayload): # RFC8910 + name = "DHCP6 Option - Captive-Portal" + fields_desc = [ShortEnumField("optcode", 103, dhcp6opts), + FieldLenField("optlen", None, length_of="URI"), + StrLenField("URI", "", + length_from=lambda pkt: pkt.optlen)] + + class DHCP6OptMudUrl(_DHCP6OptGuessPayload): # RFC8520 name = "DHCP6 Option - MUD URL" fields_desc = [ShortEnumField("optcode", 112, dhcp6opts), diff --git a/test/scapy/layers/dhcp.uts b/test/scapy/layers/dhcp.uts index a6defec10b1..8abac11f540 100644 --- a/test/scapy/layers/dhcp.uts +++ b/test/scapy/layers/dhcp.uts @@ -44,8 +44,8 @@ s3 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="05:04:03:02:01:00")/DHCP(option ("ieee802-3-encapsulation", 2),("max_dgram_reass_size", 120), ("pxelinux_path_prefix","/some/path"), "end"])) assert s3 == b'E\x00\x01=\x00\x01\x00\x00@\x11{\xad\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01)\x04i\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0005:04:03:02:01:0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc\x02\x04\x00\x00\x00{b\x0fwww.example.comp\x04\n\x00\x00\x01$\x01\x02\x16\x02\x00x\xd2\n/some/path\xff' -s4 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("mud-url", "https://example.org"), "end"])) -assert s4 == b'E\x00\x01"\x00\x01\x00\x00@\x11{\xc8\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01\x0e\tr\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0000:01:02:03:04:0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc\xa1\x13https://example.org\xff' +s4 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("mud-url", "https://example.org"), ("captive-portal", "https://example.com"), "end"])) +assert s4 == b"E\x00\x017\x00\x01\x00\x00@\x11{\xb3\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01#\xb8\xe7\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0000:01:02:03:04:0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc\xa1\x13https://example.orgr\x13https://example.com\xff" s5 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("classless_static_routes", "192.168.123.4/32:10.0.0.1", "169.254.254.0/24:10.0.1.2"), "end"])) assert s5 == b'E\x00\x01 \x00\x01\x00\x00@\x11{\xca\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01\x0c\xabQ\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0000:01:02:03:04:0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Scy\x11 \xc0\xa8{\x04\n\x00\x00\x01\x18\xa9\xfe\xfe\n\x00\x01\x02\xff' @@ -80,6 +80,7 @@ assert p3[DHCP].options[6] == "end" p4 = IP(s4) assert DHCP in p4 assert p4[DHCP].options[0] == ("mud-url", b"https://example.org") +assert p4[DHCP].options[1] == ("captive-portal", b"https://example.com") p5 = IP(s5) assert DHCP in p5 diff --git a/test/scapy/layers/dhcp6.uts b/test/scapy/layers/dhcp6.uts index ae23482e108..2806add53c3 100644 --- a/test/scapy/layers/dhcp6.uts +++ b/test/scapy/layers/dhcp6.uts @@ -1188,6 +1188,23 @@ r = b"\x00O\x00\x08\x00\x01\x00\x01\x02\x03\x04\x05" p = DHCP6OptClientLinkLayerAddr(r) assert p.clladdr == "00:01:02:03:04:05" +############ +############ ++ Test DHCP6 Option Captive-Portal + += Basic build & dissect +s = raw(DHCP6OptCaptivePortal()) +assert s == b"\x00\x67\x00\x00" + +p = DHCP6OptCaptivePortal(s) +assert p.optcode == 103 +assert p.optlen == 0 +assert p.URI == b"" + +p = DHCP6OptCaptivePortal(b"\x00\x67\x00\x13https://example.org") +assert p.optcode == 103 +assert p.optlen == 19 +assert p.URI == b"https://example.org" ############ ############ From f19daa6af6a1fda1892044110603d8ae9bc3652e Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Sat, 8 Jul 2023 00:12:23 +0200 Subject: [PATCH 022/122] Add LE3BytesEnumField and LEX3BytesEnumField (#4041) * Add LE3BytesEnumField and LEX3BytesEnumField * Cleanup, code-reuse + what I had in mind --------- Co-authored-by: gpotter2 <10530980+gpotter2@users.noreply.github.com> --- doc/scapy/build_dissect.rst | 6 ++-- scapy/contrib/loraphy2wan.py | 8 +++--- scapy/contrib/scada/pcom.py | 6 ++-- scapy/fields.py | 54 ++++++++++++++++++++++++++---------- test/fields.uts | 18 +++++++++++- 5 files changed, 68 insertions(+), 24 deletions(-) diff --git a/doc/scapy/build_dissect.rst b/doc/scapy/build_dissect.rst index b099798137a..26c0148ea14 100644 --- a/doc/scapy/build_dissect.rst +++ b/doc/scapy/build_dissect.rst @@ -869,10 +869,12 @@ Legend: XShortField X3BytesField # three bytes as hex - LEX3BytesField # little endian three bytes as hex + XLE3BytesField # little endian three bytes as hex ThreeBytesField # three bytes as decimal LEThreeBytesField # little endian three bytes as decimal - + LE3BytesEnumField + XLE3BytesEnumField + IntField SignedIntField LEIntField diff --git a/scapy/contrib/loraphy2wan.py b/scapy/contrib/loraphy2wan.py index 01487d457dd..3750466aa95 100644 --- a/scapy/contrib/loraphy2wan.py +++ b/scapy/contrib/loraphy2wan.py @@ -25,7 +25,6 @@ ConditionalField, IntField, LEShortField, - LEX3BytesField, MayEnd, MultipleTypeField, PacketField, @@ -36,6 +35,7 @@ XBitField, XByteField, XIntField, + XLE3BytesField, XLEIntField, XShortField, ) @@ -80,7 +80,7 @@ def extract_padding(self, p): class DevAddrElem(Packet): name = "DevAddrElem" fields_desc = [XByteField("NwkID", 0x0), - LEX3BytesField("NwkAddr", b"\x00" * 3)] + XLE3BytesField("NwkAddr", b"\x00" * 3)] CIDs_up = {0x01: "ResetInd", @@ -609,8 +609,8 @@ class Join_Request(Packet): class Join_Accept(Packet): name = "Join_Accept" dcflist = False - fields_desc = [LEX3BytesField("JoinAppNonce", 0), - LEX3BytesField("NetID", 0), + fields_desc = [XLE3BytesField("JoinAppNonce", 0), + XLE3BytesField("NetID", 0), XLEIntField("DevAddr", 0), DLsettings, XByteField("RxDelay", 0), diff --git a/scapy/contrib/scada/pcom.py b/scapy/contrib/scada/pcom.py index d8386a22142..98603260588 100755 --- a/scapy/contrib/scada/pcom.py +++ b/scapy/contrib/scada/pcom.py @@ -21,7 +21,7 @@ from scapy.layers.inet import TCP from scapy.fields import XShortField, ByteEnumField, XByteField, \ StrFixedLenField, StrLenField, LEShortField, \ - LEFieldLenField, LEX3BytesField, XLEShortField + LEFieldLenField, XLE3BytesField, XLEShortField from scapy.volatile import RandShort from scapy.compat import bytes_encode, orb @@ -193,7 +193,7 @@ class PCOMBinaryRequest(PCOMBinary): XByteField("id", 0x0), XByteField("reserved1", 0xfe), XByteField("reserved2", 0x1), - LEX3BytesField("reserved3", 0x0), + XLE3BytesField("reserved3", 0x0), PCOMBinaryCommandField("command", None), XByteField("reserved4", 0x0), StrFixedLenField("commandSpecific", '', 6), @@ -212,7 +212,7 @@ class PCOMBinaryResponse(PCOMBinary): XByteField("reserved1", 0xfe), XByteField("id", 0x0), XByteField("reserved2", 0x1), - LEX3BytesField("reserved3", 0x0), + XLE3BytesField("reserved3", 0x0), PCOMBinaryCommandField("command", None), XByteField("reserved4", 0x0), StrFixedLenField("commandSpecific", '', 6), diff --git a/scapy/fields.py b/scapy/fields.py index a07b11b01cd..bd031374a93 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -1085,12 +1085,21 @@ def getfield(self, pkt, s): return s[3:], self.m2i(pkt, struct.unpack(self.fmt, s[:3] + b"\x00")[0]) # noqa: E501 -class LEX3BytesField(LEThreeBytesField, XByteField): +class XLE3BytesField(LEThreeBytesField, XByteField): def i2repr(self, pkt, x): # type: (Optional[Packet], int) -> str return XByteField.i2repr(self, pkt, x) +def LEX3BytesField(*args, **kwargs): + # type: (*Any, **Any) -> Any + warnings.warn( + "LEX3BytesField is deprecated. Use XLE3BytesField", + DeprecationWarning + ) + return XLE3BytesField(*args, **kwargs) + + class NBytesField(Field[int, List[int]]): def __init__(self, name, default, sz): # type: (str, Optional[int], int) -> None @@ -2552,6 +2561,10 @@ def any2i_one(self, pkt, x): x = self.s2i_cb(x) return cast(I, x) + def _i2repr(self, pkt, x): + # type: (Optional[Packet], I) -> str + return repr(x) + def i2repr_one(self, pkt, x): # type: (Optional[Packet], I) -> str if self not in conf.noenum and not isinstance(x, VolatileValue): @@ -2564,7 +2577,7 @@ def i2repr_one(self, pkt, x): ret = self.i2s_cb(x) if ret is not None: return ret - return repr(x) + return self._i2repr(pkt, x) def any2i(self, pkt, x): # type: (Optional[Packet], Any) -> Union[I, List[I]] @@ -2713,18 +2726,31 @@ def __init__(self, name, default, enum): class XShortEnumField(ShortEnumField): - def i2repr_one(self, pkt, x): - # type: (Optional[Packet], int) -> str - if self not in conf.noenum and not isinstance(x, VolatileValue): - if self.i2s is not None: - try: - return self.i2s[x] - except KeyError: - pass - elif self.i2s_cb: - ret = self.i2s_cb(x) - if ret is not None: - return ret + def _i2repr(self, pkt, x): + # type: (Optional[Packet], Any) -> str + return lhex(x) + + +class LE3BytesEnumField(LEThreeBytesField, _EnumField[int]): + __slots__ = EnumField.__slots__ + + def __init__(self, name, default, enum): + # type: (str, Optional[int], Dict[int, str]) -> None + _EnumField.__init__(self, name, default, enum) + LEThreeBytesField.__init__(self, name, default) + + def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> int + return _EnumField.any2i(self, pkt, x) # type: ignore + + def i2repr(self, pkt, x): # type: ignore + # type: (Optional[Packet], Any) -> Union[List[str], str] + return _EnumField.i2repr(self, pkt, x) + + +class XLE3BytesEnumField(LE3BytesEnumField): + def _i2repr(self, pkt, x): + # type: (Optional[Packet], Any) -> str return lhex(x) diff --git a/test/fields.uts b/test/fields.uts index 8081107b21e..b3f3cc6f1c2 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -164,7 +164,7 @@ class TestThreeBytesField(Packet): fields_desc = [ X3BytesField('test1', None), ThreeBytesField('test2', None), - LEX3BytesField('test3', None), + XLE3BytesField('test3', None), LEThreeBytesField('test4', None), ] @@ -1199,6 +1199,22 @@ class Breakfast(Packet): assert raw(Breakfast(juice="ORANGE")) == b"\x00\x01" += LE3BytesEnumField +~ field le3bytesenumfield + +f = LE3BytesEnumField('test', 0, {0: 'Foo', 1: 'Bar'}) + += LE3BytesEnumField.i2repr_one +~ field le3bytesenumfield + +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == 'Bar' +assert f.i2repr_one(None, 2) == '2' + += XLE3BytesEnumField + +assert XLE3BytesEnumField("a", 0, {0: "test"}).i2repr_one(None, 0) == "test" +assert XLE3BytesEnumField("a", 0, {0: "test"}).i2repr_one(None, 1) == "0x1" ############ ############ From 66fdfa362185e376f7bff483bc4675f6e4a268c4 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Fri, 7 Jul 2023 15:13:50 -0700 Subject: [PATCH 023/122] Implement 802.1ah I-Tag for PBB MAC-in-MAC encapsulation (#4009) * Implement 802.1ah I-tag for PBB mac-in-mac encapsulation Signed-off-by: Alex Forencich * Add unit tests for 802.1Q Signed-off-by: Alex Forencich * Remove SPBM contrib * Fix some ETHER_TYPES --------- Signed-off-by: Alex Forencich Co-authored-by: gpotter2 <10530980+gpotter2@users.noreply.github.com> --- scapy/contrib/spbm.py | 58 ---------------------------------------- scapy/layers/l2.py | 35 +++++++++++++++++++++++- test/contrib/spbm.uts | 18 ------------- test/scapy/layers/l2.uts | 30 +++++++++++++++++++++ 4 files changed, 64 insertions(+), 77 deletions(-) delete mode 100644 scapy/contrib/spbm.py delete mode 100644 test/contrib/spbm.uts diff --git a/scapy/contrib/spbm.py b/scapy/contrib/spbm.py deleted file mode 100644 index fae33866c7b..00000000000 --- a/scapy/contrib/spbm.py +++ /dev/null @@ -1,58 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later -# This file is part of Scapy -# See https://scapy.net/ for more information - -# scapy.contrib.description = Shorest Path Bridging Mac-in-mac (SBPM) -# scapy.contrib.status = loads - -""" -IEEE 802.1aq - Shorest Path Bridging Mac-in-mac (SPBM): - -Ethernet based link state protocol that enables -- Layer 2 Unicast -- Layer 2 Multicast -- Layer 3 Unicast -- Layer 3 Multicast virtualized services - -https://en.wikipedia.org/wiki/IEEE_802.1aq -Modeled after the scapy VXLAN contribution - -Example SPB Frame Creation -__________________________ - -Note the outer Dot1Q Ethertype marking (0x88e7) - -:: - backboneEther = Ether(dst='00:bb:00:00:90:00', src='00:bb:00:00:40:00', type=0x8100) # noqa: E501 - backboneDot1Q = Dot1Q(vlan=4051,type=0x88e7) - backboneServiceID = SPBM(prio=1,isid=20011) - customerEther = Ether(dst='00:1b:4f:5e:ca:00',src='00:00:00:00:00:01',type=0x8100) # noqa: E501 - customerDot1Q = Dot1Q(prio=1,vlan=11,type=0x0800) - customerIP = IP(src='10.100.11.10',dst='10.100.12.10',id=0x0629,len=106) # noqa: E501 - customerUDP = UDP(sport=1024,dport=1025,chksum=0,len=86) - - spb_example = backboneEther/backboneDot1Q/backboneServiceID/customerEther/customerDot1Q/customerIP/customerUDP/"Payload" # noqa: E501 -""" - -from scapy.packet import Packet, bind_layers -from scapy.fields import BitField, ThreeBytesField -from scapy.layers.l2 import Ether, Dot1Q, Dot1AD - - -class SPBM(Packet): - name = "SPBM" - fields_desc = [BitField("prio", 0, 3), - BitField("dei", 0, 1), - BitField("nca", 0, 1), - BitField("res1", 0, 1), - BitField("res2", 0, 2), - ThreeBytesField("isid", 0)] - - def mysummary(self): - return self.sprintf("SPBM (isid=%SPBM.isid%") - - -bind_layers(Ether, SPBM, type=0x88e7) -bind_layers(Dot1Q, SPBM, type=0x88e7) -bind_layers(Dot1AD, SPBM, type=0x88e7) -bind_layers(SPBM, Ether) diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py index e7973a1cda8..ea3e05d808c 100644 --- a/scapy/layers/l2.py +++ b/scapy/layers/l2.py @@ -45,6 +45,7 @@ SourceIPField, StrFixedLenField, StrLenField, + ThreeBytesField, XByteField, XIntField, XShortEnumField, @@ -237,7 +238,8 @@ def i2m(self, pkt, x): 21: "ATM", } -ETHER_TYPES[0x88a8] = '802_AD' +ETHER_TYPES[0x88a8] = '802_1AD' +ETHER_TYPES[0x88e7] = '802_1AH' ETHER_TYPES[ETH_P_MACSEC] = '802_1AE' @@ -686,25 +688,55 @@ class Dot1AD(Dot1Q): name = '802_1AD' +class Dot1AH(Packet): + name = "802_1AH" + fields_desc = [BitField("prio", 0, 3), + BitField("dei", 0, 1), + BitField("nca", 0, 1), + BitField("res1", 0, 1), + BitField("res2", 0, 2), + ThreeBytesField("isid", 0)] + + def answers(self, other): + # type: (Packet) -> int + if isinstance(other, Dot1AH): + if self.isid == other.isid: + return self.payload.answers(other.payload) + return 0 + + def mysummary(self): + # type: () -> str + return self.sprintf("802.1ah (isid=%Dot1AH.isid%") + + +conf.neighbor.register_l3(Ether, Dot1AH, l2_register_l3) + + bind_layers(Dot3, LLC) bind_layers(Ether, LLC, type=122) bind_layers(Ether, LLC, type=34928) bind_layers(Ether, Dot1Q, type=33024) bind_layers(Ether, Dot1AD, type=0x88a8) +bind_layers(Ether, Dot1AH, type=0x88e7) bind_layers(Dot1AD, Dot1AD, type=0x88a8) bind_layers(Dot1AD, Dot1Q, type=0x8100) +bind_layers(Dot1AD, Dot1AH, type=0x88e7) bind_layers(Dot1Q, Dot1AD, type=0x88a8) +bind_layers(Dot1Q, Dot1AH, type=0x88e7) +bind_layers(Dot1AH, Ether) bind_layers(Ether, Ether, type=1) bind_layers(Ether, ARP, type=2054) bind_layers(CookedLinux, LLC, proto=122) bind_layers(CookedLinux, Dot1Q, proto=33024) bind_layers(CookedLinux, Dot1AD, type=0x88a8) +bind_layers(CookedLinux, Dot1AH, type=0x88e7) bind_layers(CookedLinux, Ether, proto=1) bind_layers(CookedLinux, ARP, proto=2054) bind_layers(MPacketPreamble, Ether) bind_layers(GRE, LLC, proto=122) bind_layers(GRE, Dot1Q, proto=33024) bind_layers(GRE, Dot1AD, type=0x88a8) +bind_layers(GRE, Dot1AH, type=0x88e7) bind_layers(GRE, Ether, proto=0x6558) bind_layers(GRE, ARP, proto=2054) bind_layers(GRE, GRErouting, {"routing_present": 1}) @@ -714,6 +746,7 @@ class Dot1AD(Dot1Q): bind_layers(LLC, SNAP, dsap=170, ssap=170, ctrl=3) bind_layers(SNAP, Dot1Q, code=33024) bind_layers(SNAP, Dot1AD, type=0x88a8) +bind_layers(SNAP, Dot1AH, type=0x88e7) bind_layers(SNAP, Ether, code=1) bind_layers(SNAP, ARP, code=2054) bind_layers(SNAP, STP, code=267) diff --git a/test/contrib/spbm.uts b/test/contrib/spbm.uts deleted file mode 100644 index fb135e5dede..00000000000 --- a/test/contrib/spbm.uts +++ /dev/null @@ -1,18 +0,0 @@ -% Regression tests for the spbm module - -+ Basic SPBM test - -= Test build and dissection - -backboneEther = Ether(dst='00:bb:00:00:90:00', src='00:bb:00:00:40:00', type=0x8100) -backboneDot1Q = Dot1Q(vlan=4051,type=0x88e7) -backboneServiceID = SPBM(prio=1,isid=20011) -customerEther = Ether(dst='00:1b:4f:5e:ca:00',src='00:00:00:00:00:01',type=0x8100) -customerDot1Q = Dot1Q(prio=1,vlan=11,type=0x0800) -customerIP = IP(src='10.100.11.10',dst='10.100.12.10',id=0x0629,len=106) -customerUDP = UDP(sport=1024,dport=1025,chksum=0,len=86) - -pkt = backboneEther/backboneDot1Q/backboneServiceID/customerEther/customerDot1Q/customerIP/customerUDP/"Payload" -pkt = Ether(raw(pkt)) -assert SPBM in pkt -assert pkt[SPBM].payload.payload.payload.src == '10.100.11.10' diff --git a/test/scapy/layers/l2.uts b/test/scapy/layers/l2.uts index b13b82bc676..d4d5185670d 100644 --- a/test/scapy/layers/l2.uts +++ b/test/scapy/layers/l2.uts @@ -191,6 +191,36 @@ p = ARP(pdst='192.168.178.0/24') assert "Net" in repr(p) +############ +############ ++ 802.1Q bridging tests + += 802.1Q VLAN +p = Ether(raw(Ether() / Dot1Q(vlan=99) / b"Payload")) +assert p[Dot1Q].vlan == 99 + += 802.1ad Q-in-Q +p = Ether(raw(Ether() / Dot1AD(vlan=88) / Dot1Q(vlan=99) / b"Payload")) +assert p[Dot1AD].vlan == 88 +assert p[Dot1Q].vlan == 99 + += 802.1ah PBB mac-in-mac +p = Ether(raw(Ether() / Dot1AD(vlan=88) / Dot1AH(isid=123456) / Ether() / Dot1Q(vlan=99) / b"Payload")) +assert p[Dot1AD].vlan == 88 +assert p[Dot1AH].isid == 123456 +assert p[Dot1Q].vlan == 99 + += 802.1ah PBB mac-in-mac - answer +p = Ether(raw(Ether() / Dot1AD(vlan=88) / Dot1AH(isid=123456) / Ether() / Dot1Q(vlan=99) / b"Payload")) +q = Ether(raw(Ether() / Dot1AD(vlan=88) / Dot1AH(isid=123456) / Ether() / Dot1Q(vlan=99) / b"Response")) +r = Ether(raw(Ether() / Dot1AD(vlan=88) / Dot1AH(isid=123456) / Ether() / Dot1Q(vlan=90) / b"Payload")) +s = Ether(raw(Ether() / Dot1AD(vlan=88) / Dot1AH(isid=987654) / Ether() / Dot1Q(vlan=99) / b"Payload")) + +assert q.answers(p) +assert not r.answers(p) +assert not s.answers(p) + + ############ ############ + CookedLinux From 6dbdc37bd896c5477f4f9e28e7d1bff63f3dc6fd Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Sat, 8 Jul 2023 19:03:13 +0200 Subject: [PATCH 024/122] Fix recv of HSFZ and DoIP (#4053) * Fix recv of HSFZ and DoIP * add unit test for hsfz * add test case for DoIP --- scapy/contrib/automotive/bmw/hsfz.py | 21 ++++++++++++++++++++ scapy/contrib/automotive/doip.py | 21 ++++++++++++++++++++ test/contrib/automotive/bmw/hsfz.uts | 29 ++++++++++++++++++++++++++++ test/contrib/automotive/doip.uts | 27 ++++++++++++++++++++++++++ 4 files changed, 98 insertions(+) diff --git a/scapy/contrib/automotive/bmw/hsfz.py b/scapy/contrib/automotive/bmw/hsfz.py index c177dd620c2..376a968e4b4 100644 --- a/scapy/contrib/automotive/bmw/hsfz.py +++ b/scapy/contrib/automotive/bmw/hsfz.py @@ -86,6 +86,27 @@ def __init__(self, ip='127.0.0.1', port=6801): s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.connect((self.ip, self.port)) StreamSocket.__init__(self, s, HSFZ) + self.buffer = b"" + + def recv(self, x=MTU): + # type: (int) -> Optional[Packet] + if self.buffer: + len_data = self.buffer[:4] + else: + len_data = self.ins.recv(4, socket.MSG_PEEK) + if len(len_data) != 4: + return None + + len_int = struct.unpack(">I", len_data)[0] + len_int += 6 + self.buffer += self.ins.recv(len_int - len(self.buffer)) + + if len(self.buffer) != len_int: + return None + + pkt = self.basecls(self.buffer) # type: Packet + self.buffer = b"" + return pkt class UDS_HSFZSocket(HSFZSocket): diff --git a/scapy/contrib/automotive/doip.py b/scapy/contrib/automotive/doip.py index 98dde3944ed..bce9944aaa0 100644 --- a/scapy/contrib/automotive/doip.py +++ b/scapy/contrib/automotive/doip.py @@ -287,12 +287,33 @@ def __init__(self, ip='127.0.0.1', port=13400, activate_routing=True, self.ip = ip self.port = port self.source_address = source_address + self.buffer = b"" self._init_socket() if activate_routing: self._activate_routing( source_address, target_address, activation_type, reserved_oem) + def recv(self, x=MTU): + # type: (int) -> Optional[Packet] + if self.buffer: + len_data = self.buffer[:8] + else: + len_data = self.ins.recv(8, socket.MSG_PEEK) + if len(len_data) != 8: + return None + + len_int = struct.unpack(">I", len_data[4:8])[0] + len_int += 8 + self.buffer += self.ins.recv(len_int - len(self.buffer)) + + if len(self.buffer) != len_int: + return None + + pkt = self.basecls(self.buffer) # type: Packet + self.buffer = b"" + return pkt + def _init_socket(self, sock_family=socket.AF_INET): # type: (int) -> None s = socket.socket(sock_family, socket.SOCK_STREAM) diff --git a/test/contrib/automotive/bmw/hsfz.uts b/test/contrib/automotive/bmw/hsfz.uts index e889867b1dd..aad8aa6c14a 100644 --- a/test/contrib/automotive/bmw/hsfz.uts +++ b/test/contrib/automotive/bmw/hsfz.uts @@ -74,3 +74,32 @@ assert pkt.dst == 0x10 assert pkt.type == 1 assert pkt.securitySeed == b"0" * 0xfff00 += Test HSFZSocket + + +server_up = threading.Event() +def server(): + buffer = bytes(HSFZ(type=1, src=0xf4, dst=0x10) / Raw(b'\x11\x22\x33' * 1024)) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('127.0.0.1', 6801)) + sock.listen(1) + server_up.set() + connection, address = sock.accept() + connection.send(buffer[:1024]) + time.sleep(0.1) + connection.send(buffer[1024:]) + connection.close() + finally: + sock.close() + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +sock = HSFZSocket() + +pkts = sock.sniff(timeout=1, count=1) +assert len(pkts) == 1 +assert len(pkts[0]) > 2048 diff --git a/test/contrib/automotive/doip.uts b/test/contrib/automotive/doip.uts index 99ebef167d1..5d8101651e8 100644 --- a/test/contrib/automotive/doip.uts +++ b/test/contrib/automotive/doip.uts @@ -380,3 +380,30 @@ assert req.hashret() == resp.hashret() # exclude TCP layer from answers check assert resp[3].answers(req[3]) assert not req[3].answers(resp[3]) + += Test DoIPSocket + +server_up = threading.Event() +def server(): + buffer = b'\x02\xfd\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x02\xfd\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('127.0.0.1', 13400)) + sock.listen(1) + server_up.set() + connection, address = sock.accept() + connection.send(buffer) + connection.close() + finally: + sock.close() + + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +sock = DoIPSocket(activate_routing=False) + +pkts = sock.sniff(timeout=1, count=2) +assert len(pkts) == 2 From cbcd038d84a292ced96ddb6c0d443e1f6166352c Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Mon, 10 Jul 2023 19:42:29 +0300 Subject: [PATCH 025/122] [inet6] add Captive-Portal RA option (#4059) https://www.rfc-editor.org/rfc/rfc8910.html#name-the-captive-portal-ipv6-ra- 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Type | Length | URI . +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ . . . . . . . +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Type: 37 Length: 8-bit unsigned integer. The length of the option (including the Type and Length fields) in units of 8 bytes. URI: The URI for the captive portal API endpoint to which the user should connect. This MUST be padded with NUL (0x00) to make the total option length (including the Type and Length fields) a multiple of 8 bytes. Combined with f37c4021eb191b2cf95693dc308a06d2bfb17717 this patch should fully cover RFC8910. The patch has been used and tested downstream for about a week and helped to trigger various issues like https://github.com/systemd/systemd/issues/28229 https://github.com/systemd/systemd/issues/28231 https://github.com/systemd/systemd/issues/28277 https://github.com/systemd/systemd/issues/28283 --- scapy/layers/inet6.py | 36 +++++++++++++++++++++++++++++++++++- test/scapy/layers/inet6.uts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py index b54cca234c4..2619c828808 100644 --- a/scapy/layers/inet6.py +++ b/scapy/layers/inet6.py @@ -1718,7 +1718,8 @@ def extract_padding(self, s): 24: "ICMPv6NDOptRouteInfo", 25: "ICMPv6NDOptRDNSS", 26: "ICMPv6NDOptEFA", - 31: "ICMPv6NDOptDNSSL" + 31: "ICMPv6NDOptDNSSL", + 37: "ICMPv6NDOptCaptivePortal", } icmp6ndraprefs = {0: "Medium (default)", @@ -2053,6 +2054,39 @@ class ICMPv6NDOptDNSSL(_ICMPv6NDGuessPayload, Packet): # RFC 6106 def mysummary(self): return self.sprintf("%name% ") + ", ".join(self.searchlist) + +# URI MUST be padded with NUL (0x00) to make the total option length +# (including the Type and Length fields) a multiple of 8 bytes. +# https://www.rfc-editor.org/rfc/rfc8910.html#name-the-captive-portal-ipv6-ra- +class CaptivePortalURI(StrLenField): + def i2len(self, pkt, x): + return len(self.i2m(pkt, x)) + + def i2m(self, pkt, x): + r = (len(x) + 2) % 8 + if r: + x += b"\x00" * (8 - r) + return x + + def m2i(self, pkt, x): + return x.rstrip(b"\x00") + + +class ICMPv6NDOptCaptivePortal(_ICMPv6NDGuessPayload, Packet): # RFC 8910 + name = "ICMPv6 Neighbor Discovery Option - Captive-Portal Option" + fields_desc = [ByteField("type", 37), + FieldLenField("len", None, length_of="URI", fmt="B", + adjust=lambda pkt, x: (2 + x) // 8), + + # Zero length is nonsensical but it's treated as 1 here to + # let the dissector skip bogus options more or less gracefully + CaptivePortalURI("URI", "", + length_from=lambda pkt: 8 * max(pkt.len, 1) - 2) + ] + + def mysummary(self): + return self.sprintf("%name% %URI%") + # End of ICMPv6 Neighbor Discovery Options. diff --git a/test/scapy/layers/inet6.uts b/test/scapy/layers/inet6.uts index 4bc7e4573d3..18d19babc1d 100644 --- a/test/scapy/layers/inet6.uts +++ b/test/scapy/layers/inet6.uts @@ -1208,6 +1208,36 @@ p.type == 31 and p.len == 2 and p.res == 0 and p.lifetime == 60 and p.searchlist ICMPv6NDOptDNSSL(searchlist=["home.", "office.", "{"]).mysummary() == "ICMPv6 Neighbor Discovery Option - DNS Search List Option home., office., {" +############ +############ ++ ICMPv6NDOptCaptivePortal Class Test + += ICMPv6NDOptCaptivePortal - Basic Instantiation +raw(ICMPv6NDOptCaptivePortal()) == b"\x25\x01\x00\x00\x00\x00\x00\x00" + += ICMPv6NDOptCaptivePortal - Instantiation with captive portal URI +raw(ICMPv6NDOptCaptivePortal(URI="https://example.com")) == b"\x25\x03https://example.com\x00\x00\x00" + += ICMPv6NDOptCaptivePortal - Instantiation where total length is already a multiple of 8 bytes +p = ICMPv6NDOptCaptivePortal(URI="abcdef") +len(p) == 8 and raw(p) == b"\x25\x01abcdef" and ICMPv6NDOptCaptivePortal(raw(p)).URI == b"abcdef" + += ICMPv6NDOptCaptivePortal - Basic Dissection +p = ICMPv6NDOptCaptivePortal(b"\x25\x01\x00\x00\x00\x00\x00\x00") +p.type == 37 and p.len == 1 and p.URI == b"" + += ICMPv6NDOptCaptivePortal - Basic Dissection with captive portal URI +p = ICMPv6NDOptCaptivePortal(b"\x25\x03https://example.com\x00\x00\x00") +p.type == 37 and p.len == 3 and p.URI == b"https://example.com" + += ICMPv6NDOptCaptivePortal - Dissection with zero length +p = ICMPv6NDOptCaptivePortal(b"\x25\x00abcdefgh") +p.type == 37 and p.len == 0 and p.URI == b"abcdef" and Raw in p and len(p[Raw]) == 2 + += ICMPv6NDOptCaptivePortal - Summary Output +ICMPv6NDOptCaptivePortal(URI="https://example.com").mysummary() == "ICMPv6 Neighbor Discovery Option - Captive-Portal Option 'https://example.com'" + + ############ ############ + ICMPv6NDOptEFA Class Test From 171037c5bee37f197abc586098532847cd4dc73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20David?= Date: Mon, 10 Jul 2023 18:43:51 +0200 Subject: [PATCH 026/122] Small typos in usage.rst (#4052) --- doc/scapy/usage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/scapy/usage.rst b/doc/scapy/usage.rst index 1e6c1e37842..f377259d126 100644 --- a/doc/scapy/usage.rst +++ b/doc/scapy/usage.rst @@ -784,7 +784,7 @@ Advanced Sniffing - Sniffing Sessions Scapy includes some basic Sessions, but it is possible to implement your own. Available by default: -- :py:class:`~scapy.sessions.IPSession` -> *defragment IP packets* on-the-flow, to make a stream usable by ``prn``. +- :py:class:`~scapy.sessions.IPSession` -> *defragment IP packets* on-the-fly, to make a stream usable by ``prn``. - :py:class:`~scapy.sessions.TCPSession` -> *defragment certain TCP protocols*. Currently supports: - HTTP 1.0 - TLS @@ -1798,7 +1798,7 @@ Scapy dissects slowly and/or misses packets under heavy loads. .. note:: - Please bare in mind that Scapy is not designed to be blazing fast, but rather easily hackable & extensible. The packet model makes it VERY easy to create new layers, compared to pretty much all other alternatives, but comes with a performance cost. Of course, we still do our best to make Scapy as fast as possible, but it's not the absolute main goal. + Please bear in mind that Scapy is not designed to be blazing fast, but rather easily hackable & extensible. The packet model makes it VERY easy to create new layers, compared to pretty much all other alternatives, but comes with a performance cost. Of course, we still do our best to make Scapy as fast as possible, but it's not the absolute main goal. Solution ^^^^^^^^ From ca57be524fccd648f4f923ef12be20adcd30aa14 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Mon, 10 Jul 2023 21:10:06 +0200 Subject: [PATCH 027/122] Create .git-blame-ignore-revs (#4063) --- .git-blame-ignore-revs | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..d1ab8e752f9 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,51 @@ +# This file contains the list of commits that should be excluded from +# git blame. Read more informations on: +# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view + +# PEPin - https://github.com/secdev/scapy/issues/1277 +# E231 - missing whitespace after ',' +e7365b2baeded1a0e1e3b59bc0ad14a78d6e3086 +# E30* - Incorrect number of blank lines +b770bbc58c26437b354c0bd21dc4e2fcfa3abfdf +# E20* - Incorrect number of whitespace +6861a35d8ed4466df7b2ff82341e60caf9ff869a +# E12* - visual indent +275ad3246b5231bb046a66bcfdf3654d67fdea20 +# W29* - useless whitespaces +453f2592f7b6f2b8677619769f8427932894dc1c +# E251 - unexpected spaces around keyword / parameter equals +203254afd771b42ccf0fcca96ba92dc4075cfe4a +# E26 - comments +b7a3db73dfd17ec1e7bbace8d52464982bf8ea8d +# E1 - incorrect indentation +f2f1de742aa36167e2c86247a26ed5e7393366ea +# F821 - undefined name 'name' +f8525ea9f17cedf148febcab8d1dab51ddca9afe +# E2* - whitespaces errors +1c2fe99c131bb05e009896410766371a2f870175 +# E71* - tests syntax +927c157b58918d5fdce9714a3c35627339cc8657 +# F841 - local variable 'name' is assigned to but never used +dbe409531a22d1245cf4669f72a425b42c83b0db +# PEPin several fixes +93232490193ca2b59e3b1425131913d28f408f7a +# E501 - line too long (> 79 characters) +e89d8965748439adc253714316de7a9a35b8bd73 +# F601 - dictionary key repeated with different values +0fd7d76550e56831f887664202d743846d3619dd +# F811 - redefinition of unused variable/class/... +10454d1ca243d0fd8d2ab4a148d688e3ea916e49 +# E402 - module level import not at top of file +0f4a904d2801e8bbbc82880345ad453ceb6ee34f +# E722 - do not use bare except +a35575ff22da176a8b515405faea9a689462da0c +# E741 - ambiguous variable name 'l' +7c61676aef950ca268eac480902dd91cb0abe3a4 +# F405 - variable/function/... may be undefined, or defined from star +8773983edb0336db7aa84777dee2aa9892508418 +# F401 - 'module' imported but unused +a58e1b90a704c394216a0b5a864a50931754bdf7 +# W502 - line break before binary operator +9687222c3f0af6ef89ecfe15e5b983e1f7b5b31e +# E275 - Missing whitespace after keyword +08b1f9d67c8e716fd44036a027bdc90dcb9fcfdf From 9abb9cb6dddfaef2af5b15c44d451ed63cb26bfc Mon Sep 17 00:00:00 2001 From: haradama Date: Sat, 8 Jul 2023 20:06:57 +0900 Subject: [PATCH 028/122] Update SD class to support Explicit Initial Data Control Flag --- scapy/contrib/automotive/someip.py | 3 ++- test/contrib/automotive/someip.uts | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/scapy/contrib/automotive/someip.py b/scapy/contrib/automotive/someip.py index 1bb491644bc..32b64376322 100644 --- a/scapy/contrib/automotive/someip.py +++ b/scapy/contrib/automotive/someip.py @@ -432,7 +432,8 @@ class SD(_SDPacketBase): _sdFlag = collections.namedtuple('Flag', 'mask offset') FLAGSDEF = { "REBOOT": _sdFlag(mask=0x80, offset=7), - "UNICAST": _sdFlag(mask=0x40, offset=6) + "UNICAST": _sdFlag(mask=0x40, offset=6), + "EXPLICIT_INITIAL_DATA_CONTROL": _sdFlag(mask=0x20, offset=5), } name = "SD" diff --git a/test/contrib/automotive/someip.uts b/test/contrib/automotive/someip.uts index 183b2202f1c..37b32e85d12 100644 --- a/test/contrib/automotive/someip.uts +++ b/test/contrib/automotive/someip.uts @@ -208,9 +208,16 @@ assert p.flags == 0x40 p.set_flag("UNICAST", 0) assert p.flags == 0x00 +p.set_flag("EXPLICIT_INITIAL_DATA_CONTROL", 1) +assert p.flags == 0x20 + +p.set_flag("EXPLICIT_INITIAL_DATA_CONTROL", 0) +assert p.flags == 0x00 + p.set_flag("REBOOT", 1) p.set_flag("UNICAST", 1) -assert p.flags == 0xc0 +p.set_flag("EXPLICIT_INITIAL_DATA_CONTROL", 1) +assert p.flags == 0xe0 + SD Get SOME/IP Packet From 5216e25cbcaaefd079fdaa563996b4cd00cabb24 Mon Sep 17 00:00:00 2001 From: Aniket Gargya Date: Thu, 13 Jul 2023 11:15:19 -0500 Subject: [PATCH 029/122] Add bufsz parameter to RawPcapWriter constructor (#4067) --- scapy/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scapy/utils.py b/scapy/utils.py index b882b01f923..3c1b85acb4d 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -1939,6 +1939,7 @@ def __init__(self, sync=False, # type: bool nano=False, # type: bool snaplen=MTU, # type: int + bufsz=4096, # type: int ): # type: (...) -> None """ @@ -1963,7 +1964,6 @@ def __init__(self, self.endian = endianness self.sync = sync self.nano = nano - bufsz = 4096 if sync: bufsz = 0 From 9a1675e7b74ba85373668ceef0c4025bb8a9c4ba Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Tue, 2 May 2023 22:00:00 +0000 Subject: [PATCH 030/122] Turn PIMv2HelloLANPruneDelayValue.t into BitField to make its repr work. It's just one bit so FlagsField isn't suitable there. Fixes: ``` >>> repr(PIMv2HelloLANPruneDelayValue(t=1)) Traceback (most recent call last): File "", line 2, in File "scapy/packet.py", line 563, in __repr__ val = f.i2repr(self, fval) ^^^^^^^^^^^^^^^^^^^^ File "scapy/fields.py", line 3092, in i2repr return "None" if x is None else str(self._fixup_val(x)) ^^^^^^^^^^^^^^^^^^^^^^^ File "scapy/fields.py", line 2947, in __str__ return ("+" if self.multi else "").join(r) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: sequence item 0: expected str instance, int found ``` It's a follow-up to 6e205b21608f1672a01ce2e186526fd117ac7397 --- scapy/contrib/pim.py | 4 ++-- test/contrib/pim.uts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scapy/contrib/pim.py b/scapy/contrib/pim.py index ec716f4cb36..e4f91a00697 100644 --- a/scapy/contrib/pim.py +++ b/scapy/contrib/pim.py @@ -13,7 +13,7 @@ from scapy.packet import Packet, bind_layers from scapy.fields import BitFieldLenField, BitField, BitEnumField, ByteField, \ ShortField, XShortField, IPField, PacketListField, \ - IntField, FieldLenField, BoundStrLenField, FlagsField + IntField, FieldLenField, BoundStrLenField from scapy.layers.inet import IP from scapy.utils import checksum from scapy.compat import orb @@ -114,7 +114,7 @@ class PIMv2HelloHoldtime(_PIMv2GenericHello): class PIMv2HelloLANPruneDelayValue(_PIMv2GenericHello): name = "PIMv2 Hello Options : LAN Prune Delay Value" fields_desc = [ - FlagsField("t", 0, 1, [0, 1]), + BitField("t", 0, 1), BitField("propagation_delay", 500, 15), ShortField("override_interval", 2500), ] diff --git a/test/contrib/pim.uts b/test/contrib/pim.uts index 7fbba92a565..8aa4f7e2b1c 100644 --- a/test/contrib/pim.uts +++ b/test/contrib/pim.uts @@ -24,6 +24,8 @@ assert (hello_pkt[PIMv2Hello].option[2][PIMv2HelloLANPruneDelay].value[0][PIMv2H assert (hello_pkt[PIMv2Hello].option[2][PIMv2HelloLANPruneDelay].value[0][PIMv2HelloLANPruneDelayValue].override_interval == 2500) assert (hello_pkt[PIMv2Hello].option[3][PIMv2HelloGenerationID].type == 20) +repr(PIMv2HelloLANPruneDelayValue(t=1)) + = PIMv2 Join/Prune - instantiation jp_data = b'\x01\x00^\x00\x00\r\x00\xd0\xcb\x00\xba\xe4\x08\x00E\xc0\x00rY\xfb\x00\x00\x01gT3\x15\x15\x15\x15\xe0\x00\x00\r#\x00\x1b\x18\x01\x00\x15\x15\x15\x16\x00\x04\x00\xd2\x01\x00\x00 \xef\x01\x01\x0b\x00\x01\x00\x00\x01\x00\x07 \x16\x16\x16\x15\x01\x00\x00 \xef\x01\x01\x0c\x00\x01\x00\x00\x01\x00\x07 \x16\x16\x16\x15\x01\x00\x00 \xef\x01\x01\x0b\x00\x00\x00\x01\x01\x00\x07 \x16\x16\x16\x15\x01\x00\x00 \xef\x01\x01\x0c\x00\x00\x00\x01\x01\x00\x07 \x16\x16\x16\x15' From 98f5ed2a67f41f8f028539d59a808caaff10a6ed Mon Sep 17 00:00:00 2001 From: Tera <24725862+teraa@users.noreply.github.com> Date: Sun, 18 Jun 2023 22:05:57 +0200 Subject: [PATCH 031/122] fix param name in docstring Docstring incorrectly refers to the `proto` param of `in4_chksum` method as `nh` Submitting this PR again because I deleted the fork before PR was merged. --- scapy/layers/inet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scapy/layers/inet.py b/scapy/layers/inet.py index 6b0e64dcdc4..95c07073800 100644 --- a/scapy/layers/inet.py +++ b/scapy/layers/inet.py @@ -693,7 +693,7 @@ def in4_chksum(proto, u, p): # type: (int, IP, bytes) -> int """IPv4 Pseudo Header checksum as defined in RFC793 - :param nh: value of upper layer protocol + :param proto: value of upper layer protocol :param u: upper layer instance :param p: the payload of the upper layer provided as a string """ From 36a7b8772a7f5bd91045ebc11abdc9dd4bab181a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 14 Jul 2023 17:05:25 +0200 Subject: [PATCH 032/122] Speedup isotp scan verification function (#4066) * Speedup isotp scan verification function * fix * fix flake --- scapy/contrib/isotp/isotp_scanner.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/scapy/contrib/isotp/isotp_scanner.py b/scapy/contrib/isotp/isotp_scanner.py index bfd4760fa08..15a53cd8feb 100644 --- a/scapy/contrib/isotp/isotp_scanner.py +++ b/scapy/contrib/isotp/isotp_scanner.py @@ -3,6 +3,7 @@ # See https://scapy.net/ for more information # Copyright (C) Nils Weiss # Copyright (C) Alexander Schroeder +import itertools import json # scapy.contrib.description = ISO-TP (ISO 15765-2) Scanner Utility # scapy.contrib.status = library @@ -203,16 +204,16 @@ def scan(sock, # type: SuperSocket return return_values cleaned_ret_val = dict() # type: Dict[int, Tuple[Packet, int]] - for tested_id in return_values.keys(): + retest_ids = list(set( + itertools.chain.from_iterable( + range(max(0, i - 2), i + 2) for i in return_values.keys()))) + for value in retest_ids: if stop_event is not None and stop_event.is_set(): break - for value in range(max(0, tested_id - 2), tested_id + 2, 1): - if stop_event is not None and stop_event.is_set(): - break - sock.send(get_isotp_packet(value, False, extended_can_id)) - sock.sniff(prn=lambda pkt: get_isotp_fc(value, cleaned_ret_val, - noise_ids, False, pkt), - timeout=sniff_time * 10, store=False) + sock.send(get_isotp_packet(value, False, extended_can_id)) + sock.sniff(prn=lambda pkt: get_isotp_fc(value, cleaned_ret_val, + noise_ids, False, pkt), + timeout=sniff_time * 10, store=False) return cleaned_ret_val From 10bceffd1b4f4d697ab42cf0b477f501c9b307d2 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 14 Jul 2023 17:06:25 +0200 Subject: [PATCH 033/122] Improve display of bytes strings (#4062) * Fix #4044 * fix macsec.uts * fix sebek tests --- scapy/fields.py | 2 +- test/contrib/macsec.uts | 2 +- test/contrib/sebek.uts | 24 ++++++++++++------------ test/fields.uts | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/scapy/fields.py b/scapy/fields.py index bd031374a93..c3f02ff1a05 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -1430,7 +1430,7 @@ def any2i(self, pkt, x): def i2repr(self, pkt, x): # type: (Optional[Packet], I) -> str if isinstance(x, bytes): - return repr(plain_str(x)) + return repr(x) return super(_StrField, self).i2repr(pkt, x) def i2m(self, pkt, x): diff --git a/test/contrib/macsec.uts b/test/contrib/macsec.uts index dd0ab123cbd..218373e1fad 100755 --- a/test/contrib/macsec.uts +++ b/test/contrib/macsec.uts @@ -21,7 +21,7 @@ assert m[MACsec].SC assert m[MACsec].E assert m[MACsec].C assert m[MACsec].SCI == b'\x52\x54\x00\x13\x01\x56\x00\x01' -assert m[MACsec].mysummary() == r"AN=0, PN=100, SCI='RT\x00\x13\x01V\x00\x01', IPv4" +assert m[MACsec].mysummary() == r"AN=0, PN=100, SCI=b'RT\x00\x13\x01V\x00\x01', IPv4" = MACsec - basic encryption - encrypted sa = MACsecSA(sci=b'\x52\x54\x00\x13\x01\x56\x00\x01', an=0, pn=100, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=1, send_sci=1) diff --git a/test/contrib/sebek.uts b/test/contrib/sebek.uts index f83eb1c1c3c..39ef69c3480 100644 --- a/test/contrib/sebek.uts +++ b/test/contrib/sebek.uts @@ -8,7 +8,7 @@ = Layer binding 1 pkt = IP() / UDP() / SebekHead() / SebekV1(cmd="diepotato") assert pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 1 -assert pkt.summary() == "IP / UDP / SebekHead / Sebek v1 read ('diepotato')" +assert pkt.summary() == "IP / UDP / SebekHead / Sebek v1 read (b'diepotato')" = Packet dissection 1 pkt = IP(raw(pkt)) @@ -17,7 +17,7 @@ pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 1 = Layer binding 2 pkt = IP() / UDP() / SebekHead() / SebekV2Sock(cmd="diepotato") assert pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 2 and pkt[SebekHead].type ==2 -assert pkt.summary() == "IP / UDP / SebekHead / Sebek v2 socket ('diepotato')" +assert pkt.summary() == "IP / UDP / SebekHead / Sebek v2 socket (b'diepotato')" = Packet dissection 2 pkt = IP(raw(pkt)) @@ -26,7 +26,7 @@ pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 2 and pkt[SebekHead = Layer binding 3 pkt = IPv6()/UDP()/SebekHead()/SebekV3() assert pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 3 -assert pkt.summary() == "IPv6 / UDP / SebekHead / Sebek v3 read ('')" +assert pkt.summary() == "IPv6 / UDP / SebekHead / Sebek v3 read (b'')" = Packet dissection 3 pkt = IPv6(raw(pkt)) @@ -35,12 +35,12 @@ pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 3 = Nonsense summaries assert SebekHead(version=2).summary() == "Sebek Header v2 read" -assert SebekV1(cmd="diepotato").summary() == "Sebek v1 ('diepotato')" -assert SebekV2(cmd="diepotato").summary() == "Sebek v2 ('diepotato')" -assert (SebekHead()/SebekV2(cmd="nottoday")).summary() == "SebekHead / Sebek v2 read ('nottoday')" -assert SebekV3(cmd="diepotato").summary() == "Sebek v3 ('diepotato')" -assert (SebekHead()/SebekV3(cmd="nottoday")).summary() == "SebekHead / Sebek v3 read ('nottoday')" -assert SebekV3Sock(cmd="diepotato").summary() == "Sebek v3 socket ('diepotato')" -assert (SebekHead()/SebekV3Sock(cmd="nottoday")).summary() == "SebekHead / Sebek v3 socket ('nottoday')" -assert SebekV2Sock(cmd="diepotato").summary() == "Sebek v2 socket ('diepotato')" -assert (SebekHead()/SebekV2Sock(cmd="nottoday")).summary() == "SebekHead / Sebek v2 socket ('nottoday')" +assert SebekV1(cmd="diepotato").summary() == "Sebek v1 (b'diepotato')" +assert SebekV2(cmd="diepotato").summary() == "Sebek v2 (b'diepotato')" +assert (SebekHead()/SebekV2(cmd="nottoday")).summary() == "SebekHead / Sebek v2 read (b'nottoday')" +assert SebekV3(cmd="diepotato").summary() == "Sebek v3 (b'diepotato')" +assert (SebekHead()/SebekV3(cmd="nottoday")).summary() == "SebekHead / Sebek v3 read (b'nottoday')" +assert SebekV3Sock(cmd="diepotato").summary() == "Sebek v3 socket (b'diepotato')" +assert (SebekHead()/SebekV3Sock(cmd="nottoday")).summary() == "SebekHead / Sebek v3 socket (b'nottoday')" +assert SebekV2Sock(cmd="diepotato").summary() == "Sebek v2 socket (b'diepotato')" +assert (SebekHead()/SebekV2Sock(cmd="nottoday")).summary() == "SebekHead / Sebek v2 socket (b'nottoday')" diff --git a/test/fields.uts b/test/fields.uts index b3f3cc6f1c2..0dfccfd1486 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -218,7 +218,7 @@ class TestStrField(Packet): p = TestStrField(s1="cafe", s2="deadbeef") assert raw(p) == b'\x04\x00cafedeadbeef' print(p.sprintf("%s1% %s2%")) -assert p.sprintf("%s1% %s2%") == "'cafe' 'deadbeef'" +assert p.sprintf("%s1% %s2%") == "b'cafe' b'deadbeef'" = StrFieldUtf16 From f19d36ff8221cd7b7b14eff7ef9ba5776291f9d5 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 15 Jul 2023 00:11:57 +0200 Subject: [PATCH 034/122] Fix tiny issue in inet6.uts --- test/scapy/layers/inet6.uts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/scapy/layers/inet6.uts b/test/scapy/layers/inet6.uts index 18d19babc1d..b9f78a0b9ce 100644 --- a/test/scapy/layers/inet6.uts +++ b/test/scapy/layers/inet6.uts @@ -1235,7 +1235,7 @@ p = ICMPv6NDOptCaptivePortal(b"\x25\x00abcdefgh") p.type == 37 and p.len == 0 and p.URI == b"abcdef" and Raw in p and len(p[Raw]) == 2 = ICMPv6NDOptCaptivePortal - Summary Output -ICMPv6NDOptCaptivePortal(URI="https://example.com").mysummary() == "ICMPv6 Neighbor Discovery Option - Captive-Portal Option 'https://example.com'" +ICMPv6NDOptCaptivePortal(URI="https://example.com").mysummary() == "ICMPv6 Neighbor Discovery Option - Captive-Portal Option b'https://example.com'" ############ From 5bc8fc9c1d3a4f2b078d4f6e0820650dc5c114e0 Mon Sep 17 00:00:00 2001 From: Raslan Darawsheh Date: Mon, 17 Jul 2023 16:26:25 +0300 Subject: [PATCH 035/122] NSH: fix layer binding for VXLAN GPE (#4054) Use correct field name to bind VXLAN GPE Signed-off-by: Raslan Darawsheh --- scapy/contrib/nsh.py | 2 +- test/contrib/nsh.uts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scapy/contrib/nsh.py b/scapy/contrib/nsh.py index aed7a37a619..8175e9a7627 100644 --- a/scapy/contrib/nsh.py +++ b/scapy/contrib/nsh.py @@ -75,7 +75,7 @@ def mysummary(self): bind_layers(Ether, NSH, {'type': 0x894F}, type=0x894F) -bind_layers(VXLAN, NSH, {'flags': 0xC, 'nextproto': 4}, nextproto=4) +bind_layers(VXLAN, NSH, {'flags': 0xC, 'NextProtocol': 4}, NextProtocol=4) bind_layers(GRE, NSH, {'proto': 0x894F}, proto=0x894F) bind_layers(NSH, IP, nextproto=1) diff --git a/test/contrib/nsh.uts b/test/contrib/nsh.uts index 5c8f3ac6e4b..0751edd1338 100644 --- a/test/contrib/nsh.uts +++ b/test/contrib/nsh.uts @@ -14,3 +14,7 @@ raw(Ether(src="00:00:00:00:00:01", dst="00:00:00:00:00:02")/IP(src="1.1.1.1", ds = 0 length variable length context header NSH raw(NSH(mdtype=2, spi=0xF0F0F0, si=0xFF)) == b'\x0f\xc2\x02\x03\xf0\xf0\xf0\xff' + += Build a NSH over VXLAN packet and verify bindings +raw(Ether(dst='0c:42:a1:5f:fb:e0', src='b8:59:9f:cd:de:3e')/IPv6(src='::1', dst='::2')/UDP(sport=10, dport=8472)/VXLAN(NextProtocol=4, vni=4660)/NSH()/NSH()/Ether(dst='0c:42:a1:5f:fb:e4', src='b8:59:9f:cd:de:33')/IP(src='10.200.100.10', dst='2.2.2.3')/TCP(sport=123, dport=333)) == b'\x0cB\xa1_\xfb\xe0\xb8Y\x9f\xcd\xde>\x86\xdd`\x00\x00\x00\x00v\x11@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\n!\x18\x00v\x05F\x0c\x00\x00\x04\x00\x124\x00\x0f\xc6\x01\x04\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xc6\x01\x03\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0cB\xa1_\xfb\xe4\xb8Y\x9f\xcd\xde3\x08\x00E\x00\x00(\x00\x01\x00\x00@\x06\x07\xf9\n\xc8d\n\x02\x02\x02\x03\x00{\x01M\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x1bD\x00\x00' + From eb3658d5b1537ebdef8312d71403a10eb0e3ce07 Mon Sep 17 00:00:00 2001 From: Nathan Korth Date: Tue, 18 Jul 2023 05:23:36 -0400 Subject: [PATCH 036/122] Don't require newline at end of pcapng comment (#4021) I couldn't find anything in the spec about requiring this. --- scapy/utils.py | 9 +-------- test/regression.uts | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/scapy/utils.py b/scapy/utils.py index 3c1b85acb4d..1b58464f343 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -1553,12 +1553,7 @@ def _read_options(self, options): tsresol & 127 ) if code == 1 and length >= 1 and 4 + length < len(options): - comment = options[4:4 + length] - newline_index = comment.find(b"\n") - if newline_index == -1: - warning("PcapNg: invalid comment option") - break - opts["comment"] = comment[:newline_index] + opts["comment"] = options[4:4 + length] if code == 0: if length != 0: warning("PcapNg: invalid option length %d for end-of-option" % length) # noqa: E501 @@ -2210,8 +2205,6 @@ def _write_block_epb(self, comment_opt = None if comment: comment = bytes_encode(comment) - if not comment.endswith(b"\n"): - comment += b"\n" comment_opt = struct.pack(self.endian + "HH", 1, len(comment)) # Pad Option Value to 32 bits diff --git a/test/regression.uts b/test/regression.uts index ed0e8a26bef..eb51d61863a 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -1961,7 +1961,7 @@ p = Ether() / IPv6() / TCP() p.comment = b"Hello Scapy!" wrpcapng(tmpfile, p) l = rdpcap(tmpfile) -assert l[0].comment.strip() == p.comment +assert l[0].comment == p.comment = Read a pcap file with wirelen != captured len pktpcapwirelen = rdpcap(pcapwirelenfile) From 7d31fcc9187afaa11756bc4cec516fa702c59f90 Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Thu, 20 Jul 2023 19:14:36 +0200 Subject: [PATCH 037/122] Refactor libc sockets and add BluetoothMonitorSocket (#4042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Antonio Vázquez Blanco --- doc/scapy/layers/bluetooth.rst | 20 +++++- scapy/layers/bluetooth.py | 125 +++++++++++++++++++++++---------- 2 files changed, 106 insertions(+), 39 deletions(-) diff --git a/doc/scapy/layers/bluetooth.rst b/doc/scapy/layers/bluetooth.rst index 1e8b0c69161..c9c4a535e1c 100644 --- a/doc/scapy/layers/bluetooth.rst +++ b/doc/scapy/layers/bluetooth.rst @@ -71,12 +71,28 @@ There are multiple protocols available for Bluetooth through ``AF_BLUETOOTH`` sockets: Host-controller interface (HCI) ``BTPROTO_HCI`` - Scapy class: ``BluetoothHCISocket`` - This is the "base" level interface for communicating with a Bluetooth controller. Everything is built on top of this, and this represents about as close to the physical layer as one can get with regular Bluetooth hardware. + Scapy class: ``BluetoothMonitorSocket`` + + Allows to capture all HCI transactions that are taking place over all HCI + interfaces (including in BlueZ core). It is intended to perform monitoring of + transactions, device attachment and removal, BlueZ logging... + + Scapy class: ``BluetoothUserSocket`` + + This socket interacts with a Bluetooth controller with complete and exclusive + control of de device. This means that BlueZ will not try to take control of + the interface and will not help you manage connections via this interface. + + Scapy class: ``BluetoothHCISocket`` + + Using HCI protocol, this socket interacts with a Bluetooth controller but + does not have exclusive control over it, allowing BlueZ and other + applications to still use the adapter to communicate with devices. + Logical Link Control and Adaptation Layer Protocol (L2CAP) ``BTPROTO_L2CAP`` Scapy class: ``BluetoothL2CAPSocket`` diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index 74aa01c1434..8411d3b38ec 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -52,6 +52,20 @@ from scapy.error import warning +############ +# Consts # +############ + +# From hci.h +HCI_CHANNEL_RAW = 0 +HCI_CHANNEL_USER = 1 +HCI_CHANNEL_MONITOR = 2 +HCI_CHANNEL_CONTROL = 3 +HCI_CHANNEL_LOGGING = 4 + +HCI_DEV_NONE = 0xffff + + ########## # Layers # ########## @@ -173,6 +187,18 @@ class HCI_PHDR_Hdr(Packet): } +class BT_Mon_Hdr(Packet): + ''' + Bluetooth Linux Monitor Transport Header + ''' + name = 'Bluetooth Linux Monitor Transport Header' + fields_desc = [ + LEShortField('opcode', None), + LEShortField('adapter_id', None), + LEShortField('len', None) + ] + + class HCI_Hdr(Packet): name = "HCI header" fields_desc = [ByteEnumField("type", 2, _bluetooth_packet_types)] @@ -1493,20 +1519,15 @@ class sockaddr_hci(ctypes.Structure): ] -class BluetoothUserSocket(SuperSocket): - desc = "read/write H4 over a Bluetooth user channel" - - def __init__(self, adapter_index=0): +class _BluetoothLibcSocket(SuperSocket): + def __init__(self, socket_domain, socket_type, socket_protocol, sock_address): + # type: (int, int, int, sockaddr_hci) -> None if WINDOWS: warning("Not available on Windows") return - # s = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_RAW, socket.BTPROTO_HCI) # noqa: E501 - # s.bind((0,1)) - - # yeah, if only - # thanks to Python's weak ass socket and bind implementations, we have - # to call down into libc with ctypes - + # Python socket and bind implementations do not allow us to pass down + # the correct parameters. We must call libc functions directly via + # ctypes. sockaddr_hcip = ctypes.POINTER(sockaddr_hci) ctypes.cdll.LoadLibrary("libc.so.6") libc = ctypes.CDLL("libc.so.6") @@ -1521,40 +1542,24 @@ def __init__(self, adapter_index=0): ctypes.c_int) bind.restype = ctypes.c_int - ######## - # actual code - - s = socket_c(31, 3, 1) # (AF_BLUETOOTH, SOCK_RAW, HCI_CHANNEL_USER) + # Socket + s = socket_c(socket_domain, socket_type, socket_protocol) if s < 0: - raise BluetoothSocketError("Unable to open PF_BLUETOOTH socket") + raise BluetoothSocketError( + f"Unable to open socket({socket_domain}, {socket_type}, " + f"{socket_protocol})") - sa = sockaddr_hci() - sa.sin_family = 31 # AF_BLUETOOTH - sa.hci_dev = adapter_index # adapter index - sa.hci_channel = 1 # HCI_USER_CHANNEL - - r = bind(s, sockaddr_hcip(sa), sizeof(sa)) + # Bind + r = bind(s, sockaddr_hcip(sock_address), sizeof(sock_address)) if r != 0: raise BluetoothSocketError("Unable to bind") self.hci_fd = s - self.ins = self.outs = socket.fromfd(s, 31, 3, 1) - - def send_command(self, cmd): - opcode = cmd.opcode - self.send(cmd) - while True: - r = self.recv() - if r.type == 0x04 and r.code == 0xe and r.opcode == opcode: - if r.status != 0: - raise BluetoothCommandError("Command %x failed with %x" % (opcode, r.status)) # noqa: E501 - return r - - def recv(self, x=MTU): - return HCI_Hdr(self.ins.recv(x)) + self.ins = self.outs = socket.fromfd( + s, socket_domain, socket_type, socket_protocol) def readable(self, timeout=0): - (ins, outs, foo) = select.select([self.ins], [], [], timeout) + (ins, _, _) = select.select([self.ins], [], [], timeout) return len(ins) > 0 def flush(self): @@ -1582,6 +1587,52 @@ def close(self): close(self.hci_fd) +class BluetoothUserSocket(_BluetoothLibcSocket): + desc = "read/write H4 over a Bluetooth user channel" + + def __init__(self, adapter_index=0): + sa = sockaddr_hci() + sa.sin_family = socket.AF_BLUETOOTH + sa.hci_dev = adapter_index + sa.hci_channel = HCI_CHANNEL_USER + super().__init__( + socket_domain=socket.AF_BLUETOOTH, + socket_type=socket.SOCK_RAW, + socket_protocol=socket.BTPROTO_HCI, + sock_address=sa) + + def send_command(self, cmd): + opcode = cmd.opcode + self.send(cmd) + while True: + r = self.recv() + if r.type == 0x04 and r.code == 0xe and r.opcode == opcode: + if r.status != 0: + raise BluetoothCommandError("Command %x failed with %x" % (opcode, r.status)) # noqa: E501 + return r + + def recv(self, x=MTU): + return HCI_Hdr(self.ins.recv(x)) + + +class BluetoothMonitorSocket(SuperSocket): + desc = "read/write over a Bluetooth monitor channel" + + def __init__(self): + sa = sockaddr_hci() + sa.sin_family = socket.AF_BLUETOOTH + sa.hci_dev = HCI_DEV_NONE + sa.hci_channel = HCI_CHANNEL_MONITOR + super().__init__( + socket_domain=socket.AF_BLUETOOTH, + socket_type=socket.SOCK_RAW, + socket_protocol=socket.BTPROTO_HCI, + sock_address=sa) + + def recv(self, x=MTU): + return BT_Mon_Hdr(self.ins.recv(x)) + + conf.BTsocket = BluetoothRFCommSocket # Bluetooth From b1fa81116b38c6e89af39c429cb755f538a08a3c Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Fri, 21 Jul 2023 16:14:03 +0200 Subject: [PATCH 038/122] Add conf.nameservers + small dns resolver (#4070) * Fix CacheInstance * Add dns_resolve and conf.nameservers --- scapy/arch/__init__.py | 9 +++- scapy/arch/linux.py | 3 ++ scapy/arch/unix.py | 16 +++++++ scapy/arch/windows/__init__.py | 72 +++++++++++++++++++------------ scapy/config.py | 57 ++++++++++--------------- scapy/interfaces.py | 1 + scapy/layers/dns.py | 77 +++++++++++++++++++++++++++++++++- test/scapy/layers/dns.uts | 14 +++++++ 8 files changed, 187 insertions(+), 62 deletions(-) diff --git a/scapy/arch/__init__.py b/scapy/arch/__init__.py index d4e1d81a9ad..0c1fafac153 100644 --- a/scapy/arch/__init__.py +++ b/scapy/arch/__init__.py @@ -44,6 +44,7 @@ "get_if_raw_hwaddr", "get_working_if", "in6_getifaddr", + "read_nameservers", "read_routes", "read_routes6", "SIOCGIFHWADDR", @@ -119,6 +120,7 @@ def get_if_raw_addr6(iff): # def get_if_raw_addr(iff) # def get_if_raw_hwaddr(iff) # def in6_getifaddr() +# def read_nameservers() # def read_routes() # def read_routes6() # def set_promisc(s,iff,val=1) @@ -126,7 +128,12 @@ def get_if_raw_addr6(iff): if LINUX: from scapy.arch.linux import * # noqa F403 elif BSD: - from scapy.arch.unix import read_routes, read_routes6, in6_getifaddr # noqa: E501 + from scapy.arch.unix import ( # noqa F403 + read_nameservers, + read_routes, + read_routes6, + in6_getifaddr, + ) from scapy.arch.bpf.core import * # noqa F403 if not conf.use_pcap: # Native diff --git a/scapy/arch/linux.py b/scapy/arch/linux.py index 793b8eed68b..d4de5d6995e 100644 --- a/scapy/arch/linux.py +++ b/scapy/arch/linux.py @@ -47,6 +47,9 @@ from scapy.pton_ntop import inet_ntop from scapy.supersocket import SuperSocket +# re-export +from scapy.arch.unix import read_nameservers # noqa: F401 + # Typing imports from typing import ( Any, diff --git a/scapy/arch/unix.py b/scapy/arch/unix.py index 2a1459e0612..ae90257eeb1 100644 --- a/scapy/arch/unix.py +++ b/scapy/arch/unix.py @@ -8,6 +8,7 @@ """ import os +import re import socket import struct from fcntl import ioctl @@ -409,3 +410,18 @@ def read_routes6(): fd_netstat.close() return routes + + +####### +# DNS # +####### + +def read_nameservers() -> List[str]: + """Return the nameservers configured by the OS + """ + try: + with open('/etc/resolv.conf', 'r') as fd: + return re.findall(r"nameserver\s+([^\s]+)", fd.read()) + except FileNotFoundError: + warning("Could not retrieve the OS's nameserver !") + return [] diff --git a/scapy/arch/windows/__init__.py b/scapy/arch/windows/__init__.py index d7716faea19..e8b59f3a1df 100755 --- a/scapy/arch/windows/__init__.py +++ b/scapy/arch/windows/__init__.py @@ -17,9 +17,13 @@ import winreg -from scapy.arch.windows.structures import _windows_title, \ - GetAdaptersAddresses, GetIpForwardTable, GetIpForwardTable2, \ - get_service_status +from scapy.arch.windows.structures import ( + _windows_title, + GetAdaptersAddresses, + GetIpForwardTable, + GetIpForwardTable2, + get_service_status, +) from scapy.consts import WINDOWS, WINDOWS_XP from scapy.config import conf, ProgPath from scapy.error import ( @@ -263,33 +267,33 @@ def _get_mac(x): data = bytearray(x["physical_address"]) return str2mac(bytes(data)[:size]) + def _resolve_ips(y): + # type: (List[Dict[str, Any]]) -> List[str] + if not isinstance(y, list): + return [] + ips = [] + for ip in y: + addr = ip['address']['address'].contents + if addr.si_family == socket.AF_INET6: + ip_key = "Ipv6" + si_key = "sin6_addr" + else: + ip_key = "Ipv4" + si_key = "sin_addr" + data = getattr(addr, ip_key) + data = getattr(data, si_key) + data = bytes(bytearray(data.byte)) + # Build IP + if data: + ips.append(inet_ntop(addr.si_family, data)) + return ips + def _get_ips(x): # type: (Dict[str, Any]) -> List[str] unicast = x['first_unicast_address'] anycast = x['first_anycast_address'] multicast = x['first_multicast_address'] - def _resolve_ips(y): - # type: (List[Dict[str, Any]]) -> List[str] - if not isinstance(y, list): - return [] - ips = [] - for ip in y: - addr = ip['address']['address'].contents - if addr.si_family == socket.AF_INET6: - ip_key = "Ipv6" - si_key = "sin6_addr" - else: - ip_key = "Ipv4" - si_key = "sin_addr" - data = getattr(addr, ip_key) - data = getattr(data, si_key) - data = bytes(bytearray(data.byte)) - # Build IP - if data: - ips.append(inet_ntop(addr.si_family, data)) - return ips - ips = [] ips.extend(_resolve_ips(unicast)) if extended: @@ -306,7 +310,8 @@ def _resolve_ips(y): "mac": _get_mac(x), "ipv4_metric": 0 if WINDOWS_XP else x["ipv4_metric"], "ipv6_metric": 0 if WINDOWS_XP else x["ipv6_metric"], - "ips": _get_ips(x) + "ips": _get_ips(x), + "nameservers": _resolve_ips(x["first_dns_server_address"]) } for x in GetAdaptersAddresses() ] @@ -329,6 +334,7 @@ def __init__(self, provider, data=None): self.cache_mode = None # type: Optional[bool] self.ipv4_metric = None # type: Optional[int] self.ipv6_metric = None # type: Optional[int] + self.nameservers = [] # type: List[str] self.guid = None # type: Optional[str] self.raw80211 = None # type: Optional[bool] super(NetworkInterface_Win, self).__init__(provider, data) @@ -344,6 +350,7 @@ def update(self, data): self.guid = data['guid'] self.ipv4_metric = data['ipv4_metric'] self.ipv6_metric = data['ipv6_metric'] + self.nameservers = data['nameservers'] try: # Npcap loopback interface @@ -640,7 +647,8 @@ def load(self, NetworkInterface_Win=NetworkInterface_Win): 'ipv4_metric': 0, 'ipv6_metric': 0, 'ips': ips, - 'flags': flags + 'flags': flags, + 'nameservers': [], } # No KeyError will happen here, as we get it from cache results[netw] = NetworkInterface_Win(self, data) @@ -1016,3 +1024,15 @@ def __init__(self, *args, **kargs): "winpcap is not installed. You may use conf.L3socket or" "conf.L3socket6 to access layer 3" ) + + +####### +# DNS # +####### + +def read_nameservers() -> List[str]: + """Return the nameservers configured by the OS (on the default interface) + """ + # Windows has support for different DNS servers on each network interface, + # but to be cross-platform we only return the servers for the default one. + return cast(NetworkInterface_Win, conf.iface).nameservers diff --git a/scapy/config.py b/scapy/config.py index a3a79e87ed4..cb183045d9c 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -344,8 +344,8 @@ def lsc(): print(repr(conf.commands)) -class CacheInstance(Dict[str, Any], object): - __slots__ = ["timeout", "name", "_timetable", "__dict__"] +class CacheInstance(Dict[str, Any]): + __slots__ = ["timeout", "name", "_timetable"] def __init__(self, name="noname", timeout=None): # type: (str, Optional[int]) -> None @@ -355,11 +355,8 @@ def __init__(self, name="noname", timeout=None): def flush(self): # type: () -> None - CacheInstance.__init__( - self, - name=self.name, - timeout=self.timeout - ) + self._timetable.clear() + self.clear() def __getitem__(self, item): # type: (str) -> Any @@ -403,20 +400,23 @@ def update(self, # type: ignore def iteritems(self): # type: () -> Iterator[Tuple[str, Any]] if self.timeout is None: - return self.__dict__.items() # type: ignore + return super(CacheInstance, self).items() # type: ignore t0 = time.time() return ( - (k, v) for (k, v) in self.__dict__.items() + (k, v) + for (k, v) in super(CacheInstance, self).items() if t0 - self._timetable[k] < self.timeout ) def iterkeys(self): # type: () -> Iterator[str] if self.timeout is None: - return self.__dict__.keys() # type: ignore + return super(CacheInstance, self).keys() # type: ignore t0 = time.time() return ( - k for k in self.__dict__ if t0 - self._timetable[k] < self.timeout + k + for k in super(CacheInstance, self).keys() + if t0 - self._timetable[k] < self.timeout ) def __iter__(self): @@ -426,39 +426,25 @@ def __iter__(self): def itervalues(self): # type: () -> Iterator[Tuple[str, Any]] if self.timeout is None: - return self.__dict__.values() # type: ignore + return super(CacheInstance, self).values() # type: ignore t0 = time.time() return ( - v for (k, v) in self.__dict__.items() + v + for (k, v) in super(CacheInstance, self).items() if t0 - self._timetable[k] < self.timeout ) def items(self): # type: () -> Any - if self.timeout is None: - return super(CacheInstance, self).items() - t0 = time.time() - return [ - (k, v) for (k, v) in self.__dict__.items() - if t0 - self._timetable[k] < self.timeout - ] + return list(self.iteritems()) def keys(self): # type: () -> Any - if self.timeout is None: - return super(CacheInstance, self).keys() - t0 = time.time() - return [k for k in self.__dict__ if t0 - self._timetable[k] < self.timeout] + return list(self.iterkeys()) def values(self): # type: () -> Any - if self.timeout is None: - return list(self.values()) - t0 = time.time() - return [ - v for (k, v) in self.__dict__.items() - if t0 - self._timetable[k] < self.timeout - ] + return list(self.itervalues()) def __len__(self): # type: () -> int @@ -474,9 +460,9 @@ def __repr__(self): # type: () -> str s = [] if self: - mk = max(len(k) for k in self.__dict__) + mk = max(len(k) for k in self) fmt = "%%-%is %%s" % (mk + 1) - for item in self.__dict__.items(): + for item in self.items(): s.append(fmt % item) return "\n".join(s) @@ -799,8 +785,10 @@ class Conf(ConfClass): ifaces = None # type: 'scapy.interfaces.NetworkInterfaceDict' #: holds the cache of interfaces loaded from Libpcap cache_pcapiflist = {} # type: Dict[str, Tuple[str, List[str], Any, str]] - neighbor = None # type: 'scapy.layers.l2.Neighbor' # `neighbor` will be filed by scapy.layers.l2 + neighbor = None # type: 'scapy.layers.l2.Neighbor' + #: holds the name servers IP/hosts used for custom DNS resolution + nameservers = None # type: str #: holds the Scapy IPv4 routing table and provides methods to #: manipulate it route = None # type: 'scapy.route.Route' @@ -842,6 +830,7 @@ class Conf(ConfClass): stats_classic_protocols = [] # type: List[Type[Packet]] stats_dot11_protocols = [] # type: List[Type[Packet]] temp_files = [] # type: List[str] + #: netcache holds time-based caches for net operations netcache = NetCache() geoip_city = None # can, tls, http and a few others are not loaded by default diff --git a/scapy/interfaces.py b/scapy/interfaces.py index 879cdb95a5e..3bceeac0978 100644 --- a/scapy/interfaces.py +++ b/scapy/interfaces.py @@ -287,6 +287,7 @@ def _add_fake_iface(self, ifname): 'guid': "{%s}" % uuid.uuid1(), 'ipv4_metric': 0, 'ipv6_metric': 0, + 'nameservers': [], } if WINDOWS: from scapy.arch.windows import NetworkInterface_Win, \ diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index db3370eb90d..1554a955500 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -14,7 +14,11 @@ import time import warnings -from scapy.arch import get_if_addr, get_if_addr6 +from scapy.arch import ( + get_if_addr, + get_if_addr6, + read_nameservers, +) from scapy.ansmachine import AnsweringMachine from scapy.base_classes import Net from scapy.config import conf @@ -26,7 +30,9 @@ PacketListField, ShortEnumField, ShortField, StrField, \ StrLenField, MultipleTypeField, UTCTimeField, I from scapy.sendrecv import sr1 +from scapy.supersocket import StreamSocket from scapy.pton_ntop import inet_ntop, inet_pton +from scapy.volatile import RandShort from scapy.layers.inet import IP, DestIPField, IPField, UDP, TCP from scapy.layers.inet6 import IPv6, DestIP6Field, IP6Field @@ -284,6 +290,7 @@ class DNSStrField(StrLenField): def h2i(self, pkt, x): if not x: return b"." + x = bytes_encode(x) if x[-1:] != b"." and not _is_ptr(x): return x + b"." return x @@ -1046,6 +1053,74 @@ def pre_dissect(self, s): bind_layers(TCP, DNS, dport=53) bind_layers(TCP, DNS, sport=53) +# Nameserver config +conf.nameservers = read_nameservers() +_dns_cache = conf.netcache.new_cache("dns_cache", 300) + + +@conf.commands.register +def dns_resolve(qname, qtype="A", verbose=1, **kwargs): + """ + Perform a simple DNS resolution using conf.nameservers with caching + """ + answer = _dns_cache.get("_".join([qname, qtype])) + if answer: + return answer + + kwargs.setdefault("timeout", 5) + kwargs.setdefault("verbose", 0) + for nameserver in conf.nameservers: + # Try all nameservers + try: + # Spawn a UDP socket, connect to the nameserver on port 53 + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(kwargs["timeout"]) + sock.connect((nameserver, 53)) + # Connected. Wrap it with DNS + sock = StreamSocket(sock, DNS) + # I/O + res = sock.sr1( + DNS(qd=[DNSQR(qname=qname, qtype=qtype)], id=RandShort()), + **kwargs, + ) + except IOError as ex: + if verbose: + log_runtime.warning(str(ex)) + continue + finally: + sock.close() + if res: + # We have a response ! Check for failure + if res[DNS].rcode == 2: # server failure + res = None + if verbose: + log_runtime.info( + "DNS: %s answered with failure for %s" % ( + nameserver, + qname, + ) + ) + else: + break + if res is not None: + # Calc expected qname and qtype + eqname = DNSQR.qname.h2i(None, qname) + eqtype = DNSQR.qtype.any2i_one(None, qtype) + try: + # Find answer + answer = next( + x.rdata + for x in res.an + if x.type == eqtype and x.rrname == eqname + ) + # Cache it + _dns_cache["_".join([qname, qtype])] = answer + return answer + except StopIteration: + # No answer + pass + return None + @conf.commands.register def dyndns_add(nameserver, name, rdata, type="A", ttl=10): diff --git a/test/scapy/layers/dns.uts b/test/scapy/layers/dns.uts index 27bc8985bbe..285f7404a19 100644 --- a/test/scapy/layers/dns.uts +++ b/test/scapy/layers/dns.uts @@ -17,6 +17,20 @@ def _test(): dns_ans = retry_test(_test) += DNS request using dns_resolve +~ netaccess DNS +* this is not using a raw socket so should also work without root + +val = dns_resolve(qname="google.com", qtype="A") +assert val +assert inet_pton(socket.AF_INET, val) +assert val == conf.netcache.dns_cache["google.com_A"] + +val = dns_resolve(qname="google.com", qtype="AAAA") +assert val +assert inet_pton(socket.AF_INET6, val) +assert val == conf.netcache.dns_cache["google.com_AAAA"] + = DNS labels ~ DNS query = DNSQR(qname=b"www.secdev.org") From 270fe977a715617ef4ef487efc54df3169b1cf9a Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Fri, 21 Jul 2023 15:05:57 +0000 Subject: [PATCH 039/122] Improve dns_resolve --- scapy/layers/dns.py | 52 +++++++++++++++++++++++++-------------- test/scapy/layers/dns.uts | 8 +++--- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 1554a955500..893e58ac8ca 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -9,6 +9,7 @@ import abc import operator +import itertools import socket import struct import time @@ -1059,15 +1060,27 @@ def pre_dissect(self, s): @conf.commands.register -def dns_resolve(qname, qtype="A", verbose=1, **kwargs): +def dns_resolve(qname, qtype="A", raw=False, verbose=1, **kwargs): """ Perform a simple DNS resolution using conf.nameservers with caching + + :param qname: the name to query + :param qtype: the type to query (default A) + :param raw: return the whole DNS packet (default False) """ - answer = _dns_cache.get("_".join([qname, qtype])) + # Unify types + qtype = DNSQR.qtype.any2i_one(None, qtype) + qname = DNSQR.qname.any2i(None, qname) + # Check cache + cache_ident = b";".join( + [qname, struct.pack("!B", qtype)] + + ([b"raw"] if raw else []) + ) + answer = _dns_cache.get(cache_ident) if answer: return answer - kwargs.setdefault("timeout", 5) + kwargs.setdefault("timeout", 3) kwargs.setdefault("verbose", 0) for nameserver in conf.nameservers: # Try all nameservers @@ -1103,22 +1116,23 @@ def dns_resolve(qname, qtype="A", verbose=1, **kwargs): else: break if res is not None: - # Calc expected qname and qtype - eqname = DNSQR.qname.h2i(None, qname) - eqtype = DNSQR.qtype.any2i_one(None, qtype) - try: - # Find answer - answer = next( - x.rdata - for x in res.an - if x.type == eqtype and x.rrname == eqname - ) - # Cache it - _dns_cache["_".join([qname, qtype])] = answer - return answer - except StopIteration: - # No answer - pass + if raw: + # Raw + answer = res + else: + try: + # Find answer + answer = next( + x + for x in itertools.chain(res.an, res.ns, res.ar) + if x.type == qtype + ) + except StopIteration: + # No answer + return None + # Cache it + _dns_cache[cache_ident] = answer + return answer return None diff --git a/test/scapy/layers/dns.uts b/test/scapy/layers/dns.uts index 285f7404a19..a6644cd149e 100644 --- a/test/scapy/layers/dns.uts +++ b/test/scapy/layers/dns.uts @@ -23,13 +23,13 @@ dns_ans = retry_test(_test) val = dns_resolve(qname="google.com", qtype="A") assert val -assert inet_pton(socket.AF_INET, val) -assert val == conf.netcache.dns_cache["google.com_A"] +assert inet_pton(socket.AF_INET, val.rdata) +assert val == conf.netcache.dns_cache[b'google.com.;\x01'] val = dns_resolve(qname="google.com", qtype="AAAA") assert val -assert inet_pton(socket.AF_INET6, val) -assert val == conf.netcache.dns_cache["google.com_AAAA"] +assert inet_pton(socket.AF_INET6, val.rdata) +assert val == conf.netcache.dns_cache[b'google.com.;\x1c'] = DNS labels ~ DNS From 5ac3c2886cda533d8602a6f7121a8e35dc197011 Mon Sep 17 00:00:00 2001 From: Christian Sahlmann Date: Thu, 13 Jul 2023 14:35:24 +0000 Subject: [PATCH 040/122] add support for exceptions in SNMP varbind responses [RFC 3416](https://datatracker.ietf.org/doc/html/rfc3416#section-3) ``` VarBind ::= SEQUENCE { name ObjectName, CHOICE { value ObjectSyntax, unSpecified NULL, -- in retrieval requests -- exceptions in responses noSuchObject [0] IMPLICIT NULL, noSuchInstance [1] IMPLICIT NULL, endOfMibView [2] IMPLICIT NULL } } ``` Fixes #3900 --- scapy/layers/snmp.py | 16 ++++++++++++---- test/scapy/layers/snmp.uts | 9 ++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/scapy/layers/snmp.py b/scapy/layers/snmp.py index b3a80da8cfe..d6a8bf69e56 100644 --- a/scapy/layers/snmp.py +++ b/scapy/layers/snmp.py @@ -11,7 +11,7 @@ from scapy.asn1packet import ASN1_Packet from scapy.asn1fields import ASN1F_INTEGER, ASN1F_IPADDRESS, ASN1F_OID, \ ASN1F_SEQUENCE, ASN1F_SEQUENCE_OF, ASN1F_STRING, ASN1F_TIME_TICKS, \ - ASN1F_enum_INTEGER, ASN1F_field, ASN1F_CHOICE + ASN1F_enum_INTEGER, ASN1F_field, ASN1F_CHOICE, ASN1F_optional, ASN1F_NULL from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Codecs, ASN1_NULL, \ ASN1_SEQUENCE from scapy.asn1.ber import BERcodec_SEQUENCE @@ -177,9 +177,17 @@ class ASN1F_SNMP_PDU_TRAPv2(ASN1F_SEQUENCE): class SNMPvarbind(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE(ASN1F_OID("oid", "1.3"), - ASN1F_field("value", ASN1_NULL(0)) - ) + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("oid", "1.3"), + ASN1F_optional( + ASN1F_field("value", ASN1_NULL(0)) + ), + + # exceptions in responses + ASN1F_optional(ASN1F_NULL("noSuchObject", None, implicit_tag=0x80)), + ASN1F_optional(ASN1F_NULL("noSuchInstance", None, implicit_tag=0x81)), + ASN1F_optional(ASN1F_NULL("endOfMibView", None, implicit_tag=0x82)), + ) class SNMPget(ASN1_Packet): diff --git a/test/scapy/layers/snmp.uts b/test/scapy/layers/snmp.uts index 987dcecd1d3..b281a4dd5b3 100644 --- a/test/scapy/layers/snmp.uts +++ b/test/scapy/layers/snmp.uts @@ -44,10 +44,17 @@ x = SNMPvarbind(oid=ASN1_OID("1.3.6.1.2.1.1.4.0"), value=RandBin()) x = SNMPvarbind(raw(x)) assert isinstance(x.value, ASN1_STRING) += SNMPvarbind noSuchInstance dissection +~ SNMP ASN1 +x = SNMPvarbind(b'0\x10\x06\x0c+\x06\x01\x02\x01/\x01\x01\x01\x01\n\x01\x81\x00') +assert not x.noSuchObject +assert x.noSuchInstance +assert not x.endOfMibView + = Failing SNMPvarbind dissection ~ SNMP ASN1 try: - SNMP('0a\x02\x01\x00\x04\x06public\xa3T\x02\x02D\xd0\x02\x01\x00\x02\x01\x000H0F\x06\x08+\x06\x01\x02\x01\x01\x05\x00\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D') + SNMP(b'0a\x02\x01\x00\x04\x06public\xa3T\x02\x02D\xd0\x02\x01\x00\x02\x01\x000H0F\x06\x08+\x06\x01\x02\x01\x01\x05\x00\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D') assert False except BER_Decoding_Error: pass From 204605b16590615159a7a563ea4d0820a592cd6d Mon Sep 17 00:00:00 2001 From: Lex <55185179+claire-lex@users.noreply.github.com> Date: Wed, 26 Jul 2023 09:25:36 +0200 Subject: [PATCH 041/122] Add layer HICP (#4075) --- scapy/contrib/hicp.py | 278 ++++++++++++++++++++++++++++++++++++++++++ test/contrib/hicp.uts | 113 +++++++++++++++++ 2 files changed, 391 insertions(+) create mode 100644 scapy/contrib/hicp.py create mode 100644 test/contrib/hicp.uts diff --git a/scapy/contrib/hicp.py b/scapy/contrib/hicp.py new file mode 100644 index 00000000000..61e7448ec96 --- /dev/null +++ b/scapy/contrib/hicp.py @@ -0,0 +1,278 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2023 - Claire VACHEROT + +"""HICP + +Support for HICP (Host IP Control Protocol). + +This protocol is used by HMS Anybus software for device discovery and +configuration. + +Note : As the specification is not public, this layer was built based on the +Wireshark dissector and HMS's HICP DLL. It was tested with a Anybus X-gateway +device. Therefore, this implementation may differ from what is written in the +standard. +""" + +# scapy.contrib.name = HICP +# scapy.contrib.description = HMS Anybus Host IP Control Protocol +# scapy.contrib.status = loads + +from re import match + +from scapy.packet import Packet, bind_layers, bind_bottom_up +from scapy.fields import StrField, MACField, IPField, ByteField, RawVal +from scapy.layers.inet import UDP + +# HICP command codes +CMD_MODULESCAN = b"Module scan" +CMD_MSRESPONSE = b"Module scan response" +CMD_CONFIGURE = b"Configure" +CMD_RECONFIGURED = b"Reconfigured" +CMD_INVALIDCONF = b"Invalid Configuration" +CMD_INVALIDPWD = b"Invalid Password" +CMD_WINK = b"Wink" +# These commands are implemented in the DLL but never seen in use +CMD_START = b"Start" +CMD_STOP = b"Stop" + +# Most of the fields have the format "KEY = value" for each field +KEYS = { + "protocol_version": "Protocol version", + "fieldbus_type": "FB type", + "module_version": "Module version", + "mac_address": "MAC", + "new_password": "New password", + "password": "PSWD", + "ip_address": "IP", + "subnet_mask": "SN", + "gateway_address": "GW", + "dhcp": "DHCP", + "hostname": "HN", + "dns1": "DNS1", + "dns2": "DNS2" +} + +# HICP MAC format is xx-xx-xx-xx-xx-xx (not with :) as str. +FROM_MACFIELD = lambda x: x.replace(":", "-") +TO_MACFIELD = lambda x: x.replace("-", ":") + +# Note on building and dissecting: Since the protocol is primarily text-based +# but also highly inconsistent in terms of message format, most of the +# dissection and building process must be reworked for each message type. + + +class HICPConfigure(Packet): + name = "Configure request" + fields_desc = [ + MACField("target", "ff:ff:ff:ff:ff:ff"), + StrField("password", ""), + StrField("new_password", ""), + IPField("ip_address", "255.255.255.255"), + IPField("subnet_mask", "255.255.255.0"), + IPField("gateway_address", "0.0.0.0"), + StrField("dhcp", "OFF"), # ON or OFF + StrField("hostname", ""), + IPField("dns1", "0.0.0.0"), + IPField("dns2", "0.0.0.0"), + ByteField("padding", 0) + ] + + def post_build(self, p, pay): + p = ["{0}: {1};".format(CMD_CONFIGURE.decode('utf-8'), + FROM_MACFIELD(self.target))] + for field in self.fields_desc[1:]: + if field.name in KEYS: + value = getattr(self, field.name) + if isinstance(value, bytes): + value = value.decode('utf-8') + if field.name in ["password", "new_password"] and not value: + continue + key = KEYS[field.name] + # The key for password is not the same as usual... + if field.name == "password": + key = "Password" + p.append("{0} = {1};".format(key, value)) + return "".join(p).encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + res = match(".*: ([^;]+);", s.decode('utf-8')) + if res: + self.target = TO_MACFIELD(res.group(1)) + s = s[len(self.target) + 3:] + for arg in s.split(b";"): + kv = [x.strip().replace(b"\x00", b"") for x in arg.split(b"=")] + if len(kv) != 2 or not kv[1]: + continue + kv[0] = kv[0].decode('utf-8') + if kv[0] in KEYS.values(): + field = [x for x, y in KEYS.items() if y == kv[0]][0] + setattr(self, field, kv[1]) + + +class HICPReconfigured(Packet): + name = "Reconfigured" + fields_desc = [ + MACField("source", "ff:ff:ff:ff:ff:ff") + ] + + def post_build(self, p, pay): + p = "{0}: {1}".format(CMD_RECONFIGURED.decode('utf-8'), + FROM_MACFIELD(self.source)) + return p.encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + res = match(r".*: ([a-fA-F0-9\-\:]+)", s.decode('utf-8')) + if res: + self.source = TO_MACFIELD(res.group(1)) + return None + + +class HICPInvalidConfiguration(Packet): + name = "Invalid configuration" + fields_desc = [ + MACField("source", "ff:ff:ff:ff:ff:ff") + ] + + def post_build(self, p, pay): + p = "{0}: {1}".format(CMD_INVALIDCONF.decode('utf-8'), + FROM_MACFIELD(self.source)) + return p.encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + res = match(r".*: ([a-fA-F0-9\-\:]+)", s.decode('utf-8')) + if res: + self.source = TO_MACFIELD(res.group(1)) + return None + + +class HICPInvalidPassword(Packet): + name = "Invalid password" + fields_desc = [ + MACField("source", "ff:ff:ff:ff:ff:ff") + ] + + def post_build(self, p, pay): + p = "{0}: {1}".format(CMD_INVALIDPWD.decode('utf-8'), + FROM_MACFIELD(self.source)) + return p.encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + res = match(r".*: ([a-fA-F0-9\-\:]+)", s.decode('utf-8')) + if res: + self.source = TO_MACFIELD(res.group(1)) + return None + + +class HICPWink(Packet): + name = "Wink" + fields_desc = [ + MACField("target", "ff:ff:ff:ff:ff:ff"), + ByteField("padding", 0) + ] + + def post_build(self, p, pay): + p = "To: {0};{1};".format(FROM_MACFIELD(self.target), + CMD_WINK.decode('utf-8').upper()) + return p.encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + res = match("^To: ([^;]+);", s.decode('utf-8')) + if res: + self.target = TO_MACFIELD(res.group(1)) + + +class HICPModuleScanResponse(Packet): + name = "Module scan response" + fields_desc = [ + StrField("protocol_version", "1.00"), + StrField("fieldbus_type", ""), + StrField("module_version", ""), + MACField("mac_address", "ff:ff:ff:ff:ff:ff"), + IPField("ip_address", "255.255.255.255"), + IPField("subnet_mask", "255.255.255.0"), + IPField("gateway_address", "0.0.0.0"), + StrField("dhcp", "OFF"), # ON or OFF + StrField("password", "OFF"), # ON or OFF + StrField("hostname", ""), + IPField("dns1", "0.0.0.0"), + IPField("dns2", "0.0.0.0"), + ByteField("padding", 0) + ] + + def post_build(self, p, pay): + p = [] + for field in self.fields_desc: + if field.name in KEYS: + value = getattr(self, field.name) + if isinstance(value, bytes): + value = value.decode('utf-8') + p.append("{0} = {1};".format(KEYS[field.name], value)) + return "".join(p).encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + for arg in s.split(b";"): + kv = [x.strip().replace(b"\x00", b"") for x in arg.split(b"=")] + if len(kv) != 2 or not kv[1]: + continue + kv[0] = kv[0].decode('utf-8') + if kv[0] in KEYS.values(): + field = [x for x, y in KEYS.items() if y == kv[0]][0] + if field == "mac_address": + kv[1] = TO_MACFIELD(kv[1].decode('utf-8')) + setattr(self, field, kv[1]) + + +class HICPModuleScan(Packet): + name = "Module scan request" + fields_desc = [ + StrField("hicp_command", CMD_MODULESCAN), + ByteField("padding", 0) + ] + + def do_dissect(self, s): + if len(s) > len(CMD_MODULESCAN): + self.hicp_command = s[:len(CMD_MODULESCAN)] + self.padding = s[len(CMD_MODULESCAN):] + else: + self.padding = RawVal(s) + + def post_build(self, p, pay): + return p.upper() + pay + + +class HICP(Packet): + name = "HICP" + fields_desc = [ + StrField("hicp_command", "") + ] + + def do_dissect(self, s): + for cmd in [CMD_MODULESCAN, CMD_CONFIGURE, CMD_RECONFIGURED, + CMD_INVALIDCONF, CMD_INVALIDPWD]: + if s[:len(cmd)] == cmd: + self.hicp_command = cmd + return s[len(cmd):] + if s[:len("To:")] == b"To:": + self.hicp_command = CMD_WINK + else: + self.hicp_command = CMD_MSRESPONSE + return s + + def post_build(self, p, pay): + p = p[len(self.hicp_command):] + return p + pay + + +bind_bottom_up(UDP, HICP, dport=3250) +bind_bottom_up(UDP, HICP, sport=3250) +bind_layers(UDP, HICP, sport=3250, dport=3250) +bind_layers(HICP, HICPModuleScan, hicp_command=CMD_MODULESCAN) +bind_layers(HICP, HICPModuleScanResponse, hicp_command=CMD_MSRESPONSE) +bind_layers(HICP, HICPWink, hicp_command=CMD_WINK) +bind_layers(HICP, HICPConfigure, hicp_command=CMD_CONFIGURE) +bind_layers(HICP, HICPReconfigured, hicp_command=CMD_RECONFIGURED) +bind_layers(HICP, HICPInvalidConfiguration, hicp_command=CMD_INVALIDCONF) +bind_layers(HICP, HICPInvalidPassword, hicp_command=CMD_INVALIDPWD) diff --git a/test/contrib/hicp.uts b/test/contrib/hicp.uts new file mode 100644 index 00000000000..12d0e4832b4 --- /dev/null +++ b/test/contrib/hicp.uts @@ -0,0 +1,113 @@ +% HICP test campaign + +# +# execute test: +# > test/run_tests -t test/contrib/hicp.uts +# + ++ Syntax check += Import the HICP layer +from scapy.contrib.hicp import * + ++ HICP Module scan request += Build and dissect module scan +pkt = HICPModuleScan() +assert(pkt.hicp_command == b"Module scan") +assert(raw(pkt) == b"MODULE SCAN\x00") +pkt = HICP(b"Module scan\x00") +assert(pkt.hicp_command == b"Module scan") + ++ HICP Module scan response += Build and dissect device description +pkt=HICPModuleScanResponse(fieldbus_type="kwack") +assert(pkt.protocol_version == b"1.00") +assert(pkt.fieldbus_type == b"kwack") +assert(pkt.mac_address == "ff:ff:ff:ff:ff:ff") +pkt=HICP( +b"\x50\x72\x6f\x74\x6f\x63\x6f\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e" \ +b"\x20\x3d\x20\x31\x2e\x30\x30\x3b\x46\x42\x20\x74\x79\x70\x65\x20" \ +b"\x3d\x20\x3b\x4d\x6f\x64\x75\x6c\x65\x20\x76\x65\x72\x73\x69\x6f" \ +b"\x6e\x20\x3d\x20\x3b\x4d\x41\x43\x20\x3d\x20\x65\x65\x3a\x65\x65" \ +b"\x3a\x65\x65\x3a\x65\x65\x3a\x65\x65\x3a\x65\x65\x3b\x49\x50\x20" \ +b"\x3d\x20\x32\x35\x35\x2e\x32\x35\x35\x2e\x32\x35\x35\x2e\x32\x35" \ +b"\x35\x3b\x53\x4e\x20\x3d\x20\x32\x35\x35\x2e\x32\x35\x35\x2e\x32" \ +b"\x35\x35\x2e\x30\x3b\x47\x57\x20\x3d\x20\x30\x2e\x30\x2e\x30\x2e" \ +b"\x30\x3b\x44\x48\x43\x50\x20\x3d\x20\x4f\x46\x46\x3b\x48\x4e\x20" \ +b"\x3d\x20\x3b\x44\x4e\x53\x31\x20\x3d\x20\x30\x2e\x30\x2e\x30\x2e" \ +b"\x30\x3b\x44\x4e\x53\x32\x20\x3d\x20\x30\x2e\x30\x2e\x30\x2e\x30" \ +b"\x3b\x00" +) +assert(pkt.hicp_command == b"Module scan response") +assert(pkt.protocol_version == b"1.00") +assert(pkt.mac_address == "ee:ee:ee:ee:ee:ee") +assert(pkt.subnet_mask == "255.255.255.0") +pkt=HICP(b"Protocol version = 2; FB type = TEST;Module version = 1.0.0;MAC = cc:cc:cc:cc:cc:cc;IP = 192.168.1.1;SN = 255.255.255.0;GW = 192.168.1.254;DHCP=ON;HN = bonjour;DNS1 = 1.1.1.1;DNS2 = 2.2.2.2") +assert(pkt.hicp_command == b"Module scan response") +assert(pkt.protocol_version == b"2") +assert(pkt.fieldbus_type == b"TEST") +assert(pkt.module_version == b"1.0.0") +assert(pkt.mac_address == "cc:cc:cc:cc:cc:cc") +assert(pkt.ip_address == "192.168.1.1") +assert(pkt.subnet_mask == "255.255.255.0") +assert(pkt.gateway_address == "192.168.1.254") +assert(pkt.dhcp == b"ON") +assert(pkt.hostname == b"bonjour") +assert(pkt.dns1 == "1.1.1.1") +assert(pkt.dns2 == "2.2.2.2") + ++ HICP Wink request += Build and dissect Winks +pkt = HICPWink(target="dd:dd:dd:dd:dd:dd") +assert(pkt.target == "dd:dd:dd:dd:dd:dd") +pkt = HICP(b"To: bb:bb:bb:bb:bb:bb;WINK;\x00") +assert(pkt.target == "bb:bb:bb:bb:bb:bb") + ++ HICP Configure request += Build and dissect new network settings +pkt = HICPConfigure(target="aa:aa:aa:aa:aa:aa", hostname="llama") +assert(pkt.target == "aa:aa:aa:aa:aa:aa") +assert(pkt.ip_address == "255.255.255.255") +assert(pkt.hostname == b"llama") +assert(raw(pkt) == b"Configure: aa-aa-aa-aa-aa-aa;IP = 255.255.255.255;SN = 255.255.255.0;GW = 0.0.0.0;DHCP = OFF;HN = llama;DNS1 = 0.0.0.0;DNS2 = 0.0.0.0;\x00") +pkt = HICP(b"Configure: aa-aa-aa-aa-aa-aa;IP = 255.255.255.255;SN = 255.255.255.0;GW = 0.0.0.0;DHCP = OFF;HN = llama;DNS1 = 0.0.0.0;DNS2 = 0.0.0.0;\x00") +assert(pkt.hicp_command == b"Configure") +assert(pkt.target == "aa:aa:aa:aa:aa:aa") +assert(pkt.ip_address == "255.255.255.255") +assert(pkt.hostname == b"llama") + ++ HICP Configure response += Build and dissect successful response to configure request + +pkt = HICPReconfigured(source="11:00:00:00:00:00") +assert(pkt.source == "11:00:00:00:00:00") +assert(raw(pkt) == b"Reconfigured: 11-00-00-00-00-00\x00") +pkt = HICP(b"\x52\x65\x63\x6f\x6e\x66\x69\x67\x75\x72\x65\x64\x3a\x20\x31\x31" \ +b"\x2d\x30\x30\x2d\x30\x30\x2d\x30\x30\x2d\x30\x30\x2d\x30\x30\x00") +assert(pkt.hicp_command == b"Reconfigured") +assert(pkt.source == "11:00:00:00:00:00") + ++ HICP Configure error += Build and dissect error response to configure request + +pkt = HICPInvalidConfiguration(source="00:11:00:00:00:00") +assert(pkt.source == "00:11:00:00:00:00") +assert(raw(pkt) == b"Invalid Configuration: 00-11-00-00-00-00\x00") +pkt = HICP( +b"\x49\x6e\x76\x61\x6c\x69\x64\x20\x43\x6f\x6e\x66\x69\x67\x75\x72" \ +b"\x61\x74\x69\x6f\x6e\x3a\x20\x30\x30\x2d\x31\x31\x2d\x30\x30\x2d" \ +b"\x30\x30\x2d\x30\x30\x2d\x30\x30\x00" +) +assert(pkt.hicp_command == b"Invalid Configuration") +assert(pkt.source == "00:11:00:00:00:00") + ++ HICP Configure invalid password += Build and dissect invalid password response to configure request + +pkt = HICPInvalidPassword(source="00:00:11:00:00:00") +assert(pkt.source == "00:00:11:00:00:00") +assert(raw(pkt) == b"Invalid Password: 00-00-11-00-00-00\x00") +pkt = HICP(b"\x49\x6e\x76\x61\x6c\x69" \ +b"\x64\x20\x50\x61\x73\x73\x77\x6f\x72\x64\x3a\x20\x30\x30\x2d\x30" \ +b"\x30\x2d\x31\x31\x2d\x30\x30\x2d\x30\x30\x2d\x30\x30\x00") +assert(pkt.hicp_command == b"Invalid Password") +assert(pkt.source == "00:00:11:00:00:00") From 7fced60458435297d2442604014301af32a8f9cd Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Mon, 31 Jul 2023 20:02:06 +0200 Subject: [PATCH 042/122] Scapy terminal improvements (#4073) --- doc/scapy.1 | 8 +- .../animation-scapy-themes-demo.gif | Bin 41319 -> 0 bytes doc/scapy/usage.rst | 19 +- scapy/config.py | 15 +- scapy/main.py | 269 +++++++++++++++--- test/regression.uts | 55 ++++ 6 files changed, 295 insertions(+), 71 deletions(-) delete mode 100755 doc/scapy/graphics/animations/animation-scapy-themes-demo.gif diff --git a/doc/scapy.1 b/doc/scapy.1 index a58643d63c2..6981fdd692a 100644 --- a/doc/scapy.1 +++ b/doc/scapy.1 @@ -51,13 +51,13 @@ increase log verbosity. Can be used many times. use FILE to save/load session values (variables, functions, instances, ...) .TP \fB\-p\fR PRESTART_FILE -use PRESTART_FILE instead of $HOME/.scapy_prestart.py as pre-startup file +use PRESTART_FILE instead of $HOME/.config/scapy/prestart.py as pre-startup file .TP \fB\-P\fR do not run prestart file .TP \fB\-c\fR STARTUP_FILE -use STARTUP_FILE instead of $HOME/.scapy_startup.py as startup file +use STARTUP_FILE instead of $HOME/.config/scapy/startup.py as startup file .TP \fB\-C\fR do not run startup file @@ -82,7 +82,7 @@ lists scapy's main user commands. this object contains the configuration. .SH FILES -\fB$HOME/.scapy_prestart.py\fR +\fB$HOME/.config/scapy/prestart.py\fR This file is run before Scapy core is loaded. Only the \fBconf\fP object is available. This file can be used to manipulate \fBconf.load_layers\fP list to choose which layers will be loaded: @@ -92,7 +92,7 @@ conf.load_layers.remove("bluetooth") conf.load_layers.append("new_layer") .fi -\fB$HOME/.scapy_startup.py\fR +\fB$HOME/.config/scapy/startup.py\fR This file is run after Scapy is loaded. It can be used to configure some of the Scapy behaviors: diff --git a/doc/scapy/graphics/animations/animation-scapy-themes-demo.gif b/doc/scapy/graphics/animations/animation-scapy-themes-demo.gif deleted file mode 100755 index 5a7a8331db88adbd91069dfb7990e39610067b77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41319 zcmZtNRZtyKw;VIRP%&fB6V&+<*lK0M`2#6T$ER z@#-)P767aopo@!#!wB3d2k7d@$m%o00&w; z954V_16l;cq^dv@4jDckzC9r+xjo=OL`D%003pCP2OuZ_kg-IxaI` zDJ2y?EnWZ+2+-TXegg!ENy%uKNNK;pd}A|(d#nG~Bp%>1vkdS}pNx?&4rrue;-X>t z#>gZ_%V3YiMb-eABmi%9KpZ10_c9E?@J&#bkpSR{Iq zzKY1b0p^;#I6OQFtH2wNi2NH6BPF09D*&RA{U!mzc>@|udHDn-WSL~XTMA;o0V}Y2 z--IMpL6V}pGHP$Ys|9x)U`1MqqQLJHb(JY1rR>g0Mny0UC?GU2u$GZ`ff8Y6Bs zWpyD93&2iXSxpJXygpnG;4y<$*3^N>Fe2H3BW2|6wS@HybY)DOsB8-Kz8eEz9V>kY z0Bmb+%4cL^q-ACU1NL(?lS7GvQFAo5wNZ^V|E%j^Z)Iy`VdsDz$7$za>*!!)?GRCC z4#O4?6xh%J@#{qna?+4q6-YjJ+_G|PEYN@efmmYJndC#lJ0KFVKax7bZLJC-i0WRK z;sN`DJfj9!yMpfULhIEy9$*kgftqXM15s1n>qF zbruwKw*4E(sAZs|E3%_AzrE|3owpji4?*h&*&*uLV)V#edzrMZvi^$&4PT z_1b7tjrmZKh!3$`bM4_wu}Yc#SaaR+{BPrtEdEC|jHO2B94RaQ-e12>1Y;>LjPT*yIpqWqx*cqW zk!mA#hEpba-~ksq2pB+!Fm4X-mwwv3D1x=0O$E-M6^9rJ(rbMa zNWx3I)EN@gkMo&QSC8_7alO+)G3wY>2fxCH3#s%YmG24!V?TzK{Hxd8wGHgO5cz4HYk+lf zOI4`OaqG>T^?AjESv`3~_$j$|0W=EtqO&Uv6$KcCl|E^a8pku?$8P;GUOgmTM%UM_ zc{0*nCvqisZSz2Sb9%T!2FX{ZbshvFj<8-;;J2V3C zB8B%yN=fENi=e_*PFO|Otj|Fsu@odSa&>~J+iKCV#OptA_4IlynIYMlOgrhpUts0~=0%anil2;msd-9A zzZ4Vq9D29B^Z#-+QxhbIbdIwu`6t81pZWD>)*7eTv?T#L&(QMV<-G31^B(APCbmBL z)JSnS{D(ZGtM&) z%=Oa<37t6KtIxCW%;b+KnsQdU4{!Iseujo;Ev zUCg;8u@u~=RkE(3gi8F^TDjC0Kg6JvDncVt1HPy?*%bH|f^UZk)=`HauA-=gKA?gF zw6}5RU1&+$q$Xp$Zt_Uql}HkWj~iEhN|}qJlquiLUb zK65Ef&uN2DxI!vaD^NNrP-pbF!Y0I86{d6U>)$#t_vjM7p+{v;9<@r3%;WGYo`t6N z#A5R{yzhtjf3+;w3Ib>8HECoPhqdpjU9rlj6h6qfBB~Tx)LDvbqs}xWCDrEHGKz;a zFXh(Xm9YJVT5v57DGx-rRg|6{TDw0jfdxeJQ2$mqTs|R(zgMnKk*jn{9$AUCN+SP# zWAlX!KW47<7B@Ef)b>w{-eNs_ZG#+xht$ooW9iR7>#K$C6D`U)p>Eh^re%&SeQOtI zv-r2S6i(?Q>(HnBjtVRW5V4W6W$5;wt6ICj^Fw(A&PK6a-(q(OtWC(UbN4i#y)KTd zKDtI@k8!izw|$vH3>DlS7my>#J=z9#%R}E6^&ReCXsa&NDyfC8RWYJ`Ybo+S)0cw( zZQe3AXO@!1grI+{lb`*Du7PG;})ahDVFV9xvOX8bR zW7IRK;dd{?>uHy|N40D1DfF$`qrWAJna*i+&0{4;$-`RjYjcp^ZR43wk5u=C>#>RnLqN_LR+d1){ zR!T(P)_eLmMJjSUYTooyO506`tPNbL7Iw}V6bZ*ZSOIeZ@^{r}$_Jtn9#gO03nkE{ zLgn?Nk#vaKN=(yP$nI^$I@CmDymgQ3iU*)&hVA4Px(K8But`KJ@jEsg;3b)Y!^QB- zj1;<{_}H?@IpHZ?$iGgC)8ypKyD6{HyqiKyCx*bVBM(?#0ne?PR)nu3`vY*PplE6#3KD7V+OePhIrMpuSJ~k%{Ra z)Q?@~2@bE4Q+xelq2bhaKBF=M51rym0$Mk=vQ8yyy2*B(&Cn)d(z~1@;epom@D2mR80|8k3emY1Vu{fT< zZ=Z+cz^)>1+z0O~fzkXS6fUHoZ_1639Rzj}mx@xZA2~iIv+L76&;!% z_0%6Viz}UH7SM?mz4#Elks>kYVUL0nv*!_WkYcwT?$DAGe)te`fg9UXA9L|r>Wnt_ z%U<+DW9%5R-GEE%SB_wwK?h#RU`T%KOG;erL2T-5j3#0zW0(`b;cm6y^x0Dqxg-uh zHRSlt6nHdJ#z~-ebpgy=dD%6{rCcKu;|UfMW(ML8Z=mr*e>Ki1HCt2M8BA5Nn!w3s z!OivVLY{Ko?550GiG#>Lh=<%Dw^6AUR&o%p>N>B)I*8LmlV0i(5@bsS(Q{Q2ICKEfrru!5Ii^Z5Hl^g^*+(D7 zy}Ig^A;ja*h04?=CI7W#-3={m3U%uJOHDV?Nc--omFAXIgOrh>mCCyd(N$Ae ztBd|J0O=mez;d%}KuPN>PM>|WYkf@VppEK+E?99wlA^HE{^DgVtEV;7Ch>>HZqQ{N zJ=*L*GEq~rj`6a)-7>GVvhO^z#jbzGK(b#Jv$v$Op;C$$cscM#27r}md}>nKp%qG+ zUsP|v44z~=k|S18L`|{B2UKayBS{9$98I?z`7i|=NpEba^c08*e%+50s`%Q)T%#C{&Ee^^EUC}50iA}qUO_mvHuRk1X| z&D@>(QMKnljq)*HnO;rWRhK0kCa@t)iMB9EBhSkvpEIb?Ef}WMg3iiH!k%8TT(dAD zF<%W5>V@n^2%Na-|;^EDemndBn*WgW5>@C9MQ!MG|+*?SMP~utaS)g4aXeI1e>ce&v zn2KtV<5g0XR?7L!^YfAqq`{AJu%tNLGxENqsI)B4t4Q!?(Kk=+CMxf-$3#yp-AC1* zwYREtJfXW{UUS14ThfW^---LM5T+W znoF8y3o9djJq?HTnwPP7RGF}rL^V|2rd9Tv2R6`01Qp5fB6+HE{7xdTqC@r&efsml zn=N~7Vkw^5mA(|rV| zbN#R5lJsNOoa1^jgoY%lUt45SN_dAr`dlw!zIk0iIZyL)JO2a@^x&OYig z$#rHS?C56eNOXm0lt5CrG890Njv>`^c;)!;iZRb#L>u9=v_H1=rJe5G8QQf>)HtwZfnXY40Q{}WHs52Sml+cY&xB*#I-DpRjn698Vd3;Eoed$EE?S9$bnD} z5f*j*V6-TJpe6fc9TX0Q_YT2vmTq zzKk@e4lyU?1dK@Zp0tqt&CwtJ`I}zy6WXZ#@o>?k(LHY`wyMGfWiAd~=gNv&sb?;I zOYaJGQy68SHDjKAMkn}Ybds-Sdc}yYq+p%G#23X?$4hlkHdKkHz~kP0f3H0V-VLX@ z5P8&pKT6_cWqf>hxGGNiHdvh=e=3W13iP5Ea#ZBgT+HD*738J!>N9Ba=%f*$OkR;m zeOmF92%T`*8PPPW9g!{rYj|dPeT&g3529&K3T!2^$J#X_cudoz$xvDXN8t zifbxs1q9a3h7Nnlq-F@bSgz8W<(MXUgw1N(LX>Rv^?WPRJ=DLZTfIefA&yjzBDGkb zco?Hqv-#GA9M2st{no#4gVz}lBN{VkLVM>si{(0+PS?uhtzoxV?Om3PF{j7e(hyqF zP!pma99<(3VvtXmw}TjBZ9Ndv;$LPFh?~BcG+m?Q-DcQOr!CJOj~l&$5x1?DWOH+fSg1WLq35=<8S0}s_^nE zC_%ZytIy24!HlOys6XRO<;9z@`elACs?lGA#G8u`C$PyLnjrgNh z!{&`C?UCZ(4A3rQSj$yJf9ErQujEOF)S5bBXi!YQr3!zgY~^wdLV%7hT_0m(jI6~; zLzq8$U)4j{m$hcI<^Dd260`DBTf!DO^s2+*ZSubxynOnkdS;%g`(|45k9~vCUgUkh zahd3$&vd+!DN&Lk#Ij-Ur5IoLqnM4K=nmSo7QGS=Lc|U`nGS!{9z?evhEVQjKo4`; zmjsB9^7W6rR^zK|j$-l;OV^H^b`ERv*Y21Wgk+EFvvwy7NBfVDTC*g7zn9)1R9QpV z>b2|Whia8EPwSI<1&pKGXTyvj<@)2rMCn~0()6EU4zXr9M3@9qg)n6RG41kbkc zrf4wlqLE}suU+&u?Kths@)rEm!W$=X{mb9F#?w*=t8hU&L4L3%g$0FHZ>TL)Gc8sT zEi+E&Z-yNP*fy}fDb3dDgz)>d5MS{qNUT*vt7aL&^If;_-#9Y_h?Cs3jcLA5k+_Xa zk#AS)Vpy@HYVhwp;x#xzB$4W>l&kjJs$07?$X1~zvf4Rq5V=d;)Ygc%Jajd=)gU?Q z=97*Im+Ol;aho{t>Cbf&+f;B1^~K2Bp)?~Mzhij4w8uDgJeLyEU&nmAP&&JiS$p`| zC?N{XIK%qR5_Y$Ka}#5~&+Y$^qd zn!5Y4m7nXobMjz4ri1aogbk&B(q0L9J@9|=&V1QWE%4lv*giYG(mf?o7`w^R5CFY( zWI`8MUW;7Lfe-Se03g@RPG*7my-@<5h+ftsZsTjfCOJ7H?UZ&;6fTOi%fr+*qcwcR zn3JTVp=1)1UVAW}%ARZ*yRBV$cznK8BAGDk0?LNTa0Ev*F(^&-P`OYpQy>Ii?MSs$ ztwO&qP3>5{Qug%_jzIlH)3>G21@v@4)1^fi`x9%)dG1dXa~Ny^+APybi?f_l@F0i1 zWQPDriR*$Jxo&NAnYJ-<;#bk$U?hJ&oA^`xflPjXC?b*ejm30QTRle+_4@LkKj-5rY8QM0$kG< zhd#6hp7ESGGqEWu;%)N54uUYsYDtP%F;=NS*4fFr8>4iPz$ZbYQPO?dX%q^EcX`tS z`*Sn#1z#+8J_3k8gO3`GnV%hHaRsABWf7+Gp*8Zn=e^$)s?rG2_)l}OifQWLKTfBO zfJ=+}q4e}O)wifMCj}%(rWW%2)`7A)qW-}*W9zteH_A#9RCTH&#)IdhY8XI1|oV%e&zo=Y5<8zW2KgHJ%fnat#8I zs2dG~o?jaj7UEt$l!ii{6**W}eIGW1So*n)^*^m$lq0`e{VMdxdOF9<0kRt!3s;^) z-$~p;t5ubzP;X-XB|pbyp6RZhu>IQ&8ZXk_32Q6^YRJd>M41~9k#d-%OIcp@izB zw-W!_eUnIduFB{a=X2}}V0t!>?KqwlTZRAj{!1xFpN&&%RzKg>*t@dwy_I%i<*!@P zA2Xef-xt{I3?}IDxhr93}ls`N%4yrJ*=@siQ2&DKp{I-b{&rRh<8@YX0`h zQih}J{q~kkbU%7Unk;1_@xfRf21>R^he+&T^p0)jGKG_p0Oa6_;X-<=jwtlr&A}0N zwmXRDFLnOVITgi=;+U#t_v|aOjFxC(!d7n%4lDv0L!xT*N+37J-DDUp+;Rl;n~TJg)0lG0TzEJc zXx41b*3PjNmq>kU%mBR+Lg8eCAEtR)PtuauoTRilW>O3PP{H824r}5g*a=5a(L;=- z(X@*k1U{@NtklPSI;`dlOGoBxSY*mT-U-#GFu!3;E@P9XwqF$+XUlF<@N=U|+rG@u z4lZFDMRDP*Vbya~o^Srkt`tP{LC@d{HZ>HU>R0DBIwQlAT!)xTmb6Rp&W>pOC(p`k z8qlFClrC$-RFRHJE?4H>tm9*UG*5rf+F@dH$0jbVFdzFbvA~(Qhgaqzi8?wrM0VVi z!r2mqRh8}Gk0M3^=OC84QWO&Wk56O=eojx8nlgAXtt+ttQJ0n)o4Ip10XyXjwP-jB z+(mZ*YU)wkB&xT~(D}6TMDQomLUtH}wn8;b2TWa3qj_gdtlMd)d-Rx`hbzrRq3A!_ z(UxZH)+8--aaGv}6$U7(DTT}-wN#9!=$pzthGy7dZ9n2&5}P_8JD@tRnzA&+8me5) zcd`|;xfdd++FNI8VBM5SOcWJ4S+WmGUyUQNO2X%@upJvUqRCzKED4)pVidoW&H7b0 zukMQ6D{1+V>hv+RZ`IG23=rw?CwCJHG}Egu)Ioe!TMVH6@ab~FWS`=^RJCTwi zEc_dp^2rfAsYD%usZOvE6c)O1VjM!gwtj#ANu!4d(J@@cZj33zK!Ad)XJvO&Xs24sE?uCcb&p3IPd_WKn4ka2BvicgmwUZ) zmN05_yl-Ou<4W<-0^b3LOdVqut`o{T)!8WNy+WNN^Zf6;mb;V##S4_j>LuzbBJN9J zuq&YPtED+=thyGxCxy~|R8_Sq3?csUo3fzNAD3QBbA(u=%pQBJ4V(2{Y3z;>O08Ov zui;vlhwM%s9@b_v%=3Hp1!y>;FUxo0Zsx&D**)n%8#unH~ez{W&H=Gk@|^9H)WCff=!KJ9SI|+{gH^npQdCJ zIm}~xaLgfS|8Voe02l7(EXNpk-EHO7h5?X(ldoLH4vhqN6MRZ!V#CVr6zk)Nslc^L zU&bEifF})SrG%~3j6JV%5l2<3bFPffz8l*?(TUsC*r@55lhm#L?-At%o%#|bHHcMn#Oa?eeGJwR(DWP&}*6m zx_eb1;d`i%?KS7_cUz3p$yPlDoc0o9RbX5m%aA-TN|@pv00KNoFyIBGbwZrDp)cSM zAD0yhkhhTqc)Z>c*5UjghmfE*j&Gu?_9XPf?vmFqU!t}L;9oYg`%kM_qc$gcfptZ} zTXT}i!YBKS7IWtqs4wa_I&=-M~YOHq+M3ej(^-Hax} z9Em8{vckuakk_(6M=m4|QqeEyJ-DMCd>I1h8QttIVoSz?Nbn+DVjUZ0g4m3K);auv zio!^$g5TJB;AI5`qaZXdVrj)1_Ts$>rx_rV*~64yVEec4P3&J z`NAA##ZQfUs0jsWXZut~`|o8$`Br-EQwF&6d+9RzrM6m!k42oJif{_5LeOI&{Toqq zbfN1JAx#^UCu0coy35_C<0Ow?)u-RlP#lOGU{n<%T^W?cYWLL@i__tXk3zB36)*lR z`UC#YS!5qCsW4iiP_ZH?3F~We6eu`qs1WG%tQ~p}SC@??o$)D01a~+uksHPfF5hH0 zQDyk_K*Xg{tN`3wt|)PD0tP9aLjg8DE=-`q9D3FXC%DedfI) z%^BT_F5!+gqeZ`EZB=DE#M)}G0{-Ps63R{GLwJ$fp~rB-JD<8Vf;)$7`e)fd8`neh zx^gt|i0HG@G*RtwE1mCGhK}-j%npQLqog*YMhk>wEyc#2g2zVk<>q9?Y03qIWd}!Y zK<{0~F7pOiDdkv6U zo18O!mN?4vQ^?>Q3bUyM%kGo`0!W@6#2P(of`Dkr-nU9Pr}~#~l5H%GR4z$!zT!s8 z?PT(B_HV39w_CwqH{Ct}p$|V}k^`y-60`xC@T*7^{Phn(HTLQF=&K$kIdp&AW=)6$ zM2j)}t28(jPP&+jQ0p0Ak#$m2i_?+ruASngLXImy32qgjr;;R&*6&Z<;M z7M(643eD_NXdBw1IP5OOyyGvGZW<$;ud3jKv4Tln9${Tks2Ss{bxktc1)ac*z!S@Cw(Tx=jPd)*=yX!)zQ11HFV@*=SGt$UxzYoyF5gt2i0@TuS{9EY#AtF)ZurP$tPV-XIfos!{T&CflX0yBve7p zkg7CAsp?zYXlYw_8Cx#zzMfHw=2KrX)fduMEAG^b2dRc8_J3bpi1QGoBAj~4;5R4i zc$-;RwOPBY_=F2uU9HoQ@qwh}i+HK2>hKHy#_HeORyzTyUG*7Iel@5>o+@HjJJ(x3 zHt7*T2(iBb>j)vDH7>ukt-rW#kS(rPNb0@m8Z5GdG7FYS^@8kG;dDqtKLZlCu8=w3 zjgN(KHL_q=s&FiU`OoU#fpGoROl?B+bvo0<5B-KLZSC))L-m0WUd7OXI{i!bP4Xm6 zEWhvAW++teA-MfefgqpNouBHSM1!S22h(1~u3ErBZPS%{`V@%UC=kP$aJ@;|?JwfS zU;Xq?G4&=uksR{dT%mDaIih<%j||$P^Xltz?-^HLE(jo+2SBRu;82I zb#@}UML;RG#FBQF^tRs-sVX+^2-j_3I0VU`nFJ2PD7Xb-9Dq*xcEoB;R0VcNGj`Oe z<7x;IW!JVf+qXv&O|dAyhdqFqkRgPIuz?#U%Kb_@m8OcVrbcVV8tQvUbkegYa)u6j z2SIyliNj#t)V4zPuwbRUN&P+ZD@Db<_hKdD= z9|WmefaebOCCx{pGyuESsHgLS1<*Unfbdu$iq&eD6FGb?| z(}?9souTLWUY=rCW@u;noMmp-VV*#An%gv@t`)dXo0ReJ2O`*u1NF0wmF{QpqH(MI zJ*zaoHC|kSviH`7#MW8uW=d{wigRY^n8&;){ndWf4QEG9cgdoALs`&v$-?6{@n}#z zBwi6W)o9?+#h=4`1NG4@I?QyYPjFVqUmH38H_*+9z{Hb zg6BAQ@T13}Cl$0{qKyDJLAmFdt>2iPE6h{QCA`rG=8>R`~5U>Rl z!T;`!_dwvm$%Z}<#&w2(3y#w~Ll6RXL(ewOPV-X$Vlc4We|qi~K9&U;m$%yzusxtg zzM;08Km@N-pB=cNAb{-wQu{zf@Wk24Ns@g&EtsGYfWNT2fZ8nxL_8Os?;(O=2G8y) z?XIbj5gN`B?IEw!;jnQI$mcd;Lg0y{<>$Wu*1rNhb6Xe^hgFUUm|{D$Du=y(yXUO) zIFB#9Ra+`ok5x<`!-FmJfGJs(J&bQi>Mhx$FhkisB>T{*H(-#vH` z8l2A7J6XlvhG$#av0OaLUvFkP`d3Z&x;>cB0~lb}pmo<^4UZ4@9*G_IkYq^2fMasU zZQ2(Pxce5a@oEGjGwEp}AxJ!6URCJge^h&f#u) z-gRu+V<#LNl^8P4% zPN$QDONzlba)|^+%PY#^M0ySNo1$~7(G=F((_xU3Fia2(Hauw~>Ao-m9Ie%cBLGGu zg8%Rm{>88tF%T3>ZEAT-KUc2Z;B*Q-x@B1SZ8VU;L@>QOTFc_@ncL{x75EE4?0l8F zVA9R`1Q$grB1Q+mi{&B`ql2pqI?Zst*_}Lm{dYJ;qf%+pM5$f(&07EL-vp#Ctd`fKSgJeU zgWqj*3q9a@kOrdATcr6}o>t3%M1HDvzJnbDZ3N<3HIju9S^eEG3lTdY0bF6~KIx&+ z3Ms*SRRnj3z>$fIsf0yu2pd3!vBDD##62G<;)GF4NW)Jc3)#Ysk8HE@46vlX#F(^! zKo$5a1Pb62ZeMbY7OeVb<%oCO)TyRvD0=bbM2Gr5a9k)tFvx`ULc+*9CY_uI2M~FV zceJxc@Sk$NY#@L?iqRSblNn5pWX@W^r!ZS3=UHMKR~57d@h`l_ry%jYf2+T0{=v{{W3oYeTI z7HyV&IoIA#dBd-SwWF|Qd0!pUh`R3ka&5WKq>HaR3@_S)LTLoJca>(AMYbKWgFgWQ zI5H0+2^s=Chnxzl);_$q!N9bOwI0vOpQ4kisVAPa`4vn{OonKS5h(8k?hG;CWnZj( z6@5QL2jAM@w>K!?jV><<|Lxy>zP8&%>#D1ZW$(APei{u`AwB-KBFcxuGC|5Vdqpt~ zk=05;!B;UapOR;&05KNi_WAA2XK5h!6;I61b#cAz!IcGI{T%>mS=YWAsE}s@gIB$N zOeE&>1P)FF5Wg7Oz!!Hs+*TQE1SY^DFK`I=w0?KwcivK4cwp+jErXA z*MzSOH%C2ytB7!uXDJ}WP11fwRmnR2Cx)JTHFynPkoW3OtO0>hn{ptS^YezBA0H+8 z1gvCuJQO}ARF;yG0`lQfsyZRb2AztfWJu1hz&qoGlJ*U`ORqHww2MYXuTsJ(l%!1V z-8y_0zCocjedV7;_!(Crq4EP|c~U$4I@3Ub2m<_u4#A3E(;Y(-#C7ACSO_R~sRlof4 zsUdd@eN8*c?k7i75-QR>vD7=ih)gmgRu+1wnVG)Qw^^QO9>v=bM;LIuTHd)V#S3Ex zLyLs?t!{#sK1h()l}wK0tK?}*6#T2gC(ci%E-(E{Z{$k3|7lyjr&(FZ%$1x^cu*@I zSv_nNSte$wi?NS>KGpSa`P$8p9t@8_6s$EoY51`&TuXJc{G{7ov21Xx3oip7Ui6juZk+Tnwz%taZNqoVJ~ z3i=pO7|5AK@EGfBUMqU7j^mb-4r(u@DzGrFu(~nnlUhe;9P$`gHaUWwg;OcaAX^kn zC8*ldua(^gpTn~rF7_Kc#|=Fh6}h z+iDz|`tBg++bn+!Ma?`WbW*y%XV@A3MkdJ|-kiU?^OXf@*otv5QA(BF;iub}0KZd0 z_Gb##nIN^Dl8XW8r=j=3?sD7*RtNbupHGLMnS6&)s{nhqx_t;J`NvgIpfDgLPL=oN6D?V zukqR4$2E-IR*%2O?#=fKxWz?9pO`vJE{^)uzlv+AN9`=tPPX5gP`+Lq<1sd^AF3+|7>q zr7C(GHIS=E7{kwHTJPG9G3aLVY)6qi#*LB89HN^))$1%K|8Ba`<%_`j`lh3 zc-9C+s-znwLgWT2U=w4ssNyFM*)+UoSF_5U0$y~L7ZWnx%~iIl_>rk*9=7(ao!*bq zg=LiH$j(&uD&eK=!0MbA%!7Wbm^WW}gq0o}zY#8swBHT?YCCn#K6pNbHEn)-J>#GA z-uU#!*~lVr#eg8WPSf!?OhR?tBLXm@Z^0o*2@&8=A+uuk!ub5_>{n8lP)4$s?SDxG ztglDB{tE;9!*G}%5bi#ipvVGXxT83@U`8^G9h|qD$8`}#7{Dk1pci`?%f786shWzU zl!b>02g64Q2^#`1hG6)QLc)BV@K!lum_k@Sop6)c!cbX~dpt4NoGvJx7;2>uXw?zgoYxN|0-h* zK9L7wc^OcslEH2nzPDTY%ny&q2VkO-0hFB|BEU%4y<=a4v54VevPgsnH$?crBrX66 zY2bUq0Bn4Cq9rooqwk`%-*GDO+gxBkhB?SDq{0sy!eSfZ$mGJs0C}e&WdsQUM>i#G z=gsLRqR|%A)`-+;lav^LK~)T%o}40y6rZ>kf1K>SG&xlg*(Z+Rd-hEl+Ja-Pcctn|Iy?Ub^GB%(Px65=W1^6*lu!BXmCupHa3tGNUsWN)8I zm70GkRsVW<+?LzZkQeCXO!y8jzd`P2OfF)GEJ7}vvcX%rAu{+&u|G#+i{u@3@H;D0 z6~qOVq);x8KQVh~c#hsr)N;HHgW8#sXY&G2QxH zeQYYiq)kn7L!!0s#CN9O#rH^*_YhgB4BL|px3>NrnQ)Qkl7AG_eWJwa-V&xYHrD<{ zV?|CX{@proOV4QA4AD$dIvCD}8qp=6TA#|YkSwPzlfj6>ikZx;I-e8g+`X3vhC427IRqAi|u@Y12S}#$4Q7j0+6ESBNRU z>s^`)A7dtVLBZ<>mcHB8zk;Dj*jG2|;$zL@N2L|`cpy-jXI8pz?@Apca6liOASpr{ zGLGi0P8-S^9Ast@YDXKUUK|GXqYVx%7Dk8r65K1VmOF|6rzO)r5Jm1}1H+0_>PtM(#vNI}T^)$72s>@|ZRbd~F%iizW%_@yY)`?>`Wqrl zYZ-%2M@L>FiVNl}hspY^B`>z};~*r2N=e&syzRR>lB;}*(_wjAv2cDjs|jA3Z$#p) zmK}FQ(jnb6KML+*NyZT=DP4DTplCqj;?!ewU5Q!_@#^{*76dCG7rPXP_$VT)czee( z&&{gg5+jS}D1QqN@|(7h9KVg!3S?m^{8KXZ3Cjp?aW7w9dl6JJcO=|sRXT^4i4Py2 zkT$V0k~pmjI|M4u42SLweGq}Su7c(!i@;aerEAC@*KjPBMg&jkF$no0#*PvW|rd%bHOs+cHF2?Dksv84Sl6+Bgc@){Z6i%G!-6JMI`jP=-#C{Lc4` zqSGVPxF?`#G*VidPFUp++>Fhz)8vwjUCQNs0+em4HvRG^w2n3dm1Pwnj3Rc7LlGym zkU{yYU#^uWLvvYH8G*8cjMj+>$UfA%Ta1=RCnM_mqtoRA8L*RQ;s_tjreJ?f-7)UI zmXD&IPENy9U63I>?aoS9OgHP>aaBy(iO*sOdtdF%6N@eA+pZCX&F~DS8468DY%i5E zEk4<3^PLt@jxho&j7C@GtL>-Lupe8SZ9deOyHW=WqBC#4uUuo+>9P~s#6H_3C+Lpo z-TG*^-Aqe5Woso?DJaYQr|p1eOCWhG^GSc@ zNwwYaT;=H+^Vy!=aXa(b)!E6B9hpaW5IV~xcGV^EdC*4S6?4^9d*B5*%Z+%|jXcY( zdeyBy%bl71?N;EOTh+ZE%R^|@Lk!DfQq`k6(lv9{b(Z~Aq5ZW0%X5F#%Q(yHT-ECu z3v}e7Z2_=8$bOj-QqSqb#{i`PolR`Q}@=tExe?d*j$G z1VWlDEG^7kp*T9@;S8yo!v6_1J+HGTvW5O%sF^37`EQH(w@kCfYIV4-Y`)5{=O5Ip zFJG+DLCDn`sjpaWaM&Kslxg^HsHw*Ee?iTaS(g73YBKST|AU(Q6FH)h#Bx06d(;0L zYJwGdvgBIoPnR2=eH`L5hN4(trFQ}Qpe!e%ECzCDT#2dqQH;@{1?=0^K>E&!W5_749+~g zA`QVcE8YzKC8`DvXdaoGr#jgQM@VrAJnugFp<(P*-g?N|7n_R7)nR2X%J(%muexP zW|n4Ce^{ts-A=ckq1P|9pXniDX`bbChqsgEhe&@A1%CezYEofG9OOnB678e$l9yt_O9vziI>))aLgPP1V))kF=PsI-ZeHbM#J8n%+G z&(wCZ%FdeQ3P#RaF5d6jwchkETeUsNd)v3au(sHD02t5qo$$w2qFu<@-VWX9K`jnF zmM2jET=c=a)M0kX-W#Vt|@8;6?I8k_NR4;dj3ot zKfcG>y2YDjSJcN^R-L*<+jcNDL^@8`-a}m1D>$Lyp690xp}ucSjllsJc8!4{B$bU| z2+P@np9mjwQ*f1-z~e6JnGyG123oP_ehyst$lkAH{q=*wB6*Ud(uOtSK{Ys*<=6eC6^LC^O-Q8{+j7*w*f~ptCA(nGKhcQ7GU%M#jSLgj2z@7_qgYE&G{pA z8RC$;OvmXWYb8w_;-VAD!1GaWrKp8-J_ltGWRSH{&De49*JlucquS`MBm9u!1k0)} z>{1M5rGdgC%w*Qkc9vdvkjf7xU>39$^#bktu|5;094bxhP#Qvg%S`#4y^~J?9%}M~ zk$M!`%wIO_X#>gvtgy@850{2rv~|!O35tszlZJTEF3~^QYLp~Xck>s?zRFGSCS)y( z{OkJcSv6aayirM1Tr&OBS69u-4h~+ax2tIHIeO8e%3|){vT$DH^=d7Y#1;(!M+?KZ#XP2z$VOy*PQjkWP4%s_ti5`Okfj>Gm?mpONA6)O2JWn^;fHpN*~Z zwCEc)2?n+yFP`FbEK%Bb@t6Ihrz`~S@fbba_@!T za71YSdMXN=RLWO(sv66Akz%U!U3he)R8s~WBJ+kC;F)@TnkR(W@gYD#GZU*4DyM9r z9%2W^x|9M8>-ebWDU9dqo#pEcDphH!~ zqii&5wW3%WcU#pTEi~=%XWC61ZsEaF|E$z;z+V8S>b`LpKH{u20$lj*KSfQ*K%t31 zpDw~iu0%{%Mn{qc*!VcMM4YcsSB)k{Gcc|k4n`JPnJUiC%$!#t2KK&-7BCIfR^`C} zGMwo61Zf0M==uN(DwrWe9muc1QOZKiaIUGz^RWui0gfLzJENa5s)~P`b-qTb1>2IG z*Zy5I{}2Ko>LMBkEJWnnY$*P{p1Eke94K)=*@=nqmo4$9+gAx;B)URhte#i4QMuWU zUPnG^0W|Nc>fVH};Zd}-IqmU+90JyfR%opt{;ID)hCA<^THWSLg0=gX|0!yA+Ioo7 z%r_?@iVykWmKt1NR)Qzk$&eg=gsuT8>r-sI4HA(wi!+!TP7QmQ&EAoT!6}`Wy*h(s zan=26JjCN1dg-N(1^uJs_dmMCnU0V#Uddm8Cpu7o&he)N#VHm6XX<81x$&QZnGBxk zKF^1urt;>ya*F;hNcot7-BEJa$xhMlC*~nHnxq#$dOjdwDe+6Im(&!0G^>g;WR?@8{!*Z`pub@h<0#pi6Ar^9}NFfU8m|#Uizof-HvE9yz|ZXd@ys4h(3dWV0M^j}cgmNZl!0`k z&b~T)NaWAJ?GbeS@BW-}o(*|{hszk?O%G`^zwIt;=S6Z{Pb?wEpw@Rm#^ZjFDd0|* z`~6T*8cuNI2)Z(#w~idBIRM0X2_)jhensMye@UDc@9Wd-k@g$YdI%>F9()hSkZ1QP zq~u^a1X>7!YPCbe(R`KcLqr*|7}A5&o_O7-gUAk@JEp)%PN6FZj1@#^6q&>y9bZ{9 z?A}^4o-=`%N|Zii@Q{1e@J&eAAZ3W-RM?1F@F4`C8-bQe>7UkxLAXUu z?&Pc>7utCl9vX#?#^L^G%X7lV+H;MB{A>+bN!x*WC>kIu1-(Xo0xd_Def-=6FJ=V*a<)BL?~v&ui`{TO=Df*q%E7LZcGQ-26~`MyEap)^1~ru z-mt!eaLk!x`KkEJ1U9pp^yQIsB0L@v>WsOU8NlG@k6kh*qB5vCpL^nj(A8$3T~Wjf zgt1d)#tCH_*#|3Z1+mw%n@vX+UAQ21K@2)z1T=()Gd+zew2d-Mj4$5)DpQ|KTTv3M zVdgdz=ssr-oCpM8bb}WlG0T+MYtVG5d={B};0}(bBs<7ZE<#={{8%phusi6rd3wSm z#56I-uq;O&&6jS|3y(4vu$_&a5P?4pBD{(rV~kY5$+pqUMsmYanl_>Ydp#IAqO4uQ zEo#GggrQ&1F*$?Xbl79QxWt$WM+?EhlGpL!)M-(8tWW@^xNeMpP1-NvY-z6;9g;A` zgwXJfJeR+96u1kmp)2TdPYp zi=f|M7vPa_iJN+fL9m}uV#%UH@vRFRk~)tG;q~JZc%uh*a)8S*XOtKWx^3KhOeWg6g4Fhs)H zBZvyD^%kZi2?e1W^xLPbG9|0TnkuvA0(MUOVamBW>{J<|SAWtAmhASo zNb*?;NLDGR)@-fTvGm@a_O{T6NUiz!-{fk)E!Di#Pz>=ni}#XgNr>cP2~0{E3iKy{ zxK@!6Ck5qyN-9giX900h zt*!EzP3RX&!RTkcEI>ed5QS{Dx!u^Sx{ahDW^w{=sh>6X@ik*NshPji=#rTk2%_nB zTyk7%Dd&pSgwi=cjo*%HFQ3%$TRrR80ajXi*4MR3kF=?j*1hY0CIxD@`qHeh%ceBf zemX-TgWpkTnR49cqP?4{%#=R2Nggj7uUW_ha!t`o>bRdxq2>mrJ#i_d?gU0*eBJTt zNoW+;L2%A?NMwNeCp&GpnN*BBknbcrH7|iFl$i23*tn^jvDx8=@sP$~m`V=6=m(uX!c-><0EevwF=;#myy z(FsintZ7nW8W#T?#pZ$>UXL8ss2moaitOZwS)m<`*BK$-OXesVS>+oV76_o3ie$L+ zC=eKwR31_MJc8U!ehM4qx*LpBXbGwFi;YjMu1?Lb`y*hOHq|$_&@!U1=Ffx(vy;m* z#p%DFOlud3Okz)e-9NsyG%hb4?1a{0Ge3q=)b>+y@~1Mr-Mo*J@)MVa$>qpF?}0Wy z(Yk;J7Pt9PujG!PJEjn8EeEh$7~Nx*-rn#5x=2HnzQN{!R`>;E#972hi={ z7h&V3fb%9H;MYiw91p56x#y|ux3g8Gg9SyAkNe}g23!M!U||8?OOwrC@xs6NLeC+& zBsIB_wPQ1eG)YB~;UUmhy>)QN9J|>4(oLtK#z(R_tLQW}O`~X$HRl~SIF}3gA>dTh{>Nd=p zMr+Ki89GMHdW~yLBWp%TCS&?_+IRjH-wBs^HJ9M)-*Fv18JwV)AW1N61o z^jO$RMHMyaw{_NlCMspivu^XQGaaxMJ&FzAFwF^Ge5sC2g^o>|1q}J`J0W2^a3Vkn z&vrBYZmZZXgY0&RSq#o|w7HA61((h8;!O!Y{8$xW(*lMi(cTZ&%?WyJG{S&JOD5pqBnh7 zr|;?sntM0BEA@Tz2uWV9x*;#USwRH26$6s}*`auG@K@}R_U{q>{ZX9sw&BmYFK);s zyn79T`(4@3dqXyRgyI!*?mKslM~t6N-guk{{?Wqx)NUTQTk3o)D!uvQfSmY~rX^D2 z0J$mt^EA+CL*YO}>0SewdZPL1>|@#q^M@hNx78=>6;I!L3Y>8${X-sqD2}X|XlL73+CdeDFr2$_a#L=g?^9X#cpT@s=FQb{#+Lf%fcF9ZY}Kf}3=eZf%UKa!5-H$#%y zp^HN|SXM>K{4NO3!^V2#C({^AXdqQO>EoVWmGyk1AC30DLG4d8slA};4Asngcd820-B&MaT1$A(hi-Hh+2@wpCnkgcOHuQH+N7K1TTNd0G1bpYOZcZL z=x-A&fc^1!;bUTTk%^lrzThS{ta!nC!7srGW$^%d!|Na0<3*p+Z1fjt_TGI8KsQj? zvcv^;yckCwOi7-TO;=+Bt}>5;CWy#B84bG&-jZ)U(m4>_#d-#pctkA4D~Tzq7#?y; z|35{|7wx?72?bW+?}^1R_@qc>pOs3He~{vnrqEUbOF#MK%O^u+nq4YGW82OrOZR)V zR2HA^luwT7e~Owc@!uR|_7iY#P0@?h@(<$t$*-jHc*;(o;>4&ArJsDS=#!A-E>x3e z%c)RTeBpPb)`m`V@F=MF$d5jU@yJpn2TRd{LnZbY2vC(Cz>W$U?CW}>WZS{7ZTQ<- zL!+VJHMr`yz5MLO`=?+qt#KsYi;qpJDPV2$yqqdM%b-&V{zleJ*^jCt(1I;m5?oiCB|2Uw=ng>)L-3 z6Eu$byNmQ%xDuC*{$Aa zwBM`47Qb!!`J@SS!o_wEI-3+XxOiJ;-kfo&D%} ze)qTvu8yP0_~`3pGga-7zkuf0!`v7h9y~x0;|s~fD*4Sgu^$0PCP^%!{f%K*&B)2#-^VC2KXzX>?h1UB z<<{u5(^W2!7J2o!k(fNG@0?mS3LmRRVoQ9#bANYMFc5)~q+_tqi0cOWkF{b$FR@?L zIy47)K$tP1hb+9&SLBM6?;!rhfgH&Z_`0`ck7Ye`xs~ifJi6cEeFEgZFHRV+{&kYn zJcl6=U87?4%Q0yrJD1->NYQ0tH1(oAPxLzzZff^C(y`S%iEl>%`iJNwaHo#vzdoz# zT%JVdM)g4%EY3!DC31x3Loq_s-k9tt)!HKA5mA7B&uzB>NDu!-Q! z=AsWtVl_Sm=|{7re_<1CZGaI9HnA_GRzb}&{TNcA%MY+=IfR!v{!8fV39}UxY|>c@ z6+VA}O;{Yyeu)s7uSUwT!oEdG^K3o9CXO6`Wfk+aSRE4>i>sE+*4hJXTDLR&2R7y6 znj~0MZLKHSU3Z%#|Clh}NTF?wVnD&Bt&KFiZQRXt-^UhauGPEFDA@F78|oM=#<`Up z^HD)RJI(|Jn?k=lz$Sm{yu8dfi*1u*R1qwC|@}OW7kBv&%q|J-nstu#( z+=!k1x8-mpg(+}oBsWW^I{?36X)e} z{2z|1^}J6U*W1+x>^J*ew(PgZllN?Q=NleusDC(OwtFPLaUy`d89g}26H6&Q?w6MY zrm&p?0SY$#1=i#0@hU&SrjBp*gf54Qq_^ofr9c#H;#FWl!KT<46l}7Sn{~|~2pDUn znjw*WfK6XxP_PLujU>FyAYlTwGf~@1N{eKYVaK$y2oH-ZgEE18!X>m`!S{5iT?TG)Xf49b4?ltW%ty(}uY{uwJFO4h_Hi%va#%KjeKr4}_3 zmMfA&s{vCGC@YIKwR*~JN>!yjavWk`pTqO@YoDP-X?$Y^Gp`Rv$7eFfAU~0HJ}|8R z>C5tj_aN5S9_$0=`|!kxdZst!LT%O#Cw>dJ>v(M(D&*(zWQYixFqiz`Pj`6A{#ZKq zif)hd$Vp@kvYw4~3HAZ^xIB&MmQ8}3eaH)sHv!A=npr4*`svG8w<>VK`RQTyMvp8#E>f??d8}ir7^2dAaD+?lTICW^) zCQ4b03r(KW>W$(dD$9P_^1JO2u5eCLEmsta{opd%$enCxEGlsa>l?c{jkWw5g-=Qq z5}_67l<-xR>bY`%D`B1P`B+pI2c|Z8HCr*@@XI{)Zg=<{*Gxv_`5S;-k%e~O%tY;Z z<$qz*6rM44I<&|}Ti@66HGm&Iu#@_i;@mRz1)}@kRnT?e3zVf={3B0sep~ONc50yb zSA0m)=u8))4bW+YdPM(#JfYjrX|s0ZGJFJj1kA7T5#oW*{}(o0{JRPYAUgbE`(9fF zElXf&+}cT_d|`{ni65rp@5!dLgr|SmN;wGk7wBKwCY!1|6ymi&am+EIw)zvy{nvx| ze>IaQ>&f2m1#cD1VC0=wW$Dd?2F_bHCVr`{4U$WB$h`Dl&g>@NI99&%N+*}az#PP_6seVxdj zzEfz*Q(rhquoD1rv>dHpdq;CU6sS^>`NVB$UpTm-u)!x`w>UO2hFWZ;p(P7k-* zVu_!uhxWYo5t!dHN)u?vHu8t1po|xRsmhe9%BqJ|P}iH;sgBAFBBh17#OuKlqqEll zWwWtBp=leVH?D8N+v75`V`oOs3k~3!UKENi+=lk!1rUvirUV!G4_9Y(8j5kSx zU0INXPUQLvuz~a9r^?IfAKur~*jhIswG}tLl5SVoKh*TzyE2e8b&aec?JN0~X2BjacMzxUqMoIa*ZE-FNy^w{WpmU;DT|9{GhDkmC^Sw50PHx7tT>?jmHdJyz6^;R|9>2`ciGpHYuF0hP4Y% zbJHldH4fmdW$u|J#Kt7V0G}C#=2{^;ADSwXo66}u01coS^GyWxB7WK>ln;3rLi}h0 zDzxG7@B!$>*3Fj$spm)8tBdGheOSXt*%Z_F*7+^2xFYwfd*A9Ta=#gI; z;D*ril=Bov&`Lt|kTwzlPXhyk5qd%FXykzb;CQ`(Cur_+LHEW%-jE>QDSrt>AmJr( z0A+C2CRQe8P!x`Hv!tsYBsiur*vc#rx*4oA>Rcx2S{2|*EEl5nHKblpB>f_Uhb45W z+@*br2Q`?wWK0N}3>~}-9ijx^X#JcqA?#`n9f5#n5a78faOt=M-b?(6L!7Bi@IR}d zVHO*|0hyu75@Fkv;m3mEr|fo!Vf14(fYPF;@+Nf`FaA9s{7f(cLpK6T*9@Wd2!}ZW z-8q8jDuRS6@~$InCnNlJGkhr`lKKj#OYo)Ycmx?$6ths&GnfI8ijh$_inAt)8y3Yj z9oe0QjNsK3y%dZd`x^R2h(|CXdZ{d0Wcs<-RrJ(y$Y4fjSGwNP5XQTp*HY88y=H{U z5qk19F|y>LlNq7my!h%=u>fO2YM9;%G*Tgmx+@}bZ7RwvAf?!tauZbG;(&8j9E4;&ZOhyFqx~byM34;p-*D*apR@357wKA~ddu!8LJl z&Iyp9Fl${pa8IZ_Zq&+DEG<<;KQxp&A@VB~gz6RKol|7dFa#Blg;K?baU@0tfumuG z3@a|v!w_ZFxN!OSNvHTMjtCD}5{t)=l{t93 zCUHQ`AbKmTmp!>cD81$?jpPJWb_8Eoz-0>is}!7w6;{ zDxxf^Y=K(d>h!F|$TTU13>k&EDYcT@;_QD2yI>E=8@$&2#fVn5y{rX{=`fFdlX}kknW1d*XA3G)WeN8;7F<9 zNc@sOeW;rjb`6cV{&_(~XX~8a!jU}{82@uRUP>XytClCIJ&SHT%N_>t*GoWwl5AL> zG&QXFIv;M~5RFHdVh&Be!pX-A&vj0OUBNcU@@ey3>GP@K97)Qa`TW9}QPTyDy*xe& zNlgibtqHK91ijRm!p%$-n*?Y-Zv3EmR>5_Sad6RkX3?tN8)fHW?daldJ^qi=#l~NY z4{Q04g-dibO3tr?J0f#}s0)zSC4kwVi-B}yLil&oaGWGKo@MwKQ`+{9P+%>b>}@Gf z(Xu?BmQ=r#x~`Pg_4^Y`noQ2Z(#TRe{W4b9GPLv3ZpkwCS!5Z{P1$5lSp{iXkYE`$ zez{;$xv;CjOG{erx^fAcigyKiqHk&Uu43OORVaj%ODC12uT^+pRyML#dP`S+f1meJ zk$pF@QnI|#VD{C)b!7@mRhV>@{)qz3cB08np`|Ool?9yww95LXz|oRlN5A@~L$wnP zn@e4_?MSuzEGw!c`$!6*^a|l`$r_l1z;34p*H%Q()F5XnhKJK`dJuy?bnSlW{lPWM zFnXXxMrr}K(G89LY*vw1raUY&OP@REhW51!G;6xHgRH81I1w5GQPay#QLJksQ&DlL zX}qbMY$XI6N!jOWo%F z&eGPPRQU8I^gGLSo2EgBiU53w$xy?p!(grh@Vo5;?F)R0W@m-u5bDky4F;DQJ)@pT z-rWv6+O8n1&SuH-wq9z>+vKpI&KNQ^XWA@eVK3^+hte-v8jr$!C}&r2eYciO*NA=h zQtuV8I&F^E}; zwr|;?efM*Wt5Tm2y1F}cckM1)y;ZiKUY21{MKEXcx2vMi`u_f8!>~}gSk8FXVA!X` z_+*!XCIZ$4&F&K2{1E5DyTrnA3+T#h|2k-J*stSStsu=gE(2DW-W|*ln0P zYM9!Msib+B&X9?LZsg+S$TPzitZvAWqo@(ic^>Zhk)7j_axZ8W41&hpc%L3ANH|)= zjJKN=@3fmLBpP?$KPq}RDsa=OaTQ+l^UrUqvBXTg!}pyqw6TJt9@TlymxeUR8d%}y zOy%d*xJuei(6zt`^P*9!22LBp$q8m$M(`xSVG>05-2HCy z580G=1Glf?RFA_{v0C%y+*FvTU3JD(QOi`mR#V_}&ZxWbvhUOJbexBd6{IPVHr#*6 z=cjWGXf%Vt4g=7))-#E%Vl}WC0epaxVQbd*3>bY@ie_>}A+88KYi>moblqu;3#kOx zRTIu7gJ?#RNB2V}+N}+Gk#20t=5gq_lZ81sqskoR_4B!3`WD={Tgb`#PsGZ*WNzWuxOd6{P6>E&p|N=w^Q zJZdeZ^by=WQT%@0lhiFA?aPd#*N(}ev#QW9fR;!5C1-Vo@g~rv%;Z+qQ zI#v3$VZJp@B<)A{wSmYry+&Gtg|(ieHDh<$FJkK*YU?J0vgS6NW{vC3!|OIga`yYv zj;IMTz{by1nh?*84rh0o-wPXw#~a~{#BIWxekz-ugKWRNs7E4*UqGqB$W0jOR**Ye zNEmff1aSnMI-zkZe|Rg_h>iOW=V}q3L2om1` zJh!m7fZY2RHmw@%ue)cp|S^`wNqdyI^eE})XWA^G2Zuypp8_J5zI8r6U${a%^MZth56FwZ)S3^ z_HJfHSZtc1Vmz0XBvg#Y;TvtUy_I+L6K@-u9mZw>gXZeZ!}6=PEgX=Aji?x}q2nRO zYxOrTu0KzlEpEi(va)O@vfM4rpa|J5>;IT*^$_Dt{S)JT8Z#=`t8)C_w^zNGs1;JZ z6lck!z4G9UfBDB5|MK9BPwb4+~iSu`}{0L^@DX~3d$MhPgb+t<4-_2<5h9G zC}%t_)z|HOMwut`&8!{G)~~rAFNA*2JK-n%LN0*rb$oit&7$oR&jP z5vUkX@e&o|aS1xFL9j1_NfLh3GofO z{w=O^syW!d`PFdGbNi!|*cSA&g`ouGfO5v`|8d4`QOaK=f=QOY z#dx#oC}%u^lMWT*r9C*~svL5H8F+0`s2DGTiv{J3-w&f=JUh;Z7%%0)8Rz9dilSn? zATm^pcf^K@@kpbg?F{?gLCSMWS|m7C1Ssd3e;Rp#JT3|d3`%F%~5 zQWke#$HH@w*Jrp;65p82!0U6>h>G#tIz`s6qF_o#v1JJtuB@*&*apmd!xJXP=--r^ zi>f&|{xZDGz4=9|Vqg5ie?11}jBg3smW}?H1?3$rb9`7rIpd8F&iK!~KW;O{X~-Bl ziAUSDUSyg-u|&S9|vdeIwV!U_7S@g;V((kW7L>Y}` zUrCuOXyw11~D*QHL1ZexFfn(kwUjuPek!P925vuM*^n?2mKu$SdJ-&A= zS9lyI#lNkQhv)ej;FB*$(fnFsIF{KU3r&nv;`IbTvlAy)Naw*B$Mbv(q@Zh-P~Z&f zoyfKrXIn=!YgArOSb3hYITl%}7uAk2hprTD3U*Of^o|!%F4BO{dgzYzCfawG3;Re5 zO?DFTI+*#%zVRygWmi-RjyBDX(i)=^AjeJ_q>VKiYo)lua= zD;8JFw?qS=p7&2FngcBi*b20lZN}MhF0oW8L&lbfo&d!8_QmJ)#t=v!xSrPT2dlan z^UyEUL4^BNY=OrI~bw2GB-T6(=RYIM+QY15Gj)Y|>1N`NxS zoos@&33Mok{pyi)1fJt3v~Sup2WV?jar`LP|+xq&NY6v^M^J?!o8QmFaT^k)IJN`6X; z>+*9A$be)IJSkfEdp7ksoBIH7^l0B0k3d|%zjWc39;nk>hZUD|pKyls10-0YGv~yn^XV>s$TFr3trJYAppUF&A%~;f#WzKN_r;OwT zcD(%`#$xlS11e`7@2~)7wua-gHjtS%iM^i;uH*R=lT$|Y6)~pP-@2o&Y4();e!Ztf zWdAUSZFTk*_^+TmD6Wr2Tf53)VO)j0*y~xU+AHV;if7liLlnN z&Re>Q4*gAOmdQP;I$pUSgn8UfGQYSgQd{6qyF34=K(yJbU$6%Q_~pJAcNun_9+Quh z5&M`%>~NV)TlgnH-A?IxbW*IW`l7yYMvP!M{AYcmw(UWs(MpcoBWekbfES?5>+D6R z%O+y`L$430+0%R`VD09_u$U+$ny6*p5$~e0nFK<>0o~&ru52w<3)q* z-YMW-g6%oNNIckOLWJY>gwh)*=lulX3C+Z8Yj$(4^bQ>N<`%RN{|!XSx)*M^Fi?Ks z+w``@^m!p@!7j(?;QzBn#&^low}`@BPR)mA3b^X$dw!-R&F-f_>6^uD8q;Z_JN2U# z%j}gIQ0bC*tqZtX>38IBV4!1?V(ORX?Z^4i-xlg;5AlUPaTP?}8$%1IpZMX(uItwA zuWd&0X_MjxE3h@{H&UK2&`>SVe~QanfFj|-BtY-Gm*s>s5HT0rpG_j@7p-bXO68zHU5C9{#2_yD|8I~Ad@B}5k%p&<5C4LY9 zxJZd#;HjAf#hADZ{g8p~&ikkb8XB`4l(h-IXT+|j#NChs-*e8=u4MjtmY9CYGE?E5#ngk@*R;pSHwy^ zABBRVDQcoaqCz5#F+LoIvp7e{2Zd!D$8h3CabJag=!tnrW&f@e13VOG(}VG`hrI?7 zo6QRbY{km(#VK5Yv(v*Pn(K;#9aD#_p)a(?A5l<8#_b?!R;)D9%X%1nv$P|l=ZrqGJ)wzOax=q zWF%q(pVne%8z*z?rR&&b3c)hpbF$YRV#!>Sn{s4*N&r6POl1ki#ID6??#^PrPS>?y zH>d?hJBNL$jhv?n`)q;wdU{s^;6OL61)8f7lxoe9fwT^0v(^F(Z>S;ZdVg?)Fx}uu#Eo@BCD}xNB6-OHDzXp32pW4F<$FT^9)A z7KyuLcDm%NL>7vp#mJ4vvejrRDMX7)M$$E72c|#L+9v1Z#j&3HmeAbF?>US>akVWV|6ni@f*~b8|?J*CN9IlLWoxUy4+a= zNFm?DqRuNA(}t>Mk?YyAe$z-9{zXXBP-fF@Qq#>%Q$JZVdMG!BL35`=v#ey2QfzkB zWittF3z6if;cI)cA0usx#UQ! zX*g%{M^)Mm^Oqg33_93hk%^W--NKIVtsR4~h!60pME=fiBCIB%1=V;_6H~a_B3h>j zsUmZopN>0S3^+k^UC6BCF3($zmm=Lz=5GH&j=<#ZzQFE68vF&hw$OSus4m%WykH4 zYKgg{j;On}BeyI@^gpeg({#D&wxC7e$%n1!_p6unDDw4E;bmP@##$&Q7(0i3CK!-V z?|vsaz^0!NZ?g)81jSlwS&Wok3SqnVuFWol^<7Krw~172@x6gR3Cmkz($!e?6lmy1kI=2Yg750 zfNl8Lg=U1)0okwBmES4X#D2%bsoXm|1s4C@+b9&n;s$Kz%zi=0TqF{+4au-njBsG= zyVL1YR%@T08D(Z3P~hwB$E#12&-1>Dn5PAv)D~CujWKw)Dx-~mg7wSZ5f9V@v0RgJ z`o?LN$K=r_#%JR-o?F|4##MT;pX`)cG%)cQ4oFIltbztxxOHCaj5sSZxrz=dQvEp* ziK$is3hPgaDGxDhO-9^3rI`ofi-HLar-fQ4K9LUF_Du^pOlRJ)EE>%4h0b`to1wbu z&$oW&y;~$nFtUhC;q8)6a}(lvGebf)Tk(8Wc6utna`N5h&~C(ReFLL>M2du4SaIQi z>lW}0QV57@*mS4N;g%&Uq0OZz%nlC#JE$M*eh!>7oK?=q92JT^v;O-?9si0Bctbb;wwQ}S@yKA z??Yr6E*=n#XbF%xzp^v)%`oqbwo8c#dS0_c+sF_Vvusj^-#4&qxjfEW%vvbABCWpi zhW=Sz%8EwWinAnqa$sefY2~Z^q6XUPJ01ox8w#1!)hEj-u?K zDIYlj6=I*O4`6jF^z^C=DR(tfkG5+2g2B!6{S<>AIT# zZhY@S^Ckku@W5#b>LIC{N=Gs8Zqo)t`lu8jB{pRiTH6-a4X{+YV3p-p773315axaI z#^O#Z7N~^fu5ZfDaK>ho$`kXV-F&ge0kJ24i1sF0c3su>CRFGY8~230R~FovQ|b1l zU+xzmllb@MSBaR`dG@Z0_CL2&KIFB=c*wrpFMKBT|p@OIPQt8zSL{U#R+VKLz6?gl7%bFeTQfE9T7W*E(CB z^EzNkZFJZ>oVhwt(3|*jUGpY5i(I}=Yo?yevu;KCKuMfBm}o@U7K`?NV)N~| z64j}j&}kP}+~2H0#q;ms)QV!o%{*s4oY+K7@m^dpIhW@_o@ZQ5si?bPCW+N^ zAr}ityRI*&y{!rsAm@Wt7?BB@g>`r=c9#-qmv)5dJ0X|^GmY*R+m^w*v1!!v$ZJ;g z{W5W;icdGYJe7!WCZ)O?{*jwT@u$rUw=Z7a{)l{J6Vau#h-CxElt)%#_A}f`BFPyT z?_|~P`o-@gylxdg-1b$kOuZO=19AEiZY}nus zD;J4N?oT!c!o(+Sek`CglzBUKzBd6^+*{~K(mpV1bh|@h(&`)*^h$L$7}r~H3D5wy1lpw#=pmZd&O@CpZ?bwe*%2bkT7Fun_)aAPb3u7?66<+ zhj+RE)sTpOU=Wc2Mr0O#oeNl!Li=0kgEJl}iLK8`l+l3Y3~-L>D?k2^x~2`*8nMELxMg@6u9fM;s*hk9WCF)d(}Odvw_y$A^V?T0%=oa9)&H`e`&;!C$?#9q)wM!J z<<)Vy$5WqUwM`O6UVS;vT=uZ*h@1tXaP%k7SH3F3yQ)Z)OH*I9-RkdI;WB!I@t=cr zGmKg9OdFC|`VeJc>~BPMBmwJq5>dz<;?50bfYYg-NbL{-Rww>qd#noRz_* zph5A}=R3Niwy(;XTt(&BZ1r{=zn$N+zQAbB)v%iVh%;O5`kKGHSDYE&HUNSFI@yZp zuXAXVMLFZ>*8HMB$F)CD|D5;~AbS35H15F}kLS04aK?Yj{QSomFCl+$#>K8)kj9%+ z8hhe-#;B~O_BpRT9QR2G{S6DWq(*R%y`m0wciA33rU&g!nx?t!>ysH%tF|&UK`%FK z#XauAJ(_af^6JZ-ejyFfa(d>XKxxc{6g(srA{g=tVwZdL6;09)C&Cl!87BJIjAm~g z-p4Yk-S=bF;$(3{zwW*Uq1BiA<3(k5Jl-7p;_0-adk%#zZXT z@5mny2Nt|%QdBP`@QX?Jq^I&k&%$o?kh1r>hkt(WsYAkM(-Ew}-A&%l7Qp86JxEsS zD>ZI>d+b9jhLd##;Fae680n)^$nd`El9n_}y4E%nNgGxvW2#k=<5dtcWh)O6O(w(D2 zN~B?9FuJ>?bAWWpfYBkTAfPmYfFL=}jXuBYdCqyRv%lir{kdQ7FNpUCE4A2bjh6B_ zFj*dnk#4NQ@7RdA0cPTMWd8%?WRh#96|<(^YvU6kuP4nWGPs!KXGibU;8dZxc${2H z=H>#iQV!NS!CQ)U+EePqFWIrUHTv6kC)2;(vB|A@Md1XS%G`LlAZnwb;{I_ei%@ez zVohwUjxXxwoAH(1G*$nTBgnyTn`b!SazB#KxpS+0TPu?a z1(DnOtrq3^xj=JuQ(jhFgK{OPkW-dhL9<08xr(ch4{EDqf20AtwJi$D+fw#unQHx! zSS*dRt>#Osl~2{6{Njn7M$&e?QL1~XD%4Ijo%T(cb_0jzRoIiZ_~}Z!V_y>-dtH>c zHZpm-!ba9!f4W8c2mK^)Z)RCs z(VlIYMLBr_H*EGiXImYu1}7a_XAf=#mJirE`N+2rLd#~`^BnizuU%-KjLvq*qF(qP zmz&55ypC2-whp^}y? z)MaEaBl<92WEh|In<&MjDzn`efP%9QRSOlJylEG{$w$tEYpvZJb?i#x@-%&fg1y*T?8}Ym(wScQX`^yoqg|M!%c(H)r)NQO=Sojz$;20Bo zaW1n;jTX1o>Fc}_pndh6_wicJ-}Y56SHbtb7$&~G9;hM{I=)(^%n&WaKB!skiOzgA zRnQ+L@?!hWe67uxfNOByJLXR?BIYJ4sP15*t&*yu-f5=K=g%x<|GRw!nJP0PJYoHx z?W-yM-LsgT@~ptygbYeeH7lRs6gc1(uW#|g|vShAu=xFh9hhzmt$)B7QjWw8N2d=`lYg+D*yGto$9V0 z?huM@D*K8op|5Vhj#Be0a* zaD<_6HymOA{qGx&u)21`5eznf_7!#J*Din~xH$ji2s@<&07ozZ1MMrDulk>8y>@FY z=>vEV2jEbpLp0ZpC9q>Qew{_}_w&A0;2Cpx+xO&K#iL%a{oS`cQrEnXyMXr9@AjMa zRohMbsu^fsc{Bp;tA>W>UB1UNmv$q^vxj@DPv$J^Bs}LKVMBxSwvACg7o7V2_`kZ% zF7hpUZaDH`mVEw<@P6~hW8hs5rmo^yfpIeMtVW1*fWJq}^MilH>vV9hCEKWQucv!0 zaRED~3Ky_rE^z`J!HE;#2pA3?`6m9>oQgofi=W;z(HDDe$R6DNuDY%Fzj`yod=CZ! z-T@rJq#57{(&*Rp6FU1pe$Uu+vK=pYsj>kaVT={v2qvrmN7!cpI6}bc{$Bre$K`JX ze!$hKy>|fSQl}~48q0nB`j$f1ZQ`0h{5#{=6a!sXyfs1hGGl(SD0H9Wn*~!2oB9Hb zi=?QZf`N$H$CJ5-Jl=+gLn+Tot)z#_ItBZ-gqZ-2pm#(-uXGSmR?9-ws7uS|c7Vt}U;*`~(lEy!M4CP? zpyhti^Q7fK(BXXny%%}E*n%tTT1x=~-c+B|xjN@NfFm4;4m`JJi-qx8KA>pslNBP5 z685%a9^rZ=18@X?fFm52pp{1tD6@VS(!bz-EdsQ!>?GO2Vs>peWOnH6DE)c~vY(`L<+V`88JpOKLfr zWikG3@VFaQVm4kSvkZlPH&p0xFo_@JHY<4Ijz)bxP8*lp?UIS86M!R(031QTGcI1s zpF^tjLZeLM7~lx&@41ywoKtB&)kVV73@UH!+OrNH`v4r_C>Z?t2U$Z2N&>7A0&s*w zfFqy)jv(F&a0Cy4BWxqf-b?`;As^rfQUFJ=12{qhz!AXB4ajL%C%^L5Rj|&diH54+ zRvzOkfFo>IR#U&m5g)H5!UCPtz?B`$!>}X!zB1Dht5ulc2oS3|gd$^;Dy-RWgh3a} zjW7^aT$oHhsV`CfUmW3I`>N8(Yv{|;IaMPR+aZ`sLf8En6>BM%gFVgAysITuLyRNV zTha`}L49r3)Tmvv;`jQueZRL*-N1uiUk=m%v~4O^`^*k4owWRE{}tuzOZZ3yq(_aF z_@>G%VMl+DMVPYZo}*pCkjXax_l}ZdXIq-`xLZSiy6;8Tg#L#k$T_=|znn*ig0L8I zy9970mOd@zHAP+Z3t)o2*=AbQ&nV*v5Vgkrq@qSLCJS203q}RB&9MZt^Lij8bg!c4 znig=Lk#)%|_0o3H6^zs*{x?TR6j5T|0ZLmJ(+}Y?W^-niCnOJzc`*Wq@8{S}m|Cz( zYT-$AixRw_SfZ?WUHLLB1QFhS@CPtA97od;uEAzBy4 z023%;UjKpljxM;uFCjMAaL^3|Wk0~>YKx3}jz>EVd(aS{_SIZh=e#*)w4ustWXJeI ze>Tpayj(MAhm_ikUc?D2)$!6|TziIP_zIr!iEhb3i8Zr7Z1_o)B5qLaNJaz zLvPR7?50X%%k0RW=f3cq%65(H*GtF2TjDJz+PoGMZLoK&67!SRLb1%PKNJI3NwD|+ zNO$f06ey>uMansJu-n^u8C+Pe;mPIzi1ZG zO;alR4Nvv7hmK(pI&r&8E}HB>`0Q6~Oxq7m=e3?^gr-?nyD2zIXQS^iN4X}>W{{QH zLv;S{)fa5rb=0lXUSs4{`wUwaIZ`jipT0kCm(uevky<&#*i)YQ9R)wU{6W@vijsJG z7*c$>c2spX+W+8pfpqJZ<~I+Grl-G4swPsbFN~}KV4$P69VFE19rU(v6>#;0?}OP74(6i> zDT*@!c{<=Kgf#ZxK;`v@YeFP-c?<EL~Z-FGX z$;Q1iZZ9A_vRojOVz+*F$oNcPu&m3;hAmwlOs&x)R#s1%!@F%Qz;F(VJcI^gyBXu! z9dWoY{jpUma@(6V)evC2w5%1Oqm8^ z(I60>4#fC~+h^Q3rJ%caBM7-kM7`jocfnGH6zbk&5>y@|xRx?uRyeb|zM{5!@_O2E zs6cUOdP95J;Fv*{1F0woI2B*0`6)|x`xKqhUXx|?%h z)Jzt+o(Q#M1Thu@CYtEsrY^@F^)M|_Yuy_688Ic6RlcsCR%v($WxOs8=RkeZ^h^@# znW^HLJDs58Cp7{!jwJYLGD&l?SANo{ccM#k>@A%;g91rXcO9L%Qo$Sue{N3x)F}1- zzz3(Kr^0E8Fvt{c*i|2*Up8rJGc~4^aGo3}x80Vn;L4y&0Rspg~W2_tim=-Ge)Ydwrt=oYtAH-5GiOEztu{}rbT=$%`P zyxH6N#AI8??=B<`T>(5Psn>cq*y6bd+&LC33BhJLR(GLk;SkOfi;AKs$^~Z%bgso@ zOsH9oPdJ%_u-VmFJdLZvtVifbY9v=gD1ABy`)Sx(X)1?Lb~tWS6pitBls8xyHguM6 zq!H}$C*u2QTB=x-u5e*;I7#?G;rBlU&j#5$s6VUvA z(+I)y)6839MWjc#vNS1eeX-SZS&(xMJAIIGn*Fg?oF7fGolx;3ZbV0EyiO`qUppQH z;?8;lD&a#XJ}E1fC69SG7mikdW!e^m5RizYvurmK63Sq41Ro*?S$)Mp4v-`zxg<96 z=m=UD)w@LM;W?7%q(bqm9DO#c78vx@*|s_5I?a*|ZD}T!Y=1A$CcKouCv6DNEA#hlQsuG^ZM-70gAj`3ZoQ=K|ey z($*nX8R4F*sb#tIRT~6sc|O&Q3DxHdf|m=`G)L7~27(OYH4ik)aWe$?Okx(y$@N9LLIqwzPMSP$OTf2uCjH|9&ib&x9ZUP@YA!m8tZk;uJsCE*f<30 z9h(XjBC`1;8WdO6cD9}SP<8qrgA9fmWP3hX+k;=lek7nsb08E@`}Gm``$xCdhwcWA zf7}|y^TMO1LUV%c!SEzLUo&l$s0pmlNi>EwKk(v&O;*z!C`dXgpa2PD@PBtLNTsMCas0K5RV*6%he5c zj9z;LvMnhuKZK_v<9SD7w=;i4N2XhI>`J@cR>zED=T3gdNk*p_PA6@5<9RF3DNob4 zHK(q#PHyt9GT`q7+7`Eh1>Z2GsNAh@w_KdJD^4lYuYZSm4~t*&YVpcER1$k2}p?$vrg5c}eI` z%!fS<7oSiW=6uxt4nA!quT!ohNVw0l6o^<}J0O%I$s%n$Co`->(<6Z?^QmNe-h1;e zJ9^*^PpzUtD~G;xvyL&k{xBp8(cQ6x?)Kc}^lk%T4u|^vzH$ag4%GB?!VGzU9kW(r zfcO6?-IyVcyNHPWR7Sy$s)bLt~mW}qjAH9wING<_< z+0mZLZ5b|X8x0yh93Hv$<@rrNjuaZd<2Qh3G=BF14{kef$>WZ+cWlY-`1!>08dH^kZXsW940}Dv#K*|;z{A(mkaah}jH}=z zZA$OLRBo&A7yZ!6k3SbH&P>tGY)(utb=#rmqQ;s+CA??b7AAwx{@Dbx z-A2IK&$(7sCA7OHInJ*i;G|}C(>Bg$ zDl>!$EM#ab6lHOjWGy5i7vQydj3W!$s|#V+Up^*7oCs!+Q4H#dppT0XuZ6UsmM?1) z^1*q1o8!8SuO^9vOA{57yvB*tF*eZPLE})Lcpf8v)(7!8N?*~naq8vCPtc|IeO>iD z`N8kZjfy|zwi}BzC#;IQNY6WGE(BWcE)HNX_0kOHmfEfpTQU@={eyh zDM)2i52h@hwVpnpKj)k8p8F<{)jE;3bX%o&?`OIBt7LrJa{uNH6UY*7Td@W{>4eb{DVaWevy5PmZgSj}x#*SN(QYk1MldWlit+f7wn74@)b3 zyznjH=lAxYRr^$rt9B11f{t`fr}bRiZ0HaAjvu!~O0&=Ojw904`Pb^nUfS_4((-SyiQtH_o9vN@~*8fA#pfXt>shj{Lr3?c0ZH)AXp!zBRQ?dw|Bw->TBQm#1F6Bs|8)`G-?+tJ=tnhECfC5UHyt2 z>i88*_R7?|Fr6CH;^?p)A@@;O1ch=!4PFw12>Vw-M<=@|frsT_(nHvag19p~^Z1c- zzRxog4oT_Xsw{_c&KzsmzcrD+@0X7#F|tO}k1-1VEF0Ync^VB>sR*mue6xQXU-?Tt zfJ}3LEH^THV|;5s!5SxEtF?Ix{mxSjymkEaghplk+1~Vi zyvNVyT&H?hKP4`IeA(rckNt|mg=D|zIa)wwC9*)wBFejRx z00J5oUy!SL50ve_Ud{ifef4+8EHP;R?;Z2!;gd|=1M|M92hy<2S$oUDBtDbAOuZxP z_=JL|nBf_$F398uYbVGL5|FG7YYP5QHqE|%9HJ<4xAt@$kCzAqfL^Vtuq*l&aAV%a7Ash12DMy#yKGzEt{@k!s50cD%sC; ziZ=|!?Mg4S-lfLfLoO81RA;x?SI7B^B@uCxOT0Yk!-zXJ$QPcGTq?LpEXaQ|ZgpyH zC`e-}IaYBWan0?JaOt)sr2APnA~*J{o7%>=uB;h@e^^0R+;%vWj(fS)!MD-&78QFU z={~`nVC6og8c^Z>8E9V#=-CAsQO%q5Tg7Qk{<=fG=+(bQx#aaQlyD`Cv%`CJ>>egc zq)Dyg-A2wn>-i>OUdQ?Nqwc2{uOF{;yjKR(n8TmlnqPXq&vUmof;rS%2KSHEJ(hb% zt<{C3a(n$bWLrNwa{}bp?!EiDj zCJnT&prS8XfE`o*r8#+0(@PHF3E1;F+4~=W_LcTTgbvWYYG{(>_nbhO++n43C6nX! ZVTov7Hrg{blN0(l5#{V|0c Union[str, None] - cf_path = os.path.join(os.path.expanduser("~"), cf) - try: - os.stat(cf_path) - except OSError: +def _probe_config_file(*cf, default=None): + # type: (str, Optional[str]) -> Union[str, None] + path = pathlib.Path(os.path.expanduser("~")) + if not path.exists(): + # ~ folder doesn't exist. Unsalvageable return None - else: - return cf_path + cf_path = path.joinpath(*cf) + if not cf_path.exists(): + if default is not None: + # We have a default ! set it + cf_path.parent.mkdir(parents=True, exist_ok=True) + with cf_path.open("w") as fd: + fd.write(default) + return str(cf_path.resolve()) + return None + return str(cf_path.resolve()) def _read_config_file(cf, _globals=globals(), _locals=locals(), @@ -119,8 +131,29 @@ def _validate_local(k): return k[0] != "_" and k not in ["range", "map"] -DEFAULT_PRESTART_FILE = _probe_config_file(".scapy_prestart.py") -DEFAULT_STARTUP_FILE = _probe_config_file(".scapy_startup.py") +# Default scapy prestart.py config file + +DEFAULT_PRESTART = """ +# Scapy CLI 'pre-start' config file +# see https://scapy.readthedocs.io/en/latest/api/scapy.config.html#scapy.config.Conf +# for all available options + +# default interpreter +conf.interactive_shell = "auto" + +# color theme (DefaultTheme, BrightTheme, ColorOnBlackTheme, BlackAndWhite, ...) +conf.color_theme = DefaultTheme() + +# disable INFO: tags related to dependencies missing +# log_loading.setLevel(logging.WARNING) + +# force-use libpcap +# conf.use_pcap = True +""".strip() + +DEFAULT_PRESTART_FILE = _probe_config_file(".config", "scapy", "prestart.py", + default=DEFAULT_PRESTART) +DEFAULT_STARTUP_FILE = _probe_config_file(".config", "scapy", "startup.py") def _usage(): @@ -299,6 +332,16 @@ def update_ipython_session(session): pass +def _scapy_prestart_builtins(): + # type: () -> Dict[str, Any] + """Load Scapy prestart and return all builtins""" + return { + k: v + for k, v in importlib.import_module(".config", "scapy").__dict__.copy().items() + if _validate_local(k) + } + + def _scapy_builtins(): # type: () -> Dict[str, Any] """Load Scapy and return all builtins""" @@ -412,11 +455,29 @@ def update_session(fname=None): update_ipython_session(scapy_session) +@overload +def init_session(session_name, # type: Optional[Union[str, None]] + mydict, # type: Optional[Union[Dict[str, Any], None]] + ret, # type: Literal[True] + ): + # type: (...) -> Dict[str, Any] + pass + + +@overload +def init_session(session_name, # type: Optional[Union[str, None]] + mydict=None, # type: Optional[Union[Dict[str, Any], None]] + ret=False, # type: Literal[False] + ): + # type: (...) -> None + pass + + def init_session(session_name, # type: Optional[Union[str, None]] mydict=None, # type: Optional[Union[Dict[str, Any], None]] ret=False, # type: bool ): - # type: (...) -> Optional[Dict[str, Any]] + # type: (...) -> Union[Dict[str, Any], None] from scapy.config import conf SESSION = {} # type: Optional[Dict[str, Any]] @@ -476,7 +537,7 @@ def init_session(session_name, # type: Optional[Union[str, None]] def _prepare_quote(quote, author, max_len=78): # type: (str, str, int) -> List[str] """This function processes a quote and returns a string that is ready -to be used in the fancy prompt. +to be used in the fancy banner. """ _quote = quote.split(' ') @@ -529,7 +590,7 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): if opt == "-h": _usage() elif opt == "-H": - conf.fancy_prompt = False + conf.fancy_banner = False conf.verb = 1 conf.logLevel = logging.WARNING elif opt == "-s": @@ -557,36 +618,23 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): # Reset sys.argv, otherwise IPython thinks it is for him sys.argv = sys.argv[:1] + if PRESTART_FILE: + _read_config_file( + PRESTART_FILE, + interactive=True, + _locals=_scapy_prestart_builtins() + ) + SESSION = init_session(session_name, mydict=mydict, ret=True) if STARTUP_FILE: - _read_config_file(STARTUP_FILE, interactive=True) - if PRESTART_FILE: - _read_config_file(PRESTART_FILE, interactive=True) - - if not conf.interactive_shell or conf.interactive_shell.lower() in [ - "ipython", "auto" - ]: - try: - import IPython - from IPython import start_ipython - except ImportError: - log_loading.warning( - "IPython not available. Using standard Python shell " - "instead.\nAutoCompletion, History are disabled." - ) - if WINDOWS: - log_loading.warning( - "On Windows, colors are also disabled" - ) - conf.color_theme = BlackAndWhite() - IPYTHON = False - else: - IPYTHON = True - else: - IPYTHON = False + _read_config_file( + STARTUP_FILE, + interactive=True, + _locals=SESSION + ) - if conf.fancy_prompt: + if conf.fancy_banner: from scapy.utils import get_terminal_width mini_banner = (get_terminal_width() or 84) <= 75 @@ -659,8 +707,100 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): banner_text += "\n" banner_text += mybanner - if IPYTHON: - banner = banner_text + " using IPython %s\n" % IPython.__version__ + # Configure interactive terminal + + if conf.interactive_shell not in [ + "ipython", + "python", + "ptpython", + "ptipython", + "bpython", + "auto"]: + log_loading.warning("Unknown conf.interactive_shell ! Using 'auto'") + conf.interactive_shell = "auto" + + # Auto detect available shells. + # Order: + # 1. IPython + # 2. bpython + # 3. ptpython + + _IMPORTS = { + "ipython": ["IPython"], + "bpython": ["bpython"], + "ptpython": ["ptpython"], + "ptipython": ["IPython", "ptpython"], + } + + if conf.interactive_shell == "auto": + # Auto detect + for imp in ["IPython", "bpython", "ptpython"]: + try: + importlib.import_module(imp) + conf.interactive_shell = imp.lower() + break + except ImportError: + continue + else: + log_loading.warning( + "No alternative Python interpreters found ! " + "Using standard Python shell instead." + ) + conf.interactive_shell = "python" + + if conf.interactive_shell in _IMPORTS: + # Check import + for imp in _IMPORTS[conf.interactive_shell]: + try: + importlib.import_module(imp) + except ImportError: + log_loading.warning("%s requested but not found !" % imp) + conf.interactive_shell = "python" + + # Display warning when using the default REPL + if conf.interactive_shell == "python": + log_loading.info( + "When using the default Python shell, AutoCompletion, History are disabled." + ) + if WINDOWS: + log_loading.info( + "On Windows, colors are also disabled" + ) + conf.color_theme = BlackAndWhite() + + # ptpython configure function + def ptpython_configure(repl): + # type: (Any) -> None + # Hide status bar + repl.show_status_bar = False + # Complete while typing (versus only when pressing tab) + repl.complete_while_typing = False + # Enable auto-suggestions + repl.enable_auto_suggest = True + # Disable exit confirmation + repl.confirm_exit = False + # Show signature + repl.show_signature = True + # Apply Scapy color theme: TODO + # repl.install_ui_colorscheme("scapy", + # Style.from_dict(_custom_ui_colorscheme)) + # repl.use_ui_colorscheme("scapy") + + # Start IPython or ptipython + if conf.interactive_shell in ["ipython", "ptipython"]: + import IPython + if conf.interactive_shell == "ptipython": + from ptpython.ipython import embed + banner = banner_text + " using IPython %s" % IPython.__version__ + try: + from importlib.metadata import version + ptpython_version = " " + version('ptpython') + except ImportError: + ptpython_version = "" + banner += " and ptpython%s\n" % ptpython_version + else: + banner = banner_text + " using IPython %s\n" % IPython.__version__ + from IPython import start_ipython as embed try: from traitlets.config.loader import Config except ImportError: @@ -669,7 +809,7 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): "available." ) try: - start_ipython( + embed( display_banner=False, user_ns=SESSION, exec_lines=["print(\"\"\"" + banner + "\"\"\")"] @@ -694,18 +834,57 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): conf.version) # As of IPython 6-7, the jedi completion module is a dumpster # of fire that should be scrapped never to be seen again. - cfg.Completer.use_jedi = False + # This is why the following defaults to False. Feel free to hurt + # yourself (#GH4056) :P + cfg.Completer.use_jedi = conf.ipython_use_jedi else: cfg.TerminalInteractiveShell.term_title = False cfg.HistoryAccessor.hist_file = conf.histfile cfg.InteractiveShell.banner1 = banner # configuration can thus be specified here. + _kwargs = {} + if conf.interactive_shell == "ptipython": + _kwargs["configure"] = ptpython_configure try: - start_ipython(config=cfg, user_ns=SESSION) + embed(config=cfg, user_ns=SESSION, **_kwargs) except (AttributeError, TypeError): code.interact(banner=banner_text, local=SESSION) - else: + # Start ptpython + elif conf.interactive_shell == "ptpython": + # ptpython has special, non-default handling of __repr__ which breaks Scapy. + # For instance: >>> IP() + log_loading.warning("ptpython support is currently partially broken") + try: + from importlib.metadata import version + ptpython_version = " " + version('ptpython') + except ImportError: + ptpython_version = "" + banner = banner_text + " using ptpython%s" % ptpython_version + from ptpython.repl import embed + # ptpython has no banner option + print(banner) + embed( + locals=SESSION, + history_filename=conf.histfile, + title="Scapy %s" % conf.version, + configure=ptpython_configure + ) + # Start bpython + elif conf.interactive_shell == "bpython": + import bpython + from bpython.curtsies import main as embed + banner = banner_text + " using bpython %s" % bpython.__version__ + embed( + args=["-q", "-i"], + locals_=SESSION, + banner=banner, + welcome_message="" + ) + # Start Python + elif conf.interactive_shell == "python": code.interact(banner=banner_text, local=SESSION) + else: + raise ValueError("Invalid conf.interactive_shell") if conf.session: save_session(conf.session, SESSION) diff --git a/test/regression.uts b/test/regression.uts index eb51d61863a..0ba879690aa 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -540,8 +540,19 @@ scapy_delete_temp_files() assert len(conf.temp_files) == 0 = Emulate interact() +~ interact + import mock, sys from scapy.main import interact + +from scapy.main import DEFAULT_PRESTART_FILE +# By now .config/scapy/startup.py should have been created +with open(DEFAULT_PRESTART_FILE, "r") as fd: + OLD_DEFAULT_PRESTART = fd.read() + +with open(DEFAULT_PRESTART_FILE, "w+") as fd: + fd.write("conf.interactive_shell = 'ipython'") + # Detect IPython try: import IPython @@ -568,6 +579,50 @@ except: interact_emulator(extra_args=["-d"]) # Extended += Emulate interact() and test startup.py with ptpython +~ interact + +import sys +import mock + +from scapy.main import DEFAULT_PRESTART_FILE +# By now .config/scapy/startup.py should have been created +with open(DEFAULT_PRESTART_FILE, "w+") as fd: + fd.write("conf.interactive_shell = 'ptpython'") + +called = [] +def checker(*args, **kwargs): + locals = kwargs.pop("locals") + assert locals["IP"] + history_filename = kwargs.pop("history_filename") + assert history_filename == conf.histfile + called.append(True) + +ptpython_mocked_module = Bunch( + repl=Bunch( + embed=checker + ) +) + +modules_patched = { + "ptpython": ptpython_mocked_module, + "ptpython.repl": ptpython_mocked_module.repl, + "ptpython.repl.embed": ptpython_mocked_module.repl.embed, +} + +with mock.patch.dict("sys.modules", modules_patched): + try: + interact() + finally: + sys.ps1 = ">>> " + +# Restore +with open(DEFAULT_PRESTART_FILE, "w") as fd: + print(OLD_DEFAULT_PRESTART) + r = fd.write(OLD_DEFAULT_PRESTART) + +assert called + = Test explore() with GUI mode ~ command From 8e116af0e0326f71d54cc20d4b6d894ba3c9bddb Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 3 Aug 2023 09:39:47 +0200 Subject: [PATCH 043/122] Tests: fix AS resolver tests (#4084) --- test/regression.uts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/regression.uts b/test/regression.uts index 0ba879690aa..7f43b7fc22b 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -1639,7 +1639,7 @@ retry_test(_test) def _test(): ret = conf.AS_resolver.resolve("8.8.8.8", "8.8.4.4") assert (len(ret) == 2) - all(x[1] == "AS15169" for x in ret) + assert any(x[1] == "AS15169" for x in ret) retry_test(_test) @@ -1676,7 +1676,7 @@ def _test(): as_resolver6 = AS_resolver6() ret = as_resolver6.resolve("2001:4860:4860::8888", "2001:4860:4860::4444") assert (len(ret) == 2) - assert all(x[1] == 15169 for x in ret) + assert any(x[1] == 15169 for x in ret) retry_test(_test) From 3bd29da2a86aac1e82ab9197d156ff28cdd48860 Mon Sep 17 00:00:00 2001 From: Lex <55185179+claire-lex@users.noreply.github.com> Date: Thu, 3 Aug 2023 21:40:07 +0200 Subject: [PATCH 044/122] Update Ethernet/IP contrib and tests (#4083) Add Hart and CompoNet to Codespell ignore settings --- .config/codespell_ignore.txt | 2 + scapy/contrib/enipTCP.py | 289 +++++++++++++++++++++++------------ test/contrib/enipTCP.uts | 130 ++++++++-------- 3 files changed, 254 insertions(+), 167 deletions(-) diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt index f571d4a6568..b2c1a5b5a27 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -5,6 +5,7 @@ ba byteorder cace cas +componet cros delt doas @@ -14,6 +15,7 @@ ether eventtypes fo gost +hart iff inout microsof diff --git a/scapy/contrib/enipTCP.py b/scapy/contrib/enipTCP.py index c17686537a2..e35d2e27f67 100644 --- a/scapy/contrib/enipTCP.py +++ b/scapy/contrib/enipTCP.py @@ -2,6 +2,7 @@ # This file is part of Scapy # See https://scapy.net/ for more information # Copyright (C) 2019 Jose Diogo Monteiro +# Updated (C) 2023 Claire Vacherot # scapy.contrib.description = EtherNet/IP # scapy.contrib.status = loads @@ -18,10 +19,11 @@ from scapy.layers.inet import TCP from scapy.fields import LEShortField, LEShortEnumField, LEIntEnumField, \ LEIntField, LELongField, FieldLenField, PacketListField, ByteField, \ - PacketField, MultipleTypeField, StrLenField, StrFixedLenField, \ - XLEIntField, XLEStrLenField + StrLenField, StrFixedLenField, XLEIntField, XLEStrLenField, \ + LEFieldLenField, ShortField, IPField, LongField, XLEShortField _commandIdList = { + 0x0001: "UnknownCommand", 0x0004: "ListServices", # Request Struct Don't Have Command Spec Data 0x0063: "ListIdentity", # Request Struct Don't Have Command Spec Data 0x0064: "ListInterfaces", # Request Struct Don't Have Command Spec Data @@ -43,13 +45,72 @@ 105: "unsupported_prot_rev" } -_itemID = { +_typeIdList = { 0x0000: "Null Address Item", - 0x00a1: "Connection-based Address Item", - 0x00b1: "Connected Transport packet Data Item", - 0x00b2: "Unconnected message Data Item", - 0x8000: "Sockaddr Info, originator-to-target Data Item", - 0x8001: "Sockaddr Info, target-to-originator Data Item" + 0x000c: "CIP Identity", + 0x0086: "CIP Security Information", + 0x0087: "EtherNet/IP Capability", + 0x0088: "EtherNet/IP Usage", + 0x00a1: "Connected Address Item", + 0x00B1: "Connected Data Item", + 0x00B2: "Unconnected Data Item", + 0x0100: "List Services Response", + 0x8000: "Socket Address Info O->T", + 0x8001: "Socket Address Info T->O", + 0x8002: "Sequenced Address Item", + 0x8003: "Unconnected Message over UDP" +} + +_deviceTypeList = { + 0x0000: "Generic Device (deprecated)", + 0x0002: "AC Drive", + 0x0003: "Motor Overload", + 0x0004: "Limit Switch", + 0x0005: "Inductive Proximity Switch", + 0x0006: "Photoelectric Sensor", + 0x0007: "General Purpose Discrete I/O", + 0x0009: "Resolver", + 0x000C: "Communications Adapter", + 0x000E: "Programmable Logic Controller", + 0x0010: "Position Controller", + 0x0013: "DC Drive", + 0x0015: "Contactor", + 0x0016: "Motor Starter", + 0x0017: "Soft Start", + 0x0018: "Human-Machine Interface", + 0x001A: "Mass Flow Controller", + 0x001B: "Pneumatic Valve", + 0x001C: "Vacuum Pressure Gauge", + 0x001D: "Process Control Value", + 0x001E: "Residual Gas Analyzer", + 0x001F: "DC Power Generator", + 0x0020: "RF Power Generator", + 0x0021: "Turbomolecular Vacuum Pump", + 0x0022: "Encoder", + 0x0023: "Safety Discrete I/O Device", + 0x0024: "Fluid Flow Controller", + 0x0025: "CIP Motion Drive", + 0x0026: "CompoNet Repeater", + 0x0027: "Mass Flow Controller, Enhanced", + 0x0028: "CIP Modbus Device", + 0x0029: "CIP Modbus Translator", + 0x002A: "Safety Analog I/O Device", + 0x002B: "Generic Device (keyable)", + 0x002C: "Managed Ethernet Switch", + 0x002D: "CIP Motion Safety Drive Device", + 0x002E: "Safety Drive Device", + 0x002F: "CIP Motion Encoder", + 0x0030: "CIP Motion Converter", + 0x0031: "CIP Motion I/O", + 0x0032: "ControlNet Physical Layer Component", + 0x0033: "Circuit Breaker", + 0x0034: "HART Device", + 0x0035: "CIP-HART Translator", + 0x00C8: "Embedded Component", +} + +_interfaceList = { + 0x00: "CIP" } @@ -57,7 +118,7 @@ class ItemData(Packet): """Common Packet Format""" name = "Item Data" fields_desc = [ - LEShortEnumField("typeId", 0, _itemID), + LEShortEnumField("typeId", 0, _typeIdList), LEShortField("length", 0), XLEStrLenField("data", "", length_from=lambda pkt: pkt.length), ] @@ -66,97 +127,105 @@ def extract_padding(self, s): return '', s -class EncapsulatedPacket(Packet): - """Encapsulated Packet""" - name = "Encapsulated Packet" - fields_desc = [LEShortField("itemCount", 2), PacketListField( - "item", None, ItemData, count_from=lambda pkt: pkt.itemCount), ] - - -class BaseSendPacket(Packet): - """ Abstract Class""" - fields_desc = [ - LEIntField("interfaceHandle", 0), - LEShortField("timeout", 0), - PacketField("encapsulatedPacket", None, EncapsulatedPacket), - ] +# Unknown command (0x0001) -class CommandSpecificData(Packet): - """Command Specific Data Field Default""" +class ENIPUnknownCommand(Packet): + """Unknown Command reply""" + name = "ENIPUnknownCommand" pass -class ENIPSendUnitData(BaseSendPacket): - """Send Unit Data Command Field""" - name = "ENIPSendUnitData" - - -class ENIPSendRRData(BaseSendPacket): - """Send RR Data Command Field""" - name = "ENIPSendRRData" +# List services (0x0004) -class ENIPListInterfacesReplyItems(Packet): - """List Interfaces Items Field""" - name = "ENIPListInterfacesReplyItems" +class ENIPListServicesItem(Packet): + """List Services Item Field""" + name = "ENIPListServicesItem" fields_desc = [ - LEIntField("itemTypeCode", 0), - FieldLenField("itemLength", 0, length_of="itemData"), - StrLenField("itemData", "", length_from=lambda pkt: pkt.itemLength), + LEShortEnumField("itemTypeCode", 0, _typeIdList), + LEFieldLenField("itemLength", 0), + LEShortField("protocolVersion", 0), + XLEShortField("flag", 0), # TODO: detail with BitFields + StrFixedLenField("serviceName", None, 16), ] -class ENIPListInterfacesReply(Packet): - """List Interfaces Command Field""" - name = "ENIPListInterfacesReply" +class ENIPListServices(Packet): + """List Services Command Field""" + name = "ENIPListServices" fields_desc = [ - FieldLenField("itemCount", 0, count_of="identityItems"), - PacketField("identityItems", 0, ENIPListInterfacesReplyItems), + FieldLenField("itemCount", 0, count_of="items"), + PacketListField("items", None, ENIPListServicesItem), ] -class ENIPListIdentityReplyItems(Packet): - """List Identity Items Field""" - name = "ENIPListIdentityReplyItems" +# List identity (0x0063) + + +class ENIPListIdentityItem(Packet): + """List Identity Item Fields""" + name = "ENIPListIdentityReplyItem" fields_desc = [ - LEIntField("itemTypeCode", 0), - FieldLenField("itemLength", 0, length_of="itemData"), - StrLenField("itemData", "", length_from=lambda pkt: pkt.item_length), + LEShortEnumField("itemTypeCode", 0, _typeIdList), + LEFieldLenField("itemLength", 0), + LEShortField("protocolVersion", 0), + # Socket address + ShortField("sinFamily", 0), + ShortField("sinPort", 0), + IPField("sinAddress", None), + LongField("sinZero", 0), + # End socket address + LEShortField("vendorId", 0), # Vendor list could be added (long list) + LEShortEnumField("deviceType", 0, _deviceTypeList), + LEShortField("productCode", 0), + ByteField("revisionMajor", 0), + ByteField("revisionMinor", 0), + LEShortField("status", 0), + XLEIntField("serialNumber", 0), + ByteField("productNameLength", 0), + StrLenField("productName", None, + length_from=lambda pkt: pkt.productNameLength), + ByteField("state", 0) ] -class ENIPListIdentityReply(Packet): - """List Identity Command Field""" - name = "ENIPListIdentityReply" +class ENIPListIdentity(Packet): + """List identity request and response""" + name = "ENIPListIdentity" fields_desc = [ - FieldLenField("itemCount", 0, count_of="identityItems"), - PacketField("identityItems", None, ENIPListIdentityReplyItems), + FieldLenField("itemCount", 0, count_of="items"), + PacketListField("items", None, ENIPListIdentityItem) ] -class ENIPListServicesReplyItems(Packet): - """List Services Items Field""" - name = "ENIPListServicesReplyItems" +# List Interfaces (0x0064) + + +class ENIPListInterfacesItem(Packet): + """List Interfaces Item Fields""" + name = "ENIPListInterfacesItem" fields_desc = [ - LEIntField("itemTypeCode", 0), - LEIntField("itemLength", 0), - ByteField("version", 1), - ByteField("flag", 0), - StrFixedLenField("serviceName", None, 16 * 4), + LEShortEnumField("itemTypeCode", 0, _typeIdList), + FieldLenField("itemLength", 0, length_of="itemData"), + # TODO: Could be detailed + StrLenField("itemData", "", length_from=lambda pkt: pkt.itemLength), ] -class ENIPListServicesReply(Packet): - """List Services Command Field""" - name = "ENIPListServicesReply" +class ENIPListInterfaces(Packet): + """List Interfaces Command Field""" + name = "ENIPListInterfaces" fields_desc = [ - FieldLenField("itemCount", 0, count_of="identityItems"), - PacketField("targetItems", None, ENIPListServicesReplyItems), + FieldLenField("itemCount", 0, count_of="items"), + PacketListField("items", None, ENIPListInterfacesItem), ] -class ENIPRegisterSession(CommandSpecificData): +# Register Session (0x0065) + + +class ENIPRegisterSession(Packet): """Register Session Command Field""" name = "ENIPRegisterSession" fields_desc = [ @@ -165,6 +234,47 @@ class ENIPRegisterSession(CommandSpecificData): ] +# Unregister Session (0x0066) -- Requires further testing + + +class ENIPUnregisterSession(Packet): + """Unregister Session Command Field""" + name = "ENIPUnregisterSession" + pass + + +# Send RR Data (0x006f) + + +class ENIPSendRRData(Packet): + """Send RR Data Command Field""" + name = "ENIPSendRRData" + fields_desc = [ + LEIntEnumField("interface", 0, _interfaceList), + LEShortField("timeout", 0xff), + LEFieldLenField("itemCount", 0, count_of="items"), + PacketListField("items", None, ItemData) + # TODO: Send RR Data is usually followed by a CIP packet + ] + + +# Send Unit Data (0x006f) + + +class ENIPSendUnitData(Packet): + """Send Unit Data Command Field""" + name = "ENIPSendUnitData" + fields_desc = [ + LEIntEnumField("interface", 0, _interfaceList), + LEShortField("timeout", 0xff), + LEFieldLenField("itemCount", 0, count_of="items"), + PacketListField("items", None, ItemData) + ] + + +# Main Ethernet/IP packet structure with header + + class ENIPTCP(Packet): """Ethernet/IP packet over TCP""" name = "ENIPTCP" @@ -175,38 +285,6 @@ class ENIPTCP(Packet): LEIntEnumField("status", None, _statusList), LELongField("senderContext", 0), LEIntField("options", 0), - MultipleTypeField( - [ - # List Services Reply - (PacketField("commandSpecificData", ENIPListServicesReply, - ENIPListServicesReply), - lambda pkt: pkt.commandId == 0x4), - # List Identity Reply - (PacketField("commandSpecificData", ENIPListIdentityReply, - ENIPListIdentityReply), - lambda pkt: pkt.commandId == 0x63), - # List Interfaces Reply - (PacketField("commandSpecificData", ENIPListInterfacesReply, - ENIPListInterfacesReply), - lambda pkt: pkt.commandId == 0x64), - # Register Session - (PacketField("commandSpecificData", ENIPRegisterSession, - ENIPRegisterSession), - lambda pkt: pkt.commandId == 0x65), - # Send RR Data - (PacketField("commandSpecificData", ENIPSendRRData, - ENIPSendRRData), - lambda pkt: pkt.commandId == 0x6f), - # Send Unit Data - (PacketField("commandSpecificData", ENIPSendUnitData, - ENIPSendUnitData), - lambda pkt: pkt.commandId == 0x70), - ], - PacketField( - "commandSpecificData", - None, - CommandSpecificData) # By default - ), ] def post_build(self, pkt, pay): @@ -217,3 +295,12 @@ def post_build(self, pkt, pay): bind_layers(TCP, ENIPTCP, dport=44818) bind_layers(TCP, ENIPTCP, sport=44818) + +bind_layers(ENIPTCP, ENIPUnknownCommand, commandId=0x0001) +bind_layers(ENIPTCP, ENIPListServices, commandId=0x0004) +bind_layers(ENIPTCP, ENIPListIdentity, commandId=0x0063) +bind_layers(ENIPTCP, ENIPListInterfaces, commandId=0x0064) +bind_layers(ENIPTCP, ENIPRegisterSession, commandId=0x0065) +bind_layers(ENIPTCP, ENIPUnregisterSession, commandId=0x0066) +bind_layers(ENIPTCP, ENIPSendRRData, commandId=0x006f) +bind_layers(ENIPTCP, ENIPSendUnitData, commandId=0x0070) diff --git a/test/contrib/enipTCP.uts b/test/contrib/enipTCP.uts index 3c32c2ffdda..d2d820c669e 100644 --- a/test/contrib/enipTCP.uts +++ b/test/contrib/enipTCP.uts @@ -6,6 +6,7 @@ from scapy.contrib.enipTCP import * #from scapy.all import * + + Test ENIP/TCP Encapsulation Header = Encapsulation Header Default Values pkt=ENIPTCP() @@ -17,40 +18,44 @@ assert pkt.senderContext == 0 assert pkt.options == 0 -+ ENIP List Services ++ ENIP List Services 0x0004 = ENIP List Services Reply Command ID pkt=ENIPTCP() pkt.commandId=0x4 assert pkt.commandId == 0x4 -= ENIP List Services Reply Default Values -pkt=pkt/ENIPListServicesReply() -assert pkt[ENIPListServicesReply].itemCount == 0 += ENIP List Services Default Values +pkt=ENIPListServices() +assert pkt.itemCount == 0 -= ENIP List Services Reply Items Default Values -pkt=pkt/ENIPListServicesReplyItems() -assert pkt[ENIPListServicesReplyItems].itemTypeCode == 0 -assert pkt[ENIPListServicesReplyItems].itemLength == 0 -assert pkt[ENIPListServicesReplyItems].version == 1 -assert pkt[ENIPListServicesReplyItems].flag == 0 -assert pkt[ENIPListServicesReplyItems].serviceName == None += ENIP List Services Custom Values +pkt.items.append(ENIPListServicesItem(serviceName=b'test')) +assert pkt.items[0].itemTypeCode == 0 +assert pkt.items[0].itemLength == 0 +assert pkt.items[0].protocolVersion == 0 +assert pkt.items[0].flag == 0 +assert pkt.items[0].serviceName == b'test' -+ ENIP List Identity ++ ENIP List Identity 0x0063 = ENIP List Identity Reply Command ID pkt=ENIPTCP() pkt.commandId=0x63 assert pkt.commandId == 0x63 +assert raw(pkt) == b"c\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ +b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" -= ENIP List Identity Reply Default Values -pkt=pkt/ENIPListIdentityReply() -assert pkt[ENIPListIdentityReply].itemCount == 0 += ENIP List Identity Default Values +pkt=ENIPListIdentity() +assert pkt.itemCount == 0 -= ENIP List Identity Reply Items Default Values -pkt=pkt/ENIPListIdentityReplyItems() -assert pkt[ENIPListIdentityReplyItems].itemTypeCode == 0 -assert pkt[ENIPListIdentityReplyItems].itemLength == 0 -assert pkt[ENIPListIdentityReplyItems].itemData == b'' += ENIP List Identity Custom Values +pkt=ENIPListIdentityItem(sinAddress="192.168.1.1", + productNameLength=4, productName=b"test") +assert pkt.protocolVersion == 0 +assert pkt.sinAddress == "192.168.1.1" +assert pkt.productNameLength == 4 +assert pkt.productName == b'test' + ENIP List Interfaces @@ -60,14 +65,15 @@ pkt.commandId=0x64 assert pkt.commandId == 0x64 = ENIP List Interfaces Reply Default Values -pkt=pkt/ENIPListInterfacesReply() -assert pkt[ENIPListInterfacesReply].itemCount == 0 +pkt=ENIPListInterfaces() +assert pkt.itemCount == 0 = ENIP List Interfaces Reply Items Default Values -pkt=pkt/ENIPListInterfacesReplyItems() -assert pkt[ENIPListInterfacesReplyItems].itemTypeCode == 0 -assert pkt[ENIPListInterfacesReplyItems].itemLength == 0 -assert pkt[ENIPListInterfacesReplyItems].itemData == b'' +pkt=ENIPListInterfacesItem(itemTypeCode=0x0c) +assert pkt.itemTypeCode == 0x0c +assert pkt.itemLength == 0 +assert pkt.itemData == b'' + + ENIP Register Session = ENIP Register Session Command ID @@ -76,14 +82,13 @@ pkt.commandId=0x65 assert pkt.commandId == 0x65 = ENIP Register Session Default Values -pkt=pkt/ENIPRegisterSession() -assert pkt[ENIPRegisterSession].protocolVersion == 1 -assert pkt[ENIPRegisterSession].options == 0 +pkt=ENIPRegisterSession() +assert pkt.protocolVersion == 1 +assert pkt.options == 0 = ENIP Register Session Request registerSessionReqPkt = b'\x65\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00' - pkt = ENIPTCP(registerSessionReqPkt) assert pkt.commandId == 0x65 assert pkt.length == 4 @@ -106,6 +111,7 @@ assert pkt.senderContext == 0 assert pkt.options == 0 assert pkt[ENIPRegisterSession].protocolVersion == 1 assert pkt[ENIPRegisterSession].options == 0 +raw(pkt) + ENIP Send RR Data @@ -115,10 +121,10 @@ pkt.commandId=0x6f assert pkt.commandId == 0x6f = ENIP Send RR Data Default Values -pkt=pkt/ENIPSendRRData() -assert pkt[ENIPSendRRData].interfaceHandle == 0 -assert pkt[ENIPSendRRData].timeout == 0 -assert pkt[ENIPSendRRData].encapsulatedPacket == None +pkt=ENIPSendRRData() +assert pkt.interface == 0 +assert pkt.timeout == 255 +assert pkt.itemCount == 0 = ENIP Send RR Data Request sendRRDataReqPkt = b'\x6f\x00\x3e\x00\x7b\x9a\x4e\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\xb2\x00\x2e\x00' @@ -129,10 +135,9 @@ assert pkt.session == 0xa14e9a7b assert pkt.status == 0 assert pkt.senderContext == 0 assert pkt.options == 0 -assert pkt[ENIPSendRRData].interfaceHandle == 0 -assert pkt[ENIPSendRRData].timeout == 0 -assert pkt[EncapsulatedPacket].itemCount == 2 - +assert pkt.interface == 0 +assert pkt.timeout == 0 +assert pkt.itemCount == 2 = ENIP Send RR Data Reply sendRRDataRepPkt = b'\x6f\x00\x2e\x00\x7b\x9a\x4e\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x02\x00\x00\x00\x00\x00\xb2\x00\x1e\x00' @@ -144,12 +149,13 @@ assert pkt.session == 0xa14e9a7b assert pkt.status == 0 assert pkt.senderContext == 0 assert pkt.options == 0 -assert pkt[ENIPSendRRData].interfaceHandle == 0 -assert pkt[ENIPSendRRData].timeout == 1024 -assert pkt[EncapsulatedPacket].item[0].typeId == 0 -assert pkt[EncapsulatedPacket].item[0].length == 0 -assert pkt[EncapsulatedPacket].item[1].typeId == 0x00b2 -assert pkt[EncapsulatedPacket].item[1].length == 30 +assert pkt.interface == 0 +assert pkt.timeout == 1024 +assert pkt.items[0].typeId == 0 +assert pkt.items[0].length == 0 +assert pkt.items[1].typeId == 0x00b2 +assert pkt.items[1].length == 30 + + ENIP Send Unit Data = ENIP Send Unit Data Command ID @@ -158,16 +164,14 @@ pkt.commandId=0x70 assert pkt.commandId == 0x70 = ENIP Send Unit Data Default Values -pkt=pkt/ENIPSendUnitData() -assert pkt[ENIPSendUnitData].interfaceHandle == 0 -assert pkt[ENIPSendUnitData].timeout == 0 -assert pkt[ENIPSendUnitData].encapsulatedPacket == None - +pkt=ENIPSendUnitData() +assert pkt.interface == 0 +assert pkt.timeout == 255 +assert pkt.itemCount == 0 = ENIP Send Unit Data sendUnitDataPkt = b'\x70\x00\x2d\x00\x7b\x9a\x4e\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xa1\x00\x04\x00\xcc\x60\x9a\x7b\xb1\x00\x19\x00\x01\x00' - pkt = ENIPTCP(sendUnitDataPkt) assert pkt.commandId == 0x70 assert pkt.length == 45 @@ -175,19 +179,13 @@ assert pkt.session == 0xa14e9a7b assert pkt.status == 0 assert pkt.senderContext == 0 assert pkt.options == 0 -assert pkt[ENIPSendUnitData].interfaceHandle == 0 -assert pkt[ENIPSendUnitData].timeout == 0 -assert pkt[EncapsulatedPacket].itemCount == 2 - -assert pkt[EncapsulatedPacket].item[0].typeId == 0x00a1 -assert pkt[EncapsulatedPacket].item[0].length == 4 -assert pkt[EncapsulatedPacket].item[0].data == b'\x7b\x9a\x60\xcc' -assert pkt[EncapsulatedPacket].item[1].typeId == 0x00b1 -assert pkt[EncapsulatedPacket].item[1].length == 25 -assert pkt[EncapsulatedPacket].item[1].data == b'\x00\x01' - - - - - - +assert pkt.interface == 0 +assert pkt.timeout == 0 +assert pkt.itemCount == 2 + +assert pkt.items[0].typeId == 0x00a1 +assert pkt.items[0].length == 4 +assert pkt.items[0].data == b'\x7b\x9a\x60\xcc' +assert pkt.items[1].typeId == 0x00b1 +assert pkt.items[1].length == 25 +assert pkt.items[1].data == b'\x00\x01' From a8d2bb7d1ad68b3442533fecb0510b3c02316950 Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Mon, 7 Aug 2023 19:59:28 +0300 Subject: [PATCH 045/122] [LLMNR] recognize the "T"entative bit (#4089) According to https://datatracker.ietf.org/doc/html/rfc4795#section-2.1.1 the TC bit is followed by the T bit: ``` 1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ID | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |QR| Opcode | C|TC| T| Z| Z| Z| Z| RCODE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ ``` where the 'T'entative bit is set in a response if the responder is authoritative for the name, but has not yet verified the uniqueness of the name. This patch splits the 2-bit "TC" field into two 1-bit fields corresponding to the "TC" and "T" bits. The test is added to make sure that tentative responses can be dissected. It was also cross-checked with Wireshark: ``` Link-local Multicast Name Resolution (response) Transaction ID: 0x87df Flags: 0x8100 Standard query response, No error 1... .... .... .... = Response: Message is a response .000 0... .... .... = Opcode: Standard query (0) .... .0.. .... .... = Conflict: The name is considered unique .... ..0. .... .... = Truncated: Message is not truncated .... ...1 .... .... = Tentative: Tentative .... .... .... 0000 = Reply code: No error (0) ``` --- scapy/layers/llmnr.py | 3 ++- test/scapy/layers/llmnr.uts | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/scapy/layers/llmnr.py b/scapy/layers/llmnr.py index c393d1f3109..89815e5f200 100644 --- a/scapy/layers/llmnr.py +++ b/scapy/layers/llmnr.py @@ -37,7 +37,8 @@ class LLMNRQuery(DNSCompressedPacket): BitField("qr", 0, 1), BitEnumField("opcode", 0, 4, {0: "QUERY"}), BitField("c", 0, 1), - BitField("tc", 0, 2), + BitField("tc", 0, 1), + BitField("t", 0, 1), BitField("z", 0, 4) ] + DNS.fields_desc[-9:] overload_fields = {UDP: {"sport": 5355, "dport": 5355}} diff --git a/test/scapy/layers/llmnr.uts b/test/scapy/layers/llmnr.uts index 2da7a907859..6eff7fc98fe 100644 --- a/test/scapy/layers/llmnr.uts +++ b/test/scapy/layers/llmnr.uts @@ -10,12 +10,17 @@ assert pkt.sport == 5355 assert pkt.dport == 5355 assert pkt[LLMNRQuery].opcode == 0 += Dissection with the "T"entative bit set and the "TrunCation" bit unset +r = LLMNRResponse(b'\x87\xdf\x81\x00\x00\x01\x00\x01\x00\x00\x00\x00\x01C\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x1e\x00\x04\xc0\xa8-\x15') +assert r.tc == 0 and r.t == 1 + = Packet build / dissection pkt = UDP(raw(UDP()/LLMNRResponse())) assert LLMNRResponse in pkt assert pkt.qr == 1 assert pkt.c == 0 assert pkt.tc == 0 +assert pkt.t == 0 assert pkt.z == 0 assert pkt.rcode == 0 assert pkt.qdcount == 0 From 2fe5cee7b2fdca940b1774ee26a4948e88417232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20V=C3=A1zquez=20Blanco?= Date: Fri, 21 Jul 2023 22:00:27 +0200 Subject: [PATCH 046/122] bluetooth: Add a BT monitor header for pcap parsing --- scapy/data.py | 1 + scapy/layers/bluetooth.py | 20 ++++++++++++++++---- test/scapy/layers/bluetooth.uts | 8 ++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/scapy/data.py b/scapy/data.py index 7e7f4ab8a7c..9e83fee05fb 100644 --- a/scapy/data.py +++ b/scapy/data.py @@ -128,6 +128,7 @@ DLT_NETLINK = 253 DLT_USB_DARWIN = 266 DLT_BLUETOOTH_LE_LL = 251 +DLT_BLUETOOTH_LINUX_MONITOR = 254 DLT_BLUETOOTH_LE_LL_WITH_PHDR = 256 DLT_VSOCK = 271 DLT_ETHERNET_MPACKET = 274 diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index 8411d3b38ec..31438ce103e 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -17,7 +17,11 @@ from ctypes import sizeof from scapy.config import conf -from scapy.data import DLT_BLUETOOTH_HCI_H4, DLT_BLUETOOTH_HCI_H4_WITH_PHDR +from scapy.data import ( + DLT_BLUETOOTH_HCI_H4, + DLT_BLUETOOTH_HCI_H4_WITH_PHDR, + DLT_BLUETOOTH_LINUX_MONITOR +) from scapy.packet import bind_layers, Packet from scapy.fields import ( BitField, @@ -34,6 +38,7 @@ NBytesField, PacketListField, PadField, + ShortField, SignedByteField, StrField, StrFixedLenField, @@ -188,9 +193,6 @@ class HCI_PHDR_Hdr(Packet): class BT_Mon_Hdr(Packet): - ''' - Bluetooth Linux Monitor Transport Header - ''' name = 'Bluetooth Linux Monitor Transport Header' fields_desc = [ LEShortField('opcode', None), @@ -199,6 +201,15 @@ class BT_Mon_Hdr(Packet): ] +# https://www.tcpdump.org/linktypes/LINKTYPE_BLUETOOTH_LINUX_MONITOR.html +class BT_Mon_Pcap_Hdr(BT_Mon_Hdr): + name = 'Bluetooth Linux Monitor Transport Pcap Header' + fields_desc = [ + ShortField('adapter_id', None), + ShortField('opcode', None) + ] + + class HCI_Hdr(Packet): name = "HCI header" fields_desc = [ByteEnumField("type", 2, _bluetooth_packet_types)] @@ -1277,6 +1288,7 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): conf.l2types.register(DLT_BLUETOOTH_HCI_H4, HCI_Hdr) conf.l2types.register(DLT_BLUETOOTH_HCI_H4_WITH_PHDR, HCI_PHDR_Hdr) +conf.l2types.register(DLT_BLUETOOTH_LINUX_MONITOR, BT_Mon_Pcap_Hdr) bind_layers(HCI_Command_Hdr, HCI_Cmd_Create_Connection, opcode=0x0405) bind_layers(HCI_Command_Hdr, HCI_Cmd_Disconnect, opcode=0x0406) diff --git a/test/scapy/layers/bluetooth.uts b/test/scapy/layers/bluetooth.uts index 43f55ac66cc..c103fb0081b 100644 --- a/test/scapy/layers/bluetooth.uts +++ b/test/scapy/layers/bluetooth.uts @@ -431,3 +431,11 @@ assert r == b'\rscapy\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' p = SM_Hdr(r) assert SM_DHKey_Check in p and p.dhkey_check[:5] == b"scapy" + + += Bluetooth Monitor Pcap Header + +p = BT_Mon_Pcap_Hdr(hex_bytes("00000008")) +assert BT_Mon_Pcap_Hdr in p +assert p[BT_Mon_Pcap_Hdr].adapter_id == 0 +assert p[BT_Mon_Pcap_Hdr].opcode == 8 From 62f398cc2b730dc01287fc2c7d9e469c65007761 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Tue, 22 Aug 2023 04:44:54 +0200 Subject: [PATCH 047/122] Add connect_from_ip command (#4098) connect_from_ip creates a tcp socket that spoofs another IP. --- scapy/automaton.py | 27 +++++++-------- scapy/layers/inet.py | 81 +++++++++++++++++++++++++++++++++++++++++--- scapy/layers/l2.py | 7 +++- scapy/supersocket.py | 1 + 4 files changed, 96 insertions(+), 20 deletions(-) diff --git a/scapy/automaton.py b/scapy/automaton.py index 15179e306bb..a12fb6953c5 100644 --- a/scapy/automaton.py +++ b/scapy/automaton.py @@ -16,6 +16,7 @@ import logging import os import random +import socket import sys import threading import time @@ -77,7 +78,7 @@ def select_objects(inputs, remain): [b] :param inputs: objects to process - :param remain: timeout. If 0, return []. + :param remain: timeout. If 0, poll. """ if not WINDOWS: return select.select(inputs, [], [], remain)[0] @@ -901,35 +902,33 @@ def __init__(self, wr # type: Union[int, ObjectPipe[bytes], None] ): # type: (...) -> None - if rd is not None and not isinstance(rd, (int, ObjectPipe)): - rd = rd.fileno() - if wr is not None and not isinstance(wr, (int, ObjectPipe)): - wr = wr.fileno() self.rd = rd self.wr = wr + if isinstance(self.rd, socket.socket): + self.__selectable_force_select__ = True def fileno(self): # type: () -> int - if isinstance(self.rd, ObjectPipe): - return self.rd.fileno() - elif isinstance(self.rd, int): + if isinstance(self.rd, int): return self.rd + elif self.rd: + return self.rd.fileno() return 0 def read(self, n=65535): # type: (int) -> Optional[bytes] - if isinstance(self.rd, ObjectPipe): - return self.rd.recv(n) - elif isinstance(self.rd, int): + if isinstance(self.rd, int): return os.read(self.rd, n) + elif self.rd: + return self.rd.recv(n) return None def write(self, msg): # type: (bytes) -> int - if isinstance(self.wr, ObjectPipe): - return self.wr.send(msg) - elif isinstance(self.wr, int): + if isinstance(self.wr, int): return os.write(self.wr, msg) + elif self.wr: + return self.wr.send(msg) return 0 def recv(self, n=65535): diff --git a/scapy/layers/inet.py b/scapy/layers/inet.py index 95c07073800..ad12ce82515 100644 --- a/scapy/layers/inet.py +++ b/scapy/layers/inet.py @@ -21,8 +21,16 @@ from scapy.base_classes import Gen, Net from scapy.data import ETH_P_IP, ETH_P_ALL, DLT_RAW, DLT_RAW_ALT, DLT_IPV4, \ IP_PROTOS, TCP_SERVICES, UDP_SERVICES -from scapy.layers.l2 import Ether, Dot3, getmacbyip, CookedLinux, GRE, SNAP, \ - Loopback +from scapy.layers.l2 import ( + CookedLinux, + Dot3, + Ether, + GRE, + Loopback, + SNAP, + arpcachepoison, + getmacbyip, +) from scapy.compat import raw, chb, orb, bytes_encode, Optional from scapy.config import conf from scapy.fields import ( @@ -1888,15 +1896,18 @@ class TCP_client(Automaton): :param ip: the ip to connect to :param port: + :param src: (optional) use another source IP """ - def parse_args(self, ip, port, *args, **kargs): + def parse_args(self, ip, port, srcip=None, **kargs): from scapy.sessions import TCPSession self.dst = str(Net(ip)) self.dport = port self.sport = random.randrange(0, 2**16) - self.l4 = IP(dst=ip) / TCP(sport=self.sport, dport=self.dport, flags=0, - seq=random.randrange(0, 2**32)) + self.l4 = IP(dst=ip, src=srcip) / TCP( + sport=self.sport, dport=self.dport, + flags=0, seq=random.randrange(0, 2**32) + ) self.src = self.l4.src self.sack = self.l4[TCP].ack self.rel_seq = None @@ -2160,6 +2171,66 @@ def fragleak2(target, timeout=0.4, onlyasc=0, count=None): pass +@conf.commands.register +class connect_from_ip: + """ + Open a TCP socket to a host:port while spoofing another IP. + + :param host: the host to connect to + :param port: the port to connect to + :param srcip: the IP to spoof. the cache of the gateway will + be poisonned with this IP. + :param poison: (optional, default True) ARP poison the gateway (or next hop), + so that it answers us. + :param timeout: (optional) the socket timeout. + + Example - Connect to 192.168.0.1:80 spoofing 192.168.0.2:: + + from scapy.layers.http import HTTP, HTTPRequest + client = connect_from_ip("192.168.0.1", 80, "192.168.0.2") + sock = SSLStreamSocket(client.sock, HTTP) + resp = sock.sr1(HTTP() / HTTPRequest(Path="/")) + + Example - Connect to 192.168.0.1:443 with TLS wrapping spoofing 192.168.0.2:: + + import ssl + from scapy.layers.http import HTTP, HTTPRequest + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + client = connect_from_ip("192.168.0.1", 443, "192.168.0.2") + sock = context.wrap_socket(client.sock) + sock = SSLStreamSocket(client.sock, HTTP) + resp = sock.sr1(HTTP() / HTTPRequest(Path="/")) + """ + + def __init__(self, host, port, srcip, poison=True, timeout=1): + host = str(Net(host)) + # poison the next hop + if poison: + gateway = conf.route.route(host)[2] + if gateway == "0.0.0.0": + # on lan + gateway = host + arpcachepoison(gateway, srcip, count=1, interval=0, verbose=0) + # create a socket pair + self._sock, self.sock = socket.socketpair() + self.sock.settimeout(timeout) + self.client = TCP_client( + host, port, + srcip=srcip, + external_fd={"tcp": self._sock}, + ) + # start the TCP_client + self.client.runbg() + + def close(self): + self.client.stop() + self.client.destroy() + self.sock.close() + self._sock.close() + + class ICMPEcho_am(AnsweringMachine): """Responds to ICMP Echo-Requests (ping)""" function_name = "icmpechod" diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py index ea3e05d808c..5ac5db7ce91 100644 --- a/scapy/layers/l2.py +++ b/scapy/layers/l2.py @@ -773,7 +773,9 @@ def arpcachepoison( target, # type: Union[str, List[str]] addresses, # type: Union[str, Tuple[str, str], List[Tuple[str, str]]] broadcast=False, # type: bool + count=None, # type: Optional[int] interval=15, # type: int + **kwargs, # type: Any ): # type: (...) -> None """Poison targets' ARP cache @@ -815,9 +817,12 @@ def arpcachepoison( hwsrc=y, hwdst="00:00:00:00:00:00") for x, y in couple_list ] + if count is not None: + sendp(p, iface_hint=str_target, count=count, inter=interval, **kwargs) + return try: while True: - sendp(p, iface_hint=str_target) + sendp(p, iface_hint=str_target, **kwargs) time.sleep(interval) except KeyboardInterrupt: pass diff --git a/scapy/supersocket.py b/scapy/supersocket.py index 32526d78476..eff4589cf55 100644 --- a/scapy/supersocket.py +++ b/scapy/supersocket.py @@ -373,6 +373,7 @@ def send(self, x): class SimpleSocket(SuperSocket): desc = "wrapper around a classic socket" + __selectable_force_select__ = True def __init__(self, sock): # type: (socket.socket) -> None From 3707afdaa851dc661904314787a0101d12a50a5c Mon Sep 17 00:00:00 2001 From: superuserx Date: Thu, 24 Aug 2023 09:51:25 +0200 Subject: [PATCH 048/122] Update uds.py (#4102) --- scapy/contrib/automotive/uds.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 0da2c18bef2..74952f4d884 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -1334,6 +1334,7 @@ class UDS_NR(Packet): 0x26: 'failurePreventsExecutionOfRequestedAction', 0x31: 'requestOutOfRange', 0x33: 'securityAccessDenied', + 0x34: 'authenticationRequired', 0x35: 'invalidKey', 0x36: 'exceedNumberOfAttempts', 0x37: 'requiredTimeDelayNotExpired', From 8eab848c4b3820c3792d547b637ded43e3ad6155 Mon Sep 17 00:00:00 2001 From: Hui Peng Date: Fri, 25 Aug 2023 00:40:19 -0700 Subject: [PATCH 049/122] Improve Bluetooth HCI Command packet definition (#4088) * Improve Bluetooth HCI Command packet definition - Divide opcode into ogf and ocf following Core Spec - Redefine existing HCI commands with new format * Fix some issues in existing hci command packets 1. Fix some outdated command definitions 2. Fix the names of commands, use the formal name from core spec 3. Fix the some of the tests --- scapy/layers/bluetooth.py | 398 +++++++++++++++++++++----------- test/scapy/layers/bluetooth.uts | 184 +++++++++++---- 2 files changed, 398 insertions(+), 184 deletions(-) diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index 31438ce103e..cca1d5a9086 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -25,6 +25,7 @@ from scapy.packet import bind_layers, Packet from scapy.fields import ( BitField, + XBitField, ByteEnumField, ByteField, FieldLenField, @@ -45,6 +46,7 @@ StrLenField, UUIDField, XByteField, + XLE3BytesField, XLELongField, XStrLenField, XLEShortField, @@ -898,77 +900,54 @@ def extract_padding(self, s): class HCI_Command_Hdr(Packet): name = "HCI Command header" - fields_desc = [XLEShortField("opcode", 0), + fields_desc = [XBitField("ogf", 0, 6, tot_size=-2), + XBitField("ocf", 0, 10, end_tot_size=-2), LenField("len", None, fmt="B"), ] def answers(self, other): return False + @property + def opcode(self): + return (self.ogf << 10) + self.ocf + def post_build(self, p, pay): p += pay if self.len is None: p = p[:2] + struct.pack("B", len(pay)) + p[3:] return p - -class HCI_Cmd_Reset(Packet): - name = "Reset" - - -class HCI_Cmd_Set_Event_Filter(Packet): - name = "Set Event Filter" - fields_desc = [ByteEnumField("type", 0, {0: "clear"}), ] - - -class HCI_Cmd_Connect_Accept_Timeout(Packet): - name = "Connection Attempt Timeout" - fields_desc = [LEShortField("timeout", 32000)] # 32000 slots is 20000 msec - - -class HCI_Cmd_LE_Host_Supported(Packet): - name = "LE Host Supported" - fields_desc = [ByteField("supported", 1), - ByteField("simultaneous", 1), ] - - -class HCI_Cmd_Set_Event_Mask(Packet): - name = "Set Event Mask" - fields_desc = [StrFixedLenField("mask", b"\xff\xff\xfb\xff\x07\xf8\xbf\x3d", 8)] # noqa: E501 - - -class HCI_Cmd_Read_BD_Addr(Packet): - name = "Read BD Addr" +# BLUETOOTH CORE SPECIFICATION Version 5.4 | Vol 4, Part E +# 7 HCI COMMANDS AND EVENTS +# 7.1 LINK CONTROL COMMANDS, the OGF is defined as 0x01 -class HCI_Cmd_Write_Local_Name(Packet): - name = "Write Local Name" - fields_desc = [StrField("name", "")] +class HCI_Cmd_Inquiry(Packet): + name = "HCI_Inquiry" + fields_desc = [XLE3BytesField("lap", 0x9E8B33), + ByteField("inquiry_length", 0), + ByteField("num_responses", 0)] -class HCI_Cmd_Write_Extended_Inquiry_Response(Packet): - name = "Write Extended Inquiry Response" - fields_desc = [ByteField("fec_required", 0), - PacketListField("eir_data", [], EIR_Hdr, - length_from=lambda pkt:pkt.len)] +class HCI_Cmd_Inquiry_Cancel(Packet): + name = "HCI_Inquiry_Cancel" -class HCI_Cmd_LE_Set_Scan_Parameters(Packet): - name = "LE Set Scan Parameters" - fields_desc = [ByteEnumField("type", 1, {1: "active"}), - XLEShortField("interval", 16), - XLEShortField("window", 16), - ByteEnumField("atype", 0, {0: "public"}), - ByteEnumField("policy", 0, {0: "all", 1: "whitelist"})] +class HCI_Cmd_Periodic_Inquiry_Mode(Packet): + name = "HCI_Periodic_Inquiry_Mode" + fields_desc = [LEShortField("max_period_length", 0x0003), + LEShortField("min_period_length", 0x0002), + XLE3BytesField("lap", 0x9E8B33), + ByteField("inquiry_length", 0), + ByteField("num_responses", 0)] -class HCI_Cmd_LE_Set_Scan_Enable(Packet): - name = "LE Set Scan Enable" - fields_desc = [ByteField("enable", 1), - ByteField("filter_dups", 1), ] +class HCI_Cmd_Exit_Peiodic_Inquiry_Mode(Packet): + name = "HCI_Exit_Periodic_Inquiry_Mode" class HCI_Cmd_Create_Connection(Packet): - name = "Create Connection" + name = "HCI_Create_Connection" fields_desc = [LEMACField("bd_addr", None), LEShortField("packet_type", 0xcc18), ByteField("page_scan_repetition_mode", 0x02), @@ -978,100 +957,136 @@ class HCI_Cmd_Create_Connection(Packet): class HCI_Cmd_Disconnect(Packet): - name = "Disconnect" + name = "HCI_Disconnect" fields_desc = [XLEShortField("handle", 0), ByteField("reason", 0x13), ] class HCI_Cmd_Link_Key_Request_Reply(Packet): - name = "Link Key Request Reply" + name = "HCI_Link_Key_Request_Reply" fields_desc = [LEMACField("bd_addr", None), NBytesField("link_key", None, 16), ] class HCI_Cmd_Authentication_Requested(Packet): - name = "Authentication Requested" + name = "HCI_Authentication_Requested" fields_desc = [LEShortField("handle", 0)] class HCI_Cmd_Set_Connection_Encryption(Packet): - name = "Set Connection Encryption" + name = "HCI_Set_Connection_Encryption" fields_desc = [LEShortField("handle", 0), ByteField("encryption_enable", 0)] class HCI_Cmd_Remote_Name_Request(Packet): - name = "Remote Name Request" + name = "HCI_Remote_Name_Request" fields_desc = [LEMACField("bd_addr", None), ByteField("page_scan_repetition_mode", 0x02), ByteField("reserved", 0x0), LEShortField("clock_offset", 0x0), ] -class HCI_Cmd_LE_Create_Connection(Packet): - name = "LE Create Connection" - fields_desc = [LEShortField("interval", 96), - LEShortField("window", 48), - ByteEnumField("filter", 0, {0: "address"}), - ByteEnumField("patype", 0, {0: "public", 1: "random"}), - LEMACField("paddr", None), - ByteEnumField("atype", 0, {0: "public", 1: "random"}), - LEShortField("min_interval", 40), - LEShortField("max_interval", 56), - LEShortField("latency", 0), - LEShortField("timeout", 42), - LEShortField("min_ce", 0), - LEShortField("max_ce", 0), ] +# 7.2 Link Policy commands, the OGF is defined as 0x02 +class HCI_Cmd_Hold_Mode(Packet): + name = "HCI_Hold_Mode" + fields_desc = [LEShortField("connection_handle", 0), + LEShortField("hold_mode_max_interval", 0x0002), + LEShortField("hold_mode_min_interval", 0x0002), ] -class HCI_Cmd_LE_Create_Connection_Cancel(Packet): - name = "LE Create Connection Cancel" +# 7.3 CONTROLLER & BASEBAND COMMANDS, the OGF is defined as 0x03 +class HCI_Cmd_Set_Event_Mask(Packet): + name = "HCI_Set_Event_Mask" + fields_desc = [StrFixedLenField("mask", b"\xff\xff\xfb\xff\x07\xf8\xbf\x3d", 8)] # noqa: E501 -class HCI_Cmd_LE_Read_White_List_Size(Packet): - name = "LE Read White List Size" +class HCI_Cmd_Reset(Packet): + name = "HCI_Reset" -class HCI_Cmd_LE_Clear_White_List(Packet): - name = "LE Clear White List" +class HCI_Cmd_Set_Event_Filter(Packet): + name = "HCI_Set_Event_Filter" + fields_desc = [ByteEnumField("type", 0, {0: "clear"}), ] -class HCI_Cmd_LE_Add_Device_To_White_List(Packet): - name = "LE Add Device to White List" - fields_desc = [ByteEnumField("atype", 0, {0: "public", 1: "random"}), - LEMACField("address", None)] + +class HCI_Cmd_Write_Local_Name(Packet): + name = "HCI_Write_Local_Name" + fields_desc = [StrField("name", "")] -class HCI_Cmd_LE_Remove_Device_From_White_List(HCI_Cmd_LE_Add_Device_To_White_List): # noqa: E501 - name = "LE Remove Device from White List" +class HCI_Cmd_Write_Connect_Accept_Timeout(Packet): + name = "HCI_Write_Connection_Accept_Timeout" + fields_desc = [LEShortField("timeout", 32000)] # 32000 slots is 20000 msec -class HCI_Cmd_LE_Connection_Update(Packet): - name = "LE Connection Update" - fields_desc = [XLEShortField("handle", 0), - XLEShortField("min_interval", 0), - XLEShortField("max_interval", 0), - XLEShortField("latency", 0), - XLEShortField("timeout", 0), - LEShortField("min_ce", 0), - LEShortField("max_ce", 0xffff), ] +class HCI_Cmd_Write_Extended_Inquiry_Response(Packet): + name = "HCI_Write_Extended_Inquiry_Response" + fields_desc = [ByteField("fec_required", 0), + PacketListField("eir_data", [], EIR_Hdr, + length_from=lambda pkt:pkt.len)] -class HCI_Cmd_LE_Read_Buffer_Size(Packet): - name = "LE Read Buffer Size" +class HCI_Cmd_Read_LE_Host_Support(Packet): + name = "HCI_Read_LE_Host_Support" -class HCI_Cmd_LE_Read_Remote_Used_Features(Packet): - name = "LE Read Remote Used Features" - fields_desc = [LEShortField("handle", 64)] +class HCI_Cmd_Write_LE_Host_Support(Packet): + name = "HCI_Write_LE_Host_Support" + fields_desc = [ByteField("supported", 1), + ByteField("unused", 1), ] + + +# 7.4 INFORMATIONAL PARAMETERS, the OGF is defined as 0x04 +class HCI_Cmd_Read_BD_Addr(Packet): + name = "HCI_Read_BD_ADDR" + +# 7.5 STATUS PARAMETERS, the OGF is defined as 0x05 + + +class HCI_Cmd_Read_Link_Quality(Packet): + name = "HCI_Read_Link_Quality" + fields_desc = [LEShortField("handle", 0)] + + +class HCI_Cmd_Read_RSSI(Packet): + name = "HCI_Read_RSSI" + fields_desc = [LEShortField("handle", 0)] + + +# 7.6 TESTING COMMANDS, the OGF is defined as 0x06 +class HCI_Cmd_Read_Loopback_Mode(Packet): + name = "HCI_Read_Loopback_Mode" + + +class HCI_Cmd_Write_Loopback_Mode(Packet): + name = "HCI_Write_Loopback_Mode" + fields_desc = [ByteEnumField("loopback_mode", 0, + {0: "no loopback", + 1: "enable local loopback", + 2: "enable remote loopback"})] + + +# 7.8 LE CONTROLLER COMMANDS, the OGF code is defined as 0x08 +class HCI_Cmd_LE_Read_Buffer_Size_V1(Packet): + name = "HCI_LE_Read_Buffer_Size [v1]" + + +class HCI_Cmd_LE_Read_Buffer_Size_V2(Packet): + name = "HCI_LE_Read_Buffer_Size [v2]" + + +class HCI_Cmd_LE_Read_Local_Supported_Features(Packet): + name = "HCI_LE_Read_Local_Supported_Features" class HCI_Cmd_LE_Set_Random_Address(Packet): - name = "LE Set Random Address" + name = "HCI_LE_Set_Random_Address" fields_desc = [LEMACField("address", None)] class HCI_Cmd_LE_Set_Advertising_Parameters(Packet): - name = "LE Set Advertising Parameters" + name = "HCI_LE_Set_Advertising_Parameters" fields_desc = [LEShortField("interval_min", 0x0800), LEShortField("interval_max", 0x0800), ByteEnumField("adv_type", 0, {0: "ADV_IND", 1: "ADV_DIRECT_IND", 2: "ADV_SCAN_IND", 3: "ADV_NONCONN_IND", 4: "ADV_DIRECT_IND_LOW"}), # noqa: E501 @@ -1083,7 +1098,7 @@ class HCI_Cmd_LE_Set_Advertising_Parameters(Packet): class HCI_Cmd_LE_Set_Advertising_Data(Packet): - name = "LE Set Advertising Data" + name = "HCI_LE_Set_Advertising_Data" fields_desc = [FieldLenField("len", None, length_of="data", fmt="B"), PadField( PacketListField("data", [], EIR_Hdr, @@ -1092,35 +1107,109 @@ class HCI_Cmd_LE_Set_Advertising_Data(Packet): class HCI_Cmd_LE_Set_Scan_Response_Data(Packet): - name = "LE Set Scan Response Data" + name = "HCI_LE_Set_Scan_Response_Data" fields_desc = [FieldLenField("len", None, length_of="data", fmt="B"), StrLenField("data", "", length_from=lambda pkt:pkt.len), ] class HCI_Cmd_LE_Set_Advertise_Enable(Packet): - name = "LE Set Advertise Enable" + name = "HCI_LE_Set_Advertising_Enable" fields_desc = [ByteField("enable", 0)] -class HCI_Cmd_LE_Start_Encryption_Request(Packet): - name = "LE Start Encryption" +class HCI_Cmd_LE_Set_Scan_Parameters(Packet): + name = "HCI_LE_Set_Scan_Parameters" + fields_desc = [ByteEnumField("type", 0, {0: "passive", 1: "active"}), + XLEShortField("interval", 16), + XLEShortField("window", 16), + ByteEnumField("atype", 0, {0: "public", + 1: "random", + 2: "rpa (pub)", + 3: "rpa (random)"}), + ByteEnumField("policy", 0, {0: "all", 1: "whitelist"})] + + +class HCI_Cmd_LE_Set_Scan_Enable(Packet): + name = "HCI_LE_Set_Scan_Enable" + fields_desc = [ByteField("enable", 1), + ByteField("filter_dups", 1), ] + + +class HCI_Cmd_LE_Create_Connection(Packet): + name = "HCI_LE_Create_Connection" + fields_desc = [LEShortField("interval", 96), + LEShortField("window", 48), + ByteEnumField("filter", 0, {0: "address"}), + ByteEnumField("patype", 0, {0: "public", 1: "random"}), + LEMACField("paddr", None), + ByteEnumField("atype", 0, {0: "public", 1: "random"}), + LEShortField("min_interval", 40), + LEShortField("max_interval", 56), + LEShortField("latency", 0), + LEShortField("timeout", 42), + LEShortField("min_ce", 0), + LEShortField("max_ce", 0), ] + + +class HCI_Cmd_LE_Create_Connection_Cancel(Packet): + name = "HCI_LE_Create_Connection_Cancel" + + +class HCI_Cmd_LE_Read_Filter_Accept_List_Size(Packet): + name = "HCI_LE_Read_Filter_Accept_List_Size" + + +class HCI_Cmd_LE_Clear_Filter_Accept_List(Packet): + name = "HCI_LE_Clear_Filter_Accept_List" + + +class HCI_Cmd_LE_Add_Device_To_Filter_Accept_List(Packet): + name = "HCI_LE_Add_Device_To_Filter_Accept_List" + fields_desc = [ByteEnumField("address_type", 0, {0: "public", + 1: "random", + 0xff: "anonymous"}), + LEMACField("address", None)] + + +class HCI_Cmd_LE_Remove_Device_From_Filter_Accept_List(HCI_Cmd_LE_Add_Device_To_Filter_Accept_List): # noqa: E501 + name = "HCI_LE_Remove_Device_From_Filter_Accept_List" + + +class HCI_Cmd_LE_Connection_Update(Packet): + name = "HCI_LE_Connection_Update" + fields_desc = [XLEShortField("handle", 0), + XLEShortField("min_interval", 0), + XLEShortField("max_interval", 0), + XLEShortField("latency", 0), + XLEShortField("timeout", 0), + LEShortField("min_ce", 0), + LEShortField("max_ce", 0xffff), ] + + +class HCI_Cmd_LE_Read_Remote_Features(Packet): + name = "HCI_LE_Read_Remote_Features" + fields_desc = [LEShortField("handle", 64)] + + +class HCI_Cmd_LE_Enable_Encryption(Packet): + name = "HCI_LE_Enable_Encryption" fields_desc = [LEShortField("handle", 0), StrFixedLenField("rand", None, 8), XLEShortField("ediv", 0), StrFixedLenField("ltk", b'\x00' * 16, 16), ] -class HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply(Packet): - name = "LE Long Term Key Request Negative Reply" - fields_desc = [LEShortField("handle", 0), ] - - class HCI_Cmd_LE_Long_Term_Key_Request_Reply(Packet): - name = "LE Long Term Key Request Reply" + name = "HCI_LE_Long_Term_Key_Request_Reply" fields_desc = [LEShortField("handle", 0), StrFixedLenField("ltk", b'\x00' * 16, 16), ] +class HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply(Packet): + name = "HCI_LE_Long_Term_Key_Request _Negative_Reply" + fields_desc = [LEShortField("handle", 0), ] + + class HCI_Event_Hdr(Packet): name = "HCI Event header" fields_desc = [XByteField("code", 0), @@ -1290,40 +1379,69 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): conf.l2types.register(DLT_BLUETOOTH_HCI_H4_WITH_PHDR, HCI_PHDR_Hdr) conf.l2types.register(DLT_BLUETOOTH_LINUX_MONITOR, BT_Mon_Pcap_Hdr) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Create_Connection, opcode=0x0405) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Disconnect, opcode=0x0406) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Link_Key_Request_Reply, opcode=0x040b) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Authentication_Requested, opcode=0x0411) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Connection_Encryption, opcode=0x0413) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_Name_Request, opcode=0x0419) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Event_Mask, opcode=0x0c01) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Reset, opcode=0x0c03) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Event_Filter, opcode=0x0c05) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Local_Name, opcode=0x0c13) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Connect_Accept_Timeout, opcode=0x0c16) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Extended_Inquiry_Response, opcode=0x0c52) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Host_Supported, opcode=0x0c6d) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_BD_Addr, opcode=0x1009) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Buffer_Size, opcode=0x2002) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Random_Address, opcode=0x2005) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Parameters, opcode=0x2006) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Data, opcode=0x2008) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Response_Data, opcode=0x2009) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertise_Enable, opcode=0x200a) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Parameters, opcode=0x200b) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Enable, opcode=0x200c) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection, opcode=0x200d) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection_Cancel, opcode=0x200e) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_White_List_Size, opcode=0x200f) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Clear_White_List, opcode=0x2010) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Add_Device_To_White_List, opcode=0x2011) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Remove_Device_From_White_List, opcode=0x2012) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Connection_Update, opcode=0x2013) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Remote_Used_Features, opcode=0x2016) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Start_Encryption_Request, opcode=0x2019) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Reply, opcode=0x201a) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply, opcode=0x201b) # noqa: E501 +# 7.1 LINK CONTROL COMMANDS, the OGF is defined as 0x01 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Inquiry, ogf=0x01, ocf=0x0001) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Inquiry_Cancel, ogf=0x01, ocf=0x0002) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Periodic_Inquiry_Mode, ogf=0x01, ocf=0x0003) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Exit_Peiodic_Inquiry_Mode, ogf=0x01, ocf=0x0004) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Create_Connection, ogf=0x01, ocf=0x0005) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Disconnect, ogf=0x01, ocf=0x0006) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Link_Key_Request_Reply, ogf=0x01, ocf=0x000b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Authentication_Requested, ogf=0x01, ocf=0x0011) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Connection_Encryption, ogf=0x01, ocf=0x0013) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_Name_Request, ogf=0x01, ocf=0x0019) + +# 7.2 Link Policy commands, the OGF is defined as 0x02 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Hold_Mode, ogf=0x02, ocf=0x0001) + +# 7.3 CONTROLLER & BASEBAND COMMANDS, the OGF is defined as 0x03 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Event_Mask, ogf=0x03, ocf=0x0001) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Reset, ogf=0x03, ocf=0x0003) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Event_Filter, ogf=0x03, ocf=0x0005) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Local_Name, ogf=0x03, ocf=0x0013) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Connect_Accept_Timeout, ogf=0x03, ocf=0x0016) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Extended_Inquiry_Response, ogf=0x03, ocf=0x0052) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_LE_Host_Support, ogf=0x03, ocf=0x006c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_LE_Host_Support, ogf=0x03, ocf=0x006d) + +# 7.4 INFORMATIONAL PARAMETERS, the OGF is defined as 0x04 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_BD_Addr, ogf=0x04, ocf=0x0009) + +# 7.5 STATUS PARAMETERS, the OGF is defined as 0x05 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_Link_Quality, ogf=0x05, ocf=0x0003) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_RSSI, ogf=0x05, ocf=0x0005) + +# 7.6 TESTING COMMANDS, the OGF is defined as 0x06 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_Loopback_Mode, ogf=0x06, ocf=0x0001) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Loopback_Mode, ogf=0x06, ocf=0x0002) + +# 7.8 LE CONTROLLER COMMANDS, the OGF code is defined as 0x08 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Buffer_Size_V1, ogf=0x08, ocf=0x0002) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Buffer_Size_V2, ogf=0x08, ocf=0x0060) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Local_Supported_Features, + ogf=0x08, ocf=0x0003) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Random_Address, ogf=0x08, ocf=0x0005) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Parameters, ogf=0x08, ocf=0x0006) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Data, ogf=0x08, ocf=0x0008) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Response_Data, ogf=0x08, ocf=0x0009) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertise_Enable, ogf=0x08, ocf=0x000a) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Parameters, ogf=0x08, ocf=0x000b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Enable, ogf=0x08, ocf=0x000c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection, ogf=0x08, ocf=0x000d) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection_Cancel, ogf=0x08, ocf=0x000e) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Filter_Accept_List_Size, + ogf=0x08, ocf=0x000f) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Clear_Filter_Accept_List, ogf=0x08, ocf=0x0010) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Add_Device_To_Filter_Accept_List, ogf=0x08, ocf=0x0011) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Remove_Device_From_Filter_Accept_List, ogf=0x08, ocf=0x0012) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Connection_Update, ogf=0x08, ocf=0x0013) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Remote_Features, ogf=0x08, ocf=0x0016) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Enable_Encryption, ogf=0x08, ocf=0x0019) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Reply, ogf=0x08, ocf=0x001a) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply, ogf=0x08, ocf=0x001b) # noqa: E501 + +# 7.7 EVENTS bind_layers(HCI_Event_Hdr, HCI_Event_Connect_Complete, code=0x03) bind_layers(HCI_Event_Hdr, HCI_Event_Disconnection_Complete, code=0x05) bind_layers(HCI_Event_Hdr, HCI_Event_Remote_Name_Request_Complete, code=0x07) diff --git a/test/scapy/layers/bluetooth.uts b/test/scapy/layers/bluetooth.uts index c103fb0081b..1743e6de91c 100644 --- a/test/scapy/layers/bluetooth.uts +++ b/test/scapy/layers/bluetooth.uts @@ -3,50 +3,146 @@ + Bluetooth tests = HCI layers -# a huge packet with all classes in it! -pkt = HCI_ACL_Hdr()/HCI_Cmd_Create_Connection()/HCI_Cmd_Complete_Read_BD_Addr()/HCI_Cmd_Connect_Accept_Timeout()/HCI_Cmd_Disconnect()/HCI_Cmd_LE_Connection_Update()/HCI_Cmd_LE_Create_Connection()/HCI_Cmd_LE_Create_Connection_Cancel()/HCI_Cmd_LE_Host_Supported()/HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply()/HCI_Cmd_LE_Long_Term_Key_Request_Reply()/HCI_Cmd_LE_Read_Buffer_Size()/HCI_Cmd_LE_Set_Advertise_Enable()/HCI_Cmd_LE_Set_Advertising_Data()/HCI_Cmd_LE_Set_Advertising_Parameters()/HCI_Cmd_LE_Set_Random_Address()/HCI_Cmd_LE_Set_Scan_Enable()/HCI_Cmd_LE_Set_Scan_Parameters()/HCI_Cmd_LE_Start_Encryption_Request()/HCI_Cmd_Authentication_Requested()/HCI_Cmd_Link_Key_Request_Reply()/HCI_Cmd_Read_BD_Addr()/HCI_Cmd_Remote_Name_Request()/HCI_Cmd_Reset()/HCI_Cmd_Set_Connection_Encryption()/HCI_Cmd_Set_Event_Filter()/HCI_Cmd_Set_Event_Mask()/HCI_Command_Hdr()/HCI_Event_Command_Complete()/HCI_Event_Command_Status()/HCI_Event_Connect_Complete()/HCI_Event_Disconnection_Complete()/HCI_Event_Encryption_Change()/HCI_Event_Remote_Name_Request_Complete()/HCI_Event_Hdr()/HCI_Event_LE_Meta()/HCI_Event_Number_Of_Completed_Packets()/HCI_Hdr()/HCI_LE_Meta_Advertising_Reports()/HCI_LE_Meta_Connection_Complete()/HCI_LE_Meta_Connection_Update_Complete()/HCI_LE_Meta_Long_Term_Key_Request() -assert HCI_ACL_Hdr in pkt.layers() -assert HCI_Cmd_Create_Connection in pkt.layers() -assert HCI_Cmd_Complete_Read_BD_Addr in pkt.layers() -assert HCI_Cmd_Connect_Accept_Timeout in pkt.layers() -assert HCI_Cmd_Disconnect in pkt.layers() -assert HCI_Cmd_LE_Connection_Update in pkt.layers() -assert HCI_Cmd_LE_Create_Connection in pkt.layers() -assert HCI_Cmd_LE_Create_Connection_Cancel in pkt.layers() -assert HCI_Cmd_LE_Host_Supported in pkt.layers() -assert HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply in pkt.layers() -assert HCI_Cmd_LE_Long_Term_Key_Request_Reply in pkt.layers() -assert HCI_Cmd_LE_Read_Buffer_Size in pkt.layers() -assert HCI_Cmd_LE_Set_Advertise_Enable in pkt.layers() -assert HCI_Cmd_LE_Set_Advertising_Data in pkt.layers() -assert HCI_Cmd_LE_Set_Advertising_Parameters in pkt.layers() -assert HCI_Cmd_LE_Set_Random_Address in pkt.layers() -assert HCI_Cmd_LE_Set_Scan_Enable in pkt.layers() -assert HCI_Cmd_LE_Set_Scan_Parameters in pkt.layers() -assert HCI_Cmd_LE_Start_Encryption_Request in pkt.layers() -assert HCI_Cmd_Authentication_Requested in pkt.layers() -assert HCI_Cmd_Link_Key_Request_Reply in pkt.layers() -assert HCI_Cmd_Read_BD_Addr in pkt.layers() -assert HCI_Cmd_Remote_Name_Request in pkt.layers() -assert HCI_Cmd_Reset in pkt.layers() -assert HCI_Cmd_Set_Connection_Encryption in pkt.layers() -assert HCI_Cmd_Set_Event_Filter in pkt.layers() -assert HCI_Cmd_Set_Event_Mask in pkt.layers() -assert HCI_Command_Hdr in pkt.layers() -assert HCI_Event_Command_Complete in pkt.layers() -assert HCI_Event_Command_Status in pkt.layers() -assert HCI_Event_Connect_Complete in pkt.layers() -assert HCI_Event_Disconnection_Complete in pkt.layers() -assert HCI_Event_Encryption_Change in pkt.layers() -assert HCI_Event_Remote_Name_Request_Complete in pkt.layers() -assert HCI_Event_Hdr in pkt.layers() -assert HCI_Event_LE_Meta in pkt.layers() -assert HCI_Event_Number_Of_Completed_Packets in pkt.layers() -assert HCI_Hdr in pkt.layers() -assert HCI_LE_Meta_Advertising_Reports in pkt.layers() -assert HCI_LE_Meta_Connection_Complete in pkt.layers() -assert HCI_LE_Meta_Connection_Update_Complete in pkt.layers() -assert HCI_LE_Meta_Long_Term_Key_Request in pkt.layers() + +# HCI_Command_Hdr +# default construction +hci_cmd_hdr = HCI_Command_Hdr() +assert hci_cmd_hdr.ogf == 0 +assert hci_cmd_hdr.ocf == 0 +assert hci_cmd_hdr.len == None +assert raw(hci_cmd_hdr) == b'\x00\x00\x00' + +# parsing +hci_cmd_hdr = HCI_Command_Hdr(raw(hci_cmd_hdr)) +assert hci_cmd_hdr.ogf == 0 +assert hci_cmd_hdr.ocf == 0 +assert hci_cmd_hdr.len == 0 + +# HCI_Cmd_Inquiry default construction +hci_cmd_inquiry = HCI_Command_Hdr() / HCI_Cmd_Inquiry() +assert hci_cmd_inquiry.ogf == 0x01 +assert hci_cmd_inquiry.ocf == 0x01 +assert hci_cmd_inquiry.len == None +assert hci_cmd_inquiry.lap == 0x9e8b33 +assert hci_cmd_inquiry.inquiry_length == 0 +assert hci_cmd_inquiry.num_responses == 0 + +# parsing +hci_cmd_inquiry = HCI_Command_Hdr(raw(hci_cmd_inquiry)) +assert hci_cmd_inquiry.ogf == 0x01 +assert hci_cmd_inquiry.ocf == 0x01 +assert hci_cmd_inquiry.len == 5 +assert hci_cmd_inquiry.lap == 0x9e8b33 +assert hci_cmd_inquiry.inquiry_length == 0 +assert hci_cmd_inquiry.num_responses == 0 + +# HCI_Cmd_Inquiry constructing an invalid packet +hci_cmd_inquiry = HCI_Command_Hdr(len = 10) / HCI_Cmd_Inquiry() +assert hci_cmd_inquiry.ogf == 0x01 +assert hci_cmd_inquiry.ocf == 0x01 +assert hci_cmd_inquiry.len == 10 +assert hci_cmd_inquiry.lap == 0x9e8b33 +assert hci_cmd_inquiry.inquiry_length == 0 +assert hci_cmd_inquiry.num_responses == 0 + +assert raw(hci_cmd_inquiry)[2] == 10 + +# parse the invalid packet +hci_cmd_inquiry = HCI_Command_Hdr(raw(hci_cmd_inquiry)) +assert hci_cmd_inquiry.ogf == 0x01 +assert hci_cmd_inquiry.ocf == 0x01 +assert hci_cmd_inquiry.len == 10 +assert hci_cmd_inquiry.lap == 0x9e8b33 +assert hci_cmd_inquiry.inquiry_length == 0 +assert hci_cmd_inquiry.num_responses == 0 + +# HCI_Cmd_Inquiry_Cancel default construction +hci_cmd_inquiry_cancel = HCI_Command_Hdr() / HCI_Cmd_Inquiry_Cancel() +assert hci_cmd_inquiry_cancel.ogf == 0x01 +assert hci_cmd_inquiry_cancel.ocf == 0x02 +assert hci_cmd_inquiry_cancel.len == None + +# hci_cmd_inquiry_cancel parsing +hci_cmd_inquiry_cancel = HCI_Command_Hdr(raw(hci_cmd_inquiry_cancel)) +assert hci_cmd_inquiry_cancel.ogf == 0x01 +assert hci_cmd_inquiry_cancel.ocf == 0x02 +assert hci_cmd_inquiry_cancel.len == 0 + + +# Hci_Cmd_Hold_Mode +hci_cmd_hold_mode = HCI_Command_Hdr() / HCI_Cmd_Hold_Mode() +assert hci_cmd_hold_mode.ogf == 0x02 +assert hci_cmd_hold_mode.ocf == 0x01 +assert hci_cmd_hold_mode.len == None + +# parsing +hci_cmd_hold_mode = HCI_Command_Hdr(raw(hci_cmd_hold_mode)) +assert hci_cmd_hold_mode.ogf == 0x02 +assert hci_cmd_hold_mode.ocf == 0x01 +assert hci_cmd_hold_mode.len == 6 + +# HCI_Cmd_Set_Event_Mask +hci_cmd_set_event_mask = HCI_Command_Hdr() / HCI_Cmd_Set_Event_Mask() +assert hci_cmd_set_event_mask.ogf == 0x03 +assert hci_cmd_set_event_mask.ocf == 0x01 +assert hci_cmd_set_event_mask.len == None + +# parsing +hci_cmd_set_event_mask = HCI_Command_Hdr(raw(hci_cmd_set_event_mask)) +assert hci_cmd_set_event_mask.ogf == 0x03 +assert hci_cmd_set_event_mask.ocf == 0x01 +assert hci_cmd_set_event_mask.len == 8 + +# HCI_Cmd_Read_BD_Addr +hci_cmd_read_bd_addr = HCI_Command_Hdr() / HCI_Cmd_Read_BD_Addr() +assert hci_cmd_read_bd_addr.ogf == 0x04 +assert hci_cmd_read_bd_addr.ocf == 0x09 +assert hci_cmd_read_bd_addr.len == None + +# parsing +hci_cmd_read_bd_addr = HCI_Command_Hdr(raw(hci_cmd_read_bd_addr)) +assert hci_cmd_read_bd_addr.ogf == 0x04 +assert hci_cmd_read_bd_addr.ocf == 0x09 +assert hci_cmd_read_bd_addr.len == 0 + + +# HCI_Cmd_Read_Link_Quality +hci_cmd_read_link_quality = HCI_Command_Hdr() / HCI_Cmd_Read_Link_Quality() +assert hci_cmd_read_link_quality.ogf == 0x05 +assert hci_cmd_read_link_quality.ocf == 0x03 +assert hci_cmd_read_link_quality.len == None + +# parsing +hci_cmd_read_link_quality = HCI_Command_Hdr(raw(hci_cmd_read_link_quality)) +assert hci_cmd_read_link_quality.ogf == 0x05 +assert hci_cmd_read_link_quality.ocf == 0x03 +assert hci_cmd_read_link_quality.len == 2 + + +# HCI_Cmd_Read_Loopback_Mode +hci_cmd_read_loopback_mode = HCI_Command_Hdr() / HCI_Cmd_Read_Loopback_Mode() +assert hci_cmd_read_loopback_mode.ogf == 0x06 +assert hci_cmd_read_loopback_mode.ocf == 0x01 +assert hci_cmd_read_loopback_mode.len == None + +# parsing +hci_cmd_read_loopback_mode = HCI_Command_Hdr(raw(hci_cmd_read_loopback_mode)) +assert hci_cmd_read_loopback_mode.ogf == 0x06 +assert hci_cmd_read_loopback_mode.ocf == 0x01 +assert hci_cmd_read_loopback_mode.len == 0 + + +# HCI_Cmd_LE_Read_Buffer_Size_V1 +hci_cmd_le_read_buffer_size_v1 = HCI_Command_Hdr() / HCI_Cmd_LE_Read_Buffer_Size_V1() +assert hci_cmd_le_read_buffer_size_v1.ogf == 0x08 +assert hci_cmd_le_read_buffer_size_v1.ocf == 0x02 +assert hci_cmd_le_read_buffer_size_v1.len == None + +# parsing +hci_cmd_le_read_buffer_size_v1 = HCI_Command_Hdr(raw(hci_cmd_le_read_buffer_size_v1)) +assert hci_cmd_le_read_buffer_size_v1.ogf == 0x08 +assert hci_cmd_le_read_buffer_size_v1.ocf == 0x02 +assert hci_cmd_le_read_buffer_size_v1.len == 0 + Bluetooth Transport Layers = Test HCI_PHDR_Hdr From 0bc40c69352e850500890b633d326963da4e9a1a Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 26 Aug 2023 20:42:38 +0200 Subject: [PATCH 050/122] Use L3RawSocket(6) automatically on lo (#4099) --- doc/scapy/installation.rst | 35 -------------- doc/scapy/troubleshooting.rst | 91 ++++++++++++++++++++++++++++------- doc/scapy/usage.rst | 16 +----- scapy/config.py | 4 +- scapy/interfaces.py | 33 ++++++++----- scapy/sendrecv.py | 45 +++++++++++------ test/linux.uts | 11 +++-- test/regression.uts | 1 + 8 files changed, 136 insertions(+), 100 deletions(-) diff --git a/doc/scapy/installation.rst b/doc/scapy/installation.rst index afb687b1f51..695b0403a77 100644 --- a/doc/scapy/installation.rst +++ b/doc/scapy/installation.rst @@ -309,41 +309,6 @@ Screenshots :scale: 80 :align: center -Known bugs -^^^^^^^^^^ - -You may bump into the following bugs, which are platform-specific, if Scapy didn't manage work around them automatically: - - * You may not be able to capture WLAN traffic on Windows. Reasons are explained on the `Wireshark wiki `_ and in the `WinPcap FAQ `_. Try switching off promiscuous mode with ``conf.sniff_promisc=False``. - * Packets sometimes cannot be sent to localhost (or local IP addresses on your own host). - -Winpcap/Npcap conflicts -^^^^^^^^^^^^^^^^^^^^^^^ - -As ``Winpcap`` is becoming old, it's recommended to use ``Npcap`` instead. ``Npcap`` is part of the ``Nmap`` project. - -.. note:: - This does NOT apply for Windows XP, which isn't supported by ``Npcap``. - -1. If you get the message ``'Winpcap is installed over Npcap.'`` it means that you have installed both Winpcap and Npcap versions, which isn't recommended. - -You may first **uninstall winpcap from your Program Files**, then you will need to remove:: - - C:/Windows/System32/wpcap.dll - C:/Windows/System32/Packet.dll - -And if you are on an x64 machine:: - - C:/Windows/SysWOW64/wpcap.dll - C:/Windows/SysWOW64/Packet.dll - -To use ``Npcap`` instead, as those files are not removed by the ``Winpcap`` un-installer. - -2. If you get the message ``'The installed Windump version does not work with Npcap'`` it surely means that you have installed an old version of ``Windump``, made for ``Winpcap``. -Download the correct one on https://github.com/hsluoyz/WinDump/releases - -In some cases, it could also mean that you had installed ``Npcap`` and ``Winpcap``, and that ``Windump`` is using ``Winpcap``. Fully delete ``Winpcap`` using the above method to solve the problem. - Build the documentation offline =============================== diff --git a/doc/scapy/troubleshooting.rst b/doc/scapy/troubleshooting.rst index 3025e3c097d..4131ec280f6 100644 --- a/doc/scapy/troubleshooting.rst +++ b/doc/scapy/troubleshooting.rst @@ -8,21 +8,33 @@ FAQ I can't sniff/inject packets in monitor mode. --------------------------------------------- -The use monitor mode varies greatly depending on the platform. +The use monitor mode varies greatly depending on the platform, reasons are explained on the `Wireshark wiki `_: + + *Unfortunately, changing the 802.11 capture modes is very platform/network adapter/driver/libpcap dependent, and might not be possible at all (Windows is very limited here).* + +Here is some guidance on how to properly use monitor mode with Scapy: + +- **Using Libpcap (or Npcap)**: + ``libpcap`` must be called differently by Scapy in order for it to create the sockets in monitor mode. You will need to pass the ``monitor=True`` to any calls that open a socket (``send``, ``sniff``...) or to a Scapy socket that you create yourself (``conf.L2Socket``...) + + **On Windows**, you additionally need to turn on monitor mode on the WiFi card, use:: + + # Of course, conf.iface can be replaced by any interfaces accessed through conf.ifaces + >>> conf.iface.setmonitor(True) -- **Using Libpcap** - ``libpcap`` must be called differently by Scapy in order for it to create the sockets in monitor mode. You will need to pass the ``monitor=True`` to any calls that open a socket (``send``, ``sniff``...) or to a Scapy socket that you create yourself (``conf.L2Socket``...) - **Native Linux (with libpcap disabled):** - You should set the interface in monitor mode on your own. I personally like - to use iwconfig for that (replace ``monitor`` by ``managed`` to disable):: + You should set the interface in monitor mode on your own. The easiest way to do that is to use ``airmon-ng``:: + + $ sudo airmon-ng start wlan0 + + You can also use:: - $ sudo ifconfig IFACE down - $ sudo iwconfig IFACE mode monitor - $ sudo ifconfig IFACE up + $ iw dev wlan0 interface add mon0 type monitor + $ ifconfig mon0 up -**If you are using Npcap:** please note that Npcap ``npcap-0.9983`` broke the 802.11 util back in 2019. It has yet to be fixed (as of Npcap 0.9994) so in the meantime, use `npcap-0.9982.exe `_ + If you want to enable monitor mode manually, have a look at https://wiki.wireshark.org/CaptureSetup/WLAN#linux -.. note:: many adapters do not support monitor mode, especially on Windows, or may incorrectly report the headers. See `the Wireshark doc about this `_ +.. warning:: **If you are using Npcap:** please note that Npcap ``npcap-0.9983`` broke the 802.11 support until ``npcap-1.3.0``. Avoid using those versions. We make our best to make this work, if your adapter works with Wireshark for instance, but not with Scapy, feel free to report an issue. @@ -35,12 +47,14 @@ I can't ping 127.0.0.1 (or ::1). Scapy does not work with 127.0.0.1 (or ::1) on The loopback interface is a very special interface. Packets going through it are not really assembled and disassembled. The kernel routes the packet to its destination while it is still stored an internal structure. What you see with ```tcpdump -i lo``` is only a fake to make you think everything is normal. The kernel is not aware of what Scapy is doing behind his back, so what you see on the loopback interface is also a fake. Except this one did not come from a local structure. Thus the kernel will never receive it. -On Linux, in order to speak to local IPv4 applications, you need to build your packets one layer upper, using a PF_INET/SOCK_RAW socket instead of a PF_PACKET/SOCK_RAW (or its equivalent on other systems than Linux):: +.. note:: Starting from Scapy > **2.5.0**, Scapy will automatically use ``L3RawSocket`` when necessary when using L3-functions (sr-like) on the loopback interface, when libpcap is not in use. + +**On Linux**, in order to speak to local IPv4 applications, you need to build your packets one layer upper, using a PF_INET/SOCK_RAW socket instead of a PF_PACKET/SOCK_RAW (or its equivalent on other systems than Linux):: >>> conf.L3socket >>> conf.L3socket = L3RawSocket - >>> sr1(IP(dst) / ICMP()) + >>> sr1(IP() / ICMP()) > With IPv6, you can simply do:: @@ -50,11 +64,20 @@ With IPv6, you can simply do:: > # Layer 2 - >>> conf.iface = "lo" - >>> srp1(Ether() / IPv6() / ICMPv6EchoRequest()) + >>> srp1(Ether() / IPv6() / ICMPv6EchoRequest(), iface=conf.loopback_name) >> -On Windows, BSD, and macOS, you must deactivate the local firewall and set ````conf.iface``` to the loopback interface prior to using the following commands:: +.. warning:: + On Linux, libpcap does not support loopback IPv4 pings: + >>> conf.use_pcap = True + >>> sr1(IP() / ICMP()) + Begin emission: + Finished sending 1 packets. + ..................................... + + You can disable libpcap using ``conf.use_pcap = False`` or bypass it on layer 3 using ``conf.L3socket = L3RawSocket``. + +**On Windows, BSD, and macOS**, you must deactivate/configure the local firewall prior to using the following commands:: # Layer 3 >>> sr1(IP() / ICMP()) @@ -63,11 +86,45 @@ On Windows, BSD, and macOS, you must deactivate the local firewall and set ````c > # Layer 2 - >>> srp1(Loopback() / IP() / ICMP()) + >>> srp1(Loopback() / IP() / ICMP(), iface=conf.loopback_name) >> - >>> srp1(Loopback() / IPv6() / ICMPv6EchoRequest()) + >>> srp1(Loopback() / IPv6() / ICMPv6EchoRequest(), iface=conf.loopback_name) >> +Getting 'failed to set hardware filter to promiscuous mode' error +----------------------------------------------------------------- + +Disable promiscuous mode:: + + conf.sniff_promisc = False + +Scapy says there are 'Winpcap/Npcap conflicts' +---------------------------------------------- + +**On Windows**, as ``Winpcap`` is becoming old, it's recommended to use ``Npcap`` instead. ``Npcap`` is part of the ``Nmap`` project. + +.. note:: + This does NOT apply for Windows XP, which isn't supported by ``Npcap``. On XP, uninstall ``Npcap`` and keep ``Winpcap``. + +1. If you get the message ``'Winpcap is installed over Npcap.'`` it means that you have installed both Winpcap and Npcap versions, which isn't recommended. + +You may first **uninstall winpcap from your Program Files**, then you will need to remove some files that are not deleted by the ``Winpcap`` uninstaller:: + + C:/Windows/System32/wpcap.dll + C:/Windows/System32/Packet.dll + +And if you are on an x64 machine, additionally the 32-bit variants:: + + C:/Windows/SysWOW64/wpcap.dll + C:/Windows/SysWOW64/Packet.dll + +Once that is done, you'll be able to use ``Npcap`` properly. + +2. If you get the message ``'The installed Windump version does not work with Npcap'`` it means that you have probably installed an old version of ``Windump``, made for ``Winpcap``. +Download the one compatible with ``Npcap`` on https://github.com/hsluoyz/WinDump/releases + +In some cases, it could also mean that you had installed both ``Npcap`` and ``Winpcap``, and that the Npcap ``Windump`` is using ``Winpcap``. Fully delete ``Winpcap`` using the above method to solve the problem. + BPF filters do not work. I'm on a ppp link ------------------------------------------ diff --git a/doc/scapy/usage.rst b/doc/scapy/usage.rst index 020ab493a6e..a06b5ba200d 100644 --- a/doc/scapy/usage.rst +++ b/doc/scapy/usage.rst @@ -1213,21 +1213,9 @@ Wireless frame injection single: FakeAP, Dot11, wireless, WLAN .. note:: - See the TroubleShooting section for more information on the usage of Monitor mode among Scapy. + See the :doc:`TroubleShooting ` section for more information on the usage of Monitor mode among Scapy. -Provided that your wireless card and driver are correctly configured for frame injection - -:: - - $ iw dev wlan0 interface add mon0 type monitor - $ ifconfig mon0 up - -On Windows, if using Npcap, the equivalent would be to call:: - - >>> # Of course, conf.iface can be replaced by any interfaces accessed through conf.ifaces - ... conf.iface.setmonitor(True) - -you can have a kind of FakeAP:: +Provided that your wireless card and driver are correctly configured for frame injection, you can have a kind of FakeAP:: >>> sendp(RadioTap()/ Dot11(addr1="ff:ff:ff:ff:ff:ff", diff --git a/scapy/config.py b/scapy/config.py index 679f18e53ba..639d19e7556 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -747,10 +747,8 @@ class Conf(ConfClass): check_TCPerror_seqack = False verb = 2 #: level of verbosity, from 0 (almost mute) to 3 (verbose) prompt = Interceptor("prompt", ">>> ", _prompt_changer) - #: default mode for listening socket (to get answers if you + #: default mode for the promiscuous mode of a socket (to get answers if you #: spoof on a lan) - promisc = True - #: default mode for sniff() sniff_promisc = True # type: bool raw_layer = None # type: Type[Packet] raw_summary = False # type: Union[bool, Callable[[bytes], Any]] diff --git a/scapy/interfaces.py b/scapy/interfaces.py index 3bceeac0978..17985c4e900 100644 --- a/scapy/interfaces.py +++ b/scapy/interfaces.py @@ -12,7 +12,7 @@ from collections import defaultdict from scapy.config import conf -from scapy.consts import WINDOWS +from scapy.consts import WINDOWS, LINUX from scapy.utils import pretty_list from scapy.utils6 import in6_isvalid @@ -20,6 +20,7 @@ import scapy from scapy.compat import UserDict from typing import ( + cast, Any, DefaultDict, Dict, @@ -49,19 +50,27 @@ def reload(self): """Same than load() but for reloads. By default calls load""" return self.load() - def l2socket(self): - # type: () -> Type[scapy.supersocket.SuperSocket] + def _l2socket(self, dev): + # type: (NetworkInterface) -> Type[scapy.supersocket.SuperSocket] """Return L2 socket used by interfaces of this provider""" return conf.L2socket - def l2listen(self): - # type: () -> Type[scapy.supersocket.SuperSocket] + def _l2listen(self, dev): + # type: (NetworkInterface) -> Type[scapy.supersocket.SuperSocket] """Return L2listen socket used by interfaces of this provider""" return conf.L2listen - def l3socket(self): - # type: () -> Type[scapy.supersocket.SuperSocket] + def _l3socket(self, dev, ipv6): + # type: (NetworkInterface, bool) -> Type[scapy.supersocket.SuperSocket] """Return L3 socket used by interfaces of this provider""" + if LINUX and not self.libpcap and dev.name == conf.loopback_name: + # handle the loopback case. see troubleshooting.rst + if ipv6: + from scapy.layers.inet6 import L3RawSocket6 + return cast(Type['scapy.supersocket.SuperSocket'], L3RawSocket6) + else: + from scapy.supersocket import L3RawSocket + return L3RawSocket return conf.L3socket def _is_valid(self, dev): @@ -156,15 +165,15 @@ def is_valid(self): def l2socket(self): # type: () -> Type[scapy.supersocket.SuperSocket] - return self.provider.l2socket() + return self.provider._l2socket(self) def l2listen(self): # type: () -> Type[scapy.supersocket.SuperSocket] - return self.provider.l2listen() + return self.provider._l2listen(self) - def l3socket(self): - # type: () -> Type[scapy.supersocket.SuperSocket] - return self.provider.l3socket() + def l3socket(self, ipv6): + # type: (bool) -> Type[scapy.supersocket.SuperSocket] + return self.provider._l3socket(self, ipv6) def __repr__(self): # type: () -> str diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py index 8628a000e96..85136ab18a5 100644 --- a/scapy/sendrecv.py +++ b/scapy/sendrecv.py @@ -11,6 +11,7 @@ from threading import Thread, Event import os import re +import socket import subprocess import time @@ -24,6 +25,7 @@ NetworkInterface, ) from scapy.packet import Packet +from scapy.pton_ntop import inet_pton from scapy.utils import get_temp_file, tcpdump, wrpcap, \ ContextManagerSubprocess, PcapReader, EDecimal from scapy.plist import ( @@ -439,10 +441,10 @@ def send(x, # type: _PacketIterable :param monitor: (not on linux) send in monitor mode :returns: None """ - iface = _interface_selection(iface, x) + iface, ipv6 = _interface_selection(iface, x) return _send( x, - lambda iface: iface.l3socket(), + lambda iface: iface.l3socket(ipv6), iface=iface, **kargs ) @@ -616,19 +618,26 @@ def _parse_tcpreplay_result(stdout_b, stderr_b, argv): def _interface_selection(iface, # type: Optional[_GlobInterfaceType] packet # type: _PacketIterable ): - # type: (...) -> _GlobInterfaceType + # type: (...) -> Tuple[NetworkInterface, bool] """ Select the network interface according to the layer 3 destination """ - + _iff, src, _ = next(packet.__iter__()).route() + ipv6 = False + if src: + try: + inet_pton(socket.AF_INET6, src) + ipv6 = True + except OSError: + pass if iface is None: try: - iff = next(packet.__iter__()).route()[0] + iff = resolve_iface(_iff or conf.iface) except AttributeError: iff = None - return iff or conf.iface + return iff or conf.iface, ipv6 - return iface + return resolve_iface(iface), ipv6 @conf.commands.register @@ -644,9 +653,11 @@ def sr(x, # type: _PacketIterable """ Send and receive packets at layer 3 """ - iface = _interface_selection(iface, x) - s = conf.L3socket(promisc=promisc, filter=filter, - iface=iface, nofilter=nofilter) + iface, ipv6 = _interface_selection(iface, x) + s = iface.l3socket(ipv6)( + promisc=promisc, filter=filter, + iface=iface, nofilter=nofilter, + ) result = sndrcv(s, x, *args, **kargs) s.close() return result @@ -887,8 +898,11 @@ def srflood(x, # type: _PacketIterable :param filter: provide a BPF filter :param iface: listen answers only on the given interface """ - iface = resolve_iface(iface or conf.iface) - s = iface.l3socket()(promisc=promisc, filter=filter, iface=iface, nofilter=nofilter) # noqa: E501 + iface, ipv6 = _interface_selection(iface, x) + s = iface.l3socket(ipv6)( + promisc=promisc, filter=filter, + iface=iface, nofilter=nofilter, + ) r = sndrcvflood(s, x, *args, **kargs) s.close() return r @@ -912,8 +926,11 @@ def sr1flood(x, # type: _PacketIterable :param filter: provide a BPF filter :param iface: listen answers only on the given interface """ - iface = resolve_iface(iface or conf.iface) - s = iface.l3socket()(promisc=promisc, filter=filter, nofilter=nofilter, iface=iface) # noqa: E501 + iface, ipv6 = _interface_selection(iface, x) + s = iface.l3socket(ipv6)( + promisc=promisc, filter=filter, + nofilter=nofilter, iface=iface, + ) ans, _ = sndrcvflood(s, x, *args, **kargs) s.close() if len(ans) > 0: diff --git a/test/linux.uts b/test/linux.uts index b6a6fc5712c..48e27921cbd 100644 --- a/test/linux.uts +++ b/test/linux.uts @@ -11,12 +11,9 @@ = L3RawSocket ~ netaccess IP TCP linux needs_root -old_l3socket = conf.L3socket -conf.L3socket = L3RawSocket with no_debug_dissector(): x = sr1(IP(dst="www.google.com")/TCP(sport=RandShort(), dport=80, flags="S"),timeout=3) -conf.L3socket = old_l3socket x assert x[IP].ottl() in [32, 64, 128, 255] assert 0 <= x[IP].hops() <= 126 @@ -309,16 +306,20 @@ assert test_L3PacketSocket_sendto_python3() import os from scapy.sendrecv import _interface_selection -assert _interface_selection(None, IP(dst="8.8.8.8")/UDP()) == conf.iface +assert _interface_selection(None, IP(dst="8.8.8.8")/UDP()) == (conf.iface, False) exit_status = os.system("ip link add name scapy0 type dummy") exit_status = os.system("ip addr add 192.0.2.1/24 dev scapy0") +exit_status = os.system("ip addr add fc00::/24 dev scapy0") exit_status = os.system("ip link set scapy0 up") conf.ifaces.reload() conf.route.resync() -assert _interface_selection(None, IP(dst="192.0.2.42")/UDP()) == "scapy0" +conf.route6.resync() +assert _interface_selection(None, IP(dst="192.0.2.42")/UDP()) == ("scapy0", False) +assert _interface_selection(None, IPv6(dst="fc00::ae0d")/UDP()) == ("scapy0", True) exit_status = os.system("ip link del name dev scapy0") conf.ifaces.reload() conf.route.resync() +conf.route6.resync() = Test 802.Q sniffing ~ linux needs_root veth diff --git a/test/regression.uts b/test/regression.uts index 7f43b7fc22b..3f36ec9c47a 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -1518,6 +1518,7 @@ retry_test(_test) = Latency check: localhost ICMP ~ netaccess needs_root linux latency +# Note: still needs to enforce L3RawSocket as this won't work otherwise with libpcap sock = conf.L3socket conf.L3socket = L3RawSocket From e45499000cc74f3c40ca2e1ba31c28ee6b29c106 Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Tue, 29 Aug 2023 11:28:57 +0300 Subject: [PATCH 051/122] [INET6] add the PREF64 ND option (#4105) --- scapy/layers/inet6.py | 23 +++++++++++++++++++++++ test/scapy/layers/inet6.uts | 26 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py index 2619c828808..0dbb19e0f14 100644 --- a/scapy/layers/inet6.py +++ b/scapy/layers/inet6.py @@ -1720,6 +1720,7 @@ def extract_padding(self, s): 26: "ICMPv6NDOptEFA", 31: "ICMPv6NDOptDNSSL", 37: "ICMPv6NDOptCaptivePortal", + 38: "ICMPv6NDOptPREF64", } icmp6ndraprefs = {0: "Medium (default)", @@ -2087,6 +2088,28 @@ class ICMPv6NDOptCaptivePortal(_ICMPv6NDGuessPayload, Packet): # RFC 8910 def mysummary(self): return self.sprintf("%name% %URI%") + +class _PREF64(IP6Field): + def addfield(self, pkt, s, val): + return s + self.i2m(pkt, val)[:12] + + def getfield(self, pkt, s): + return s[12:], self.m2i(pkt, s[:12] + b"\x00" * 4) + + +class ICMPv6NDOptPREF64(_ICMPv6NDGuessPayload, Packet): # RFC 8781 + name = "ICMPv6 Neighbor Discovery Option - PREF64 Option" + fields_desc = [ByteField("type", 38), + ByteField("len", 2), + BitField("scaledlifetime", 0, 13), + BitEnumField("plc", 0, 3, + ["/96", "/64", "/56", "/48", "/40", "/32"]), + _PREF64("prefix", "::")] + + def mysummary(self): + plc = self.sprintf("%plc%") if self.plc < 6 else f"[invalid PLC({self.plc})]" + return self.sprintf("%name% %prefix%") + plc + # End of ICMPv6 Neighbor Discovery Options. diff --git a/test/scapy/layers/inet6.uts b/test/scapy/layers/inet6.uts index b9f78a0b9ce..3c06be0c117 100644 --- a/test/scapy/layers/inet6.uts +++ b/test/scapy/layers/inet6.uts @@ -1250,6 +1250,32 @@ a=ICMPv6NDOptEFA(b'\x1a\x01\x00\x00\x00\x00\x00\x00') a.type==26 and a.len==1 and a.res == 0 +############ +############ ++ ICMPv6NDOptPREF64 Class Test + += ICMPv6NDOptPREF64 - Basic Instantiation +raw(ICMPv6NDOptPREF64()) == b'\x26\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + += ICMPv6NDOptPREF64 - Basic Dissection +p = ICMPv6NDOptPREF64(b'\x26\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') +assert p.type == 38 and p.len == 2 and p.scaledlifetime == 0 and p.plc == 0 and p.prefix == '::' + += ICMPv6NDOptPREF64 - Instantiation/Dissection with specific values +p = ICMPv6NDOptPREF64(scaledlifetime=225, plc='/64', prefix='2003:da8:1::') +assert raw(p) == b'\x26\x02\x07\x09\x20\x03\x0d\xa8\x00\x01\x00\x00\x00\x00\x00\x00' + +p = ICMPv6NDOptPREF64(raw(p)) +assert p.type == 38 and p.len == 2 and p.scaledlifetime == 225 and p.plc == 1 and p.prefix == '2003:da8:1::' + +p = ICMPv6NDOptPREF64(raw(p) + b'\x00\x00\x00\x00') +assert Raw in p and len(p[Raw]) == 4 + += ICMPv6NDOptPREF64 - Summary Output +ICMPv6NDOptPREF64(prefix='12:34:56::', plc='/32').mysummary() == "ICMPv6 Neighbor Discovery Option - PREF64 Option 12:34:56::/32" +ICMPv6NDOptPREF64(prefix='12:34:56::', plc=6).mysummary() == "ICMPv6 Neighbor Discovery Option - PREF64 Option 12:34:56::[invalid PLC(6)]" + + ############ ############ + Test Node Information Query - ICMPv6NIQueryNOOP From 3e6900776698cd5472c5405294414d5b672a3f18 Mon Sep 17 00:00:00 2001 From: Olivier Matz Date: Tue, 29 Aug 2023 10:54:10 +0200 Subject: [PATCH 052/122] PPP: fix default size of protocol field (#4106) In commit 2f5d9bd4dab8 ("PPP: protocol field can be limited to one byte"), a new class PPP_ was added to manage parsing and generation a PPP header with a one byte PPP protocol field. This was later reworked by commit 834309f91c1d ("Small doc cleanups"), which removed the PPP_ class, and changed the default behavior of the PPP class to generate a one byte protocol field by default, when its value was lower than 0x100. The RFC states that "by default, all implementations MUST transmit packets with two octet PPP Protocol fields". A header with a one byte protocol field is issued by implementations when the compression is negociated. This patch reverts to the original behavior, which is to generate a two bytes protocol field by default, but make it possible to explicitly generate a one byte protocol by passing the value as bytes(). The PPP class is still able to parse either a one or two bytes protocol. Link: https://www.rfc-editor.org/rfc/rfc1661.html#section-6.5 Fixes #3913 --- scapy/layers/ppp.py | 24 +++++++++++++++++++----- test/scapy/layers/ppp.uts | 16 ++++++++++++++-- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/scapy/layers/ppp.py b/scapy/layers/ppp.py index b5cd42b46ec..e0f4c593d0c 100644 --- a/scapy/layers/ppp.py +++ b/scapy/layers/ppp.py @@ -292,6 +292,14 @@ class _PPPProtoField(EnumField): See RFC 1661 section 2 + + The generated proto field is two bytes when not specified, or when specified + as an integer or a string: + PPP() + PPP(proto=0x21) + PPP(proto="Internet Protocol version 4") + To explicitly forge a one byte proto field, use the bytes representation: + PPP(proto=b'\x21') """ def getfield(self, pkt, s): if ord(s[:1]) & 0x01: @@ -304,12 +312,18 @@ def getfield(self, pkt, s): return super(_PPPProtoField, self).getfield(pkt, s) def addfield(self, pkt, s, val): - if val < 0x100: - self.fmt = "!B" - self.sz = 1 + if isinstance(val, bytes): + if len(val) == 1: + fmt, sz = "!B", 1 + elif len(val) == 2: + fmt, sz = "!H", 2 + else: + raise TypeError('Invalid length for PPP proto') + val = struct.Struct(fmt).unpack(val)[0] else: - self.fmt = "!H" - self.sz = 2 + fmt, sz = "!H", 2 + self.fmt = fmt + self.sz = sz self.struct = struct.Struct(self.fmt) return super(_PPPProtoField, self).addfield(pkt, s, val) diff --git a/test/scapy/layers/ppp.uts b/test/scapy/layers/ppp.uts index 42bd818a313..1961580fa6e 100644 --- a/test/scapy/layers/ppp.uts +++ b/test/scapy/layers/ppp.uts @@ -112,12 +112,24 @@ assert raw(p) == raw(q) assert q[PPP_ECP_Option].data == b"ABCDEFG" -= PPP with only one byte for protocol += PPP IP check that default protocol length is 2 bytes +~ ppp ip + +p = PPP()/IP() +p +r = raw(p) +r +assert r.startswith(b'\x00\x21') +assert len(r) == 22 + + += PPP check parsing with only one byte for protocol ~ ppp -assert len(raw(PPP() / IP())) == 21 +assert len(raw(PPP(proto=b'\x21') / IP())) == 21 p = PPP(b'!E\x00\x00<\x00\x00@\x008\x06\xa5\xce\x85wP)\xc0\xa8Va\x01\xbbd\x8a\xe2}r\xb8O\x95\xb5\x84\xa0\x12q \xc8\x08\x00\x00\x02\x04\x02\x18\x04\x02\x08\nQ\xdf\xd6\xb0\x00\x07LH\x01\x03\x03\x07Ao') assert IP in p assert TCP in p +assert PPP(b"\x00\x21" + raw(IP())) == PPP(b"\x21" + raw(IP())) From c57c02beb60538f23c3355cd8e207ed4f44462b6 Mon Sep 17 00:00:00 2001 From: Hui Peng Date: Thu, 31 Aug 2023 13:30:19 -0700 Subject: [PATCH 053/122] Adding a few more Bluetooth HCI Commands (#4103) * Document the section of definition of some HCI commands * Add more Bluetooth HCI Commands [1] * Add more Bluetooth HCI commands [2] * Fix the definition of HCI_Connection_Complete - fix the name - add missing fields - update test * Add HCI_Inquiry_Complete event --- scapy/layers/bluetooth.py | 349 +++++++++++++++++++++++++++++++- test/scapy/layers/bluetooth.uts | 8 +- 2 files changed, 349 insertions(+), 8 deletions(-) diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index cca1d5a9086..3b82842f6bb 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -34,6 +34,7 @@ IntField, LEShortEnumField, LEShortField, + LEIntField, LenField, MultipleTypeField, NBytesField, @@ -923,6 +924,12 @@ def post_build(self, p, pay): class HCI_Cmd_Inquiry(Packet): + """ + + 7.1.1 Inquiry command + + """ + name = "HCI_Inquiry" fields_desc = [XLE3BytesField("lap", 0x9E8B33), ByteField("inquiry_length", 0), @@ -930,10 +937,22 @@ class HCI_Cmd_Inquiry(Packet): class HCI_Cmd_Inquiry_Cancel(Packet): + """ + + 7.1.2 Inquiry Cancel command + + """ + name = "HCI_Inquiry_Cancel" class HCI_Cmd_Periodic_Inquiry_Mode(Packet): + """ + + 7.1.3 Periodic Inquiry Mode command + + """ + name = "HCI_Periodic_Inquiry_Mode" fields_desc = [LEShortField("max_period_length", 0x0003), LEShortField("min_period_length", 0x0002), @@ -943,10 +962,22 @@ class HCI_Cmd_Periodic_Inquiry_Mode(Packet): class HCI_Cmd_Exit_Peiodic_Inquiry_Mode(Packet): + """ + + 7.1.4 Exit Periodic Inquiry Mode command + + """ + name = "HCI_Exit_Periodic_Inquiry_Mode" class HCI_Cmd_Create_Connection(Packet): + """ + + 7.1.5 Create Connection command + + """ + name = "HCI_Create_Connection" fields_desc = [LEMACField("bd_addr", None), LEShortField("packet_type", 0xcc18), @@ -957,28 +988,162 @@ class HCI_Cmd_Create_Connection(Packet): class HCI_Cmd_Disconnect(Packet): + """ + + 7.1.6 Disconnect command + + """ + name = "HCI_Disconnect" fields_desc = [XLEShortField("handle", 0), ByteField("reason", 0x13), ] +class HCI_Cmd_Create_Connection_Cancel(Packet): + """ + + 7.1.7 Create Connection Cancel command + + """ + + name = "HCI_Create_Connection_Cancel" + fields_desc = [LEMACField("bd_addr", None), ] + + +class HCI_Cmd_Accept_Connection_Request(Packet): + """ + + 7.1.8 Accept Connection Request command + + """ + + name = "HCI_Accept_Connection_Request" + fields_desc = [LEMACField("bd_addr", None), + ByteField("role", 0x1), ] + + +class HCI_Cmd_Reject_Connection_Response(Packet): + """ + + 7.1.9 Reject Connection Request command + + """ + name = "HCI_Reject_Connection_Response" + fields_desc = [LEMACField("bd_addr", None), + ByteField("reason", 0x1), ] + + class HCI_Cmd_Link_Key_Request_Reply(Packet): + """ + + 7.1.10 Link Key Request Reply command + + """ + name = "HCI_Link_Key_Request_Reply" fields_desc = [LEMACField("bd_addr", None), NBytesField("link_key", None, 16), ] +class HCI_Cmd_Link_Key_Request_Negative_Reply(Packet): + """ + + 7.1.11 Link Key Request Negative Reply command + + """ + + name = "HCI_Link_Key_Request_Negative_Reply" + fields_desc = [LEMACField("bd_addr", None), ] + + +class HCI_Cmd_PIN_Code_Request_Reply(Packet): + """ + + 7.1.12 PIN Code Request Reply command + + """ + + name = "HCI_PIN_Code_Request_Reply" + fields_desc = [LEMACField("bd_addr", None), + ByteField("pin_code_length", 7), + NBytesField("pin_code", b"\x00" * 16, sz=16), ] + + +class HCI_Cmd_PIN_Code_Request_Negative_Reply(Packet): + """ + + 7.1.13 PIN Code Request Negative Reply command + + """ + + name = "HCI_PIN_Code_Request_Negative_Reply" + fields_desc = [LEMACField("bd_addr", None), ] + + +class HCI_Cmd_Change_Connection_Packet_Type(Packet): + """ + + 7.1.14 Change Connection Packet Type command + + """ + + name = "HCI_Cmd_Change_Connection_Packet_Type" + fields_desc = [XLEShortField("connection_handle", None), + LEShortField("packet_type", 0), ] + + class HCI_Cmd_Authentication_Requested(Packet): + """ + + 7.1.15 Authentication Requested command + + """ + name = "HCI_Authentication_Requested" fields_desc = [LEShortField("handle", 0)] class HCI_Cmd_Set_Connection_Encryption(Packet): + """ + + 7.1.16 Set Connection Encryption command + + """ + name = "HCI_Set_Connection_Encryption" fields_desc = [LEShortField("handle", 0), ByteField("encryption_enable", 0)] +class HCI_Cmd_Change_Connection_Link_Key(Packet): + """ + + 7.1.17 Change Connection Link Key command + + """ + + name = "HCI_Change_Connection_Link_Key" + fields_desc = [LEShortField("handle", 0), ] + + +class HCI_Cmd_Link_Key_Selection(Packet): + """ + + 7.1.18 Change Connection Link Key command + + """ + + name = "HCI_Cmd_Link_Key_Selection" + fields_desc = [ByteEnumField("handle", 0, {0: "Use semi-permanent Link Keys", + 1: "Use Temporary Link Key", }), ] + + class HCI_Cmd_Remote_Name_Request(Packet): + """ + + 7.1.19 Remote Name Request command + + """ + name = "HCI_Remote_Name_Request" fields_desc = [LEMACField("bd_addr", None), ByteField("page_scan_repetition_mode", 0x02), @@ -986,7 +1151,137 @@ class HCI_Cmd_Remote_Name_Request(Packet): LEShortField("clock_offset", 0x0), ] +class HCI_Cmd_Remote_Name_Request_Cancel(Packet): + """ + + 7.1.20 Remote Name Request Cancel command + + """ + + name = "HCI_Remote_Name_Request_Cancel" + fields_desc = [LEMACField("bd_addr", None), ] + + +class HCI_Cmd_Read_Remote_Supported_Features(Packet): + """ + + 7.1.21 Read Remote Supported Features command + + """ + + name = "HCI_Read_Remote_Supported_Features" + fields_desc = [LEShortField("connection_handle", None), ] + + +class HCI_Cmd_Read_Remote_Extended_Features(Packet): + """ + + 7.1.22 Read Remote Extended Features command + + """ + + name = "HCI_Read_Remote_Supported_Features" + fields_desc = [LEShortField("connection_handle", None), + ByteField("page_number", None), ] + + +class HCI_Cmd_IO_Capability_Request_Reply(Packet): + """ + + 7.1.29 IO Capability Request Reply command + + """ + + name = "HCI_Read_Remote_Supported_Features" + fields_desc = [LEMACField("bd_addr", None), + ByteEnumField("io_capability", None, {0x00: "DisplayOnly", + 0x01: "DisplayYesNo", + 0x02: "KeyboardOnly", + 0x03: "NoInputNoOutput", }), + ByteEnumField("oob_data_present", None, {0x00: "Not Present", + 0x01: "P-192", + 0x02: "P-256", + 0x03: "P-192 + P-256", }), + ByteEnumField("authentication_requirement", None, + {0x00: "MITM Not Required", + 0x01: "MITM Required, No Bonding", + 0x02: "MITM Not Required + Dedicated Pairing", + 0x03: "MITM Required + Dedicated Pairing", + 0x04: "MITM Not Required, General Bonding", + 0x05: "MITM Required + General Bonding"}), ] + + +class HCI_Cmd_User_Confirmation_Request_Reply(Packet): + """ + + 7.1.30 User Confirmation Request Reply command + + """ + + name = "HCI_User_Confirmation_Request_Reply" + fields_desc = [LEMACField("bd_addr", None), ] + + +class HCI_Cmd_User_Confirmation_Request_Negative_Reply(Packet): + """ + + 7.1.31 User Confirmation Request Negative Reply command + + """ + + name = "HCI_User_Confirmation_Request_Negative_Reply" + fields_desc = [LEMACField("bd_addr", None), ] + + +class HCI_Cmd_User_Passkey_Request_Reply(Packet): + """ + + 7.1.32 User Passkey Request Reply command + + """ + + name = "HCI_User_Passkey_Request_Reply" + fields_desc = [LEMACField("bd_addr", None), + LEIntField("numeric_value", None), ] + + +class HCI_Cmd_User_Passkey_Request_Negative_Reply(Packet): + """ + + 7.1.33 User Passkey Request Negative Reply command + + """ + + name = "HCI_User_Passkey_Request_Negative_Reply" + fields_desc = [LEMACField("bd_addr", None), ] + + +class HCI_Cmd_Remote_OOB_Data_Request_Reply(Packet): + """ + + 7.1.34 Remote OOB Data Request Reply command + + """ + + name = "HCI_Remote_OOB_Data_Request_Reply" + fields_desc = [LEMACField("bd_addr", None), + NBytesField("C", b"\x00" * 16, sz=16), + NBytesField("R", b"\x00" * 16, sz=16), ] + + +class HCI_Cmd_Remote_OOB_Data_Request_Negative_Reply(Packet): + """ + + 7.1.35 Remote OOB Data Request Negative Reply command + + """ + + name = "HCI_Remote_OOB_Data_Request_Negative_Reply" + fields_desc = [LEMACField("bd_addr", None), ] + # 7.2 Link Policy commands, the OGF is defined as 0x02 + + class HCI_Cmd_Hold_Mode(Packet): name = "HCI_Hold_Mode" fields_desc = [LEShortField("connection_handle", 0), @@ -1223,11 +1518,29 @@ def answers(self, other): return self.payload.answers(other) -class HCI_Event_Connect_Complete(Packet): - name = "Connect Complete" +class HCI_Event_Inquiry_Complete(Packet): + """ + 7.7.1 Inquiry Complete event + """ + + name = "HCI_Inquiry_Complete" + fields_desc = [ByteField("status", 0), ] + + +class HCI_Event_Connection_Complete(Packet): + """ + 7.7.3 Connection Complete event + """ + + name = "HCI_Connection_Complete" fields_desc = [ByteField("status", 0), LEShortField("handle", 0x0100), - LEMACField("bd_addr", None), ] + LEMACField("bd_addr", None), + ByteEnumField("link_type", 0, {0: "SCO connection", + 1: "ACL connection", }), + ByteEnumField("encryption_enaled", 0, + {0: "link level encryption disabled", + 1: "link level encryption enabled", }), ] class HCI_Event_Disconnection_Complete(Packet): @@ -1387,10 +1700,36 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): bind_layers(HCI_Command_Hdr, HCI_Cmd_Exit_Peiodic_Inquiry_Mode, ogf=0x01, ocf=0x0004) bind_layers(HCI_Command_Hdr, HCI_Cmd_Create_Connection, ogf=0x01, ocf=0x0005) bind_layers(HCI_Command_Hdr, HCI_Cmd_Disconnect, ogf=0x01, ocf=0x0006) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Create_Connection_Cancel, ogf=0x01, ocf=0x0008) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Accept_Connection_Request, ogf=0x01, ocf=0x0009) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Reject_Connection_Response, ogf=0x01, ocf=0x000a) bind_layers(HCI_Command_Hdr, HCI_Cmd_Link_Key_Request_Reply, ogf=0x01, ocf=0x000b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Link_Key_Request_Negative_Reply, + ogf=0x01, ocf=0x000c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_PIN_Code_Request_Reply, ogf=0x01, ocf=0x000d) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Change_Connection_Packet_Type, + ogf=0x01, ocf=0x000f) bind_layers(HCI_Command_Hdr, HCI_Cmd_Authentication_Requested, ogf=0x01, ocf=0x0011) bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Connection_Encryption, ogf=0x01, ocf=0x0013) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Change_Connection_Link_Key, ogf=0x01, ocf=0x0017) bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_Name_Request, ogf=0x01, ocf=0x0019) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_Name_Request_Cancel, ogf=0x01, ocf=0x001a) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_Remote_Supported_Features, + ogf=0x01, ocf=0x001b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_Remote_Supported_Features, + ogf=0x01, ocf=0x001c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_IO_Capability_Request_Reply, ogf=0x01, ocf=0x002b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_User_Confirmation_Request_Reply, + ogf=0x01, ocf=0x002c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_User_Confirmation_Request_Negative_Reply, + ogf=0x01, ocf=0x002d) +bind_layers(HCI_Command_Hdr, HCI_Cmd_User_Passkey_Request_Reply, ogf=0x01, ocf=0x002e) +bind_layers(HCI_Command_Hdr, HCI_Cmd_User_Passkey_Request_Negative_Reply, + ogf=0x01, ocf=0x002f) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_OOB_Data_Request_Reply, + ogf=0x01, ocf=0x0030) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_OOB_Data_Request_Negative_Reply, + ogf=0x01, ocf=0x0033) # 7.2 Link Policy commands, the OGF is defined as 0x02 bind_layers(HCI_Command_Hdr, HCI_Cmd_Hold_Mode, ogf=0x02, ocf=0x0001) @@ -1442,7 +1781,9 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply, ogf=0x08, ocf=0x001b) # noqa: E501 # 7.7 EVENTS -bind_layers(HCI_Event_Hdr, HCI_Event_Connect_Complete, code=0x03) +bind_layers(HCI_Event_Hdr, HCI_Event_Inquiry_Complete, code=0x01) + +bind_layers(HCI_Event_Hdr, HCI_Event_Connection_Complete, code=0x03) bind_layers(HCI_Event_Hdr, HCI_Event_Disconnection_Complete, code=0x05) bind_layers(HCI_Event_Hdr, HCI_Event_Remote_Name_Request_Complete, code=0x07) bind_layers(HCI_Event_Hdr, HCI_Event_Encryption_Change, code=0x08) diff --git a/test/scapy/layers/bluetooth.uts b/test/scapy/layers/bluetooth.uts index 1743e6de91c..539a3d97ffd 100644 --- a/test/scapy/layers/bluetooth.uts +++ b/test/scapy/layers/bluetooth.uts @@ -268,10 +268,10 @@ assert expected_cmd_raw_data == cmd_raw_data evt_raw_data = hex_bytes("04030b00000176d56f9501000100") evt_pkt = HCI_Hdr(evt_raw_data) -assert HCI_Event_Connect_Complete in evt_pkt -assert evt_pkt[HCI_Event_Connect_Complete].status == 0 -assert evt_pkt[HCI_Event_Connect_Complete].handle == 256 -assert evt_pkt[HCI_Event_Connect_Complete].bd_addr == "00:01:95:6f:d5:76" +assert HCI_Event_Connection_Complete in evt_pkt +assert evt_pkt[HCI_Event_Connection_Complete].status == 0 +assert evt_pkt[HCI_Event_Connection_Complete].handle == 256 +assert evt_pkt[HCI_Event_Connection_Complete].bd_addr == "00:01:95:6f:d5:76" = Remote Name Request Complete From e9462cb028bc0e2305a21429ad8817f119d2767b Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 2 Sep 2023 09:01:23 +0200 Subject: [PATCH 054/122] Do not compress 1 octet DNS strings (#4110) --- scapy/layers/dns.py | 2 ++ test/scapy/layers/dns.uts | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 893e58ac8ca..4191a7cf665 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -215,6 +215,8 @@ def field_gen(dns_pkt): def possible_shortens(dat): """Iterates through all possible compression parts in a DNS string""" + if dat == b".": # we'd lose by compressing it + return yield dat for x in range(1, dat.count(b".")): yield dat.split(b".", x)[x] diff --git a/test/scapy/layers/dns.uts b/test/scapy/layers/dns.uts index a6644cd149e..a27bec3d91b 100644 --- a/test/scapy/layers/dns.uts +++ b/test/scapy/layers/dns.uts @@ -259,6 +259,14 @@ assert p.qd[0].qname == b'scapy.' assert p.an[0].rrname == b'scapy.' assert p.ar[0].rrname == b'.' += DNS - dns_compress with 1-length strings + +data = b'\xac\x81\x81\x80\x00\x01\x00\x06\x00\r\x00\x00\x04mqtt\x0bweatherflow\x03com\x00\x00\x01\x00\x01\xc0\x0c\x00\x05\x00\x01\x00\x00\x00\xe4\x00 Date: Sat, 2 Sep 2023 12:20:37 +0300 Subject: [PATCH 055/122] [DHCPv4] add the IPv6-Only Preferred Option (#4108) --- scapy/layers/dhcp.py | 1 + test/scapy/layers/dhcp.uts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scapy/layers/dhcp.py b/scapy/layers/dhcp.py index 0e1aef72679..ce493813367 100644 --- a/scapy/layers/dhcp.py +++ b/scapy/layers/dhcp.py @@ -318,6 +318,7 @@ def randval(self): 98: StrField("uap-servers", ""), 100: StrField("pcode", ""), 101: StrField("tcode", ""), + 108: IntField("ipv6-only-preferred", 0), 112: IPField("netinfo-server-address", "0.0.0.0"), 113: StrField("netinfo-server-tag", ""), 114: StrField("captive-portal", ""), diff --git a/test/scapy/layers/dhcp.uts b/test/scapy/layers/dhcp.uts index 8abac11f540..3010d3e0933 100644 --- a/test/scapy/layers/dhcp.uts +++ b/test/scapy/layers/dhcp.uts @@ -44,8 +44,8 @@ s3 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="05:04:03:02:01:00")/DHCP(option ("ieee802-3-encapsulation", 2),("max_dgram_reass_size", 120), ("pxelinux_path_prefix","/some/path"), "end"])) assert s3 == b'E\x00\x01=\x00\x01\x00\x00@\x11{\xad\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01)\x04i\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0005:04:03:02:01:0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc\x02\x04\x00\x00\x00{b\x0fwww.example.comp\x04\n\x00\x00\x01$\x01\x02\x16\x02\x00x\xd2\n/some/path\xff' -s4 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("mud-url", "https://example.org"), ("captive-portal", "https://example.com"), "end"])) -assert s4 == b"E\x00\x017\x00\x01\x00\x00@\x11{\xb3\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01#\xb8\xe7\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0000:01:02:03:04:0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc\xa1\x13https://example.orgr\x13https://example.com\xff" +s4 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("mud-url", "https://example.org"), ("captive-portal", "https://example.com"), ("ipv6-only-preferred", 0xffffffff), "end"])) +assert s4 == b"E\x00\x01=\x00\x01\x00\x00@\x11{\xad\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01)L\xd7\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0000:01:02:03:04:0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc\xa1\x13https://example.orgr\x13https://example.com\x6c\x04\xff\xff\xff\xff\xff" s5 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("classless_static_routes", "192.168.123.4/32:10.0.0.1", "169.254.254.0/24:10.0.1.2"), "end"])) assert s5 == b'E\x00\x01 \x00\x01\x00\x00@\x11{\xca\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01\x0c\xabQ\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0000:01:02:03:04:0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Scy\x11 \xc0\xa8{\x04\n\x00\x00\x01\x18\xa9\xfe\xfe\n\x00\x01\x02\xff' @@ -81,6 +81,7 @@ p4 = IP(s4) assert DHCP in p4 assert p4[DHCP].options[0] == ("mud-url", b"https://example.org") assert p4[DHCP].options[1] == ("captive-portal", b"https://example.com") +assert p4[DHCP].options[2] == ("ipv6-only-preferred", 0xffffffff) p5 = IP(s5) assert DHCP in p5 From f0843cc7ad2d0e6e2a96fdd84fe14a1d5e8d4a8c Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 3 Sep 2023 10:33:13 +0200 Subject: [PATCH 056/122] Add wireshark extcap support (#4101) --- .config/mypy/mypy_enabled.txt | 3 + doc/scapy/layers/bluetooth.rst | 23 +++ doc/scapy/routing.rst | 87 ++++++++--- scapy/arch/__init__.py | 3 + scapy/arch/bpf/core.py | 9 +- scapy/arch/linux.py | 9 +- scapy/arch/windows/__init__.py | 11 +- scapy/arch/windows/structures.py | 62 ++++++++ scapy/config.py | 24 ++- scapy/contrib/nrf_sniffer.py | 154 +++++++++++++++++++ scapy/data.py | 1 + scapy/interfaces.py | 32 ++-- scapy/layers/bluetooth4LE.py | 7 +- scapy/layers/usb.py | 145 +----------------- scapy/libs/extcap.py | 254 +++++++++++++++++++++++++++++++ scapy/route.py | 4 +- scapy/utils.py | 32 +++- test/regression.uts | 89 +++++++++++ test/scapy/layers/usb.uts | 52 ------- 19 files changed, 742 insertions(+), 259 deletions(-) create mode 100644 scapy/contrib/nrf_sniffer.py create mode 100644 scapy/libs/extcap.py diff --git a/.config/mypy/mypy_enabled.txt b/.config/mypy/mypy_enabled.txt index 42f1c6db7b8..6238db12561 100644 --- a/.config/mypy/mypy_enabled.txt +++ b/.config/mypy/mypy_enabled.txt @@ -86,6 +86,9 @@ scapy/contrib/isotp/isotp_utils.py scapy/contrib/roce.py scapy/contrib/tcpao.py +# LIBS +scapy/libs/extcap.py + # TEST test/testsocket.py diff --git a/doc/scapy/layers/bluetooth.rst b/doc/scapy/layers/bluetooth.rst index c9c4a535e1c..d1d9add131b 100644 --- a/doc/scapy/layers/bluetooth.rst +++ b/doc/scapy/layers/bluetooth.rst @@ -654,3 +654,26 @@ Results in the output: | | len= 5 | |###[ Raw ]### | | load= '\x03\x18\xc0\xb5%' + + +Using Nordic Semiconductor's nRF Sniffer +======================================== + +Since **Scapy >2.5.0**, Scapy supports `Wireshark's extcap `_ interfaces. +You can therefore use your USB nordic bluetooth dongle, provided that you `have installed `_ the Wireshark module properly. + +.. code:: pycon + + >>> load_contrib("nrf_sniffer") + >>> load_extcap() + >>> conf.ifaces + Source Index Name Address + nrf_sniffer_ble 100 nRF Sniffer for Bluetooth LE /dev/ttyUSB0-None + [...] + >>> sniff(iface="/dev/ttyUSB0-None", prn=lambda x: x.summary()) + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_IND + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_IND + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_IND + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_NONCONN_IND + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_NONCONN_IND + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_IND diff --git a/doc/scapy/routing.rst b/doc/scapy/routing.rst index a426d9fd93d..eec9dffbca1 100644 --- a/doc/scapy/routing.rst +++ b/doc/scapy/routing.rst @@ -1,36 +1,79 @@ -************* -Scapy routing -************* +******************* +Scapy network stack +******************* -Scapy needs to know many things related to the network configuration of your machine, to be able to route packets properly. For instance, the interface list, the IPv4 and IPv6 routes... +Scapy maintains its own network stack, which is independent from the one of your operating system. +It possesses its own *interfaces list*, *routing table*, *ARP cache*, *IPv6 neighbour* cache, *nameservers* config... and so on, all of which is configurable. -This means that Scapy has implemented bindings to get this information. Those bindings are OS specific. This will show you how to use it for a different usage. +Here are a few examples of where this is used:: +- When you use ``sr()/send()``, Scapy will use internally its own routing table (``conf.route``) in order to find which interface to use, and eventually send an ARP request. +- When using ``dns_resolve()``, Scapy uses its own nameservers list (``conf.nameservers``) to perform the request +- etc. .. note:: - Scapy will have OS-specific functions underlying some high level functions. This page ONLY presents the cross platform ones + What's important to note is that Scapy initializes its own tables by querying the OS-specific ones. + It has therefore implemented bindings for Linux/Windows/BSD.. in order to retrieve such data, which may also be used as a high-level API, documented below. -List interfaces +Interfaces list --------------- -Use ``get_if_list()`` to get the interface list +Scapy stores its interfaces list in the :py:attr:`conf.ifaces ` object. +It provides a few utility functions such as :py:attr:`dev_from_networkname() `, :py:attr:`dev_from_name() ` or :py:attr:`dev_from_index() ` in order to access those. + +.. code-block:: pycon + + >>> conf.ifaces + Source Index Name MAC IPv4 IPv6 + sys 1 lo 00:00:00:00:00:00 127.0.0.1 ::1 + sys 2 eth0 Microsof:12:cb:ef 10.0.0.5 fe80::10a:2bef:dc12:afae + >>> conf.ifaces.dev_from_index(2) + + +You can also use the older ``get_if_list()`` function in order to only get the interface names. .. code-block:: pycon >>> get_if_list() ['lo', 'eth0'] -You can also use the :py:attr:`conf.ifaces ` object to get interfaces. -In this example, the object is first displayed as as column. Then, the :py:attr:`dev_from_index() ` is used to access the interface at index 2. +Extcap interfaces +~~~~~~~~~~~~~~~~~ + +Scapy supports sniffing on `Wireshark's extcap `_ interfaces. You can simply enable it using ``load_extcap()`` (from ``scapy.libs.extcap``). .. code-block:: pycon + >>> load_extcap() >>> conf.ifaces - SRC INDEX IFACE IPv4 IPv6 MAC - sys 2 eth0 10.0.0.5 fe80::10a:2bef:dc12:afae Microsof:12:cb:ef - sys 1 lo 127.0.0.1 ::1 00:00:00:00:00:00 - >>> conf.ifaces.dev_from_index(2) - + Source Index Name Address + ciscodump 100 Cisco remote capture ciscodump + dpauxmon 100 DisplayPort AUX channel monitor capture dpauxmon + randpktdump 100 Random packet generator randpkt + sdjournal 100 systemd Journal Export sdjournal + sshdump 100 SSH remote capture sshdump + udpdump 100 UDP Listener remote capture udpdump + wifidump 100 Wi-Fi remote capture wifidump + Source Index Name MAC IPv4 IPv6 + sys 1 lo 00:00:00:00:00:00 127.0.0.1 ::1 + sys 2 eth0 Microsof:12:cb:ef 10.0.0.5 fe80::10a:2bef:dc12:afae + + +Here's an example of how to use `sshdump `_. As you can see you can pass arguments that are properly converted: + +.. code-block:: pycon + + >>> load_extcap() + >>> sniff( + ... iface="sshdump", + ... prn=lambda x: x.summary(), + ... remote_host="192.168.0.1", + ... remote_username="root", + ... remote_password="SCAPY", + ... ) + + +.. todo:: The sections below can be greatly improved. IPv4 routes ----------- @@ -63,8 +106,8 @@ IPv6 routes Same than IPv4 but with :py:attr:`conf.route6 ` -Get router IP address ---------------------- +Get default gateway IP address +------------------------------ .. code-block:: pycon @@ -72,8 +115,8 @@ Get router IP address >>> gw '10.0.0.1' -Get local IP / IP of an interface ---------------------------------- +Get the IP of an interface +-------------------------- Use ``conf.iface`` @@ -84,8 +127,8 @@ Use ``conf.iface`` >>> ip '10.0.0.5' -Get local MAC / MAC of an interface ------------------------------------ +Get the MAC of an interface +--------------------------- .. code-block:: pycon @@ -97,6 +140,8 @@ Get local MAC / MAC of an interface Get MAC by IP ------------- +This basically performs a cached ARP who-has. + .. code-block:: pycon >>> mac = getmacbyip("10.0.0.1") diff --git a/scapy/arch/__init__.py b/scapy/arch/__init__.py index 0c1fafac153..806e137029c 100644 --- a/scapy/arch/__init__.py +++ b/scapy/arch/__init__.py @@ -24,6 +24,8 @@ from scapy.interfaces import NetworkInterface, network_name from scapy.pton_ntop import inet_pton, inet_ntop +from scapy.libs.extcap import load_extcap + # Typing imports from typing import ( Optional, @@ -47,6 +49,7 @@ "read_nameservers", "read_routes", "read_routes6", + "load_extcap", "SIOCGIFHWADDR", ] diff --git a/scapy/arch/bpf/core.py b/scapy/arch/bpf/core.py index b3439892461..76eb7796cef 100644 --- a/scapy/arch/bpf/core.py +++ b/scapy/arch/bpf/core.py @@ -27,8 +27,11 @@ from scapy.consts import LINUX from scapy.data import ARPHDR_LOOPBACK, ARPHDR_ETHER from scapy.error import Scapy_Exception, warning -from scapy.interfaces import InterfaceProvider, IFACES, NetworkInterface, \ - network_name +from scapy.interfaces import ( + InterfaceProvider, + NetworkInterface, + network_name, +) from scapy.pton_ntop import inet_ntop if LINUX: @@ -250,4 +253,4 @@ def load(self): return data -IFACES.register_provider(BPFInterfaceProvider) +conf.ifaces.register_provider(BPFInterfaceProvider) diff --git a/scapy/arch/linux.py b/scapy/arch/linux.py index d4de5d6995e..d597ca4457a 100644 --- a/scapy/arch/linux.py +++ b/scapy/arch/linux.py @@ -40,8 +40,11 @@ log_runtime, warning, ) -from scapy.interfaces import IFACES, InterfaceProvider, NetworkInterface, \ - network_name +from scapy.interfaces import ( + InterfaceProvider, + NetworkInterface, + network_name, +) from scapy.libs.structures import sock_fprog from scapy.packet import Packet, Padding from scapy.pton_ntop import inet_ntop @@ -449,7 +452,7 @@ def load(self): return data -IFACES.register_provider(LinuxInterfaceProvider) +conf.ifaces.register_provider(LinuxInterfaceProvider) if os.uname()[4] in ['x86_64', 'aarch64']: def get_last_packet_timestamp(sock): diff --git a/scapy/arch/windows/__init__.py b/scapy/arch/windows/__init__.py index e8b59f3a1df..198fcee257f 100755 --- a/scapy/arch/windows/__init__.py +++ b/scapy/arch/windows/__init__.py @@ -189,18 +189,15 @@ def _reload(self): self.hexedit = win_find_exe("hexer") self.sox = win_find_exe("sox") self.wireshark = win_find_exe("wireshark", "wireshark") - self.usbpcapcmd = win_find_exe( - "USBPcapCMD", - installsubdir="USBPcap", - env="programfiles" - ) + self.extcap_folders = [ + os.path.join(os.environ.get("appdata", ""), "Wireshark", "extcap"), + os.path.join(os.environ.get("programfiles", ""), "Wireshark", "extcap"), + ] self.powershell = win_find_exe( "powershell", installsubdir="System32\\WindowsPowerShell\\v1.0", env="SystemRoot" ) - self.cscript = win_find_exe("cscript", installsubdir="System32", - env="SystemRoot") self.cmd = win_find_exe("cmd", installsubdir="System32", env="SystemRoot") if self.wireshark: diff --git a/scapy/arch/windows/structures.py b/scapy/arch/windows/structures.py index 90399d62535..a74cce21118 100644 --- a/scapy/arch/windows/structures.py +++ b/scapy/arch/windows/structures.py @@ -23,13 +23,16 @@ from scapy.config import conf from scapy.consts import WINDOWS_XP +from scapy.data import MTU # Typing imports from typing import ( Any, Dict, + IO, List, Optional, + Tuple, ) ANY_SIZE = 65500 # FIXME quite inefficient :/ @@ -42,6 +45,7 @@ ULONG = ctypes.wintypes.ULONG ULONGLONG = ctypes.c_ulonglong HANDLE = ctypes.wintypes.HANDLE +LPVOID = ctypes.wintypes.LPVOID LPWSTR = ctypes.wintypes.LPWSTR VOID = ctypes.c_void_p INT = ctypes.c_int @@ -607,3 +611,61 @@ def GetIpForwardTable2(AF=AddressFamily.AF_UNSPEC): results.append(_struct_to_dict(table.contents.Table[i])) _FreeMibTable(table) return results + + +############## +#### FIFO #### +############## + +class _SECURITY_ATTRIBUTES(Structure): + _fields_ = [("nLength", DWORD), + ("lpSecurityDescriptor", LPVOID), + ("bInheritHandle", BOOL)] + + +LPSECURITY_ATTRIBUTES = POINTER(_SECURITY_ATTRIBUTES) + + +def _get_win_fifo() -> Tuple[str, Any]: + """Create a windows fifo and returns the (client file, server fd) + """ + from scapy.volatile import RandString + f = r"\\.\pipe\scapy%s" % str(RandString(6)) + buffer = create_string_buffer(ctypes.sizeof(_SECURITY_ATTRIBUTES)) + sec = ctypes.cast(buffer, LPSECURITY_ATTRIBUTES) + sec.contents.nLength = ctypes.sizeof(_SECURITY_ATTRIBUTES) + res = ctypes.windll.kernel32.CreateNamedPipeA( + create_string_buffer(f.encode()), + 0x00000003 | 0x40000000, + 0, + 1, 65536, 65536, + 300, + sec, + ) + if res == -1: + raise OSError(ctypes.FormatError()) + return f, res + + +def _win_fifo_open(fd: Any) -> IO[bytes]: + """Connect NamedPipe and return a fake open() file + """ + ctypes.windll.kernel32.ConnectNamedPipe(fd, None) + + class _opened(IO[bytes]): + def read(self, x: int = MTU) -> bytes: + buf = ctypes.create_string_buffer(x) + res = ctypes.windll.kernel32.ReadFile( + fd, + buf, + x, + None, + None, + ) + if res == 0: + raise OSError(ctypes.FormatError()) + return buf.raw + def close(self) -> None: + # ignore failures + ctypes.windll.kernel32.CloseHandle(fd) + return _opened() # type: ignore \ No newline at end of file diff --git a/scapy/config.py b/scapy/config.py index 639d19e7556..4ffc88324d7 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -148,6 +148,10 @@ class ProgPath(ConfClass): tshark = "tshark" wireshark = "wireshark" ifconfig = "ifconfig" + extcap_folders = [ + os.path.join(os.path.expanduser("~"), ".config", "wireshark", "extcap"), + "/usr/lib/x86_64-linux-gnu/wireshark/extcap", + ] class ConfigFieldList: @@ -616,9 +620,7 @@ def _set_conf_sockets(): L3pcapSocket, filter="ip6") conf.L2socket = L2pcapSocket conf.L2listen = L2pcapListenSocket - conf.ifaces.reload() - return - if conf.use_bpf: + elif conf.use_bpf: from scapy.arch.bpf.supersocket import L2bpfListenSocket, \ L2bpfSocket, L3bpfSocket conf.L3socket = L3bpfSocket @@ -626,9 +628,7 @@ def _set_conf_sockets(): L3bpfSocket, filter="ip6") conf.L2socket = L2bpfSocket conf.L2listen = L2bpfListenSocket - conf.ifaces.reload() - return - if LINUX: + elif LINUX: from scapy.arch.linux import L3PacketSocket, L2Socket, L2ListenSocket conf.L3socket = L3PacketSocket conf.L3socket6 = cast( @@ -640,23 +640,20 @@ def _set_conf_sockets(): ) conf.L2socket = L2Socket conf.L2listen = L2ListenSocket - conf.ifaces.reload() - return - if WINDOWS: + elif WINDOWS: from scapy.arch.windows import _NotAvailableSocket from scapy.arch.windows.native import L3WinSocket, L3WinSocket6 conf.L3socket = L3WinSocket conf.L3socket6 = L3WinSocket6 conf.L2socket = _NotAvailableSocket conf.L2listen = _NotAvailableSocket - conf.ifaces.reload() - # No need to update globals on Windows - return else: from scapy.supersocket import L3RawSocket from scapy.layers.inet6 import L3RawSocket6 conf.L3socket = L3RawSocket conf.L3socket6 = L3RawSocket6 + # Reload the interfaces + conf.ifaces.reload() def _socket_changer(attr, val, old): @@ -761,7 +758,6 @@ class Conf(ConfClass): L2socket = None # type: Type[scapy.supersocket.SuperSocket] L2listen = None # type: Type[scapy.supersocket.SuperSocket] BTsocket = None # type: Type[scapy.supersocket.SuperSocket] - USBsocket = None # type: Type[scapy.supersocket.SuperSocket] min_pkt_size = 60 #: holds MIB direct access dictionary mib = None # type: 'scapy.asn1.mib.MIBDict' @@ -827,8 +823,6 @@ class Conf(ConfClass): use_bpf = Interceptor("use_bpf", False, _socket_changer) use_npcap = False ipv6_enabled = socket.has_ipv6 - #: path or list of paths where extensions are to be looked for - extensions_paths = "." stats_classic_protocols = [] # type: List[Type[Packet]] stats_dot11_protocols = [] # type: List[Type[Packet]] temp_files = [] # type: List[str] diff --git a/scapy/contrib/nrf_sniffer.py b/scapy/contrib/nrf_sniffer.py new file mode 100644 index 00000000000..86ba91dfce2 --- /dev/null +++ b/scapy/contrib/nrf_sniffer.py @@ -0,0 +1,154 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Michael Farrell + +""" +nRF sniffer + +Firmware and documentation related to this module is available at: +https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-Sniffer +https://github.com/adafruit/Adafruit_BLESniffer_Python +https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-nordic_ble.c +""" + +# scapy.contrib.description = nRF sniffer +# scapy.contrib.status = works + +import struct + +from scapy.config import conf +from scapy.data import DLT_NORDIC_BLE +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + LEIntField, + LEShortField, + LenField, + ScalingField, +) +from scapy.layers.bluetooth4LE import BTLE +from scapy.packet import Packet, bind_layers + + +# nRF Sniffer v2 + + +class NRFS2_Packet(Packet): + """ + nRF Sniffer v2 Packet + """ + + fields_desc = [ + LenField("len", None, fmt=" Tuple[str, str, str, List[str], List[str]] + # type: (...) -> Tuple[Union[str, List[str]], ...] """Returns the elements used by show() If a tuple is returned, this consist of the strings that will be @@ -97,6 +97,12 @@ def _format(self, index = str(dev.index) return (index, dev.description, mac or "", dev.ips[4], dev.ips[6]) + def __repr__(self) -> str: + """ + repr + """ + return "" % self.name + class NetworkInterface(object): def __init__(self, @@ -171,7 +177,7 @@ def l2listen(self): # type: () -> Type[scapy.supersocket.SuperSocket] return self.provider._l2listen(self) - def l3socket(self, ipv6): + def l3socket(self, ipv6=False): # type: (bool) -> Type[scapy.supersocket.SuperSocket] return self.provider._l3socket(self, ipv6) @@ -222,6 +228,9 @@ def register_provider(self, provider): # type: (type) -> None prov = provider() self.providers[provider] = prov + if self.data: + # late registration + self._load(prov.reload(), prov) def load_confiface(self): # type: () -> None @@ -242,8 +251,10 @@ def _reload_provs(self): def reload(self): # type: () -> None self._reload_provs() - if conf.route: - self.load_confiface() + if not conf.route: + # routes are not loaded yet. + return + self.load_confiface() def dev_from_name(self, name): # type: (str) -> NetworkInterface @@ -326,15 +337,16 @@ def show(self, print_result=True, hidden=False, **kwargs): if not hidden and not dev.is_valid(): continue prov = dev.provider - res[prov].append( + res[(prov.headers, prov.header_sort)].append( (prov.name,) + prov._format(dev, **kwargs) ) output = "" - for provider in res: + for key in res: + hdrs, sortBy = key output += pretty_list( - res[provider], # type: ignore - [("Source",) + provider.headers], - sortBy=provider.header_sort + res[key], + [("Source",) + hdrs], + sortBy=sortBy ) + "\n" output = output[:-1] if print_result: diff --git a/scapy/layers/bluetooth4LE.py b/scapy/layers/bluetooth4LE.py index cd1abb429c8..0611d3edd26 100644 --- a/scapy/layers/bluetooth4LE.py +++ b/scapy/layers/bluetooth4LE.py @@ -11,8 +11,11 @@ from scapy.compat import orb, chb from scapy.config import conf -from scapy.data import DLT_BLUETOOTH_LE_LL, DLT_BLUETOOTH_LE_LL_WITH_PHDR, \ - PPI_BTLE +from scapy.data import ( + DLT_BLUETOOTH_LE_LL, + DLT_BLUETOOTH_LE_LL_WITH_PHDR, + PPI_BTLE, +) from scapy.packet import Packet, bind_layers from scapy.fields import ( BitEnumField, diff --git a/scapy/layers/usb.py b/scapy/layers/usb.py index c7313b862f3..d5a7f39837c 100644 --- a/scapy/layers/usb.py +++ b/scapy/layers/usb.py @@ -10,22 +10,14 @@ # TODO: support USB headers for Linux and Darwin (usbmon/netmon) # https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-usb.c # noqa: E501 -import re -import subprocess - from scapy.config import conf -from scapy.consts import WINDOWS -from scapy.compat import chb, plain_str -from scapy.data import MTU, DLT_USBPCAP -from scapy.error import warning +from scapy.compat import chb +from scapy.data import DLT_USBPCAP from scapy.fields import ByteField, XByteField, ByteEnumField, LEShortField, \ LEShortEnumField, LEIntField, LEIntEnumField, XLELongField, \ LenField -from scapy.interfaces import NetworkInterface, InterfaceProvider, \ - network_name, IFACES from scapy.packet import Packet, bind_top_down -from scapy.supersocket import SuperSocket -from scapy.utils import PcapReader + # USBpcap @@ -152,134 +144,3 @@ class USBpcapTransferControl(Packet): bind_top_down(USBpcap, USBpcapTransferControl, transfer=2) conf.l2types.register(DLT_USBPCAP, USBpcap) - - -def _extcap_call(prog, args, keyword, values): - """Function used to call a program using the extcap format, - then parse the results""" - p = subprocess.Popen( - [prog] + args, - stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - data, err = p.communicate() - if p.returncode != 0: - raise OSError("%s returned with error code %s: %s" % (prog, - p.returncode, - err)) - data = plain_str(data) - res = [] - for ifa in data.split("\n"): - ifa = ifa.strip() - if not ifa.startswith(keyword): - continue - res.append(tuple([re.search(r"{%s=([^}]*)}" % val, ifa).group(1) - for val in values])) - return res - - -if WINDOWS: - def _usbpcap_check(): - if not conf.prog.usbpcapcmd: - raise OSError("USBpcap is not installed ! (USBpcapCMD not found)") - - def get_usbpcap_interfaces(): - """Return a list of available USBpcap interfaces""" - _usbpcap_check() - return _extcap_call( - conf.prog.usbpcapcmd, - ["--extcap-interfaces"], - "interface", - ["value", "display"] - ) - - class UsbpcapInterfaceProvider(InterfaceProvider): - name = "USBPcap" - headers = ("Index", "Name", "Address") - header_sort = 1 - - def load(self): - data = {} - try: - interfaces = get_usbpcap_interfaces() - except OSError: - return {} - for netw_name, name in interfaces: - index = re.search(r".*(\d+)", name) - if index: - index = int(index.group(1)) + 100 - else: - index = 100 - if_data = { - "name": name, - "network_name": netw_name, - "description": name, - "index": index, - } - data[netw_name] = NetworkInterface(self, if_data) - return data - - def l2socket(self): - return conf.USBsocket - l2listen = l2socket - - def l3socket(self): - raise ValueError("No L3 available for USBpcap !") - - def _format(self, dev, **kwargs): - """Returns a tuple of the elements used by show()""" - return (str(dev.index), dev.name, dev.network_name) - - IFACES.register_provider(UsbpcapInterfaceProvider) - - def get_usbpcap_devices(iface, enabled=True): - """Return a list of devices on an USBpcap interface""" - _usbpcap_check() - devices = _extcap_call( - conf.prog.usbpcapcmd, - ["--extcap-interface", - iface, - "--extcap-config"], - "value", - ["value", "display", "enabled"] - ) - devices = [(dev[0], - dev[1], - dev[2] == "true") for dev in devices] - if enabled: - return [dev for dev in devices if dev[2]] - return devices - - class USBpcapSocket(SuperSocket): - """ - Read packets at layer 2 using USBPcapCMD - """ - nonblocking_socket = True - - @staticmethod - def select(sockets, remain=None): - return sockets - - def __init__(self, iface=None, *args, **karg): - _usbpcap_check() - if iface is None: - warning("Available interfaces: [%s]", - " ".join(x[0] for x in get_usbpcap_interfaces())) - raise NameError("No interface specified !" - " See get_usbpcap_interfaces()") - iface = network_name(iface) - self.outs = None - args = ['-d', iface, '-b', '134217728', '-A', '-o', '-'] - self.usbpcap_proc = subprocess.Popen( - [conf.prog.usbpcapcmd] + args, - stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - self.ins = PcapReader(self.usbpcap_proc.stdout) - - def recv(self, x=MTU): - return self.ins.recv(x) - - def close(self): - SuperSocket.close(self) - self.usbpcap_proc.kill() - - conf.USBsocket = USBpcapSocket diff --git a/scapy/libs/extcap.py b/scapy/libs/extcap.py new file mode 100644 index 00000000000..b61912e057a --- /dev/null +++ b/scapy/libs/extcap.py @@ -0,0 +1,254 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Wireshark extcap API utils +https://www.wireshark.org/docs/wsdg_html_chunked/ChCaptureExtcap.html +""" + +import collections +import functools +import pathlib +import re +import subprocess + +from scapy.config import conf +from scapy.consts import WINDOWS +from scapy.data import MTU +from scapy.error import warning +from scapy.interfaces import ( + network_name, + resolve_iface, + InterfaceProvider, + NetworkInterface, +) +from scapy.packet import Packet +from scapy.supersocket import SuperSocket +from scapy.utils import PcapReader, _create_fifo, _open_fifo + +# Typing +from typing import ( + cast, + Any, + Dict, + List, + NoReturn, + Optional, + Tuple, + Type, + Union, +) + + +def _extcap_call(prog: str, + args: List[str], + format: Dict[str, List[str]], + ) -> Dict[str, List[Tuple[str, ...]]]: + """ + Function used to call a program using the extcap format, + then parse the results + """ + p = subprocess.Popen( + [prog] + args, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True + ) + data, err = p.communicate() + if p.returncode != 0: + raise OSError("%s returned with error code %s: %s" % (prog, p.returncode, err)) + res = collections.defaultdict(list) + for ifa in data.split("\n"): + ifa = ifa.strip() + for keyword, values in format.items(): + if not ifa.startswith(keyword): + continue + + def _match(val: str, ifa: str) -> str: + m = re.search(r"{%s=([^}]*)}" % val, ifa) + if m: + return m.group(1) + return "" + res[keyword].append( + tuple( + [_match(val, ifa) for val in values] + ) + ) + break + return cast(Dict[str, List[Tuple[str, ...]]], res) + + +class _ExtcapNetworkInterface(NetworkInterface): + """ + Extcap NetworkInterface + """ + + def get_extcap_config(self) -> Dict[str, Tuple[str, ...]]: + """ + Return a list of available configuration options on an extcap interface + """ + return _extcap_call( + self.provider.cmdprog, # type: ignore + ["--extcap-interface", self.network_name, "--extcap-config"], + { + "arg": ["number", "call", "display", "default", "required"], + "value": ["arg", "value", "display", "default"], + }, + ) + + def get_extcap_cmd(self, **kwarg: Dict[str, str]) -> List[str]: + """ + Return the extcap command line options + """ + cmds = [] + for x in self.get_extcap_config()["arg"]: + key = x[1].strip("-").replace("-", "_") + if key in kwarg: + # Apply argument + cmds += [x[1], str(kwarg[key])] + else: + # Apply default + if x[4] == "true": # required + raise ValueError( + "Missing required argument: '%s' on iface %s." % ( + key, + self.network_name, + ) + ) + elif not x[3] or x[3] == "false": # no default (or false) + continue + if x[3] == "true": + cmds += [x[1]] + else: + cmds += [x[1], x[3]] + return cmds + + +class _ExtcapSocket(SuperSocket): + """ + Read packets at layer 2 using an extcap command + """ + + nonblocking_socket = True + + @staticmethod + def select(sockets: List[SuperSocket], + remain: Optional[float] = None) -> List[SuperSocket]: + return sockets + + def __init__(self, *_: Any, **kwarg: Any) -> None: + cmdprog = kwarg.pop("cmdprog") + iface = kwarg.pop("iface", None) + if iface is None: + raise NameError("Must select an interface for a extcap socket !") + iface = resolve_iface(iface) + if not isinstance(iface, _ExtcapNetworkInterface): + raise ValueError("Interface should be an _ExtcapNetworkInterface") + args = iface.get_extcap_cmd(**kwarg) + iface = network_name(iface) + self.outs = None # extcap sockets can't write + # open fifo + fifo, fd = _create_fifo() + args = ["--extcap-interface", iface, "--capture", "--fifo", fifo] + args + self.proc = subprocess.Popen( + [cmdprog] + args, + ) + self.fd = _open_fifo(fd) + self.reader = PcapReader(self.fd) # type: ignore + self.ins = self.reader # type: ignore + + def recv(self, x: int = MTU) -> Packet: + return self.reader.recv(x) + + def close(self) -> None: + self.proc.kill() + self.proc.wait(timeout=2) + SuperSocket.close(self) + self.fd.close() + + +class _ExtcapInterfaceProvider(InterfaceProvider): + """ + Interface provider made to hook on a extcap binary + """ + + headers = ("Index", "Name", "Address") + header_sort = 1 + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.cmdprog = kwargs.pop("cmdprog") + super(_ExtcapInterfaceProvider, self).__init__(*args, **kwargs) + + def load(self) -> Dict[str, NetworkInterface]: + data: Dict[str, NetworkInterface] = {} + try: + interfaces = _extcap_call( + self.cmdprog, + ["--extcap-interfaces"], + {"interface": ["value", "display"]}, + )["interface"] + except OSError as ex: + warning( + "extcap %s failed to load: %s", + self.name, + str(ex).strip().split("\n")[-1] + ) + return {} + for netw_name, name in interfaces: + _index = re.search(r".*(\d+)", name) + if _index: + index = int(_index.group(1)) + 100 + else: + index = 100 + if_data = { + "name": name, + "network_name": netw_name, + "description": name, + "index": index, + } + data[netw_name] = _ExtcapNetworkInterface(self, if_data) + return data + + def _l2listen(self, _: Any) -> Type[SuperSocket]: + return functools.partial(_ExtcapSocket, cmdprog=self.cmdprog) # type: ignore + + def _l3socket(self, *_: Any) -> NoReturn: + raise ValueError("Only sniffing is available for an extcap provider !") + + _l2socket = _l3socket # type: ignore + + def _is_valid(self, dev: NetworkInterface) -> bool: + return True + + def _format(self, + dev: NetworkInterface, + **kwargs: Any + ) -> Tuple[Union[str, List[str]], ...]: + """Returns a tuple of the elements used by show()""" + return (str(dev.index), dev.name, dev.network_name) + + +def load_extcap() -> None: + """ + Load extcap folder from wireshark and populate providers + """ + if WINDOWS: + pattern = re.compile(r"^[^.]+(?:\.bat|\.exe)?$") + else: + pattern = re.compile(r"^[^.]+(?:\.sh)?$") + for fld in conf.prog.extcap_folders: + root = pathlib.Path(fld) + for _cmdprog in root.glob("*"): + if not _cmdprog.is_file() or not pattern.match(_cmdprog.name): + continue + cmdprog = str((root / _cmdprog).absolute()) + # success + provname = pathlib.Path(cmdprog).name.rsplit(".", 1)[0] + + class _prov(_ExtcapInterfaceProvider): + name = provname + + conf.ifaces.register_provider( + functools.partial(_prov, cmdprog=cmdprog) # type: ignore + ) diff --git a/scapy/route.py b/scapy/route.py index cb36fa181ab..a811aff9e0f 100644 --- a/scapy/route.py +++ b/scapy/route.py @@ -215,5 +215,5 @@ def get_if_bcast(self, iff): conf.route = Route() -# Load everything, update conf.iface -conf.ifaces.reload() +# Update conf.iface +conf.ifaces.load_confiface() diff --git a/scapy/utils.py b/scapy/utils.py index 1b58464f343..6c6fd2dfa7e 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -181,12 +181,12 @@ def get_temp_file(keep, autoext, fd): @overload -def get_temp_file(keep=False, autoext="", fd=False): # noqa: F811 +def get_temp_file(keep=False, autoext="", fd=False): # type: (bool, str, Literal[False]) -> str pass -def get_temp_file(keep=False, autoext="", fd=False): # noqa: F811 +def get_temp_file(keep=False, autoext="", fd=False): # type: (bool, str, bool) -> Union[IO[bytes], str] """Creates a temporary file. @@ -225,6 +225,34 @@ def get_temp_dir(keep=False): return dname +def _create_fifo() -> Tuple[str, Any]: + """Creates a temporary fifo. + + You must then use open_fifo() on the server_fd once + the client is connected to use it. + + :returns: (client_file, server_fd) + """ + if WINDOWS: + from scapy.arch.windows.structures import _get_win_fifo + return _get_win_fifo() + else: + f = get_temp_file() + os.unlink(f) + os.mkfifo(f) + return f, f + + +def _open_fifo(fd: Any, mode: str = "rb") -> IO[bytes]: + """Open the server_fd (see create_fifo) + """ + if WINDOWS: + from scapy.arch.windows.structures import _win_fifo_open + return _win_fifo_open(fd) + else: + return open(fd, mode) + + def sane(x, color=False): # type: (AnyStr, bool) -> str r = "" diff --git a/test/regression.uts b/test/regression.uts index 3f36ec9c47a..c1a1ad0c7f6 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -345,6 +345,95 @@ assert output == data conf.ifaces.reload() += Test extcap detection in conf.ifaces +~ linux extcap + +import os +from scapy.libs.extcap import load_extcap + +_bkp_extcap = conf.prog.extcap_folders +_bkp_providers = conf.ifaces.providers.copy() + +conf.ifaces.providers.clear() + +# Create some sort of extcap parody program +extcapfld = get_temp_dir() +extcapprog = os.path.join(extcapfld, "runner.sh") +data = """#!/usr/bin/env python3 + +import struct +import argparse +parser = argparse.ArgumentParser() +parser.add_argument('--extcap-interfaces', action='store_true') +parser.add_argument('--capture', action='store_true') +parser.add_argument('--extcap-config', action='store_true') +parser.add_argument('--scan-follow-rsp', action='store_true') +parser.add_argument('--scan-follow-aux', action='store_true') +parser.add_argument('--extcap-interface', type=str) +parser.add_argument('--fifo', type=str) + +args = parser.parse_args() +if args.extcap_interfaces: + # List interfaces + print(bytes.fromhex("0a657874636170207b76657273696f6e3d342e312e317d7b646973706c61793d6e524620536e696666657220666f7220426c7565746f6f7468204c457d7b68656c703d68747470733a2f2f7777772e6e6f7264696373656d692e636f6d2f536f6674776172652d616e642d546f6f6c732f446576656c6f706d656e742d546f6f6c732f6e52462d536e69666665722d666f722d426c7565746f6f74682d4c457d0a696e74657266616365207b76616c75653d2f6465762f747479555342352d4e6f6e657d7b646973706c61793d6e524620536e696666657220666f7220426c7565746f6f7468204c457d0a636f6e74726f6c207b6e756d6265723d307d7b747970653d73656c6563746f727d7b646973706c61793d4465766963657d7b746f6f6c7469703d446576696365206c6973747d0a636f6e74726f6c207b6e756d6265723d317d7b747970653d73656c6563746f727d7b646973706c61793d4b65797d7b746f6f6c7469703d7d0a636f6e74726f6c207b6e756d6265723d327d7b747970653d737472696e677d7b646973706c61793d56616c75657d7b746f6f6c7469703d3620646967697420706173736b6579206f72203136206f7220333220627974657320656e6372797074696f6e206b657920696e2068657861646563696d616c207374617274696e67207769746820273078272c2062696720656e6469616e20666f726d61742e49662074686520656e7465726564206b65792069732073686f72746572207468616e203136206f722033322062797465732c2069742077696c6c206265207a65726f2d70616464656420696e2066726f6e74277d7b76616c69646174696f6e3d5c625e28285b302d395d7b367d297c2830785b302d39612d66412d465d7b312c36347d297c285b302d39412d46612d665d7b327d5b3a2d5d297b357d285b302d39412d46612d665d7b327d2920287075626c69637c72616e646f6d2929245c627d0a636f6e74726f6c207b6e756d6265723d337d7b747970653d737472696e677d7b646973706c61793d41647620486f707d7b64656661756c743d33372c33382c33397d7b746f6f6c7469703d4164766572746973696e67206368616e6e656c20686f702073657175656e63652e204368616e676520746865206f7264657220696e2077686963682074686520736e6966666572207377697463686573206164766572746973696e67206368616e6e656c732e2056616c6964206368616e6e656c73206172652033372c20333820616e642033392073657061726174656420627920636f6d6d612e7d7b76616c69646174696f6e3d5e5c732a282833377c33387c3339295c732a2c5c732a297b302c327d2833377c33387c3339297b317d5c732a247d7b72657175697265643d747275657d0a636f6e74726f6c207b6e756d6265723d377d7b747970653d627574746f6e7d7b646973706c61793d436c6561727d7b746f6f6c746f703d436c656172206f722072656d6f7665206465766963652066726f6d20446576696365206c6973747d0a636f6e74726f6c207b6e756d6265723d347d7b747970653d627574746f6e7d7b726f6c653d68656c707d7b646973706c61793d48656c707d7b746f6f6c7469703d416363657373207573657220677569646520286c61756e636865732062726f77736572297d0a636f6e74726f6c207b6e756d6265723d357d7b747970653d627574746f6e7d7b726f6c653d726573746f72657d7b646973706c61793d44656661756c74737d7b746f6f6c7469703d52657365747320746865207573657220696e7465726661636520616e6420636c6561727320746865206c6f672066696c657d0a636f6e74726f6c207b6e756d6265723d367d7b747970653d627574746f6e7d7b726f6c653d6c6f676765727d7b646973706c61793d4c6f677d7b746f6f6c7469703d4c6f672070657220696e746572666163657d0a76616c7565207b636f6e74726f6c3d307d7b76616c75653d207d7b646973706c61793d416c6c206164766572746973696e6720646576696365737d7b64656661756c743d747275657d0a76616c7565207b636f6e74726f6c3d307d7b76616c75653d5b30302c30302c30302c30302c30302c30302c305d7d7b646973706c61793d466f6c6c6f772049524b7d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d307d7b646973706c61793d4c656761637920506173736b65797d7b64656661756c743d747275657d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d317d7b646973706c61793d4c6567616379204f4f4220646174617d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d327d7b646973706c61793d4c6567616379204c544b7d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d337d7b646973706c61793d5343204c544b7d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d347d7b646973706c61793d53432050726976617465204b65797d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d357d7b646973706c61793d49524b7d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d367d7b646973706c61793d416464204c4520616464726573737d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d377d7b646973706c61793d466f6c6c6f77204c4520616464726573737d").decode()) +elif args.extcap_interface and args.extcap_config: + # List config + print(bytes.fromhex("617267207b6e756d6265723d307d7b63616c6c3d2d2d6f6e6c792d6164766572746973696e677d7b646973706c61793d4f6e6c79206164766572746973696e67207061636b6574737d7b746f6f6c7469703d54686520736e69666665722077696c6c206f6e6c792063617074757265206164766572746973696e67207061636b6574732066726f6d207468652073656c6563746564206465766963657d7b747970653d626f6f6c666c61677d7b736176653d747275657d0a617267207b6e756d6265723d317d7b63616c6c3d2d2d6f6e6c792d6c65676163792d6164766572746973696e677d7b646973706c61793d4f6e6c79206c6567616379206164766572746973696e67207061636b6574737d7b746f6f6c7469703d54686520736e69666665722077696c6c206f6e6c792063617074757265206c6567616379206164766572746973696e67207061636b6574732066726f6d207468652073656c6563746564206465766963657d7b747970653d626f6f6c666c61677d7b736176653d747275657d0a617267207b6e756d6265723d327d7b63616c6c3d2d2d7363616e2d666f6c6c6f772d7273707d7b646973706c61793d46696e64207363616e20726573706f6e736520646174617d7b746f6f6c7469703d54686520736e69666665722077696c6c20666f6c6c6f77207363616e20726571756573747320616e64207363616e20726573706f6e73657320696e207363616e206d6f64657d7b747970653d626f6f6c666c61677d7b64656661756c743d747275657d7b736176653d747275657d0a617267207b6e756d6265723d337d7b63616c6c3d2d2d7363616e2d666f6c6c6f772d6175787d7b646973706c61793d46696e6420617578696c6961727920706f696e74657220646174617d7b746f6f6c7469703d54686520736e69666665722077696c6c20666f6c6c6f772061757820706f696e7465727320696e207363616e206d6f64657d7b747970653d626f6f6c666c61677d7b64656661756c743d747275657d7b736176653d747275657d0a617267207b6e756d6265723d337d7b63616c6c3d2d2d636f6465647d7b646973706c61793d5363616e20616e6420666f6c6c6f772064657669636573206f6e204c4520436f646564205048597d7b746f6f6c7469703d5363616e20666f72206465766963657320616e6420666f6c6c6f772061647665727469736572206f6e204c4520436f646564205048597d7b747970653d626f6f6c666c61677d7b64656661756c743d66616c73657d7b736176653d747275657d").decode()) +elif args.capture and args.extcap_interface and args.fifo: + # Capture + pkts = [ + bytes.fromhex("ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172") + ] + with open(args.fifo, "wb", 0) as fd: + # header + fd.write( + struct.pack( + "IHHIIII", + 0xa1b2c3d4, + 2, 4, 0, 0, 65535, 1 + ) + ) + for pkt in pkts: + fd.write(struct.pack("IIII", 0, 0, len(pkt), len(pkt))) + fd.write(bytes(pkt)) +else: + raise ValueError("Bad arguments") +""".strip() +with open(extcapprog, "w") as fd: + fd.write(data) + +print(data) + +os.chmod(extcapprog, 0o777) + +# Inject and load provider +conf.prog.extcap_folders = [extcapfld] +load_extcap() +print(conf.ifaces.providers) +conf.ifaces.reload() + +# Now do the tests +iface = conf.ifaces.dev_from_networkname('/dev/ttyUSB5-None') +assert iface.name == "nRF Sniffer for Bluetooth LE" +sock = iface.l2listen()(iface=iface) +pkts = sock.sniff(timeout=2) +sock.close() +assert UDP in pkts[0] + +config = iface.get_extcap_config() +assert config["arg"] == [ + ('0', '--only-advertising', 'Only advertising packets', '', ''), + ('1', '--only-legacy-advertising', 'Only legacy advertising packets', '', ''), + ('2', '--scan-follow-rsp', 'Find scan response data', 'true', ''), + ('3', '--scan-follow-aux', 'Find auxiliary pointer data', 'true', ''), + ('3', '--coded', 'Scan and follow devices on LE Coded PHY', 'false', '') +] + +# Restore +conf.prog.extcap_folders = _bkp_extcap +conf.ifaces.providers = _bkp_providers +conf.ifaces.reload() + = Test read_routes6() - default output routes6 = read_routes6() diff --git a/test/scapy/layers/usb.uts b/test/scapy/layers/usb.uts index d70878b2145..1ef2aaf197f 100644 --- a/test/scapy/layers/usb.uts +++ b/test/scapy/layers/usb.uts @@ -36,55 +36,3 @@ assert raw(pkt) == b"'\x00u\x925\x00\x00\x00\x00\x00\x00\x00\x00\x005\x12\n#\x00 pkt = USBpcap(irpId=0x359275, function=0x1235, info=10, bus=35)/USBpcapTransferControl(stage=11) assert raw(pkt) == b'\x1c\x00u\x925\x00\x00\x00\x00\x00\x00\x00\x00\x005\x12\n#\x00\x00\x00\x00\x02\x01\x00\x00\x00\x0b' - -= mocked get_usbpcap_interfaces() -~ mock windows - -import mock - -@mock.patch("scapy.layers.usb.subprocess.Popen") -def test_get_usbpcap_interfaces(mock_Popen): - conf.prog.usbpcapcmd = "C:/the_program_is_not_installed__test_only" - data = """ -interface {value=\\\\.\\USBPcap1}{display=USBPcap1} -""" - mock_Popen.side_effect = lambda *args, **kwargs: Bunch(returncode=0, communicate=(lambda *args, **kargs: (data,None))) - assert get_usbpcap_interfaces() == [('\\\\.\\USBPcap1', 'USBPcap1')] - - -test_get_usbpcap_interfaces() - -= mocked get_usbpcap_devices() -~ mock windows - -import mock - -@mock.patch("scapy.layers.usb.subprocess.Popen") -def test_get_usbpcap_devices(mock_Popen): - conf.prog.usbpcapcmd = "C:/the_program_is_not_installed__test_only" - data = """ -arg {number=0}{call=--snaplen}{display=Snapshot length}{tooltip=Snapshot length}{type=integer}{range=0,65535}{default=65535} -arg {number=1}{call=--bufferlen}{display=Capture buffer length}{tooltip=USBPcap kernel-mode capture buffer length in bytes}{type=integer}{range=0,134217728}{default=1048576} -arg {number=2}{call=--capture-from-all-devices}{display=Capture from all devices connected}{tooltip=Capture from all devices connected despite other options}{type=boolflag}{default=true} -arg {number=3}{call=--capture-from-new-devices}{display=Capture from newly connected devices}{tooltip=Automatically start capture on all newly connected devices}{type=boolflag}{default=true} -arg {number=99}{call=--devices}{display=Attached USB Devices}{tooltip=Select individual devices to capture from}{type=multicheck} -value {arg=99}{value=2}{display=[2] Marvell AVASTAR Bluetooth Radio Adapter}{enabled=true} -value {arg=99}{value=3}{display=[3] Peripherique d entree USB}{enabled=true} -value {arg=99}{value=3_1}{display=Surface Type Cover Filter Device}{enabled=false}{parent=3} -value {arg=99}{value=3_2}{display=Souris HID}{enabled=false}{parent=3} -value {arg=99}{value=3_3}{display=Peripherique de control consommateur conforme aux Peripheriques d'interface utilisateur (HID)}{enabled=false}{parent=3} -value {arg=99}{value=3_4}{display=Surface Pro 4 Type Cover Integration}{enabled=false}{parent=3} -value {arg=99}{value=3_5}{display=Surface Keyboard Backlight}{enabled=false}{parent=3_4} -value {arg=99}{value=3_6}{display=Surface Pro 4 Firmware Update}{enabled=false}{parent=3_4} -value {arg=99}{value=3_7}{display=Peripherique fournisseur HID}{enabled=false}{parent=3} -value {arg=99}{value=3_8}{display=Surface PTP Filter}{enabled=false}{parent=3} -value {arg=99}{value=3_9}{display=Microsoft Input Configuration Device}{enabled=false}{parent=3} -value {arg=99}{value=3_10}{display=Peripherique fournisseur HID}{enabled=false}{parent=3} -value {arg=99}{value=3_11}{display=Peripherique fournisseur HID}{enabled=false}{parent=3} -value {arg=99}{value=3_12}{display=Peripherique fournisseur HID}{enabled=false}{parent=3} -""" - mock_Popen.side_effect = lambda *args, **kwargs: Bunch(returncode=0, communicate=(lambda *args, **kargs: (data,None))) - assert get_usbpcap_devices('\\\\.\\USBPcap1') == [('2', '[2] Marvell AVASTAR Bluetooth Radio Adapter', True),('3', '[3] Peripherique d entree USB', True)] - - -test_get_usbpcap_devices() From 353b2413f41c01eeacf3516eebc70821c82f70ef Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Thu, 7 Sep 2023 21:57:38 +0200 Subject: [PATCH 057/122] Let Scapy load on unsupported platforms (#4111) --- scapy/arch/__init__.py | 21 +++++++++++++++++++++ scapy/config.py | 3 +-- scapy/interfaces.py | 2 +- scapy/supersocket.py | 28 +++++++++++++++++++++++++++- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/scapy/arch/__init__.py b/scapy/arch/__init__.py index 806e137029c..ecfeda2359f 100644 --- a/scapy/arch/__init__.py +++ b/scapy/arch/__init__.py @@ -28,7 +28,9 @@ # Typing imports from typing import ( + List, Optional, + Tuple, Union, ) @@ -155,6 +157,25 @@ def get_if_raw_addr6(iff): ) SIOCGIFHWADDR = 0 # mypy compat + # DUMMYS + def get_if_raw_addr(iff: Union[NetworkInterface, str]) -> bytes: + return b"\0\0\0\0" + + def get_if_raw_hwaddr(iff: Union[NetworkInterface, str]) -> Tuple[int, bytes]: + return -1, b"" + + def in6_getifaddr() -> List[Tuple[str, int, str]]: + return [] + + def read_nameservers() -> List[str]: + return [] + + def read_routes() -> List[str]: + return [] + + def read_routes6() -> List[str]: + return [] + if LINUX or BSD: conf.load_layers.append("tuntap") diff --git a/scapy/config.py b/scapy/config.py index 4ffc88324d7..3be0bfec4a7 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -648,8 +648,7 @@ def _set_conf_sockets(): conf.L2socket = _NotAvailableSocket conf.L2listen = _NotAvailableSocket else: - from scapy.supersocket import L3RawSocket - from scapy.layers.inet6 import L3RawSocket6 + from scapy.supersocket import L3RawSocket, L3RawSocket6 conf.L3socket = L3RawSocket conf.L3socket6 = L3RawSocket6 # Reload the interfaces diff --git a/scapy/interfaces.py b/scapy/interfaces.py index 10ec9577808..6a4b51822d2 100644 --- a/scapy/interfaces.py +++ b/scapy/interfaces.py @@ -66,7 +66,7 @@ def _l3socket(self, dev, ipv6): if LINUX and not self.libpcap and dev.name == conf.loopback_name: # handle the loopback case. see troubleshooting.rst if ipv6: - from scapy.layers.inet6 import L3RawSocket6 + from scapy.supersocket import L3RawSocket6 return cast(Type['scapy.supersocket.SuperSocket'], L3RawSocket6) else: from scapy.supersocket import L3RawSocket diff --git a/scapy/supersocket.py b/scapy/supersocket.py index eff4589cf55..8afec10b173 100644 --- a/scapy/supersocket.py +++ b/scapy/supersocket.py @@ -16,7 +16,13 @@ from scapy.config import conf from scapy.consts import DARWIN, WINDOWS -from scapy.data import MTU, ETH_P_IP, SOL_PACKET, SO_TIMESTAMPNS +from scapy.data import ( + MTU, + ETH_P_IP, + ETH_P_IPV6, + SOL_PACKET, + SO_TIMESTAMPNS, +) from scapy.compat import raw from scapy.error import warning, log_runtime from scapy.interfaces import network_name @@ -370,6 +376,26 @@ def send(self, x): log_runtime.error(msg) return 0 + class L3RawSocket6(L3RawSocket): + def __init__(self, + type: int = ETH_P_IPV6, + filter: Optional[str] = None, + iface: Optional[_GlobInterfaceType] = None, + promisc: Optional[bool] = None, + nofilter: bool = False) -> None: + # NOTE: if fragmentation is needed, it will be done by the kernel (RFC 2292) # noqa: E501 + self.outs = socket.socket( + socket.AF_INET6, + socket.SOCK_RAW, + socket.IPPROTO_RAW + ) + self.ins = socket.socket( + socket.AF_PACKET, + socket.SOCK_RAW, + socket.htons(type) + ) + self.iface = cast(_GlobInterfaceType, iface) + class SimpleSocket(SuperSocket): desc = "wrapper around a classic socket" From 4c620080bfe335e6cb7058d79dc4b983683594af Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Mon, 11 Sep 2023 17:27:40 +0200 Subject: [PATCH 058/122] Added support for CanFD in the ISOTPSoftSocket (#4048) * Added support for CanFD in the ISOTPSoftSocket * Code review. * Fixing mypy and removing an useless import in the test campaign * fix test * fix test * remove unnessesary test case file --------- Co-authored-by: bhonnef --- scapy/contrib/isotp/isotp_packet.py | 18 +- scapy/contrib/isotp/isotp_soft_socket.py | 57 ++++- test/contrib/isotp_soft_socket.uts | 289 ++++++++++++++++++++++- 3 files changed, 346 insertions(+), 18 deletions(-) diff --git a/scapy/contrib/isotp/isotp_packet.py b/scapy/contrib/isotp/isotp_packet.py index 2eb8a6ff149..6f62614ca2b 100644 --- a/scapy/contrib/isotp/isotp_packet.py +++ b/scapy/contrib/isotp/isotp_packet.py @@ -16,7 +16,7 @@ BitEnumField, ByteField, XByteField, BitFieldLenField, StrField, \ FieldLenField, IntField, ShortField from scapy.compat import chb, orb -from scapy.layers.can import CAN +from scapy.layers.can import CAN, CAN_FD_MAX_DLEN as CAN_FD_MAX_DLEN from scapy.error import Scapy_Exception # Typing imports @@ -96,11 +96,21 @@ def fragment(self, *args, **kargs): """Helper function to fragment an ISOTP message into multiple CAN frames. + :param fd: type: Optional[bool]: will fragment the can frames + with size CAN_FD_MAX_DLEN + :return: A list of CAN frames """ - data_bytes_in_frame = 7 + + fd = kargs.pop("fd", False) + + def _get_data_len(): + # type: () -> int + return CAN_MAX_DLEN if not fd else CAN_FD_MAX_DLEN + + data_bytes_in_frame = _get_data_len() - 1 if self.rx_ext_address is not None: - data_bytes_in_frame = 6 + data_bytes_in_frame = data_bytes_in_frame - 1 if len(self.data) > ISOTP_MAX_DLEN_2015: raise Scapy_Exception("Too much data in ISOTP message") @@ -125,7 +135,7 @@ def fragment(self, *args, **kargs): frame_header = struct.pack(">HI", 0x1000, len(self.data)) if self.rx_ext_address: frame_header = struct.pack('B', self.rx_ext_address) + frame_header - idx = 8 - len(frame_header) + idx = _get_data_len() - len(frame_header) frame_data = self.data[0:idx] if self.rx_id is None or self.rx_id <= 0x7ff: frame = CAN(identifier=self.rx_id, data=frame_header + frame_data) diff --git a/scapy/contrib/isotp/isotp_soft_socket.py b/scapy/contrib/isotp/isotp_soft_socket.py index 52100b008d9..cc6be464d70 100644 --- a/scapy/contrib/isotp/isotp_soft_socket.py +++ b/scapy/contrib/isotp/isotp_soft_socket.py @@ -14,6 +14,7 @@ import socket from threading import Thread, Event, RLock +from bisect import bisect_left from scapy.packet import Packet from scapy.layers.can import CAN @@ -24,7 +25,7 @@ from scapy.utils import EDecimal from scapy.automaton import ObjectPipe, select_objects from scapy.contrib.isotp.isotp_packet import ISOTP, CAN_MAX_DLEN, N_PCI_SF, \ - N_PCI_CF, N_PCI_FC, N_PCI_FF, ISOTP_MAX_DLEN, ISOTP_MAX_DLEN_2015 + N_PCI_CF, N_PCI_FC, N_PCI_FF, ISOTP_MAX_DLEN, ISOTP_MAX_DLEN_2015, CAN_FD_MAX_DLEN # Typing imports from typing import ( @@ -112,6 +113,7 @@ class ISOTPSoftSocket(SuperSocket): :param listen_only: Does not send Flow Control frames if a First Frame is received :param basecls: base class of the packets emitted by this socket + :param fd: enables the CanFD support for this socket """ # noqa: E501 def __init__(self, @@ -124,7 +126,8 @@ def __init__(self, stmin=0, # type: int padding=False, # type: bool listen_only=False, # type: bool - basecls=ISOTP # type: Type[Packet] + basecls=ISOTP, # type: Type[Packet] + fd=False # type: bool ): # type: (...) -> None @@ -138,6 +141,7 @@ def __init__(self, self.rx_ext_address = rx_ext_address or ext_address self.tx_id = tx_id self.rx_id = rx_id + self.fd = fd impl = ISOTPSocketImplementation( can_socket, @@ -148,7 +152,8 @@ def __init__(self, rx_ext_address=self.rx_ext_address, bs=bs, stmin=stmin, - listen_only=listen_only + listen_only=listen_only, + fd=fd ) # Cast for compatibility to functions from SuperSocket. @@ -486,7 +491,8 @@ def __init__(self, rx_ext_address=None, # type: Optional[int] bs=0, # type: int stmin=0, # type: int - listen_only=False # type: bool + listen_only=False, # type: bool + fd=False # type: bool ): # type: (...) -> None self.can_socket = can_socket @@ -496,6 +502,10 @@ def __init__(self, self.fc_timeout = 1 self.cf_timeout = 1 + self.fd = fd + + self.max_dlen = CAN_FD_MAX_DLEN if fd else CAN_MAX_DLEN + self.filter_warning_emitted = False self.closed = False @@ -553,8 +563,21 @@ def __del__(self): def can_send(self, load): # type: (bytes) -> None + def _get_padding_size(pl_size): + # type: (int) -> int + if not self.fd: + return CAN_MAX_DLEN + else: + fd_accepted_sizes = [0, 8, 12, 16, 20, 24, 32, 48, 64] + pos = bisect_left(fd_accepted_sizes, pl_size) + if pos == 0: + return fd_accepted_sizes[0] + if pos == len(fd_accepted_sizes): + return fd_accepted_sizes[-1] + return fd_accepted_sizes[pos] + if self.padding: - load += b"\xCC" * (CAN_MAX_DLEN - len(load)) + load += b"\xCC" * (_get_padding_size(len(load)) - len(load)) if self.tx_id is None or self.tx_id <= 0x7ff: self.can_socket.send(CAN(identifier=self.tx_id, data=load)) else: @@ -644,7 +667,7 @@ def _tx_timer_handler(self): elif self.tx_state == ISOTP_SENDING: # push out the next segmented pdu src_off = len(self.ea_hdr) - max_bytes = 7 - src_off + max_bytes = (self.max_dlen - 1) - src_off if self.tx_buf is None: self.tx_state = ISOTP_IDLE log_isotp.warning("TX buffer is not filled") @@ -783,10 +806,19 @@ def _recv_sf(self, data, ts): self.rx_state = ISOTP_IDLE length = data[0] & 0xf + is_fd_frame = self.fd and length == 0 and len(data) >= 2 + + if is_fd_frame: + length = data[1] + if len(data) - 1 < length: return - msg = data[1:1 + length] + msg = None + if is_fd_frame: + msg = data[2:2 + length] + else: + msg = data[1:1 + length] self.rx_queue.send((msg, ts)) def _recv_ff(self, data, ts): @@ -922,10 +954,15 @@ def begin_send(self, x): if length > ISOTP_MAX_DLEN_2015: log_isotp.warning("Too much data for ISOTP message") - if len(self.ea_hdr) + length <= 7: + sf_size_check = self.max_dlen - 1 + + if len(self.ea_hdr) + length + int(self.fd) <= sf_size_check: # send a single frame data = self.ea_hdr - data += struct.pack("B", length) + if not self.fd or length <= 7: + data += struct.pack("B", length) + else: + data += struct.pack("BB", 0, length) data += x self.tx_state = ISOTP_IDLE self.can_send(data) @@ -937,7 +974,7 @@ def begin_send(self, x): data += struct.pack(">HI", 0x1000, length) else: data += struct.pack(">H", 0x1000 | length) - load = x[0:8 - len(data)] + load = x[0:self.max_dlen - len(data)] data += load self.can_send(data) diff --git a/test/contrib/isotp_soft_socket.uts b/test/contrib/isotp_soft_socket.uts index d60fc371927..de7f731b2e8 100644 --- a/test/contrib/isotp_soft_socket.uts +++ b/test/contrib/isotp_soft_socket.uts @@ -16,10 +16,7 @@ from test.testsocket import TestSocket, cleanup_testsockets import logging from scapy.error import log_runtime -try: - from cStringIO import StringIO # Python 2 -except ImportError: - from io import StringIO +from io import StringIO log_stream = StringIO() handler = logging.StreamHandler(log_stream) @@ -73,6 +70,28 @@ with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_ msg = pkts[0] assert msg.data == dhex("01 02 03 04 05") += Single-frame receive FD + +with TestSocket(CANFD) as cans, TestSocket(CANFD) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241, fd=True) as s: + pl_sizes_testings = [1, 5, 7, 8, 15, 20, 35, 40, 46, 62] + data_str = "" + data_str_offset = 0 + cans.pair(stim) + for size_to_send in pl_sizes_testings: + if size_to_send > 7: + data_str = "00 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 6 + else: + data_str = "{} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 2 + stim.send(CANFD(identifier=0x241, data=dhex(data_str))) + pkts = s.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + msg = pkts[0] + assert msg.data == dhex(data_str[data_str_offset:]) + = Single-frame send with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241) as s: @@ -85,6 +104,28 @@ with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_ msg = pkts[0] assert msg.data == dhex("05 01 02 03 04 05") += Single-frame send FD + +with TestSocket(CANFD) as cans, TestSocket(CANFD) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241, fd=True) as s: + pl_sizes_testings = [1, 5, 7, 8, 15, 20, 35, 40, 46, 62] + data_str = "" + data_str_offset = 0 + cans.pair(stim) + for size_to_send in pl_sizes_testings: + if size_to_send > 7: + data_str = "00 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 6 + else: + data_str = "{} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 2 + s.send(ISOTP(dhex(data_str[data_str_offset:]))) + pkts = stim.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + msg = pkts[0] + assert msg.data == dhex(data_str) + = Two frame receive with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241) as s: @@ -105,6 +146,26 @@ with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_ assert msg.data == dhex("01 02 03 04 05 06 07 08 09") += Two frame receive FD + +with TestSocket(CANFD) as cans, TestSocket(CANFD) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241, fd=True) as s: + cans.pair(stim) + stim.send(CANFD(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06 07 08 09 0A 0B"))) + pkts = stim.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + c = pkts[0] + assert (c.data == dhex("30 00 00")) + stim.send(CANFD(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) + pkts = s.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + msg = pkts[0] + assert msg.data == dhex("01 02 03 04 05 06 07 08 09") + + = 20000 bytes receive def test(): @@ -145,6 +206,25 @@ def test(): test() += 20000 bytes send FD + +def testfd(): + with TestSocket(CANFD) as cans, TestSocket(CANFD) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241, fd=True) as s: + cans.pair(stim) + data = dhex("01 02 03 04 05")*4006 + msg = ISOTP(data, rx_id=0x641) + fragments = msg.fragment(fd=True) + ack = CANFD(identifier=0x241, data=dhex("30 00 00")) + ff = stim.sniff(timeout=1, count=1, + started_callback=lambda:s.send(msg)) + assert len(ff) == 1 + cfs = stim.sniff(timeout=20, count=len(fragments) - 1, + started_callback=lambda: stim.send(ack)) + for fragment, cf in zip(fragments, ff + cfs): + assert (bytes(fragment) == bytes(cf)) + +testfd() + = Close ISOTPSoftSocket with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241) as s: @@ -158,6 +238,22 @@ with ISOTPSoftSocket(TestSocket(CAN), tx_id=0x641, rx_id=0x241) as s: msg, ts = s.ins.rx_queue.recv() assert msg == dhex("01 02 03 04 05") += Test on_recv function with single frame FD +with ISOTPSoftSocket(TestSocket(CANFD), tx_id=0x641, rx_id=0x241, fd=True) as s: + pl_sizes_testings = [1, 5, 7, 8, 15, 20, 35, 40, 46, 62] + data_str = "" + data_str_offset = 0 + for size_to_send in pl_sizes_testings: + if size_to_send > 7: + data_str = "00 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 6 + else: + data_str = "{} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 2 + s.ins.on_recv(CANFD(identifier=0x241, data=dhex(data_str))) + msg, ts = s.ins.rx_queue.recv() + assert msg == dhex(data_str[data_str_offset:]) + = Test on_recv function with empty frame with ISOTPSoftSocket(TestSocket(CAN), tx_id=0x641, rx_id=0x241) as s: s.ins.on_recv(CAN(identifier=0x241, data=b"")) @@ -171,6 +267,25 @@ with ISOTPSoftSocket(TestSocket(CAN), tx_id=0x641, rx_id=0x241, rx_ext_address=0 assert msg == dhex("01 02 03 04 05") assert ts == cf.time + += Test on_recv function with single frame and extended addressing FD +with ISOTPSoftSocket(TestSocket(CANFD), tx_id=0x641, rx_id=0x241, rx_ext_address=0xea, fd=True) as s: + pl_sizes_testings = [1, 5, 7, 8, 15, 20, 35, 40, 46, 62] + data_str = "" + data_str_offset = 0 + for size_to_send in pl_sizes_testings: + if size_to_send > 7: + data_str = "EA 00 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 8 + else: + data_str = "EA {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 5 + cf = CANFD(identifier=0x241, data=dhex(data_str)) + s.ins.on_recv(cf) + msg, ts = s.ins.rx_queue.recv() + assert msg == dhex(data_str[data_str_offset:]) + assert ts == cf.time + = CF is sent when first frame is received cans = TestSocket(CAN) can_out = TestSocket(CAN) @@ -230,6 +345,19 @@ with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241 assert can[0].identifier == 0x641 assert can[0].data == dhex("21 07 08") += Send two-frame ISOTP message, using send FD +with TestSocket(CANFD) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, fd=True) as s, TestSocket(CANFD) as cans: + size_to_send = 100 + max_pl_size = 62 + data_str = "{}".format(" ".join(["%02X" % x for x in range(size_to_send)])) + cans.pair(isocan) + can = cans.sniff(timeout=1, count=1, started_callback=lambda: s.send(dhex(data_str))) + assert can[0].identifier == 0x641 + assert can[0].data == dhex("10 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(max_pl_size)]))) + can = cans.sniff(timeout=1, count=1, started_callback=lambda: cans.send(CANFD(identifier = 0x241, data=dhex("30 00 00")))) + assert can[0].identifier == 0x641 + assert can[0].data == dhex("21 {}".format(" ".join(["%02X" % x for x in range(max_pl_size, size_to_send)]))) + = Send single frame ISOTP message with TestSocket(CAN) as cans, TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s: cans.pair(isocan) @@ -284,6 +412,53 @@ thread.join(15) acks.close() assert not thread.is_alive() += Send two-frame ISOTP message FD + +acks = TestSocket(CANFD) + +acker_ready = threading.Event() +def acker(): + acker_ready.set() + can_pkt = acks.sniff(timeout=1, count=1) + can = can_pkt[0] + acks.send(CANFD(identifier = 0x241, data=dhex("30 00 00"))) + +thread = Thread(target=acker) +thread.start() +acker_ready.wait(timeout=5) +with TestSocket(CANFD) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, fd=True) as s, TestSocket(CANFD) as cans: + size_to_send = 123 + max_pl_size = 62 + data_str = "{}".format(" ".join(["%02X" % x for x in range(size_to_send)])) + cans.pair(isocan) + cans.pair(acks) + isocan.pair(acks) + s.send(dhex(data_str)) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("10 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(max_pl_size)]))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x241 + assert can.data == dhex("30 00 00") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("21 {}".format(" ".join(["%02X" % x for x in range(max_pl_size, size_to_send)]))) + +thread.join(15) +acks.close() +assert not thread.is_alive() = Send two-frame ISOTP message with bs @@ -328,6 +503,51 @@ thread.join(15) acks.close() assert not thread.is_alive() += Send two-frame ISOTP message with bs FD + +acks = TestSocket(CANFD) +acker_ready = threading.Event() +def acker(): + acker_ready.set() + can_pkt = acks.sniff(timeout=1, count=1) + acks.send(CANFD(identifier = 0x241, data=dhex("30 20 00"))) + +thread = Thread(target=acker) +thread.start() +acker_ready.wait(timeout=5) +with TestSocket(CANFD) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, fd=True) as s, TestSocket(CANFD) as cans: + size_to_send = 124 + max_pl_size = 62 + data_str = "{}".format(" ".join(["%02X" % x for x in range(size_to_send)])) + cans.pair(isocan) + cans.pair(acks) + isocan.pair(acks) + s.send(ISOTP(data=dhex(data_str))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("10 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(max_pl_size)]))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x241 + assert can.data == dhex("30 20 00") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("21 {}".format(" ".join(["%02X" % x for x in range(max_pl_size, size_to_send)]))) + +thread.join(15) +acks.close() +assert not thread.is_alive() = Send two-frame ISOTP message with ST acks = TestSocket(CAN) @@ -371,6 +591,51 @@ thread.join(15) acks.close() assert not thread.is_alive() += Send two-frame ISOTP message with ST FD +acks = TestSocket(CANFD) +acker_ready = threading.Event() +def acker(): + acker_ready.set() + acks.sniff(timeout=1, count=1) + acks.send(CANFD(identifier = 0x241, data=dhex("30 00 10"))) + +thread = Thread(target=acker) +thread.start() +acker_ready.wait(timeout=5) +with TestSocket(CANFD) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, fd=True) as s, TestSocket(CANFD) as cans: + size_to_send = 124 + max_pl_size = 62 + data_str = "{}".format(" ".join(["%02X" % x for x in range(size_to_send)])) + cans.pair(isocan) + cans.pair(acks) + isocan.pair(acks) + s.send(dhex(data_str)) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("10 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(max_pl_size)]))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x241 + assert can.data == dhex("30 00 10") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("21 {}".format(" ".join(["%02X" % x for x in range(max_pl_size, size_to_send)]))) + +thread.join(15) +acks.close() +assert not thread.is_alive() + = Receive a single frame ISOTP message with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: @@ -867,6 +1132,22 @@ with TestSocket(CAN) as cs1, ISOTPSoftSocket(cs1, tx_id=0x641, rx_id=0x241, padd res = pkts[0] assert res.length == 8 += Send a single frame ISOTP message with padding FD + +with TestSocket(CANFD) as cs1, ISOTPSoftSocket(cs1, tx_id=0x641, rx_id=0x241, padding=True, fd=True) as s: + with TestSocket(CANFD) as cans: + cs1.pair(cans) + pl_sizes_testings = [1, 5, 7, 8, 9, 12, 15, 17, 20, 21, 27, 35, 40, 46, 50, 62] + pl_sizes_expected = [8, 8, 8, 12, 12, 16, 20, 20, 24, 24, 32, 48, 48, 48, 64, 64] + for i, pl_size in enumerate(pl_sizes_testings): + s.send(dhex(" ".join(["%02X" % x for x in range(pl_size)]))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + res = pkts[0] + assert res.length == pl_sizes_expected[i] + = Send a two-frame ISOTP message with padding From 070a2621c9f2780670ffa72d20523bc1d3744d29 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 12 Sep 2023 13:40:08 +0200 Subject: [PATCH 059/122] UDS: more precise parsing of DTCs (#4094) --- scapy/contrib/automotive/uds.py | 90 +++++++++++++++++++++++++++------ test/contrib/automotive/uds.uts | 23 ++++++++- 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 74952f4d884..8b0dcf6f18f 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -16,7 +16,8 @@ BitEnumField, BitField, XByteField, FieldListField, \ XShortField, X3BytesField, XIntField, ByteField, \ ShortField, ObservableDict, XShortEnumField, XByteEnumField, StrLenField, \ - FieldLenField, XStrFixedLenField, XStrLenField + FieldLenField, XStrFixedLenField, XStrLenField, FlagsField, PacketListField, \ + PacketField from scapy.packet import Packet, bind_layers, NoPayload from scapy.config import conf from scapy.error import log_loading @@ -956,12 +957,39 @@ class UDS_RDTCI(Packet): 20: 'reportDTCFaultDetectionCounter', 21: 'reportDTCWithPermanentStatus' } + dtcStatus = { + 1: 'TestFailed', + 2: 'TestFailedThisOperationCycle', + 4: 'PendingDTC', + 8: 'ConfirmedDTC', + 16: 'TestNotCompletedSinceLastClear', + 32: 'TestFailedSinceLastClear', + 64: 'TestNotCompletedThisOperationCycle', + 128: 'WarningIndicatorRequested' + } + dtcStatusMask = { + 1: 'ActiveDTCs', + 4: 'PendingDTCs', + 8: 'ConfirmedOrStoredDTCs', + 255: 'AllRecordDTCs' + } + dtcSeverityMask = { + # 0: 'NoSeverityInformation', + 1: 'NoClassInformation', + 2: 'WWH-OBDClassA', + 4: 'WWH-OBDClassB1', + 8: 'WWH-OBDClassB2', + 16: 'WWH-OBDClassC', + 32: 'MaintenanceRequired', + 64: 'CheckAtNextHalt', + 128: 'CheckImmediately' + } name = 'ReadDTCInformation' fields_desc = [ ByteEnumField('reportType', 0, reportTypes), - ConditionalField(ByteField('DTCSeverityMask', 0), + ConditionalField(FlagsField('DTCSeverityMask', 0, 8, dtcSeverityMask), lambda pkt: pkt.reportType in [0x07, 0x08]), - ConditionalField(XByteField('DTCStatusMask', 0), + ConditionalField(FlagsField('DTCStatusMask', 0, 8, dtcStatusMask), lambda pkt: pkt.reportType in [ 0x01, 0x02, 0x07, 0x08, 0x0f, 0x11, 0x12, 0x13]), ConditionalField(ByteField('DTCHighByte', 0), @@ -983,16 +1011,47 @@ class UDS_RDTCI(Packet): bind_layers(UDS, UDS_RDTCI, service=0x19) +class DTC(Packet): + name = 'Diagnostic Trouble Code' + fields_desc = [ + BitEnumField("system", 0, 2, { + 0: "Powertrain", + 1: "Chassis", + 2: "Body", + 3: "Network"}), + BitEnumField("type", 0, 2, { + 0: "Generic", + 1: "ManufacturerSpecific", + 2: "Generic", + 3: "Generic"}), + BitField("numeric_value_code", 0, 12), + ByteField("additional_information_code", 0), + ] + + def extract_padding(self, s): + return '', s + + +class DTC_Status(Packet): + name = 'DTC and status record' + fields_desc = [ + PacketField("dtc", None, pkt_cls=DTC), + FlagsField("status", 0, 8, UDS_RDTCI.dtcStatus) + ] + + def extract_padding(self, s): + return '', s + + class UDS_RDTCIPR(Packet): name = 'ReadDTCInformationPositiveResponse' fields_desc = [ ByteEnumField('reportType', 0, UDS_RDTCI.reportTypes), - ConditionalField(XByteField('DTCStatusAvailabilityMask', 0), - lambda pkt: pkt.reportType in [0x01, 0x07, 0x11, - 0x12, 0x02, 0x0A, - 0x0B, 0x0C, 0x0D, - 0x0E, 0x0F, 0x13, - 0x15]), + ConditionalField( + FlagsField('DTCStatusAvailabilityMask', 0, 8, UDS_RDTCI.dtcStatus), + lambda pkt: pkt.reportType in [0x01, 0x07, 0x11, 0x12, 0x02, 0x0A, + 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x13, + 0x15]), ConditionalField(ByteEnumField('DTCFormatIdentifier', 0, {0: 'ISO15031-6DTCFormat', 1: 'UDS-1DTCFormat', @@ -1003,7 +1062,8 @@ class UDS_RDTCIPR(Packet): ConditionalField(ShortField('DTCCount', 0), lambda pkt: pkt.reportType in [0x01, 0x07, 0x11, 0x12]), - ConditionalField(StrField('DTCAndStatusRecord', b""), + ConditionalField(PacketListField('DTCAndStatusRecord', None, + pkt_cls=DTC_Status), lambda pkt: pkt.reportType in [0x02, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x13, 0x15]), @@ -1259,15 +1319,15 @@ def _contains_data_format_identifier(packet): fmt='B'), lambda p: p.modeOfOperation != 2), ConditionalField(StrLenField('maxNumberOfBlockLength', b"", - length_from=lambda p: p.lengthFormatIdentifier), + length_from=lambda p: p.lengthFormatIdentifier), lambda p: p.modeOfOperation != 2), ConditionalField(BitField('compressionMethod', 0, 4), lambda p: p.modeOfOperation != 0x02), ConditionalField(BitField('encryptingMethod', 0, 4), lambda p: p.modeOfOperation != 0x02), ConditionalField(FieldLenField('fileSizeOrDirInfoParameterLength', - None, - length_of='fileSizeUncompressedOrDirInfoLength'), + None, + length_of='fileSizeUncompressedOrDirInfoLength'), lambda p: p.modeOfOperation not in [1, 2, 3]), ConditionalField(StrLenField('fileSizeUncompressedOrDirInfoLength', b"", @@ -1275,8 +1335,8 @@ def _contains_data_format_identifier(packet): p.fileSizeOrDirInfoParameterLength), lambda p: p.modeOfOperation not in [1, 2, 3]), ConditionalField(StrLenField('fileSizeCompressed', b"", - length_from=lambda p: - p.fileSizeOrDirInfoParameterLength), + length_from=lambda p: + p.fileSizeOrDirInfoParameterLength), lambda p: p.modeOfOperation not in [1, 2, 3, 5]), ] diff --git a/test/contrib/automotive/uds.uts b/test/contrib/automotive/uds.uts index ead0097accb..ea3bb085530 100644 --- a/test/contrib/automotive/uds.uts +++ b/test/contrib/automotive/uds.uts @@ -1046,6 +1046,20 @@ assert rdtcipr.DTCCount == 0xddaa assert rdtcipr.answers(rdtci) +rdtcipr1 = UDS(b'\x59\x02\xff\x11\x07\x11\'\x022\x12\'\x01\x07\x11\'\x01\x18\x12\'\x01\x13\x12\'\x01"\x11\'\x06C\x00\'\x06S\x00\'\x161\x00\'\x14\x03\x12\'') + +assert len(rdtcipr1.DTCAndStatusRecord) == 10 +assert rdtcipr1.DTCAndStatusRecord[0].dtc.system == 0 +assert rdtcipr1.DTCAndStatusRecord[0].dtc.type == 1 +assert rdtcipr1.DTCAndStatusRecord[0].dtc.numeric_value_code == 263 +assert rdtcipr1.DTCAndStatusRecord[0].dtc.additional_information_code == 17 +assert rdtcipr1.DTCAndStatusRecord[0].status == 0x27 +assert rdtcipr1.DTCAndStatusRecord[-1].dtc.system == 0 +assert rdtcipr1.DTCAndStatusRecord[-1].dtc.type == 1 +assert rdtcipr1.DTCAndStatusRecord[-1].dtc.numeric_value_code == 1027 +assert rdtcipr1.DTCAndStatusRecord[-1].dtc.additional_information_code == 18 +assert rdtcipr1.DTCAndStatusRecord[-1].status == 0x27 + = Check UDS_RDTCI rdtci = UDS(b'\x19\x02\xff') @@ -1156,11 +1170,16 @@ assert rdtci.DTCExtendedDataRecordNumber == 0xaa = Check UDS_RDTCIPR -rdtcipr = UDS(b'\x59\x02\xff\xee\xdd\xaa') +rdtcipr = UDS(b'\x59\x02\xff\xee\xdd\xaa\x02') +rdtcipr.show() assert rdtcipr.service == 0x59 assert rdtcipr.reportType == 2 assert rdtcipr.DTCStatusAvailabilityMask == 0xff -assert rdtcipr.DTCAndStatusRecord == b'\xee\xdd\xaa' +assert rdtcipr.DTCAndStatusRecord[0].dtc.system == 3 +assert rdtcipr.DTCAndStatusRecord[0].dtc.type == 2 +assert rdtcipr.DTCAndStatusRecord[0].dtc.numeric_value_code == 3805 +assert rdtcipr.DTCAndStatusRecord[0].dtc.additional_information_code == 170 +assert rdtcipr.DTCAndStatusRecord[0].status == 2 assert not rdtcipr.answers(rdtci) From 63838a3fb471907fa5cc30dc1b92311666263078 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:04:36 +0200 Subject: [PATCH 060/122] Fix readthedocs: update doc dependencies --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 11a805bef76..15d52fc90eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,8 +56,8 @@ all = [ "matplotlib", ] docs = [ - "sphinx>=3.0.0", - "sphinx_rtd_theme>=0.4.3", + "sphinx>=7.0.0", + "sphinx_rtd_theme>=1.3.0", "tox>=3.0.0", ] From f311149a3073dc592fd6488ca83574dcad782e1a Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:29:01 +0000 Subject: [PATCH 061/122] Fix Automaton.graph() with indirection --- scapy/automaton.py | 23 ++++++++++++++++++++--- test/scapy/automaton.uts | 21 +++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/scapy/automaton.py b/scapy/automaton.py index a12fb6953c5..27e2cef3e1e 100644 --- a/scapy/automaton.py +++ b/scapy/automaton.py @@ -775,20 +775,37 @@ def build_graph(self): s += se for st in self.states.values(): - for n in st.atmt_origfunc.__code__.co_names + st.atmt_origfunc.__code__.co_consts: # noqa: E501 + names = list( + st.atmt_origfunc.__code__.co_names + + st.atmt_origfunc.__code__.co_consts + ) + while names: + n = names.pop() if n in self.states: - s += '\t"%s" -> "%s" [ color=green ];\n' % (st.atmt_state, n) # noqa: E501 + s += '\t"%s" -> "%s" [ color=green ];\n' % (st.atmt_state, n) + elif n in self.__dict__: + # function indirection + if callable(self.__dict__[n]): + names.extend(self.__dict__[n].__code__.co_names) + names.extend(self.__dict__[n].__code__.co_consts) for c, k, v in ([("purple", k, v) for k, v in self.conditions.items()] + # noqa: E501 [("red", k, v) for k, v in self.recv_conditions.items()] + # noqa: E501 [("orange", k, v) for k, v in self.ioevents.items()]): for f in v: - for n in f.__code__.co_names + f.__code__.co_consts: + names = list(f.__code__.co_names + f.__code__.co_consts) + while names: + n = names.pop() if n in self.states: line = f.atmt_condname for x in self.actions[f.atmt_condname]: line += "\\l>[%s]" % x.__name__ s += '\t"%s" -> "%s" [label="%s", color=%s];\n' % (k, n, line, c) # noqa: E501 + elif n in self.__dict__: + # function indirection + if callable(self.__dict__[n]): + names.extend(self.__dict__[n].__code__.co_names) + names.extend(self.__dict__[n].__code__.co_consts) for k, timers in self.timeout.items(): for timer in timers: for n in (timer._func.__code__.co_names + diff --git a/test/scapy/automaton.uts b/test/scapy/automaton.uts index bc103f3126d..ab107b8bc74 100644 --- a/test/scapy/automaton.uts +++ b/test/scapy/automaton.uts @@ -446,6 +446,27 @@ graph = HelloWorld.build_graph() assert graph.startswith("digraph") assert '"BEGIN" -> "END"' in graph += Automaton graph - with indirection +~ automaton + +class HelloWorld(Automaton): + @ATMT.state(initial=1) + def BEGIN(self): + self.count1 = 0 + self.count2 = 0 + @ATMT.condition(BEGIN) + def cnd_1(self): + self.cnd_generic() + def cnd_generic(self): + raise END + @ATMT.state(final=1) + def END(self): + pass + +graph = HelloWorld.build_graph() +assert graph.startswith("digraph") +assert '"BEGIN" -> "END"' in graph + = TCP_client automaton ~ automaton netaccess needs_root * This test retries on failure because it may fail quite easily From 219f2febd8456c440988b329b44c1dbcd0e47d0d Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Tue, 19 Sep 2023 08:29:31 +0200 Subject: [PATCH 062/122] Cleanup IP fragmentation, TCP session and TLS sessions (#4082) --- doc/scapy/layers/automotive.rst | 4 +- doc/scapy/usage.rst | 3 +- scapy/arch/bpf/supersocket.py | 4 +- scapy/arch/libpcap.py | 9 +- scapy/arch/linux.py | 6 +- scapy/automaton.py | 8 +- scapy/config.py | 20 +- scapy/contrib/automotive/bmw/hsfz.py | 13 +- scapy/contrib/automotive/doip.py | 13 +- scapy/contrib/automotive/ecu.py | 9 +- scapy/contrib/isotp/isotp_native_socket.py | 7 +- scapy/contrib/isotp/isotp_soft_socket.py | 6 +- scapy/contrib/isotp/isotp_utils.py | 22 +- scapy/layers/dcerpc.py | 14 +- scapy/layers/inet.py | 202 +++++++------- scapy/layers/netflow.py | 21 +- scapy/layers/tls/handshake.py | 11 +- scapy/layers/tls/keyexchange.py | 12 +- scapy/layers/tls/record.py | 34 ++- scapy/layers/tls/record_sslv2.py | 2 +- scapy/layers/tls/record_tls13.py | 20 +- scapy/layers/tls/session.py | 154 +++++++---- scapy/libs/extcap.py | 4 +- scapy/packet.py | 18 +- scapy/sendrecv.py | 45 ++-- scapy/sessions.py | 254 ++++++++---------- scapy/supersocket.py | 38 +-- scapy/tools/UTscapy.py | 5 +- scapy/utils.py | 35 ++- test/configs/linux.utsc | 8 +- test/configs/windows.utsc | 5 +- test/configs/windows2.utsc | 16 +- test/contrib/automotive/ecu.uts | 16 +- test/contrib/isotp_soft_socket.uts | 9 +- test/pcaps/tls_tcp_frag_withnss.pcap.gz | Bin 0 -> 5666 bytes test/scapy/layers/inet.uts | 99 ++++++- test/{ => scapy/layers}/tls/__init__.py | 0 test/{ => scapy/layers/tls}/cert.uts | 0 test/{ => scapy/layers}/tls/example_client.py | 0 test/{ => scapy/layers}/tls/example_server.py | 0 test/{ => scapy/layers}/tls/pki/ca_cert.pem | 0 test/{ => scapy/layers}/tls/pki/ca_key.pem | 0 test/{ => scapy/layers}/tls/pki/cli_cert.pem | 0 test/{ => scapy/layers}/tls/pki/cli_key.pem | 0 test/{ => scapy/layers}/tls/pki/srv_cert.pem | 0 test/{ => scapy/layers}/tls/pki/srv_key.pem | 0 test/{ => scapy/layers/tls}/sslv2.uts | 4 +- test/{ => scapy/layers/tls}/tls.uts | 49 +++- test/{ => scapy/layers/tls}/tls13.uts | 2 + .../layers/tls/tlsclientserver.uts} | 14 +- test/testsocket.py | 6 +- 51 files changed, 707 insertions(+), 514 deletions(-) create mode 100644 test/pcaps/tls_tcp_frag_withnss.pcap.gz rename test/{ => scapy/layers}/tls/__init__.py (100%) rename test/{ => scapy/layers/tls}/cert.uts (100%) rename test/{ => scapy/layers}/tls/example_client.py (100%) mode change 100755 => 100644 rename test/{ => scapy/layers}/tls/example_server.py (100%) mode change 100755 => 100644 rename test/{ => scapy/layers}/tls/pki/ca_cert.pem (100%) rename test/{ => scapy/layers}/tls/pki/ca_key.pem (100%) rename test/{ => scapy/layers}/tls/pki/cli_cert.pem (100%) rename test/{ => scapy/layers}/tls/pki/cli_key.pem (100%) rename test/{ => scapy/layers}/tls/pki/srv_cert.pem (100%) rename test/{ => scapy/layers}/tls/pki/srv_key.pem (100%) rename test/{ => scapy/layers/tls}/sslv2.uts (99%) rename test/{ => scapy/layers/tls}/tls.uts (96%) rename test/{ => scapy/layers/tls}/tls13.uts (99%) rename test/{tls/tests_tls_netaccess.uts => scapy/layers/tls/tlsclientserver.uts} (95%) diff --git a/doc/scapy/layers/automotive.rst b/doc/scapy/layers/automotive.rst index bf74fc3c561..8316d8e14e9 100644 --- a/doc/scapy/layers/automotive.rst +++ b/doc/scapy/layers/automotive.rst @@ -1158,7 +1158,7 @@ then casted to ``UDS`` objects through the ``basecls`` parameter Usage example:: with PcapReader("test/contrib/automotive/ecu_trace.pcap") as sock: - udsmsgs = sniff(session=ISOTPSession, session_kwargs={"use_ext_addr":False, "basecls":UDS}, count=50, opened_socket=sock) + udsmsgs = sniff(session=ISOTPSession(use_ext_addr=False, basecls=UDS), count=50, opened_socket=sock) ecu = Ecu() @@ -1183,7 +1183,7 @@ Usage example:: session = EcuSession() with PcapReader("test/contrib/automotive/ecu_trace.pcap") as sock: - udsmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "use_ext_addr":False, "basecls":UDS}, count=50, opened_socket=sock) + udsmsgs = sniff(session=ISOTPSession(use_ext_addr=False, basecls=UDS, supersession=session)), count=50, opened_socket=sock) ecu = session.ecu print(ecu.log) diff --git a/doc/scapy/usage.rst b/doc/scapy/usage.rst index a06b5ba200d..dc4aca70014 100644 --- a/doc/scapy/usage.rst +++ b/doc/scapy/usage.rst @@ -783,9 +783,8 @@ Those sessions can be used using the ``session=`` parameter of ``sniff()``. Exam .. note:: To implement your own Session class, in order to support another flow-based protocol, start by copying a sample from `scapy/sessions.py `_ - Your custom ``Session`` class only needs to extend the :py:class:`~scapy.sessions.DefaultSession` class, and implement a ``on_packet_received`` function, such as in the example. + Your custom ``Session`` class only needs to extend the :py:class:`~scapy.sessions.DefaultSession` class, and implement a ``process`` or a ``recv`` function, such as in the examples. -.. note:: Would you need it, you can use: ``class TLS_over_TCP(TLSSession, TCPSession): pass`` to sniff TLS packets that are defragmented. How to use TCPSession to defragment TCP packets ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/scapy/arch/bpf/supersocket.py b/scapy/arch/bpf/supersocket.py index 4f3aea6cd32..3e57a64d1b0 100644 --- a/scapy/arch/bpf/supersocket.py +++ b/scapy/arch/bpf/supersocket.py @@ -425,9 +425,9 @@ def nonblock_recv(self): class L3bpfSocket(L2bpfSocket): - def recv(self, x=BPF_BUFFER_LENGTH): + def recv(self, x=BPF_BUFFER_LENGTH, **kwargs): """Receive on layer 3""" - r = SuperSocket.recv(self, x) + r = SuperSocket.recv(self, x, **kwargs) if r: r.payload.time = r.time return r.payload diff --git a/scapy/arch/libpcap.py b/scapy/arch/libpcap.py index 7ac3fcf66fe..fc4b210f4fa 100644 --- a/scapy/arch/libpcap.py +++ b/scapy/arch/libpcap.py @@ -38,13 +38,14 @@ import scapy.consts from typing import ( - cast, + Any, Dict, List, NoReturn, Optional, Tuple, Type, + cast, ) if not scapy.consts.WINDOWS: @@ -571,9 +572,9 @@ def send(self, x): class L3pcapSocket(L2pcapSocket): desc = "read/write packets at layer 3 using only libpcap" - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] - r = L2pcapSocket.recv(self, x) + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] + r = L2pcapSocket.recv(self, x, **kwargs) if r: r.payload.time = r.time return r.payload diff --git a/scapy/arch/linux.py b/scapy/arch/linux.py index d597ca4457a..58100e5479a 100644 --- a/scapy/arch/linux.py +++ b/scapy/arch/linux.py @@ -593,9 +593,9 @@ def send(self, x): class L3PacketSocket(L2Socket): desc = "read/write packets at layer 3 using Linux PF_PACKET sockets" - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] - pkt = SuperSocket.recv(self, x) + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] + pkt = SuperSocket.recv(self, x, **kwargs) if pkt and self.lvl == 2: pkt.payload.time = pkt.time return pkt.payload diff --git a/scapy/automaton.py b/scapy/automaton.py index 27e2cef3e1e..63a03ce3f03 100644 --- a/scapy/automaton.py +++ b/scapy/automaton.py @@ -636,11 +636,13 @@ def fileno(self): # type: () -> int return self.spb.fileno() - def recv(self, n=MTU): - # type: (Optional[int]) -> Any + # note: _ATMT_supersocket may return bytes in certain cases, which + # is expected. We cheat on typing. + def recv(self, n=MTU, **kwargs): # type: ignore + # type: (int, **Any) -> Any r = self.spb.recv(n) if self.proto is not None and r is not None: - r = self.proto(r) + r = self.proto(r, **kwargs) return r def close(self): diff --git a/scapy/config.py b/scapy/config.py index 3be0bfec4a7..7c4215998ff 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -701,6 +701,13 @@ def _iface_changer(attr, val, old): return val # type: ignore +def _reset_tls_nss_keys(attr, val, old): + # type: (str, Any, Any) -> Any + """Reset conf.tls_nss_keys when conf.tls_nss_filename changes""" + conf.tls_nss_keys = None + return val + + class Conf(ConfClass): """ This object contains the configuration of Scapy. @@ -775,7 +782,8 @@ class Conf(ConfClass): filter = "" #: when 1, store received packet that are not matched into `debug.recv` debug_match = False - #: When 1, print some TLS session secrets when they are computed. + #: When 1, print some TLS session secrets when they are computed, and + #: warn about the session recognition. debug_tls = False wepkey = "" #: holds the Scapy interface list and manager @@ -901,6 +909,16 @@ class Conf(ConfClass): #: a safety mechanism: the maximum amount of items included in a PacketListField #: or a FieldListField max_list_count = 100 + #: When the TLS module is loaded (not by default), the following turns on sessions + tls_session_enable = False + #: Filename containing NSS Keys Log + tls_nss_filename = Interceptor( + "tls_nss_filename", + None, + _reset_tls_nss_keys + ) + #: Dictionary containing parsed NSS Keys + tls_nss_keys = None def __getattribute__(self, attr): # type: (str) -> Any diff --git a/scapy/contrib/automotive/bmw/hsfz.py b/scapy/contrib/automotive/bmw/hsfz.py index 376a968e4b4..558141fc8bc 100644 --- a/scapy/contrib/automotive/bmw/hsfz.py +++ b/scapy/contrib/automotive/bmw/hsfz.py @@ -19,6 +19,7 @@ from scapy.data import MTU from typing import ( + Any, Optional, Tuple, Type, @@ -88,8 +89,8 @@ def __init__(self, ip='127.0.0.1', port=6801): StreamSocket.__init__(self, s, HSFZ) self.buffer = b"" - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] if self.buffer: len_data = self.buffer[:4] else: @@ -104,7 +105,7 @@ def recv(self, x=MTU): if len(self.buffer) != len_int: return None - pkt = self.basecls(self.buffer) # type: Packet + pkt = self.basecls(self.buffer, **kwargs) # type: Packet self.buffer = b"" return pkt @@ -141,11 +142,11 @@ def send(self, x): self.close() return 0 - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] pkt = super(UDS_HSFZSocket, self).recv(x) if pkt: - return self.outputcls(bytes(pkt.payload)) + return self.outputcls(bytes(pkt.payload), **kwargs) else: return pkt diff --git a/scapy/contrib/automotive/doip.py b/scapy/contrib/automotive/doip.py index bce9944aaa0..096bdb9e586 100644 --- a/scapy/contrib/automotive/doip.py +++ b/scapy/contrib/automotive/doip.py @@ -33,6 +33,7 @@ from scapy.data import MTU from typing import ( + Any, Union, Tuple, Optional, @@ -294,8 +295,8 @@ def __init__(self, ip='127.0.0.1', port=13400, activate_routing=True, self._activate_routing( source_address, target_address, activation_type, reserved_oem) - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] if self.buffer: len_data = self.buffer[:8] else: @@ -310,7 +311,7 @@ def recv(self, x=MTU): if len(self.buffer) != len_int: return None - pkt = self.basecls(self.buffer) # type: Packet + pkt = self.basecls(self.buffer, **kwargs) # type: Packet self.buffer = b"" return pkt @@ -407,9 +408,9 @@ def send(self, x): return super(UDS_DoIPSocket, self).send(pkt) - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] - pkt = super(UDS_DoIPSocket, self).recv(x) + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] + pkt = super(UDS_DoIPSocket, self).recv(x, **kwargs) if pkt and pkt.payload_type == 0x8001: return pkt.payload else: diff --git a/scapy/contrib/automotive/ecu.py b/scapy/contrib/automotive/ecu.py index c2caa769de6..7458468c95b 100644 --- a/scapy/contrib/automotive/ecu.py +++ b/scapy/contrib/automotive/ecu.py @@ -469,17 +469,16 @@ class EcuSession(DefaultSession): """ def __init__(self, *args, **kwargs): # type: (Any, Any) -> None - DefaultSession.__init__(self, *args, **kwargs) self.ecu = Ecu(logging=kwargs.pop("logging", True), verbose=kwargs.pop("verbose", True), store_supported_responses=kwargs.pop("store_supported_responses", True)) # noqa: E501 + super(EcuSession, self).__init__(*args, **kwargs) - def on_packet_received(self, pkt): - # type: (Optional[Packet]) -> None + def process(self, pkt: Packet) -> Optional[Packet]: if not pkt: - return + return None self.ecu.update(pkt) - DefaultSession.on_packet_received(self, pkt) + return pkt class EcuResponse: diff --git a/scapy/contrib/isotp/isotp_native_socket.py b/scapy/contrib/isotp/isotp_native_socket.py index 77e4d0f7af9..34a77cb84aa 100644 --- a/scapy/contrib/isotp/isotp_native_socket.py +++ b/scapy/contrib/isotp/isotp_native_socket.py @@ -23,6 +23,7 @@ # Typing imports from typing import ( + Any, Optional, Union, Tuple, @@ -387,9 +388,9 @@ def recv_raw(self, x=0xffff): ts = get_last_packet_timestamp(self.ins) return self.basecls, pkt, ts - def recv(self, x=0xffff): - # type: (int) -> Optional[Packet] - msg = SuperSocket.recv(self, x) + def recv(self, x=0xffff, **kwargs): + # type: (int, **Any) -> Optional[Packet] + msg = SuperSocket.recv(self, x, **kwargs) if msg is None: return msg diff --git a/scapy/contrib/isotp/isotp_soft_socket.py b/scapy/contrib/isotp/isotp_soft_socket.py index cc6be464d70..96411c41908 100644 --- a/scapy/contrib/isotp/isotp_soft_socket.py +++ b/scapy/contrib/isotp/isotp_soft_socket.py @@ -186,9 +186,9 @@ def recv_raw(self, x=0xffff): return self.basecls, tup[0], float(tup[1]) return self.basecls, None, None - def recv(self, x=0xffff): - # type: (int) -> Optional[Packet] - msg = super(ISOTPSoftSocket, self).recv(x) + def recv(self, x=0xffff, **kwargs): + # type: (int, **Any) -> Optional[Packet] + msg = super(ISOTPSoftSocket, self).recv(x, **kwargs) if msg is None: return None diff --git a/scapy/contrib/isotp/isotp_utils.py b/scapy/contrib/isotp/isotp_utils.py index 49269f20aab..da1d5e73ab9 100644 --- a/scapy/contrib/isotp/isotp_utils.py +++ b/scapy/contrib/isotp/isotp_utils.py @@ -14,12 +14,15 @@ from scapy.utils import EDecimal from scapy.packet import Packet from scapy.sessions import DefaultSession +from scapy.supersocket import SuperSocket from scapy.contrib.isotp.isotp_packet import ISOTP, N_PCI_CF, N_PCI_SF, \ N_PCI_FF, N_PCI_FC # Typing imports from typing import ( + cast, Iterable, + Iterator, Optional, Union, List, @@ -336,20 +339,23 @@ class ISOTPSession(DefaultSession): def __init__(self, *args, **kwargs): # type: (Any, Any) -> None - super(ISOTPSession, self).__init__(*args, **kwargs) self.m = ISOTPMessageBuilder( use_ext_address=kwargs.pop("use_ext_address", None), rx_id=kwargs.pop("rx_id", None), basecls=kwargs.pop("basecls", ISOTP)) + super(ISOTPSession, self).__init__(*args, **kwargs) - def on_packet_received(self, pkt): - # type: (Optional[Packet]) -> None + def recv(self, sock: SuperSocket) -> Iterator[Packet]: + """ + Will be called by sniff() to ask for a packet + """ + pkt = sock.recv() if not pkt: return self.m.feed(pkt) while len(self.m) > 0: - rcvd = self.m.pop() - if self._supersession: - self._supersession.on_packet_received(rcvd) - else: - super(ISOTPSession, self).on_packet_received(rcvd) + rcvd = cast(Optional[Packet], self.m.pop()) + if rcvd: + rcvd = self.process(rcvd) + if rcvd: + yield rcvd diff --git a/scapy/layers/dcerpc.py b/scapy/layers/dcerpc.py index 8caf6857fb3..e1901c30b1c 100644 --- a/scapy/layers/dcerpc.py +++ b/scapy/layers/dcerpc.py @@ -91,6 +91,11 @@ EPacketListField, ) +# Typing imports +from typing import ( + Optional, +) + # DCE/RPC Packet DCE_RPC_TYPE = { @@ -1895,12 +1900,11 @@ def _process_dcerpc_packet(self, pkt): pkt = self._parse_with_opnum(pkt, opnum, opts) return pkt - def on_packet_received(self, pkt): + def process(self, pkt: Packet) -> Optional[Packet]: if DceRpc5 in pkt: - return super(DceRpcSession, self).on_packet_received( - self._process_dcerpc_packet(pkt) - ) - return super(DceRpcSession, self).on_packet_received(pkt) + return self._process_dcerpc_packet(pkt) + else: + return pkt # --- TODO cleanup below diff --git a/scapy/layers/inet.py b/scapy/layers/inet.py index ad12ce82515..8741b3bdad6 100644 --- a/scapy/layers/inet.py +++ b/scapy/layers/inet.py @@ -527,7 +527,6 @@ def i2h(self, pkt, x): class IP(Packet, IPTools): - __slots__ = ["_defrag_pos"] name = "IP" fields_desc = [BitField("version", 4, 4), BitField("ihl", None, 4), @@ -625,38 +624,7 @@ def mysummary(self): def fragment(self, fragsize=1480): """Fragment IP datagrams""" - lastfragsz = fragsize - fragsize -= fragsize % 8 - lst = [] - fnb = 0 - fl = self - while fl.underlayer is not None: - fnb += 1 - fl = fl.underlayer - - for p in fl: - s = raw(p[fnb].payload) - if len(s) <= lastfragsz: - lst.append(p) - continue - - nb = (len(s) - lastfragsz + fragsize - 1) // fragsize + 1 - for i in range(nb): - q = p.copy() - del q[fnb].payload - del q[fnb].chksum - del q[fnb].len - if i != nb - 1: - q[fnb].flags |= 1 - fragend = (i + 1) * fragsize - else: - fragend = i * fragsize + lastfragsz - q[fnb].frag += i * fragsize // 8 - r = conf.raw_layer(load=s[i * fragsize:fragend]) - r.overload_fields = p[fnb].payload.overload_fields.copy() - q.add_payload(r) - lst.append(q) - return lst + return fragment(self, fragsize=fragsize) def in4_pseudoheader(proto, u, plen): @@ -1145,6 +1113,9 @@ def inet_register_l3(l2, l3): @conf.commands.register def fragment(pkt, fragsize=1480): """Fragment a big IP datagram""" + if fragsize < 8: + warning("fragsize cannot be lower than 8") + fragsize = max(fragsize, 8) lastfragsz = fragsize fragsize -= fragsize % 8 lst = [] @@ -1189,86 +1160,115 @@ def overlap_frag(p, overlap, fragsize=8, overlap_fragsize=None): return qfrag + fragment(p, fragsize) -def _defrag_list(lst, defrag, missfrag): - """Internal usage only. Part of the _defrag_logic""" - p = lst[0] - lastp = lst[-1] - if p.frag > 0 or lastp.flags.MF: # first or last fragment missing - missfrag.extend(lst) - return - p = p.copy() - if conf.padding_layer in p: - del p[conf.padding_layer].underlayer.payload - ip = p[IP] - if ip.len is None or ip.ihl is None: - c_len = len(ip.payload) - else: - c_len = ip.len - (ip.ihl << 2) - txt = conf.raw_layer() - for q in lst[1:]: - if c_len != q.frag << 3: # Wrong fragmentation offset - if c_len > q.frag << 3: - warning("Fragment overlap (%i > %i) %r || %r || %r" % (c_len, q.frag << 3, p, txt, q)) # noqa: E501 - missfrag.extend(lst) - break - if q[IP].len is None or q[IP].ihl is None: - c_len += len(q[IP].payload) +class BadFragments(ValueError): + def __init__(self, *args, **kwargs): + self.frags = kwargs.pop("frags", None) + super(BadFragments, self).__init__(*args, **kwargs) + + +def _defrag_iter_and_check_offsets(frags): + """ + Internal generator used in _defrag_ip_pkt + """ + offset = 0 + for pkt, o, length in frags: + if offset != o: + if offset > o: + op = ">" + else: + op = "<" + warning("Fragment overlap (%i %s %i) on %r" % (offset, op, o, pkt)) + raise BadFragments + offset += length + yield bytes(pkt[IP].payload) + + +def _defrag_ip_pkt(pkt, frags): + """ + Defragment a single IP packet. + + :param pkt: the new pkt + :param frags: a defaultdict(list) used for storage + :return: a tuple (fragmented, defragmented_value) + """ + ip = pkt[IP] + if pkt.frag != 0 or ip.flags.MF: + # fragmented ! + uid = (ip.id, ip.src, ip.dst, ip.proto) + if ip.len is None or ip.ihl is None: + fraglen = len(ip.payload) else: - c_len += q[IP].len - (q[IP].ihl << 2) - if conf.padding_layer in q: - del q[conf.padding_layer].underlayer.payload - txt.add_payload(q[IP].payload.copy()) - if q.time > p.time: - p.time = q.time - else: - ip.flags.MF = False - del ip.chksum - del ip.len - p = p / txt - p._defrag_pos = max(x._defrag_pos for x in lst) - defrag.append(p) + fraglen = ip.len - (ip.ihl << 2) + # (pkt, frag offset, frag len) + frags[uid].append((pkt, ip.frag << 3, fraglen)) + if not ip.flags.MF: # no more fragments = last fragment + curfrags = sorted(frags[uid], key=lambda x: x[1]) # sort by offset + try: + data = b"".join(_defrag_iter_and_check_offsets(curfrags)) + except ValueError: + # bad fragment + badfrags = frags[uid] + del frags[uid] + raise BadFragments(frags=badfrags) + # re-build initial packet without fragmentation + p = curfrags[0][0].copy() + pay_class = p[IP].payload.__class__ + p[IP].flags.MF = False + p[IP].remove_payload() + p[IP].len = None + p[IP].chksum = None + # append defragmented payload + p /= pay_class(data) + # cleanup + del frags[uid] + return True, p + return True, None + return False, pkt def _defrag_logic(plist, complete=False): - """Internal function used to defragment a list of packets. + """ + Internal function used to defragment a list of packets. It contains the logic behind the defrag() and defragment() functions """ - frags = defaultdict(lambda: []) + frags = defaultdict(list) final = [] - pos = 0 - for p in plist: - p._defrag_pos = pos - pos += 1 - if IP in p: - ip = p[IP] - if ip.frag != 0 or ip.flags.MF: - uniq = (ip.id, ip.src, ip.dst, ip.proto) - frags[uniq].append(p) - continue - final.append(p) - - defrag = [] - missfrag = [] - for lst in frags.values(): - lst.sort(key=lambda x: x.frag) - _defrag_list(lst, defrag, missfrag) - defrag2 = [] - for p in defrag: - q = p.__class__(raw(p)) - q._defrag_pos = p._defrag_pos - q.time = p.time - defrag2.append(q) + notfrag = [] + badfrag = [] + # Defrag + for i, pkt in enumerate(plist): + if IP not in pkt: + # no IP layer + if complete: + final.append(pkt) + continue + try: + fragmented, defragmented_value = _defrag_ip_pkt( + pkt, + frags, + ) + except BadFragments as ex: + if complete: + final.extend(ex.frags) + else: + badfrag.extend(ex.frags) + continue + if complete and defragmented_value: + final.append(defragmented_value) + elif defragmented_value: + if fragmented: + final.append(defragmented_value) + else: + notfrag.append(defragmented_value) + # Return if complete: - final.extend(defrag2) - final.extend(missfrag) - final.sort(key=lambda x: x._defrag_pos) if hasattr(plist, "listname"): name = "Defragmented %s" % plist.listname else: name = "Defragmented" return PacketList(final, name=name) else: - return PacketList(final), PacketList(defrag2), PacketList(missfrag) + return PacketList(notfrag), PacketList(final), PacketList(badfrag) @conf.commands.register @@ -1911,7 +1911,7 @@ def parse_args(self, ip, port, srcip=None, **kargs): self.src = self.l4.src self.sack = self.l4[TCP].ack self.rel_seq = None - self.rcvbuf = TCPSession(prn=self._transmit_packet, store=False) + self.rcvbuf = TCPSession() bpf = "host %s and host %s and port %i and port %i" % (self.src, self.dst, self.sport, @@ -1996,7 +1996,7 @@ def receive_data(self, pkt): # Answer with an Ack self.send(self.l4) # Process data - will be sent to the SuperSocket through this - self.rcvbuf.on_packet_received(pkt) + self._transmit_packet(self.rcvbuf.process(pkt)) @ATMT.ioevent(ESTABLISHED, name="tcp", as_supersocket="tcplink") def outgoing_data_received(self, fd): diff --git a/scapy/layers/netflow.py b/scapy/layers/netflow.py index 0ce79dd4a91..69cfc6b77a3 100644 --- a/scapy/layers/netflow.py +++ b/scapy/layers/netflow.py @@ -64,11 +64,16 @@ ) from scapy.packet import Packet, bind_layers, bind_bottom_up from scapy.plist import PacketList -from scapy.sessions import IPSession, DefaultSession +from scapy.sessions import IPSession from scapy.layers.inet import UDP from scapy.layers.inet6 import IP6Field +# Typing imports +from typing import ( + Optional, +) + class NetflowHeader(Packet): name = "Netflow Header" @@ -1596,25 +1601,21 @@ class NetflowSession(IPSession): See help(scapy.layers.netflow) for more infos. """ def __init__(self, *args, **kwargs): - IPSession.__init__(self, *args, **kwargs) self.definitions = {} self.definitions_opts = {} self.ignored = set() + super(NetflowSession, self).__init__(*args, **kwargs) - def _process_packet(self, pkt): + def process(self, pkt: Packet) -> Optional[Packet]: + pkt = super(NetflowSession, self).process(pkt) + if not pkt: + return _netflowv9_defragment_packet(pkt, self.definitions, self.definitions_opts, self.ignored) return pkt - def on_packet_received(self, pkt): - # First, defragment IP if necessary - pkt = self._ip_process_packet(pkt) - # Now handle NetflowV9 defragmentation - pkt = self._process_packet(pkt) - DefaultSession.on_packet_received(self, pkt) - class NetflowOptionsRecordScopeV9(NetflowRecordV9): name = "Netflow Options Template Record V9/10 - Scope" diff --git a/scapy/layers/tls/handshake.py b/scapy/layers/tls/handshake.py index eeffad947bb..9d283e3ea75 100644 --- a/scapy/layers/tls/handshake.py +++ b/scapy/layers/tls/handshake.py @@ -463,7 +463,7 @@ def tls_session_update(self, msg_str): # RFC 8701: GREASE of TLS will send unknown versions # here. We have to ignore them if ver in _tls_version: - self.tls_session.advertised_tls_version = ver + s.advertised_tls_version = ver break if isinstance(e, TLS_Ext_SignatureAlgorithms): s.advertised_sig_algs = e.sig_algs @@ -1329,7 +1329,14 @@ def tls_session_update(self, msg_str): self.tls_session.session_hash = ( Hash_MD5().digest(to_hash) + Hash_SHA().digest(to_hash) ) - self.tls_session.compute_ms_and_derive_keys() + if self.tls_session.pre_master_secret: + self.tls_session.compute_ms_and_derive_keys() + + if not self.tls_session.master_secret: + # There are still no master secret (we're just passive) + if self.tls_session.use_nss_master_secret_if_present(): + # we have a NSS file + self.tls_session.compute_ms_and_derive_keys() ############################################################################### diff --git a/scapy/layers/tls/keyexchange.py b/scapy/layers/tls/keyexchange.py index dc6546d9919..cd81a20adaf 100644 --- a/scapy/layers/tls/keyexchange.py +++ b/scapy/layers/tls/keyexchange.py @@ -747,7 +747,7 @@ def fill_missing(self): if s.client_kx_privkey and s.server_kx_pubkey: pms = s.client_kx_privkey.exchange(s.server_kx_pubkey) s.pre_master_secret = pms.lstrip(b"\x00") - if not s.extms or s.session_hash: + if not s.extms: # If extms is set (extended master secret), the key will # need the session hash to be computed. This is provided # by the TLSClientKeyExchange. Same in all occurrences @@ -781,7 +781,7 @@ def post_dissection(self, m): if s.server_kx_privkey and s.client_kx_pubkey: ZZ = s.server_kx_privkey.exchange(s.client_kx_pubkey) s.pre_master_secret = ZZ.lstrip(b"\x00") - if not s.extms or s.session_hash: + if not s.extms: s.compute_ms_and_derive_keys() def guess_payload_class(self, p): @@ -828,7 +828,7 @@ def fill_missing(self): if s.client_kx_privkey and s.server_kx_pubkey: s.pre_master_secret = pms - if not s.extms or s.session_hash: + if not s.extms: s.compute_ms_and_derive_keys() def post_build(self, pkt, pay): @@ -854,7 +854,7 @@ def post_dissection(self, m): if s.server_kx_privkey and s.client_kx_pubkey: ZZ = s.server_kx_privkey.exchange(ec.ECDH(), s.client_kx_pubkey) s.pre_master_secret = ZZ - if not s.extms or s.session_hash: + if not s.extms: s.compute_ms_and_derive_keys() @@ -918,7 +918,7 @@ def pre_dissect(self, m): warning(err) s.pre_master_secret = pms - if not s.extms or s.session_hash: + if not s.extms: s.compute_ms_and_derive_keys() return pms @@ -934,7 +934,7 @@ def post_build(self, pkt, pay): s = self.tls_session s.pre_master_secret = enc - if not s.extms or s.session_hash: + if not s.extms: s.compute_ms_and_derive_keys() if s.server_tmp_rsa_key is not None: diff --git a/scapy/layers/tls/record.py b/scapy/layers/tls/record.py index c8e55a34757..e6c59456913 100644 --- a/scapy/layers/tls/record.py +++ b/scapy/layers/tls/record.py @@ -40,14 +40,6 @@ if conf.crypto_valid_advanced: from scapy.layers.tls.crypto.cipher_aead import Cipher_CHACHA20_POLY1305 -# Util - - -def _tls_version_check(version, min): - """Returns if version >= min, or False if version == None""" - if version is None: - return False - return version >= min ############################################################################### # TLS Record Protocol # @@ -216,7 +208,7 @@ def addfield(self, pkt, s, val): # Add TLS13ClientHello in case of HelloRetryRequest # Add ChangeCipherSpec for middlebox compatibility if (isinstance(pkt, _GenericTLSSessionInheritance) and - _tls_version_check(pkt.tls_session.tls_version, 0x0304) and + pkt.tls_session.tls_version == 0x0304 and not isinstance(pkt.msg[0], TLS13ServerHello) and not isinstance(pkt.msg[0], TLS13ClientHello) and not isinstance(pkt.msg[0], TLSChangeCipherSpec)): @@ -336,8 +328,14 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): return SSLv2 # Not SSLv2: continuation return _TLSEncryptedContent + if plen >= 5: + # Check minimum length + msglen = struct.unpack('!H', _pkt[3:5])[0] + 5 + if plen < msglen: + # This is a fragment + return conf.padding_layer # Check TLS 1.3 - if s and _tls_version_check(s.tls_version, 0x0304): + if s and s.tls_version == 0x0304: _has_cipher = lambda x: ( x and not isinstance(x.cipher, Cipher_NULL) ) @@ -575,12 +573,24 @@ def do_dissect_payload(self, s): as the TLS session to be used would get lost. """ if s: + # Check minimum length + if len(s) < 5: + p = conf.raw_layer(s, _internal=1, _underlayer=self) + self.add_payload(p) + return + msglen = struct.unpack('!H', s[3:5])[0] + 5 + if len(s) < msglen: + # This is a fragment + self.add_payload(conf.padding_layer(s)) + return try: p = TLS(s, _internal=1, _underlayer=self, tls_session=self.tls_session) except KeyboardInterrupt: raise except Exception: + if conf.debug_dissector: + raise p = conf.raw_layer(s, _internal=1, _underlayer=self) self.add_payload(p) @@ -734,11 +744,11 @@ def post_build(self, pkt, pay): return hdr + efrag + pay def mysummary(self): - s = super(TLS, self).mysummary() + s, n = super(TLS, self).mysummary() if self.msg: s += " / " s += " / ".join(getattr(x, "_name", x.name) for x in self.msg) - return s + return s, n ############################################################################### # TLS ChangeCipherSpec # diff --git a/scapy/layers/tls/record_sslv2.py b/scapy/layers/tls/record_sslv2.py index abe5004610a..8d311faaadc 100644 --- a/scapy/layers/tls/record_sslv2.py +++ b/scapy/layers/tls/record_sslv2.py @@ -141,7 +141,7 @@ def pre_dissect(self, s): is_mac_ok = self._sslv2_mac_verify(cfrag + pad, mac) if not is_mac_ok: pkt_info = self.firstlayer().summary() - log_runtime.info("TLS: record integrity check failed [%s]", pkt_info) # noqa: E501 + log_runtime.info("SSLv2: record integrity check failed [%s]", pkt_info) # noqa: E501 reconstructed_body = mac + cfrag + pad return hdr + reconstructed_body + r diff --git a/scapy/layers/tls/record_tls13.py b/scapy/layers/tls/record_tls13.py index b505bc8e20f..ff8f0acec4d 100644 --- a/scapy/layers/tls/record_tls13.py +++ b/scapy/layers/tls/record_tls13.py @@ -15,7 +15,6 @@ import struct -from scapy.config import conf from scapy.error import log_runtime, warning from scapy.compat import raw, orb from scapy.fields import ByteEnumField, PacketField, XStrField @@ -125,7 +124,7 @@ def _tls_auth_decrypt(self, s): return e.args except AEADTagError as e: pkt_info = self.firstlayer().summary() - log_runtime.info("TLS: record integrity check failed [%s]", pkt_info) # noqa: E501 + log_runtime.info("TLS 1.3: record integrity check failed [%s]", pkt_info) # noqa: E501 return e.args def pre_dissect(self, s): @@ -172,15 +171,7 @@ def do_dissect_payload(self, s): Note that overloading .guess_payload_class() would not be enough, as the TLS session to be used would get lost. """ - if s: - try: - p = TLS(s, _internal=1, _underlayer=self, - tls_session=self.tls_session) - except KeyboardInterrupt: - raise - except Exception: - p = conf.raw_layer(s, _internal=1, _underlayer=self) - self.add_payload(p) + return TLS.do_dissect_payload(self, s) # Building methods @@ -223,3 +214,10 @@ def post_build(self, pkt, pay): self.tls_session.triggered_pwcs_commit = False return hdr + frag + pay + + def mysummary(self): + s, n = super(TLS13, self).mysummary() + if self.inner and self.inner.msg: + s += " / " + s += " / ".join(getattr(x, "_name", x.name) for x in self.inner.msg) + return s, n diff --git a/scapy/layers/tls/session.py b/scapy/layers/tls/session.py index 8121bb24cfa..d78562508e7 100644 --- a/scapy/layers/tls/session.py +++ b/scapy/layers/tls/session.py @@ -10,6 +10,7 @@ """ import binascii +import collections import socket import struct @@ -18,7 +19,7 @@ from scapy.error import log_runtime, warning from scapy.packet import Packet from scapy.pton_ntop import inet_pton -from scapy.sessions import DefaultSession +from scapy.sessions import TCPSession from scapy.utils import repr_hex, strxor from scapy.layers.inet import TCP from scapy.layers.tls.crypto.compression import Comp_NULL @@ -34,7 +35,8 @@ def load_nss_keys(filename): """ Parses a NSS Keys log and returns unpacked keys in a dictionary. """ - keys = {} + # http://udn.realityripple.com/docs/Mozilla/Projects/NSS/Key_Log_Format + keys = collections.defaultdict(dict) try: fd = open(filename) fd.close() @@ -65,11 +67,10 @@ def load_nss_keys(filename): # Warn that a duplicated entry was detected. The latest one # will be kept in the resulting dictionary. - if data[0] in keys: + if client_random in keys[data[0]]: warning("Duplicated entry for %s !", data[0]) - keys[data[0]] = {"ClientRandom": client_random, - "Secret": secret} + keys[data[0]][client_random] = secret return keys @@ -368,6 +369,9 @@ def __init__(self, self.dport = dport self.sid = sid + # Identify duplicate sessions + self.firsttcp = None + # Our TCP socket. None until we send (or receive) a packet. self.sock = None @@ -529,6 +533,21 @@ def __setattr__(self, name, val): self.pwcs.connection_end = val super(tlsSession, self).__setattr__(name, val) + # Get infos from underlayer + + def set_underlayer(self, _underlayer): + if isinstance(_underlayer, TCP): + tcp = _underlayer + self.sport = tcp.sport + self.dport = tcp.dport + try: + self.ipsrc = tcp.underlayer.src + self.ipdst = tcp.underlayer.dst + except AttributeError: + pass + if self.firsttcp is None: + self.firsttcp = tcp.seq + # Mirroring def mirror(self): @@ -541,15 +560,15 @@ def mirror(self): client and the server. In such a situation, it should be used every time the message being read comes from a different side than the one read right before, as the reading state becomes the writing state, and - vice versa. For instance you could do: + vice versa. For instance you could do:: - client_hello = open('client_hello.raw').read() - + client_hello = open('client_hello.raw').read() + - m1 = TLS(client_hello) - m2 = TLS(server_hello, tls_session=m1.tls_session.mirror()) - m3 = TLS(server_cert, tls_session=m2.tls_session) - m4 = TLS(client_keyexchange, tls_session=m3.tls_session.mirror()) + m1 = TLS(client_hello) + m2 = TLS(server_hello, tls_session=m1.tls_session.mirror()) + m3 = TLS(server_cert, tls_session=m2.tls_session) + m4 = TLS(client_keyexchange, tls_session=m3.tls_session.mirror()) """ self.ipdst, self.ipsrc = self.ipsrc, self.ipdst @@ -598,12 +617,16 @@ def compute_master_secret(self): if conf.debug_tls: log_runtime.debug("TLS: master secret: %s", repr_hex(ms)) - def compute_ms_and_derive_keys(self): + def use_nss_master_secret_if_present(self) -> bool: # Load the master secret from an NSS Key dictionary - if self.nss_keys and self.nss_keys.get("CLIENT_RANDOM", False) and \ - self.nss_keys["CLIENT_RANDOM"].get("Secret", False): - self.master_secret = self.nss_keys["CLIENT_RANDOM"]["Secret"] + if not self.nss_keys or "CLIENT_RANDOM" not in self.nss_keys: + return False + if self.client_random in self.nss_keys["CLIENT_RANDOM"]: + self.master_secret = self.nss_keys["CLIENT_RANDOM"][self.client_random] + return True + return False + def compute_ms_and_derive_keys(self): if not self.master_secret: self.compute_master_secret() @@ -903,13 +926,20 @@ def eq(self, other): return False - def __repr__(self): + def repr(self, _underlayer=None): sid = repr(self.sid) if len(sid) > 12: sid = sid[:11] + "..." + if _underlayer and _underlayer.dport != self.dport: + return "%s:%s > %s:%s" % (self.ipdst, str(self.dport), + self.ipsrc, str(self.sport)) return "%s:%s > %s:%s" % (self.ipsrc, str(self.sport), self.ipdst, str(self.dport)) + def __repr__(self): + return self.repr() + + ############################################################################### # Session singleton # ############################################################################### @@ -946,14 +976,8 @@ def __init__(self, _pkt="", post_transform=None, _internal=0, self.wcs_snap_init = self.tls_session.wcs.snapshot() if isinstance(_underlayer, TCP): - tcp = _underlayer - self.tls_session.sport = tcp.sport - self.tls_session.dport = tcp.dport - try: - self.tls_session.ipsrc = tcp.underlayer.src - self.tls_session.ipdst = tcp.underlayer.dst - except AttributeError: - pass + # Get information from _underlayer + self.tls_session.set_underlayer(_underlayer) # Load a NSS Key Log file if conf.tls_nss_filename is not None: @@ -1079,25 +1103,52 @@ def show2(self): s.rcs = rcs_snap s.wcs = wcs_snap - def mysummary(self): - return "TLS %s / %s" % (repr(self.tls_session), - getattr(self, "_name", self.name)) + def mysummary(self, first=True): + from scapy.layers.tls.record import TLS + from scapy.layers.tls.record_tls13 import TLS13 + if ( + self.underlayer and + isinstance(self.underlayer, _GenericTLSSessionInheritance) + ): + summary = getattr(self, "_name", self.name) + else: + _underlayer = None + if self.underlayer and isinstance(self.underlayer, TCP): + _underlayer = self.underlayer + summary = "TLS %s / %s" % ( + self.tls_session.repr(_underlayer=_underlayer), + getattr(self, "_name", self.name) + ) + return summary, [TLS, TLS13] @classmethod def tcp_reassemble(cls, data, metadata, session): - # Used with TLSSession + # Used with TCPSession from scapy.layers.tls.record import TLS from scapy.layers.tls.record_tls13 import TLS13 if cls in (TLS, TLS13): length = struct.unpack("!H", data[3:5])[0] + 5 - if len(data) == length: - return cls(data) - elif len(data) > length: - pkt = cls(data) - if hasattr(pkt.payload, "tcp_reassemble"): - return pkt.payload.tcp_reassemble(data[length:], metadata, session) - else: - return pkt + if len(data) >= length: + # get the underlayer as it is used to populate tls_session + underlayer = metadata["original"][TCP].copy() + underlayer.remove_payload() + # eventually get the tls_session now for TLS.dispatch_hook + tls_session = None + if conf.tls_session_enable: + s = tlsSession() + s.set_underlayer(underlayer) + tls_session = conf.tls_sessions.find(s) + if tls_session: + if tls_session.dport != underlayer.dport: + tls_session = tls_session.mirror() + if tls_session.firsttcp == underlayer.seq: + log_runtime.info( + "TLS: session %s is a duplicate of a previous " + "dissection. Discard it" % repr(tls_session) + ) + conf.tls_sessions.rem(tls_session, force=True) + tls_session = None + return cls(data, _underlayer=underlayer, tls_session=tls_session) else: return cls(data) @@ -1123,11 +1174,12 @@ def add(self, session): else: self.sessions[h] = [session] - def rem(self, session): - s = self.find(session) - if s: - log_runtime.info("TLS: previous session shall not be overwritten") - return + def rem(self, session, force=False): + if not force: + s = self.find(session) + if s: + log_runtime.info("TLS: previous session shall not be overwritten") + return h = session.hash() self.sessions[h].remove(session) @@ -1140,10 +1192,10 @@ def find(self, session): if h in self.sessions: for k in self.sessions[h]: if k.eq(session): - if conf.tls_verbose: + if conf.debug_tls: log_runtime.info("TLS: found session matching %s", k) return k - if conf.tls_verbose: + if conf.debug_tls: log_runtime.info("TLS: did not find session matching %s", session) return None @@ -1162,8 +1214,13 @@ def __repr__(self): return "\n".join(map(lambda x: fmt % x, res)) -class TLSSession(DefaultSession): +class TLSSession(TCPSession): def __init__(self, *args, **kwargs): + # XXX this doesn't bring any value. + warning( + "TLSSession is deprecated and will be removed in a future version. " + "Please use TCPSession instead with conf.tls_session_enable=True" + ) server_rsa_key = kwargs.pop("server_rsa_key", None) super(TLSSession, self).__init__(*args, **kwargs) self._old_conf_status = conf.tls_session_enable @@ -1176,10 +1233,5 @@ def toPacketList(self): return super(TLSSession, self).toPacketList() +# Instantiate the TLS sessions holder conf.tls_sessions = _tls_sessions() -conf.tls_session_enable = False -conf.tls_verbose = False -# Filename containing NSS Keys Log -conf.tls_nss_filename = None -# Dictionary containing parsed NSS Keys -conf.tls_nss_keys = None diff --git a/scapy/libs/extcap.py b/scapy/libs/extcap.py index b61912e057a..be0e6b05663 100644 --- a/scapy/libs/extcap.py +++ b/scapy/libs/extcap.py @@ -158,8 +158,8 @@ def __init__(self, *_: Any, **kwarg: Any) -> None: self.reader = PcapReader(self.fd) # type: ignore self.ins = self.reader # type: ignore - def recv(self, x: int = MTU) -> Packet: - return self.reader.recv(x) + def recv(self, x: int = MTU, **kwargs: Any) -> Packet: + return self.reader.recv(x, **kwargs) def close(self) -> None: self.proc.kill() diff --git a/scapy/packet.py b/scapy/packet.py index e35b1bfc45a..4074d12789f 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -88,6 +88,7 @@ class Packet( "packetfields", "original", "explicit", "raw_packet_cache", "raw_packet_cache_fields", "_pkt", "post_transforms", + "stop_dissection_after", # then payload, underlayer and parent "payload", "underlayer", "parent", "name", @@ -146,6 +147,7 @@ def __init__(self, _internal=0, # type: int _underlayer=None, # type: Optional[Packet] _parent=None, # type: Optional[Packet] + stop_dissection_after=None, # type: Optional[Type[Packet]] **fields # type: Any ): # type: (...) -> None @@ -174,6 +176,7 @@ def __init__(self, self.direction = None # type: Optional[int] self.sniffed_on = None # type: Optional[_GlobInterfaceType] self.comment = None # type: Optional[bytes] + self.stop_dissection_after = stop_dissection_after if _pkt: self.dissect(_pkt) if not _internal: @@ -1033,9 +1036,22 @@ def do_dissect_payload(self, s): :param str s: the raw layer """ if s: + if ( + self.stop_dissection_after and + isinstance(self, self.stop_dissection_after) + ): + # stop dissection here + p = conf.raw_layer(s, _internal=1, _underlayer=self) + self.add_payload(p) + return cls = self.guess_payload_class(s) try: - p = cls(s, _internal=1, _underlayer=self) + p = cls( + s, + stop_dissection_after=self.stop_dissection_after, + _internal=1, + _underlayer=self, + ) except KeyboardInterrupt: raise except Exception: diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py index 85136ab18a5..a994d0399b3 100644 --- a/scapy/sendrecv.py +++ b/scapy/sendrecv.py @@ -1092,20 +1092,17 @@ def _run(self, iface=None, # type: Optional[_GlobInterfaceType] started_callback=None, # type: Optional[Callable[[], Any]] session=None, # type: Optional[_GlobSessionType] - session_kwargs={}, # type: Dict[str, Any] **karg # type: Any ): # type: (...) -> None self.running = True + self.count = 0 + lst = [] # Start main thread # instantiate session if not isinstance(session, DefaultSession): session = session or DefaultSession - session = session(prn=prn, store=store, - **session_kwargs) - else: - session.prn = prn - session.store = store + session = session() # sniff_sockets follows: {socket: label} sniff_sockets = {} # type: Dict[SuperSocket, _GlobInterfaceType] if opened_socket is not None: @@ -1238,8 +1235,28 @@ def stop_cb(): for s in sockets: if s is close_pipe: # type: ignore break + # The session object is passed the socket to call recv() on, + # and may perform additional processing (ip defrag, etc.) try: - p = s.recv() + packets = session.recv(s) + # A session can return multiple objects + for p in packets: + if lfilter and not lfilter(p): + continue + p.sniffed_on = sniff_sockets[s] + # post-processing + self.count += 1 + if store: + lst.append(p) + if prn: + result = prn(p) + if result is not None: + print(result) + # check + if (stop_filter and stop_filter(p)) or \ + (0 < count <= self.count): + self.continue_sniff = False + break except EOFError: # End of stream try: @@ -1262,18 +1279,6 @@ def stop_cb(): if conf.debug_dissector >= 2: raise continue - if p is None: - continue - if lfilter and not lfilter(p): - continue - p.sniffed_on = sniff_sockets[s] - # on_packet_received handles the prn/storage - session.on_packet_received(p) - # check - if (stop_filter and stop_filter(p)) or \ - (0 < count <= session.count): - self.continue_sniff = False - break # Removed dead sockets for s in dead_sockets: del sniff_sockets[s] @@ -1289,7 +1294,7 @@ def stop_cb(): s.close() elif close_pipe: close_pipe.close() - self.results = session.toPacketList() + self.results = PacketList(lst, "Sniffed") def start(self): # type: () -> None diff --git a/scapy/sessions.py b/scapy/sessions.py index 8317204060f..72856484564 100644 --- a/scapy/sessions.py +++ b/scapy/sessions.py @@ -10,105 +10,55 @@ import socket import struct -from scapy.compat import raw, orb +from scapy.compat import orb from scapy.config import conf from scapy.packet import NoPayload, Packet -from scapy.plist import PacketList from scapy.pton_ntop import inet_pton # Typing imports from typing import ( Any, - Callable, DefaultDict, Dict, + Iterator, List, Optional, Tuple, - cast + cast, + TYPE_CHECKING, ) +from scapy.compat import Self +if TYPE_CHECKING: + from scapy.supersocket import SuperSocket class DefaultSession(object): """Default session: no stream decoding""" - def __init__( - self, - prn=None, # type: Optional[Callable[[Packet], Any]] - store=False, # type: bool - supersession=None, # type: Optional[DefaultSession] - *args, # type: Any - **karg # type: Any - ): - # type: (...) -> None - self.__prn = prn - self.__store = store - self.lst = [] # type: List[Packet] - self.__count = 0 - self._supersession = supersession - if self._supersession: - self._supersession.prn = self.__prn - self._supersession.store = self.__store - self.__store = False - self.__prn = None - - @property - def store(self): - # type: () -> bool - return self.__store - - @store.setter - def store(self, val): - # type: (bool) -> None - if self._supersession: - self._supersession.store = val - else: - self.__store = val - - @property - def prn(self): - # type: () -> Optional[Callable[[Packet], Any]] - return self.__prn - - @prn.setter - def prn(self, f): - # type: (Optional[Any]) -> None - if self._supersession: - self._supersession.prn = f - else: - self.__prn = f + def __init__(self, supersession: Optional[Self] = None): + if supersession and not isinstance(supersession, DefaultSession): + supersession = supersession() + self.supersession = supersession - @property - def count(self): - # type: () -> int - if self._supersession: - return self._supersession.count - else: - return self.__count - - def toPacketList(self): - # type: () -> PacketList - if self._supersession: - return PacketList(self._supersession.lst, "Sniffed") - else: - return PacketList(self.lst, "Sniffed") + def process(self, pkt: Packet) -> Optional[Packet]: + """ + Called to pre-process the packet + """ + # Optionally handle supersession + if self.supersession: + return self.supersession.process(pkt) + return pkt - def on_packet_received(self, pkt): - # type: (Optional[Packet]) -> None - """DEV: entry point. Will be called by sniff() for each - received packet (that passes the filters). + def recv(self, sock: 'SuperSocket') -> Iterator[Packet]: """ + Will be called by sniff() to ask for a packet + """ + pkt = sock.recv() if not pkt: return - if not isinstance(pkt, Packet): - raise TypeError("Only provide a Packet.") - self.__count += 1 - if self.store: - self.lst.append(pkt) - if self.prn: - result = self.prn(pkt) - if result is not None: - print(result) + pkt = self.process(pkt) + if pkt: + yield pkt class IPSession(DefaultSession): @@ -123,39 +73,11 @@ def __init__(self, *args, **kwargs): DefaultSession.__init__(self, *args, **kwargs) self.fragments = defaultdict(list) # type: DefaultDict[Tuple[Any, ...], List[Packet]] # noqa: E501 - def _ip_process_packet(self, packet): - # type: (Packet) -> Optional[Packet] - from scapy.layers.inet import _defrag_list, IP + def process(self, packet: Packet) -> Optional[Packet]: + from scapy.layers.inet import IP, _defrag_ip_pkt if IP not in packet: return packet - ip = packet[IP] - packet._defrag_pos = 0 - if ip.frag != 0 or ip.flags.MF: - uniq = (ip.id, ip.src, ip.dst, ip.proto) - self.fragments[uniq].append(packet) - if not ip.flags.MF: # end of frag - try: - if self.fragments[uniq][0].frag == 0: - # Has first fragment (otherwise ignore) - defrag = [] # type: List[Packet] - _defrag_list(self.fragments[uniq], defrag, []) - defragmented_packet = defrag[0] - defragmented_packet = defragmented_packet.__class__( - raw(defragmented_packet) - ) - defragmented_packet.time = packet.time - return defragmented_packet - finally: - del self.fragments[uniq] - return None - else: - return packet - - def on_packet_received(self, pkt): - # type: (Optional[Packet]) -> None - if not pkt: - return None - super(IPSession, self).on_packet_received(self._ip_process_packet(pkt)) + return _defrag_ip_pkt(packet, self.fragments)[1] # type: ignore class StringBuffer(object): @@ -174,16 +96,26 @@ def __init__(self): # type: () -> None self.content = bytearray(b"") self.content_len = 0 + self.noff = 0 # negative offset self.incomplete = [] # type: List[Tuple[int, int]] - def append(self, data, seq): - # type: (bytes, int) -> None + def append(self, data: bytes, seq: Optional[int] = None) -> None: data_len = len(data) - seq = seq - 1 + if seq is None: + seq = self.content_len + seq = seq - 1 - self.noff + if seq < 0: + # Data is located before the start of the current buffer + # (e.g. the first fragment was missing) + self.content = bytearray(b"\x00" * (-seq)) + self.content + self.content_len += (-seq) + self.noff += seq + seq = 0 if seq + data_len > self.content_len: + # Data is located after the end of the current buffer self.content += b"\x00" * (seq - self.content_len + data_len) - # If data was missing, mark it. - self.incomplete.append((self.content_len, seq)) + # As data was missing, mark it. + # self.incomplete.append((self.content_len, seq)) self.content_len = seq + data_len assert len(self.content) == self.content_len # XXX removes empty space marker. @@ -192,6 +124,10 @@ def append(self, data, seq): # self.incomplete.remove([???]) memoryview(self.content)[seq:seq + data_len] = data + def shiftleft(self, i: int) -> None: + self.content = self.content[i:] + self.content_len -= i + def full(self): # type: () -> bool # Should only be true when all missing data was filled up, @@ -254,7 +190,7 @@ def __init__(self, app=False, *args, **kwargs): super(TCPSession, self).__init__(*args, **kwargs) self.app = app if app: - self.data = b"" + self.data = StringBuffer() self.metadata = {} # type: Dict[str, Any] self.session = {} # type: Dict[str, Any] else: @@ -266,6 +202,9 @@ def __init__(self, app=False, *args, **kwargs): self.tcp_sessions = defaultdict( dict ) # type: DefaultDict[bytes, Dict[str, Any]] + # Setup stopping dissection condition + from scapy.layers.inet import TCP + self.stop_dissection_after = TCP def _get_ident(self, pkt, session=False): # type: (Packet, bool) -> bytes @@ -283,28 +222,50 @@ def xor(x, y): # Uni-directional return src + dst + struct.pack("!HH", pkt.dport, pkt.sport) - def _process_packet(self, pkt): - # type: (Packet) -> Optional[Packet] + def _strip_padding(self, pkt: Packet) -> Optional[bytes]: + """Strip the packet of any padding, and return the padding. + """ + pad = pkt.getlayer(conf.padding_layer) + if pad is not None and pad.underlayer is not None: + # strip padding + del pad.underlayer.payload + return cast(bytes, pad.load) + return None + + def process(self, pkt: Packet) -> Optional[Packet]: """Process each packet: matches the TCP seq/ack numbers to follow the TCP streams, and orders the fragments. """ + _pkt = super(TCPSession, self).process(pkt) + if pkt is None: + return None + else: # Python 3.8 := would be nice + pkt = cast(Packet, _pkt) + packet = None # type: Optional[Packet] if self.app: # Special mode: Application layer. Use on top of TCP pay_class = pkt.__class__ - if not hasattr(pay_class, "tcp_reassemble"): - # Being on top of TCP, we have no way of knowing - # when a packet ends. - return pkt - self.data += bytes(pkt) - pkt = pay_class.tcp_reassemble( - self.data, + if hasattr(pay_class, "tcp_reassemble"): + tcp_reassemble = pay_class.tcp_reassemble + else: + # There is no tcp_reassemble. Just dissect the packet + tcp_reassemble = lambda data, *_: pay_class(data) + self.data.append(bytes(pkt)) + packet = tcp_reassemble( + bytes(self.data), self.metadata, self.session ) - if pkt: - self.data = b"" - self.metadata = {} - return pkt + if packet: + padding = self._strip_padding(packet) + if padding: + # There is remaining data for the next payload. + self.data.shiftleft(len(self.data) - len(padding)) + else: + # No padding (data) left. Clear + self.data.clear() + self.metadata.clear() + return packet return None from scapy.layers.inet import IP, TCP @@ -321,13 +282,12 @@ def _process_packet(self, pkt): tcp_session = self.tcp_sessions[self._get_ident(pkt, True)] # Let's guess which class is going to be used if "pay_class" not in metadata: - pay_class = pay.__class__ + pay_class = pkt[TCP].guess_payload_class(new_data) if hasattr(pay_class, "tcp_reassemble"): tcp_reassemble = pay_class.tcp_reassemble else: - # We can't know for sure when a packet ends. - # Ignore. - return pkt + # There is no tcp_reassemble. Just dissect the packet + tcp_reassemble = lambda data, *_: pay_class(data) metadata["pay_class"] = pay_class metadata["tcp_reassemble"] = tcp_reassemble else: @@ -352,9 +312,9 @@ def _process_packet(self, pkt): metadata["tcp_psh"] = True # XXX TODO: check that no empty space is missing in the buffer. # XXX Currently, if a TCP fragment was missing, we won't notice it. - packet = None # type: Optional[Packet] if data.full(): # Reassemble using all previous packets + metadata["original"] = pkt packet = tcp_reassemble( bytes(data), metadata, @@ -364,11 +324,19 @@ def _process_packet(self, pkt): if packet: if "seq" in metadata: pkt[TCP].seq = metadata["seq"] - # Clear buffer - data.clear() # Clear TCP reassembly metadata metadata.clear() - del self.tcp_frags[ident] + # Check for padding + padding = self._strip_padding(packet) + if padding: + # There is remaining data for the next payload. + full_length = data.content_len - len(padding) + metadata["relative_seq"] = relative_seq + full_length + data.shiftleft(full_length) + else: + # No padding (data) left. Clear + data.clear() + del self.tcp_frags[ident] # Rebuild resulting packet pay.underlayer.remove_payload() if IP in pkt: @@ -379,18 +347,14 @@ def _process_packet(self, pkt): return pkt return None - def on_packet_received(self, pkt): - # type: (Optional[Packet]) -> None - """Hook to the Sessions API: entry point of the dissection. - This will defragment IP if necessary, then process to - TCP reassembly. + def recv(self, sock: 'SuperSocket') -> Iterator[Packet]: """ - if not pkt: - return None - # First, defragment IP if necessary - pkt = self._ip_process_packet(pkt) + Will be called by sniff() to ask for a packet + """ + pkt = sock.recv(stop_dissection_after=self.stop_dissection_after) if not pkt: return None # Now handle TCP reassembly - pkt = self._process_packet(pkt) - DefaultSession.on_packet_received(self, pkt) + pkt = self.process(pkt) + if pkt: + yield pkt diff --git a/scapy/supersocket.py b/scapy/supersocket.py index 8afec10b173..06f7019a78f 100644 --- a/scapy/supersocket.py +++ b/scapy/supersocket.py @@ -175,13 +175,13 @@ def recv_raw(self, x=MTU): """Returns a tuple containing (cls, pkt_data, time)""" return conf.raw_layer, self.ins.recv(x), None - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] cls, val, ts = self.recv_raw(x) if not val or not cls: return None try: - pkt = cls(val) # type: Packet + pkt = cls(val, **kwargs) # type: Packet except KeyboardInterrupt: raise except Exception: @@ -321,8 +321,8 @@ def __init__(self, msg = "Your Linux Kernel does not support Auxiliary Data!" log_runtime.info(msg) - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] data, sa_ll, ts = self._recv_raw(self.ins, x) if sa_ll[2] == socket.PACKET_OUTGOING: return None @@ -338,7 +338,7 @@ def recv(self, x=MTU): lvl = 3 try: - pkt = cls(data) + pkt = cls(data, **kwargs) except KeyboardInterrupt: raise except Exception: @@ -418,13 +418,13 @@ def __init__(self, sock, basecls=None): SimpleSocket.__init__(self, sock) self.basecls = basecls - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] data = self.ins.recv(x, socket.MSG_PEEK) x = len(data) if x == 0: return None - pkt = self.basecls(data) # type: Packet + pkt = self.basecls(data, **kwargs) # type: Packet pad = pkt.getlayer(conf.padding_layer) if pad is not None and pad.underlayer is not None: del pad.underlayer.payload @@ -445,12 +445,12 @@ def __init__(self, sock, basecls=None): super(SSLStreamSocket, self).__init__(sock, basecls) # 65535, the default value of x is the maximum length of a TLS record - def recv(self, x=65535): - # type: (int) -> Optional[Packet] + def recv(self, x=65535, **kwargs): + # type: (int, **Any) -> Optional[Packet] pkt = None # type: Optional[Packet] if self._buf != b"": try: - pkt = self.basecls(self._buf) + pkt = self.basecls(self._buf, **kwargs) except Exception: # We assume that the exception is generated by a buffer underflow # noqa: E501 pass @@ -462,7 +462,7 @@ def recv(self, x=65535): self._buf += buf x = len(self._buf) - pkt = self.basecls(self._buf) + pkt = self.basecls(self._buf, **kwargs) if pkt is not None: pad = pkt.getlayer(conf.padding_layer) @@ -511,9 +511,9 @@ def __init__(self, self.reader = PcapReader(self.tcpdump_proc.stdout) self.ins = self.reader # type: ignore - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] - return self.reader.recv(x) + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] + return self.reader.recv(x, **kwargs) def close(self): # type: () -> None @@ -562,11 +562,11 @@ def select(sockets, remain=None): # type: (List[SuperSocket], Any) -> List[SuperSocket] return sockets - def recv(self, *args): - # type: (*Any) -> Optional[Packet] + def recv(self, x=None, **kwargs): + # type: (Optional[int], Any) -> Optional[Packet] try: pkt = next(self.iter) - return pkt.__class__(bytes(pkt)) + return pkt.__class__(bytes(pkt), **kwargs) except StopIteration: raise EOFError diff --git a/scapy/tools/UTscapy.py b/scapy/tools/UTscapy.py index da6ca39a2ef..ad734a7b04b 100644 --- a/scapy/tools/UTscapy.py +++ b/scapy/tools/UTscapy.py @@ -89,9 +89,12 @@ def scapy_path(fname): class no_debug_dissector: """Context object used to disable conf.debug_dissector""" + def __init__(self, reverse=False): + self.new_value = reverse + def __enter__(self): self.old_dbg = conf.debug_dissector - conf.debug_dissector = False + conf.debug_dissector = self.new_value def __exit__(self, exc_type, exc_value, traceback): conf.debug_dissector = self.old_dbg diff --git a/scapy/utils.py b/scapy/utils.py index 6c6fd2dfa7e..211378cc9eb 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -1411,15 +1411,15 @@ def __enter__(self): # type: () -> PcapReader return self - def read_packet(self, size=MTU): - # type: (int) -> Packet + def read_packet(self, size=MTU, **kwargs): + # type: (int, **Any) -> Packet rp = super(PcapReader, self)._read_packet(size=size) if rp is None: raise EOFError s, pkt_info = rp try: - p = self.LLcls(s) # type: Packet + p = self.LLcls(s, **kwargs) # type: Packet except KeyboardInterrupt: raise except Exception: @@ -1436,9 +1436,9 @@ def read_packet(self, size=MTU): p.wirelen = pkt_info.wirelen return p - def recv(self, size=MTU): # type: ignore - # type: (int) -> Packet - return self.read_packet(size=size) + def recv(self, size=MTU, **kwargs): # type: ignore + # type: (int, **Any) -> Packet + return self.read_packet(size=size, **kwargs) def __next__(self): # type: ignore # type: () -> Packet @@ -1720,7 +1720,7 @@ def _read_block_dsb(self, block, size): # TLS Key Log if secrets_type == 0x544c534b: - if getattr(conf, "tls_nss_keys", False) is False: + if getattr(conf, "tls_sessions", False) is False: warning("PcapNg: TLS Key Log available, but " "the TLS layer is not loaded! Scapy won't be able " "to decrypt the packets.") @@ -1739,8 +1739,8 @@ def _read_block_dsb(self, block, size): else: # Note: these attributes are only available when the TLS # layer is loaded. - conf.tls_nss_keys = keys # type: ignore - conf.tls_session_enable = True # type: ignore + conf.tls_nss_keys = keys + conf.tls_session_enable = True else: warning("PcapNg: Unknown DSB secrets type (0x%x)!", secrets_type) @@ -1757,15 +1757,15 @@ def __enter__(self): # type: () -> PcapNgReader return self - def read_packet(self, size=MTU): - # type: (int) -> Packet + def read_packet(self, size=MTU, **kwargs): + # type: (int, **Any) -> Packet rp = super(PcapNgReader, self)._read_packet(size=size) if rp is None: raise EOFError s, (linktype, tsresol, tshigh, tslow, wirelen, comment) = rp try: cls = conf.l2types.num2layer[linktype] # type: Type[Packet] - p = cls(s) # type: Packet + p = cls(s, **kwargs) # type: Packet except KeyboardInterrupt: raise except Exception: @@ -1781,9 +1781,8 @@ def read_packet(self, size=MTU): p.comment = comment return p - def recv(self, size=MTU): # type: ignore - # type: (int) -> Packet - return self.read_packet() + def recv(self, size: int = MTU, **kwargs: Any) -> 'Packet': # type: ignore + return self.read_packet(size=size, **kwargs) class GenericPcapWriter(object): @@ -2383,8 +2382,8 @@ def _convert_erf_timestamp(self, t): # The details of ERF Packet format can be see here: # https://www.endace.com/erf-extensible-record-format-types.pdf - def read_packet(self, size=MTU): - # type: (int) -> Packet + def read_packet(self, size=MTU, **kwargs): + # type: (int, **Any) -> Packet # General ERF Header have exactly 16 bytes hdr = self.f.read(16) @@ -2414,7 +2413,7 @@ def read_packet(self, size=MTU): pb = s[2:size] from scapy.layers.l2 import Ether try: - p = Ether(pb) # type: Packet + p = Ether(pb, **kwargs) # type: Packet except KeyboardInterrupt: raise except Exception: diff --git a/test/configs/linux.utsc b/test/configs/linux.utsc index f43e870c136..25fbb6bd1d6 100644 --- a/test/configs/linux.utsc +++ b/test/configs/linux.utsc @@ -2,6 +2,7 @@ "testfiles": [ "test/*.uts", "test/scapy/layers/*.uts", + "test/scapy/layers/tls/*.uts", "test/contrib/*.uts", "test/tools/*.uts", "test/contrib/automotive/*.uts", @@ -10,8 +11,7 @@ "test/contrib/automotive/gm/*.uts", "test/contrib/automotive/bmw/*.uts", "test/contrib/automotive/xcp/*.uts", - "test/contrib/automotive/autosar/*.uts", - "test/tls/tests_tls_netaccess.uts" + "test/contrib/automotive/autosar/*.uts" ], "remove_testfiles": [ "test/windows.uts", @@ -21,9 +21,7 @@ "onlyfailed": true, "preexec": { "test/contrib/*.uts": "load_contrib(\"%name%\")", - "test/cert.uts": "load_layer(\"tls\")", - "test/sslv2.uts": "load_layer(\"tls\")", - "test/tls*.uts": "load_layer(\"tls\")" + "test/scapy/layers/tls/*.uts": "load_layer(\"tls\")" }, "kw_ko": [ "osx", diff --git a/test/configs/windows.utsc b/test/configs/windows.utsc index c065cb604cb..5aaab483f48 100644 --- a/test/configs/windows.utsc +++ b/test/configs/windows.utsc @@ -2,6 +2,7 @@ "testfiles": [ "test\\*.uts", "test\\scapy\\layers\\*.uts", + "test\\scapy\\layers\\tls\\*.uts", "test\\tls\\tests_tls_netaccess.uts", "test\\contrib\\automotive\\obd\\*.uts", "test\\contrib\\automotive\\scanner\\*.uts", @@ -20,9 +21,7 @@ "onlyfailed": true, "preexec": { "test\\contrib\\*.uts": "load_contrib(\"%name%\")", - "test\\cert.uts": "load_layer(\"tls\")", - "test\\sslv2.uts": "load_layer(\"tls\")", - "test\\tls*.uts": "load_layer(\"tls\")" + "test\\scapy\\layers\\tls\\*.uts": "load_layer(\"tls\")" }, "kw_ko": [ "brotli", diff --git a/test/configs/windows2.utsc b/test/configs/windows2.utsc index e82fe0f8575..c231de57f85 100644 --- a/test/configs/windows2.utsc +++ b/test/configs/windows2.utsc @@ -2,12 +2,12 @@ "testfiles": [ "*.uts", "scapy\\layers\\*.uts", - "test\\contrib\\automotive\\obd\\*.uts", - "test\\contrib\\automotive\\gm\\*.uts", - "test\\contrib\\automotive\\bmw\\*.uts", - "test\\contrib\\automotive\\*.uts", - "test\\contrib\\automotive\\autosar\\*.uts", - "tls\\tests_tls_netaccess.uts", + "scapy\\layers\\tls\\*.uts", + "contrib\\automotive\\obd\\*.uts", + "contrib\\automotive\\gm\\*.uts", + "contrib\\automotive\\bmw\\*.uts", + "contrib\\automotive\\*.uts", + "contrib\\automotive\\autosar\\*.uts", "contrib\\*.uts" ], "remove_testfiles": [ @@ -18,9 +18,7 @@ "onlyfailed": true, "preexec": { "contrib\\*.uts": "load_contrib(\"%name%\")", - "cert.uts": "load_layer(\"tls\")", - "sslv2.uts": "load_layer(\"tls\")", - "tls*.uts": "load_layer(\"tls\")" + "scapy\\layers\\tls\\*.uts": "load_layer(\"tls\")" }, "format": "html", "kw_ko": [ diff --git a/test/contrib/automotive/ecu.uts b/test/contrib/automotive/ecu.uts index ede976bc0f5..4ced520357b 100644 --- a/test/contrib/automotive/ecu.uts +++ b/test/contrib/automotive/ecu.uts @@ -607,7 +607,7 @@ assert unanswered_packets[0].diagnosticSessionType == 4 = Analyze multiple UDS messages udsmsgs = sniff(offline=scapy_path("test/pcaps/ecu_trace.pcap.gz"), - session=ISOTPSession, session_kwargs={"use_ext_address":False, "basecls":UDS}, + session=ISOTPSession(use_ext_address=False, basecls=UDS), count=50, timeout=3) assert len(udsmsgs) == 50 @@ -638,7 +638,7 @@ assert len(ecu.log["TransferData"]) == 2 session = EcuSession() with PcapReader(scapy_path("test/pcaps/ecu_trace.pcap.gz")) as sock: - udsmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "use_ext_address":False, "basecls":UDS}, count=50, opened_socket=sock, timeout=3) + udsmsgs = sniff(session=ISOTPSession(supersession=session, use_ext_address=False, basecls=UDS), count=50, opened_socket=sock, timeout=3) assert len(udsmsgs) == 50 @@ -668,12 +668,12 @@ session = EcuSession() conf.contribs['CAN']['swap-bytes'] = True with PcapReader(scapy_path("test/pcaps/gmlan_trace.pcap.gz")) as sock: - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=2, opened_socket=sock, timeout=3) + gmlanmsgs = sniff(session=ISOTPSession(supersession=session, rx_id=[0x241, 0x641, 0x101], basecls=GMLAN), count=2, opened_socket=sock, timeout=3) ecu = session.ecu print("Check 1 after change to diagnostic mode") assert len(ecu.supported_responses) == 1 assert ecu.state == EcuState(session=3) - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=8, opened_socket=sock) + gmlanmsgs = sniff(session=ISOTPSession(supersession=session, rx_id=[0x241, 0x641, 0x101], basecls=GMLAN), count=6, opened_socket=sock) ecu = session.ecu print("Check 2 after some more messages were read1") assert len(ecu.supported_responses) == 3 @@ -681,13 +681,13 @@ with PcapReader(scapy_path("test/pcaps/gmlan_trace.pcap.gz")) as sock: assert ecu.state.session == 3 print("assert 1") assert ecu.state.communication_control == 1 - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=10, opened_socket=sock) + gmlanmsgs = sniff(session=ISOTPSession(supersession=session, rx_id=[0x241, 0x641, 0x101], basecls=GMLAN), count=2, opened_socket=sock) ecu = session.ecu print("Check 3 after change to programming mode (bootloader)") assert len(ecu.supported_responses) == 4 assert ecu.state.session == 2 assert ecu.state.communication_control == 1 - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=16, opened_socket=sock) + gmlanmsgs = sniff(session=ISOTPSession(supersession=session, rx_id=[0x241, 0x641, 0x101], basecls=GMLAN), count=6, opened_socket=sock) ecu = session.ecu print("Check 4 after gaining security access") assert len(ecu.supported_responses) == 6 @@ -703,8 +703,8 @@ conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 conf.contribs['CAN']['swap-bytes'] = True conf.debug_dissector = True -gmlanmsgs = sniff(offline=scapy_path("test/pcaps/gmlan_trace.pcap.gz"), session=ISOTPSession, - session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, +gmlanmsgs = sniff(offline=scapy_path("test/pcaps/gmlan_trace.pcap.gz"), + session=ISOTPSession(supersession=session, rx_id=[0x241, 0x641, 0x101], basecls=GMLAN), count=200, timeout=6) ecu = session.ecu diff --git a/test/contrib/isotp_soft_socket.uts b/test/contrib/isotp_soft_socket.uts index de7f731b2e8..b2b7295805d 100644 --- a/test/contrib/isotp_soft_socket.uts +++ b/test/contrib/isotp_soft_socket.uts @@ -726,7 +726,7 @@ candump_fd = BytesIO(b''' vcan0 541 [8] 10 0A DE AD BE EF AA AA vcan0 241 [3] 30 00 00 vcan0 541 [5] 21 AA AA AA AA''') -pkts = sniff(opened_socket=CandumpReader(candump_fd), session=ISOTPSession, timeout=1, session_kwargs={"use_ext_address": False}) +pkts = sniff(opened_socket=CandumpReader(candump_fd), session=ISOTPSession(use_ext_address=False), timeout=1) assert len(pkts) == 6 if not len(pkts): @@ -809,13 +809,6 @@ isotp = pkts[0] assert isotp.data == dhex("") assert (isotp.rx_id == 0x241) -= ISOTPSession tests - -ses = ISOTPSession() -ses.on_packet_received(None) -ses.on_packet_received([None, None]) -assert True - = Receive a two-frame ISOTP message with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: diff --git a/test/pcaps/tls_tcp_frag_withnss.pcap.gz b/test/pcaps/tls_tcp_frag_withnss.pcap.gz new file mode 100644 index 0000000000000000000000000000000000000000..a9c19fcc770784c9103b6bac3d120c0aaae4d2fd GIT binary patch literal 5666 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 Ik~kg!02pWvEC2ui literal 0 HcmV?d00001 diff --git a/test/scapy/layers/inet.uts b/test/scapy/layers/inet.uts index e16c769e4bc..e767afcbad9 100644 --- a/test/scapy/layers/inet.uts +++ b/test/scapy/layers/inet.uts @@ -82,6 +82,16 @@ sniff(offline=tmp_file, session=IPSession, prn=callback) assert len(dissected_packets) == 1 assert raw(dissected_packets[0]) == raw(packet) += IPSession - contains non-IP packets + +pkts = fragment(IP(dst="10.0.0.5")/ICMP()/("X"*1500)) +pkts.insert(1, ARP()) +assert len(pkts) == 3 + +pkts = sniff(offline=pkts, session=IPSession) +assert len(pkts) == 2 +assert pkts[1].load == b"X" * 1500 + = StringBuffer buffer = StringBuffer() @@ -150,6 +160,25 @@ assert len(frags2) == 2 assert len(frags2[0]) == 20 + paylen - paylen % 8 assert len(frags2[1]) == 20 + 1 + paylen % 8 += fragment() with fragsize lower than 8 +paylen = 5 +fragsize = paylen +frags1 = fragment(IP() / ("X" * paylen), paylen) +assert len(frags1) == 1 +assert bytes(frags1[0].payload) == b"X" * paylen + +fragsize = paylen + 1 +frags2 = fragment(IP() / ("X" * paylen), fragsize) +assert len(frags2) == 1 +assert bytes(frags2[0].payload) == b"X" * paylen + +paylen = 16 +fragsize = 5 +frags3 = fragment(IP() / ("X" * paylen), fragsize) +assert len(frags3) == 2 +assert bytes(frags3[0].payload) == b"X" * 8 +assert bytes(frags3[1].payload) == b"X" * 8 + = defrag() nonfrag, unfrag, badfrag = defrag(frags) assert not nonfrag @@ -161,7 +190,7 @@ defrags = defragment(frags) * we should have one single packet assert len(defrags) == 1 * which should be the same as pkt reconstructed -assert defrags[0] == IP(raw(pkt)) +assert bytes(defrags[0]) == bytes(pkt) = defragment() uses timestamp of last fragment payloadlen, fragsize = 100, 8 @@ -191,10 +220,8 @@ b = base64.b64decode('bnmYJ63mREVTUwEACABFAAV0U8UgrDIR+kEEAgIECv0DxApz1F5olFRytj c = base64.b64decode('bnmYJ63mREVTUwEACABFAAFHU8UBWDIRHcMEAgIECv0DxDtlufeCT1zQktat4aEVA8MF0FO1sNbpEQtqfu5Al//OJISaRvtaArR/tLUj2CoZjS7uEnl7QpP/Ui/gR0YtyLurk9yTw7Vei0lSz4cnaOJqDiTGAKYwzVxjnoR1F3n8lplgQaOalVsHx9UAAQABAAADLAAEobkBA8epAAEAAQAAAywABKG5AQzHvwABAAEAAAMsAASnmYIMx5MAAQABAAADLAAEp5mCDcn9AAEAAQAAAqUABKeZhAvKFAABAAEAAAOEAAShuQIfyisAAQABAAADhAAEobkCKcpCAAEAAQAAA4QABKG5AjPKWQABAAEAAAOEAAShuQI9ynAAAQABAAADhAAEobkCC8nPAAEAAQAAA4QABKG5AgzJ5gABAAEAAAOEAASnmYQMAAApIAAAAAAAAAA=') d = base64.b64decode('////////REVTUwEACABFAABOawsAAIARtGoK/QExCv0D/wCJAIkAOry/3wsBEAABAAAAAAAAIEVKRkRFQkZFRUJGQUNBQ0FDQUNBQ0FDQUNBQ0FDQUFBAAAgAAEAABYP/WUAAB6N4XIAAB6E4XsAAACR/24AADyEw3sAABfu6BEAAAkx9s4AABXB6j4AAANe/KEAAAAT/+wAAB7z4QwAAEuXtGgAAB304gsAABTB6z4AAAdv+JAAACCu31EAADm+xkEAABR064sAABl85oMAACTw2w8AADrKxTUAABVk6psAABnF5joAABpA5b8AABjP5zAAAAqV9WoAAAUW+ukAACGS3m0AAAEP/vAAABoa5eUAABYP6fAAABX/6gAAABUq6tUAADXIyjcAABpy5Y0AABzb4yQAABqi5V0AAFXaqiUAAEmRtm4AACrL1TQAAESzu0wAAAzs8xMAAI7LcTQAABxN47IAAAbo+RcAABLr7RQAAB3Q4i8AAAck+NsAABbi6R0AAEdruJQAAJl+ZoEAABDH7zgAACOA3H8AAAB5/4YAABQk69sAAEo6tcUAABJU7asAADO/zEAAABGA7n8AAQ9L8LMAAD1DwrwAAB8F4PoAABbG6TkAACmC1n0AAlHErjkAABG97kIAAELBvT4AAEo0tcsAABtC5L0AAA9u8JEAACBU36sAAAAl/9oAABBO77EAAA9M8LMAAA8r8NQAAAp39YgAABB874MAAEDxvw4AAEgyt80AAGwsk9MAAB1O4rEAAAxL87QAADtmxJkAAATo+xcAAAM8/MMAABl55oYAACKh3V4AACGj3lwAAE5ssZMAAC1x0o4AAAO+/EEAABNy7I0AACYp2dYAACb+2QEAABB974IAABc36MgAAA1c8qMAAAf++AEAABDo7xcAACLq3RUAAA8L8PQAAAAV/+oAACNU3KsAABBv75AAABFI7rcAABuH5HgAABAe7+EAAB++4EEAACBl35oAAB7c4SMAADgJx/YAADeVyGoAACKN3XIAAA/C8D0AAASq+1UAAOHPHjAAABRI67cAAABw/48=') -old_debug_dissector = conf.debug_dissector -conf.debug_dissector = 0 -plist = PacketList([Ether(x) for x in [a, b, c, d]]) -conf.debug_dissector = old_debug_dissector +with no_debug_dissector(): + plist = PacketList([Ether(x) for x in [a, b, c, d]]) left, defragmented, errored = defrag(plist) assert len(left) == 1 @@ -465,6 +492,63 @@ pkt2.len = 0 pkt3 = IP(raw(pkt2)) assert pkt3.load == data += TCPSession: test tcp_reassemble with variable orders + +class CustomPacket(Packet): + fields_desc = [ + ByteField("len", 0), + StrLenField("a", 0, length_from=lambda pkt: pkt.len - 1), + ] + @classmethod + def tcp_reassemble(cls, data, metadata, session): + length = struct.unpack("!B", data[:1])[0] + if len(data) < length: + return None + return CustomPacket(data) + + +# above we have a CustomPacket that is X bytes long. +bind_layers(TCP, CustomPacket, sport=12345) + +with no_debug_dissector(reverse=True): + # incremental order + pkts = sniff(offline=[ + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=1)/b"\x05a", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=3)/"b", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=4)/"c", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=5)/"d", + ], session=TCPSession) + assert pkts[0][CustomPacket].a == b"abcd" + # same with a pcapng + tmp_file = get_temp_file() + wrpcap(tmp_file, [ + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=1)/b"\x05a", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=3)/"b", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=4)/"c", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=5)/"d", + ]) + pkts = sniff(offline=tmp_file, session=TCPSession) + assert pkts[0][CustomPacket].a == b"abcd" + # messed up order: fragments 2 and 3 arrive in the wrong order + pkts = sniff(offline=[ + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=1)/b"\x05a", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=4)/"c", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=3)/"b", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=5)/"d", + ], session=TCPSession) + assert pkts[0][CustomPacket].a == b"abcd" + # messed up order: fragment 1 arrives not in first position + pkts = sniff(offline=[ + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=6)/"e", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=4)/"c", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=3)/"b", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=5)/"d", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=1)/b"\x06a", + ], session=TCPSession) + assert pkts[0][CustomPacket].a == b"abcde" + +split_layers(TCP, CustomPacket, sport=12345) + = Layer binding @@ -500,9 +584,8 @@ value == 26908070 test.i2repr("", value) == '7:28:28.70' = IPv4 - UDP null checksum -conf.debug_dissector = False -IP(raw(IP()/UDP()/Raw(b"\xff\xff\x01\x6a")))[UDP].chksum == 0xFFFF -conf.debug_dissector = True +with no_debug_dissector(): + IP(raw(IP()/UDP()/Raw(b"\xff\xff\x01\x6a")))[UDP].chksum == 0xFFFF = IPv4 - (IP|UDP|TCP|ICMP)Error query = IP(dst="192.168.0.1", src="192.168.0.254", ttl=1)/UDP()/DNS() diff --git a/test/tls/__init__.py b/test/scapy/layers/tls/__init__.py similarity index 100% rename from test/tls/__init__.py rename to test/scapy/layers/tls/__init__.py diff --git a/test/cert.uts b/test/scapy/layers/tls/cert.uts similarity index 100% rename from test/cert.uts rename to test/scapy/layers/tls/cert.uts diff --git a/test/tls/example_client.py b/test/scapy/layers/tls/example_client.py old mode 100755 new mode 100644 similarity index 100% rename from test/tls/example_client.py rename to test/scapy/layers/tls/example_client.py diff --git a/test/tls/example_server.py b/test/scapy/layers/tls/example_server.py old mode 100755 new mode 100644 similarity index 100% rename from test/tls/example_server.py rename to test/scapy/layers/tls/example_server.py diff --git a/test/tls/pki/ca_cert.pem b/test/scapy/layers/tls/pki/ca_cert.pem similarity index 100% rename from test/tls/pki/ca_cert.pem rename to test/scapy/layers/tls/pki/ca_cert.pem diff --git a/test/tls/pki/ca_key.pem b/test/scapy/layers/tls/pki/ca_key.pem similarity index 100% rename from test/tls/pki/ca_key.pem rename to test/scapy/layers/tls/pki/ca_key.pem diff --git a/test/tls/pki/cli_cert.pem b/test/scapy/layers/tls/pki/cli_cert.pem similarity index 100% rename from test/tls/pki/cli_cert.pem rename to test/scapy/layers/tls/pki/cli_cert.pem diff --git a/test/tls/pki/cli_key.pem b/test/scapy/layers/tls/pki/cli_key.pem similarity index 100% rename from test/tls/pki/cli_key.pem rename to test/scapy/layers/tls/pki/cli_key.pem diff --git a/test/tls/pki/srv_cert.pem b/test/scapy/layers/tls/pki/srv_cert.pem similarity index 100% rename from test/tls/pki/srv_cert.pem rename to test/scapy/layers/tls/pki/srv_cert.pem diff --git a/test/tls/pki/srv_key.pem b/test/scapy/layers/tls/pki/srv_key.pem similarity index 100% rename from test/tls/pki/srv_key.pem rename to test/scapy/layers/tls/pki/srv_key.pem diff --git a/test/sslv2.uts b/test/scapy/layers/tls/sslv2.uts similarity index 99% rename from test/sslv2.uts rename to test/scapy/layers/tls/sslv2.uts index bde0a9e0d96..aeab19ba0aa 100644 --- a/test/sslv2.uts +++ b/test/scapy/layers/tls/sslv2.uts @@ -85,7 +85,7 @@ mk_enc.decryptedkey is None = Reading SSLv2 session - Importing server compromised key import os -filename = scapy_path("/test/tls/pki/srv_key.pem") +filename = scapy_path("/test/scapy/layers/tls/pki/srv_key.pem") rsa_key = PrivKeyRSA(filename) t.tls_session.server_rsa_key = rsa_key @@ -278,5 +278,5 @@ s.wcs.cipher.iv == b'\x01'*8 ############################ Automaton behaviour ############################## ############################################################################### -# see test/tls/tests_tls_netaccess.uts +# see scapy/layers/tls/clientserver.uts diff --git a/test/tls.uts b/test/scapy/layers/tls/tls.uts similarity index 96% rename from test/tls.uts rename to test/scapy/layers/tls/tls.uts index 72bca4b8084..0d51bb00c1b 100644 --- a/test/tls.uts +++ b/test/scapy/layers/tls/tls.uts @@ -1188,7 +1188,7 @@ load_layer("tls") from scapy.layers.tls.cert import PrivKeyRSA from scapy.layers.tls.record import TLSApplicationData import os -filename = scapy_path("/test/tls/pki/srv_key.pem") +filename = scapy_path("/test/scapy/layers/tls/pki/srv_key.pem") key = PrivKeyRSA(filename) ch = b'\x16\x03\x01\x005\x01\x00\x001\x03\x01X\xac\x0e\x8c\xe46\xe9\xedo\xda\x085$M\xae$\x90\xd9\xa93\xb7(\x13J\xf9\xc5?\xef\xf4\x96\xa1\xfa\x00\x00\x04\x00/\x00\xff\x01\x00\x00\x04\x00#\x00\x00' sh = b'\x16\x03\x01\x005\x02\x00\x001\x03\x01\x88\xac\xd4\xaf\x93~\xb5\x1b8c\xe7)\xa6\x9b\xa9\xed\xf3\xf3*\xdb\x00\x8bB\xf6\n\xcbz\x8eP\x83`G\x00\x00/\x00\x00\t\xff\x01\x00\x01\x00\x00#\x00\x00\x16\x03\x01\x03\xac\x0b\x00\x03\xa8\x00\x03\xa5\x00\x03\xa20\x82\x03\x9e0\x82\x02\x86\xa0\x03\x02\x01\x02\x02\t\x00\xfe\x04W\r\xc7\'\xe9\xf60\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x000T1\x0b0\t\x06\x03U\x04\x06\x13\x02MN1\x140\x12\x06\x03U\x04\x07\x0c\x0bUlaanbaatar1\x170\x15\x06\x03U\x04\x0b\x0c\x0eScapy Test PKI1\x160\x14\x06\x03U\x04\x03\x0c\rScapy Test CA0\x1e\x17\r160916102811Z\x17\r260915102811Z0X1\x0b0\t\x06\x03U\x04\x06\x13\x02MN1\x140\x12\x06\x03U\x04\x07\x0c\x0bUlaanbaatar1\x170\x15\x06\x03U\x04\x0b\x0c\x0eScapy Test PKI1\x1a0\x18\x06\x03U\x04\x03\x0c\x11Scapy Test Server0\x82\x01"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x01\x0f\x000\x82\x01\n\x02\x82\x01\x01\x00\xcc\xf1\xf1\x9b`-`\xae\xf2\x98\r\')\xd9\xc0\tYL\x0fJ0\xa8R\xdf\xe5\xb1!\x9fO\xc3=V\x93\xdd_\xc6\xf7\xb3\xf6U\x8b\xe7\x92\xe2\xde\xf2\x85I\xb4\xa1,\xf4\xfdv\xa8g\xca\x04 `\x11\x18\xa6\xf2\xa9\xb6\xa6\x1d\xd9\xaa\xe5\xd9\xdb\xaf\xe6\xafUW\x9f\xffR\x89e\xe6\x80b\x80!\x94\xbc\xcf\x81\x1b\xcbg\xc2\x9d\xb5\x05w\x04\xa6\xc7\x88\x18\x80xh\x956\xde\x97\x1b\xb6a\x87B\x1au\x98E\x82\xeb>2\x11\xc8\x9b\x86B9\x8dM\x12\xb7X\x1b\x19\xf3\x9d+\xa1\x98\x82\xca\xd7;$\xfb\t9\xb0\xbc\xc2\x95\xcf\x82)u\x16)?B \x17+M@\x8cVl\xad\xba\x0f4\x85\xb1\x7f@yqx\xb7\xa5\x04\xbb\x94\xf7\xb5A\x95\xee|\xeb\x8d\x0cyhY\xef\xcb\xb3\xfa>x\x1e\xeegLz\xdd\xe0\x99\xef\xda\xe7\xef\xb2\t]\xbe\x80 !\x05\x83,D\xdb]*v)\xa5\xb0#\x88t\x07T"\xd6)z\x92\xf5o-\x9e\xe7\xf8&+\x9cXe\x02\x03\x01\x00\x01\xa3o0m0\t\x06\x03U\x1d\x13\x04\x020\x000\x0b\x06\x03U\x1d\x0f\x04\x04\x03\x02\x05\xe00\x1d\x06\x03U\x1d\x0e\x04\x16\x04\x14\xa1+ p\xd2k\x80\xe5e\xbc\xeb\x03\x0f\x88\x9ft\xad\xdd\xf6\x130\x1f\x06\x03U\x1d#\x04\x180\x16\x80\x14fS\x94\xf4\x15\xd1\xbdgh\xb0Q725\xe1\xa4\xaa\xde\x07|0\x13\x06\x03U\x1d%\x04\x0c0\n\x06\x08+\x06\x01\x05\x05\x07\x03\x010\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x00\x03\x82\x01\x01\x00\x81\x88\x92sk\x93\xe7\x95\xd6\xddA\xee\x8e\x1e\xbd\xa3HX\xa7A5?{}\xd07\x98\x0e\xb8,\x94w\xc8Q6@\xadY\t(\xc8V\xd6\xea[\xac\xb4\xd8?h\xb7f\xca\xe1V7\xa9\x00e\xeaQ\xc9\xec\xb2iI]\xf9\xe3\xc0\xedaT\xc9\x12\x9f\xc6\xb0\nsU\xe8U5`\xef\x1c6\xf0\xda\xd1\x90wV\x04\xb8\xab8\xee\xf7\t\xc5\xa5\x98\x90#\xea\x1f\xdb\x15\x7f2(\x81\xab\x9b\x85\x02K\x95\xe77Q{\x1bH.\xfb>R\xa3\r\xb4F\xa9\x92:\x1c\x1f\xd7\n\x1eXJ\xfa.Q\x8f)\xc6\x1e\xb8\x0e1\x0es\xf1\'\x88\x17\xca\xc8i\x0c\xfa\x83\xcd\xb3y\x0e\x14\xb0\xb8\x9b/:-\t\xe3\xfc\x06\xf0:n\xfd6;+\x1a\t*\xe8\xab_\x8c@\xe4\x81\xb2\xbc\xf7\x83g\x11nN\x93\xea"\xaf\xff\xa3\x9awWv\xd0\x0b8\xac\xf8\x8a\x945\x8e\xd7\xd4a\xcc\x01\xff$\xb4\x8fa#\xba\x88\xd7Y\xe4\xe9\xba*N\xb5\x15\x0f\x9c\xd0\xea\x06\x91\xd9\xde\xab\x16\x03\x01\x00\x04\x0e\x00\x00\x00' @@ -1206,13 +1206,18 @@ assert isinstance(t.msg[0], TLSApplicationData) assert t.msg[0].data == b"" t.getlayer(TLS, 2).msg[0].data == b"To boldly go where no man has gone before...\n" -= Auto provide the session += Auto-provide the session: use TCPSession with conf.tls_session_enable conf.debug_dissector = 2 + +conf.tls_session_enable = True +conf.tls_sessions.server_rsa_key = key + client = "192.168.0.1" server = "1.2.3.4" -bc = Ether()/IP(src=client, dst=server)/TCP(sport=51478, dport=443, seq=1) -bs = Ether()/IP(src=server, dst=client)/TCP(sport=443, dport=51478, seq=1) +bc = Ether()/IP(src=client, dst=server)/TCP(sport=51478, dport=443, seq=RandShort()) +bs = Ether()/IP(src=server, dst=client)/TCP(sport=443, dport=51478, seq=RandShort()) + pcap = [ bc/ch, bs/sh, @@ -1220,11 +1225,12 @@ pcap = [ bs/fin, bc/data ] -res = sniff(offline=pcap, session=TLSSession(server_rsa_key=key)) +res = sniff(offline=pcap, session=TCPSession) res[4].show() assert res[4].getlayer(TLS, 2).msg[0].data == b"To boldly go where no man has gone before...\n" +conf.tls_session_enable = False ############################################################################### ############################## Building packets ############################### @@ -1356,7 +1362,7 @@ test_tls_without_cryptography() with no_debug_dissector(): pkt = Ether(hex_bytes('00155dfb587a00155dfb58430800450005dc54d3400070065564400410d40a00000d01bb044e8b86744e16063ac45010faf06ba9000016030317c30200005503035cb336a067d53a5d2cedbdfec666ac740afbd0637ddd13eddeab768c3c63abee20981a0000d245f1c905b329323ad67127cd4b907a49f775c331d0794149aca7cdc02800000d0005000000170000ff010001000b000ec6000ec300090530820901308206e9a00302010202132000036e72aded906765595fae000000036e72300d06092a864886f70d01010b050030818b310b30090603550406130255533113')) - assert TLSServerHello in pkt + assert conf.padding_layer in pkt ############################################################################### ########################### TLS Misc tests #################################### @@ -1484,7 +1490,7 @@ assert raw(p) == a with no_debug_dissector(): p = Ether(b'RU\x10\x00\x02\x02RT\x00\x124V\x08\x00E\x00\x05\xc8\r\xd8\x00\x00@\x06\x96\x9d\x9c&\xce\x12\xc0\xa8\xa5\xd9\x01\xbb\xc0\x1f\x00w$\x02\x03\xbe\xc5#P\x10#(\x0b\x9e\x00\x00\x16\x03\x03\x0e4\x02\x00\x00M\x03\x03^\xfa\xb5~\x88\xdf\xdc#}\'\xa0\xff\xa2\xe2\xb5\xec\x0e\x93\xa8\xe0\xde\x01[\x13[F\x151 x\xc6\xcc `)\x00\x00\x8aZ\x90l\xda\x0b\xe1\xec[i\x13\xa7\x8e\xb9a\x98"\x8a7L\x9d\x90\xe0\x01\x06c$9\xc0\'\x00\x00\x05\xff\x01\x00\x01\x00\x0b\x00\x0c\x8e\x00\x0c\x8b\x00\x06n0\x82\x06j0\x82\x05R\xa0\x03\x02\x01\x02\x02\x10EY\xe8\x1c\x1e\x9a\xe0?X\xaa\xc3\xbc\xcd`jh0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x000\x81\x8f1\x0b0\t\x06\x03U\x04\x06\x13\x02GB1\x1b0\x19\x06\x03U\x04\x08\x13\x12Greater Manchester1\x100\x0e\x06\x03U\x04\x07\x13\x07Salford1\x180\x16\x06\x03U\x04\n\x13\x0fSectigo Limited1705\x06\x03U\x04\x03\x13.Sectigo RSA Domain Validation Secure Server CA0\x1e\x17\r190309000000Z\x17\r210308235959Z0W1!0\x1f\x06\x03U\x04\x0b\x13\x18Domain Control Validated1\x1d0\x1b\x06\x03U\x04\x0b\x13\x14PositiveSSL Wildcard1\x130\x11\x06\x03U\x04\x03\x0c\n*.mql5.net0\x82\x01"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x01\x0f\x000\x82\x01\n\x02\x82\x01\x01\x00\xcb\xbcn=\xbaGd\xe1XB\x07\xc9\xb1\xc8/\x86\xaa4Z\xbdNk\xfb\xffR\x8f\xe4\x1c^\x91m8\xb9^\x97\xa5\xd3N\xfb\x80\x92\x8ap\xda\x15\x9f\xee\xe7\xb3\xc8?\xb0>~\xaa\x07\x91\xb1\x99q\xe2\xe5\xc8\x9b\x1d5\xa0\x96,\x98\xdaW\x93\x95\x8e%\xe8\xd4L\xeb\xcbSg\x15"\xba\xb7\xc7\x1f\xe9\xd6\x1a\xe6E\x1d\xc8\x1e%\xd36\xe0/r\xd1\xce1C\xce\x91&\xa1\x08*R\xbf\x8cu\xb0\xda\x0e\x1e2\xd66\x1df&3\x9b\x03\x0b\xcam:\xf7\x12\xd9ud(\xae\xdc\xbci\x85\xbd\xcf\xeb{\x15:\xbd\x0e\x11\x1bi\xd8\xff]y~E\x15\x95\xee\xe9\xea\xc6Cr~\xaa\x07\x91\xb1\x99q\xe2\xe5\xc8\x9b\x1d5\xa0\x96,\x98\xdaW\x93\x95\x8e%\xe8\xd4L\xeb\xcbSg\x15"\xba\xb7\xc7\x1f\xe9\xd6\x1a\xe6E\x1d\xc8\x1e%\xd36\xe0/r\xd1\xce1C\xce\x91&\xa1\x08*R\xbf\x8cu\xb0\xda\x0e\x1e2\xd66\x1df&3\x9b\x03\x0b\xcam:\xf7\x12\xd9ud(\xae\xdc\xbci\x85\xbd\xcf\xeb{\x15:\xbd\x0e\x11\x1bi\xd8\xff]y~E\x15\x95\xee\xe9\xea\xc6Cr~\xaa\x07\x91\xb1\x99q\xe2\xe5\xc8\x9b\x1d5\xa0\x96,\x98\xdaW\x93\x95\x8e%\xe8\xd4L\xeb\xcbSg\x15"\xba\xb7\xc7\x1f\xe9\xd6\x1a\xe6E\x1d\xc8\x1e%\xd36\xe0/r\xd1\xce1C\xce\x91&\xa1\x08*R\xbf\x8cu\xb0\xda\x0e\x1e2\xd66\x1df&3\x9b\x03\x0b\xcam:\xf7\x12\xd9ud(\xae\xdc\xbci\x85\xbd\xcf\xeb{\x15:\xbd\x0e\x11\x1bi\xd8\xff]y~E\x15\x95\xee\xe9\xea\xc6Cr.Q2MzGY[k@" in packets[13].msg[0].data conf = bck_conf + += pcap file & external TLS Key Log file with TCPSession (without extms) +* GH3722 + +# Write SSLKEYLOGFILE +temp_sslkeylog = get_temp_file() +with open(temp_sslkeylog, "w") as fd: + fd.write("CLIENT_RANDOM 09F91DA01B1FEB50B691C932959111E5E1D676437F7A42DE47EA881F6295D4E7 EE119869B732F0F9561FFDD95E50A2ACBF268EE0C7C33B409E68C1972E0B280944F7345E845E82F909CCFEB61C456E1F\n") + +bck_conf = conf +conf.tls_session_enable = True +conf.tls_nss_filename = temp_sslkeylog + +packets = sniff(offline=scapy_path("test/pcaps/tls_tcp_frag_withnss.pcap.gz"), session=TCPSession) +packets.show() + +assert packets[8].getlayer(TLS, 3).msg[0].msgtype == 20 +assert packets[8].getlayer(TLS, 3).msg[0].vdata == b'\n\xd4`\xf0\xd9X\x02\x10Z\x81\xf4l' +assert packets[10].getlayer(TLS, 3).msg[0].msgtype == 20 +assert packets[10].getlayer(TLS, 3).msg[0].vdata == b'\xa6>f\xd8\xacf\x99| \xbd<\xa1' +assert packets[11].msg[0].data == b'GET /uuid HTTP/1.1\r\nUser-Agent: Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.22000.832\r\nHost: httpbin.org\r\nConnection: Keep-Alive\r\n\r\n' +assert packets[13].msg[0].data == b'HTTP/1.1 200 OK\r\nDate: Sat, 20 Aug 2022 22:32:24 GMT\r\nContent-Type: application/json\r\nContent-Length: 53\r\nConnection: keep-alive\r\nServer: gunicorn/19.9.0\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Credentials: true\r\n\r\n{\n "uuid": "5bad226d-504a-4416-a11a-8a5f8edbdbbd"\n}\n' + +# Test summary() +assert packets[6].summary() == 'Ether / IP / TCP / TLS 52.87.105.151:443 > 10.211.55.3:51933 / TLS / TLS Handshake - Certificate / TLS / TLS Handshake - Server Key Exchange / TLS / TLS Handshake - Server Hello Done' +assert packets[8].summary() == 'Ether / IP / TCP / TLS 10.211.55.3:51933 > 52.87.105.151:443 / TLS / TLS Handshake - Client Key Exchange / TLS / TLS ChangeCipherSpec / TLS / TLS Handshake - Finished' +conf = bck_conf diff --git a/test/tls13.uts b/test/scapy/layers/tls/tls13.uts similarity index 99% rename from test/tls13.uts rename to test/scapy/layers/tls/tls13.uts index c046048ed1c..12d58560874 100644 --- a/test/tls13.uts +++ b/test/scapy/layers/tls/tls13.uts @@ -1214,6 +1214,8 @@ assert len(ch.client_shares[0].key_exchange) == 97 = Parse TLS 1.3 Client Hello with non-rfc 5077 ticket +from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_PreSharedKey_CH + ch = TLS(b'\x16\x03\x01\x01\x1a\x01\x00\x01\x16\x03\x03\xec\x9c>\xb2\x9e|B\x05\x17f\x86\xc8\x18\x0421\x87\x87\x12\xf6\xec\xa2J\x95\x84[\xf8\xab\xe9gK> \xc6%\xff&wn)\xb2\xf5\xe8_x\x96\xe9\nEsK\xda\x86o\x82f\xa5\xbadk\xf4Ar~}\x00\x08\x13\x02\x13\x03\x13\x01\x00\xff\x01\x00\x00\xc5\x00\x0b\x00\x04\x03\x00\x01\x02\x00\n\x00\x16\x00\x14\x00\x1d\x00\x17\x00\x1e\x00\x19\x00\x18\x01\x00\x01\x01\x01\x02\x01\x03\x01\x04\x00#\x00\x00\x00\x16\x00\x00\x00\x17\x00\x00\x00\r\x00\x1e\x00\x1c\x04\x03\x05\x03\x06\x03\x08\x07\x08\x08\x08\t\x08\n\x08\x0b\x08\x04\x08\x05\x08\x06\x04\x01\x05\x01\x06\x01\x00+\x00\x03\x02\x03\x04\x00-\x00\x02\x01\x01\x003\x00&\x00$\x00\x1d\x00 l\x19\xe1f1 )6\xbf\x91\x9e\xab\xd2\x06\x16\x0b|\x88\xf7,\xf1\x88\x99Z\xb6\xb3\x93\xe4\x08z\x8a\t\x00)\x00:\x00\x15\x00\x0fClient_identity\x00\x00\x00\x00\x00! m\xf3^\xc1l\xac5\xf2\xe3=\xeb\xe3\x81\xd3\xb3\xdd\xbd\xbd\x01\xc9\xdd\x01i\x8c1\xa0ye\xcd\x04\x9e\x9c') assert isinstance(ch.msg[0].ext[9], TLS_Ext_PreSharedKey_CH) diff --git a/test/tls/tests_tls_netaccess.uts b/test/scapy/layers/tls/tlsclientserver.uts similarity index 95% rename from test/tls/tests_tls_netaccess.uts rename to test/scapy/layers/tls/tlsclientserver.uts index 6b6324a8396..f0a86c0e3c5 100644 --- a/test/tls/tests_tls_netaccess.uts +++ b/test/scapy/layers/tls/tlsclientserver.uts @@ -67,8 +67,8 @@ def run_tls_test_server(expected_data, q, curve=None, cookie=False, client_auth= print("Server started !") with captured_output() as (out, err): # Prepare automaton - mycert = scapy_path("/test/tls/pki/srv_cert.pem") - mykey = scapy_path("/test/tls/pki/srv_key.pem") + mycert = scapy_path("/test/scapy/layers/tls/pki/srv_cert.pem") + mykey = scapy_path("/test/scapy/layers/tls/pki/srv_key.pem") print(mykey) print(mycert) assert os.path.exists(mycert) @@ -97,9 +97,9 @@ def run_tls_test_server(expected_data, q, curve=None, cookie=False, client_auth= def run_openssl_client(msg, suite="", version="", tls13=False, client_auth=False, psk=None, sess_out=None): # Run client - CA_f = scapy_path("/test/tls/pki/ca_cert.pem") - mycert = scapy_path("/test/tls/pki/cli_cert.pem") - mykey = scapy_path("/test/tls/pki/cli_key.pem") + CA_f = scapy_path("/test/scapy/layers/tls/pki/ca_cert.pem") + mycert = scapy_path("/test/scapy/layers/tls/pki/cli_cert.pem") + mykey = scapy_path("/test/scapy/layers/tls/pki/cli_key.pem") args = [ "openssl", "s_client", "-connect", "127.0.0.1:4433", "-debug", @@ -215,8 +215,8 @@ def run_tls_test_client(send_data=None, cipher_suite_code=None, version=None, client_auth=False, key_update=False, stop_server=True, session_ticket_file_out=None, session_ticket_file_in=None): print("Loading client...") - mycert = scapy_path("/test/tls/pki/cli_cert.pem") if client_auth else None - mykey = scapy_path("/test/tls/pki/cli_key.pem") if client_auth else None + mycert = scapy_path("/test/scapy/layers/tls/pki/cli_cert.pem") if client_auth else None + mykey = scapy_path("/test/scapy/layers/tls/pki/cli_key.pem") if client_auth else None commands = [send_data] if key_update: commands.append(b"key_update") diff --git a/test/testsocket.py b/test/testsocket.py index 8017bfef98f..9709a99a350 100644 --- a/test/testsocket.py +++ b/test/testsocket.py @@ -136,8 +136,8 @@ def send(self, x): self.no_error_for_x_tx_pkts -= 1 return super(UnstableSocket, self).send(x) - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] if self.no_error_for_x_tx_pkts == 0: if random.randint(0, 1000) == 42: self.no_error_for_x_tx_pkts = 10 @@ -153,7 +153,7 @@ def recv(self, x=MTU): return None if self.no_error_for_x_tx_pkts > 0: self.no_error_for_x_tx_pkts -= 1 - return super(UnstableSocket, self).recv(x) + return super(UnstableSocket, self).recv(x, **kwargs) def cleanup_testsockets(): From 0d61686e963ba074a90e42e90b91338235e1be0a Mon Sep 17 00:00:00 2001 From: superuserx Date: Tue, 19 Sep 2023 20:08:03 +0200 Subject: [PATCH 063/122] Fix doipsocket6 (#4076) * added buffer variable in DoIPSocket6 * added test for DoIPSocket6 * fix doip.uts * fix fix doip.uts --- scapy/contrib/automotive/doip.py | 1 + test/contrib/automotive/doip.uts | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/scapy/contrib/automotive/doip.py b/scapy/contrib/automotive/doip.py index 096bdb9e586..675840a095c 100644 --- a/scapy/contrib/automotive/doip.py +++ b/scapy/contrib/automotive/doip.py @@ -376,6 +376,7 @@ def __init__(self, ip='::1', port=13400, activate_routing=True, self.ip = ip self.port = port self.source_address = source_address + self.buffer = b"" super(DoIPSocket6, self)._init_socket(socket.AF_INET6) if activate_routing: diff --git a/test/contrib/automotive/doip.uts b/test/contrib/automotive/doip.uts index 5d8101651e8..19f44cbd3ba 100644 --- a/test/contrib/automotive/doip.uts +++ b/test/contrib/automotive/doip.uts @@ -407,3 +407,30 @@ sock = DoIPSocket(activate_routing=False) pkts = sock.sniff(timeout=1, count=2) assert len(pkts) == 2 + += Test DoIPSocket6 + +server_up = threading.Event() +def server(): + buffer = b'\x02\xfd\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x02\xfd\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('::1', 13400)) + sock.listen(1) + server_up.set() + connection, address = sock.accept() + connection.send(buffer) + connection.close() + finally: + sock.close() + + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +sock = DoIPSocket6(activate_routing=False) + +pkts = sock.sniff(timeout=1, count=2) +assert len(pkts) == 2 From c5024ddef83ffdcd14ddeb4ece900541a4d05cba Mon Sep 17 00:00:00 2001 From: Paul Gear Date: Tue, 15 Aug 2023 21:53:25 +1000 Subject: [PATCH 064/122] Fix NTP poll and precision data types These fields are 8-bit signed integers, per https://datatracker.ietf.org/doc/html/rfc5905#page-21 (bottom of the page). --- scapy/layers/ntp.py | 6 +++--- test/scapy/layers/ntp.uts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scapy/layers/ntp.py b/scapy/layers/ntp.py index 9b0d4e4ae6b..57d155e9af9 100644 --- a/scapy/layers/ntp.py +++ b/scapy/layers/ntp.py @@ -448,8 +448,8 @@ class NTPHeader(NTP): BitField("version", 4, 3), BitEnumField("mode", 3, 3, _ntp_modes), BitField("stratum", 2, 8), - BitField("poll", 0xa, 8), - BitField("precision", 0, 8), + SignedByteField("poll", 0xa), + SignedByteField("precision", 0), FixedPointField("delay", 0, size=32, frac_bits=16), FixedPointField("dispersion", 0, size=32, frac_bits=16), ConditionalField(IPField("id", "127.0.0.1"), lambda p: p.stratum > 1), @@ -1120,7 +1120,7 @@ class NTPInfoSys(Packet): ByteField("peer_mode", 0), ByteField("leap", 0), ByteField("stratum", 0), - ByteField("precision", 0), + SignedByteField("precision", 0), FixedPointField("rootdelay", 0, size=32, frac_bits=16), FixedPointField("rootdispersion", 0, size=32, frac_bits=16), IPField("refid", 0), diff --git a/test/scapy/layers/ntp.uts b/test/scapy/layers/ntp.uts index eddac823816..6ae79063f11 100644 --- a/test/scapy/layers/ntp.uts +++ b/test/scapy/layers/ntp.uts @@ -582,7 +582,7 @@ assert p.data[0].peer == "127.127.1.0" assert p.data[0].peer_mode == 3 assert p.data[0].leap == 0 assert p.data[0].stratum == 11 -assert p.data[0].precision == 240 +assert p.data[0].precision == -16 assert p.data[0].refid == "127.127.1.0" @@ -1113,7 +1113,7 @@ assert p.data[0].ifname.startswith(b"lo") from decimal import Decimal -precision = b"\xec" # 236 +precision = b"\xec" # -20 dispersion = b"\x00\x00\xf2\xce" # 0.948455810546875 time_stamp = b"\xe6}gt\x00\x00\x00\x00" # Sat, 16 Jul 2022 16:36:04 +0000 @@ -1142,7 +1142,7 @@ assert pkt_1.recv.val == time_stamp, pkt_1.recv.val time_stamp_hex = 0x00000000e67d6774 pkt_2 = NTP( - precision=236, + precision=-20, dispersion=Decimal('0.948455810546875'), orig=time_stamp_hex, sent=time_stamp_hex, From 4b386fffbbdfcc46f41433e7f96ca00488ac30c6 Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Thu, 21 Sep 2023 18:04:11 +0300 Subject: [PATCH 065/122] SSLv2: s/debug_dissect/debug_dissector/ (#4129) to prevent do_dissect_payload from failing with ``` File "scapy/scapy/layers/tls/record_sslv2.py", line 177, in do_dissect_payload if conf.debug_dissect: ^^^^^^^^^^^^^^^^^^ File "scapy/scapy/config.py", line 950, in __getattribute__ return object.__getattribute__(self, attr) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AttributeError: 'Conf' object has no attribute 'debug_dissect' ``` The test is added to exercise the Exception clause. --- scapy/layers/tls/record_sslv2.py | 2 +- test/scapy/layers/tls/sslv2.uts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/scapy/layers/tls/record_sslv2.py b/scapy/layers/tls/record_sslv2.py index 8d311faaadc..8e99a3522b2 100644 --- a/scapy/layers/tls/record_sslv2.py +++ b/scapy/layers/tls/record_sslv2.py @@ -174,7 +174,7 @@ def do_dissect_payload(self, s): except KeyboardInterrupt: raise except Exception: - if conf.debug_dissect: + if conf.debug_dissector: raise p = conf.raw_layer(s, _internal=1, _underlayer=self) self.add_payload(p) diff --git a/test/scapy/layers/tls/sslv2.uts b/test/scapy/layers/tls/sslv2.uts index aeab19ba0aa..c523e8153ec 100644 --- a/test/scapy/layers/tls/sslv2.uts +++ b/test/scapy/layers/tls/sslv2.uts @@ -273,6 +273,11 @@ assert isinstance(s.wcs.ciphersuite, SSL_CK_DES_192_EDE3_CBC_WITH_MD5) s.rcs.cipher.iv == b'\x01'*8 s.wcs.cipher.iv == b'\x01'*8 += Dissect invalid payload +p = SSLv2() +with no_debug_dissector(): + p.do_dissect_payload(b'\x00') + assert raw(p.payload) == b'\x00' ############################################################################### ############################ Automaton behaviour ############################## From 112aa9bb3a86665437a34675ef022155dd3c93f5 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 25 Sep 2023 15:32:04 +0200 Subject: [PATCH 066/122] fix(http2): HPackHdrTable().parse_txt_hdrs() accepts str & bytes (#4131) * fix(http2): HPackHdrTable().parse_txt_hdrs() accepts str & bytes * tests(http2): test HPackHdrTable().parse_txt_hdrs() w/ both str & bytes --- scapy/contrib/http2.py | 4 +- test/contrib/http2.uts | 97 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/scapy/contrib/http2.py b/scapy/contrib/http2.py index 08edfea3e32..6c4706608b1 100644 --- a/scapy/contrib/http2.py +++ b/scapy/contrib/http2.py @@ -2618,7 +2618,7 @@ def _parse_header_line(self, line): return plain_str(hdr_name.lower()), plain_str(grp.group(3)) def parse_txt_hdrs(self, - s, # type: str + s, # type: Union[bytes, str] stream_id=1, # type: int body=None, # type: Optional[str] max_frm_sz=4096, # type: int @@ -2664,7 +2664,7 @@ def parse_txt_hdrs(self, :raises: Exception """ - sio = BytesIO(s) + sio = BytesIO(s.encode() if isinstance(s, str) else s) base_frm_len = len(raw(H2Frame())) diff --git a/test/contrib/http2.uts b/test/contrib/http2.uts index 26ac760bcff..11cbcb827b5 100644 --- a/test/contrib/http2.uts +++ b/test/contrib/http2.uts @@ -1693,7 +1693,7 @@ user-agent: Mozilla/5.0 Generated by hand x-generated-by: Me x-generation-date: 2016-08-11 x-generation-software: scapy -'''.format(len(body)).encode() +'''.format(len(body)) h = h2.HPackHdrTable() h.register(h2.HPackLitHdrFldWithIncrIndexing( @@ -1789,6 +1789,101 @@ assert isinstance(p.payload, h2.H2DataFrame) pay = p[h2.H2DataFrame] assert pay.data == body +# now with bytes +h = h2.HPackHdrTable() +h.register(h2.HPackLitHdrFldWithIncrIndexing( + hdr_name=h2.HPackHdrString(data=h2.HPackZString('X-Generation-Date')), + hdr_value=h2.HPackHdrString(data=h2.HPackLiteralString('2016-08-11')) +)) +seq = h.parse_txt_hdrs( + hdrs.encode(), + stream_id=1, + body=body, + should_index=lambda name: name in ['user-agent', 'x-generation-software'], + is_sensitive=lambda name, value: name in ['x-generated-by', ':path'] +) +assert isinstance(seq, h2.H2Seq) +assert len(seq.frames) == 2 +p = seq.frames[0] +assert isinstance(p, h2.H2Frame) +assert p.type == 1 +assert len(p.flags) == 1 +assert 'EH' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2HeadersFrame) +hdrs_frm = p[h2.H2HeadersFrame] +assert len(p.hdrs) == 9 +hdr = p.hdrs[0] +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 3 +hdr = p.hdrs[1] +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 1 +assert hdr.index in [4, 5] +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(/login.php)' +hdr = p.hdrs[2] +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 7 +hdr = p.hdrs[3] +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 31 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)' +hdr = p.hdrs[4] +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 28 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(22)' +hdr = p.hdrs[5] +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 58 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)' +hdr = p.hdrs[6] +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 1 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generated-by)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(Me)' +hdr = p.hdrs[7] +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 63 +hdr = p.hdrs[8] +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generation-software)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(scapy)' + +p = seq.frames[1] +assert isinstance(p, h2.H2Frame) +assert p.type == 0 +assert len(p.flags) == 1 +assert 'ES' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2DataFrame) +pay = p[h2.H2DataFrame] +assert pay.data == body + = HTTP/2 HPackHdrTable : Parsing Textual Representation without body ~ http2 hpack hpackhdrtable helpers From 341b285cea0f17ed7e52a01b5cc59bd62d5e6ec9 Mon Sep 17 00:00:00 2001 From: Justin Breed <14262936+jbreed@users.noreply.github.com> Date: Tue, 26 Sep 2023 15:21:00 -0600 Subject: [PATCH 067/122] Resolve Issue_4006 Sendpfast accepts a float for the PPS value as well as the underlying tcpreplay; however, this is converted to an integer in the tcpreplay string causing flows needing refined throughput values to be inaccurate. This is a very simple fix by changing it from an integer conversion to a float. I have been doing this manually using sed in docker build to patch this, but should have submitted this when originally found. --- scapy/sendrecv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py index a994d0399b3..5c7a5a2b3c5 100644 --- a/scapy/sendrecv.py +++ b/scapy/sendrecv.py @@ -515,7 +515,7 @@ def sendpfast(x, # type: _PacketIterable iface = conf.iface argv = [conf.prog.tcpreplay, "--intf1=%s" % network_name(iface)] if pps is not None: - argv.append("--pps=%i" % pps) + argv.append("--pps=%f" % pps) elif mbps is not None: argv.append("--mbps=%f" % mbps) elif realtime is not None: From 148ab7b41f951bdd424ee8c61b8fd97eea761677 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 6 Oct 2023 16:15:10 +0200 Subject: [PATCH 068/122] Fix codespell (#4137) * fix codespell * revert change * Revert "revert change" This reverts commit a36d24a0f2dc058d6d61676094c6db746434a721. * revert changes * add exception to codespell * add exception to codespell --- .config/codespell_ignore.txt | 1 + doc/scapy/layers/http.rst | 2 +- scapy/contrib/ife.py | 2 +- scapy/contrib/ppi_geotag.py | 2 +- scapy/contrib/socks.py | 4 ++-- scapy/layers/bluetooth.py | 2 +- scapy/layers/gssapi.py | 2 +- scapy/libs/winpcapy.py | 2 +- scapy/modules/krack/__init__.py | 2 +- scapy/modules/krack/automaton.py | 2 +- test/contrib/automotive/ccp.uts | 4 ++-- test/contrib/eigrp.uts | 2 +- test/scapy/layers/tls/tlsclientserver.uts | 2 +- 13 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt index b2c1a5b5a27..6405c1d1111 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -39,3 +39,4 @@ vas wan wanna webp +widgits diff --git a/doc/scapy/layers/http.rst b/doc/scapy/layers/http.rst index 9912f80aa68..aef497386cd 100644 --- a/doc/scapy/layers/http.rst +++ b/doc/scapy/layers/http.rst @@ -23,7 +23,7 @@ To summarize, the frames can be split in 3 different ways: - using ``Content-Length``: the header of the HTTP frame announces the total length of the frame - None of the above: the HTTP frame ends when the TCP stream ends / when a TCP push happens. -Moreover, each frame may be aditionnally compressed, depending on the algorithm specified in the HTTP header: +Moreover, each frame may be additionally compressed, depending on the algorithm specified in the HTTP header: - ``compress``: compressed using *LZW* - ``deflate``: compressed using *ZLIB* diff --git a/scapy/contrib/ife.py b/scapy/contrib/ife.py index b71368b885f..ac93f2aebb9 100644 --- a/scapy/contrib/ife.py +++ b/scapy/contrib/ife.py @@ -59,7 +59,7 @@ class IFETlv(Packet): """ - Parent Class interhit by all ForCES TLV strucutures + Parent Class interhit by all ForCES TLV structures """ name = "IFETlv" diff --git a/scapy/contrib/ppi_geotag.py b/scapy/contrib/ppi_geotag.py index ecb8c226de8..80e47f78769 100644 --- a/scapy/contrib/ppi_geotag.py +++ b/scapy/contrib/ppi_geotag.py @@ -278,7 +278,7 @@ def _FlagsList(myfields): 8: "GPS Derived", 9: "INS Derived", 10: "Compass Derived", - 11: "Acclerometer Derived", + 11: "Accelerometer Derived", 12: "Human Derived", }) diff --git a/scapy/contrib/socks.py b/scapy/contrib/socks.py index 1aadb5cf2f8..983e23f47f4 100644 --- a/scapy/contrib/socks.py +++ b/scapy/contrib/socks.py @@ -110,7 +110,7 @@ class SOCKS4Reply(Packet): overload_fields = {SOCKS: {"vn": 0x0}} fields_desc = [ ByteEnumField("cd", 90, _socks4_cd_reply), - ] + SOCKS4Request.fields_desc[1:-2] # Re-use dstport, dst and userid + ] + SOCKS4Request.fields_desc[1:-2] # Reuse dstport, dst and userid # SOCKS v5 - TCP @@ -174,7 +174,7 @@ class SOCKS5UDP(Packet): fields_desc = [ ShortField("res", 0), ByteField("frag", 0), - ] + SOCKS5Request.fields_desc[2:] # Re-use the atyp, addr and port fields + ] + SOCKS5Request.fields_desc[2:] # Reuse the atyp, addr and port fields def guess_payload_class(self, s): if self.port == 0: diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index 3b82842f6bb..e87b3541c5f 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -1878,7 +1878,7 @@ class LowEnergyBeaconHelper: """ Helpers for building packets for Bluetooth Low Energy Beacons. - Implementors provide a :meth:`build_eir` implementation. + Implementers provide a :meth:`build_eir` implementation. This is designed to be used as a mix-in -- see ``scapy.contrib.eddystone`` and ``scapy.contrib.ibeacon`` for examples. diff --git a/scapy/layers/gssapi.py b/scapy/layers/gssapi.py index 954cb65c868..ae598c6c0af 100644 --- a/scapy/layers/gssapi.py +++ b/scapy/layers/gssapi.py @@ -233,7 +233,7 @@ class NEGOEX_MESSAGE_HEADER(Packet): 0x01: "ACCEPTOR_NEGO", 0x02: "INITIATOR_META_DATA", 0x03: "ACCEPTOR_META_DATA", - 0x04: "CHALENGE", + 0x04: "CHALLENGE", 0x05: "AP_REQUEST", 0x06: "VERIFY", 0x07: "ALERT"}), diff --git a/scapy/libs/winpcapy.py b/scapy/libs/winpcapy.py index 38ea0a83359..8c9b88c677f 100644 --- a/scapy/libs/winpcapy.py +++ b/scapy/libs/winpcapy.py @@ -6,7 +6,7 @@ # Modified for scapy's usage - To support Npcap/Monitor mode # -# NOTE: the "winpcap" in the name nonwithstanding, this is for use +# NOTE: the "winpcap" in the name notwithstanding, this is for use # with libpcap on non-Windows platforms, as well as for WinPcap and Npcap. from ctypes import * diff --git a/scapy/modules/krack/__init__.py b/scapy/modules/krack/__init__.py index 0c72d40a6fd..f4178b62f47 100644 --- a/scapy/modules/krack/__init__.py +++ b/scapy/modules/krack/__init__.py @@ -22,7 +22,7 @@ The output logs will indicate if one of the vulnerability have been triggered. Outputs for vulnerable devices: -- IV re-use!! Client seems to be vulnerable to handshake 3/4 replay +- IV reuse!! Client seems to be vulnerable to handshake 3/4 replay (CVE-2017-13077) - Broadcast packet accepted twice!! (CVE-2017-13080) - Client has installed an all zero encryption key (TK)!! diff --git a/scapy/modules/krack/automaton.py b/scapy/modules/krack/automaton.py index bb05728a271..5fd6fc99ae8 100644 --- a/scapy/modules/krack/automaton.py +++ b/scapy/modules/krack/automaton.py @@ -722,7 +722,7 @@ def extract_iv(self, pkt): self.last_iv = iv else: if iv <= self.last_iv: - log_runtime.warning("IV re-use!! Client seems to be " + log_runtime.warning("IV reuse!! Client seems to be " "vulnerable to handshake 3/4 replay " "(CVE-2017-13077)" ) diff --git a/test/contrib/automotive/ccp.uts b/test/contrib/automotive/ccp.uts index 467d41c287b..b267317a580 100644 --- a/test/contrib/automotive/ccp.uts +++ b/test/contrib/automotive/ccp.uts @@ -877,7 +877,7 @@ assert dto.hashret() == cro.hashret() + Tests on a virtual CAN-Bus -= CAN Socket sr1 with dto.ansers(cro) == True += CAN Socket sr1 with dto.answers(cro) == True sock1 = TestSocket(CCP) sock2 = TestSocket(CAN) @@ -903,7 +903,7 @@ assert hasattr(dto, "load") == False assert dto.MTA0_extension == 2 assert dto.MTA0_address == 0x34002006 -= CAN Socket sr1 with dto.ansers(cro) == False += CAN Socket sr1 with dto.answers(cro) == False sock1 = TestSocket(CCP) sock2 = TestSocket(CAN) diff --git a/test/contrib/eigrp.uts b/test/contrib/eigrp.uts index cb29649e953..70e5ca48be7 100644 --- a/test/contrib/eigrp.uts +++ b/test/contrib/eigrp.uts @@ -160,7 +160,7 @@ p = IP()/EIGRP(tlvlist=[EIGRPv6ExtRoute(prefixlen=99, dst="2000::")]) struct.unpack("!H", p[EIGRPv6ExtRoute].build()[2:4])[0] == 70 + Stub Flags -* The receive-only flag is always set, when a router anounces itself as stub router. +* The receive-only flag is always set, when a router announces itself as stub router. = Receive-Only p = IP()/EIGRP(tlvlist=[EIGRPStub(flags="receive-only")]) diff --git a/test/scapy/layers/tls/tlsclientserver.uts b/test/scapy/layers/tls/tlsclientserver.uts index f0a86c0e3c5..87b8a593db6 100644 --- a/test/scapy/layers/tls/tlsclientserver.uts +++ b/test/scapy/layers/tls/tlsclientserver.uts @@ -254,7 +254,7 @@ def test_tls_client(suite, version, curve=None, cookie=False, client_auth=False, name="test_tls_client %s %s" % (suite, version), daemon=True) th_.start() # Synchronise threads - print("Syncrhonising...") + print("Synchronising...") assert q_.get(timeout=5) is True time.sleep(1) print("Thread synchronised") From 619d1e45f39f2f705e59e8da417b090ea4ec3049 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Mon, 9 Oct 2023 09:06:23 +0200 Subject: [PATCH 069/122] Fix tests (#4140) * Fix PyPy3 tests * Fix Windows tests --- test/configs/windows.utsc | 1 - tox.ini | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/configs/windows.utsc b/test/configs/windows.utsc index 5aaab483f48..09691d2af34 100644 --- a/test/configs/windows.utsc +++ b/test/configs/windows.utsc @@ -3,7 +3,6 @@ "test\\*.uts", "test\\scapy\\layers\\*.uts", "test\\scapy\\layers\\tls\\*.uts", - "test\\tls\\tests_tls_netaccess.uts", "test\\contrib\\automotive\\obd\\*.uts", "test\\contrib\\automotive\\scanner\\*.uts", "test\\contrib\\automotive\\gm\\*.uts", diff --git a/tox.ini b/tox.ini index 26272335d98..ebd54457f1b 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,8 @@ deps = mock coverage[toml] python-can # disabled on windows because they require c++ dependencies - brotli ; sys_platform != 'win32' + # brotli 1.1.0 broken https://github.com/google/brotli/issues/1072 + brotli < 1.1.0 ; sys_platform != 'win32' zstandard ; sys_platform != 'win32' platform = linux_non_root,linux_root: linux From 06fc74b0f37e4fc68abde735b6164085aae9cfbe Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Mon, 9 Oct 2023 14:59:39 +0200 Subject: [PATCH 070/122] Fix LatexTheme to escape "#" (#4138) --- scapy/packet.py | 6 +++++- scapy/themes.py | 9 +++++++++ test/regression.uts | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/scapy/packet.py b/scapy/packet.py index 4074d12789f..a4b94491d14 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -1442,7 +1442,11 @@ def _show_or_dump(self, fvalue = self.getfieldval(f.name) if isinstance(fvalue, Packet) or (f.islist and f.holds_packets and isinstance(fvalue, list)): # noqa: E501 pad = max(0, 10 - len(f.name)) * " " - s += "%s \\%s%s\\\n" % (label_lvl + lvl, ncol(f.name), pad) + s += "%s %s%s%s%s\n" % (label_lvl + lvl, + ct.punct("\\"), + ncol(f.name), + pad, + ct.punct("\\")) fvalue_gen = SetGen( fvalue, _iterpacket=0 diff --git a/scapy/themes.py b/scapy/themes.py index b293021718c..528b6584fec 100644 --- a/scapy/themes.py +++ b/scapy/themes.py @@ -271,6 +271,10 @@ def __getattr__(self, attr): class LatexTheme(FormatTheme): + r""" + You can prepend the output from this theme with + \tt\obeyspaces\obeylines\tiny\noindent + """ style_prompt = r"\textcolor{blue}{%s}" style_not_printable = r"\textcolor{gray}{%s}" style_layer_name = r"\textcolor{red}{\bf %s}" @@ -289,6 +293,11 @@ class LatexTheme(FormatTheme): # style_odd = "" style_logo = r"\textcolor{green}{\bf %s}" + def __getattr__(self, attr: str) -> Callable[[Any], str]: + from scapy.utils import tex_escape + styler = super(LatexTheme, self).__getattr__(attr) + return lambda x: styler(tex_escape(x)) + class LatexTheme2(FormatTheme): style_prompt = r"@`@textcolor@[@blue@]@@[@%s@]@" diff --git a/test/regression.uts b/test/regression.uts index c1a1ad0c7f6..b4ec23cb79f 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -110,6 +110,23 @@ def test_list_contrib(): test_list_contrib() += Test packet show() on LatexTheme +% with LatexTheme + +class SmallPacket(Packet): + fields_desc = [ByteField("a", 0)] + +conf_color_theme = conf.color_theme +conf.color_theme = LatexTheme() +pkt = SmallPacket() +with ContextManagerCaptureOutput() as cmco: + pkt.show() + result = cmco.get_output().strip() + +assert result == '\\#\\#\\#[ \\textcolor{red}{\\bf SmallPacket} ]\\#\\#\\# \n \\textcolor{blue}{a} = \\textcolor{purple}{0}' +conf.color_theme = conf_color_theme + + = Test automatic doc generation ~ command From e6bf2d4396f782f3a0ce4ef70b55437532599c9d Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Mon, 9 Oct 2023 23:49:30 +0300 Subject: [PATCH 071/122] [DHCPv6] add the NTP Server Option (#4113) --- scapy/layers/dhcp6.py | 56 +++++++++++++++++++++++++++ test/scapy/layers/dhcp6.uts | 75 +++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/scapy/layers/dhcp6.py b/scapy/layers/dhcp6.py index 776d2113c13..4022655648e 100644 --- a/scapy/layers/dhcp6.py +++ b/scapy/layers/dhcp6.py @@ -116,6 +116,7 @@ def _dhcp6_dispatcher(x, *args, **kargs): 41: "OPTION_NEW_POSIX_TIMEZONE", # RFC4833 42: "OPTION_NEW_TZDB_TIMEZONE", # RFC4833 48: "OPTION_LQ_CLIENT_LINK", # RFC5007 + 56: "OPTION_NTP_SERVER", # RFC5908 59: "OPT_BOOTFILE_URL", # RFC5970 60: "OPT_BOOTFILE_PARAM", # RFC5970 61: "OPTION_CLIENT_ARCH_TYPE", # RFC5970 @@ -174,6 +175,7 @@ def _dhcp6_dispatcher(x, *args, **kargs): # 46: "DHCP6OptLQClientTime", #RFC5007 # 47: "DHCP6OptLQRelayData", #RFC5007 48: "DHCP6OptLQClientLink", # RFC5007 + 56: "DHCP6OptNTPServer", # RFC5908 59: "DHCP6OptBootFileUrl", # RFC5790 60: "DHCP6OptBootFileParam", # RFC5970 61: "DHCP6OptClientArchType", # RFC5970 @@ -961,6 +963,60 @@ class DHCP6OptLQClientLink(_DHCP6OptGuessPayload): # RFC5007 length_from=lambda pkt: pkt.optlen)] +class DHCP6NTPSubOptSrvAddr(Packet): # RFC5908 sect 4.1 + name = "DHCP6 NTP Server Address Suboption" + fields_desc = [ShortField("optcode", 1), + ShortField("optlen", 16), + IP6Field("addr", "::")] + + def extract_padding(self, s): + return b"", s + + +class DHCP6NTPSubOptMCAddr(Packet): # RFC5908 sect 4.2 + name = "DHCP6 NTP Multicast Address Suboption" + fields_desc = [ShortField("optcode", 2), + ShortField("optlen", 16), + IP6Field("addr", "::")] + + def extract_padding(self, s): + return b"", s + + +class DHCP6NTPSubOptSrvFQDN(Packet): # RFC5908 sect 4.3 + name = "DHCP6 NTP Server FQDN Suboption" + fields_desc = [ShortField("optcode", 3), + FieldLenField("optlen", None, length_of="fqdn"), + DNSStrField("fqdn", "", + length_from=lambda pkt: pkt.optlen)] + + def extract_padding(self, s): + return b"", s + + +_ntp_subopts = {1: DHCP6NTPSubOptSrvAddr, + 2: DHCP6NTPSubOptMCAddr, + 3: DHCP6NTPSubOptSrvFQDN} + + +def _ntp_subopt_dispatcher(p, **kwargs): + cls = conf.raw_layer + if len(p) >= 2: + o = struct.unpack("!H", p[:2])[0] + cls = _ntp_subopts.get(o, conf.raw_layer) + return cls(p, **kwargs) + + +class DHCP6OptNTPServer(_DHCP6OptGuessPayload): # RFC5908 + name = "DHCP6 NTP Server Option" + fields_desc = [ShortEnumField("optcode", 56, dhcp6opts), + FieldLenField("optlen", None, length_of="ntpserver", + fmt="!H"), + PacketListField("ntpserver", [], + _ntp_subopt_dispatcher, + length_from=lambda pkt: pkt.optlen)] + + class DHCP6OptBootFileUrl(_DHCP6OptGuessPayload): # RFC5970 name = "DHCP6 Boot File URL Option" fields_desc = [ShortEnumField("optcode", 59, dhcp6opts), diff --git a/test/scapy/layers/dhcp6.uts b/test/scapy/layers/dhcp6.uts index 2806add53c3..9aebfe4c94f 100644 --- a/test/scapy/layers/dhcp6.uts +++ b/test/scapy/layers/dhcp6.uts @@ -1054,6 +1054,81 @@ raw(DHCP6OptLQClientLink(linkaddress=["2001:db8::1", "2001:db8::2"])) == b'\x000 a = DHCP6OptLQClientLink(b'\x000\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02') a.optcode == 48 and a.optlen == 32 and len(a.linkaddress) == 2 and a.linkaddress[0] == "2001:db8::1" and a.linkaddress[1] == "2001:db8::2" + +############ +############ ++ Test DHCP6 Option - NTP Server + += DHCP6NTPSubOptSrvAddr - Basic dissection/instantiation +b = b'\x00\x01' + b'\x00\x10' + b'\x00' * 16 +assert raw(DHCP6NTPSubOptSrvAddr()) == b + +p = DHCP6NTPSubOptSrvAddr(b) +assert p.optcode == 1 and p.optlen == 16 and p.addr == '::' + += DHCP6NTPSubOptSrvAddr - Dissection/instantiation with specific values +b = b'\x00\x01' + b'\x00\x10' + b'\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' +assert raw(DHCP6NTPSubOptSrvAddr(addr='2001:db8::1')) == b + +p = DHCP6NTPSubOptSrvAddr(b) +assert p.optcode == 1 and p.optlen == 16 and p.addr == '2001:db8::1' + += DHCP6NTPSubOptMCAddr - Basic dissection/instantiation +b = b'\x00\x02' + b'\x00\x10' + b'\x00' * 16 +assert raw(DHCP6NTPSubOptMCAddr()) == b + +p = DHCP6NTPSubOptMCAddr(b) +assert p.optcode == 2 and p.optlen == 16 and p.addr == '::' + += DHCP6NTPSubOptMCAddr - Dissection/instantiation with specific values +b = b'\x00\x02' + b'\x00\x10' + b'\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01' +assert raw(DHCP6NTPSubOptMCAddr(addr='ff02::101')) == b + +p = DHCP6NTPSubOptMCAddr(b) +assert p.optcode == 2 and p.optlen == 16 and p.addr == 'ff02::101' + += DHCP6NTPSubOptSrvFQDN - Basic dissection/instantiation +b = b'\x00\x03' + b'\x00\x01' + b'\x00' +assert raw(DHCP6NTPSubOptSrvFQDN()) == b + +p = DHCP6NTPSubOptSrvFQDN(b) +assert p.optcode == 3 and p.optlen == 1 and p.fqdn == b'.' + += DHCP6NTPSubOptSrvFQDN - Dissection/instantiation with specific values +b = b'\x00\x03' + b'\x00\x0d' + b'\x07example\x03com\x00' +assert raw(DHCP6NTPSubOptSrvFQDN(fqdn='example.com')) == b + +p = DHCP6NTPSubOptSrvFQDN(b) +assert p.optcode == 3 and p.optlen == 13 and p.fqdn == b'example.com.' + += DHCP6OptNTPServer - Basic dissection/instantiation +b = b'\x00\x38' + b'\x00\x00' +assert raw(DHCP6OptNTPServer()) == b + +p = DHCP6OptNTPServer(b) +assert p.optcode == 56 and p.optlen == 0 and p.ntpserver == [] + += DHCP6OptNTPServer - Dissection/instantiation with specific values +srv_addr = b'\x00\x01' + b'\x00\x10' + b'\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' +mc_addr = b'\x00\x02' + b'\x00\x10' + b'\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01' +srv_fqdn = b'\x00\x03' + b'\x00\x0d' + b'\x07example\x03com\x00' +b = b'\x00\x38' + b'\x00\x39' + srv_addr + mc_addr + srv_fqdn + +p = DHCP6OptNTPServer( + ntpserver=[DHCP6NTPSubOptSrvAddr(addr='2001:db8::1'), + DHCP6NTPSubOptMCAddr(addr='ff02::101'), + DHCP6NTPSubOptSrvFQDN(fqdn='example.com'), + ] +) +assert raw(p) == b + +p = DHCP6OptNTPServer(b) +assert p.optcode == 56 and p.optlen == 57 and len(p.ntpserver) == 3 +assert p.ntpserver[0] == DHCP6NTPSubOptSrvAddr(srv_addr) +assert p.ntpserver[1] == DHCP6NTPSubOptMCAddr(mc_addr) +assert p.ntpserver[2] == DHCP6NTPSubOptSrvFQDN(srv_fqdn) + + ############ ############ + Test DHCP6 Option - Boot File URL From f872e339d0288611a626a069020f20573b08e5fc Mon Sep 17 00:00:00 2001 From: Antonio V??zquez Date: Mon, 28 Aug 2023 10:01:50 +0200 Subject: [PATCH 072/122] bluetooth: BluetoothMonitorSocket fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Antonio Vázquez --- scapy/layers/bluetooth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index e87b3541c5f..ba4528f9a7d 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -2086,7 +2086,7 @@ def recv(self, x=MTU): return HCI_Hdr(self.ins.recv(x)) -class BluetoothMonitorSocket(SuperSocket): +class BluetoothMonitorSocket(_BluetoothLibcSocket): desc = "read/write over a Bluetooth monitor channel" def __init__(self): From bb0164b692ff689a6720fb874e649227a2e458a5 Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Sun, 1 Oct 2023 21:35:43 +0000 Subject: [PATCH 073/122] DNS: add Extended DNS Error EDNS0 Option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://www.rfc-editor.org/rfc/rfc8914.html The patch was also cross-checked with Wireshark: ``` Option: Extended DNS Error Option Code: Extended DNS Error (15) Option Length: 45 Option Data: 000670726f6f66206f66206e6f6e2d6578697374656e6365206f66206578616d706c652e… Info Code: DNSSEC Bogus (6) Extra Text: proof of non-existence of example.com. NSEC ``` --- scapy/layers/dns.py | 53 ++++++++++++++++++++++++++++++++- test/scapy/layers/dns_edns0.uts | 23 ++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 4191a7cf665..80634759bac 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -370,7 +370,8 @@ def i2m(self, pkt, s): # RFC 2671 - Extension Mechanisms for DNS (EDNS0) edns0types = {0: "Reserved", 1: "LLQ", 2: "UL", 3: "NSID", 4: "Reserved", - 5: "PING", 8: "edns-client-subnet", 10: "COOKIE"} + 5: "PING", 8: "edns-client-subnet", 10: "COOKIE", + 15: "Extended DNS Error"} class EDNS0TLV(Packet): @@ -394,6 +395,8 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): edns0type = struct.unpack("!H", _pkt[:2])[0] if edns0type == 8: return EDNS0ClientSubnet + if edns0type == 15: + return EDNS0ExtendedDNSError return EDNS0TLV @@ -491,6 +494,54 @@ class EDNS0ClientSubnet(Packet): length_from=lambda p: p.source_plen))] +# RFC 8914 - Extended DNS Errors + +# https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#extended-dns-error-codes +extended_dns_error_codes = { + 0: "Other", + 1: "Unsupported DNSKEY Algorithm", + 2: "Unsupported DS Digest Type", + 3: "Stale Answer", + 4: "Forged Answer", + 5: "DNSSEC Indeterminate", + 6: "DNSSEC Bogus", + 7: "Signature Expired", + 8: "Signature Not Yet Valid", + 9: "DNSKEY Missing", + 10: "RRSIGs Missing", + 11: "No Zone Key Bit Set", + 12: "NSEC Missing", + 13: "Cached Error", + 14: "Not Ready", + 15: "Blocked", + 16: "Censored", + 17: "Filtered", + 18: "Prohibited", + 19: "Stale NXDOMAIN Answer", + 20: "Not Authoritative", + 21: "Not Supported", + 22: "No Reachable Authority", + 23: "Network Error", + 24: "Invalid Data", + 25: "Signature Expired before Valid", + 26: "Too Early", + 27: "Unsupported NSEC3 Iterations Value", + 28: "Unable to conform to policy", + 29: "Synthesized", +} + + +# https://www.rfc-editor.org/rfc/rfc8914.html +class EDNS0ExtendedDNSError(Packet): + name = "DNS EDNS0 Extended DNS Error" + fields_desc = [ShortEnumField("optcode", 15, edns0types), + FieldLenField("optlen", None, length_of="extra_text", fmt="!H", + adjust=lambda pkt, x: x + 2), + ShortEnumField("info_code", 0, extended_dns_error_codes), + StrLenField("extra_text", "", + length_from=lambda pkt: pkt.optlen - 2)] + + # RFC 4034 - Resource Records for the DNS Security Extensions diff --git a/test/scapy/layers/dns_edns0.uts b/test/scapy/layers/dns_edns0.uts index d4893aac0ab..bd186694afa 100644 --- a/test/scapy/layers/dns_edns0.uts +++ b/test/scapy/layers/dns_edns0.uts @@ -96,3 +96,26 @@ assert raw(d) == raw_d d = DNSRROPT(raw_d) assert EDNS0ClientSubnet in d.rdata[0] and d.rdata[0].family == 2 and d.rdata[0].address == "2001:db8::" + + ++ EDNS0 - Extended DNS Error + += Basic instantiation & dissection + +b = b'\x00\x0f\x00\x02\x00\x00' + +p = EDNS0ExtendedDNSError() +assert raw(p) == b + +p = EDNS0ExtendedDNSError(b) +assert p.optcode == 15 and p.optlen == 2 and p.info_code == 0 and p.extra_text == b'' + +b = raw(EDNS0ExtendedDNSError(info_code="DNSSEC Bogus", extra_text="proof of non-existence of example.com. NSEC")) + +p = EDNS0ExtendedDNSError(b) +assert p.info_code == 6 and p.optlen == 45 and p.extra_text == b'proof of non-existence of example.com. NSEC' + +rropt = DNSRROPT(b'\x00\x00)\x04\xd0\x00\x00\x00\x00\x001\x00\x0f\x00-\x00\x06proof of non-existence of example.com. NSEC') +assert len(rropt.rdata) == 1 +p = rropt.rdata[0] +assert p.info_code == 6 and p.optlen == 45 and p.extra_text == b'proof of non-existence of example.com. NSEC' From 86a953badf10fc8c8b4f25d88e54e6e7c1c69c8d Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Fri, 6 Oct 2023 18:59:13 +0000 Subject: [PATCH 074/122] Add BitLenField --- scapy/fields.py | 34 ++++++++++++++++++++++++++++++---- scapy/layers/sixlowpan.py | 4 ++-- test/fields.uts | 19 +++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/scapy/fields.py b/scapy/fields.py index c3f02ff1a05..9c68ab002e9 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -2427,7 +2427,7 @@ class BitField(_BitField[int]): __doc__ = _BitField.__doc__ -class BitFixedLenField(BitField): +class BitLenField(BitField): __slots__ = ["length_from"] def __init__(self, @@ -2437,7 +2437,7 @@ def __init__(self, ): # type: (...) -> None self.length_from = length_from - super(BitFixedLenField, self).__init__(name, default, 0) + super(BitLenField, self).__init__(name, default, 0) def getfield(self, # type: ignore pkt, # type: Packet @@ -2445,7 +2445,7 @@ def getfield(self, # type: ignore ): # type: (...) -> Union[Tuple[Tuple[bytes, int], int], Tuple[bytes, int]] # noqa: E501 self.size = self.length_from(pkt) - return super(BitFixedLenField, self).getfield(pkt, s) + return super(BitLenField, self).getfield(pkt, s) def addfield(self, # type: ignore pkt, # type: Packet @@ -2454,7 +2454,7 @@ def addfield(self, # type: ignore ): # type: (...) -> Union[Tuple[bytes, int, int], bytes] self.size = self.length_from(pkt) - return super(BitFixedLenField, self).addfield(pkt, s, val) + return super(BitLenField, self).addfield(pkt, s, val) class BitFieldLenField(BitField): @@ -2661,6 +2661,32 @@ def i2repr(self, return _EnumField.i2repr(self, pkt, x) +class BitLenEnumField(BitLenField, _EnumField[int]): + __slots__ = EnumField.__slots__ + + def __init__(self, + name, # type: str + default, # type: Optional[int] + length_from, # type: Callable[[Packet], int] + enum, # type: Dict[int, str] + **kwargs, # type: Any + ): + # type: (...) -> None + _EnumField.__init__(self, name, default, enum) + BitLenField.__init__(self, name, default, length_from, **kwargs) + + def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> int + return _EnumField.any2i(self, pkt, x) # type: ignore + + def i2repr(self, + pkt, # type: Optional[Packet] + x, # type: Union[List[int], int] + ): + # type: (...) -> Any + return _EnumField.i2repr(self, pkt, x) + + class ShortEnumField(EnumField[int]): __slots__ = EnumField.__slots__ diff --git a/scapy/layers/sixlowpan.py b/scapy/layers/sixlowpan.py index 91469092bb4..fd6247715ce 100644 --- a/scapy/layers/sixlowpan.py +++ b/scapy/layers/sixlowpan.py @@ -58,7 +58,7 @@ from scapy.fields import ( BitEnumField, BitField, - BitFixedLenField, + BitLenField, BitScalingField, ByteEnumField, ByteField, @@ -282,7 +282,7 @@ class LoWPAN_HC1(Packet): lambda pkt: pkt.nh == 1 and pkt.hc2 ), # Out of spec - BitFixedLenField("pad", 0, _get_hc1_pad) + BitLenField("pad", 0, _get_hc1_pad) ] def post_dissect(self, data): diff --git a/test/fields.uts b/test/fields.uts index 0dfccfd1486..ae66d1095bf 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -336,7 +336,26 @@ p = TestBFLenF(b"\xff\x6fabcdeFGH") p assert p.len == 6 and p.str == b"abcde" and Raw in p and p[Raw].load == b"FGH" += Test BitLenField +~ field + +SIZES = {0: 6, 1: 6, 2: 14, 3: 22} + +class TestBitLenField(Packet): + fields_desc = [ + BitField("mode", 0, 2), + BitLenField("value", 0, length_from=lambda pkt: SIZES[pkt.mode]) + ] + +p = TestBitLenField(mode=1, value=50) +assert bytes(p) == b"r" + +p = TestBitLenField(mode=2, value=5000) +assert bytes(p) == b'\x93\x88' +p = TestBitLenField(b'\xc0\x01\xf4') +assert p.mode == 3 +assert p.value == 500 ############ ############ From 35b47566e82253436ca0ae55ce5b7245bd14ffc7 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Tue, 10 Oct 2023 11:54:50 +0200 Subject: [PATCH 075/122] Fix L2 dst address computation (very intrusive) (#4145) * Less intrusive L2 dst computation (especially ARP) * Apply guedou suggestions --- scapy/layers/inet.py | 3 +++ scapy/layers/inet6.py | 3 +++ scapy/layers/l2.py | 47 ++++++++++++++++++++++++++------------ test/scapy/layers/inet.uts | 22 ++++++++++++++++++ 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/scapy/layers/inet.py b/scapy/layers/inet.py index 8741b3bdad6..247a2905f48 100644 --- a/scapy/layers/inet.py +++ b/scapy/layers/inet.py @@ -1099,6 +1099,9 @@ def mysummary(self): def inet_register_l3(l2, l3): + """ + Resolves the default L2 destination address when IP is used. + """ return getmacbyip(l3.dst) diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py index 0dbb19e0f14..d795bef446e 100644 --- a/scapy/layers/inet6.py +++ b/scapy/layers/inet6.py @@ -475,6 +475,9 @@ def dispatch_hook(cls, _pkt=None, *_, **kargs): def inet6_register_l3(l2, l3): + """ + Resolves the default L2 destination address when IPv6 is used. + """ return getmacbyip6(l3.dst) diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py index 5ac5db7ce91..06ceccebb14 100644 --- a/scapy/layers/l2.py +++ b/scapy/layers/l2.py @@ -166,6 +166,12 @@ def __init__(self, name): def i2h(self, pkt, x): # type: (Optional[Packet], Optional[str]) -> str + if x is None and pkt is not None: + x = "None (resolved on build)" + return super(DestMACField, self).i2h(pkt, x) + + def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[str]) -> bytes if x is None and pkt is not None: try: x = conf.neighbor.resolve(pkt, pkt.payload) @@ -176,12 +182,10 @@ def i2h(self, pkt, x): raise ScapyNoDstMacException() else: x = "ff:ff:ff:ff:ff:ff" - warning("Mac address to reach destination not found. Using broadcast.") # noqa: E501 - return super(DestMACField, self).i2h(pkt, x) - - def i2m(self, pkt, x): - # type: (Optional[Packet], Optional[str]) -> bytes - return super(DestMACField, self).i2m(pkt, self.i2h(pkt, x)) + warning( + "MAC address to reach destination not found. Using broadcast." + ) + return super(DestMACField, self).i2m(pkt, x) class SourceMACField(MACField): @@ -311,8 +315,10 @@ class LLC(Packet): ByteField("ctrl", 0)] -def l2_register_l3(l2, l3): - # type: (Packet, Packet) -> Optional[str] +def l2_register_l3(l2: Packet, l3: Packet) -> Optional[str]: + """ + Delegates resolving the default L2 destination address to the payload of L3. + """ neighbor = conf.neighbor # type: Neighbor return neighbor.resolve(l2, l3.payload) @@ -554,15 +560,28 @@ def mysummary(self): return self.sprintf("ARP %op% %psrc% > %pdst%") -def l2_register_l3_arp(l2, l3): - # type: (Packet, Packet) -> Optional[str] - # TODO: support IPv6? - plen = l3.plen if l3.plen is not None else l3.get_field("pdst").i2len(l3, l3.pdst) +def l2_register_l3_arp(l2: Packet, l3: Packet) -> Optional[str]: + """ + Resolves the default L2 destination address when ARP is used. + """ + if l3.op == 1: # who-has + return "ff:ff:ff:ff:ff:ff" + elif l3.op == 2: # is-at + log_runtime.warning( + "You should be providing the Ethernet destination MAC address when " + "sending an is-at ARP." + ) + # Need ARP request to send ARP request... + plen = l3.get_field("pdst").i2len(l3, l3.pdst) if plen == 4: return getmacbyip(l3.pdst) + elif plen == 32: + from scapy.layers.inet6 import getmacbyip6 + return getmacbyip6(l3.pdst) + # Can't even do that log_runtime.warning( - "Unable to guess L2 MAC address from an ARP packet with a " - "non-IPv4 pdst. Provide it manually !" + "You should be providing the Ethernet destination mac when sending this " + "kind of ARP packets." ) return None diff --git a/test/scapy/layers/inet.uts b/test/scapy/layers/inet.uts index e767afcbad9..5b98c1c74f3 100644 --- a/test/scapy/layers/inet.uts +++ b/test/scapy/layers/inet.uts @@ -572,6 +572,28 @@ assert isinstance(pkt, UDP) and pkt.dport == 5353 pkt = pkt.payload assert isinstance(pkt, DNS) and isinstance(pkt.payload, NoPayload) += Layer binding with show() +* getmacbyip must only be called when building + +import mock + +def _err(*_): + raise ValueError + +with mock.patch("scapy.layers.l2.getmacbyip", side_effect=_err): + with mock.patch("scapy.layers.inet.getmacbyip", side_effect=_err): + # ARP who-has should never call getmacbyip + pkt1 = Ether() / ARP(pdst="10.0.0.1") + pkt1.show() + bytes(pkt1) + # IP should only call getmacbyip when building + pkt2 = Ether() / IP(dst="10.0.0.1") + pkt2.show() + try: + bytes(pkt2) + assert False, "Should have called getmacbyip" + except ValueError: + pass ############ ############ From e3074451823394e3bb750a1a810dc880e7ea7082 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 10 Oct 2023 15:05:37 +0200 Subject: [PATCH 076/122] Add more BMW software version definitions (#4143) * Add more BMW software version definitions * fix codespell * add englisch translation --- .config/codespell_ignore.txt | 5 +- scapy/contrib/automotive/bmw/definitions.py | 67 ++++++++++++++++++--- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt index 6405c1d1111..f425573c7ee 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -1,6 +1,7 @@ aci ans archtypes +applikation ba byteorder cace @@ -11,12 +12,13 @@ delt doas doubleclick ether -ether eventtypes fo +funktion gost hart iff +interaktive inout microsof mitre @@ -32,6 +34,7 @@ ser singl slac te +temporaere tim ue uint diff --git a/scapy/contrib/automotive/bmw/definitions.py b/scapy/contrib/automotive/bmw/definitions.py index aa11be6209d..143827a5c38 100644 --- a/scapy/contrib/automotive/bmw/definitions.py +++ b/scapy/contrib/automotive/bmw/definitions.py @@ -15,7 +15,6 @@ from scapy.contrib.automotive.uds import UDS, UDS_RDBI, UDS_DSC, UDS_IOCBI, \ UDS_RC, UDS_RD, UDS_RSDBI, UDS_RDBIPR - BMW_specific_enum = { 0: "requestIdentifiedBCDDTCAndStatus", 1: "requestSupportedBCDDTCAndStatus", @@ -252,10 +251,60 @@ def i2repr(self, pkt, x): class SVK_Entry(Packet): + process_classes = { + "0x01": "HWEL", + "0x02": "HWAP", + "0x03": "HWFR", + "0x05": "CAFD", + "0x06": "BTLD", + "0x08": "SWFL", + "0x09": "SWFF", + "0x0A": "SWPF", + "0x0B": "ONPS", + "0x0F": "FAFP", + "0x1A": "TLRT", + "0x1B": "TPRG", + "0x07": "FLSL", + "0x0C": "IBAD", + "0x10": "FCFA", + "0x1C": "BLUP", + "0x1D": "FLUP", + "0xC0": "SWUP", + "0xC1": "SWIP", + "0xA0": "ENTD", + "0xA1": "NAVD", + "0xA2": "FCFN", + "0x04": "GWTB", + "0x0D": "SWFK", + } + """ + HWEL - Hardware (Elektronik) - Hardware (Electronics) + HWAP - Hardwareauspraegung - Hardware Configuration + HWFR - Hardwarefarbe - Hardware Color + CAFD - Codierdaten - Coding Data + BTLD - Bootloader - Bootloader + SWFL - Software ECU Speicherimage - Software ECU Storage Image + SWFF - Flash File Software - Flash File Software + SWPF - Pruefsoftware - Testing Software + ONPS - Onboard Programmiersystem - Onboard Programming System + FAFP - FA2FP - FA2FP + TLRT - Temporaere Loeschroutine - Temporary Deletion Routine + TPRG - Temporaere Programmierroutine - Temporary Programming Routine + FLSL - Flashloader Slave - Flashloader Slave + IBAD - Interaktive Betriebsanleitung Daten - Interactive Operating Manual Data + FCFA - Freischaltcode Fahrzeug-Auftrag - Vehicle Order Unlock Code + BLUP - Bootloader-Update Applikation - Bootloader Update Application + FLUP - Flashloader-Update Applikation - Flashloader Update Application + SWUP - Software-Update Package - Software Update Package + SWIP - Index Software-Update Package - Software Update Package Index + ENTD - Entertainment Daten - Entertainment Data + NAVD - Navigation Daten - Navigation Data + FCFN - Freischaltcode Funktion - Function Unlock Code + GWTB - Gateway-Tabelle - Gateway Table + SWFK - BEGU: Detaillierung auf SWE-Ebene - BEGU: Detailing at SWE Level + """ fields_desc = [ - ByteEnumField("processClass", 0, {1: "HWEL", 2: "HWAP", 4: "GWTB", - 5: "CAFD", 6: "BTLD", 7: "FLSL", - 8: "SWFL"}), + ByteEnumField("processClass", 0, process_classes), XStrFixedLenField("svk_id", b"", length=4), ByteField("mainVersion", 0), ByteField("subVersion", 0), @@ -372,7 +421,6 @@ class WEBSERVER(Packet): bind_layers(DEV_JOB, READ_MEM, identifier=0xffff) bind_layers(DEV_JOB_PR, READ_MEM_PR, identifier=0xffff) - bind_layers(UDS_RDBIPR, SVK, dataIdentifier=0xf101) bind_layers(UDS_RDBIPR, SVK, dataIdentifier=0xf102) bind_layers(UDS_RDBIPR, SVK, dataIdentifier=0xf103) @@ -438,7 +486,6 @@ class WEBSERVER(Packet): bind_layers(UDS_RDBIPR, SVK, dataIdentifier=0xf13f) bind_layers(UDS_RDBIPR, SVK, dataIdentifier=0xf140) - UDS_RDBI.dataIdentifiers[0x0014] = "RDBCI_IS_LESEN_DETAIL_REQ" UDS_RDBI.dataIdentifiers[0x0015] = "RDBCI_HS_LESEN_DETAIL_REQ" UDS_RDBI.dataIdentifiers[0x0e80] = "AirbagLock" @@ -1505,7 +1552,7 @@ class WEBSERVER(Packet): UDS_RDBI.dataIdentifiers[0x22fd] = "afterSalesServiceData_2200_22FF" UDS_RDBI.dataIdentifiers[0x22fe] = "afterSalesServiceData_2200_22FF" UDS_RDBI.dataIdentifiers[0x22ff] = "afterSalesServiceData_2200_22FF" -UDS_RDBI.dataIdentifiers[0x2300] = "operatingData" # or RDBCI_BETRIEBSDATEN_LESEN_REQ # noqa E501 +UDS_RDBI.dataIdentifiers[0x2300] = "operatingData" # or RDBCI_BETRIEBSDATEN_LESEN_REQ # noqa E501 UDS_RDBI.dataIdentifiers[0x2301] = "additionalOperatingData 2301-23FF" UDS_RDBI.dataIdentifiers[0x2302] = "additionalOperatingData 2301-23FF" UDS_RDBI.dataIdentifiers[0x2303] = "additionalOperatingData 2301-23FF" @@ -1831,13 +1878,13 @@ class WEBSERVER(Packet): UDS_RDBI.dataIdentifiers[0x2503] = "ProgrammingCounterMax" UDS_RDBI.dataIdentifiers[0x2504] = "FlashTimings" UDS_RDBI.dataIdentifiers[0x2505] = "MaxBlocklength" -UDS_RDBI.dataIdentifiers[0x2506] = "ReadMemoryAddress" # or maximaleBlockLaenge # noqa E501 +UDS_RDBI.dataIdentifiers[0x2506] = "ReadMemoryAddress" # or maximaleBlockLaenge # noqa E501 UDS_RDBI.dataIdentifiers[0x2507] = "EcuSupportsDeleteSwe" UDS_RDBI.dataIdentifiers[0x2508] = "GWRoutingStatus" UDS_RDBI.dataIdentifiers[0x2509] = "RoutingTable" UDS_RDBI.dataIdentifiers[0x2530] = "SubnetStatus" UDS_RDBI.dataIdentifiers[0x2541] = "STATUS_CALCVN" -UDS_RDBI.dataIdentifiers[0x3000] = "RDBI_CD_REQ" # or WDBI_CD_REQ +UDS_RDBI.dataIdentifiers[0x3000] = "RDBI_CD_REQ" # or WDBI_CD_REQ UDS_RDBI.dataIdentifiers[0x300a] = "Codier-VIN" UDS_RDBI.dataIdentifiers[0x37fe] = "Codierpruefstempel" UDS_RDBI.dataIdentifiers[0x3f00] = "SVT-Ist" @@ -4864,7 +4911,7 @@ class WEBSERVER(Packet): UDS_RC.routineControlIdentifiers[0x0f09] = "checkSignature" UDS_RC.routineControlIdentifiers[0x0f0a] = "checkProgrammingStatus" UDS_RC.routineControlIdentifiers[0x0f0b] = "ExecuteDiagnosticService" -UDS_RC.routineControlIdentifiers[0x0f0c] = "SetEnergyMode" # or controlEnergySavingMode # noqa E501 +UDS_RC.routineControlIdentifiers[0x0f0c] = "SetEnergyMode" # or controlEnergySavingMode # noqa E501 UDS_RC.routineControlIdentifiers[0x0f0d] = "resetSystemFaultMessage" UDS_RC.routineControlIdentifiers[0x0f0e] = "timeControlledPowerDown" UDS_RC.routineControlIdentifiers[0x0f0f] = "disableCommunicationOverGateway" From 2a4c4ae25e267c34129ba091a2b411aafdaebcfd Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 11 Oct 2023 12:08:19 +0200 Subject: [PATCH 077/122] Add parsing of ExtendedDataRecord to UDS_DTCs (#4117) * Add UDS_DTC parsing for ExtendedDataRecords * add dict for DTC descriptions --- scapy/contrib/automotive/uds.py | 34 ++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 8b0dcf6f18f..bbe0316144c 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -1013,6 +1013,8 @@ class UDS_RDTCI(Packet): class DTC(Packet): name = 'Diagnostic Trouble Code' + dtc_descriptions = {} # Customize this dictionary for each individual ECU / OEM + fields_desc = [ BitEnumField("system", 0, 2, { 0: "Powertrain", @@ -1032,7 +1034,7 @@ def extract_padding(self, s): return '', s -class DTC_Status(Packet): +class DTCAndStatusRecord(Packet): name = 'DTC and status record' fields_desc = [ PacketField("dtc", None, pkt_cls=DTC), @@ -1043,6 +1045,26 @@ def extract_padding(self, s): return '', s +class DTCExtendedData(Packet): + name = 'Diagnostic Trouble Code Extended Data' + dataTypes = ObservableDict() + + fields_desc = [ + ByteEnumField("data_type", 0, dataTypes), + XByteField("record", 0) + ] + + def extract_padding(self, s): + return '', s + + +class DTCExtendedDataRecord(Packet): + fields_desc = [ + PacketField("dtcAndStatus", None, pkt_cls=DTCAndStatusRecord), + PacketListField("extendedData", None, pkt_cls=DTCExtendedData) + ] + + class UDS_RDTCIPR(Packet): name = 'ReadDTCInformationPositiveResponse' fields_desc = [ @@ -1063,14 +1085,16 @@ class UDS_RDTCIPR(Packet): lambda pkt: pkt.reportType in [0x01, 0x07, 0x11, 0x12]), ConditionalField(PacketListField('DTCAndStatusRecord', None, - pkt_cls=DTC_Status), + pkt_cls=DTCAndStatusRecord), lambda pkt: pkt.reportType in [0x02, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x13, 0x15]), ConditionalField(StrField('dataRecord', b""), - lambda pkt: pkt.reportType in [0x03, 0x04, 0x05, - 0x06, 0x08, 0x09, - 0x10, 0x14]) + lambda pkt: pkt.reportType in [0x03, 0x08, 0x09, + 0x10, 0x14]), + ConditionalField(PacketField('extendedDataRecord', None, + pkt_cls=DTCExtendedDataRecord), + lambda pkt: pkt.reportType in [0x06]) ] def answers(self, other): From 4fefbb09a232b753a08e63cd1c805414011e18e7 Mon Sep 17 00:00:00 2001 From: leonidokner <88691968+leonidokner@users.noreply.github.com> Date: Thu, 19 Oct 2023 23:30:55 -0700 Subject: [PATCH 078/122] Added logic to compute ICRC for RoCEv2 over IPv6 (#4154) * Added logic to compute ICRC for RoCEv2 over IPv6. * Fixed RoCEv2 icrc computation. --------- Co-authored-by: leonidokner --- scapy/contrib/roce.py | 20 +++++++++++++++++++- test/contrib/roce.uts | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/scapy/contrib/roce.py b/scapy/contrib/roce.py index 9a6683cefa1..93dceab693b 100644 --- a/scapy/contrib/roce.py +++ b/scapy/contrib/roce.py @@ -14,6 +14,7 @@ from scapy.fields import ByteEnumField, ByteField, XByteField, \ ShortField, XShortField, XLongField, BitField, XBitField, FCSField from scapy.layers.inet import IP, UDP +from scapy.layers.inet6 import IPv6 from scapy.layers.l2 import Ether from scapy.compat import raw from scapy.error import warning @@ -179,8 +180,25 @@ def compute_icrc(self, p): pshdr[UDP].payload = Raw(bth + payload + icrc_placeholder) icrc = crc32(raw(pshdr)[:-4]) & 0xffffffff return self.pack_icrc(icrc) + elif isinstance(ip, IPv6): + # pseudo-LRH / IPv6 / UDP / BTH / payload + pshdr = Raw(b'\xff' * 8) / ip.copy() + pshdr.hlim = 0xff + pshdr.fl = 0xfffff + pshdr.tc = 0xff + pshdr[UDP].chksum = 0xffff + pshdr[BTH].fecn = 1 + pshdr[BTH].becn = 1 + pshdr[BTH].resv6 = 0xff + bth = pshdr[BTH].self_build() + payload = raw(pshdr[BTH].payload) + # add ICRC placeholder just to get the right IPv6.plen and + # UDP.length + icrc_placeholder = b'\xff\xff\xff\xff' + pshdr[UDP].payload = Raw(bth + payload + icrc_placeholder) + icrc = crc32(raw(pshdr)[:-4]) & 0xffffffff + return self.pack_icrc(icrc) else: - # TODO support IPv6 warning("The underlayer protocol %s is not supported.", ip and ip.name) return self.pack_icrc(0) diff --git a/test/contrib/roce.uts b/test/contrib/roce.uts index 757163d3049..ff19affec3c 100644 --- a/test/contrib/roce.uts +++ b/test/contrib/roce.uts @@ -124,3 +124,24 @@ assert not pkt[BTH].ackreq assert pkt[AETH].syndrome == 0 assert pkt[AETH].msn == 5 assert pkt.icrc == 0x25f0c038 + += RoCE over IPv6 + +# an example UC packet +pkt = Ether(dst='24:8a:07:a8:fa:22', src='24:8a:07:a8:fa:22')/ \ + IPv6(nh=17,src='2022::1023', dst='2023::1024', \ + version=6,hlim=255,plen=44,fl=0x1face,tc=226)/ \ + UDP(sport=49152, dport=4791, len=44)/ \ + BTH(opcode='UC_SEND_ONLY', migreq=1, padcount=2, pkey=0xffff, dqpn=211, psn=13571856)/ \ + Raw(b'F0\x81\x8b\xe2\x895\xd9\x0e\x9a\x95PT\x01\xbe\x88^P\x00\x00') + +# include ICRC placeholder +pkt = Ether(pkt.build() + b'\x00' * 4) + +assert IPv6 in pkt.layers() +assert UDP in pkt.layers() +print(hex(pkt[UDP].chksum)) +assert pkt[UDP].chksum == 0xe7c5 +assert BTH in pkt.layers() +print(hex(pkt[BTH].icrc)) +assert pkt[BTH].icrc == 0x3e5b743b From 9ca3cc9646036f4cf562e7fd274d3edf17575957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Pachocki?= <101826859+ppachocki@users.noreply.github.com> Date: Mon, 23 Oct 2023 22:42:51 +0200 Subject: [PATCH 079/122] Fix UDP header in IPSec NAT-Traversal. (#4125) * Fix UDP header in IPSec NAT-Traversal. * Added tests for IPSec NAT-Traversal. --- scapy/layers/ipsec.py | 7 ++-- test/scapy/layers/ipsec.uts | 83 +++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/scapy/layers/ipsec.py b/scapy/layers/ipsec.py index ecdc14af960..fbf3b173862 100644 --- a/scapy/layers/ipsec.py +++ b/scapy/layers/ipsec.py @@ -1046,17 +1046,16 @@ def _encrypt_esp(self, pkt, seq_num=None, iv=None, esn_en=None, esn=None): ip_header /= nat_t_header if ip_header.version == 4: - ip_header.len = len(ip_header) + len(esp) + del ip_header.len del ip_header.chksum - ip_header = ip_header.__class__(raw(ip_header)) else: - ip_header.plen = len(ip_header.payload) + len(esp) + del ip_header.plen # sequence number must always change, unless specified by the user if seq_num is None: self.seq_num += 1 - return ip_header / esp + return ip_header.__class__(raw(ip_header / esp)) def _encrypt_ah(self, pkt, seq_num=None, esn_en=False, esn=0): diff --git a/test/scapy/layers/ipsec.uts b/test/scapy/layers/ipsec.uts index 39697a40eca..eedac7bc6bd 100644 --- a/test/scapy/layers/ipsec.uts +++ b/test/scapy/layers/ipsec.uts @@ -3277,6 +3277,89 @@ try: except IPSecIntegrityError as err: err +############################################################################### ++ IPv4 / UDP / ESP - NAT-Traversal + +####################################### += IPv4 / UDP / ESP - NAT-Traversal - Tunnel +~ -crypto + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('testdata') +p = IP(raw(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='NULL', crypt_key=None, + auth_algo='NULL', auth_key=None, + tunnel_header=IP(src='11.11.11.11', dst='22.22.22.22'), + nat_t_header=UDP(dport=5000)) + +e = sa.encrypt(p) +e + +assert isinstance(e, IP) +* after encryption packet should be encapsulated with the given ip tunnel header +assert e.src == '11.11.11.11' and e.dst == '22.22.22.22' +assert e.chksum != p.chksum +* the encrypted packet should have an UDP layer +assert e.proto == socket.IPPROTO_UDP +assert e.haslayer(UDP) +assert e[UDP].sport == 4500 +assert e[UDP].dport == 5000 +assert e[UDP].chksum == 0 +assert e.haslayer(ESP) +assert not e.haslayer(TCP) +assert e[ESP].spi == sa.spi +assert b'testdata' in e[ESP].data + +d = sa.decrypt(e) +d + +* after decryption the original packet payload should be unaltered +assert d[TCP] == p[TCP] + +####################################### += IPv4 / UDP / ESP - NAT-Traversal - Transport +~ -crypto + +import socket + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('testdata') +p = IP(raw(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='NULL', crypt_key=None, + auth_algo='NULL', auth_key=None, + nat_t_header=UDP(dport=5000)) + +e = sa.encrypt(p) +e + +assert isinstance(e, IP) +assert e.src == '1.1.1.1' and e.dst == '2.2.2.2' +assert e.chksum != p.chksum +* the encrypted packet should have an UDP layer +assert e.proto == socket.IPPROTO_UDP +assert e.haslayer(UDP) +assert e[UDP].sport == 4500 +assert e[UDP].dport == 5000 +assert e[UDP].chksum == 0 +assert e.haslayer(ESP) +assert not e.haslayer(TCP) +assert e[ESP].spi == sa.spi +assert b'testdata' in e[ESP].data + +d = sa.decrypt(e) +d + +* after decryption the original packet payload should be unaltered +assert d[TCP] == p[TCP] + ############################################################################### + IPv6 / ESP From f9113dc5ce5bea1d748a9f35904c82b70ec93d64 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 12 Sep 2023 13:50:48 +0200 Subject: [PATCH 080/122] Minor cleanup and add utility function for Automotive Scanner --- scapy/contrib/automotive/scanner/executor.py | 7 +++++++ scapy/contrib/isotp/isotp_soft_socket.py | 18 ++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/scapy/contrib/automotive/scanner/executor.py b/scapy/contrib/automotive/scanner/executor.py index 196a88b6002..5150c285ce7 100644 --- a/scapy/contrib/automotive/scanner/executor.py +++ b/scapy/contrib/automotive/scanner/executor.py @@ -33,8 +33,11 @@ Callable, Type, cast, + TypeVar, ) +T = TypeVar("T") + class AutomotiveTestCaseExecutor(metaclass=abc.ABCMeta): """ @@ -415,6 +418,10 @@ def show_testcases_status(self): data += [(repr(s), t.__class__.__name__, t.has_completed(s))] make_lined_table(data, lambda *tup: (tup[0], tup[1], tup[2])) + def get_test_cases_by_class(self, cls): + # type: (Type[T]) -> List[T] + return [x for x in self.configuration.test_cases if isinstance(x, cls)] + @property def supported_responses(self): # type: () -> List[EcuResponse] diff --git a/scapy/contrib/isotp/isotp_soft_socket.py b/scapy/contrib/isotp/isotp_soft_socket.py index 96411c41908..3b8dc7f80ce 100644 --- a/scapy/contrib/isotp/isotp_soft_socket.py +++ b/scapy/contrib/isotp/isotp_soft_socket.py @@ -228,6 +228,8 @@ class TimeoutScheduler: # use heapq functions on _handles! _handles = [] # type: List[TimeoutScheduler.Handle] + logger = logging.getLogger("scapy.contrib.automotive.timeout_scheduler") + @classmethod def schedule(cls, timeout, callback): # type: (float, Callable[[], None]) -> TimeoutScheduler.Handle @@ -316,13 +318,13 @@ def _wait(cls, handle): # Wait until the next timeout, # or until event.set() gets called in another thread. if to_wait > 0: - log_isotp.debug("TimeoutScheduler Thread going to sleep @ %f " + - "for %fs", now, to_wait) + cls.logger.debug("Thread going to sleep @ %f " + + "for %fs", now, to_wait) interrupted = cls._event.wait(to_wait) new = cls._time() - log_isotp.debug("TimeoutScheduler Thread awake @ %f, slept for" + - " %f, interrupted=%d", new, new - now, - interrupted) + cls.logger.debug("Thread awake @ %f, slept for" + + " %f, interrupted=%d", new, new - now, + interrupted) # Clear the event so that we can wait on it again, # Must be done before doing the callbacks to avoid losing a set(). @@ -335,7 +337,7 @@ def _task(cls): start when the first timeout is added and stop when the last timeout is removed or executed.""" - log_isotp.debug("TimeoutScheduler Thread spawning @ %f", cls._time()) + cls.logger.debug("Thread spawning @ %f", cls._time()) time_empty = None @@ -357,7 +359,7 @@ def _task(cls): finally: # Worst case scenario: if this thread dies, the next scheduled # timeout will start a new one - log_isotp.debug("TimeoutScheduler Thread died @ %f", cls._time()) + cls.logger.debug("Thread died @ %f", cls._time()) cls._thread = None @classmethod @@ -379,7 +381,7 @@ def _poll(cls): callback = handle._cb handle._cb = True - # Call the callback here, outside of the mutex + # Call the callback here, outside the mutex if callable(callback): try: callback() From d54a4578d7610265f4a97e0e1312d9c676f4604e Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 22 Oct 2023 21:23:26 +0200 Subject: [PATCH 081/122] Support stupid OSes --- scapy/config.py | 2 +- scapy/layers/dns.py | 32 +++++++--- scapy/layers/hsrp.py | 6 +- scapy/layers/ntp.py | 2 +- scapy/main.py | 145 +++++++++++++++++++++++-------------------- scapy/pton_ntop.py | 4 ++ scapy/utils.py | 5 +- 7 files changed, 117 insertions(+), 79 deletions(-) diff --git a/scapy/config.py b/scapy/config.py index 7c4215998ff..7b03d9debfe 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -952,7 +952,7 @@ def __getattribute__(self, attr): if not Conf.ipv6_enabled: log_scapy.warning("IPv6 support disabled in Python. Cannot load Scapy IPv6 layers.") # noqa: E501 - for m in ["inet6", "dhcp6"]: + for m in ["inet6", "dhcp6", "sixlowpan"]: if m in Conf.load_layers: Conf.load_layers.remove(m) diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 80634759bac..6bed0be230c 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -26,18 +26,32 @@ from scapy.compat import orb, raw, chb, bytes_encode, plain_str from scapy.error import log_runtime, warning, Scapy_Exception from scapy.packet import Packet, bind_layers, Raw -from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ - ConditionalField, Field, FieldLenField, FlagsField, IntField, \ - PacketListField, ShortEnumField, ShortField, StrField, \ - StrLenField, MultipleTypeField, UTCTimeField, I +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + Field, + FieldLenField, + FlagsField, + I, + IP6Field, + IntField, + MultipleTypeField, + PacketListField, + ShortEnumField, + ShortField, + StrField, + StrLenField, + UTCTimeField, +) from scapy.sendrecv import sr1 from scapy.supersocket import StreamSocket from scapy.pton_ntop import inet_ntop, inet_pton from scapy.volatile import RandShort from scapy.layers.inet import IP, DestIPField, IPField, UDP, TCP -from scapy.layers.inet6 import IPv6, DestIP6Field, IP6Field - from typing import ( Any, @@ -1103,7 +1117,9 @@ def pre_dissect(self, s): bind_layers(UDP, DNS, dport=53) bind_layers(UDP, DNS, sport=53) DestIPField.bind_addr(UDP, "224.0.0.251", dport=5353) -DestIP6Field.bind_addr(UDP, "ff02::fb", dport=5353) +if conf.ipv6_enabled: + from scapy.layers.inet6 import DestIP6Field + DestIP6Field.bind_addr(UDP, "ff02::fb", dport=5353) bind_layers(TCP, DNS, dport=53) bind_layers(TCP, DNS, sport=53) @@ -1135,6 +1151,7 @@ def dns_resolve(qname, qtype="A", raw=False, verbose=1, **kwargs): kwargs.setdefault("timeout", 3) kwargs.setdefault("verbose", 0) + res = None for nameserver in conf.nameservers: # Try all nameservers try: @@ -1310,6 +1327,7 @@ def is_request(self, req): ) def make_reply(self, req): + from scapy.layers.inet6 import IPv6 if IPv6 in req: resp = IPv6(dst=req[IPv6].src, src=self.src_ip6) else: diff --git a/scapy/layers/hsrp.py b/scapy/layers/hsrp.py index ca0868f927e..ad9554382f9 100644 --- a/scapy/layers/hsrp.py +++ b/scapy/layers/hsrp.py @@ -12,11 +12,11 @@ http://www.smartnetworks.jp/2006/02/hsrp_8_hsrp_version_2.html """ +from scapy.config import conf from scapy.fields import ByteEnumField, ByteField, IPField, SourceIPField, \ StrFixedLenField, XIntField, XShortField from scapy.packet import Packet, bind_layers, bind_bottom_up from scapy.layers.inet import DestIPField, UDP -from scapy.layers.inet6 import DestIP6Field class HSRP(Packet): @@ -66,4 +66,6 @@ def post_build(self, p, pay): bind_layers(UDP, HSRP, dport=1985, sport=1985) bind_layers(UDP, HSRP, dport=2029, sport=2029) DestIPField.bind_addr(UDP, "224.0.0.2", dport=1985) -DestIP6Field.bind_addr(UDP, "ff02::66", dport=2029) +if conf.ipv6_enabled: + from scapy.layers.inet6 import DestIP6Field + DestIP6Field.bind_addr(UDP, "ff02::66", dport=2029) diff --git a/scapy/layers/ntp.py b/scapy/layers/ntp.py index 57d155e9af9..51eb4b1002b 100644 --- a/scapy/layers/ntp.py +++ b/scapy/layers/ntp.py @@ -22,6 +22,7 @@ FieldListField, FixedPointField, FlagsField, + IP6Field, IPField, IntField, LEIntField, @@ -39,7 +40,6 @@ XByteField, XStrFixedLenField, ) -from scapy.layers.inet6 import IP6Field from scapy.layers.inet import UDP from scapy.utils import lhex from scapy.compat import orb diff --git a/scapy/main.py b/scapy/main.py index 464e125e965..254da828918 100644 --- a/scapy/main.py +++ b/scapy/main.py @@ -561,6 +561,84 @@ def _len(line): return lines +def get_fancy_banner(mini: Optional[bool] = None) -> str: + """ + Generates the fancy Scapy banner + + :param mini: if set, force a mini banner or not. Otherwise detect + """ + from scapy.config import conf + from scapy.utils import get_terminal_width + if mini is None: + mini_banner = (get_terminal_width() or 84) <= 75 + else: + mini_banner = mini + + the_logo = [ + " ", + " aSPY//YASa ", + " apyyyyCY//////////YCa ", + " sY//////YSpcs scpCY//Pp ", + " ayp ayyyyyyySCP//Pp syY//C ", + " AYAsAYYYYYYYY///Ps cY//S", + " pCCCCY//p cSSps y//Y", + " SPPPP///a pP///AC//Y", + " A//A cyP////C", + " p///Ac sC///a", + " P////YCpc A//A", + " scccccp///pSP///p p//Y", + " sY/////////y caa S//P", + " cayCyayP//Ya pY/Ya", + " sY/PsY////YCc aC//Yp ", + " sc sccaCY//PCypaapyCP//YSs ", + " spCPY//////YPSps ", + " ccaacs ", + " ", + ] + + # Used on mini screens + the_logo_mini = [ + " .SYPACCCSASYY ", + "P /SCS/CCS ACS", + " /A AC", + " A/PS /SPPS", + " YP (SC", + " SPS/A. SC", + " Y/PACC PP", + " PY*AYC CAA", + " YYCY//SCYP ", + ] + + the_banner = [ + "", + "", + " |", + " | Welcome to Scapy", + " | Version %s" % conf.version, + " |", + " | https://github.com/secdev/scapy", + " |", + " | Have fun!", + " |", + ] + + if mini_banner: + the_logo = the_logo_mini + the_banner = [x[2:] for x in the_banner[3:-1]] + the_banner = [""] + the_banner + [""] + else: + quote, author = choice(QUOTES) + the_banner.extend(_prepare_quote(quote, author, max_len=39)) + the_banner.append(" |") + return "\n".join( + logo + banner for logo, banner in zip_longest( + (conf.color_theme.logo(line) for line in the_logo), + (conf.color_theme.success(line) for line in the_banner), + fillvalue="" + ) + ) + + def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): # type: (Optional[Any], Optional[Any], Optional[Any], int) -> None """ @@ -635,72 +713,7 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): ) if conf.fancy_banner: - from scapy.utils import get_terminal_width - mini_banner = (get_terminal_width() or 84) <= 75 - - the_logo = [ - " ", - " aSPY//YASa ", - " apyyyyCY//////////YCa ", - " sY//////YSpcs scpCY//Pp ", - " ayp ayyyyyyySCP//Pp syY//C ", - " AYAsAYYYYYYYY///Ps cY//S", - " pCCCCY//p cSSps y//Y", - " SPPPP///a pP///AC//Y", - " A//A cyP////C", - " p///Ac sC///a", - " P////YCpc A//A", - " scccccp///pSP///p p//Y", - " sY/////////y caa S//P", - " cayCyayP//Ya pY/Ya", - " sY/PsY////YCc aC//Yp ", - " sc sccaCY//PCypaapyCP//YSs ", - " spCPY//////YPSps ", - " ccaacs ", - " ", - ] - - # Used on mini screens - the_logo_mini = [ - " .SYPACCCSASYY ", - "P /SCS/CCS ACS", - " /A AC", - " A/PS /SPPS", - " YP (SC", - " SPS/A. SC", - " Y/PACC PP", - " PY*AYC CAA", - " YYCY//SCYP ", - ] - - the_banner = [ - "", - "", - " |", - " | Welcome to Scapy", - " | Version %s" % conf.version, - " |", - " | https://github.com/secdev/scapy", - " |", - " | Have fun!", - " |", - ] - - if mini_banner: - the_logo = the_logo_mini - the_banner = [x[2:] for x in the_banner[3:-1]] - the_banner = [""] + the_banner + [""] - else: - quote, author = choice(QUOTES) - the_banner.extend(_prepare_quote(quote, author, max_len=39)) - the_banner.append(" |") - banner_text = "\n".join( - logo + banner for logo, banner in zip_longest( - (conf.color_theme.logo(line) for line in the_logo), - (conf.color_theme.success(line) for line in the_banner), - fillvalue="" - ) - ) + banner_text = get_fancy_banner() else: banner_text = "Welcome to Scapy (%s)" % conf.version if mybanner is not None: diff --git a/scapy/pton_ntop.py b/scapy/pton_ntop.py index 8c4129ae748..9fa13e89012 100644 --- a/scapy/pton_ntop.py +++ b/scapy/pton_ntop.py @@ -87,6 +87,8 @@ def inet_pton(af, addr): addr = plain_str(addr) # Use inet_pton if available try: + if not socket.has_ipv6: + raise AttributeError return socket.inet_pton(af, addr) except AttributeError: try: @@ -134,6 +136,8 @@ def inet_ntop(af, addr): # Use inet_ntop if available addr = bytes_encode(addr) try: + if not socket.has_ipv6: + raise AttributeError return socket.inet_ntop(af, addr) except AttributeError: try: diff --git a/scapy/utils.py b/scapy/utils.py index 211378cc9eb..7a131a60b91 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -2927,8 +2927,9 @@ def get_terminal_width(): s = struct.pack('HHHH', 0, 0, 0, 0) x = fcntl.ioctl(1, termios.TIOCGWINSZ, s) sizex = struct.unpack('HHHH', x)[1] - except IOError: - pass + except (IOError, ModuleNotFoundError): + # If everything failed, return default terminal size + sizex = 79 return sizex From b95fdc7aa85c4b523c1f082aa449b4c8bccc07fa Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 25 Oct 2023 08:48:18 +0200 Subject: [PATCH 082/122] Improve exception handling in PeriodicSender Thread (#4152) --- scapy/utils.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/scapy/utils.py b/scapy/utils.py index 7a131a60b91..2fd4d0c89ca 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -3171,8 +3171,8 @@ def whois(ip_address): class PeriodicSenderThread(threading.Thread): - def __init__(self, sock, pkt, interval=0.5): - # type: (Any, _PacketIterable, float) -> None + def __init__(self, sock, pkt, interval=0.5, ignore_exceptions=True): + # type: (Any, _PacketIterable, float, bool) -> None """ Thread to send packets periodically Args: @@ -3187,13 +3187,20 @@ def __init__(self, sock, pkt, interval=0.5): self._socket = sock self._stopped = threading.Event() self._interval = interval + self._ignore_exceptions = ignore_exceptions threading.Thread.__init__(self) def run(self): # type: () -> None while not self._stopped.is_set() and not self._socket.closed: for p in self._pkts: - self._socket.send(p) + try: + self._socket.send(p) + except (OSError, TimeoutError) as e: + if self._ignore_exceptions: + return + else: + raise e self._stopped.wait(timeout=self._interval) if self._stopped.is_set() or self._socket.closed: break From 36c074d59e19168fe6e1582fd90a29a8265ac076 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 25 Oct 2023 08:48:56 +0200 Subject: [PATCH 083/122] Fix enum in UDS BMW definitions (#4150) --- scapy/contrib/automotive/bmw/definitions.py | 48 ++++++++++----------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/scapy/contrib/automotive/bmw/definitions.py b/scapy/contrib/automotive/bmw/definitions.py index 143827a5c38..3746fc9a296 100644 --- a/scapy/contrib/automotive/bmw/definitions.py +++ b/scapy/contrib/automotive/bmw/definitions.py @@ -252,30 +252,30 @@ def i2repr(self, pkt, x): class SVK_Entry(Packet): process_classes = { - "0x01": "HWEL", - "0x02": "HWAP", - "0x03": "HWFR", - "0x05": "CAFD", - "0x06": "BTLD", - "0x08": "SWFL", - "0x09": "SWFF", - "0x0A": "SWPF", - "0x0B": "ONPS", - "0x0F": "FAFP", - "0x1A": "TLRT", - "0x1B": "TPRG", - "0x07": "FLSL", - "0x0C": "IBAD", - "0x10": "FCFA", - "0x1C": "BLUP", - "0x1D": "FLUP", - "0xC0": "SWUP", - "0xC1": "SWIP", - "0xA0": "ENTD", - "0xA1": "NAVD", - "0xA2": "FCFN", - "0x04": "GWTB", - "0x0D": "SWFK", + 0x01: "HWEL", + 0x02: "HWAP", + 0x03: "HWFR", + 0x05: "CAFD", + 0x06: "BTLD", + 0x08: "SWFL", + 0x09: "SWFF", + 0x0A: "SWPF", + 0x0B: "ONPS", + 0x0F: "FAFP", + 0x1A: "TLRT", + 0x1B: "TPRG", + 0x07: "FLSL", + 0x0C: "IBAD", + 0x10: "FCFA", + 0x1C: "BLUP", + 0x1D: "FLUP", + 0xC0: "SWUP", + 0xC1: "SWIP", + 0xA0: "ENTD", + 0xA1: "NAVD", + 0xA2: "FCFN", + 0x04: "GWTB", + 0x0D: "SWFK", } """ HWEL - Hardware (Elektronik) - Hardware (Electronics) From 363d3766f53c3d55e92b0d51c5cdde7185733e3b Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Fri, 27 Oct 2023 15:14:25 +0200 Subject: [PATCH 084/122] More consistent sendpfast API (breaking) (#4157) --- scapy/sendrecv.py | 30 +++++++++++++++++------------- scapy/supersocket.py | 2 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py index 5c7a5a2b3c5..c62ecc6901e 100644 --- a/scapy/sendrecv.py +++ b/scapy/sendrecv.py @@ -485,15 +485,16 @@ def sendp(x, # type: _PacketIterable @conf.commands.register -def sendpfast(x, # type: _PacketIterable - pps=None, # type: Optional[float] - mbps=None, # type: Optional[float] - realtime=False, # type: bool - loop=None, # type: Optional[int] - file_cache=False, # type: bool - iface=None, # type: Optional[_GlobInterfaceType] - replay_args=None, # type: Optional[List[str]] - parse_results=False, # type: bool +def sendpfast(x: _PacketIterable, + pps: Optional[float] = None, + mbps: Optional[float] = None, + realtime: bool = False, + count: Optional[int] = None, + loop: int = 0, + file_cache: bool = False, + iface: Optional[_GlobInterfaceType] = None, + replay_args: Optional[List[str]] = None, + parse_results: bool = False, ): # type: (...) -> Optional[Dict[str, Any]] """Send packets at layer 2 using tcpreplay for performance @@ -501,8 +502,8 @@ def sendpfast(x, # type: _PacketIterable :param pps: packets per second :param mbps: MBits per second :param realtime: use packet's timestamp, bending time with real-time value - :param loop: number of times to process the packet list. 0 implies - infinite loop + :param loop: send the packet indefinitely (default 0) + :param count: number of packets to send (default None=1) :param file_cache: cache packets in RAM instead of reading from disk at each iteration :param iface: output interface @@ -523,8 +524,11 @@ def sendpfast(x, # type: _PacketIterable else: argv.append("--topspeed") - if loop is not None: - argv.append("--loop=%i" % loop) + if count: + assert not loop, "Can't use loop and count at the same time in sendpfast" + argv.append("--loop=%i" % count) + elif loop: + argv.append("--loop=0") if file_cache: argv.append("--preload-pcap") diff --git a/scapy/supersocket.py b/scapy/supersocket.py index 06f7019a78f..0a05749f214 100644 --- a/scapy/supersocket.py +++ b/scapy/supersocket.py @@ -214,7 +214,7 @@ def close(self): def sr(self, *args, **kargs): # type: (Any, Any) -> Tuple[SndRcvList, PacketList] from scapy import sendrecv - ans, unans = sendrecv.sndrcv(self, *args, **kargs) # type: SndRcvList, PacketList # noqa: E501 + ans, unans = sendrecv.sndrcv(self, *args, **kargs) return ans, unans def sr1(self, *args, **kargs): From ba51704fcfc60094da386c3614fb52c348d82020 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Tue, 24 Oct 2023 23:22:32 +0200 Subject: [PATCH 085/122] Improve answering machines, dns_resolve, renames --- doc/scapy/routing.rst | 1 + doc/scapy/usage.rst | 26 ++++--- scapy/ansmachine.py | 22 +++++- scapy/layers/dhcp.py | 2 +- scapy/layers/dns.py | 151 +++++++++++++++++++++--------------- scapy/layers/llmnr.py | 15 +++- scapy/layers/netbios.py | 2 +- test/answering_machines.uts | 14 +++- test/scapy/layers/dhcp.uts | 2 +- 9 files changed, 154 insertions(+), 81 deletions(-) diff --git a/doc/scapy/routing.rst b/doc/scapy/routing.rst index eec9dffbca1..8355ce0c352 100644 --- a/doc/scapy/routing.rst +++ b/doc/scapy/routing.rst @@ -6,6 +6,7 @@ Scapy maintains its own network stack, which is independent from the one of your It possesses its own *interfaces list*, *routing table*, *ARP cache*, *IPv6 neighbour* cache, *nameservers* config... and so on, all of which is configurable. Here are a few examples of where this is used:: + - When you use ``sr()/send()``, Scapy will use internally its own routing table (``conf.route``) in order to find which interface to use, and eventually send an ARP request. - When using ``dns_resolve()``, Scapy uses its own nameservers list (``conf.nameservers``) to perform the request - etc. diff --git a/doc/scapy/usage.rst b/doc/scapy/usage.rst index dc4aca70014..e78c0947f46 100644 --- a/doc/scapy/usage.rst +++ b/doc/scapy/usage.rst @@ -1434,28 +1434,32 @@ Visualizing the results in a list:: >>> res.nsummary(prn=lambda s,r: r.src, lfilter=lambda s,r: r.haslayer(ISAKMP) ) -DNS spoof ---------- +DNS server +---------- -See :class:`~scapy.layers.dns.DNS_am`:: +By default, ``dnsd`` uses a joker (IPv4 only): it answers to all unknown servers with the joker. See :class:`~scapy.layers.dns.DNS_am`:: - >>> dns_spoof(iface="tap0", joker="192.168.1.1") + >>> dnsd(iface="tap0", match={"google.com": "1.1.1.1"}, joker="192.168.1.1") -LLMNR spoof ------------ +You can also use ``relay=True`` to replace the joker behavior with a forward to a server included in ``conf.nameservers``. + +LLMNR server +------------ See :class:`~scapy.layers.llmnr.LLMNR_am`:: >>> conf.iface = "tap0" - >>> llmnr_spoof(iface="tap0", from_ip=Net("10.0.0.1/24")) + >>> llmnrd(iface="tap0", from_ip=Net("10.0.0.1/24")) -Netbios spoof -------------- +Note that ``llmnrd`` extends the ``dnsd`` API. + +Netbios server +-------------- See :class:`~scapy.layers.netbios.NBNS_am`:: - >>> nbns_spoof(iface="eth0") # With local IP - >>> nbns_spoof(iface="eth0", ip="192.168.122.17") # With some other IP + >>> nbnsd(iface="eth0") # With local IP + >>> nbnsd(iface="eth0", ip="192.168.122.17") # With some other IP Node status request (get NetbiosName from IP) --------------------------------------------- diff --git a/scapy/ansmachine.py b/scapy/ansmachine.py index ee8b428b52d..e821df54014 100644 --- a/scapy/ansmachine.py +++ b/scapy/ansmachine.py @@ -13,6 +13,7 @@ import abc import functools +import threading import socket import warnings @@ -225,12 +226,14 @@ def sniff_bg(self): class AnsweringMachineTCP(AnsweringMachine[Packet]): """ An answering machine that use the classic socket.socket to - answer multiple clients + answer multiple TCP clients """ + TYPE = socket.SOCK_STREAM + def parse_options(self, port=80, cls=conf.raw_layer): # type: (int, Type[Packet]) -> None self.port = port - self.cls = conf.raw_layer + self.cls = cls def close(self): # type: () -> None @@ -239,7 +242,7 @@ def close(self): def sniff(self): # type: () -> None from scapy.supersocket import StreamSocket - ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + ssock = socket.socket(socket.AF_INET, self.TYPE) ssock.bind( (get_if_addr(self.optsniff.get("iface", conf.iface)), self.port)) ssock.listen() @@ -267,6 +270,19 @@ def sniff(self): self.close() ssock.close() + def sniff_bg(self): + # type: () -> None + self.sniffer = threading.Thread(target=self.sniff) # type: ignore + self.sniffer.start() + def make_reply(self, req, address=None): # type: (Packet, Optional[Any]) -> Packet return req + + +class AnsweringMachineUDP(AnsweringMachineTCP): + """ + An answering machine that use the classic socket.socket to + answer multiple UDP clients + """ + TYPE = socket.SOCK_DGRAM diff --git a/scapy/layers/dhcp.py b/scapy/layers/dhcp.py index ce493813367..8ff0e6ec786 100644 --- a/scapy/layers/dhcp.py +++ b/scapy/layers/dhcp.py @@ -593,7 +593,7 @@ def parse_options(self, network="192.168.1.0/24", gw="192.168.1.1", nameserver=None, - domain="localnet", + domain=None, renewal_time=60, lease_time=1800): """ diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 6bed0be230c..606fde284f9 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -1072,13 +1072,13 @@ def mysummary(self): name = "" if self.qr: type = "Ans" - if self.an and isinstance(self.an, DNSRR): - name = ' "%s"' % self.an[0].rdata + if self.an and isinstance(self.an[0], DNSRR): + name = ' %s' % self.an[0].rdata else: type = "Qry" - if self.qd and isinstance(self.qd, DNSQR): - name = ' "%s"' % self.qd[0].qname - return 'DNS %s%s ' % (type, name) + if self.qd and isinstance(self.qd[0], DNSQR): + name = ' %s' % self.qd[0].qname + return 'DNS %s%s' % (type, name) def post_build(self, pkt, pay): if isinstance(self.underlayer, TCP) and self.length is None: @@ -1129,13 +1129,16 @@ def pre_dissect(self, s): @conf.commands.register -def dns_resolve(qname, qtype="A", raw=False, verbose=1, **kwargs): +def dns_resolve(qname, qtype="A", raw=False, verbose=1, timeout=3, **kwargs): """ Perform a simple DNS resolution using conf.nameservers with caching :param qname: the name to query :param qtype: the type to query (default A) :param raw: return the whole DNS packet (default False) + :param verbose: show verbose errors + :param timeout: seconds until timeout (per server) + :raise TimeoutError: if no DNS servers were reached in time. """ # Unify types qtype = DNSQR.qtype.any2i_one(None, qtype) @@ -1149,7 +1152,7 @@ def dns_resolve(qname, qtype="A", raw=False, verbose=1, **kwargs): if answer: return answer - kwargs.setdefault("timeout", 3) + kwargs.setdefault("timeout", timeout) kwargs.setdefault("verbose", 0) res = None for nameserver in conf.nameservers: @@ -1203,7 +1206,8 @@ def dns_resolve(qname, qtype="A", raw=False, verbose=1, **kwargs): # Cache it _dns_cache[cache_ident] = answer return answer - return None + else: + raise TimeoutError @conf.commands.register @@ -1247,14 +1251,15 @@ def dyndns_del(nameserver, name, type="ALL", ttl=10): class DNS_am(AnsweringMachine): - function_name = "dns_spoof" + function_name = "dnsd" filter = "udp port 53" - cls = DNS # We use this automaton for llmnr_spoof + cls = DNS # We also use this automaton for llmnrd def parse_options(self, joker=None, match=None, srvmatch=None, joker6=False, + relay=False, from_ip=None, from_ip6=None, src_ip=None, @@ -1265,40 +1270,50 @@ def parse_options(self, joker=None, Set to False to disable, None to mirror the interface's IP. :param joker6: default IPv6 for unresolved domains (Default: False) set to False to disable, None to mirror the interface's IPv6. + :param relay: relay unresolved domains to conf.nameservers (Default: False). :param match: a dictionary of {name: val} where name is a string representing a domain name (A, AAAA) and val is a tuple of 2 elements, each - representing an IP or a list of IPs + representing an IP or a list of IPs. If val is a single element, + (A, None) is assumed. :param srvmatch: a dictionary of {name: (port, target)} used for SRV :param from_ip: an source IP to filter. Can contain a netmask :param from_ip6: an source IPv6 to filter. Can contain a netmask :param ttl: the DNS time to live (in seconds) - :param src_ip: + :param src_ip: override the source IP :param src_ip6: Example: - >>> dns_spoof(joker="192.168.0.2", iface="eth0") - >>> dns_spoof(match={ + $ sudo iptables -I OUTPUT -p icmp --icmp-type 3/3 -j DROP + >>> dnsd(match={"google.com": "1.1.1.1"}, joker="192.168.0.2", iface="eth0") + >>> dnsd(srvmatch={ ... "_ldap._tcp.dc._msdcs.DOMAIN.LOCAL.": (389, "srv1.domain.local") ... }) """ + def normv(v): + if isinstance(v, (tuple, list)) and len(v) == 2: + return v + elif isinstance(v, str): + return (v, None) + else: + raise ValueError("Bad match value: '%s'" % repr(v)) + + def normk(k): + k = bytes_encode(k).lower() + if not k.endswith(b"."): + k += b"." + return k if match is None: self.match = {} else: - assert all(isinstance(x, (tuple, list)) for x in match.values()), ( - "'match' values must be a tuple of 2 elements: ('', '')" - ". They can be None" - ) - self.match = {bytes_encode(k): v for k, v in match.items()} + self.match = {normk(k): normv(v) for k, v in match.items()} if srvmatch is None: self.srvmatch = {} else: - assert all(isinstance(x, (tuple, list)) for x in srvmatch.values()), ( - "'srvmatch' values must be a tuple of 2 elements: (port, 'target')" - ) - self.srvmatch = {bytes_encode(k): v for k, v in srvmatch.items()} + self.srvmatch = {normk(k): normv(v) for k, v in srvmatch.items()} self.joker = joker self.joker6 = joker6 + self.relay = relay if isinstance(from_ip, str): self.from_ip = Net(from_ip) else: @@ -1341,51 +1356,65 @@ def make_reply(self, req): if rq.qtype == 28: # AAAA try: - rdata = self.match[rq.qname][1] + rdata = self.match[rq.qname.lower()][1] except KeyError: - if self.joker6 is False: - return - rdata = self.joker6 or get_if_addr6( - self.optsniff.get("iface", conf.iface) - ) + if self.relay or self.joker6 is False: + rdata = None + else: + rdata = self.joker6 or get_if_addr6( + self.optsniff.get("iface", conf.iface) + ) elif rq.qtype == 1: # A try: - rdata = self.match[rq.qname][0] + rdata = self.match[rq.qname.lower()][0] except KeyError: - if self.joker is False: - return - rdata = self.joker or get_if_addr( - self.optsniff.get("iface", conf.iface) - ) - if rdata is None: - # Ignore None - return - # Common A and AAAA - if not isinstance(rdata, list): - rdata = [rdata] - ans.extend([ - DNSRR(rrname=rq.qname, ttl=self.ttl, rdata=x, type=rq.qtype) - for x in rdata - ]) + if self.relay or self.joker is False: + rdata = None + else: + rdata = self.joker or get_if_addr( + self.optsniff.get("iface", conf.iface) + ) + if rdata is not None: + # Common A and AAAA + if not isinstance(rdata, list): + rdata = [rdata] + ans.extend([ + DNSRR(rrname=rq.qname, ttl=self.ttl, rdata=x, type=rq.qtype) + for x in rdata + ]) + continue # next elif rq.qtype == 33: # SRV try: - port, target = self.srvmatch[rq.qname] + port, target = self.srvmatch[rq.qname.lower()] + ans.append(DNSRRSRV( + rrname=rq.qname, + port=port, + target=target, + weight=100, + ttl=self.ttl + )) + continue # next except KeyError: - return - ans.append(DNSRRSRV( - rrname=rq.qname, - port=port, - target=target, - weight=100, - ttl=self.ttl - )) - else: - # Not handled - continue - # Common: All - if not ans: - return - resp /= self.cls(id=req.id, qr=1, qd=req.qd, an=ans) + # No result + pass + # It it arrives here, there is currently no answer + if self.relay: + # Relay mode ? + try: + _rslv = dns_resolve(rq.qname, qtype=rq.qtype) + if _rslv is not None: + ans.append(_rslv) + continue # next + except TimeoutError: + pass + # Error + break + else: + # All rq were answered + resp /= self.cls(id=req.id, qr=1, qd=req.qd, an=ans) + return resp + # An error happened + resp /= self.cls(id=req.id, qr=1, qd=req.qd, rcode=3) return resp diff --git a/scapy/layers/llmnr.py b/scapy/layers/llmnr.py index 89815e5f200..fef6a97c58a 100644 --- a/scapy/layers/llmnr.py +++ b/scapy/layers/llmnr.py @@ -14,7 +14,13 @@ import struct -from scapy.fields import BitEnumField, BitField, ShortField +from scapy.fields import ( + BitEnumField, + BitField, + DestField, + DestIP6Field, + ShortField, +) from scapy.packet import Packet, bind_layers, bind_bottom_up from scapy.compat import orb from scapy.layers.inet import UDP @@ -88,9 +94,14 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): bind_bottom_up(UDP, _LLMNR, sport=5355) bind_layers(UDP, _LLMNR, sport=5355, dport=5355) +DestField.bind_addr(LLMNRQuery, _LLMNR_IPv4_mcast_addr, dport=5355) +DestField.bind_addr(LLMNRResponse, _LLMNR_IPv4_mcast_addr, dport=5355) +DestIP6Field.bind_addr(LLMNRQuery, _LLMNR_IPv6_mcast_Addr, dport=5355) +DestIP6Field.bind_addr(LLMNRResponse, _LLMNR_IPv6_mcast_Addr, dport=5355) + class LLMNR_am(DNS_am): - function_name = "llmnr_spoof" + function_name = "llmnrd" filter = "udp port 5355" cls = LLMNRQuery diff --git a/scapy/layers/netbios.py b/scapy/layers/netbios.py index 678fa6c4f7c..07af77b4365 100644 --- a/scapy/layers/netbios.py +++ b/scapy/layers/netbios.py @@ -355,7 +355,7 @@ def post_build(self, pkt, pay): class NBNS_am(AnsweringMachine): - function_name = "nbns_spoof" + function_name = "nbnsd" filter = "udp port 137" sniff_options = {"store": 0} diff --git a/test/answering_machines.uts b/test/answering_machines.uts index bb80f66c391..e28de57daf2 100644 --- a/test/answering_machines.uts +++ b/test/answering_machines.uts @@ -37,7 +37,8 @@ def check_DHCP_am_reply(packet): test_am(DHCP_am, Ether()/IP()/UDP()/BOOTP(op=1)/DHCP(options=[('message-type', 'request')]), - check_DHCP_am_reply) + check_DHCP_am_reply, + domain="localnet") = ARP_am @@ -73,6 +74,17 @@ test_am(DNS_am, check_DNS_am_reply, joker="192.168.1.1") +def check_DNS_am_reply2(packet): + assert DNS in packet and packet[DNS].ancount == 2 + assert packet[DNS].an[0].rdata == "128.0.0.1" + assert packet[DNS].an[1].rdata == "::1" + +test_am(DNS_am, + IP(b'E\x00\x00H\x00\x01\x00\x00@\x11|\xa2\x7f\x00\x00\x01\x7f\x00\x00\x01\x005\x005\x004\xe8\x9a\x00\x00\x01\x00\x00\x02\x00\x00\x00\x00\x00\x00\x06gaagle\x03com\x00\x00\x01\x00\x01\x06google\x03com\x00\x00\x1c\x00\x01'), + check_DNS_am_reply2, + match={"google.com": ("127.0.0.1", "::1"), "gaagle.com": "128.0.0.1"}, + joker=False) + = DHCPv6_am - Basic Instantiaion ~ osx netaccess a = DHCPv6_am() diff --git a/test/scapy/layers/dhcp.uts b/test/scapy/layers/dhcp.uts index 3010d3e0933..3d8dcda99c5 100644 --- a/test/scapy/layers/dhcp.uts +++ b/test/scapy/layers/dhcp.uts @@ -121,5 +121,5 @@ assert DHCPRevOptions['static-routes'][0] == 33 assert dhcpd import IPython -assert IPython.lib.pretty.pretty(dhcpd) == '' +assert IPython.lib.pretty.pretty(dhcpd) == '' From 2f86cff6e1bec0c176b9469dd9054e3bdce3d6a3 Mon Sep 17 00:00:00 2001 From: devrim-ayyildiz <39125734+devrim-ayyildiz@users.noreply.github.com> Date: Wed, 1 Nov 2023 07:13:08 +0100 Subject: [PATCH 086/122] Pass the fd to underlying NativeCANSocket (#4158) ISOTPSoftSocket should pass the fd (CanFD support) to the NativeCANSocket instance it creates. Otherwise it will always be created as non CanFD. --- scapy/contrib/isotp/isotp_soft_socket.py | 2 +- test/contrib/isotp_soft_socket.uts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/scapy/contrib/isotp/isotp_soft_socket.py b/scapy/contrib/isotp/isotp_soft_socket.py index 3b8dc7f80ce..119917e5f44 100644 --- a/scapy/contrib/isotp/isotp_soft_socket.py +++ b/scapy/contrib/isotp/isotp_soft_socket.py @@ -133,7 +133,7 @@ def __init__(self, if LINUX and isinstance(can_socket, str): from scapy.contrib.cansocket_native import NativeCANSocket - can_socket = NativeCANSocket(can_socket) + can_socket = NativeCANSocket(can_socket, fd=fd) elif isinstance(can_socket, str): raise Scapy_Exception("Provide a CANSocket object instead") diff --git a/test/contrib/isotp_soft_socket.uts b/test/contrib/isotp_soft_socket.uts index b2b7295805d..ece7ef5f621 100644 --- a/test/contrib/isotp_soft_socket.uts +++ b/test/contrib/isotp_soft_socket.uts @@ -11,6 +11,8 @@ from scapy.layers.can import * from scapy.contrib.isotp import * from scapy.contrib.isotp.isotp_soft_socket import TimeoutScheduler from test.testsocket import TestSocket, cleanup_testsockets +with open(scapy_path("test/contrib/automotive/interface_mockup.py")) as f: + exec(f.read()) = Redirect logging import logging @@ -58,6 +60,18 @@ assert sniffed[0]['ISOTP'].rx_ext_address == 0xEA + ISOTPSoftSocket tests += CAN socket FD +~ not_pypy needs_root linux vcan_socket + +with ISOTPSoftSocket(iface0, tx_id=0x641, rx_id=0x241, fd=True) as s: + assert s.impl.can_socket.fd == True + += CAN socket non-FD +~ not_pypy needs_root linux vcan_socket + +with ISOTPSoftSocket(iface0, tx_id=0x641, rx_id=0x241) as s: + assert s.impl.can_socket.fd == False + = Single-frame receive with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241) as s: From 0474c37bf1d147c969173d52ab3ac76d2404d981 Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Thu, 2 Nov 2023 09:56:57 +0300 Subject: [PATCH 087/122] DHCPv4: add the rapid commit option (#4166) --- scapy/layers/dhcp.py | 1 + test/scapy/layers/dhcp.uts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/scapy/layers/dhcp.py b/scapy/layers/dhcp.py index 8ff0e6ec786..4875dbf1e4a 100644 --- a/scapy/layers/dhcp.py +++ b/scapy/layers/dhcp.py @@ -303,6 +303,7 @@ def randval(self): 77: "user_class", 78: "slp_service_agent", 79: "slp_service_scope", + 80: "rapid_commit", 81: "client_FQDN", 82: "relay_agent_information", 85: IPField("nds-server", "0.0.0.0"), diff --git a/test/scapy/layers/dhcp.uts b/test/scapy/layers/dhcp.uts index 3d8dcda99c5..2202eb262f4 100644 --- a/test/scapy/layers/dhcp.uts +++ b/test/scapy/layers/dhcp.uts @@ -47,8 +47,8 @@ assert s3 == b'E\x00\x01=\x00\x01\x00\x00@\x11{\xad\x7f\x00\x00\x01\x7f\x00\x00\ s4 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("mud-url", "https://example.org"), ("captive-portal", "https://example.com"), ("ipv6-only-preferred", 0xffffffff), "end"])) assert s4 == b"E\x00\x01=\x00\x01\x00\x00@\x11{\xad\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01)L\xd7\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0000:01:02:03:04:0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc\xa1\x13https://example.orgr\x13https://example.com\x6c\x04\xff\xff\xff\xff\xff" -s5 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("classless_static_routes", "192.168.123.4/32:10.0.0.1", "169.254.254.0/24:10.0.1.2"), "end"])) -assert s5 == b'E\x00\x01 \x00\x01\x00\x00@\x11{\xca\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01\x0c\xabQ\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0000:01:02:03:04:0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Scy\x11 \xc0\xa8{\x04\n\x00\x00\x01\x18\xa9\xfe\xfe\n\x00\x01\x02\xff' +s5 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("classless_static_routes", "192.168.123.4/32:10.0.0.1", "169.254.254.0/24:10.0.1.2"), ("rapid_commit", b""), "end"])) +assert s5 == b'E\x00\x01"\x00\x01\x00\x00@\x11{\xc8\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01\x0e\xaa\xfd\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0000:01:02:03:04:0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Scy\x11 \xc0\xa8{\x04\n\x00\x00\x01\x18\xa9\xfe\xfe\n\x00\x01\x02P\x00\xff' = DHCP - fuzz @@ -86,11 +86,13 @@ assert p4[DHCP].options[2] == ("ipv6-only-preferred", 0xffffffff) p5 = IP(s5) assert DHCP in p5 assert p5[DHCP].options[0] == ("classless_static_routes", ["192.168.123.4/32:10.0.0.1", "169.254.254.0/24:10.0.1.2"]) +assert p5[DHCP].options[1] == ("rapid_commit", b"") repr(DHCP(b"\x01\x00")) assert DHCP(b"\x01\x00").options == [b"\x01\x00"] assert DHCP(b"\x28\x00").options == [("NIS_domain", b"")] assert DHCP(b"\x37\x00").options == [("param_req_list", [])] +assert DHCP(b"\x50\x00").options == [("rapid_commit", b"")] assert DHCP(b"\x79\x00").options == [("classless_static_routes", [])] assert DHCP(b"\x01\x0C\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b").options == [("subnet_mask", "0.1.2.3", "4.5.6.7", "8.9.10.11")] From cb4c7155a088b9f34d979eb3293942c93e3a62d0 Mon Sep 17 00:00:00 2001 From: ntheule <91799347+ntheule@users.noreply.github.com> Date: Fri, 10 Nov 2023 13:52:00 -0500 Subject: [PATCH 088/122] Intro doc edits (#4162) * Removed extra "and" * Remove misused parentheses * Consistent use of contractions in sentence * Changing to be less confusing * Fixing typos * Changes for better readability * Adding missing comma * Add missing "the" * Removing extra "the" --- doc/scapy/introduction.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/scapy/introduction.rst b/doc/scapy/introduction.rst index b960e789c9d..ab8b97cf1f6 100644 --- a/doc/scapy/introduction.rst +++ b/doc/scapy/introduction.rst @@ -7,7 +7,7 @@ Introduction About Scapy =========== -Scapy is a Python program that enables the user to send, sniff and dissect and forge network packets. This capability allows construction of tools that can probe, scan or attack networks. +Scapy is a Python program that enables the user to send, sniff, dissect and forge network packets. This capability allows construction of tools that can probe, scan or attack networks. In other words, Scapy is a powerful interactive packet manipulation program. It is able to forge or decode packets of a wide number of protocols, send them on the wire, capture them, match requests and replies, and much more. Scapy can easily handle most classical tasks like scanning, tracerouting, probing, unit tests, attacks or network discovery. It can replace hping, arpspoof, arp-sk, arping, p0f and even some parts of Nmap, tcpdump, and tshark. @@ -16,38 +16,38 @@ In other words, Scapy is a powerful interactive packet manipulation program. It Scapy also performs very well on a lot of other specific tasks that most other tools can't handle, like sending invalid frames, injecting your own 802.11 frames, combining techniques (VLAN hopping+ARP cache poisoning, VOIP decoding on WEP encrypted channel, ...), etc. -The idea is simple. Scapy mainly does two things: sending packets and receiving answers. You define a set of packets, it sends them, receives answers, matches requests with answers and returns a list of packet couples (request, answer) and a list of unmatched packets. This has the big advantage over tools like Nmap or hping that an answer is not reduced to (open/closed/filtered), but is the whole packet. +The idea is simple. Scapy mainly does two things: sending packets and receiving answers. You define a set of packets, it sends them, receives answers, matches requests with answers and returns a list of packet couples (request, answer) and a list of unmatched packets. This has the big advantage over tools like Nmap or hping that an answer is not reduced to open, closed, or filtered, but is the whole packet. -On top of this can be build more high level functions, for example, one that does traceroutes and give as a result only the start TTL of the request and the source IP of the answer. One that pings a whole network and gives the list of machines answering. One that does a portscan and returns a LaTeX report. +On top of this can be built more high level functions. For example, one that does traceroutes and give as a result only the start TTL of the request and the source IP of the answer. One that pings a whole network and gives the list of machines answering. One that does a portscan and returns a LaTeX report. What makes Scapy so special =========================== -First, with most other networking tools, you won't build something the author did not imagine. These tools have been built for a specific goal and can't deviate much from it. For example, an ARP cache poisoning program won't let you use double 802.1q encapsulation. Or try to find a program that can send, say, an ICMP packet with padding (I said *padding*, not *payload*, see?). In fact, each time you have a new need, you have to build a new tool. +First, with most other networking tools, you won't build something the author didn't imagine. These tools have been built for a specific goal and can't deviate much from it. For example, an ARP cache poisoning program won't let you use double 802.1q encapsulation. Or try to find a program that can send, say, an ICMP packet with padding (I said *padding*, not *payload*, see?). In fact, each time you have a new need, you have to build a new tool. Second, they usually confuse decoding and interpreting. Machines are good at decoding and can help human beings with that. Interpretation is reserved for human beings. Some programs try to mimic this behavior. For instance they say "*this port is open*" instead of "*I received a SYN-ACK*". Sometimes they are right. Sometimes not. It's easier for beginners, but when you know what you're doing, you keep on trying to deduce what really happened from the program's interpretation to make your own, which is hard because you lost a big amount of information. And you often end up using ``tcpdump -xX`` to decode and interpret what the tool missed. -Third, even programs which only decode do not give you all the information they received. The network's vision they give you is the one their author thought was sufficient. But it is not complete, and you have a bias. For instance, do you know a tool that reports the Ethernet padding? +Third, even programs which only decode do not give you all the information they received. The vision of the network they give you is the one their author thought was sufficient. But it is not complete, and you have a bias. For instance, do you know a tool that reports the Ethernet padding? -Scapy tries to overcome those problems. It enables you to build exactly the packets you want. Even if I think stacking a 802.1q layer on top of TCP has no sense, it may have some for somebody else working on some product I don't know. Scapy has a flexible model that tries to avoid such arbitrary limits. You're free to put any value you want in any field you want and stack them like you want. You're an adult after all. +Scapy tries to overcome those problems. It enables you to build exactly the packets you want. Even if I think stacking an 802.1q layer on top of TCP has no sense, it may have some for somebody else working on some product I don't know. Scapy has a flexible model that tries to avoid such arbitrary limits. You're free to put any value you want in any field you want and stack them like you want. You're an adult after all. In fact, it's like building a new tool each time, but instead of dealing with a hundred line C program, you only write 2 lines of Scapy. -After a probe (scan, traceroute, etc.) Scapy always gives you the full decoded packets from the probe, before any interpretation. That means that you can probe once and interpret many times, ask for a traceroute and look at the padding for instance. +After a probe (scan, traceroute, etc.) Scapy always gives you the full decoded packets from the probe, before any interpretation. That means that you can probe once and interpret many times. Ask for a traceroute and look at the padding, for instance. Fast packet design ------------------ Other tools stick to the **program-that-you-run-from-a-shell** paradigm. The result is an awful syntax to describe a packet. For these tools, the solution adopted uses a higher but less powerful description, in the form of scenarios imagined by the tool's author. As an example, only the IP address must be given to a port scanner to trigger the **port scanning** scenario. Even if the scenario is tweaked a bit, you still are stuck to a port scan. -Scapy's paradigm is to propose a Domain Specific Language (DSL) that enables a powerful and fast description of any kind of packet. Using the Python syntax and a Python interpreter as the DSL syntax and interpreter has many advantages: there is no need to write a separate interpreter, users don't need to learn yet another language and they benefit from a complete, concise and very powerful language. +Scapy's paradigm is to propose a Domain Specific Language (DSL) that enables a powerful and fast description of any kind of packet. Using the Python syntax and a Python interpreter as the DSL syntax and interpreter has many advantages: there is no need to write a separate interpreter, users don't need to learn yet another language, and they benefit from a complete, concise, and very powerful language. -Scapy enables the user to describe a packet or set of packets as layers that are stacked one upon another. Fields of each layer have useful default values that can be overloaded. Scapy does not oblige the user to use predetermined methods or templates. This alleviates the requirement of writing a new tool each time a different scenario is required. In C, it may take an average of 60 lines to describe a packet. With Scapy, the packets to be sent may be described in only a single line with another line to print the result. 90\% of the network probing tools can be rewritten in 2 lines of Scapy. +Scapy enables the user to describe a packet or set of packets as layers that are stacked one upon another. Fields of each layer have useful default values that can be overloaded. Scapy does not oblige the user to use predetermined methods or templates. This alleviates the requirement of writing a new tool each time a different scenario is required. In C, it may take an average of 60 lines to describe a packet. With Scapy, the packets to be sent may be described in only a single line, with another line to print the result. 90\% of network probing tools can be rewritten in 2 lines of Scapy. Probe once, interpret many -------------------------- -Network discovery is blackbox testing. When probing a network, many stimuli are sent while only a few of them are answered. If the right stimuli are chosen, the desired information may be obtained by the responses or the lack of responses. Unlike many tools, Scapy gives all the information, i.e. all the stimuli sent and all the responses received. Examination of this data will give the user the desired information. When the dataset is small, the user can just dig for it. In other cases, the interpretation of the data will depend on the point of view taken. Most tools choose the viewpoint and discard all the data not related to that point of view. Because Scapy gives the complete raw data, that data may be used many times allowing the viewpoint to evolve during analysis. For example, a TCP port scan may be probed and the data visualized as the result of the port scan. The data could then also be visualized with respect to the TTL of response packet. A new probe need not be initiated to adjust the viewpoint of the data. +Network discovery is blackbox testing. When probing a network, many stimuli are sent, while only a few of them are answered. If the right stimuli are chosen, the desired information may be obtained by the responses or the lack of responses. Unlike many tools, Scapy gives all the information, i.e. all the stimuli sent and all the responses received. Examination of this data will give the user the desired information. When the dataset is small, the user can just dig for it. In other cases, the interpretation of the data will depend on the point of view taken. Most tools choose the viewpoint and discard all the data not related to that point of view. Because Scapy gives the complete raw data, that data may be used many times allowing the viewpoint to evolve during analysis. For example, a TCP port scan may be probed and the data visualized as the result of the port scan. The data could then also be visualized with respect to the TTL of the response packet. A new probe need not be initiated to adjust the viewpoint of the data. .. image:: graphics/scapy-concept.* :scale: 80 @@ -55,9 +55,9 @@ Network discovery is blackbox testing. When probing a network, many stimuli are Scapy decodes, it does not interpret ------------------------------------ -A common problem with network probing tools is they try to interpret the answers received instead of only decoding and giving facts. Reporting something like **Received a TCP Reset on port 80** is not subject to interpretation errors. Reporting **Port 80 is closed** is an interpretation that may be right most of the time but wrong in some specific contexts the tool's author did not imagine. For instance, some scanners tend to report a filtered TCP port when they receive an ICMP destination unreachable packet. This may be right, but in some cases, it means the packet was not filtered by the firewall but rather there was no host to forward the packet to. +A common problem with network probing tools is they try to interpret the answers received instead of only decoding and giving facts. Reporting something like **Received a TCP Reset on port 80** is not subject to interpretation errors. Reporting **Port 80 is closed** is an interpretation that may be right most of the time but wrong in some specific contexts the tool's author did not imagine. For instance, some scanners tend to report a filtered TCP port when they receive an ICMP destination unreachable packet. This may be right, but in some cases, it means the packet was not filtered by the firewall, but rather there was no host to forward the packet to. -Interpreting results can help users that don't know what a port scan is but it can also make more harm than good, as it injects bias into the results. What can tend to happen is that so that they can do the interpretation themselves, knowledgeable users will try to reverse engineer the tool's interpretation to derive the facts that triggered that interpretation. Unfortunately, much information is lost in this operation. +Interpreting results can help users that don't know what a port scan is, but it can also make more harm than good, as it injects bias into the results. What can tend to happen is that knowledgeable users will try to reverse engineer the tool's interpretation to derive the facts that triggered that interpretation, so that they can do the interpretation themselves. Unfortunately, much information is lost in this operation. Quick demo ========== From c4b6ef76ee3a1c00190008849f676b2bd212dca8 Mon Sep 17 00:00:00 2001 From: gidder Date: Thu, 2 Nov 2023 19:52:57 +0100 Subject: [PATCH 089/122] tacacs: support unencrypted packets --- scapy/contrib/tacacs.py | 7 +++---- test/contrib/tacacs.uts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/scapy/contrib/tacacs.py b/scapy/contrib/tacacs.py index ed1ca0640be..814136dd87d 100755 --- a/scapy/contrib/tacacs.py +++ b/scapy/contrib/tacacs.py @@ -362,7 +362,8 @@ def post_dissect(self, pay): if self.flags == 0: pay = obfuscate(pay, SECRET, self.session_id, self.version, self.seq) # noqa: E501 - return pay + + return pay class TacacsHeader(TacacsClientPacket): @@ -420,11 +421,9 @@ def post_build(self, p, pay): p = p[:-4] + struct.pack('!I', len(pay)) if self.flags == 0: - pay = obfuscate(pay, SECRET, self.session_id, self.version, self.seq) # noqa: E501 - return p + pay - return p + return p + pay def hashret(self): return struct.pack('I', self.session_id) diff --git a/test/contrib/tacacs.uts b/test/contrib/tacacs.uts index 011e78bf706..94f07ad6a42 100644 --- a/test/contrib/tacacs.uts +++ b/test/contrib/tacacs.uts @@ -1,3 +1,8 @@ +# TACACS+ related regression tests +# +# Type the following command to launch the tests: +# $ test/run_tests -P "load_contrib('tacacs')" -t test/contrib/tacacs.uts + + Tacacs+ header = default instantiation @@ -188,3 +193,14 @@ scapy.contrib.tacacs.SECRET = 'foobar' pkt = Ether(b'\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x009\x00\x01\x00\x00@\x06|\xbc\x7f\x00\x00\x01\x7f\x00\x00\x01\x001\x001\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00/,\x00\x00\xc0\x03\x02\x00\x1aM\x05\r\x00\x00\x00\x05S)\x9b\xb4\x92') pkt.status == 1 ++ Unencrypted Authentication + += instantiation + +pkt = IP()/TCP(dport=49)/TacacsHeader(seq=1, flags=1, session_id=2424164486, length=28)/TacacsAuthenticationStart(user_len=5, port_len=4, rem_addr_len=11, data_len=0, user='scapy', port='tty2', rem_addr='172.10.10.1') +raw(pkt) == b"E\x00\x00P\x00\x01\x00\x00@\x06|\xa5\x7f\x00\x00\x01\x7f\x00\x00\x01\x001\x001\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00sG\x00\x00\xc0\x01\x01\x01\x90}\xd0\x86\x00\x00\x00\x1c\x01\x01\x01\x01\x05\x04\x0b\x00scapytty2172.10.10.1" + += dissection + +pkt = Ether(b'\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x00P\x00\x01\x00\x00@\x06|\xa5\x7f\x00\x00\x01\x7f\x00\x00\x01\x001\x001\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00sG\x00\x00\xc0\x01\x01\x01\x90}\xd0\x86\x00\x00\x00\x1c\x01\x01\x01\x01\x05\x04\x0b\x00scapytty2172.10.10.1') +pkt.user == b'scapy' and pkt.port == b'tty2' From 42badf86db5f9d334f4333306df445094b71e4ad Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 12 Nov 2023 15:50:38 +0100 Subject: [PATCH 090/122] ReadTheDocs: fix displayed Scapy version --- .readthedocs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index ecf566c59b4..026135b6fc7 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -12,7 +12,13 @@ build: os: ubuntu-20.04 tools: python: "3.9" + # To show the correct Scapy version, we must unshallow + # https://docs.readthedocs.io/en/stable/build-customization.html#unshallow-git-clone + jobs: + post_checkout: + - git fetch --unshallow || true +# https://docs.readthedocs.io/en/stable/config-file/v2.html#python python: install: - method: pip From bc6eb8f37886ccdbc6d17415701f8d7b9fb3fb8b Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 12 Nov 2023 16:53:08 +0100 Subject: [PATCH 091/122] Update build versions --- .github/workflows/unittests.yml | 21 +++++++++++---------- .readthedocs.yml | 4 ++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 01835687ee8..5296d7142f2 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -20,7 +20,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - name: Install tox run: pip install tox - name: Run flake8 tests @@ -32,15 +32,16 @@ jobs: - name: Run gitarchive check run: tox -e gitarchive docs: + # 'runs-on' and 'python-version' should match the ones defined in .readthedocs.yml name: Build doc - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout Scapy uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Install tox run: pip install tox - name: Build docs @@ -69,7 +70,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python: ["3.7", "3.8", "3.9", "3.10"] + python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] mode: [non_root] installmode: [''] flags: [" -K scanner"] @@ -77,7 +78,7 @@ jobs: include: # Linux root tests - os: ubuntu-latest - python: "3.10" + python: "3.11" mode: root flags: " -K scanner" # PyPy tests: root only @@ -87,18 +88,18 @@ jobs: flags: " -K scanner" # Libpcap test - os: ubuntu-latest - python: "3.10" + python: "3.11" mode: root installmode: 'libpcap' flags: " -K scanner" # macOS tests - os: macos-12 - python: "3.10" + python: "3.11" mode: both flags: " -K scanner" # Scanner tests - os: ubuntu-latest - python: "3.10" + python: "3.11" mode: root allow-failure: 'true' flags: " -k scanner" @@ -108,7 +109,7 @@ jobs: allow-failure: 'true' flags: " -k scanner" - os: macos-12 - python: "3.10" + python: "3.11" mode: both allow-failure: 'true' flags: " -k scanner" @@ -138,7 +139,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - name: Install tox run: pip install tox # pyca/cryptography's CI installs cryptography diff --git a/.readthedocs.yml b/.readthedocs.yml index 026135b6fc7..6c006d87efd 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,9 +9,9 @@ formats: - pdf build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.9" + python: "3.12" # To show the correct Scapy version, we must unshallow # https://docs.readthedocs.io/en/stable/build-customization.html#unshallow-git-clone jobs: From a0dc5025f2c5039600ec54872a8209a94d4563a3 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 12 Nov 2023 16:55:09 +0100 Subject: [PATCH 092/122] Support 3.11 and 3.12 --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 15d52fc90eb..0aa486a4795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,8 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Security", "Topic :: System :: Networking", "Topic :: System :: Networking :: Monitoring", From 0d9cca1a89f5f426d7c05236f6484b940690aef7 Mon Sep 17 00:00:00 2001 From: Gilad Beeri Date: Tue, 14 Nov 2023 01:49:33 +0200 Subject: [PATCH 093/122] 802.11: support badly implemented Country Information padding (#4133) * fixed parsing of 802.11 frames with Country Information element tags that don't comply with the standard by avoiding the necessary padding byte that should make the tag length even * Fix PEP8 --------- Co-authored-by: gpotter2 <10530980+gpotter2@users.noreply.github.com> --- scapy/layers/dot11.py | 9 ++++++++- test/regression.uts | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/scapy/layers/dot11.py b/scapy/layers/dot11.py index 1cc38b6f8e9..8c95a019c6f 100644 --- a/scapy/layers/dot11.py +++ b/scapy/layers/dot11.py @@ -1282,7 +1282,14 @@ class Dot11EltCountry(Dot11Elt): # When this extension is last, padding appears to be omitted ConditionalField( ByteField("pad", 0), - lambda pkt: (len(pkt.descriptors) + 1) % 2 + # The length should be 3 bytes per each triplet, and 3 bytes for the + # country_string field. The standard dictates that the element length + # must be even, so if the result is odd, add a padding byte. + # Some transmitters don't comply with the standard, so instead of assuming + # the length, we test whether there is a padding byte. + # Some edge cases are still not covered, for example, if the tag length + # (pkt.len) is an arbitrary number. + lambda pkt: ((len(pkt.descriptors) + 1) % 2) if pkt.len is None else (pkt.len % 3) # noqa: E501 ) ] diff --git a/test/regression.uts b/test/regression.uts index b4ec23cb79f..7409ec29873 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -1585,6 +1585,24 @@ beacon = frame.getlayer(5) ssid = beacon.network_stats()['ssid'] assert ssid == "ROUTE-821E295" += SSID is parsed properly even when the Country Information Tag Element has an odd length (not complying with the standard) and a missing pad byte +~ Missing Pad Byte in Country Info +# A regression test for https://github.com/secdev/scapy/pull/2685. +# https://github.com/secdev/scapy/issues/4132 describes a packet with +# a Country Information element tag that has an odd length, even though it's against the standard. +# The transmitter should have added a padding byte to make the length even, but it didn't. +# The effect on scapy used to be improper parsing of the next tag elements, causing the SSID to be overridden. +# This test checks the SSID is parsed properly. +from io import BytesIO +pcapfile = BytesIO(b'\n\r\r\n\x80\x00\x00\x00M<+\x1a\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x03\x00\x10\x00Linux 6.1.21-v8+\x04\x00E\x00Dumpcap (Wireshark) 3.4.10 (Git v3.4.10 packaged as 3.4.10-0+deb11u1)\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x01\x00\x00\x00@\x00\x00\x00\x7f\x00\x00\x00\x00\x04\x00\x00\x02\x00\x05\x00wifi2\x00\x00\x00\t\x00\x01\x00\t\x00\x00\x00\x0c\x00\x10\x00Linux 6.1.21-v8+\x00\x00\x00\x00@\x00\x00\x00\x06\x00\x00\x00\xb0\x01\x00\x00\x00\x00\x00\x00c\xd3\x87\x17\xe3c5\x82\x90\x01\x00\x00\x90\x01\x00\x00\x00\x00 \x00\xae@\x00\xa0 \x08\x00\xa0 \x08\x00\x00\x10\x0cd\x14@\x01\xa9\x00\x0c\x00\x00\x00\xa6\x00\xa8\x01\x80\x00\x00\x00\xff\xff\xff\xff\xff\xff\x02\xbf\xaf\x9f\xf8\x07\x02\xbf\xaf\x9f\xf8\x070\x96[p\xdcM\x06\x00\x00\x00d\x00\x11\x00\x00\x00\x01\x08\x8c\x12\x98$\xb0H`l\x03\x01,\x05\x04\x00\x01\x00\x00\x07QUS \x01\r\x80$\x01\x80(\x01\x80,\x01\x800\x01\x804\x01\x808\x01\x80<\x01\x80@\x01\x80d\x01\x80h\x01\x80l\x01\x80p\x01\x80t\x01\x80x\x01\x80|\x01\x80\x80\x01\x80\x84\x01\x80\x88\x01\x80\x8c\x01\x80\x90\x01\x80\x95\x01\x80\x99\x01\x80\x9d\x01\x80\xa1\x01\x80\xa5\x01\x800\x14\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x02\x0c\x00;\x02s\x00-\x1a,\t\x13\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00,\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00=\x16,\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xdd\x18\x00P\xf2\x02\x01\x01\x81\x00\x03\xa4\x00\x00\'\xa4\x00\x00BC]\x00a\x11.\x00\xdd;\x00P\xf2\x04\x10J\x00\x01\x10\x10D\x00\x01\x02\x10I\x00\x06\x007*\x00\x01 \x10\x11\x00\x1358" Hisense Roku TV\x10T\x00\x08\x00\x07\x00P\xf2\x04\x00\x01\xdd\x16\xc8:k\x01\x01\x1048<@dhlptx|\x80\x84\x88\x8c\x90\xdd\x12Po\x9a\t\x02\x02\x00!\x0b\x03\x06\x00\x02\xbf\xaf\x9f\xf8\x07\xdd\rPo\x9a\n\x00\x00\x06\x01\x11\x1cD\x002\xf5N\xfbh\xb0\x01\x00\x00') +pktpcap = rdpcap(pcapfile) +frame = pktpcap[0] +beacon = frame.getlayer(4) +stats = beacon.network_stats() +ssid = stats['ssid'] +assert ssid == "" +country = stats['country'] +assert country == 'US' ############ ############ From 78f153e5617f134e8cb6d2df9f122fc4ad595fae Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Mon, 13 Nov 2023 23:04:27 +0100 Subject: [PATCH 094/122] Bump mypy to 1.7.0 and tox --- .config/ci/test.sh | 6 +- .github/workflows/unittests.yml | 16 +++--- pyproject.toml | 2 +- scapy/asn1fields.py | 8 +-- scapy/compat.py | 7 ++- scapy/config.py | 99 +++++++++++++++++---------------- scapy/packet.py | 10 ++-- scapy/plist.py | 2 +- scapy/themes.py | 84 +++++++++++++++++----------- scapy/utils.py | 36 +++++++----- tox.ini | 29 +++++----- 11 files changed, 166 insertions(+), 133 deletions(-) diff --git a/.config/ci/test.sh b/.config/ci/test.sh index 30e43fa2a8d..93c1c79a24d 100755 --- a/.config/ci/test.sh +++ b/.config/ci/test.sh @@ -84,13 +84,13 @@ if [ -z $TOXENV ] then case ${SCAPY_TOX_CHOSEN} in both) - export TOXENV="${TESTVER}_non_root,${TESTVER}_root" + export TOXENV="${TESTVER}-non_root,${TESTVER}-root" ;; root) - export TOXENV="${TESTVER}_root" + export TOXENV="${TESTVER}-root" ;; *) - export TOXENV="${TESTVER}_non_root" + export TOXENV="${TESTVER}-non_root" ;; esac fi diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 5296d7142f2..a02cd99ae8a 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -20,7 +20,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - name: Install tox run: pip install tox - name: Run flake8 tests @@ -55,7 +55,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - name: Install tox run: pip install tox - name: Run mypy @@ -78,7 +78,7 @@ jobs: include: # Linux root tests - os: ubuntu-latest - python: "3.11" + python: "3.12" mode: root flags: " -K scanner" # PyPy tests: root only @@ -88,18 +88,18 @@ jobs: flags: " -K scanner" # Libpcap test - os: ubuntu-latest - python: "3.11" + python: "3.12" mode: root installmode: 'libpcap' flags: " -K scanner" # macOS tests - os: macos-12 - python: "3.11" + python: "3.12" mode: both flags: " -K scanner" # Scanner tests - os: ubuntu-latest - python: "3.11" + python: "3.12" mode: root allow-failure: 'true' flags: " -k scanner" @@ -109,7 +109,7 @@ jobs: allow-failure: 'true' flags: " -k scanner" - os: macos-12 - python: "3.11" + python: "3.12" mode: both allow-failure: 'true' flags: " -k scanner" @@ -139,7 +139,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - name: Install tox run: pip install tox # pyca/cryptography's CI installs cryptography diff --git a/pyproject.toml b/pyproject.toml index 0aa486a4795..92f1734adc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ all = [ "cryptography>=2.0", "matplotlib", ] -docs = [ +doc = [ "sphinx>=7.0.0", "sphinx_rtd_theme>=1.3.0", "tox>=3.0.0", diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 39573f9124d..6cd782a9ed8 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -452,7 +452,7 @@ def is_empty(self, pkt): def get_fields_list(self): # type: () -> List[ASN1F_field[Any, Any]] - return reduce(lambda x, y: x + y.get_fields_list(), # type: ignore + return reduce(lambda x, y: x + y.get_fields_list(), self.seq, []) def m2i(self, pkt, s): @@ -497,7 +497,7 @@ def dissect(self, pkt, s): def build(self, pkt): # type: (ASN1_Packet) -> bytes - s = reduce(lambda x, y: x + y.build(pkt), # type: ignore + s = reduce(lambda x, y: x + y.build(pkt), self.seq, b"") return super(ASN1F_SEQUENCE, self).i2m(pkt, s) @@ -506,7 +506,7 @@ class ASN1F_SET(ASN1F_SEQUENCE): ASN1_tag = ASN1_Class_UNIVERSAL.SET -_SEQ_T = Union['ASN1_Packet', Type[ASN1F_field], 'ASN1F_PACKET'] +_SEQ_T = Union['ASN1_Packet', Type[ASN1F_field[Any, Any]], 'ASN1F_PACKET'] class ASN1F_SEQUENCE_OF(ASN1F_field[List[_SEQ_T], @@ -656,7 +656,7 @@ def i2repr(self, pkt, x): return self._field.i2repr(pkt, x) -_CHOICE_T = Union['ASN1_Packet', Type[ASN1F_field], 'ASN1F_PACKET'] +_CHOICE_T = Union['ASN1_Packet', Type[ASN1F_field[Any, Any]], 'ASN1F_PACKET'] class ASN1F_CHOICE(ASN1F_field[_CHOICE_T, ASN1_Object[Any]]): diff --git a/scapy/compat.py b/scapy/compat.py index 2906022a3ed..e3e2c2875a1 100644 --- a/scapy/compat.py +++ b/scapy/compat.py @@ -26,6 +26,7 @@ # typing 'DecoratorCallable', 'Literal', + 'Protocol', 'Self', 'UserDict', # compat @@ -45,7 +46,7 @@ # Note: # supporting typing on multiple python versions is a nightmare. # we provide a FakeType class to be able to use types added on -# later Python versions (since we run mypy on 3.11), on older +# later Python versions (since we run mypy on 3.12), on older # ones. @@ -77,9 +78,13 @@ def __repr__(self): # Python 3.8 Only if sys.version_info >= (3, 8): from typing import Literal + from typing import Protocol else: Literal = _FakeType("Literal") + class Protocol: + pass + # Python 3.9 Only if sys.version_info >= (3, 9): diff --git a/scapy/config.py b/scapy/config.py index 7b03d9debfe..bc01f1019d6 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -23,7 +23,7 @@ from scapy.base_classes import BasePacket from scapy.consts import DARWIN, WINDOWS, LINUX, BSD, SOLARIS from scapy.error import log_scapy, warning, ScapyInvalidPlatformException -from scapy.themes import NoTheme, apply_ipython_style +from scapy.themes import ColorTheme, NoTheme, apply_ipython_style # Typing imports from typing import ( @@ -135,20 +135,20 @@ def _readonly(name): class ProgPath(ConfClass): - _default = "" - universal_open = "open" if DARWIN else "xdg-open" - pdfreader = universal_open - psreader = universal_open - svgreader = universal_open - dot = "dot" - display = "display" - tcpdump = "tcpdump" - tcpreplay = "tcpreplay" - hexedit = "hexer" - tshark = "tshark" - wireshark = "wireshark" - ifconfig = "ifconfig" - extcap_folders = [ + _default: str = "" + universal_open: str = "open" if DARWIN else "xdg-open" + pdfreader: str = universal_open + psreader: str = universal_open + svgreader: str = universal_open + dot: str = "dot" + display: str = "display" + tcpdump: str = "tcpdump" + tcpreplay: str = "tcpreplay" + hexedit: str = "hexer" + tshark: str = "tshark" + wireshark: str = "wireshark" + ifconfig: str = "ifconfig" + extcap_folders: List[str] = [ os.path.join(os.path.expanduser("~"), ".config", "wireshark", "extcap"), "/usr/lib/x86_64-linux-gnu/wireshark/extcap", ] @@ -389,7 +389,7 @@ def __setitem__(self, item, v): self._timetable[item] = time.time() super(CacheInstance, self).__setitem__(item, v) - def update(self, # type: ignore + def update(self, other, # type: Any **kwargs # type: Any ): @@ -404,7 +404,7 @@ def update(self, # type: ignore def iteritems(self): # type: () -> Iterator[Tuple[str, Any]] if self.timeout is None: - return super(CacheInstance, self).items() # type: ignore + return super(CacheInstance, self).items() t0 = time.time() return ( (k, v) @@ -415,7 +415,7 @@ def iteritems(self): def iterkeys(self): # type: () -> Iterator[str] if self.timeout is None: - return super(CacheInstance, self).keys() # type: ignore + return super(CacheInstance, self).keys() t0 = time.time() return ( k @@ -430,7 +430,7 @@ def __iter__(self): def itervalues(self): # type: () -> Iterator[Tuple[str, Any]] if self.timeout is None: - return super(CacheInstance, self).values() # type: ignore + return super(CacheInstance, self).values() t0 = time.time() return ( v @@ -616,7 +616,7 @@ def _set_conf_sockets(): Interceptor.set_from_hook(conf, "use_pcap", False) else: conf.L3socket = L3pcapSocket - conf.L3socket6 = functools.partial( # type: ignore + conf.L3socket6 = functools.partial( L3pcapSocket, filter="ip6") conf.L2socket = L2pcapSocket conf.L2listen = L2pcapListenSocket @@ -624,7 +624,7 @@ def _set_conf_sockets(): from scapy.arch.bpf.supersocket import L2bpfListenSocket, \ L2bpfSocket, L3bpfSocket conf.L3socket = L3bpfSocket - conf.L3socket6 = functools.partial( # type: ignore + conf.L3socket6 = functools.partial( L3bpfSocket, filter="ip6") conf.L2socket = L2bpfSocket conf.L2listen = L2bpfListenSocket @@ -698,7 +698,7 @@ def _iface_changer(attr, val, old): "See conf.ifaces output" ) return iface - return val # type: ignore + return val def _reset_tls_nss_keys(attr, val, old): @@ -712,8 +712,8 @@ class Conf(ConfClass): """ This object contains the configuration of Scapy. """ - version = ReadOnlyAttribute("version", VERSION) - session = "" #: filename where the session will be saved + version: str = ReadOnlyAttribute("version", VERSION) + session: str = "" #: filename where the session will be saved interactive = False #: can be "ipython", "bpython", "ptpython", "ptipython", "python" or "auto". #: Default: Auto @@ -723,15 +723,15 @@ class Conf(ConfClass): #: if 1, prevents any unwanted packet to go out (ARP, DNS, ...) stealth = "not implemented" #: selects the default output interface for srp() and sendp(). - iface = Interceptor("iface", None, _iface_changer) # type: 'scapy.interfaces.NetworkInterface' # type: ignore # noqa: E501 - layers = LayersList() + iface = Interceptor("iface", None, _iface_changer) # type: 'scapy.interfaces.NetworkInterface' # noqa: E501 + layers: LayersList = LayersList() commands = CommandsList() # type: CommandsList #: Codec used by default for ASN1 objects ASN1_default_codec = None # type: 'scapy.asn1.asn1.ASN1Codec' #: choose the AS resolver class to use AS_resolver = None # type: scapy.as_resolvers.AS_resolver dot15d4_protocol = None # Used in dot15d4.py - logLevel = Interceptor("logLevel", log_scapy.level, _loglevel_changer) + logLevel: int = Interceptor("logLevel", log_scapy.level, _loglevel_changer) #: if 0, doesn't check that IPID matches between IP sent and #: ICMP IP citation received #: if 1, checks that they either are equal or byte swapped @@ -749,7 +749,7 @@ class Conf(ConfClass): #: ones in ICMP citation check_TCPerror_seqack = False verb = 2 #: level of verbosity, from 0 (almost mute) to 3 (verbose) - prompt = Interceptor("prompt", ">>> ", _prompt_changer) + prompt: str = Interceptor("prompt", ">>> ", _prompt_changer) #: default mode for the promiscuous mode of a socket (to get answers if you #: spoof on a lan) sniff_promisc = True # type: bool @@ -757,8 +757,8 @@ class Conf(ConfClass): raw_summary = False # type: Union[bool, Callable[[bytes], Any]] padding_layer = None # type: Type[Packet] default_l2 = None # type: Type[Packet] - l2types = Num2Layer() - l3types = Num2Layer() + l2types: Num2Layer = Num2Layer() + l3types: Num2Layer = Num2Layer() L3socket = None # type: Type[scapy.supersocket.SuperSocket] L3socket6 = None # type: Type[scapy.supersocket.SuperSocket] L2socket = None # type: Type[scapy.supersocket.SuperSocket] @@ -769,10 +769,13 @@ class Conf(ConfClass): mib = None # type: 'scapy.asn1.mib.MIBDict' bufsize = 2**16 #: history file - histfile = os.getenv('SCAPY_HISTFILE', - os.path.join(os.path.expanduser("~"), - ".config", "scapy", - "history")) + histfile: str = os.getenv( + 'SCAPY_HISTFILE', + os.path.join( + os.path.expanduser("~"), + ".config", "scapy", "history" + ) + ) #: includes padding in disassembled packets padding = 1 #: BPF filter for packets to ignore @@ -808,36 +811,36 @@ class Conf(ConfClass): auto_fragment = True #: raise exception when a packet dissector raises an exception debug_dissector = False - color_theme = Interceptor("color_theme", NoTheme(), _prompt_changer) + color_theme: ColorTheme = Interceptor("color_theme", NoTheme(), _prompt_changer) #: how much time between warnings from the same place warning_threshold = 5 - prog = ProgPath() + prog: ProgPath = ProgPath() #: holds list of fields for which resolution should be done - resolve = Resolve() + resolve: Resolve = Resolve() #: holds list of enum fields for which conversion to string #: should NOT be done - noenum = Resolve() - emph = Emphasize() + noenum: Resolve = Resolve() + emph: Emphasize = Emphasize() #: read only attribute to show if PyPy is in use - use_pypy = ReadOnlyAttribute("use_pypy", isPyPy()) + use_pypy: bool = ReadOnlyAttribute("use_pypy", isPyPy()) #: use libpcap integration or not. Changing this value will update #: the conf.L[2/3] sockets - use_pcap = Interceptor( + use_pcap: bool = Interceptor( "use_pcap", os.getenv("SCAPY_USE_LIBPCAP", "").lower().startswith("y"), _socket_changer ) - use_bpf = Interceptor("use_bpf", False, _socket_changer) + use_bpf: bool = Interceptor("use_bpf", False, _socket_changer) use_npcap = False - ipv6_enabled = socket.has_ipv6 + ipv6_enabled: bool = socket.has_ipv6 stats_classic_protocols = [] # type: List[Type[Packet]] stats_dot11_protocols = [] # type: List[Type[Packet]] temp_files = [] # type: List[str] #: netcache holds time-based caches for net operations - netcache = NetCache() + netcache: NetCache = NetCache() geoip_city = None # can, tls, http and a few others are not loaded by default - load_layers = [ + load_layers: List[str] = [ 'bluetooth', 'bluetooth4LE', 'dcerpc', @@ -903,7 +906,7 @@ class Conf(ConfClass): #: When True, raise exception if no dst MAC found otherwise broadcast. #: Default is False. raise_no_dst_mac = False - loopback_name = "lo" if LINUX else "lo0" + loopback_name: str = "lo" if LINUX else "lo0" nmap_base = "" # type: str nmap_kdb = None # type: Optional[NmapKnowledgeBase] #: a safety mechanism: the maximum amount of items included in a PacketListField @@ -918,7 +921,7 @@ class Conf(ConfClass): _reset_tls_nss_keys ) #: Dictionary containing parsed NSS Keys - tls_nss_keys = None + tls_nss_keys: Dict[str, bytes] = None def __getattribute__(self, attr): # type: (str) -> Any @@ -971,7 +974,7 @@ def func_in(*args, **kwargs): raise ImportError("Cannot execute crypto-related method! " "Please install python-cryptography v1.7 or later.") # noqa: E501 return func(*args, **kwargs) - return func_in # type: ignore + return func_in def scapy_delete_temp_files(): diff --git a/scapy/packet.py b/scapy/packet.py index a4b94491d14..e66e7a1bf16 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -1267,7 +1267,7 @@ def haslayer(self, cls, _subclass=None): if _subclass: match = issubtype else: - match = lambda cls1, cls2: bool(cls1 == cls2) + match = lambda x, t: bool(x == t) if cls is None or match(self.__class__, cls) \ or cls in [self.__class__.__name__, self._name]: return True @@ -1300,7 +1300,7 @@ def getlayer(self, if _subclass: match = issubtype else: - match = lambda cls1, cls2: bool(cls1 == cls2) + match = lambda x, t: bool(x == t) # Note: # cls can be int, packet, str # string_class_name can be packet, str (packet or packet+field) @@ -1422,8 +1422,8 @@ def _show_or_dump(self, """ if dump: - from scapy.themes import AnsiColorTheme - ct = AnsiColorTheme() # No color for dump output + from scapy.themes import ColorTheme, AnsiColorTheme + ct: ColorTheme = AnsiColorTheme() # No color for dump output else: ct = conf.color_theme s = "%s%s %s %s \n" % (label_lvl, @@ -2121,7 +2121,7 @@ def explore(layer=None): # Check for prompt_toolkit >= 3.0.0 call_ptk = lambda x: cast(str, x) # type: Callable[[Any], str] if _version_checker(prompt_toolkit, (3, 0)): - call_ptk = lambda x: x.run() # type: ignore + call_ptk = lambda x: x.run() # 1 - Ask for layer or contrib btn_diag = button_dialog( title="Scapy v%s" % conf.version, diff --git a/scapy/plist.py b/scapy/plist.py index e888e5662f3..daaccd3f419 100644 --- a/scapy/plist.py +++ b/scapy/plist.py @@ -379,7 +379,7 @@ def multiplot(self, kargs = MATPLOTLIB_DEFAULT_PLOT_KARGS if plot_xy: - lines = [plt.plot(*list(zip(*pl)), **dict(kargs, label=k)) # type: ignore + lines = [plt.plot(*list(zip(*pl)), **dict(kargs, label=k)) for k, pl in d.items()] else: lines = [plt.plot(pl, **dict(kargs, label=k)) diff --git a/scapy/themes.py b/scapy/themes.py index 528b6584fec..a30c179a2c2 100644 --- a/scapy/themes.py +++ b/scapy/themes.py @@ -16,11 +16,12 @@ from typing import ( Any, - Callable, List, Optional, Tuple, + cast, ) +from scapy.compat import Protocol class ColorTable: @@ -75,14 +76,27 @@ def ansi_to_pygments(self, x): Color = ColorTable() +class _ColorFormatterType(Protocol): + def __call__(self, + val: Any, + fmt: Optional[str] = None, + fmt2: str = "", + before: str = "", + after: str = "") -> str: + pass + + def create_styler(fmt=None, # type: Optional[str] before="", # type: str after="", # type: str fmt2="%s" # type: str ): - # type: (...) -> Callable[[Any], str] - def do_style(val, fmt=fmt, fmt2=fmt2, before=before, after=after): - # type: (Any, Optional[str], str, str, str) -> str + # type: (...) -> _ColorFormatterType + def do_style(val: Any, + fmt: Optional[str] = fmt, + fmt2: str = fmt2, + before: str = before, + after: str = after) -> str: if fmt is None: sval = str(val) else: @@ -92,6 +106,30 @@ def do_style(val, fmt=fmt, fmt2=fmt2, before=before, after=after): class ColorTheme: + style_normal = "" + style_prompt = "" + style_punct = "" + style_id = "" + style_not_printable = "" + style_layer_name = "" + style_field_name = "" + style_field_value = "" + style_emph_field_name = "" + style_emph_field_value = "" + style_packetlist_name = "" + style_packetlist_proto = "" + style_packetlist_value = "" + style_fail = "" + style_success = "" + style_odd = "" + style_even = "" + style_opening = "" + style_active = "" + style_closed = "" + style_left = "" + style_right = "" + style_logo = "" + def __repr__(self): # type: () -> str return "<%s>" % self.__class__.__name__ @@ -101,7 +139,7 @@ def __reduce__(self): return (self.__class__, (), ()) def __getattr__(self, attr): - # type: (str) -> Callable[[Any], str] + # type: (str) -> _ColorFormatterType if attr in ["__getstate__", "__setstate__", "__getinitargs__", "__reduce_ex__"]: raise AttributeError() @@ -120,7 +158,7 @@ class NoTheme(ColorTheme): class AnsiColorTheme(ColorTheme): def __getattr__(self, attr): - # type: (str) -> Callable[[Any], str] + # type: (str) -> _ColorFormatterType if attr.startswith("__"): raise AttributeError(attr) s = "style_%s" % attr @@ -135,30 +173,6 @@ def __getattr__(self, attr): return create_styler(before=before, after=after) - style_normal = "" - style_prompt = "" - style_punct = "" - style_id = "" - style_not_printable = "" - style_layer_name = "" - style_field_name = "" - style_field_value = "" - style_emph_field_name = "" - style_emph_field_value = "" - style_packetlist_name = "" - style_packetlist_proto = "" - style_packetlist_value = "" - style_fail = "" - style_success = "" - style_odd = "" - style_even = "" - style_opening = "" - style_active = "" - style_closed = "" - style_left = "" - style_right = "" - style_logo = "" - class BlackAndWhite(AnsiColorTheme, NoTheme): pass @@ -262,8 +276,7 @@ class ColorOnBlackTheme(AnsiColorTheme): class FormatTheme(ColorTheme): - def __getattr__(self, attr): - # type: (str) -> Callable[[Any], str] + def __getattr__(self, attr: str) -> _ColorFormatterType: if attr.startswith("__"): raise AttributeError(attr) colfmt = self.__class__.__dict__.get("style_%s" % attr, "%s") @@ -293,10 +306,13 @@ class LatexTheme(FormatTheme): # style_odd = "" style_logo = r"\textcolor{green}{\bf %s}" - def __getattr__(self, attr: str) -> Callable[[Any], str]: + def __getattr__(self, attr: str) -> _ColorFormatterType: from scapy.utils import tex_escape styler = super(LatexTheme, self).__getattr__(attr) - return lambda x: styler(tex_escape(x)) + return cast( + _ColorFormatterType, + lambda x, *args, **kwargs: styler(tex_escape(x), *args, **kwargs), + ) class LatexTheme2(FormatTheme): diff --git a/scapy/utils.py b/scapy/utils.py index 2fd4d0c89ca..1906390f18c 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -440,9 +440,11 @@ def hexdiff(a, b, autojunk=False): # Print the diff x = y = i = 0 - colorize = {0: lambda x: x, - -1: conf.color_theme.left, - 1: conf.color_theme.right} + colorize: Dict[int, Callable[[str], str]] = { + 0: lambda x: x, + -1: conf.color_theme.left, + 1: conf.color_theme.right + } dox = 1 doy = 0 @@ -1476,12 +1478,17 @@ def __init__(self, filename, fdesc=None, magic=None): # type: ignore self.default_options = { "tsresol": 1000000 } - self.blocktypes = { - 1: self._read_block_idb, - 2: self._read_block_pkt, - 3: self._read_block_spb, - 6: self._read_block_epb, - 10: self._read_block_dsb, + self.blocktypes: Dict[ + int, + Callable[ + [bytes, int], + Optional[Tuple[bytes, RawPcapNgReader.PacketMetadata]] + ]] = { + 1: self._read_block_idb, + 2: self._read_block_pkt, + 3: self._read_block_spb, + 6: self._read_block_epb, + 10: self._read_block_dsb, } self.endian = "!" # Will be overwritten by first SHB @@ -1516,10 +1523,9 @@ def _read_block(self, size=MTU): raise EOFError block = self.f.read(blocklen - 12) self._read_block_tail(blocklen) - return self.blocktypes.get( - blocktype, - lambda block, size: None - )(block, size) + if blocktype in self.blocktypes: + return self.blocktypes[blocktype](block, size) + return None def _read_block_tail(self, blocklen): # type: (int) -> None @@ -1598,10 +1604,10 @@ def _read_block_idb(self, block, _): # 4 bytes Snaplen options = self._read_options(block[8:-4]) try: - interface = struct.unpack( # type: ignore + interface: Tuple[int, int, int] = struct.unpack( self.endian + "HxxI", block[:8] - ) + (options["tsresol"],) # type: Tuple[int, int, int] + ) + (options["tsresol"],) except struct.error: warning("PcapNg: IDB is too small %d/8 !" % len(block)) raise EOFError diff --git a/tox.ini b/tox.ini index ebd54457f1b..dd282e510f2 100644 --- a/tox.ini +++ b/tox.ini @@ -2,11 +2,15 @@ # Copyright (C) 2020 Guillaume Valadon +# Tox environments: +# py{version}-{os}-{non_root,root} +# In our testing, version can be 37 to 312 or py39 for pypy39 + [tox] -envlist = py{27,34,35,36,37,38,39,310,311,py27,py39}-{linux,bsd}_{non_root,root}, - py{27,34,35,36,37,38,39,310,311,py27,py39}-windows, +# minversion = 4.0 skip_missing_interpreters = true -minversion = 4.0 +# envlist = default when doing 'tox' +envlist = py{37,38,39,310,311,312}-{linux,bsd,windows}-non_root # Main tests @@ -36,14 +40,14 @@ deps = mock brotli < 1.1.0 ; sys_platform != 'win32' zstandard ; sys_platform != 'win32' platform = - linux_non_root,linux_root: linux - bsd_non_root,bsd_root: darwin|freebsd|openbsd|netbsd + linux: linux + bsd: darwin|freebsd|openbsd|netbsd windows: win32 commands = - linux_non_root: {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c ./test/configs/linux.utsc -N {posargs} - linux_root: sudo -E {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c ./test/configs/linux.utsc {posargs} - bsd_non_root: {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c test/configs/bsd.utsc -K manufdb -K tshark -N {posargs} - bsd_root: sudo -E {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c test/configs/bsd.utsc -K manufdb -K tshark {posargs} + linux-non_root: {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c ./test/configs/linux.utsc -N {posargs} + linux-root: sudo -E {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c ./test/configs/linux.utsc {posargs} + bsd-non_root: {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c test/configs/bsd.utsc -K manufdb -K tshark -N {posargs} + bsd-root: sudo -E {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c test/configs/bsd.utsc -K manufdb -K tshark {posargs} windows: {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c test/configs/windows.utsc {posargs} {env:DISABLE_COVERAGE:coverage combine} {env:DISABLE_COVERAGE:coverage xml -i} @@ -99,7 +103,7 @@ commands = [testenv:mypy] description = "Check Scapy compliance against static typing" skip_install = true -deps = mypy==1.1.1 +deps = mypy==1.7.0 typing types-mock commands = python .config/mypy/mypy_check.py linux @@ -108,10 +112,9 @@ commands = python .config/mypy/mypy_check.py linux [testenv:docs] description = "Build the docs" -skip_install = true +deps = +extras = doc changedir = {toxinidir}/doc/scapy -deps = sphinx>=2.4.2 - sphinx_rtd_theme commands = sphinx-build -W --keep-going -b html . _build/html From 7e19b7b99ebefdf4e0c89e71273f39c4bff51be5 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Tue, 14 Nov 2023 21:13:00 +0100 Subject: [PATCH 095/122] README: new badge URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d71f4115759..8168cb94df6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Scapy   Scapy -[![Scapy unit tests](https://github.com/secdev/scapy/workflows/Scapy%20unit%20tests/badge.svg?event=push)](https://github.com/secdev/scapy/actions?query=workflow%3A%22Scapy+unit+tests%22+branch%3Amaster+event%3Apush) +[![Scapy unit tests](https://github.com/secdev/scapy/actions/workflows/unittests.yml/badge.svg?branch=master&event=push)](https://github.com/secdev/scapy/actions/workflows/unittests.yml?query=event%3Apush) [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/os03daotfja0wtp7/branch/master?svg=true)](https://ci.appveyor.com/project/secdev/scapy/branch/master) [![Codecov Status](https://codecov.io/gh/secdev/scapy/branch/master/graph/badge.svg)](https://codecov.io/gh/secdev/scapy) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/30ee6772bb264a689a2604f5cdb0437b)](https://www.codacy.com/app/secdev/scapy) From adc6cb08b0a354f6bd7de4db4ac20b57c302949a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20V=C3=A1zquez=20Blanco?= Date: Tue, 31 Oct 2023 12:25:00 +0100 Subject: [PATCH 096/122] Fix exception on failed _BluetoothLibcSockets --- scapy/layers/bluetooth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index ba4528f9a7d..75b3b05ba79 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -2055,7 +2055,8 @@ def close(self): if hasattr(self, "ins"): if self.ins and (WINDOWS or self.ins.fileno() != -1): close(self.ins.fileno()) - close(self.hci_fd) + if hasattr(self, "hci_fd"): + close(self.hci_fd) class BluetoothUserSocket(_BluetoothLibcSocket): From 9676e957e2c29ab6322fc9dca5f2b97a6b8c949c Mon Sep 17 00:00:00 2001 From: Jose Luis Duran Date: Sun, 29 Oct 2023 06:59:55 +0000 Subject: [PATCH 097/122] packet: Remove trailing whitespace --- scapy/packet.py | 8 ++++---- test/regression.uts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scapy/packet.py b/scapy/packet.py index e66e7a1bf16..6759dfc4c8c 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -1426,10 +1426,10 @@ def _show_or_dump(self, ct: ColorTheme = AnsiColorTheme() # No color for dump output else: ct = conf.color_theme - s = "%s%s %s %s \n" % (label_lvl, - ct.punct("###["), - ct.layer_name(self.name), - ct.punct("]###")) + s = "%s%s %s %s\n" % (label_lvl, + ct.punct("###["), + ct.layer_name(self.name), + ct.punct("]###")) for f in self.fields_desc: if isinstance(f, ConditionalField) and not f._evalcond(self): continue diff --git a/test/regression.uts b/test/regression.uts index 7409ec29873..c73e3d27321 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -123,7 +123,7 @@ with ContextManagerCaptureOutput() as cmco: pkt.show() result = cmco.get_output().strip() -assert result == '\\#\\#\\#[ \\textcolor{red}{\\bf SmallPacket} ]\\#\\#\\# \n \\textcolor{blue}{a} = \\textcolor{purple}{0}' +assert result == '\\#\\#\\#[ \\textcolor{red}{\\bf SmallPacket} ]\\#\\#\\#\n \\textcolor{blue}{a} = \\textcolor{purple}{0}' conf.color_theme = conf_color_theme From 42bfd7ccea26acfde1e3d85bf5ad0819a452ee4f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 17 Nov 2023 20:42:24 +0100 Subject: [PATCH 098/122] Modify packet definition of UDS_IOCBI packet to allow customization (#4161) --- scapy/contrib/automotive/uds.py | 8 ++------ scapy/contrib/automotive/uds_scan.py | 2 +- test/contrib/automotive/uds.uts | 3 +-- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index bbe0316144c..5d67394bc04 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -1374,11 +1374,8 @@ def answers(self, other): # #########################IOCBI################################### class UDS_IOCBI(Packet): name = 'InputOutputControlByIdentifier' - dataIdentifiers = ObservableDict() fields_desc = [ - XShortEnumField('dataIdentifier', 0, dataIdentifiers), - ByteField('controlOptionRecord', 0), - StrField('controlEnableMaskRecord', b"", fmt="B") + XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -1388,8 +1385,7 @@ class UDS_IOCBI(Packet): class UDS_IOCBIPR(Packet): name = 'InputOutputControlByIdentifierPositiveResponse' fields_desc = [ - XShortField('dataIdentifier', 0), - StrField('controlStatusRecord', b"", fmt="B") + XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] def answers(self, other): diff --git a/scapy/contrib/automotive/uds_scan.py b/scapy/contrib/automotive/uds_scan.py index 8552a5d1af3..a23c7c6027d 100644 --- a/scapy/contrib/automotive/uds_scan.py +++ b/scapy/contrib/automotive/uds_scan.py @@ -873,7 +873,7 @@ def _get_table_entry_y(self, tup): if resp is not None: return "0x%04x: %s" % \ (tup[1].dataIdentifier, - resp.controlStatusRecord) + repr(resp.payload)) else: return "0x%04x: No response" % tup[1].dataIdentifier diff --git a/test/contrib/automotive/uds.uts b/test/contrib/automotive/uds.uts index ea3bb085530..8a05bf4ac34 100644 --- a/test/contrib/automotive/uds.uts +++ b/test/contrib/automotive/uds.uts @@ -1362,8 +1362,7 @@ assert rtepr.answers(rte) iocbi = UDS(b'\x2f\x23\x34\xffcoffee') assert iocbi.service == 0x2f assert iocbi.dataIdentifier == 0x2334 -assert iocbi.controlOptionRecord == 255 -assert iocbi.controlEnableMaskRecord == b'coffee' +assert iocbi.load == b'\xffcoffee' = Check UDS_RFT From 94bc3720927a5911bf16e3903f74baeb0f6e4dc8 Mon Sep 17 00:00:00 2001 From: hujingfei Date: Sat, 7 Oct 2023 11:00:56 +0800 Subject: [PATCH 099/122] Fix and add test for GENEVE.optionlen --- scapy/contrib/geneve.py | 2 +- test/contrib/geneve.uts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/scapy/contrib/geneve.py b/scapy/contrib/geneve.py index 77ce847a5ed..60430fee1f8 100644 --- a/scapy/contrib/geneve.py +++ b/scapy/contrib/geneve.py @@ -66,7 +66,7 @@ class GENEVE(Packet): def post_build(self, p, pay): if self.optionlen is None: tmp_len = (len(p) - 8) // 4 - p = chb(tmp_len & 0x2f | orb(p[0]) & 0xc0) + p[1:] + p = chb(tmp_len & 0x3f | orb(p[0]) & 0xc0) + p[1:] return p + pay def answers(self, other): diff --git a/test/contrib/geneve.uts b/test/contrib/geneve.uts index 5811f56741b..e40533b3558 100644 --- a/test/contrib/geneve.uts +++ b/test/contrib/geneve.uts @@ -66,3 +66,9 @@ a = GENEVE(proto=0x0800)/b'E\x00\x00\x1c\x00\x01\x00\x00@\x01\xfa$\xc0\xa8\x00w\ a = GENEVE(raw(a)) assert a.summary() == 'GENEVE / IP / ICMP 192.168.0.119 > 172.217.18.195 echo-request 0' assert a.mysummary() in ['GENEVE (vni=0x0,optionlen=0,proto=0x800)', 'GENEVE (vni=0x0,optionlen=0,proto=IPv4)'] + += GENEVE - Optionlen + +data = raw(RandString(size=random.randint(0, pow(2, 6)) * 4)) +p = GENEVE(raw(GENEVE(options=GeneveOptions(data=data)))) +assert p[GENEVE].optionlen == (len(data) // 4 + 1) From 81aa7fa918ee69750f7c6ef1aefae71e9310d8d8 Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Fri, 17 Nov 2023 19:54:48 +0000 Subject: [PATCH 100/122] DNS: make it possible to include empty strings in TXT RDATA According to https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 TXT RDATA holds one or more s where is a single length octet followed by that number of characters. This patch makes it possible to include empty strings (by appending zero-bytes showing that their length is zero). It also changes the default value to avoid generating TXT RRs with zero-length TXT RDATA by default. It's still possible to generate zero-length TXT RDATA by passing rdata=[] explicitly. --- scapy/layers/dns.py | 5 ++++- test/scapy/layers/dns.uts | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 606fde284f9..94cf6d78144 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -369,6 +369,9 @@ def i2len(self, pkt, x): def i2m(self, pkt, s): ret_s = b"" for text in s: + if not text: + ret_s += b"\x00" + continue text = bytes_encode(text) # The initial string must be split into a list of strings # prepended with theirs sizes. @@ -943,7 +946,7 @@ class DNSRR(Packet): length_from=lambda pkt: pkt.rdlen), lambda pkt: pkt.type in [2, 3, 4, 5, 12]), # TEXT - (DNSTextField("rdata", [], + (DNSTextField("rdata", [""], length_from=lambda pkt: pkt.rdlen), lambda pkt: pkt.type == 16), ], diff --git a/test/scapy/layers/dns.uts b/test/scapy/layers/dns.uts index a27bec3d91b..c0c1de8b016 100644 --- a/test/scapy/layers/dns.uts +++ b/test/scapy/layers/dns.uts @@ -213,6 +213,31 @@ p = DNS(raw(DNS(id=1,ra=1,qd=[],an=DNSRR(rrname='secdev', type='TXT', rdata=["sw assert p[DNS].an[0].rdata == [b"sweet", b"celestia"] assert raw(p) == b'\x00\x01\x01\x80\x00\x00\x00\x01\x00\x00\x00\x00\x06secdev\x00\x00\x10\x00\x01\x00\x00\x00\x01\x00\x0f\x05sweet\x08celestia' +# TXT RR with one empty string +b = b'\x05scapy\x00\x00\x10\x00\x01\x00\x00\x00\x00\x00\x01\x00' +rr = DNSRR(b) +assert rr.rdata == [b""] +assert rr.rdlen == 1 +rr.clear_cache() +assert DNSRR(raw(rr)).rdata == [b""] + +rr = DNSRR(rrname='scapy', type='TXT', rdata=[""]) +assert raw(rr) == b + +rr = DNSRR(rrname='scapy', type='TXT') +assert raw(rr) == b + +# TXT RR with zero-length RDATA +b = b'\x05scapy\x00\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00' +rr = DNSRR(b) +assert rr.rdata == [] +assert rr.rdlen == 0 +rr.clear_cache() +assert DNSRR(raw(rr)).rdata == [] + +rr = DNSRR(rrname='scapy', type='TXT', rdata=[]) +assert raw(rr) == b + = DNS - Malformed DNS over TCP message _old_dbg = conf.debug_dissector From 28e95575d16dd69a9f324fe6b9aab590b0bbb60b Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Fri, 17 Nov 2023 22:16:59 +0100 Subject: [PATCH 101/122] Disable enumerator tests --- test/contrib/automotive/scanner/enumerator.uts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/contrib/automotive/scanner/enumerator.uts b/test/contrib/automotive/scanner/enumerator.uts index 6e9eb198eb9..32d9cf1483a 100644 --- a/test/contrib/automotive/scanner/enumerator.uts +++ b/test/contrib/automotive/scanner/enumerator.uts @@ -1,4 +1,5 @@ % Regression tests for enumerators +~ disabled + Load general modules @@ -1075,4 +1076,4 @@ assert len(args) == 2 assert args["req"] == UDS()/UDS_DSC(b"\x03") assert "diagnosticSessionType" in args["desc"] and "extendedDiagnosticSession" in args["desc"] -assert not tce.enter_state(EcuState(session=1), EcuState(session=3)) \ No newline at end of file +assert not tce.enter_state(EcuState(session=1), EcuState(session=3)) From 6294c6e48c21bcc61a7ce24b27d3ceddd53d356f Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Fri, 17 Nov 2023 21:55:16 +0100 Subject: [PATCH 102/122] Fix geneve tests --- scapy/contrib/geneve.py | 12 ++++++------ test/contrib/geneve.uts | 8 +++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/scapy/contrib/geneve.py b/scapy/contrib/geneve.py index 60430fee1f8..b44cb543331 100644 --- a/scapy/contrib/geneve.py +++ b/scapy/contrib/geneve.py @@ -9,7 +9,7 @@ """ Geneve: Generic Network Virtualization Encapsulation -draft-ietf-nvo3-geneve-16 +https://datatracker.ietf.org/doc/html/rfc8926 """ import struct @@ -19,7 +19,6 @@ from scapy.layers.inet import IP, UDP from scapy.layers.inet6 import IPv6 from scapy.layers.l2 import Ether, ETHER_TYPES -from scapy.compat import chb, orb CLASS_IDS = {0x0100: "Linux", 0x0101: "Open vSwitch", @@ -42,12 +41,12 @@ class GeneveOptions(Packet): XByteField("type", 0x00), BitField("reserved", 0, 3), BitField("length", None, 5), - StrLenField('data', '', length_from=lambda x:x.length * 4)] + StrLenField('data', '', length_from=lambda x: x.length * 4)] def post_build(self, p, pay): if self.length is None: tmp_len = len(self.data) // 4 - p = p[:3] + struct.pack("!B", tmp_len) + p[4:] + p = p[:3] + struct.pack("!B", (p[3] & 0x3) | (tmp_len & 0x1f)) + p[4:] return p + pay @@ -61,12 +60,13 @@ class GENEVE(Packet): XShortEnumField("proto", 0x0000, ETHER_TYPES), X3BytesField("vni", 0), XByteField("reserved2", 0x00), - PacketListField("options", [], GeneveOptions, length_from=lambda pkt:pkt.optionlen * 4)] + PacketListField("options", [], GeneveOptions, + length_from=lambda pkt: pkt.optionlen * 4)] def post_build(self, p, pay): if self.optionlen is None: tmp_len = (len(p) - 8) // 4 - p = chb(tmp_len & 0x3f | orb(p[0]) & 0xc0) + p[1:] + p = struct.pack("!B", (p[0] & 0xc0) | (tmp_len & 0x3f)) + p[1:] return p + pay def answers(self, other): diff --git a/test/contrib/geneve.uts b/test/contrib/geneve.uts index e40533b3558..697a359f788 100644 --- a/test/contrib/geneve.uts +++ b/test/contrib/geneve.uts @@ -69,6 +69,8 @@ assert a.mysummary() in ['GENEVE (vni=0x0,optionlen=0,proto=0x800)', 'GENEVE (vn = GENEVE - Optionlen -data = raw(RandString(size=random.randint(0, pow(2, 6)) * 4)) -p = GENEVE(raw(GENEVE(options=GeneveOptions(data=data)))) -assert p[GENEVE].optionlen == (len(data) // 4 + 1) +for size in range(0, 0x1f, 4): + p = GENEVE(bytes(GENEVE(options=GeneveOptions(data=RandString(size))))) + assert p[GENEVE].optionlen == (size // 4 + 1) + assert len(p[GENEVE].options[0].data) == size + From cf8c254af5556caadd3fb82794a7702e6fc8f381 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Tue, 10 Oct 2023 09:18:34 +0000 Subject: [PATCH 103/122] Fix padwith in PadField --- scapy/fields.py | 7 ++----- test/regression.uts | 5 +++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/scapy/fields.py b/scapy/fields.py index 9c68ab002e9..f1acd8f616f 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -632,11 +632,8 @@ def addfield(self, ): # type: (...) -> bytes sval = self.fld.addfield(pkt, b"", val) - return s + sval + struct.pack( - "%is" % ( - self.padlen(len(sval), pkt) - ), - self._padwith + return s + sval + ( + self.padlen(len(sval), pkt) * self._padwith ) diff --git a/test/regression.uts b/test/regression.uts index c73e3d27321..2fbb322800a 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -1470,9 +1470,10 @@ assert p.len == 23 and len(p) == 29 ~ PadField padding class TestPad(Packet): - fields_desc = [ PadField(StrNullField("st", b""),4), StrField("id", b"")] + fields_desc = [ PadField(StrNullField("st", b""), 6, padwith=b"\xff"), StrField("id", b"")] -TestPad() == TestPad(raw(TestPad())) +assert TestPad() == TestPad(raw(TestPad())) +assert raw(TestPad(st=b"st", id=b"id")) == b'st\x00\xff\xff\xffid' = ReversePadField ~ PadField padding From 2150170eedea8a4f36b0b6019c9ee7d54f315768 Mon Sep 17 00:00:00 2001 From: Cooper de Nicola Date: Sat, 18 Nov 2023 16:13:56 -0800 Subject: [PATCH 104/122] Updated DDS RTPS Vendor IDs using DDS-Foundation spec (#4159) * Updated DDS RTPS Vendor IDs using DDS-Foundation spec * linter fix --------- Co-authored-by: Cooper de Nicola --- scapy/contrib/rtps/common_types.py | 32 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/scapy/contrib/rtps/common_types.py b/scapy/contrib/rtps/common_types.py index adf8cf8b30f..70bc8cc5770 100644 --- a/scapy/contrib/rtps/common_types.py +++ b/scapy/contrib/rtps/common_types.py @@ -279,23 +279,27 @@ def extract_padding(self, p): _rtps_vendor_ids = { 0x0000: "VENDOR_ID_UNKNOWN (0x0000)", - 0x0101: "Real-Time Innovations, Inc. - Connext DDS", - 0x0102: "PrismTech Inc. - OpenSplice DDS", - 0x0103: "Object Computing Incorporated, Inc. (OCI) - OpenDDS", - 0x0104: "MilSoft", - 0x0105: "Gallium Visual Systems Inc. - InterCOM DDS", - 0x0106: "TwinOaks Computing, Inc. - CoreDX DDS", + 0x0101: "Real-Time Innovations, Inc. (RTI) - Connext DDS", + 0x0102: "ADLink Ltd. - OpenSplice DDS", + 0x0103: "Object Computing Inc. (OCI) - OpenDDS", + 0x0104: "MilSoft - Mil-DDS", + 0x0105: "Kongsberg - InterCOM DDS", + 0x0106: "Twin Oaks Computing, Inc. - CoreDX DDS", 0x0107: "Lakota Technical Solutions, Inc.", 0x0108: "ICOUP Consulting", - 0x0109: "ETRI Electronics and Telecommunication Research Institute", + 0x0109: "Electronics and Telecommunication Research Institute (ETRI) - Diamond DDS", 0x010A: "Real-Time Innovations, Inc. (RTI) - Connext DDS Micro", - 0x010B: "PrismTech - OpenSplice Mobile", - 0x010C: "PrismTech - OpenSplice Gateway", - 0x010D: "PrismTech - OpenSplice Lite", - 0x010E: "Technicolor Inc. - Qeo", - 0x010F: "eProsima - Fast-RTPS", - 0x0110: "ADLINK - Cyclone DDS", - 0x0111: "GurumNetworks - GurumDDS", + 0x010B: "ADLink Ltd. - VortexCafe", + 0x010C: "PrismTech Ltd", + 0x010D: "ADLink Ltd. - Vortex Lite", + 0x010E: "Technicolor - Qeo", + 0x010F: "eProsima - FastRTPS, FastDDS", + 0x0110: "Eclipse Foundation - Cyclone DDS", + 0x0111: "Gurum Networks, Inc. - GurumDDS", + 0x0112: "Atostek - RustDDS", + 0x0113: "Nanjing Zhenrong Software Technology Co. \ + - Zhenrong Data Distribution Service (ZRDDS)", + 0x0114: "S2E Software Systems B.V. - Dust DDS", } From 2a841f8945e8921f1f09ecdfe244164db00a9181 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 18 Nov 2023 22:21:38 +0000 Subject: [PATCH 105/122] Improve main.interact() banner + support exts --- scapy/config.py | 141 +++++++++++++++++++++++++++++++++++++- scapy/contrib/__init__.py | 3 + scapy/layers/__init__.py | 3 + scapy/main.py | 51 ++++++++++---- scapy/modules/__init__.py | 3 + 5 files changed, 188 insertions(+), 13 deletions(-) diff --git a/scapy/config.py b/scapy/config.py index bc01f1019d6..1f2ff4b9eea 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -18,11 +18,19 @@ import time import warnings +from dataclasses import dataclass +from enum import Enum + import scapy from scapy import VERSION from scapy.base_classes import BasePacket from scapy.consts import DARWIN, WINDOWS, LINUX, BSD, SOLARIS -from scapy.error import log_scapy, warning, ScapyInvalidPlatformException +from scapy.error import ( + log_loading, + log_scapy, + ScapyInvalidPlatformException, + warning, +) from scapy.themes import ColorTheme, NoTheme, apply_ipython_style # Typing imports @@ -513,6 +521,132 @@ def __repr__(self): return "\n".join(c.summary() for c in self._caches_list) +class ScapyExt: + __slots__ = ["modules", "name", "version"] + + class MODE(Enum): + LAYERS = "layers" + CONTRIB = "contrib" + MODULES = "modules" + + @dataclass + class ScapyExtModule: + name: str + mode: 'ScapyExt.MODE' + module: Optional[ModuleType] + + def __init__(self): + self.modules: Dict[str, 'ScapyExt.ScapyExtModule'] = {} + + def config(self, name, version): + self.name = name + self.version = version + + def register(self, name, mode, module=None): + assert mode in self.MODE, "mode must be one of ScapyExt.MODE !" + self.modules[name] = self.ScapyExtModule(name, mode, module) + + def __repr__(self): + return "" % ( + self.name, + self.version, + len(self.modules), + ) + + +class ExtsManager: + __slots__ = ["exts", "_loaded"] + + SCAPY_PLUGIN_CLASSIFIER = 'Framework :: Scapy' + GPLV2_CLASSIFIERS = [ + 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', + 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', + ] + + def __init__(self): + self.exts: List[ScapyExt] = [] + self._loaded = [] + + def _register_module(self, name, mode, module): + sys.modules[f"scapy.{mode.value}.{name}"] = module + + def load(self): + """ + Find and loads Extensions. This is executed when Scapy loads. + An ext must include the Scapy Framework classifier, a scapy_ext func and be + under GPLv2. + """ + import importlib + import importlib.metadata + for distr in importlib.metadata.distributions(): + if any( + v == self.SCAPY_PLUGIN_CLASSIFIER + for k, v in distr.metadata.items() if k == 'Classifier' + ): + try: + pkg = next( + k + for k, v in importlib.metadata.packages_distributions().items() + if distr.name in v + ) + except KeyError: + pkg = distr.name + if pkg in self._loaded: + continue + if not any( + v in self.GPLV2_CLASSIFIERS + for k, v in distr.metadata.items() if k == 'Classifier' + ): + log_loading.warning( + "'%s' has no GPLv2 classifier therefore cannot be loaded." % pkg # noqa: E501 + ) + continue + self._loaded.append(pkg) + ext = ScapyExt() + try: + scapy_ext = importlib.import_module(pkg) + except Exception as ex: + log_loading.warning( + "'%s' failed during import with %s" % ( + pkg, + ex + ) + ) + continue + try: + scapy_ext_func = scapy_ext.scapy_ext + except AttributeError: + log_loading.info( + "Module '%s' included the Scapy Framework specifier " + "but did not include a scapy_ext" % pkg + ) + continue + try: + scapy_ext_func(ext) + except Exception as ex: + log_loading.warning( + "'%s' failed during initialization with %s" % ( + pkg, + ex + ) + ) + continue + for mod in ext.modules.values(): + self._register_module(mod.name, mod.mode, mod.module) + self.exts.append(ext) + + def __repr__(self): + from scapy.utils import pretty_list + return pretty_list( + [ + (x.name, x.version, [y.name for y in x.modules.values()]) + for x in self.exts + ], + [("Name", "Version", "Modules")], + sortBy=0, + ) + + def _version_checker(module, minver): # type: (ModuleType, Tuple[int, ...]) -> bool """Checks that module has a higher version that minver. @@ -893,6 +1027,7 @@ class Conf(ConfClass): #: a dict which can be used by contrib layers to store local #: configuration contribs = dict() # type: Dict[str, Any] + exts: ExtsManager = ExtsManager() crypto_valid = isCryptographyValid() crypto_valid_advanced = isCryptographyAdvanced() #: controls whether or not to display the fancy banner @@ -961,6 +1096,10 @@ def __getattribute__(self, attr): conf = Conf() # type: Conf +# Python 3.8 Only +if sys.version_info >= (3, 8): + conf.exts.load() + def crypto_validator(func): # type: (DecoratorCallable) -> DecoratorCallable diff --git a/scapy/contrib/__init__.py b/scapy/contrib/__init__.py index 82f83176f1d..8c54a8489ae 100644 --- a/scapy/contrib/__init__.py +++ b/scapy/contrib/__init__.py @@ -6,3 +6,6 @@ """ Package of contrib modules that have to be loaded explicitly. """ + +# Make sure config is loaded +import scapy.config # noqa: F401 diff --git a/scapy/layers/__init__.py b/scapy/layers/__init__.py index 79156832c8e..74f9906a2f4 100644 --- a/scapy/layers/__init__.py +++ b/scapy/layers/__init__.py @@ -6,3 +6,6 @@ """ Layer package. """ + +# Make sure config is loaded +import scapy.config # noqa: F401 diff --git a/scapy/main.py b/scapy/main.py index 254da828918..b7be643520a 100644 --- a/scapy/main.py +++ b/scapy/main.py @@ -352,6 +352,21 @@ def _scapy_builtins(): } +def _scapy_exts(): + # type: () -> Dict[str, Any] + """Load Scapy exts and return their builtins""" + from scapy.config import conf + res = {} + for ext in conf.exts.exts: + for mod in ext.modules.values(): + res.update({ + k: v + for k, v in mod.module.__dict__.copy().items() + if _validate_local(k) + }) + return res + + def save_session(fname="", session=None, pickleProto=-1): # type: (str, Optional[Dict[str, Any]], int) -> None """Save current Scapy session to the file specified in the fname arg. @@ -518,6 +533,9 @@ def init_session(session_name, # type: Optional[Union[str, None]] # Load Scapy scapy_builtins = _scapy_builtins() + # Load exts + scapy_builtins.update(_scapy_exts()) + SESSION.update(scapy_builtins) SESSION["_scpybuiltins"] = scapy_builtins.keys() builtins.__dict__["scapy_session"] = SESSION @@ -799,20 +817,36 @@ def ptpython_configure(repl): # Style.from_dict(_custom_ui_colorscheme)) # repl.use_ui_colorscheme("scapy") - # Start IPython or ptipython + # Extend banner text if conf.interactive_shell in ["ipython", "ptipython"]: import IPython if conf.interactive_shell == "ptipython": - from ptpython.ipython import embed banner = banner_text + " using IPython %s" % IPython.__version__ try: from importlib.metadata import version ptpython_version = " " + version('ptpython') except ImportError: ptpython_version = "" - banner += " and ptpython%s\n" % ptpython_version + banner += " and ptpython%s" % ptpython_version + else: + banner = banner_text + " using IPython %s" % IPython.__version__ + elif conf.interactive_shell == "ptpython": + try: + from importlib.metadata import version + ptpython_version = " " + version('ptpython') + except ImportError: + ptpython_version = "" + banner = banner_text + " using ptpython%s" % ptpython_version + elif conf.interactive_shell == "bpython": + import bpython + banner = banner_text + " using bpython %s" % bpython.__version__ + + # Start IPython or ptipython + if conf.interactive_shell in ["ipython", "ptipython"]: + if conf.interactive_shell == "ptipython": + from ptpython.ipython import embed + banner += "\n" else: - banner = banner_text + " using IPython %s\n" % IPython.__version__ from IPython import start_ipython as embed try: from traitlets.config.loader import Config @@ -867,14 +901,9 @@ def ptpython_configure(repl): # ptpython has special, non-default handling of __repr__ which breaks Scapy. # For instance: >>> IP() log_loading.warning("ptpython support is currently partially broken") - try: - from importlib.metadata import version - ptpython_version = " " + version('ptpython') - except ImportError: - ptpython_version = "" - banner = banner_text + " using ptpython%s" % ptpython_version from ptpython.repl import embed # ptpython has no banner option + banner += "\n" print(banner) embed( locals=SESSION, @@ -884,9 +913,7 @@ def ptpython_configure(repl): ) # Start bpython elif conf.interactive_shell == "bpython": - import bpython from bpython.curtsies import main as embed - banner = banner_text + " using bpython %s" % bpython.__version__ embed( args=["-q", "-i"], locals_=SESSION, diff --git a/scapy/modules/__init__.py b/scapy/modules/__init__.py index 0c399dc5ef3..1bf976f08af 100644 --- a/scapy/modules/__init__.py +++ b/scapy/modules/__init__.py @@ -6,3 +6,6 @@ """ Package of extension modules that have to be loaded explicitly. """ + +# Make sure config is loaded +import scapy.config # noqa: F401 From a1f0287f1460925a580876d2151e0216a1c0669b Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 19 Nov 2023 16:23:42 +0100 Subject: [PATCH 106/122] Cache `get_if_hwaddr` for SourceMACField (#4187) * Cache get_if_hwaddr for SourceMACField * Cache gateway before poisoning in connect_from_ip * Apply suggestion by guedou Co-authored-by: Guillaume Valadon * Fix PEP8 --------- Co-authored-by: Guillaume Valadon --- scapy/arch/libpcap.py | 10 ++++++++-- scapy/fields.py | 2 +- scapy/interfaces.py | 6 +++--- scapy/layers/inet.py | 12 ++++++++---- scapy/layers/l2.py | 7 ++----- test/linux.uts | 21 ++++++++++----------- 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/scapy/arch/libpcap.py b/scapy/arch/libpcap.py index fc4b210f4fa..ee25b16334c 100644 --- a/scapy/arch/libpcap.py +++ b/scapy/arch/libpcap.py @@ -432,15 +432,21 @@ def load(self): from scapy.arch import get_if_hwaddr try: mac = get_if_hwaddr(ifname) - except Exception: + except Exception as ex: # There are at least 3 different possible exceptions + log_loading.warning( + "Could not get MAC address of interface '%s': %s." % ( + ifname, + ex, + ) + ) continue if_data = { 'name': ifname, 'description': description or ifname, 'network_name': ifname, 'index': i, - 'mac': mac or '00:00:00:00:00:00', + 'mac': mac, 'ips': ips, 'flags': flags } diff --git a/scapy/fields.py b/scapy/fields.py index f1acd8f616f..d2813190097 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -781,7 +781,7 @@ def __init__(self, name, default): def i2m(self, pkt, x): # type: (Optional[Packet], Optional[str]) -> bytes - if x is None: + if not x: return b"\0\0\0\0\0\0" try: y = mac2str(x) diff --git a/scapy/interfaces.py b/scapy/interfaces.py index 6a4b51822d2..f40f529ea8b 100644 --- a/scapy/interfaces.py +++ b/scapy/interfaces.py @@ -291,8 +291,8 @@ def dev_from_index(self, if_index): return self.dev_from_networkname(conf.loopback_name) raise ValueError("Unknown network interface index %r" % if_index) - def _add_fake_iface(self, ifname): - # type: (str) -> None + def _add_fake_iface(self, ifname, mac="00:00:00:00:00:00"): + # type: (str, str) -> None """Internal function used for a testing purpose""" data = { 'name': ifname, @@ -300,7 +300,7 @@ def _add_fake_iface(self, ifname): 'network_name': ifname, 'index': -1000, 'dummy': True, - 'mac': '00:00:00:00:00:00', + 'mac': mac, 'flags': 0, 'ips': ["127.0.0.1", "::"], # Windows only diff --git a/scapy/layers/inet.py b/scapy/layers/inet.py index 247a2905f48..f46ed7f5e4e 100644 --- a/scapy/layers/inet.py +++ b/scapy/layers/inet.py @@ -1999,7 +1999,9 @@ def receive_data(self, pkt): # Answer with an Ack self.send(self.l4) # Process data - will be sent to the SuperSocket through this - self._transmit_packet(self.rcvbuf.process(pkt)) + pkt = self.rcvbuf.process(pkt) + if pkt: + self._transmit_packet(pkt) @ATMT.ioevent(ESTABLISHED, name="tcp", as_supersocket="tcplink") def outgoing_data_received(self, fd): @@ -2184,7 +2186,7 @@ class connect_from_ip: :param srcip: the IP to spoof. the cache of the gateway will be poisonned with this IP. :param poison: (optional, default True) ARP poison the gateway (or next hop), - so that it answers us. + so that it answers us (only one packet). :param timeout: (optional) the socket timeout. Example - Connect to 192.168.0.1:80 spoofing 192.168.0.2:: @@ -2207,14 +2209,15 @@ class connect_from_ip: resp = sock.sr1(HTTP() / HTTPRequest(Path="/")) """ - def __init__(self, host, port, srcip, poison=True, timeout=1): + def __init__(self, host, port, srcip, poison=True, timeout=1, debug=0): host = str(Net(host)) - # poison the next hop if poison: + # poison the next hop gateway = conf.route.route(host)[2] if gateway == "0.0.0.0": # on lan gateway = host + getmacbyip(gateway) # cache real gateway before poisoning arpcachepoison(gateway, srcip, count=1, interval=0, verbose=0) # create a socket pair self._sock, self.sock = socket.socketpair() @@ -2223,6 +2226,7 @@ def __init__(self, host, port, srcip, poison=True, timeout=1): host, port, srcip=srcip, external_fd={"tcp": self._sock}, + debug=debug, ) # start the TCP_client self.client.runbg() diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py index 06ceccebb14..07c8c0c652a 100644 --- a/scapy/layers/l2.py +++ b/scapy/layers/l2.py @@ -51,7 +51,7 @@ XShortEnumField, XShortField, ) -from scapy.interfaces import _GlobInterfaceType +from scapy.interfaces import _GlobInterfaceType, resolve_iface from scapy.packet import bind_layers, Packet from scapy.plist import ( PacketList, @@ -203,10 +203,7 @@ def i2h(self, pkt, x): if iff is None: iff = conf.iface if iff: - try: - x = get_if_hwaddr(iff) - except Exception as e: - warning("Could not get the source MAC: %s" % e) + x = resolve_iface(iff).mac if x is None: x = "00:00:00:00:00:00" return super(SourceMACField, self).i2h(pkt, x) diff --git a/test/linux.uts b/test/linux.uts index 48e27921cbd..33728a4c059 100644 --- a/test/linux.uts +++ b/test/linux.uts @@ -110,23 +110,22 @@ assert exit_status == 0 = IPv6 link-local address selection -conf.ifaces._add_fake_iface("scapy0") +conf.ifaces._add_fake_iface("scapy0", 'e2:39:91:79:19:10') from mock import patch conf.route6.routes = [('fe80::', 64, '::', 'scapy0', ['fe80::e039:91ff:fe79:1910'], 256)] conf.route6.ipv6_ifaces = set(['scapy0']) bck_conf_iface = conf.iface conf.iface = "scapy0" -with patch("scapy.layers.l2.get_if_hwaddr") as mgih: - mgih.return_value = 'e2:39:91:79:19:10' - p = Ether()/IPv6(dst="ff02::1")/ICMPv6NIQueryName(data="ff02::1") - print(p.sprintf("%Ether.src% > %Ether.dst%\n%IPv6.src% > %IPv6.dst%")) - ip6_ll_address = 'fe80::e039:91ff:fe79:1910' - print(p[IPv6].src, ip6_ll_address) - assert p[IPv6].src == ip6_ll_address - mac_address = 'e2:39:91:79:19:10' - print(p[Ether].src, mac_address) - assert p[Ether].src == mac_address + +p = Ether()/IPv6(dst="ff02::1")/ICMPv6NIQueryName(data="ff02::1") +print(p.sprintf("%Ether.src% > %Ether.dst%\n%IPv6.src% > %IPv6.dst%")) +ip6_ll_address = 'fe80::e039:91ff:fe79:1910' +print(p[IPv6].src, ip6_ll_address) +assert p[IPv6].src == ip6_ll_address +mac_address = 'e2:39:91:79:19:10' +print(p[Ether].src, mac_address) +assert p[Ether].src == mac_address conf.iface = bck_conf_iface conf.route6.resync() From 5160430bd16c6084d5aef2a10e47dc0455aace40 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 12 Nov 2023 15:14:11 +0100 Subject: [PATCH 107/122] Add SSHv2 (RFC 4250, 4251, 4252, 4253 and 4254) --- scapy/layers/ssh.py | 496 ++++++++++++++++++++++++++++++++++++ test/pcaps/ssh_ed25519.pcap | Bin 0 -> 8188 bytes test/scapy/layers/ssh.uts | 37 +++ 3 files changed, 533 insertions(+) create mode 100644 scapy/layers/ssh.py create mode 100644 test/pcaps/ssh_ed25519.pcap create mode 100644 test/scapy/layers/ssh.uts diff --git a/scapy/layers/ssh.py b/scapy/layers/ssh.py new file mode 100644 index 00000000000..a7fbbd52714 --- /dev/null +++ b/scapy/layers/ssh.py @@ -0,0 +1,496 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Secure Shell (SSH) Transport Layer Protocol + +RFC 4250, 4251, 4252, 4253 and 4254 +""" + +from scapy.config import conf +from scapy.compat import plain_str +from scapy.fields import ( + BitLenField, + ByteField, + ByteEnumField, + IntEnumField, + IntField, + PacketField, + PacketListField, + PacketLenField, + FieldLenField, + FieldListField, + StrLenField, + StrFixedLenField, + StrNullField, + YesNoByteField, +) +from scapy.packet import Packet, bind_bottom_up, bind_layers + +from scapy.layers.inet import TCP + + +class StrCRLFField(StrNullField): + DELIMITER = b"\r\n" + + +class _SSHHeaderField(FieldListField): + def getfield(self, pkt, s): + val = [] + while s: + s, v = self.field.getfield(pkt, s) + val.append(v) + if v[:4] == b"SSH-": + return s, val + return s, val + + +# RFC 4251 - SSH Architecture +# This RFC defines some types + +# RFC 4251 - sect 5 + + +class _ComaStrField(StrLenField): + islist = 1 + + def m2i(self, pkt, x): + return super(_ComaStrField, self).m2i(pkt, x).split(b",") + + def i2m(self, pkt, x): + return super(_ComaStrField, self).i2m(pkt, b",".join(x)) + + +class SSHString(Packet): + fields_desc = [ + FieldLenField("length", None, length_of="value", fmt="!I"), + StrLenField("value", 0, length_from=lambda pkt: pkt.length), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class SSHPacketStringField(PacketField): + __slots__ = ["sub_cls"] + + def __init__(self, name, sub_cls): + self.sub_cls = sub_cls + super(SSHPacketStringField, self).__init__(name, SSHString(), SSHString) + + def m2i(self, pkt, x): + x = super(SSHPacketStringField, self).m2i(pkt, x) + x.value = self.sub_cls(x.value) + return x + + +class NameList(Packet): + fields_desc = [ + FieldLenField("length", None, length_of="names", fmt="!I"), + _ComaStrField("names", [], length_from=lambda pkt: pkt.length), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class Mpint(Packet): + fields_desc = [ + FieldLenField("length", None, length_of="value", fmt="!I"), + BitLenField("value", 0, length_from=lambda pkt: pkt.length * 8), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +# RFC4250 - sect 4.1.2 + +_SSH_message_numbers = { + # RFC4253 - SSH-TRANS + 1: "SSH_MSG_DISCONNECT", + 2: "SSH_MSG_IGNORE", + 3: "SSH_MSG_UNIMPLEMENTED", + 4: "SSH_MSG_DEBUG", + 5: "SSH_MSG_SERVICE_REQUEST", + 6: "SSH_MSG_SERVICE_ACCEPT", + 7: "SSH_MSG_EXT_INFO", # RFC 8308 + 8: "SSH_MSG_NEWCOMPRESS", + 20: "SSH_MSG_KEXINIT", + 21: "SSH_MSG_NEWKEYS", + # Errata 152 of RFC4253 + 30: "SSH_MSG_KEXDH_INIT", + 31: "SSH_MSG_KEXDH_REPLY", + # RFC4252 - SSH-USERAUTH + 50: "SSH_MSG_USERAUTH_REQUEST", + 51: "SSH_MSG_USERAUTH_FAILURE", + 52: "SSH_MSG_USERAUTH_SUCCESS", + 53: "SSH_MSG_USERAUTH_BANNER", + # RFC4254 - SSH-CONNECT + 80: "SSH_MSG_GLOBAL_REQUEST", + 81: "SSH_MSG_REQUEST_SUCCESS", + 82: "SSH_MSG_REQUEST_FAILURE", + 90: "SSH_MSG_CHANNEL_OPEN", + 91: "SSH_MSG_CHANNEL_OPEN_CONFIRMATION", + 92: "SSH_MSG_CHANNEL_OPEN_FAILURE", + 93: "SSH_MSG_CHANNEL_WINDOW_ADJUST", + 94: "SSH_MSG_CHANNEL_DATA", + 95: "SSH_MSG_CHANNEL_EXTENDED_DATA", + 96: "SSH_MSG_CHANNEL_EOF", + 97: "SSH_MSG_CHANNEL_CLOSE", + 98: "SSH_MSG_CHANNEL_REQUEST", + 99: "SSH_MSG_CHANNEL_SUCCESS", + 100: "SSH_MSG_CHANNEL_FAILURE", +} + +# RFC4253 - sect 6 + +_SSH_messages = {} + + +def _SSHPayload(x, **kwargs): + return _SSH_messages.get(x and x[0], conf.raw_layer)(x) + + +class SSH(Packet): + name = "SSH - Binary Packet" + fields_desc = [ + IntField("packet_length", None), + ByteField("padding_length", None), + PacketLenField( + "pay", + None, + _SSHPayload, + length_from=lambda pkt: pkt.packet_length - pkt.padding_length - 1, + ), + StrLenField("random_padding", b"", length_from=lambda pkt: pkt.padding_length), + # StrField("mac", b""), + ] + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 4 and _pkt[:4] == b"SSH-": + return SSHVersionExchange + return cls + + def mysummary(self): + if self.pay: + if isinstance(self.pay, conf.raw_layer): + return "SSH type " + str(self.pay.load[0]), [TCP, SSH] + return "SSH " + self.pay.sprintf("%type%"), [TCP, SSH] + return "SSH", [TCP, SSH] + + +# RFC4253 - sect 4.2 + + +class SSHVersionExchange(Packet): + name = "SSH - Protocol Version Exchange" + fields_desc = [ + _SSHHeaderField( + "lines", + [], + StrCRLFField("", b""), + ) + ] + + def mysummary(self): + return "SSH - Version Exchange %s" % plain_str(self.lines[-1]), [TCP] + + +# RFC4253 - sect 6.6 + +_SSH_certificates = {} +_SSH_publickeys = {} +_SSH_signatures = {} + + +class _SSHCertificate(PacketField): + def m2i(self, pkt, x): + return _SSH_certificates.get(pkt.format_identifier.value, self.cls)(x) + + +class _SSHPublicKey(PacketField): + def m2i(self, pkt, x): + return _SSH_publickeys.get(pkt.format_identifier.value, self.cls)(x) + + +class _SSHSignature(PacketField): + def m2i(self, pkt, x): + return _SSH_signatures.get(pkt.format_identifier.value, self.cls)(x) + + +class SSHCertificate(Packet): + fields_desc = [ + PacketField("format_identifier", SSHString(), SSHString), + _SSHCertificate("data", None, conf.raw_layer), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class SSHPublicKey(Packet): + fields_desc = [ + PacketField("format_identifier", SSHString(), SSHString), + _SSHPublicKey("data", None, conf.raw_layer), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class SSHSignature(Packet): + fields_desc = [ + PacketField("format_identifier", SSHString(), SSHString), + _SSHSignature("data", None, conf.raw_layer), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +# RFC4253 - sect 7.1 + + +class SSHKexInit(Packet): + fields_desc = [ + ByteEnumField("type", 20, _SSH_message_numbers), + StrFixedLenField("cookie", b"", length=16), + PacketField("kex_algorithms", NameList(), NameList), + PacketField("server_host_key_algorithms", NameList(), NameList), + PacketField("encryption_algorithms_client_to_server", NameList(), NameList), + PacketField("encryption_algorithms_server_to_client", NameList(), NameList), + PacketField("mac_algorithms_client_to_server", NameList(), NameList), + PacketField("mac_algorithms_server_to_client", NameList(), NameList), + PacketField("compression_algorithms_client_to_server", NameList(), NameList), + PacketField("compression_algorithms_server_to_client", NameList(), NameList), + PacketField("languages_client_to_server", NameList(), NameList), + PacketField("languages_server_to_client", NameList(), NameList), + YesNoByteField("first_kex_packet_follows", 0), + IntField("reserved", 0), + ] + + +_SSH_messages[20] = SSHKexInit + +# RFC4253 - sect 7.3 + + +class SSHNewKeys(Packet): + fields_desc = [ + ByteEnumField("type", 21, _SSH_message_numbers), + ] + + +_SSH_messages[21] = SSHNewKeys + + +# RFC4253 - sect 8 + + +class SSHKexDHInit(Packet): + fields_desc = [ + ByteEnumField("type", 30, _SSH_message_numbers), + PacketField("e", Mpint(), Mpint), + ] + + +_SSH_messages[30] = SSHKexDHInit + + +class SSHKexDHReply(Packet): + fields_desc = [ + ByteEnumField("type", 31, _SSH_message_numbers), + SSHPacketStringField("K_S", SSHPublicKey), + PacketField("f", Mpint(), Mpint), + SSHPacketStringField("H_hash", SSHSignature), + ] + + +_SSH_messages[31] = SSHKexDHReply + +# RFC4253 - sect 10 + + +class SSHServiceRequest(Packet): + fields_desc = [ + ByteEnumField("type", 5, _SSH_message_numbers), + PacketField("service_name", SSHString(), SSHString), + ] + + +_SSH_messages[5] = SSHServiceRequest + + +class SSHServiceAccept(Packet): + fields_desc = [ + ByteEnumField("type", 6, _SSH_message_numbers), + PacketField("service_name", SSHString(), SSHString), + ] + + +_SSH_messages[6] = SSHServiceAccept + +# RFC4253 - sect 11.1 + + +class SSHDisconnect(Packet): + fields_desc = [ + ByteEnumField("type", 1, _SSH_message_numbers), + IntEnumField( + "reason_code", + 0, + { + 1: "SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT", + 2: "SSH_DISCONNECT_PROTOCOL_ERROR", + 3: "SSH_DISCONNECT_KEY_EXCHANGE_FAILED", + 4: "SSH_DISCONNECT_RESERVED", + 5: "SSH_DISCONNECT_MAC_ERROR", + 6: "SSH_DISCONNECT_COMPRESSION_ERROR", + 7: "SSH_DISCONNECT_SERVICE_NOT_AVAILABLE", + 8: "SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED", + 9: "SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE", + 10: "SSH_DISCONNECT_CONNECTION_LOST", + 11: "SSH_DISCONNECT_BY_APPLICATION", + 12: "SSH_DISCONNECT_TOO_MANY_CONNECTIONS", + 13: "SSH_DISCONNECT_AUTH_CANCELLED_BY_USER", + 14: "SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE", + 15: "SSH_DISCONNECT_ILLEGAL_USER_NAME", + }, + ), + PacketField("description", SSHString(), SSHString), + PacketField("language_tag", SSHString(), SSHString), + ] + + +_SSH_messages[1] = SSHDisconnect + +# RFC4253 - sect 11.2 + + +class SSHIgnore(Packet): + fields_desc = [ + ByteEnumField("type", 2, _SSH_message_numbers), + PacketField("data", SSHString(), SSHString), + ] + + +_SSH_messages[2] = SSHIgnore + +# RFC4253 - sect 11.3 + + +class SSHServiceDebug(Packet): + fields_desc = [ + ByteEnumField("type", 4, _SSH_message_numbers), + YesNoByteField("always_display", 0), + PacketField("message", SSHString(), SSHString), + PacketField("language_tag", SSHString(), SSHString), + ] + + +_SSH_messages[4] = SSHServiceDebug + +# RFC4253 - sect 11.4 + + +class SSHUnimplemented(Packet): + fields_desc = [ + ByteEnumField("type", 3, _SSH_message_numbers), + IntField("seq_num", 0), + ] + + +_SSH_messages[3] = SSHUnimplemented + +# RFC8308 - sect 2.3 + + +class SSHExtension(Packet): + fields_desc = [ + PacketField("extension_name", SSHString(), SSHString), + PacketField("extension_value", SSHString(), SSHString), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class SSHExtInfo(Packet): + fields_desc = [ + ByteEnumField("type", 7, _SSH_message_numbers), + FieldLenField("nr_extensions", None, length_of="extensions"), + PacketListField("extensions", [], SSHExtension), + ] + + +_SSH_messages[7] = SSHExtInfo + +# RFC8308 - sect 3.2 + + +class SSHNewCompress(Packet): + fields_desc = [ + ByteEnumField("type", 3, _SSH_message_numbers), + ] + + +_SSH_messages[8] = SSHNewCompress + +# RFC8709 + + +class SSHPublicKeyEd25519(Packet): + fields_desc = [ + PacketField("key", SSHString(), SSHString), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_SSH_publickeys[b"ssh-ed25519"] = SSHPublicKeyEd25519 + + +class SSHPublicKeyEd448(Packet): + fields_desc = [ + PacketField("key", SSHString(), SSHString), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_SSH_publickeys[b"ssh-ed448"] = SSHPublicKeyEd448 + + +class SSHSignatureEd25519(Packet): + fields_desc = [ + PacketField("key", SSHString(), SSHString), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_SSH_signatures[b"ssh-ed25519"] = SSHSignatureEd25519 + + +class SSHSignatureEd448(Packet): + fields_desc = [ + PacketField("key", SSHString(), SSHString), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_SSH_signatures[b"ssh-ed448"] = SSHSignatureEd448 + +bind_layers(SSH, SSH) + +bind_bottom_up(TCP, SSH, sport=22) +bind_layers(TCP, SSH, dport=22) diff --git a/test/pcaps/ssh_ed25519.pcap b/test/pcaps/ssh_ed25519.pcap new file mode 100644 index 0000000000000000000000000000000000000000..8d10143541a6f474bcbe2914f7600cd1e71b40c6 GIT binary patch literal 8188 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 Raf4$JsJet$bu@%{=|3UgoE888 literal 0 HcmV?d00001 diff --git a/test/scapy/layers/ssh.uts b/test/scapy/layers/ssh.uts new file mode 100644 index 00000000000..b6c82830c9e --- /dev/null +++ b/test/scapy/layers/ssh.uts @@ -0,0 +1,37 @@ +% SSH regression tests for Scapy + ++ SSH tests + += Load SSH and SSH pcap + +from scapy.layers.ssh import * +pkts = rdpcap(scapy_path("/test/pcaps/ssh_ed25519.pcap")) + += Check for SSHVersionExchange + +assert SSHVersionExchange in pkts[3] +assert pkts[3].lines == [b'SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u1'] + += Check for SSH KexInit + +assert pkts[8].pay.type == 20 +assert pkts[8].pay.kex_algorithms.names == [b'sntrup761x25519-sha512@openssh.com', b'curve25519-sha256', b'curve25519-sha256@libssh.org', b'ecdh-sha2-nistp256', b'ecdh-sha2-nistp384', b'ecdh-sha2-nistp521', b'diffie-hellman-group-exchange-sha256', b'diffie-hellman-group16-sha512', b'diffie-hellman-group18-sha512', b'diffie-hellman-group14-sha256'] +assert pkts[8].pay.compression_algorithms_client_to_server.names == [b'none', b'zlib@openssh.com'] +assert pkts[8].pay.first_kex_packet_follows == 0 + += Check for SSH Kex DH Init + +assert pkts[9].pay.e.value == 2350579254774455149352841074576538343990628078324267734527140871419932900538846241321452574779013468175018290651674793284015144587365340014962083771650859331476013596977123734998706481058518220105378857548427645559226229797476788395456646389818256564838400739135010680681163456095677232493468587230912056633743356721223955966756143014970734820639779746710511156996619195651512803235508645669051962031830043263352566212925544898655158819407252176433755590900240990111833619058714386338971655960765233885975850331922799954445954999296511309262036243757363804224821843032668273263064448847356873248470173458896243397517402866118125112555466428030166305568609671333602038983517505792245686243281766138834921907336198416449172686346486278292406454736650900489703602941311383274571253473117352192402069818356338637658276508681778175858698292872544113899589241205559671386224999719303843179598966006636814844667908804397048115462945951578163865384872376314062218622823399683168509281546378022234848416282276373248755506541244682438394426446625521247772730497030387798046748675856950435919346613694108323269457293633349708282520556429147475379754811181108827452704284587405668562299209768965780662855380191554093502874303015220051947466960323628390298028334555285261078171376959928596505395834904631983924930632066431620277301098016669514461539415396461120090857273167687140578309080011600491384868409678444601854368584606594031430672989514629489628693896623746874263590779323124144977231406480674784862894820443935735009785417326990153059454525335480905124180809168192695170554860881795940302149730125034860576014797577021907464402342887433222178995989216549376816454492040151004613910636289921361266911700572137074963622410473946132501034758965925448585513804452940921661377181371668124810916661795313840472724039889785883499146147917756012320556865741641210760458632895093416475238323450214341924898457846364890041182141641464569458439289152005646151462927271290045368143992045845833755058875621892664952349241777000175525150234301417133611325716295866942917806328460205857145603834255471170372323643072574234093090258963511137747272416302757220165305443240348220594316744411327905569373474701071080394539231361841208268626239088329642014216286141161796678306068065801822739617419769840590119143287370990841250896367782388086153939896000077437989471526045058990545907990089943059323343511158016141104461822684535344848072465778114834380180144470998703244485996404968078774187171096252472206846575112317045051585989901412734220984229769099373462781991394933642599850052765470790711255335450011529534223200229563089616493679704133500670071803056311472370457584460617950784473886654145020569892882621834458180061425761806601776138949785217977296683442504030198235793054259542236826904098257847794792743244091577002001218915723232763883537852453240602434246755006330557239933 + += Check for SSH Kex DH Reply + +assert isinstance(pkts[10].pay, SSHKexDHReply) +assert isinstance(pkts[10].pay.K_S.value.data, SSHPublicKeyEd25519) +assert pkts[10].pay.f.value == 145420364842225773825302401106325914711274265993324154430728894326534621359109155840425186538544552052796050053335958730866886288740744420249345515750154798851184330959497070659898985425204715378366354679146309457749164371561091155243958216182101971799434050687511559028317449152411472762323723877627671103812914723157350965167617881557068354019877391362267976527576493473875435265184048851428107514944286989616342786043599413975131699425817361398892615937862444397978104862748600515902989933687456311656405442430739088222322061894563421315591443786569893421006874109563323602421056664468719115729999515282373592751468532774515579030227333862046510775187524340678261311443115463596625632382798119365245475607690175500571706486276645913449452000600385503347151840872914773898773488397031589360149311688536059026933073591802120869627115324168091970764557964769308675365930500125154235572029870366848539246435374954851006770023189648291776010080795050223050860998900405137902471191697225277049222592746894837282272020541849100564888026189233806723871439229668619801557051355230295711162074261723735096669381118352514087748543069098521714367520620776857909533548692973709024859908263199571215346407936984296807266382121546828903054910125941912141681820440324847067053005923257053547200527533308902169030187411617725866120378101642906954603853930588700927719183637036840650380578915269559991390749067662922590313459051714023483342069069486856997828131877064697883838294364044597377634856362822832142618450301805844505073311557951656608292385708401134544514223462642265767599035258374748229336714718608533685329531126529049892131138601419901815421341388895007293701087086445997233255224283053634387459108049782685439584490166669027769404082346078709263888381794126372684739109951124329930500566714883267402922809647283904702829959640898613561998011861738175990862617646085551086342592758425640217942375761120002214263525285687683437628809639146334705175599606153814250017075639638206689953262483413749172593472713439934441043308651524160071237216451477801106668255062822659635335764848170476026942604710330092513922989750910298327358097823488084536544440798321307308541642435897397586864585774450444856007727437988290169282904777426371810586287022758237175995926455562260123808781040290584381913532810485127812346450200037604344159195037050778864761984776712383681923622054756893185075573777827838180404632794820045063257647197822508971656160962510350864007240071912329456453627835389896781002210494913596666104457655076724437210855739938617334378596008363125551567605259368940675801716 +assert isinstance(pkts[10].pay.H_hash.value.data, SSHSignatureEd25519) +assert pkts[10].pay.H_hash.value.data.key.value == b"\xef\xecj=~\xe4Y'\xe9\xad\xb7?\xfe?[\xf3\xddn\x1e\x91\xb5\x1c\xb6O\xf5&\xc7$\x9f\x0c\xeb\x1c<\xf2n\x87iH\xb9\xaf\xf5\xdfXB\xb7\x99\xd1\xbe%\x92\x98a)+\x01\\\xa7\xb4\xb2\x82!\x05e\x0e" + += Check for the 2 SSH New Msgs + +assert isinstance(pkts[10][SSH:2].pay, SSHNewKeys) +assert isinstance(pkts[12].pay, SSHNewKeys) From 1cd3aa44db01283bff3ffe9267d2dd04cf24b263 Mon Sep 17 00:00:00 2001 From: TAKAHiRO TOMiNAGA <52474650+kokoichi206@users.noreply.github.com> Date: Tue, 21 Nov 2023 00:55:03 +0900 Subject: [PATCH 108/122] Fix documentation (#4181) --- doc/scapy/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/scapy/usage.rst b/doc/scapy/usage.rst index e78c0947f46..c32e2420b52 100644 --- a/doc/scapy/usage.rst +++ b/doc/scapy/usage.rst @@ -391,7 +391,7 @@ The above will send a single SYN packet to Google's port 80 and will quit after From the above output, we can see Google returned “SA” or SYN-ACK flags indicating an open port. -Use either notations to scan ports 400 through 443 on the system: +Use either notations to scan ports 440 through 443 on the system: >>> sr(IP(dst="192.168.1.1")/TCP(sport=666,dport=(440,443),flags="S")) From dd0e497f0f7b916017e1288031b5ed3cdc044417 Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Mon, 20 Nov 2023 18:55:38 +0200 Subject: [PATCH 109/122] L2ListenTcpdump: Support quiet mode (#4172) --- scapy/supersocket.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scapy/supersocket.py b/scapy/supersocket.py index 0a05749f214..caf8b0e23b9 100644 --- a/scapy/supersocket.py +++ b/scapy/supersocket.py @@ -484,6 +484,7 @@ def __init__(self, filter=None, # type: Optional[str] nofilter=False, # type: bool prog=None, # type: Optional[str] + quiet=False, # type: bool *arg, # type: Any **karg # type: Any ): @@ -507,7 +508,8 @@ def __init__(self, filter = "not (%s)" % conf.except_filter if filter is not None: args.append(filter) - self.tcpdump_proc = tcpdump(None, prog=prog, args=args, getproc=True) + self.tcpdump_proc = tcpdump( + None, prog=prog, args=args, getproc=True, quiet=quiet) self.reader = PcapReader(self.tcpdump_proc.stdout) self.ins = self.reader # type: ignore From ef1875acdcd6ae65d30311c093288f40fc0e1b9d Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Mon, 20 Nov 2023 21:24:52 +0000 Subject: [PATCH 110/122] enable enumerator unit tests for linux --- test/contrib/automotive/scanner/enumerator.uts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/contrib/automotive/scanner/enumerator.uts b/test/contrib/automotive/scanner/enumerator.uts index 32d9cf1483a..70f725a51ce 100644 --- a/test/contrib/automotive/scanner/enumerator.uts +++ b/test/contrib/automotive/scanner/enumerator.uts @@ -1,5 +1,5 @@ % Regression tests for enumerators -~ disabled +~ linux + Load general modules From b9482979fb4619e711629f131af58871b9fef497 Mon Sep 17 00:00:00 2001 From: Gal Fudim <32942415+galfudim@users.noreply.github.com> Date: Wed, 8 Nov 2023 08:46:20 -0500 Subject: [PATCH 111/122] Fixed typo in routing.rst --- doc/scapy/routing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/scapy/routing.rst b/doc/scapy/routing.rst index 8355ce0c352..883259622da 100644 --- a/doc/scapy/routing.rst +++ b/doc/scapy/routing.rst @@ -105,7 +105,7 @@ Get the route for a specific IP: :py:func:`conf.route.route() ` +Same as IPv4 but with :py:attr:`conf.route6 ` Get default gateway IP address ------------------------------ From 83918924c3227cd16427451ad8ea24766d9db456 Mon Sep 17 00:00:00 2001 From: Omer Rosenbaum <52040016+Omerr@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:55:03 +0200 Subject: [PATCH 112/122] Fix broken documentation link --- doc/scapy/layers/tcp.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/scapy/layers/tcp.rst b/doc/scapy/layers/tcp.rst index 23bac6f5033..18972139ee1 100644 --- a/doc/scapy/layers/tcp.rst +++ b/doc/scapy/layers/tcp.rst @@ -61,6 +61,6 @@ Use external projects - `muXTCP`_ - Writing your own flexible Userland TCP/IP Stack - Ninja Style!!! - Integrating `pynids`_ -.. _Automata's documentation: ../advanced_usage#automata +.. _Automata's documentation: ../advanced_usage.html#automata .. _muXTCP: http://events.ccc.de/congress/2005/fahrplan/events/529.en.html .. _pynids: http://jon.oberheide.org/pynids/ From 389cb53fc213f99d0104f8c825f9de02504c42da Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 22 Nov 2023 21:54:22 +0100 Subject: [PATCH 113/122] IPython banner: restore \n --- scapy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scapy/main.py b/scapy/main.py index b7be643520a..0b33007b0f2 100644 --- a/scapy/main.py +++ b/scapy/main.py @@ -843,9 +843,9 @@ def ptpython_configure(repl): # Start IPython or ptipython if conf.interactive_shell in ["ipython", "ptipython"]: + banner += "\n" if conf.interactive_shell == "ptipython": from ptpython.ipython import embed - banner += "\n" else: from IPython import start_ipython as embed try: From 1477ddd5328ed85e81856903b40fea3a1744daba Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Thu, 23 Nov 2023 07:54:21 +0100 Subject: [PATCH 114/122] readthedocs: fix renamed extra requirement --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 6c006d87efd..8084a8d4a7f 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -24,4 +24,4 @@ python: - method: pip path: . extra_requirements: - - docs + - doc From 088d58ac2aee30091ad389b3625e46dffd651647 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 23 Nov 2023 09:10:10 +0100 Subject: [PATCH 115/122] Improve parsing of UDS DTC Snapshots and ExtendedData (#4149) * Improve parsing of UDS DTC Snapshots and ExtendedData * fix unit test * fix unit test * fix unit test --- scapy/contrib/automotive/uds.py | 103 ++++++++++++------ test/contrib/automotive/gm/scanner.uts | 6 + .../automotive/scanner/uds_scanner.uts | 5 + test/contrib/automotive/uds.uts | 37 ++++--- 4 files changed, 102 insertions(+), 49 deletions(-) diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 5d67394bc04..f69bdd5d0a8 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -11,6 +11,7 @@ """ import struct +from collections import defaultdict from scapy.fields import ByteEnumField, StrField, ConditionalField, \ BitEnumField, BitField, XByteField, FieldListField, \ @@ -18,7 +19,7 @@ ShortField, ObservableDict, XShortEnumField, XByteEnumField, StrLenField, \ FieldLenField, XStrFixedLenField, XStrLenField, FlagsField, PacketListField, \ PacketField -from scapy.packet import Packet, bind_layers, NoPayload +from scapy.packet import Packet, bind_layers, NoPayload, Raw from scapy.config import conf from scapy.error import log_loading from scapy.utils import PeriodicSenderThread @@ -42,6 +43,9 @@ conf.contribs['UDS'] = {'treat-response-pending-as-answer': False} +conf.debug_dissector = True + + class UDS(ISOTP): services = ObservableDict( {0x10: 'DiagnosticSessionControl', @@ -908,6 +912,30 @@ def answers(self, other): bind_layers(UDS, UDS_WMBAPR, service=0x7D) +# ##########################DTC##################################### +class DTC(Packet): + name = 'Diagnostic Trouble Code' + dtc_descriptions = {} # Customize this dictionary for each individual ECU / OEM + + fields_desc = [ + BitEnumField("system", 0, 2, { + 0: "Powertrain", + 1: "Chassis", + 2: "Body", + 3: "Network"}), + BitEnumField("type", 0, 2, { + 0: "Generic", + 1: "ManufacturerSpecific", + 2: "Generic", + 3: "Generic"}), + BitField("numeric_value_code", 0, 12), + ByteField("additional_information_code", 0), + ] + + def extract_padding(self, s): + return '', s + + # #########################CDTCI################################### class UDS_CDTCI(Packet): name = 'ClearDiagnosticInformation' @@ -992,13 +1020,7 @@ class UDS_RDTCI(Packet): ConditionalField(FlagsField('DTCStatusMask', 0, 8, dtcStatusMask), lambda pkt: pkt.reportType in [ 0x01, 0x02, 0x07, 0x08, 0x0f, 0x11, 0x12, 0x13]), - ConditionalField(ByteField('DTCHighByte', 0), - lambda pkt: pkt.reportType in [0x3, 0x4, 0x6, - 0x10, 0x09]), - ConditionalField(ByteField('DTCMiddleByte', 0), - lambda pkt: pkt.reportType in [0x3, 0x4, 0x6, - 0x10, 0x09]), - ConditionalField(ByteField('DTCLowByte', 0), + ConditionalField(PacketField("dtc", None, pkt_cls=DTC), lambda pkt: pkt.reportType in [0x3, 0x4, 0x6, 0x10, 0x09]), ConditionalField(ByteField('DTCSnapshotRecordNumber', 0), @@ -1011,29 +1033,6 @@ class UDS_RDTCI(Packet): bind_layers(UDS, UDS_RDTCI, service=0x19) -class DTC(Packet): - name = 'Diagnostic Trouble Code' - dtc_descriptions = {} # Customize this dictionary for each individual ECU / OEM - - fields_desc = [ - BitEnumField("system", 0, 2, { - 0: "Powertrain", - 1: "Chassis", - 2: "Body", - 3: "Network"}), - BitEnumField("type", 0, 2, { - 0: "Generic", - 1: "ManufacturerSpecific", - 2: "Generic", - 3: "Generic"}), - BitField("numeric_value_code", 0, 12), - ByteField("additional_information_code", 0), - ] - - def extract_padding(self, s): - return '', s - - class DTCAndStatusRecord(Packet): name = 'DTC and status record' fields_desc = [ @@ -1048,7 +1047,6 @@ def extract_padding(self, s): class DTCExtendedData(Packet): name = 'Diagnostic Trouble Code Extended Data' dataTypes = ObservableDict() - fields_desc = [ ByteEnumField("data_type", 0, dataTypes), XByteField("record", 0) @@ -1065,6 +1063,33 @@ class DTCExtendedDataRecord(Packet): ] +class DTCSnapshot(Packet): + identifiers = defaultdict(list) # for later extension + + @staticmethod + def next_identifier_cb(pkt, lst, cur, remain): + return Raw + + fields_desc = [ + ByteField("record_number", 0), + ByteField("record_number_of_identifiers", 0), + PacketListField( + "snapshotData", None, + next_cls_cb=lambda pkt, lst, cur, remain: DTCSnapshot.next_identifier_cb( + pkt, lst, cur, remain)) + ] + + def extract_padding(self, s): + return '', s + + +class DTCSnapshotRecord(Packet): + fields_desc = [ + PacketField("dtcAndStatus", None, pkt_cls=DTCAndStatusRecord), + PacketListField("snapshots", None, pkt_cls=DTCSnapshot) + ] + + class UDS_RDTCIPR(Packet): name = 'ReadDTCInformationPositiveResponse' fields_desc = [ @@ -1092,14 +1117,24 @@ class UDS_RDTCIPR(Packet): ConditionalField(StrField('dataRecord', b""), lambda pkt: pkt.reportType in [0x03, 0x08, 0x09, 0x10, 0x14]), + ConditionalField(PacketField('snapshotRecord', None, + pkt_cls=DTCSnapshotRecord), + lambda pkt: pkt.reportType in [0x04]), ConditionalField(PacketField('extendedDataRecord', None, pkt_cls=DTCExtendedDataRecord), lambda pkt: pkt.reportType in [0x06]) ] def answers(self, other): - return isinstance(other, UDS_RDTCI) \ - and other.reportType == self.reportType + if not isinstance(other, UDS_RDTCI): + return False + if not other.reportType == self.reportType: + return False + if self.reportType == 0x06: + return other.dtc == self.extendedDataRecord.dtcAndStatus.dtc + if self.reportType == 0x04: + return other.dtc == self.snapshotRecord.dtcAndStatus.dtc + return True bind_layers(UDS, UDS_RDTCIPR, service=0x59) diff --git a/test/contrib/automotive/gm/scanner.uts b/test/contrib/automotive/gm/scanner.uts index 6a232d94c14..59a5e265dc6 100644 --- a/test/contrib/automotive/gm/scanner.uts +++ b/test/contrib/automotive/gm/scanner.uts @@ -108,6 +108,11 @@ config = {} s = EcuState(session=1) +debug_dissector_backup = conf.debug_dissector + +# This tests involves corrupted Packets, therefore we need to disable the debug_dissector +conf.debug_dissector = False + assert False == e._evaluate_response(s, GMLAN(b"\x27\x01"), None, **config) config = {"exit_if_service_not_supported": True} assert not e._retry_pkt[s] @@ -125,6 +130,7 @@ assert True == e._evaluate_response(s, GMLAN(b"\x27\x01"), GMLAN(b"\x67\x01ab"), assert not e._retry_pkt[s] assert False == e._evaluate_response(s, GMLAN(b"\x27\x01"), GMLAN(b"\x67\x02ab"), **config) assert not e._retry_pkt[s] +conf.debug_dissector = debug_dissector_backup = Simulate ECU and run Scanner diff --git a/test/contrib/automotive/scanner/uds_scanner.uts b/test/contrib/automotive/scanner/uds_scanner.uts index 8dc67970ae0..df72230dd8d 100644 --- a/test/contrib/automotive/scanner/uds_scanner.uts +++ b/test/contrib/automotive/scanner/uds_scanner.uts @@ -276,8 +276,13 @@ resps = [EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSup es = [UDS_ServiceEnumerator] +debug_dissector_backup = conf.debug_dissector + +# This Enumerator is sending corrupted Packets, therefore we need to disable the debug_dissector +conf.debug_dissector = False scanner = executeScannerInVirtualEnvironment( resps, es, UDS_ServiceEnumerator_kwargs={"request_length": 3}, unstable_socket=False) +conf.debug_dissector = debug_dissector_backup assert scanner.scan_completed assert scanner.progress() > 0.95 diff --git a/test/contrib/automotive/uds.uts b/test/contrib/automotive/uds.uts index 8a05bf4ac34..2c58d781e32 100644 --- a/test/contrib/automotive/uds.uts +++ b/test/contrib/automotive/uds.uts @@ -1100,9 +1100,7 @@ assert rdtci.DTCStatusMask == 0xff rdtci = UDS(b'\x19\x03\xff\xee\xdd\xaa') assert rdtci.service == 0x19 assert rdtci.reportType == 0x03 -assert rdtci.DTCHighByte == 0xff -assert rdtci.DTCMiddleByte == 0xee -assert rdtci.DTCLowByte == 0xdd +assert rdtci.dtc == DTC(bytes.fromhex("ffeedd")) assert rdtci.DTCSnapshotRecordNumber == 0xaa = Check UDS_RDTCI @@ -1110,9 +1108,7 @@ assert rdtci.DTCSnapshotRecordNumber == 0xaa rdtci = UDS(b'\x19\x04\xff\xee\xdd\xaa') assert rdtci.service == 0x19 assert rdtci.reportType == 0x04 -assert rdtci.DTCHighByte == 0xff -assert rdtci.DTCMiddleByte == 0xee -assert rdtci.DTCLowByte == 0xdd +assert rdtci.dtc == DTC(bytes.fromhex("ffeedd")) assert rdtci.DTCSnapshotRecordNumber == 0xaa = Check UDS_RDTCI @@ -1127,9 +1123,7 @@ assert rdtci.DTCSnapshotRecordNumber == 0xaa rdtci = UDS(b'\x19\x06\xff\xee\xdd\xaa') assert rdtci.service == 0x19 assert rdtci.reportType == 0x06 -assert rdtci.DTCHighByte == 0xff -assert rdtci.DTCMiddleByte == 0xee -assert rdtci.DTCLowByte == 0xdd +assert rdtci.dtc == DTC(bytes.fromhex("ffeedd")) assert rdtci.DTCExtendedDataRecordNumber == 0xaa = Check UDS_RDTCI @@ -1153,18 +1147,14 @@ assert rdtci.DTCStatusMask == 0xbb rdtci = UDS(b'\x19\x09\xff\xee\xdd') assert rdtci.service == 0x19 assert rdtci.reportType == 0x09 -assert rdtci.DTCHighByte == 0xff -assert rdtci.DTCMiddleByte == 0xee -assert rdtci.DTCLowByte == 0xdd +assert rdtci.dtc == DTC(bytes.fromhex("ffeedd")) = Check UDS_RDTCI rdtci = UDS(b'\x19\x10\xff\xee\xdd\xaa') assert rdtci.service == 0x19 assert rdtci.reportType == 0x10 -assert rdtci.DTCHighByte == 0xff -assert rdtci.DTCMiddleByte == 0xee -assert rdtci.DTCLowByte == 0xdd +assert rdtci.dtc == DTC(bytes.fromhex("ffeedd")) assert rdtci.DTCExtendedDataRecordNumber == 0xaa @@ -1190,6 +1180,23 @@ assert rdtcipr.service == 0x59 assert rdtcipr.reportType == 3 assert rdtcipr.dataRecord == b'\xff\xee\xdd\xaa' + += Check UDS_RDTCIPR 2 +req = UDS(bytes.fromhex("1904480a46ff")) +resp = UDS(bytes.fromhex("5904480a46af000b170002ff6417010a8278fa170c2ff1800000800104800200028003400a8004808005054002400a400004010b170002ff6417010a82ec69170c2f2c800000800100800200028003400a80048080050540024017400004")) + +assert resp.answers(req) + +req = UDS(bytes.fromhex("1904480a47ff")) +resp = UDS(bytes.fromhex("5904480a46af000b170002ff6417010a8278fa170c2ff1800000800104800200028003400a8004808005054002400a400004010b170002ff6417010a82ec69170c2f2c800000800100800200028003400a80048080050540024017400004")) + +assert not resp.answers(req) + +req = UDS(bytes.fromhex("1906480a46ff")) +resp = UDS(bytes.fromhex("5906480a46af010002070328")) + +assert resp.answers(req) + = Check UDS_RC rc = UDS(b'\x31\x03\xff\xee\xdd\xaa') From 5a2dbf08d76367ef8c392c9ff9c62674c9732d2f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 24 Nov 2023 13:22:23 +0100 Subject: [PATCH 116/122] Add additional errno handling to ISOTPNativeSocket to prevent accidental close (#4193) --- scapy/contrib/isotp/isotp_native_socket.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scapy/contrib/isotp/isotp_native_socket.py b/scapy/contrib/isotp/isotp_native_socket.py index 34a77cb84aa..7718365c954 100644 --- a/scapy/contrib/isotp/isotp_native_socket.py +++ b/scapy/contrib/isotp/isotp_native_socket.py @@ -380,7 +380,13 @@ def recv_raw(self, x=0xffff): "Increasing `stmin` could solve this problem.") elif e.errno == 110: log_isotp.warning('Captured no data, socket read timed out.') + elif e.errno == 70: + log_isotp.warning( + 'Communication error on send. ' + 'TX path flowcontrol reception timeout.') else: + log_isotp.error( + 'Unknown error code received %d. Closing socket!', e.errno) self.close() return None, None, None From 3eee39e1b13f90ac256038d693b5f70d8647c9f2 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Tue, 28 Nov 2023 22:10:38 +0100 Subject: [PATCH 117/122] Type hint arch/bpf (#3825) * Type hint arch/bpf * Enable mypy for more libs --- .config/mypy/mypy_enabled.txt | 11 ++- scapy/arch/bpf/consts.py | 17 +++-- scapy/arch/bpf/core.py | 32 ++++++-- scapy/arch/bpf/supersocket.py | 135 +++++++++++++++++++++------------- scapy/arch/linux.py | 13 ++-- test/bpf.uts | 3 +- 6 files changed, 141 insertions(+), 70 deletions(-) diff --git a/.config/mypy/mypy_enabled.txt b/.config/mypy/mypy_enabled.txt index 6238db12561..076316559bc 100644 --- a/.config/mypy/mypy_enabled.txt +++ b/.config/mypy/mypy_enabled.txt @@ -10,11 +10,15 @@ scapy/__main__.py scapy/all.py scapy/ansmachine.py scapy/arch/__init__.py +scapy/arch/bpf/__init__.py +scapy/arch/bpf/consts.py +scapy/arch/bpf/core.py +scapy/arch/bpf/supersocket.py scapy/arch/common.py scapy/arch/libpcap.py scapy/arch/linux.py -scapy/arch/unix.py scapy/arch/solaris.py +scapy/arch/unix.py scapy/arch/windows/__init__.py scapy/arch/windows/native.py scapy/arch/windows/structures.py @@ -87,7 +91,12 @@ scapy/contrib/roce.py scapy/contrib/tcpao.py # LIBS +scapy/libs/__init__.py +scapy/libs/ethertypes.py scapy/libs/extcap.py +scapy/libs/matplot.py +scapy/libs/structures.py +scapy/libs/test_pyx.py # TEST test/testsocket.py diff --git a/scapy/arch/bpf/consts.py b/scapy/arch/bpf/consts.py index 3ea84277496..df207a3397f 100644 --- a/scapy/arch/bpf/consts.py +++ b/scapy/arch/bpf/consts.py @@ -12,6 +12,12 @@ from scapy.libs.structures import bpf_program from scapy.data import MTU +# Type hints +from typing import ( + Any, + Callable, +) + SIOCGIFFLAGS = 0xc0206911 BPF_BUFFER_LENGTH = MTU @@ -23,19 +29,20 @@ IOC_IN = 0x80000000 IOC_INOUT = IOC_IN | IOC_OUT -_th = lambda x: x if isinstance(x, int) else ctypes.sizeof(x) +_th = lambda x: x if isinstance(x, int) else ctypes.sizeof(x) # type: Callable[[Any], int] # noqa: E501 def _IOC(inout, group, num, len): + # type: (int, str, int, Any) -> int return (inout | ((_th(len) & IOCPARM_MASK) << 16) | (ord(group) << 8) | (num)) -_IO = lambda g, n: _IOC(IOC_VOID, g, n, 0) -_IOR = lambda g, n, t: _IOC(IOC_OUT, g, n, t) -_IOW = lambda g, n, t: _IOC(IOC_IN, g, n, t) -_IOWR = lambda g, n, t: _IOC(IOC_INOUT, g, n, t) +_IO = lambda g, n: _IOC(IOC_VOID, g, n, 0) # type: Callable[[str, int], int] +_IOR = lambda g, n, t: _IOC(IOC_OUT, g, n, t) # type: Callable[[str, int, Any], int] +_IOW = lambda g, n, t: _IOC(IOC_IN, g, n, t) # type: Callable[[str, int, Any], int] +_IOWR = lambda g, n, t: _IOC(IOC_INOUT, g, n, t) # type: Callable[[str, int, Any], int] # Length of some structures _bpf_stat = 8 diff --git a/scapy/arch/bpf/core.py b/scapy/arch/bpf/core.py index 76eb7796cef..b7c31ff8807 100644 --- a/scapy/arch/bpf/core.py +++ b/scapy/arch/bpf/core.py @@ -31,9 +31,18 @@ InterfaceProvider, NetworkInterface, network_name, + _GlobInterfaceType, ) from scapy.pton_ntop import inet_ntop +# Typing +from typing import ( + Dict, + List, + Optional, + Tuple, +) + if LINUX: raise OSError("BPF conflicts with Linux") @@ -67,7 +76,10 @@ class if_nameindex(Structure): def get_if_raw_addr(ifname): - """Returns the IPv4 address configured on 'ifname', packed with inet_pton.""" # noqa: E501 + # type: (_GlobInterfaceType) -> bytes + """ + Returns the IPv4 address configured on 'ifname', packed with inet_pton. + """ ifname = network_name(ifname) @@ -99,6 +111,7 @@ def get_if_raw_addr(ifname): def get_if_raw_hwaddr(ifname): + # type: (_GlobInterfaceType) -> Tuple[int, bytes] """Returns the packed MAC address configured on 'ifname'.""" NULL_MAC_ADDRESS = b'\x00' * 6 @@ -128,19 +141,19 @@ def get_if_raw_hwaddr(ifname): raise Scapy_Exception("No MAC address found on %s !" % ifname) # Pack and return the MAC address - mac = addresses[0].split(' ')[1] - mac = [chr(int(b, 16)) for b in mac.split(':')] + mac = [int(b, 16) for b in addresses[0].split(' ')[1].split(':')] # Check that the address length is correct if len(mac) != 6: raise Scapy_Exception("No MAC address found on %s !" % ifname) - return (ARPHDR_ETHER, ''.join(mac)) + return (ARPHDR_ETHER, struct.pack("!BBBBBB", *mac)) # BPF specific functions def get_dev_bpf(): + # type: () -> Tuple[int, int] """Returns an opened BPF file object""" # Get the first available BPF handle @@ -160,6 +173,7 @@ def get_dev_bpf(): def attach_filter(fd, bpf_filter, iface): + # type: (int, str, _GlobInterfaceType) -> None """Attach a BPF filter to the BPF file descriptor""" bp = compile_filter(bpf_filter, iface) # Assign the BPF program to the interface @@ -171,6 +185,7 @@ def attach_filter(fd, bpf_filter, iface): # Interface manipulation functions def _get_ifindex_list(): + # type: () -> List[Tuple[str, int]] """ Returns a list containing (iface, index) """ @@ -189,6 +204,7 @@ def _get_ifindex_list(): def _get_if_flags(ifname): + # type: (_GlobInterfaceType) -> Optional[int] """Internal function to get interface flags""" # Get interface flags try: @@ -206,6 +222,7 @@ class BPFInterfaceProvider(InterfaceProvider): name = "BPF" def _is_valid(self, dev): + # type: (NetworkInterface) -> bool if not dev.flags & 0x1: # not IFF_UP return False # Get a BPF handle @@ -228,17 +245,20 @@ def _is_valid(self, dev): os.close(fd) def load(self): + # type: () -> Dict[str, NetworkInterface] from scapy.fields import FlagValue data = {} ips = in6_getifaddr() for ifname, index in _get_ifindex_list(): try: - ifflags = _get_if_flags(ifname) + ifflags_int = _get_if_flags(ifname) + if ifflags_int is None: + continue mac = scapy.utils.str2mac(get_if_raw_hwaddr(ifname)[1]) ip = inet_ntop(socket.AF_INET, get_if_raw_addr(ifname)) except Scapy_Exception: continue - ifflags = FlagValue(ifflags, _iff_flags) + ifflags = FlagValue(ifflags_int, _iff_flags) if_data = { "name": ifname, "network_name": ifname, diff --git a/scapy/arch/bpf/supersocket.py b/scapy/arch/bpf/supersocket.py index 3e57a64d1b0..06a6dbc9012 100644 --- a/scapy/arch/bpf/supersocket.py +++ b/scapy/arch/bpf/supersocket.py @@ -8,6 +8,8 @@ """ from select import select + +import abc import ctypes import errno import fcntl @@ -36,10 +38,22 @@ from scapy.consts import DARWIN, FREEBSD, NETBSD from scapy.data import ETH_P_ALL, DLT_IEEE802_11_RADIO from scapy.error import Scapy_Exception, warning -from scapy.interfaces import network_name +from scapy.interfaces import network_name, _GlobInterfaceType from scapy.supersocket import SuperSocket from scapy.compat import raw +# Typing +from typing import ( + Any, + List, + Optional, + Tuple, + Type, + TYPE_CHECKING, +) +if TYPE_CHECKING: + from scapy.packet import Packet + # Structures & c types if FREEBSD or NETBSD: @@ -61,7 +75,7 @@ class bpf_timeval(ctypes.Structure): _fields_ = [("tv_sec", ctypes.c_ulong), ("tv_usec", ctypes.c_ulong)] else: - class bpf_timeval(ctypes.Structure): + class bpf_timeval(ctypes.Structure): # type: ignore _fields_ = [("tv_sec", ctypes.c_uint32), ("tv_usec", ctypes.c_uint32)] @@ -81,19 +95,26 @@ class bpf_hdr(ctypes.Structure): class _L2bpfSocket(SuperSocket): """"Generic Scapy BPF Super Socket""" + __slots__ = ["bpf_fd"] desc = "read/write packets using BPF" nonblocking_socket = True - def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, - nofilter=0, monitor=False): + def __init__(self, + iface=None, # type: Optional[_GlobInterfaceType] + type=ETH_P_ALL, # type: int + promisc=None, # type: Optional[bool] + filter=None, # type: Optional[str] + nofilter=0, # type: int + monitor=False, # type: bool + ): if monitor: raise Scapy_Exception( "We do not natively support monitor mode on BPF. " "Please turn on libpcap using conf.use_pcap = True" ) - self.fd_flags = None + self.fd_flags = None # type: Optional[int] self.assigned_interface = None # SuperSocket mandatory variables @@ -104,15 +125,13 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, self.iface = network_name(iface or conf.iface) # Get the BPF handle - self.ins = None - (self.ins, self.dev_bpf) = get_dev_bpf() - self.outs = self.ins + self.bpf_fd, self.dev_bpf = get_dev_bpf() if FREEBSD: # Set the BPF timeval format. Availability issues here ! try: fcntl.ioctl( - self.ins, BIOCSTSTAMP, + self.bpf_fd, BIOCSTSTAMP, struct.pack('I', BPF_T_NANOTIME) ) except IOError: @@ -121,7 +140,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, # Set the BPF buffer length try: fcntl.ioctl( - self.ins, BIOCSBLEN, + self.bpf_fd, BIOCSBLEN, struct.pack('I', BPF_BUFFER_LENGTH) ) except IOError: @@ -131,7 +150,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, # Assign the network interface to the BPF handle try: fcntl.ioctl( - self.ins, BIOCSETIF, + self.bpf_fd, BIOCSETIF, struct.pack("16s16x", self.iface.encode()) ) except IOError: @@ -140,7 +159,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, # Set the interface into promiscuous if self.promisc: - self.set_promisc(1) + self.set_promisc(True) # Set the interface to monitor mode # Note: - trick from libpcap/pcap-bpf.c - monitor_mode() @@ -160,7 +179,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, if macos_version < 101500: dlt_radiotap = struct.pack('I', DLT_IEEE802_11_RADIO) try: - fcntl.ioctl(self.ins, BIOCSDLT, dlt_radiotap) + fcntl.ioctl(self.bpf_fd, BIOCSDLT, dlt_radiotap) except IOError: raise Scapy_Exception("Can't set %s into monitor mode!" % self.iface) @@ -170,7 +189,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, # Don't block on read try: - fcntl.ioctl(self.ins, BIOCIMMEDIATE, struct.pack('I', 1)) + fcntl.ioctl(self.bpf_fd, BIOCIMMEDIATE, struct.pack('I', 1)) except IOError: raise Scapy_Exception("BIOCIMMEDIATE failed on /dev/bpf%i" % self.dev_bpf) @@ -178,7 +197,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, # Scapy will provide the link layer source address # Otherwise, it is written by the kernel try: - fcntl.ioctl(self.ins, BIOCSHDRCMPLT, struct.pack('i', 1)) + fcntl.ioctl(self.bpf_fd, BIOCSHDRCMPLT, struct.pack('i', 1)) except IOError: raise Scapy_Exception("BIOCSHDRCMPLT failed on /dev/bpf%i" % self.dev_bpf) @@ -193,7 +212,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, filter = "not (%s)" % conf.except_filter if filter is not None: try: - attach_filter(self.ins, filter, self.iface) + attach_filter(self.bpf_fd, filter, self.iface) filter_attached = True except ImportError as ex: warning("Cannot set filter: %s" % ex) @@ -204,7 +223,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, # more than ensuring the length frame is not null. filter = "greater 0" try: - attach_filter(self.ins, filter, self.iface) + attach_filter(self.bpf_fd, filter, self.iface) except ImportError as ex: warning("Cannot set filter: %s" % ex) @@ -212,15 +231,17 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, self.guessed_cls = self.guess_cls() def set_promisc(self, value): + # type: (bool) -> None """Set the interface in promiscuous mode""" try: - fcntl.ioctl(self.ins, BIOCPROMISC, struct.pack('i', value)) + fcntl.ioctl(self.bpf_fd, BIOCPROMISC, struct.pack('i', value)) except IOError: raise Scapy_Exception("Cannot set promiscuous mode on interface " "(%s)!" % self.iface) def __del__(self): + # type: () -> None """Close the file descriptor on delete""" # When the socket is deleted on Scapy exits, __del__ is # sometimes called "too late", and self is None @@ -228,12 +249,13 @@ def __del__(self): self.close() def guess_cls(self): + # type: () -> type """Guess the packet class that must be used on the interface""" # Get the data link type try: - ret = fcntl.ioctl(self.ins, BIOCGDLT, struct.pack('I', 0)) - ret = struct.unpack('I', ret)[0] + ret = fcntl.ioctl(self.bpf_fd, BIOCGDLT, struct.pack('I', 0)) + linktype = struct.unpack('I', ret)[0] except IOError: cls = conf.default_l2 warning("BIOCGDLT failed: unable to guess type. Using %s !", @@ -242,18 +264,20 @@ def guess_cls(self): # Retrieve the corresponding class try: - return conf.l2types[ret] + return conf.l2types.num2layer[linktype] except KeyError: cls = conf.default_l2 - warning("Unable to guess type (type %i). Using %s", ret, cls.name) + warning("Unable to guess type (type %i). Using %s", linktype, cls.name) + return cls def set_nonblock(self, set_flag=True): + # type: (bool) -> None """Set the non blocking flag on the socket""" # Get the current flags if self.fd_flags is None: try: - self.fd_flags = fcntl.fcntl(self.ins, fcntl.F_GETFL) + self.fd_flags = fcntl.fcntl(self.bpf_fd, fcntl.F_GETFL) except IOError: warning("Cannot get flags on this file descriptor !") return @@ -265,50 +289,58 @@ def set_nonblock(self, set_flag=True): new_fd_flags = self.fd_flags & ~os.O_NONBLOCK try: - fcntl.fcntl(self.ins, fcntl.F_SETFL, new_fd_flags) + fcntl.fcntl(self.bpf_fd, fcntl.F_SETFL, new_fd_flags) self.fd_flags = new_fd_flags except Exception: warning("Can't set flags on this file descriptor !") def get_stats(self): + # type: () -> Tuple[Optional[int], Optional[int]] """Get received / dropped statistics""" try: - ret = fcntl.ioctl(self.ins, BIOCGSTATS, struct.pack("2I", 0, 0)) + ret = fcntl.ioctl(self.bpf_fd, BIOCGSTATS, struct.pack("2I", 0, 0)) return struct.unpack("2I", ret) except IOError: warning("Unable to get stats from BPF !") return (None, None) def get_blen(self): + # type: () -> Optional[int] """Get the BPF buffer length""" try: - ret = fcntl.ioctl(self.ins, BIOCGBLEN, struct.pack("I", 0)) - return struct.unpack("I", ret)[0] + ret = fcntl.ioctl(self.bpf_fd, BIOCGBLEN, struct.pack("I", 0)) + return struct.unpack("I", ret)[0] # type: ignore except IOError: warning("Unable to get the BPF buffer length") - return + return None def fileno(self): + # type: () -> int """Get the underlying file descriptor""" - return self.ins + return self.bpf_fd def close(self): + # type: () -> None """Close the Super Socket""" - if not self.closed and self.ins is not None: - os.close(self.ins) + if not self.closed and self.bpf_fd != -1: + os.close(self.bpf_fd) self.closed = True - self.ins = None + self.bpf_fd = -1 + @abc.abstractmethod def send(self, x): + # type: (Packet) -> int """Dummy send method""" raise Exception( "Can't send anything with %s" % self.__class__.__name__ ) + @abc.abstractmethod def recv_raw(self, x=BPF_BUFFER_LENGTH): + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 """Dummy recv method""" raise Exception( "Can't recv anything with %s" % self.__class__.__name__ @@ -316,6 +348,7 @@ def recv_raw(self, x=BPF_BUFFER_LENGTH): @staticmethod def select(sockets, remain=None): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] """This function is called during sendrecv() routine to select the available sockets. """ @@ -327,14 +360,17 @@ class L2bpfListenSocket(_L2bpfSocket): """"Scapy L2 BPF Listen Super Socket""" def __init__(self, *args, **kwargs): - self.received_frames = [] + # type: (*Any, **Any) -> None + self.received_frames = [] # type: List[Tuple[Optional[type], Optional[bytes], Optional[float]]] # noqa: E501 super(L2bpfListenSocket, self).__init__(*args, **kwargs) def buffered_frames(self): + # type: () -> int """Return the number of frames in the buffer""" return len(self.received_frames) def get_frame(self): + # type: () -> Tuple[Optional[type], Optional[bytes], Optional[float]] """Get a frame or packet from the received list""" if self.received_frames: return self.received_frames.pop(0) @@ -343,12 +379,14 @@ def get_frame(self): @staticmethod def bpf_align(bh_h, bh_c): + # type: (int, int) -> int """Return the index to the end of the current packet""" # from return ((bh_h + bh_c) + (BPF_ALIGNMENT - 1)) & ~(BPF_ALIGNMENT - 1) def extract_frames(self, bpf_buffer): + # type: (bytes) -> None """ Extract all frames from the buffer and stored them in the received list """ @@ -381,6 +419,7 @@ def extract_frames(self, bpf_buffer): self.extract_frames(bpf_buffer[end:]) def recv_raw(self, x=BPF_BUFFER_LENGTH): + # type: (int) -> Tuple[Optional[type], Optional[bytes], Optional[float]] """Receive a frame from the network""" x = min(x, BPF_BUFFER_LENGTH) @@ -391,7 +430,7 @@ def recv_raw(self, x=BPF_BUFFER_LENGTH): # Get data from BPF try: - bpf_buffer = os.read(self.ins, x) + bpf_buffer = os.read(self.bpf_fd, x) except EnvironmentError as exc: if exc.errno != errno.EAGAIN: warning("BPF recv_raw()", exc_info=True) @@ -406,10 +445,12 @@ class L2bpfSocket(L2bpfListenSocket): """"Scapy L2 BPF Super Socket""" def send(self, x): + # type: (Packet) -> int """Send a frame""" - return os.write(self.outs, raw(x)) + return os.write(self.bpf_fd, raw(x)) def nonblock_recv(self): + # type: () -> Optional[Packet] """Non blocking receive""" if self.buffered_frames(): @@ -425,7 +466,7 @@ def nonblock_recv(self): class L3bpfSocket(L2bpfSocket): - def recv(self, x=BPF_BUFFER_LENGTH, **kwargs): + def recv(self, x: int = BPF_BUFFER_LENGTH, **kwargs: Any) -> Optional['Packet']: """Receive on layer 3""" r = SuperSocket.recv(self, x, **kwargs) if r: @@ -434,6 +475,7 @@ def recv(self, x=BPF_BUFFER_LENGTH, **kwargs): return r def send(self, pkt): + # type: (Packet) -> int """Send a packet""" from scapy.layers.l2 import Loopback @@ -445,7 +487,7 @@ def send(self, pkt): # Assign the network interface to the BPF handle if self.assigned_interface != iff: try: - fcntl.ioctl(self.outs, BIOCSETIF, struct.pack("16s16x", iff.encode())) # noqa: E501 + fcntl.ioctl(self.bpf_fd, BIOCSETIF, struct.pack("16s16x", iff.encode())) # noqa: E501 except IOError: raise Scapy_Exception("BIOCSETIF failed on %s" % iff) self.assigned_interface = iff @@ -476,7 +518,7 @@ def send(self, pkt): # the problem will eventually go away. They already don't work on Macs # with Apple Silicon (M1). if DARWIN and iff.startswith('tun') and self.guessed_cls == Loopback: - frame = raw(pkt) + frame = pkt elif FREEBSD and (iff.startswith('tun') or iff.startswith('tap')): # On FreeBSD, the bpf manpage states that it is only possible # to write packets to Ethernet and SLIP network interfaces @@ -487,36 +529,29 @@ def send(self, pkt): warning("Cannot write to %s according to the documentation!", iff) return else: - frame = raw(self.guessed_cls() / pkt) + frame = self.guessed_cls() / pkt pkt.sent_time = time.time() # Send the frame - L2bpfSocket.send(self, frame) + return L2bpfSocket.send(self, frame) # Sockets manipulation functions -def isBPFSocket(obj): - """Return True is obj is a BPF Super Socket""" - return isinstance( - obj, - (L2bpfListenSocket, L2bpfListenSocket, L3bpfSocket) - ) - - def bpf_select(fds_list, timeout=None): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] """A call to recv() can return several frames. This functions hides the fact that some frames are read from the internal buffer.""" # Check file descriptors types - bpf_scks_buffered = list() + bpf_scks_buffered = list() # type: List[SuperSocket] select_fds = list() for tmp_fd in fds_list: # Specific BPF sockets: get buffers status - if isBPFSocket(tmp_fd) and tmp_fd.buffered_frames(): + if isinstance(tmp_fd, L2bpfListenSocket) and tmp_fd.buffered_frames(): bpf_scks_buffered.append(tmp_fd) continue diff --git a/scapy/arch/linux.py b/scapy/arch/linux.py index 58100e5479a..b79cda1315e 100644 --- a/scapy/arch/linux.py +++ b/scapy/arch/linux.py @@ -44,6 +44,7 @@ InterfaceProvider, NetworkInterface, network_name, + _GlobInterfaceType, ) from scapy.libs.structures import sock_fprog from scapy.packet import Packet, Padding @@ -121,7 +122,7 @@ def get_if_raw_addr(iff): - # type: (Union[NetworkInterface, str]) -> bytes + # type: (_GlobInterfaceType) -> bytes r""" Return the raw IPv4 address of an interface. If unavailable, returns b"\0\0\0\0" @@ -147,7 +148,7 @@ def _get_if_list(): def attach_filter(sock, bpf_filter, iface): - # type: (socket.socket, str, Union[NetworkInterface, str]) -> None + # type: (socket.socket, str, _GlobInterfaceType) -> None """ Compile bpf filter and attach it to a socket @@ -159,17 +160,17 @@ def attach_filter(sock, bpf_filter, iface): if conf.use_pypy and sys.pypy_version_info <= (7, 3, 2): # type: ignore # PyPy < 7.3.2 has a broken behavior # https://foss.heptapod.net/pypy/pypy/-/issues/3298 - bp = struct.pack( + bp = struct.pack( # type: ignore 'HL', bp.bf_len, ctypes.addressof(bp.bf_insns.contents) ) else: - bp = sock_fprog(bp.bf_len, bp.bf_insns) + bp = sock_fprog(bp.bf_len, bp.bf_insns) # type: ignore sock.setsockopt(socket.SOL_SOCKET, SO_ATTACH_FILTER, bp) def set_promisc(s, iff, val=1): - # type: (socket.socket, Union[NetworkInterface, str], int) -> None + # type: (socket.socket, _GlobInterfaceType, int) -> None mreq = struct.pack("IHH8s", get_if_index(iff), PACKET_MR_PROMISC, 0, b"") if val: cmd = PACKET_ADD_MEMBERSHIP @@ -407,7 +408,7 @@ def proc2r(p): def get_if_index(iff): - # type: (Union[NetworkInterface, str]) -> int + # type: (_GlobInterfaceType) -> int return int(struct.unpack("I", get_if(iff, SIOCGIFINDEX)[16:20])[0]) diff --git a/test/bpf.uts b/test/bpf.uts index fa4db648f5d..13bfb1e2bfb 100644 --- a/test/bpf.uts +++ b/test/bpf.uts @@ -50,8 +50,7 @@ len(iflist) > 0 = Misc functions ~ needs_root -from scapy.arch.bpf.supersocket import isBPFSocket, bpf_select -isBPFSocket(L2bpfListenSocket()) and isBPFSocket(L2bpfSocket()) and isBPFSocket(L3bpfSocket()) +from scapy.arch.bpf.supersocket import bpf_select l = bpf_select([L2bpfSocket()]) l = bpf_select([L2bpfSocket(), sys.stdin.fileno()]) From cb6dcf47f81bc2377b97c9d4fe96c8d4aced9c85 Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Sat, 2 Dec 2023 00:33:56 +0000 Subject: [PATCH 118/122] DNS: tweak extract_padding in EDNS(0) options Without this patch EDNS0ClientSubnet and EDNS0ExtendedDNSError consume all the bytes (including all the options following them): ```sh >>> DNSRROPT(raw(DNSRROPT(rdata=[EDNS0ExtendedDNSError(), EDNS0TLV()]))).rdata [>] ``` With this patch applied the options are fully split: ```sh >>> DNSRROPT(raw(DNSRROPT(rdata=[EDNS0ExtendedDNSError(), EDNS0TLV()]))).rdata [, ] ``` --- scapy/layers/dns.py | 6 ++++++ test/scapy/layers/dns_edns0.uts | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 94cf6d78144..ec1f9d8d3c3 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -510,6 +510,9 @@ class EDNS0ClientSubnet(Packet): ClientSubnetv4("address", "192.168.0.0", length_from=lambda p: p.source_plen))] + def extract_padding(self, p): + return "", p + # RFC 8914 - Extended DNS Errors @@ -558,6 +561,9 @@ class EDNS0ExtendedDNSError(Packet): StrLenField("extra_text", "", length_from=lambda pkt: pkt.optlen - 2)] + def extract_padding(self, p): + return "", p + # RFC 4034 - Resource Records for the DNS Security Extensions diff --git a/test/scapy/layers/dns_edns0.uts b/test/scapy/layers/dns_edns0.uts index bd186694afa..02385d845e0 100644 --- a/test/scapy/layers/dns_edns0.uts +++ b/test/scapy/layers/dns_edns0.uts @@ -119,3 +119,7 @@ rropt = DNSRROPT(b'\x00\x00)\x04\xd0\x00\x00\x00\x00\x001\x00\x0f\x00-\x00\x06pr assert len(rropt.rdata) == 1 p = rropt.rdata[0] assert p.info_code == 6 and p.optlen == 45 and p.extra_text == b'proof of non-existence of example.com. NSEC' + +p = DNSRROPT(raw(DNSRROPT(rdata=[EDNS0ExtendedDNSError(), EDNS0ClientSubnet(), EDNS0TLV()]))) +assert len(p.rdata) == 3 +assert all(Raw not in opt for opt in p.rdata) From e80b3d4457a30c361beb31e53e416cddb5bbbbb4 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 26 Nov 2023 15:35:44 +0000 Subject: [PATCH 119/122] hexdiff: 2 algorithms, doc --- scapy/utils.py | 128 +++++++++++++++++++++++++++++++++----------- test/regression.uts | 10 ++-- 2 files changed, 102 insertions(+), 36 deletions(-) diff --git a/scapy/utils.py b/scapy/utils.py index 1906390f18c..5720ed90e10 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -44,7 +44,12 @@ hex_bytes, bytes_encode, ) -from scapy.error import log_runtime, Scapy_Exception, warning +from scapy.error import ( + log_interactive, + log_runtime, + Scapy_Exception, + warning, +) from scapy.pton_ntop import inet_pton # Typing imports @@ -392,50 +397,111 @@ def repr_hex(s): @conf.commands.register -def hexdiff(a, b, autojunk=False): - # type: (Union[Packet, AnyStr], Union[Packet, AnyStr], bool) -> None +def hexdiff( + a: Union['Packet', AnyStr], + b: Union['Packet', AnyStr], + algo: Optional[str] = None, + autojunk: bool = False, +) -> None: """ Show differences between 2 binary strings, Packets... - For the autojunk parameter, see - https://docs.python.org/3.8/library/difflib.html#difflib.SequenceMatcher + Available algorithms: + - wagnerfischer: Use the Wagner and Fischer algorithm to compute the + Levenstein distance between the strings then backtrack. + - difflib: Use the difflib.SequenceMatcher implementation. This based on a + modified version of the Ratcliff and Obershelp algorithm. + This is much faster, but far less accurate. + https://docs.python.org/3.8/library/difflib.html#difflib.SequenceMatcher :param a: :param b: The binary strings, packets... to compare - :param autojunk: Setting it to True will likely increase the comparison - speed a lot on big byte strings, but will reduce accuracy (will tend - to miss insertion and see replacements instead for instance). + :param algo: Force the algo to be 'wagnerfischer' or 'difflib'. + By default, this is chosen depending on the complexity, optimistically + preferring wagnerfischer unless really necessary. + :param autojunk: (difflib only) See difflib documentation. """ - - # Compare the strings using difflib - xb = bytes_encode(a) yb = bytes_encode(b) - sm = difflib.SequenceMatcher(a=xb, b=yb, autojunk=autojunk) - xarr = [xb[i:i + 1] for i in range(len(xb))] - yarr = [yb[i:i + 1] for i in range(len(yb))] + if algo is None: + # Choose the best algorithm + complexity = len(xb) * len(yb) + if complexity < 1e7: + # Comparing two (non-jumbos) Ethernet packets is ~2e6 which is manageable. + # Anything much larger than this shouldn't be attempted by default. + algo = "wagnerfischer" + if complexity > 1e6: + log_interactive.info( + "Complexity is a bit high. hexdiff will take a few seconds." + ) + else: + algo = "difflib" backtrackx = [] backtracky = [] - for opcode in sm.get_opcodes(): - typ, x0, x1, y0, y1 = opcode - if typ == 'delete': - backtrackx += xarr[x0:x1] - backtracky += [b''] * (x1 - x0) - elif typ == 'insert': - backtrackx += [b''] * (y1 - y0) - backtracky += yarr[y0:y1] - elif typ in ['equal', 'replace']: - backtrackx += xarr[x0:x1] - backtracky += yarr[y0:y1] - - if autojunk: + + if algo == "wagnerfischer": + xb = xb[::-1] + yb = yb[::-1] + + # costs for the 3 operations + INSERT = 1 + DELETE = 1 + SUBST = 1 + + # Typically, d[i,j] will hold the distance between + # the first i characters of xb and the first j characters of yb. + # We change the Wagner Fischer to also store pointers to all + # the intermediate steps taken while calculating the Levenstein distance. + d = {(-1, -1): (0, (-1, -1))} + for j in range(len(yb)): + d[-1, j] = (j + 1) * INSERT, (-1, j - 1) + for i in range(len(xb)): + d[i, -1] = (i + 1) * INSERT + 1, (i - 1, -1) + + # Compute the Levenstein distance between the two strings, but + # store all the steps to be able to backtrack at the end. + for j in range(len(yb)): + for i in range(len(xb)): + d[i, j] = min( + (d[i - 1, j - 1][0] + SUBST * (xb[i] != yb[j]), (i - 1, j - 1)), + (d[i - 1, j][0] + DELETE, (i - 1, j)), + (d[i, j - 1][0] + INSERT, (i, j - 1)), + ) + + # Iterate through the steps backwards to create the diff + i = len(xb) - 1 + j = len(yb) - 1 + while not (i == j == -1): + i2, j2 = d[i, j][1] + backtrackx.append(xb[i2 + 1:i + 1]) + backtracky.append(yb[j2 + 1:j + 1]) + i, j = i2, j2 + elif algo == "difflib": + sm = difflib.SequenceMatcher(a=xb, b=yb, autojunk=autojunk) + xarr = [xb[i:i + 1] for i in range(len(xb))] + yarr = [yb[i:i + 1] for i in range(len(yb))] + # Iterate through opcodes to build the backtrack + for opcode in sm.get_opcodes(): + typ, x0, x1, y0, y1 = opcode + if typ == 'delete': + backtrackx += xarr[x0:x1] + backtracky += [b''] * (x1 - x0) + elif typ == 'insert': + backtrackx += [b''] * (y1 - y0) + backtracky += yarr[y0:y1] + elif typ in ['equal', 'replace']: + backtrackx += xarr[x0:x1] + backtracky += yarr[y0:y1] # Some lines may have been considered as junk. Check the sizes - lbx = len(backtrackx) - lby = len(backtracky) - backtrackx += [b''] * (max(lbx, lby) - lbx) - backtracky += [b''] * (max(lbx, lby) - lby) + if autojunk: + lbx = len(backtrackx) + lby = len(backtracky) + backtrackx += [b''] * (max(lbx, lby) - lbx) + backtracky += [b''] * (max(lbx, lby) - lby) + else: + raise ValueError("Unknown algorithm '%s'" % algo) # Print the diff diff --git a/test/regression.uts b/test/regression.uts index 2fbb322800a..9a09899d972 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -867,11 +867,11 @@ assert fletcher16_checkbytes(b"\x28\x07", 1) == b"\xaf(" = Test hexdiff function ~ not_pypy -def test_hexdiff(a, b, autojunk=False): +def test_hexdiff(a, b, algo=None, autojunk=False): conf_color_theme = conf.color_theme conf.color_theme = BlackAndWhite() with ContextManagerCaptureOutput() as cmco: - hexdiff(a, b, autojunk=autojunk) + hexdiff(a, b, algo=algo, autojunk=autojunk) result_hexdiff = cmco.get_output() conf.interactive = True conf.color_theme = conf_color_theme @@ -901,12 +901,12 @@ expected += "0010 7F 00 00 01 .... expected += " 0010 7F 00 00 02 ....\n" assert result_hexdiff == expected -# Compare using autojunk +# Compare using difflib a = "A" * 1000 + "findme" + "B" * 1000 b = "A" * 1000 + "B" * 1000 -ret1 = test_hexdiff(a, b) -ret2 = test_hexdiff(a, b, autojunk=True) +ret1 = test_hexdiff(a, b, algo="difflib") +ret2 = test_hexdiff(a, b, algo="difflib", autojunk=True) expected_ret1 = """ 03d0 03d0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA From d33c7b491d398cf1704b33d008aaa5fcafbe69e9 Mon Sep 17 00:00:00 2001 From: Gnought <1684105+gnought@users.noreply.github.com> Date: Tue, 10 Oct 2023 02:10:31 +0800 Subject: [PATCH 120/122] update type of `default` param in LEFieldLenField The `default` type should be same as its parent `FieldLenField` --- scapy/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scapy/fields.py b/scapy/fields.py index d2813190097..f082be247e1 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -2894,7 +2894,7 @@ class LEFieldLenField(FieldLenField): def __init__( self, name, # type: str - default, # type: int + default, # type: Optional[Any] length_of=None, # type: Optional[str] fmt=" Date: Wed, 6 Dec 2023 04:58:54 +0000 Subject: [PATCH 121/122] DNS: use the right type in MX RRs According to https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4 it's 15. --- scapy/layers/dns.py | 2 +- test/scapy/layers/dns.uts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index ec1f9d8d3c3..9566faf600c 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -706,7 +706,7 @@ def default_payload_class(self, payload): class DNSRRMX(_DNSRRdummy): name = "DNS MX Resource Record" fields_desc = [DNSStrField("rrname", ""), - ShortEnumField("type", 6, dnstypes), + ShortEnumField("type", 15, dnstypes), ShortEnumField("rclass", 1, dnsclasses), IntField("ttl", 0), ShortField("rdlen", None), diff --git a/test/scapy/layers/dns.uts b/test/scapy/layers/dns.uts index c0c1de8b016..ed4d79df81c 100644 --- a/test/scapy/layers/dns.uts +++ b/test/scapy/layers/dns.uts @@ -204,6 +204,11 @@ full = b"\x04data\xc0\x0f" assert dns_get_str(full, full=full)[0] == b"data." += DNS record type 15 (MX) + +p = DNS(raw(DNS(qd=[],an=DNSRRMX(exchange='example.com')))) +assert p.an[0].exchange == b'example.com.' + = DNS record type 16 (TXT) p = DNS(raw(DNS(id=1,ra=1,qd=[],an=DNSRR(rrname='scapy', type='TXT', rdata="niceday", ttl=1)))) From 0dd08cd064134bcbf7c6bfbdbd356b69cdee05ac Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Sun, 3 Dec 2023 03:48:07 +0000 Subject: [PATCH 122/122] DNS: add the DAU, DHU and N3U EDNS(0) options These options are used for signaling cryptographic algorithm understanding in DNS Security Extensions (DNSSEC): https://www.rfc-editor.org/rfc/rfc6975.html The patch was cross-checked with Wireshark: ```sh >>> dau = EDNS0DAU(alg_code=['RSA/SHA-256', 'RSA/SHA-512']) >>> dhu = EDNS0DHU(alg_code=['SHA-1', 'SHA-256', 'SHA-384']) >>> n3u = EDNS0N3U(alg_code=['SHA-1']) >>> tdecode(Ether()/IP()/UDP()/DNS(ar=[DNSRROPT(rdata=[dau, dhu, n3u])])) ... Option: DAU - DNSSEC Algorithm Understood (RFC6975) Option Code: DAU - DNSSEC Algorithm Understood (RFC6975) (5) Option Length: 2 Option Data: 080a DAU: RSA/SHA-256 (8) DAU: RSA/SHA-512 (10) Option: DHU - DS Hash Understood (RFC6975) Option Code: DHU - DS Hash Understood (RFC6975) (6) Option Length: 3 Option Data: 010204 DHU: SHA-1 (1) DHU: SHA-256 (2) DHU: SHA-384 (4) Option: N3U - NSEC3 Hash Understood (RFC6975) Option Code: N3U - NSEC3 Hash Understood (RFC6975) (7) Option Length: 1 Option Data: 01 N3U: SHA-1 (1) ``` --- scapy/layers/dns.py | 100 ++++++++++++++++++++++---------- test/scapy/layers/dns_edns0.uts | 62 ++++++++++++++++++++ 2 files changed, 130 insertions(+), 32 deletions(-) diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 9566faf600c..30498247c5d 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -34,6 +34,7 @@ ConditionalField, Field, FieldLenField, + FieldListField, FlagsField, I, IP6Field, @@ -89,6 +90,22 @@ dnsclasses = {1: 'IN', 2: 'CS', 3: 'CH', 4: 'HS', 255: 'ANY'} +# 09/2013 from http://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml # noqa: E501 +dnssecalgotypes = {0: "Reserved", 1: "RSA/MD5", 2: "Diffie-Hellman", 3: "DSA/SHA-1", # noqa: E501 + 4: "Reserved", 5: "RSA/SHA-1", 6: "DSA-NSEC3-SHA1", + 7: "RSASHA1-NSEC3-SHA1", 8: "RSA/SHA-256", 9: "Reserved", + 10: "RSA/SHA-512", 11: "Reserved", 12: "GOST R 34.10-2001", + 13: "ECDSA Curve P-256 with SHA-256", 14: "ECDSA Curve P-384 with SHA-384", # noqa: E501 + 252: "Reserved for Indirect Keys", 253: "Private algorithms - domain name", # noqa: E501 + 254: "Private algorithms - OID", 255: "Reserved"} + +# 09/2013 from http://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml +dnssecdigesttypes = {0: "Reserved", 1: "SHA-1", 2: "SHA-256", 3: "GOST R 34.11-94", 4: "SHA-384"} # noqa: E501 + +# 12/2023 from https://www.iana.org/assignments/dnssec-nsec3-parameters/dnssec-nsec3-parameters.xhtml # noqa: E501 +dnssecnsec3algotypes = {0: "Reserved", 1: "SHA-1"} + + def dns_get_str(s, full=None, _ignore_compression=False): """This function decompresses a string s, starting from the given pointer. @@ -387,21 +404,25 @@ def i2m(self, pkt, s): # RFC 2671 - Extension Mechanisms for DNS (EDNS0) edns0types = {0: "Reserved", 1: "LLQ", 2: "UL", 3: "NSID", 4: "Reserved", - 5: "PING", 8: "edns-client-subnet", 10: "COOKIE", + 5: "DAU", 6: "DHU", 7: "N3U", 8: "edns-client-subnet", 10: "COOKIE", 15: "Extended DNS Error"} -class EDNS0TLV(Packet): +class _EDNS0Dummy(Packet): + name = "Dummy class that implements extract_padding()" + + def extract_padding(self, p): + # type: (bytes) -> Tuple[bytes, Optional[bytes]] + return "", p + + +class EDNS0TLV(_EDNS0Dummy): name = "DNS EDNS0 TLV" fields_desc = [ShortEnumField("optcode", 0, edns0types), FieldLenField("optlen", None, "optdata", fmt="H"), StrLenField("optdata", "", length_from=lambda pkt: pkt.optlen)] - def extract_padding(self, p): - # type: (bytes) -> Tuple[bytes, Optional[bytes]] - return "", p - @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): # type: (Optional[bytes], *Any, **Any) -> Type[Packet] @@ -410,11 +431,7 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): if len(_pkt) < 2: return Raw edns0type = struct.unpack("!H", _pkt[:2])[0] - if edns0type == 8: - return EDNS0ClientSubnet - if edns0type == 15: - return EDNS0ExtendedDNSError - return EDNS0TLV + return EDNS0OPT_DISPATCHER.get(edns0type, EDNS0TLV) class DNSRROPT(Packet): @@ -432,6 +449,36 @@ class DNSRROPT(Packet): length_from=lambda pkt: pkt.rdlen)] +# RFC 6975 - Signaling Cryptographic Algorithm Understanding in +# DNS Security Extensions (DNSSEC) + +class EDNS0DAU(_EDNS0Dummy): + name = "DNSSEC Algorithm Understood (DAU)" + fields_desc = [ShortEnumField("optcode", 5, edns0types), + FieldLenField("optlen", None, count_of="alg_code", fmt="H"), + FieldListField("alg_code", None, + ByteEnumField("", 0, dnssecalgotypes), + count_from=lambda pkt:pkt.optlen)] + + +class EDNS0DHU(_EDNS0Dummy): + name = "DS Hash Understood (DHU)" + fields_desc = [ShortEnumField("optcode", 6, edns0types), + FieldLenField("optlen", None, count_of="alg_code", fmt="H"), + FieldListField("alg_code", None, + ByteEnumField("", 0, dnssecdigesttypes), + count_from=lambda pkt:pkt.optlen)] + + +class EDNS0N3U(_EDNS0Dummy): + name = "NSEC3 Hash Understood (N3U)" + fields_desc = [ShortEnumField("optcode", 7, edns0types), + FieldLenField("optlen", None, count_of="alg_code", fmt="H"), + FieldListField("alg_code", None, + ByteEnumField("", 0, dnssecnsec3algotypes), + count_from=lambda pkt:pkt.optlen)] + + # RFC 7871 - Client Subnet in DNS Queries class ClientSubnetv4(StrLenField): @@ -489,7 +536,7 @@ class ClientSubnetv6(ClientSubnetv4): af_default = b"\x20" # 2000:: -class EDNS0ClientSubnet(Packet): +class EDNS0ClientSubnet(_EDNS0Dummy): name = "DNS EDNS0 Client Subnet" fields_desc = [ShortEnumField("optcode", 8, edns0types), FieldLenField("optlen", None, "address", fmt="H", @@ -510,9 +557,6 @@ class EDNS0ClientSubnet(Packet): ClientSubnetv4("address", "192.168.0.0", length_from=lambda p: p.source_plen))] - def extract_padding(self, p): - return "", p - # RFC 8914 - Extended DNS Errors @@ -552,7 +596,7 @@ def extract_padding(self, p): # https://www.rfc-editor.org/rfc/rfc8914.html -class EDNS0ExtendedDNSError(Packet): +class EDNS0ExtendedDNSError(_EDNS0Dummy): name = "DNS EDNS0 Extended DNS Error" fields_desc = [ShortEnumField("optcode", 15, edns0types), FieldLenField("optlen", None, length_of="extra_text", fmt="!H", @@ -561,25 +605,17 @@ class EDNS0ExtendedDNSError(Packet): StrLenField("extra_text", "", length_from=lambda pkt: pkt.optlen - 2)] - def extract_padding(self, p): - return "", p - - -# RFC 4034 - Resource Records for the DNS Security Extensions - -# 09/2013 from http://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml # noqa: E501 -dnssecalgotypes = {0: "Reserved", 1: "RSA/MD5", 2: "Diffie-Hellman", 3: "DSA/SHA-1", # noqa: E501 - 4: "Reserved", 5: "RSA/SHA-1", 6: "DSA-NSEC3-SHA1", - 7: "RSASHA1-NSEC3-SHA1", 8: "RSA/SHA-256", 9: "Reserved", - 10: "RSA/SHA-512", 11: "Reserved", 12: "GOST R 34.10-2001", - 13: "ECDSA Curve P-256 with SHA-256", 14: "ECDSA Curve P-384 with SHA-384", # noqa: E501 - 252: "Reserved for Indirect Keys", 253: "Private algorithms - domain name", # noqa: E501 - 254: "Private algorithms - OID", 255: "Reserved"} +EDNS0OPT_DISPATCHER = { + 5: EDNS0DAU, + 6: EDNS0DHU, + 7: EDNS0N3U, + 8: EDNS0ClientSubnet, + 15: EDNS0ExtendedDNSError, +} -# 09/2013 from http://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml -dnssecdigesttypes = {0: "Reserved", 1: "SHA-1", 2: "SHA-256", 3: "GOST R 34.11-94", 4: "SHA-384"} # noqa: E501 +# RFC 4034 - Resource Records for the DNS Security Extensions def bitmap2RRlist(bitmap): """ diff --git a/test/scapy/layers/dns_edns0.uts b/test/scapy/layers/dns_edns0.uts index 02385d845e0..dd7e5df6065 100644 --- a/test/scapy/layers/dns_edns0.uts +++ b/test/scapy/layers/dns_edns0.uts @@ -78,6 +78,63 @@ def _test(): retry_test(_test) ++ EDNS0 - DAU + += Basic instantiation & dissection + +b = b'\x00\x05\x00\x00' + +p = EDNS0DAU() +assert raw(p) == b + +p = EDNS0DAU(b) +assert p.optcode == 5 and p.optlen == 0 and p.alg_code == [] + +b = raw(EDNS0DAU(alg_code=['RSA/SHA-256', 'RSA/SHA-512'])) + +p = EDNS0DAU(b) +repr(p) +assert p.optcode == 5 and p.optlen == 2 and p.alg_code == [8, 10] + + ++ EDNS0 - DHU + += Basic instantiation & dissection + +b = b'\x00\x06\x00\x00' + +p = EDNS0DHU() +assert raw(p) == b + +p = EDNS0DHU(b) +assert p.optcode == 6 and p.optlen == 0 and p.alg_code == [] + +b = raw(EDNS0DHU(alg_code=['SHA-1', 'SHA-256', 'SHA-384'])) + +p = EDNS0DHU(b) +repr(p) +assert p.optcode == 6 and p.optlen == 3 and p.alg_code == [1, 2, 4] + + ++ EDNS0 - N3U + += Basic instantiation & dissection + +b = b'\x00\x07\x00\x00' + +p = EDNS0N3U() +assert raw(p) == b + +p = EDNS0N3U(b) +assert p.optcode == 7 and p.optlen == 0 and p.alg_code == [] + +b = raw(EDNS0N3U(alg_code=['SHA-1'])) + +p = EDNS0N3U(b) +repr(p) +assert p.optcode == 7 and p.optlen == 1 and p.alg_code == [1] + + + EDNS0 - Client Subnet = Basic instantiation & dissection @@ -123,3 +180,8 @@ assert p.info_code == 6 and p.optlen == 45 and p.extra_text == b'proof of non-ex p = DNSRROPT(raw(DNSRROPT(rdata=[EDNS0ExtendedDNSError(), EDNS0ClientSubnet(), EDNS0TLV()]))) assert len(p.rdata) == 3 assert all(Raw not in opt for opt in p.rdata) + +for opt_class in EDNS0OPT_DISPATCHER.values(): + p = DNSRROPT(raw(DNSRROPT(rdata=[EDNS0TLV(), opt_class(), opt_class()]))) + assert len(p.rdata) == 3 + assert all(Raw not in opt for opt in p.rdata)