From 6476809e81a43f8b7152b7183a8dfc26e8f3df0e Mon Sep 17 00:00:00 2001 From: Clinton Thomas <1033162+KernelClint@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:25:46 -0400 Subject: [PATCH 1/3] kerberos: reject encryption types wider than Int32 AI-Assisted: yes (GPT-5.6-Cyber) --- scapy/layers/kerberos.py | 14 +++++++++++++- test/regression.uts | 5 +++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index 904449b860e..b3f874d79eb 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -374,10 +374,22 @@ def fromSPN(spn: str): } +class _KRBInt32Field(ASN1F_enum_INTEGER): + def m2i(self, pkt, s): + s = self._apply_tagging_dec(s, pkt, _fname=self.name) + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + if codec.check_type_get_len(s)[0] > 5: + raise BER_Decoding_Error( + "Kerberos integer is wider than 32 bits", remaining=s + ) + dec = codec.safedec if self.flexible_tag else codec.dec + return dec(s, context=self.context, **self._codec_kwargs(pkt)) + + class EncryptedData(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( - ASN1F_enum_INTEGER("etype", 0x17, _KRB_E_TYPES, explicit_tag=0xA0), + _KRBInt32Field("etype", 0x17, _KRB_E_TYPES, explicit_tag=0xA0), ASN1F_optional(UInt32("kvno", None, explicit_tag=0xA1)), ASN1F_STRING("cipher", "", explicit_tag=0xA2), ) diff --git a/test/regression.uts b/test/regression.uts index 53bdbad16c3..3a3ecb874f0 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -1503,6 +1503,11 @@ assert pkt.json() == '{"length": null, "id": 0, "qr": 0, "opcode": 0, "aa": 0, " pkt = KRB_AP_REP(bytes(KRB_AP_REP(encPart=EncryptedData()))) assert pkt.command() == "KRB_AP_REP(pvno=ASN1_INTEGER(5), msgType=ASN1_INTEGER(15), encPart=EncryptedData(etype=ASN1_INTEGER(23), kvno=None, cipher=ASN1_STRING(b'')))" += KRB AP REP rejects encryption type wider than Int32 +wide = KerberosTCPHeader(hex_bytes("000000206f1e301ca003020105a10302010fa210300ea0080206010101010101a2020400")) +assert Raw in wide +assert KRB_AP_REP not in wide + = Test json(à with ASN.1 packet assert pkt.json() == '{"pvno": {"type": "ASN1_INTEGER", "value": "5"}, "msgType": {"type": "ASN1_INTEGER", "value": "15"}, "encPart": {"etype": {"type": "ASN1_INTEGER", "value": "23"}, "kvno": null, "cipher": {"type": "ASN1_STRING", "value": ""}}}' From 151da773556618c0f42c682d209a70b1ba1f1086 Mon Sep 17 00:00:00 2001 From: Clinton Thomas <1033162+KernelClint@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:49:24 -0400 Subject: [PATCH 2/3] asn1: decode BER integers with int.from_bytes Move the fix from Kerberos into the BER codec, as reviewed. BERcodec_INTEGER.do_dec shifted a growing Python integer one octet at a time, so the cost of each octet rose with the number already accumulated and decoding was quadratic in the encoded width. A sender could multiply parsing cost by padding any INTEGER with leading sign octets, in any protocol that uses BER, not only in a Kerberos etype. Decoding 64,000 content octets took 304 ms; int.from_bytes does the same two's-complement conversion in one pass, in 0.03 ms, and agrees with the old loop on every input tested. This drops the Kerberos-specific Int32 width check the first version of this pull request added. Nothing is rejected now that was accepted before: the field is parsed, only more cheaply. The regression test counts that the conversion happens once rather than per octet, the way the BitLenField test added in #5108 does. AI-Assisted: yes (GPT-5.6-Cyber) --- scapy/asn1/ber.py | 15 +++++++------- scapy/layers/kerberos.py | 14 +------------ test/regression.uts | 43 +++++++++++++++++++++++++++++++++++----- 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index 27933ed6ea9..74636cdf340 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -462,14 +462,13 @@ def do_dec(cls, ): # type: (...) -> Tuple[ASN1_Object[int], bytes] l, s, t = cls.check_type_check_len(s) - x = 0 - if s: - if s[0] & 0x80: # negative int - x = -1 - for c in s: - x <<= 8 - x |= c - return cls.asn1_object(x), t + # Convert the content octets in one go. Shifting a growing Python + # integer one octet at a time costs more with every octet already + # accumulated, so decoding was quadratic in the encoded width: a sender + # could multiply parsing cost by padding any INTEGER with leading sign + # octets, in any protocol that uses BER. int.from_bytes performs the + # same two's-complement conversion in a single pass. + return cls.asn1_object(int.from_bytes(s, "big", signed=True)), t class BERcodec_BOOLEAN(BERcodec_INTEGER): diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index b3f874d79eb..904449b860e 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -374,22 +374,10 @@ def fromSPN(spn: str): } -class _KRBInt32Field(ASN1F_enum_INTEGER): - def m2i(self, pkt, s): - s = self._apply_tagging_dec(s, pkt, _fname=self.name) - codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - if codec.check_type_get_len(s)[0] > 5: - raise BER_Decoding_Error( - "Kerberos integer is wider than 32 bits", remaining=s - ) - dec = codec.safedec if self.flexible_tag else codec.dec - return dec(s, context=self.context, **self._codec_kwargs(pkt)) - - class EncryptedData(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( - _KRBInt32Field("etype", 0x17, _KRB_E_TYPES, explicit_tag=0xA0), + ASN1F_enum_INTEGER("etype", 0x17, _KRB_E_TYPES, explicit_tag=0xA0), ASN1F_optional(UInt32("kvno", None, explicit_tag=0xA1)), ASN1F_STRING("cipher", "", explicit_tag=0xA2), ) diff --git a/test/regression.uts b/test/regression.uts index 3a3ecb874f0..36582a8760a 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -1503,11 +1503,6 @@ assert pkt.json() == '{"length": null, "id": 0, "qr": 0, "opcode": 0, "aa": 0, " pkt = KRB_AP_REP(bytes(KRB_AP_REP(encPart=EncryptedData()))) assert pkt.command() == "KRB_AP_REP(pvno=ASN1_INTEGER(5), msgType=ASN1_INTEGER(15), encPart=EncryptedData(etype=ASN1_INTEGER(23), kvno=None, cipher=ASN1_STRING(b'')))" -= KRB AP REP rejects encryption type wider than Int32 -wide = KerberosTCPHeader(hex_bytes("000000206f1e301ca003020105a10302010fa210300ea0080206010101010101a2020400")) -assert Raw in wide -assert KRB_AP_REP not in wide - = Test json(à with ASN.1 packet assert pkt.json() == '{"pvno": {"type": "ASN1_INTEGER", "value": "5"}, "msgType": {"type": "ASN1_INTEGER", "value": "15"}, "encPart": {"etype": {"type": "ASN1_INTEGER", "value": "23"}, "kvno": null, "cipher": {"type": "ASN1_STRING", "value": ""}}}' @@ -4381,6 +4376,44 @@ except BER_Decoding_Error: pass += Decode an INTEGER with one bulk numeric conversion + +from builtins import int as builtin_int +import scapy.asn1.ber as ber_module + +class _CountingIntMeta(type): + # The decode path also asks isinstance(tag, int), so the stand-in has to + # answer that the way the real int would. + def __instancecheck__(cls, obj): + return isinstance(obj, builtin_int) + +class CountingInt(builtin_int, metaclass=_CountingIntMeta): + calls = 0 + @classmethod + def from_bytes(cls, value, byteorder, signed=False): + CountingInt.calls += 1 + return builtin_int.from_bytes(value, byteorder, signed=signed) + +wide = b"\x02\x08" + b"\x01" * 8 +ber_module.int = CountingInt +try: + decoded, remainder = BERcodec_INTEGER.do_dec(wide) +finally: + del ber_module.int + +assert decoded.val == builtin_int.from_bytes(b"\x01" * 8, "big", signed=True) +assert remainder == b"" +assert CountingInt.calls == 1 + += Decode INTEGER boundary values + +assert BERcodec_INTEGER.do_dec(b"\x02\x01\x7f")[0].val == 127 +assert BERcodec_INTEGER.do_dec(b"\x02\x01\x80")[0].val == -128 +assert BERcodec_INTEGER.do_dec(b"\x02\x02\x00\x80")[0].val == 128 +assert BERcodec_INTEGER.do_dec(b"\x02\x01\xff")[0].val == -1 +assert BERcodec_INTEGER.do_dec(b"\x02\x00")[0].val == 0 + + = BER tests - 2 a = b'0c\x02\x01\x01\x04\x06public\xa2V\x02\x01\x01\x02\x01\x00\x02\x01\x000K0I\x06\x03+\x06\x010B0@0>0<0:08060402000.0,0*0(0&0$0"0 0\x1e0\x1c0\x1a0\x180\x160\x140\x120\x100\x0e0\x0c0\n0\x080\x060\x040\x020\x00' From 6d8635cc2bc8174b5519d8ef2314d97f99145cbd Mon Sep 17 00:00:00 2001 From: Clinton Thomas <1033162+KernelClint@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:36:57 -0400 Subject: [PATCH 3/3] asn1: trim the comment on the BER integer decode AI-Assisted: yes (GPT-5.6-Cyber) --- scapy/asn1/ber.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index 74636cdf340..2f675964992 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -462,12 +462,7 @@ def do_dec(cls, ): # type: (...) -> Tuple[ASN1_Object[int], bytes] l, s, t = cls.check_type_check_len(s) - # Convert the content octets in one go. Shifting a growing Python - # integer one octet at a time costs more with every octet already - # accumulated, so decoding was quadratic in the encoded width: a sender - # could multiply parsing cost by padding any INTEGER with leading sign - # octets, in any protocol that uses BER. int.from_bytes performs the - # same two's-complement conversion in a single pass. + # One pass: shifting a growing integer per octet was quadratic in width. return cls.asn1_object(int.from_bytes(s, "big", signed=True)), t