diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 1b4184f1ae..d98ca05016 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -33,3 +33,6 @@ f5a76c1aedc7f8886bc6abef0dfaa8065684b1f6 # clang-format: Format JSON with clang-format e6256446ddef5c5d5240eefff974556f2e12ac46 + +# analyzer/protocol: Reformat with spicy-format +d70bcd07b9b26036b16092fe950eca40e2f5a032 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0bf1dd4174..9b6440ef04 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -52,8 +52,7 @@ repos: exclude: '^(.typos.toml|src/SmithWaterman.cc|testing/.*|auxil/.*|scripts/base/frameworks/files/magic/.*|CHANGES|scripts/base/protocols/ssl/mozilla-ca-list.zeek)$' - repo: https://github.com/bbannier/spicy-format - rev: v0.25.0 + rev: v0.26.0 hooks: - id: spicy-format - # TODO: Reformat existing large analyzers just before 8.0. - exclude: '(^testing/.*)|(protocol/ldap/.*)|(protocol/quic/.*)|(protocol/websocket/.*)' + exclude: '^testing/.*' diff --git a/CHANGES b/CHANGES index 2844c4d92e..5e70fa031b 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,11 @@ +8.0.0-dev.775 | 2025-07-29 10:05:20 +0200 + + * Update .git-blame-ignore-revs (Arne Welzel, Corelight) + + * analyzer/protocol: Reformat with spicy-format (Arne Welzel, Corelight) + + * pre-commit-config: Bump spicy-format to 0.26.0 (Arne Welzel, Corelight) + 8.0.0-dev.770 | 2025-07-28 14:18:15 -0700 * dce-rpc: Make named_pipe filed docs extensive (Arne Welzel, Corelight) diff --git a/VERSION b/VERSION index 35b063546f..e5220912c2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.0-dev.770 +8.0.0-dev.775 diff --git a/src/analyzer/protocol/ldap/asn1.spicy b/src/analyzer/protocol/ldap/asn1.spicy index 60c26c5081..8aee9c0bde 100644 --- a/src/analyzer/protocol/ldap/asn1.spicy +++ b/src/analyzer/protocol/ldap/asn1.spicy @@ -23,237 +23,222 @@ import spicy; # https://www.obj-sys.com/asn1tutorial/node10.html public type ASN1Type = enum { - Boolean = 1, - Integer = 2, - BitString = 3, - OctetString = 4, - NullVal = 5, - ObjectIdentifier = 6, - ObjectDescriptor = 7, - InstanceOf = 8, - Real = 9, - Enumerated = 10, - EmbeddedPDV = 11, - UTF8String = 12, - RelativeOID = 13, - Sequence = 16, - Set = 17, - NumericString = 18, - PrintableString = 19, - TeletextString = 20, - VideotextString = 21, - IA5String = 22, - UTCTime = 23, - GeneralizedTime = 24, - GraphicString = 25, - VisibleString = 26, - GeneralString = 27, - UniversalString = 28, - CharacterString = 29, - BMPString = 30 + Boolean = 1, + Integer = 2, + BitString = 3, + OctetString = 4, + NullVal = 5, + ObjectIdentifier = 6, + ObjectDescriptor = 7, + InstanceOf = 8, + Real = 9, + Enumerated = 10, + EmbeddedPDV = 11, + UTF8String = 12, + RelativeOID = 13, + Sequence = 16, + Set = 17, + NumericString = 18, + PrintableString = 19, + TeletextString = 20, + VideotextString = 21, + IA5String = 22, + UTCTime = 23, + GeneralizedTime = 24, + GraphicString = 25, + VisibleString = 26, + GeneralString = 27, + UniversalString = 28, + CharacterString = 29, + BMPString = 30, }; #- ASN.1 data classes -------------------------------------------------------- public type ASN1Class = enum { - Universal = 0, - Application = 1, - ContextSpecific = 2, - Private = 3 + Universal = 0, + Application = 1, + ContextSpecific = 2, + Private = 3, }; #- ASN.1 tag definition (including length) ------------------------------------ type LengthType = unit { - var len: uint64; - var tag_len: uint8; + var len: uint64; + var tag_len: uint8; - data : bitfield(8) { - num: 0..6; - islong: 7; - }; + data: bitfield(8) { + num: 0..6; + islong: 7; + }; - - switch ( self.data.islong ) { - 0 -> : void { - self.len = self.data.num; - self.tag_len = 1; - } - 1 -> : bytes &size=self.data.num - &convert=$$.to_uint(spicy::ByteOrder::Network) { - self.len = $$; - self.tag_len = self.data.num + 1; - } - }; + switch (self.data.islong) { + 0 -> : void { + self.len = self.data.num; + self.tag_len = 1; + } + 1 -> : bytes &size=self.data.num &convert=$$.to_uint(spicy::ByteOrder::Network) { + self.len = $$; + self.tag_len = self.data.num + 1; + } + }; }; type ASN1Tag = unit { - : bitfield(8) { - type_: 0..4 &convert=ASN1Type($$); - constructed: 5 &convert=cast($$); - class: 6..7 &convert=ASN1Class($$); - }; + : bitfield(8) { + type_: 0..4 &convert=ASN1Type($$); + constructed: 5 &convert=cast($$); + class: 6..7 &convert=ASN1Class($$); + }; }; #- ASN.1 bit string ----------------------------------------------------------- # https://www.obj-sys.com/asn1tutorial/node10.html type ASN1BitString = unit(len: uint64, constructed: bool) { - : uint8; # unused bits - value_bits: bytes &size=(len - 1); + : uint8; # unused bits + value_bits: bytes &size=(len - 1); - # TODO - constructed form - # https://github.com/zeek/spicy/issues/921 - # `bytes` needs << and >> support before we can implement complex bitstrings - # + # TODO - constructed form + # https://github.com/zeek/spicy/issues/921 + # `bytes` needs << and >> support before we can implement complex bitstrings + # }; #- ASN.1 octet string --------------------------------------------------------- # https://www.obj-sys.com/asn1tutorial/node10.html type ASN1OctetString = unit(len: uint64, constructed: bool) { - value: bytes &size = len; + value: bytes &size=len; - # TODO - constructed form + # TODO - constructed form }; #- ASN.1 various string types ------------------------------------------------- # https://www.obj-sys.com/asn1tutorial/node124.html type ASN1String = unit(tag: ASN1Tag, len: uint64) { - var encoding: spicy::Charset; + var encoding: spicy::Charset; - on %init { - switch ( tag.type_ ) { - # see "Restricted Character String Types" in - # "Generic String Encoding Rules (GSER) for ASN.1 Types" - # (https://datatracker.ietf.org/doc/html/rfc3641#section-3.2) - - case ASN1Type::PrintableString, - ASN1Type::GeneralizedTime, - ASN1Type::UTCTime: { - self.encoding = spicy::Charset::ASCII; - } - - case ASN1Type::UTF8String, - ASN1Type::GeneralString, - ASN1Type::CharacterString, - ASN1Type::GraphicString, - ASN1Type::IA5String, - ASN1Type::NumericString, - ASN1Type::TeletextString, - ASN1Type::VideotextString, - ASN1Type::VisibleString, - # TODO: RFC3641 mentions special UTF-8 mapping rules for - # BMPString and UniversalString. This *may* not be correct. - ASN1Type::BMPString, - ASN1Type::UniversalString: { - self.encoding = spicy::Charset::UTF8; - } + on %init { + switch (tag.type_) { + # see "Restricted Character String Types" in + # "Generic String Encoding Rules (GSER) for ASN.1 Types" + # (https://datatracker.ietf.org/doc/html/rfc3641#section-3.2) + case ASN1Type::PrintableString, + ASN1Type::GeneralizedTime, + ASN1Type::UTCTime: { + self.encoding = spicy::Charset::ASCII; + } + case ASN1Type::UTF8String, + ASN1Type::GeneralString, + ASN1Type::CharacterString, + ASN1Type::GraphicString, + ASN1Type::IA5String, + ASN1Type::NumericString, + ASN1Type::TeletextString, + ASN1Type::VideotextString, + ASN1Type::VisibleString, + # TODO: RFC3641 mentions special UTF-8 mapping rules for + # BMPString and UniversalString. This *may* not be correct. + ASN1Type::BMPString, + ASN1Type::UniversalString: { + self.encoding = spicy::Charset::UTF8; + } + } } - } - value: ASN1OctetString(len, tag.constructed) &convert=$$.value.decode(self.encoding); + value: ASN1OctetString(len, tag.constructed) &convert=$$.value.decode(self.encoding); } &convert=self.value; #- ASN.1 OID ------------------------------------------------------------------ # https://www.obj-sys.com/asn1tutorial/node124.html type ASN1ObjectIdentifierNibble = unit { - data : bitfield(8) { - num: 0..6; - more: 7; - }; + data: bitfield(8) { + num: 0..6; + more: 7; + }; } &convert=self.data; type ASN1ObjectIdentifier = unit(len: uint64) { - var oidbytes: bytes; - var temp: uint64; - var oidstring: string; + var oidbytes: bytes; + var temp: uint64; + var oidstring: string; - : uint8 if ( len >= 1 ) { - self.temp = $$ / 40; - self.oidbytes += ("%d" % (self.temp)).encode(); - self.temp = $$ % 40; - self.oidbytes += (".%d" % (self.temp)).encode(); - self.temp = 0; - } - - sublist: ASN1ObjectIdentifierNibble[len - 1] foreach { - self.temp = ( self.temp<<7 ) | $$.num; - if ( $$.more != 1 ) { - self.oidbytes += (".%d" % (self.temp)).encode(); - self.temp = 0; + : uint8 if(len >= 1) { + self.temp = $$ / 40; + self.oidbytes += ("%d" % (self.temp)).encode(); + self.temp = $$ % 40; + self.oidbytes += (".%d" % (self.temp)).encode(); + self.temp = 0; } - } - on %done { - self.oidstring = self.oidbytes.decode(); - } + sublist: ASN1ObjectIdentifierNibble[len - 1] foreach { + self.temp = (self.temp << 7) | $$.num; + if ($$.more != 1) { + self.oidbytes += (".%d" % (self.temp)).encode(); + self.temp = 0; + } + } + + on %done { + self.oidstring = self.oidbytes.decode(); + } }; - #- ASN.1 message header (tag + length information) ---------------------------- public type ASN1Header = unit { - tag: ASN1Tag; - len: LengthType; + tag: ASN1Tag; + len: LengthType; }; #- ASN.1 message body --------------------------------------------------------- public type ASN1Body = unit(head: ASN1Header, recursive: bool) { - switch ( head.tag.type_ ) { + switch (head.tag.type_) { + ASN1Type::Boolean -> bool_value: uint8 &convert=cast($$) &requires=head.len.len == 1; + ASN1Type::Integer, + ASN1Type::Enumerated -> num_value: bytes &size=head.len.len &convert=$$.to_int(spicy::ByteOrder::Big); + ASN1Type::NullVal -> null_value: bytes &size=0 &requires=head.len.len == 0; + ASN1Type::BitString -> bitstr_value: ASN1BitString(head.len.len, head.tag.constructed); + ASN1Type::OctetString -> str_value: ASN1OctetString(head.len.len, head.tag.constructed) &convert=$$.value.decode(spicy::Charset::ASCII); + ASN1Type::ObjectIdentifier -> str_value: ASN1ObjectIdentifier(head.len.len) &convert=$$.oidstring; + ASN1Type::BMPString, + ASN1Type::CharacterString, + ASN1Type::GeneralizedTime, + ASN1Type::GeneralString, + ASN1Type::GraphicString, + ASN1Type::IA5String, + ASN1Type::NumericString, + ASN1Type::PrintableString, + ASN1Type::TeletextString, + ASN1Type::UTCTime, + ASN1Type::UTF8String, + ASN1Type::VideotextString, + ASN1Type::VisibleString, + ASN1Type::UniversalString -> str_value: ASN1String(head.tag, head.len.len); + ASN1Type::Sequence, + ASN1Type::Set -> seq: ASN1SubMessages(head.len.len) if(recursive); - ASN1Type::Boolean -> bool_value: uint8 &convert=cast($$) &requires=head.len.len==1; + # TODO: ASN1Type values not handled yet + ASN1Type::ObjectDescriptor, + ASN1Type::InstanceOf, + ASN1Type::Real, + ASN1Type::EmbeddedPDV, + ASN1Type::RelativeOID -> unimplemented_value: bytes &size=head.len.len; - ASN1Type::Integer, - ASN1Type::Enumerated -> num_value: bytes &size=head.len.len - &convert=$$.to_int(spicy::ByteOrder::Big); - - ASN1Type::NullVal -> null_value: bytes &size=0 &requires=head.len.len==0; - - ASN1Type::BitString -> bitstr_value: ASN1BitString(head.len.len, head.tag.constructed); - - ASN1Type::OctetString -> str_value: ASN1OctetString(head.len.len, head.tag.constructed) - &convert=$$.value.decode(spicy::Charset::ASCII); - - ASN1Type::ObjectIdentifier -> str_value: ASN1ObjectIdentifier(head.len.len) - &convert=$$.oidstring; - - ASN1Type::BMPString, - ASN1Type::CharacterString, - ASN1Type::GeneralizedTime, - ASN1Type::GeneralString, - ASN1Type::GraphicString, - ASN1Type::IA5String, - ASN1Type::NumericString, - ASN1Type::PrintableString, - ASN1Type::TeletextString, - ASN1Type::UTCTime, - ASN1Type::UTF8String, - ASN1Type::VideotextString, - ASN1Type::VisibleString, - ASN1Type::UniversalString -> str_value: ASN1String(head.tag, head.len.len); - - ASN1Type::Sequence, ASN1Type::Set -> seq: ASN1SubMessages(head.len.len) if (recursive); - - # TODO: ASN1Type values not handled yet - ASN1Type::ObjectDescriptor, - ASN1Type::InstanceOf, - ASN1Type::Real, - ASN1Type::EmbeddedPDV, - ASN1Type::RelativeOID -> unimplemented_value: bytes &size=head.len.len; - - # unknown (to me) ASN.1 enumeration, skip over silently - * -> unimplemented_value: bytes &size=head.len.len; - }; + # unknown (to me) ASN.1 enumeration, skip over silently + * -> unimplemented_value: bytes &size=head.len.len; + }; }; #- ASN.1 array of ASN.1 sequence/set sub-messages (up to msgLen bytes) -------- public type ASN1SubMessages = unit(msgLen: uint64) { - submessages: ASN1Message(True)[] &eod; + submessages: ASN1Message(True)[] &eod; } &size=msgLen; #- ASN.1 message with header and body ----------------------------------------- @@ -262,18 +247,15 @@ public type ASN1SubMessages = unit(msgLen: uint64) { # - else, application_data:bytes stores data array public type ASN1Message = unit(recursive: bool) { - var application_id: int32; + var application_id: int32; - head: ASN1Header; - switch ( self.head.tag.class ) { - - ASN1Class::Universal -> body: ASN1Body(self.head, recursive); - - ASN1Class::Application, - ASN1Class::ContextSpecific, - ASN1Class::Private -> application_data: bytes &size=self.head.len.len { - self.application_id = cast(self.head.tag.type_); - } - - }; + head: ASN1Header; + switch (self.head.tag.class) { + ASN1Class::Universal -> body: ASN1Body(self.head, recursive); + ASN1Class::Application, + ASN1Class::ContextSpecific, + ASN1Class::Private -> application_data: bytes &size=self.head.len.len { + self.application_id = cast(self.head.tag.type_); + } + }; }; diff --git a/src/analyzer/protocol/ldap/ldap.spicy b/src/analyzer/protocol/ldap/ldap.spicy index 4848109616..7cd500e851 100644 --- a/src/analyzer/protocol/ldap/ldap.spicy +++ b/src/analyzer/protocol/ldap/ldap.spicy @@ -11,119 +11,116 @@ import spicy; #- Operation opcode ---------------------------------------------------------- public type ProtocolOpcode = enum { - BIND_REQUEST = 0, - BIND_RESPONSE = 1, - UNBIND_REQUEST = 2, - SEARCH_REQUEST = 3, - SEARCH_RESULT_ENTRY = 4, - SEARCH_RESULT_DONE = 5, - MODIFY_REQUEST = 6, - MODIFY_RESPONSE = 7, - ADD_REQUEST = 8, - ADD_RESPONSE = 9, - DEL_REQUEST = 10, - DEL_RESPONSE = 11, - MOD_DN_REQUEST = 12, - MOD_DN_RESPONSE = 13, - COMPARE_REQUEST = 14, - COMPARE_RESPONSE = 15, - ABANDON_REQUEST = 16, - SEARCH_RESULT_REFERENCE = 19, - EXTENDED_REQUEST = 23, - EXTENDED_RESPONSE = 24, - INTERMEDIATE_RESPONSE = 25, + BIND_REQUEST = 0, + BIND_RESPONSE = 1, + UNBIND_REQUEST = 2, + SEARCH_REQUEST = 3, + SEARCH_RESULT_ENTRY = 4, + SEARCH_RESULT_DONE = 5, + MODIFY_REQUEST = 6, + MODIFY_RESPONSE = 7, + ADD_REQUEST = 8, + ADD_RESPONSE = 9, + DEL_REQUEST = 10, + DEL_RESPONSE = 11, + MOD_DN_REQUEST = 12, + MOD_DN_RESPONSE = 13, + COMPARE_REQUEST = 14, + COMPARE_RESPONSE = 15, + ABANDON_REQUEST = 16, + SEARCH_RESULT_REFERENCE = 19, + EXTENDED_REQUEST = 23, + EXTENDED_RESPONSE = 24, + INTERMEDIATE_RESPONSE = 25, }; #- Result code --------------------------------------------------------------- public type ResultCode = enum { - SUCCESS = 0, - OPERATIONS_ERROR = 1, - PROTOCOL_ERROR = 2, - TIME_LIMIT_EXCEEDED = 3, - SIZE_LIMIT_EXCEEDED = 4, - COMPARE_FALSE = 5, - COMPARE_TRUE = 6, - AUTH_METHOD_NOT_SUPPORTED = 7, - STRONGER_AUTH_REQUIRED = 8, - PARTIAL_RESULTS = 9, - REFERRAL = 10, - ADMIN_LIMIT_EXCEEDED = 11, - UNAVAILABLE_CRITICAL_EXTENSION = 12, - CONFIDENTIALITY_REQUIRED = 13, - SASL_BIND_IN_PROGRESS = 14, - NO_SUCH_ATTRIBUTE = 16, - UNDEFINED_ATTRIBUTE_TYPE = 17, - INAPPROPRIATE_MATCHING = 18, - CONSTRAINT_VIOLATION = 19, - ATTRIBUTE_OR_VALUE_EXISTS = 20, - INVALID_ATTRIBUTE_SYNTAX = 21, - NO_SUCH_OBJECT = 32, - ALIAS_PROBLEM = 33, - INVALID_DNSYNTAX = 34, - ALIAS_DEREFERENCING_PROBLEM = 36, - INAPPROPRIATE_AUTHENTICATION = 48, - INVALID_CREDENTIALS = 49, - INSUFFICIENT_ACCESS_RIGHTS = 50, - BUSY = 51, - UNAVAILABLE = 52, - UNWILLING_TO_PERFORM = 53, - LOOP_DETECT = 54, - SORT_CONTROL_MISSING = 60, - OFFSET_RANGE_ERROR = 61, - NAMING_VIOLATION = 64, - OBJECT_CLASS_VIOLATION = 65, - NOT_ALLOWED_ON_NON_LEAF = 66, - NOT_ALLOWED_ON_RDN = 67, - ENTRY_ALREADY_EXISTS = 68, - OBJECT_CLASS_MODS_PROHIBITED = 69, - RESULTS_TOO_LARGE = 70, - AFFECTS_MULTIPLE_DSAS = 71, - CONTROL_ERROR = 76, - OTHER = 80, - SERVER_DOWN = 81, - LOCAL_ERROR = 82, - ENCODING_ERROR = 83, - DECODING_ERROR = 84, - TIMEOUT = 85, - AUTH_UNKNOWN = 86, - FILTER_ERROR = 87, - USER_CANCELED = 88, - PARAM_ERROR = 89, - NO_MEMORY = 90, - CONNECT_ERROR = 91, - NOT_SUPPORTED = 92, - CONTROL_NOT_FOUND = 93, - NO_RESULTS_RETURNED = 94, - MORE_RESULTS_TO_RETURN = 95, - CLIENT_LOOP = 96, - REFERRAL_LIMIT_EXCEEDED = 97, - INVALID_RESPONSE = 100, - AMBIGUOUS_RESPONSE = 101, - TLS_NOT_SUPPORTED = 112, - INTERMEDIATE_RESPONSE = 113, - UNKNOWN_TYPE = 114, - LCUP_INVALID_DATA = 115, - LCUP_UNSUPPORTED_SCHEME = 116, - LCUP_RELOAD_REQUIRED = 117, - CANCELED = 118, - NO_SUCH_OPERATION = 119, - TOO_LATE = 120, - CANNOT_CANCEL = 121, - ASSERTION_FAILED = 122, - AUTHORIZATION_DENIED = 123, + SUCCESS = 0, + OPERATIONS_ERROR = 1, + PROTOCOL_ERROR = 2, + TIME_LIMIT_EXCEEDED = 3, + SIZE_LIMIT_EXCEEDED = 4, + COMPARE_FALSE = 5, + COMPARE_TRUE = 6, + AUTH_METHOD_NOT_SUPPORTED = 7, + STRONGER_AUTH_REQUIRED = 8, + PARTIAL_RESULTS = 9, + REFERRAL = 10, + ADMIN_LIMIT_EXCEEDED = 11, + UNAVAILABLE_CRITICAL_EXTENSION = 12, + CONFIDENTIALITY_REQUIRED = 13, + SASL_BIND_IN_PROGRESS = 14, + NO_SUCH_ATTRIBUTE = 16, + UNDEFINED_ATTRIBUTE_TYPE = 17, + INAPPROPRIATE_MATCHING = 18, + CONSTRAINT_VIOLATION = 19, + ATTRIBUTE_OR_VALUE_EXISTS = 20, + INVALID_ATTRIBUTE_SYNTAX = 21, + NO_SUCH_OBJECT = 32, + ALIAS_PROBLEM = 33, + INVALID_DNSYNTAX = 34, + ALIAS_DEREFERENCING_PROBLEM = 36, + INAPPROPRIATE_AUTHENTICATION = 48, + INVALID_CREDENTIALS = 49, + INSUFFICIENT_ACCESS_RIGHTS = 50, + BUSY = 51, + UNAVAILABLE = 52, + UNWILLING_TO_PERFORM = 53, + LOOP_DETECT = 54, + SORT_CONTROL_MISSING = 60, + OFFSET_RANGE_ERROR = 61, + NAMING_VIOLATION = 64, + OBJECT_CLASS_VIOLATION = 65, + NOT_ALLOWED_ON_NON_LEAF = 66, + NOT_ALLOWED_ON_RDN = 67, + ENTRY_ALREADY_EXISTS = 68, + OBJECT_CLASS_MODS_PROHIBITED = 69, + RESULTS_TOO_LARGE = 70, + AFFECTS_MULTIPLE_DSAS = 71, + CONTROL_ERROR = 76, + OTHER = 80, + SERVER_DOWN = 81, + LOCAL_ERROR = 82, + ENCODING_ERROR = 83, + DECODING_ERROR = 84, + TIMEOUT = 85, + AUTH_UNKNOWN = 86, + FILTER_ERROR = 87, + USER_CANCELED = 88, + PARAM_ERROR = 89, + NO_MEMORY = 90, + CONNECT_ERROR = 91, + NOT_SUPPORTED = 92, + CONTROL_NOT_FOUND = 93, + NO_RESULTS_RETURNED = 94, + MORE_RESULTS_TO_RETURN = 95, + CLIENT_LOOP = 96, + REFERRAL_LIMIT_EXCEEDED = 97, + INVALID_RESPONSE = 100, + AMBIGUOUS_RESPONSE = 101, + TLS_NOT_SUPPORTED = 112, + INTERMEDIATE_RESPONSE = 113, + UNKNOWN_TYPE = 114, + LCUP_INVALID_DATA = 115, + LCUP_UNSUPPORTED_SCHEME = 116, + LCUP_RELOAD_REQUIRED = 117, + CANCELED = 118, + NO_SUCH_OPERATION = 119, + TOO_LATE = 120, + CANNOT_CANCEL = 121, + ASSERTION_FAILED = 122, + AUTHORIZATION_DENIED = 123, }; #----------------------------------------------------------------------------- public type Result = unit { - code: ASN1::ASN1Message(True) &convert=cast(cast($$.body.num_value)) - &default=ResultCode::Undef; - matchedDN: ASN1::ASN1Message(True) &convert=$$.body.str_value - &default=""; - diagnosticMessage: ASN1::ASN1Message(True) &convert=$$.body.str_value - &default=""; + code: ASN1::ASN1Message(True) &convert=cast(cast($$.body.num_value)) &default=ResultCode::Undef; + matchedDN: ASN1::ASN1Message(True) &convert=$$.body.str_value &default=""; + diagnosticMessage: ASN1::ASN1Message(True) &convert=$$.body.str_value &default=""; - # TODO: if we want to parse referral URIs in result - # https://tools.ietf.org/html/rfc4511#section-4.1.10 + # TODO: if we want to parse referral URIs in result + # https://tools.ietf.org/html/rfc4511#section-4.1.10 }; # 1.2.840.48018.1.2.2 (MS KRB5 - Microsoft Kerberos 5) @@ -131,231 +128,231 @@ const GSSAPI_MECH_MS_KRB5 = "1.2.840.48018.1.2.2"; # Supported SASL stripping modes. type MessageMode = enum { - MS_KRB5 = 1, # Payload starts with a 4 byte length followed by a wrap token that may or may not be sealed. - TLS = 2, # Client/server used StartTLS, forward to SSL analyzer. - MAYBE_ENCRYPTED = 3, # Use a heuristic to determine encrypted traffic. - CLEARTEXT = 4, # Assume cleartext. - ENCRYPTED = 5, # Assume encrypted. + MS_KRB5 = 1, # Payload starts with a 4 byte length followed by a wrap token that may or may not be sealed. + TLS = 2, # Client/server used StartTLS, forward to SSL analyzer. + MAYBE_ENCRYPTED = 3, # Use a heuristic to determine encrypted traffic. + CLEARTEXT = 4, # Assume cleartext. + ENCRYPTED = 5, # Assume encrypted. }; type Ctx = struct { - messageMode: MessageMode; # Message dispatching mode - saslMechanism: string; # The SASL mechanism selected by the client. - startTlsRequested: bool; # Did the client use the StartTLS extended request? + messageMode: MessageMode; # Message dispatching mode + saslMechanism: string; # The SASL mechanism selected by the client. + startTlsRequested: bool; # Did the client use the StartTLS extended request? }; #----------------------------------------------------------------------------- public type Messages = unit { - %context = Ctx; - : MessageDispatch(self.context())[]; + %context = Ctx; + : MessageDispatch(self.context())[]; }; #----------------------------------------------------------------------------- public type MessageDispatch = unit(ctx: Ctx&) { - switch( ctx.messageMode ) { - MessageMode::Undef -> : Message(ctx); - MessageMode::MS_KRB5 -> : SaslMsKrb5Stripper(ctx); - MessageMode::TLS -> : TlsForward; # never returns - MessageMode::MAYBE_ENCRYPTED -> : MaybeEncrypted(ctx); - MessageMode::CLEARTEXT -> : Message(ctx); - MessageMode::ENCRYPTED -> : EncryptedMessage; - }; + switch (ctx.messageMode) { + MessageMode::Undef -> : Message(ctx); + MessageMode::MS_KRB5 -> : SaslMsKrb5Stripper(ctx); + MessageMode::TLS -> : TlsForward; # never returns + MessageMode::MAYBE_ENCRYPTED -> : MaybeEncrypted(ctx); + MessageMode::CLEARTEXT -> : Message(ctx); + MessageMode::ENCRYPTED -> : EncryptedMessage; + }; }; - #----------------------------------------------------------------------------- type MaybeEncrypted = unit(ctx: Ctx&) { - # A plaintext LDAP message always starts with at least 3 bytes and the first - # byte is 0x30 for the sequence. A SASL encrypted message starts with a 4 byte - # length field. The heuristic here is that if the first byte is a 0x30, - # assume it's unencrypted LDAP. This should be pretty good, if it was an - # encrypted/SASL wrapped message, it would have a size between 0x30000000 and - # 0x30FFFFFF, meaning at least a size of ~768MB, which seems unlikely. - var start: iterator; - var saslLen: uint64; - var mech: bytes; + # A plaintext LDAP message always starts with at least 3 bytes and the first + # byte is 0x30 for the sequence. A SASL encrypted message starts with a 4 byte + # length field. The heuristic here is that if the first byte is a 0x30, + # assume it's unencrypted LDAP. This should be pretty good, if it was an + # encrypted/SASL wrapped message, it would have a size between 0x30000000 and + # 0x30FFFFFF, meaning at least a size of ~768MB, which seems unlikely. + var start: iterator; + var saslLen: uint64; + var mech: bytes; - on %init { - self.start = self.input(); - # Don't have starts_with() on string, work around that. - # https://github.com/zeek/spicy/issues/1807 - self.mech = ctx.saslMechanism.encode(spicy::Charset::UTF8); - } - - first: uint8 { - if ( $$ == 0x30 ) { - ctx.messageMode = MessageMode::CLEARTEXT; - } else { - ctx.messageMode = MessageMode::ENCRYPTED; + on %init { + self.start = self.input(); + # Don't have starts_with() on string, work around that. + # https://github.com/zeek/spicy/issues/1807 + self.mech = ctx.saslMechanism.encode(spicy::Charset::UTF8); } - } - # As a further heuristic, if encrypted mode was decided and the client - # requested GSSAPI or GSS-SPNEGO (or we just didn't see it) peak a bit - # into the SASL payload and check if it starts with a 0504 (WRAP_TOKEN). - # If so, switch into KRB mode assuming that's what is being used and - # have a chance seeing some more plaintext LDAP in non-sealed tokens. - rem: uint8[3] if ( ctx.messageMode == MessageMode::ENCRYPTED && (|self.mech| == 0 || self.mech.starts_with(b"GSS")) ) { - self.saslLen = (uint64(self.first) << 24) + (uint64($$[0]) << 16) + (uint64($$[1]) << 8) + uint64($$[2]); - } - - : uint16 if ( self.saslLen >= 2 ) { - if ( $$ == 0x0504 ) { - ctx.messageMode = MessageMode::MS_KRB5; + first: uint8 { + if ($$ == 0x30) { + ctx.messageMode = MessageMode::CLEARTEXT; + } else { + ctx.messageMode = MessageMode::ENCRYPTED; + } } - } - # Rewind the input. - : void { - # Prevent MessageDispatch from recursing endlessly. - assert ctx.messageMode != MessageMode::MAYBE_ENCRYPTED; - self.set_input(self.start); - } + # As a further heuristic, if encrypted mode was decided and the client + # requested GSSAPI or GSS-SPNEGO (or we just didn't see it) peak a bit + # into the SASL payload and check if it starts with a 0504 (WRAP_TOKEN). + # If so, switch into KRB mode assuming that's what is being used and + # have a chance seeing some more plaintext LDAP in non-sealed tokens. + rem: uint8[3] if(ctx.messageMode == MessageMode::ENCRYPTED && (|self.mech| == 0 || self.mech.starts_with(b"GSS"))) { + self.saslLen = (uint64(self.first) << 24) + (uint64($$[0]) << 16) + (uint64($$[1]) << 8) + uint64($$[2]); + } - # One recursion to parse with the new ctx.messageMode setting. - : MessageDispatch(ctx); + : uint16 if(self.saslLen >= 2) { + if ($$ == 0x0504) { + ctx.messageMode = MessageMode::MS_KRB5; + } + } + + # Rewind the input. + : void { + # Prevent MessageDispatch from recursing endlessly. + assert ctx.messageMode != MessageMode::MAYBE_ENCRYPTED; + self.set_input(self.start); + } + + # One recursion to parse with the new ctx.messageMode setting. + : MessageDispatch(ctx); }; #----------------------------------------------------------------------------- type EncryptedMessage = unit { - len: uint32; - : skip bytes &size=self.len; + len: uint32; + : skip bytes &size=self.len; }; #----------------------------------------------------------------------------- type TlsForward = unit { - # Just consume everything. This is hooked in ldap_zeek.spicy - chunk: bytes &chunked &eod; + # Just consume everything. This is hooked in ldap_zeek.spicy + chunk: bytes &chunked &eod; }; type KrbWrapToken = unit { - # https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.6.2 + # https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.6.2 - # Number of bytes to expect *after* the payload. - var trailer_ec: uint64; - var header_ec: uint64; + # Number of bytes to expect *after* the payload. + var trailer_ec: uint64; + var header_ec: uint64; - ctx_flags: bitfield(8) { - send_by_acceptor: 0; - sealed: 1; - acceptor_subkey: 2; - }; - filler: skip b"\xff"; - ec: uint16; # extra count - rrc: uint16 { # right rotation count - # Handle rrc == ec or rrc == 0. - if ( self.rrc == self.ec ) { - self.header_ec = self.ec; - } else if ( self.rrc == 0 ) { - self.trailer_ec = self.ec; - } else { - if ( ! self.ctx_flags.sealed ) - # If it's sealed, we'll consume until &eod anyhow - # and ec/rrc shouldn't apply, otherwise, bail. - throw "Unhandled rc %s and ec %s" % (self.ec, self.rrc); + ctx_flags: bitfield(8) { + send_by_acceptor: 0; + sealed: 1; + acceptor_subkey: 2; + }; + filler: skip b"\xff"; + ec: uint16; # extra count + rrc: uint16 { + # right rotation count + # Handle rrc == ec or rrc == 0. + if (self.rrc == self.ec) { + self.header_ec = self.ec; + } else if (self.rrc == 0) { + self.trailer_ec = self.ec; + } else { + if (!self.ctx_flags.sealed) + # If it's sealed, we'll consume until &eod anyhow + # and ec/rrc shouldn't apply, otherwise, bail. + throw "Unhandled rc %s and ec %s" % (self.ec, self.rrc); + } } - } - snd_seq: uint64; - header_e: skip bytes &size=self.header_ec; + snd_seq: uint64; + header_e: skip bytes &size=self.header_ec; }; #----------------------------------------------------------------------------- type SaslMsKrb5Stripper = unit(ctx: Ctx&) { - # This is based on Wireshark output and example traffic we have. There's always - # a 4 byte length field followed by the krb5_tok_id field in messages after - # MS_KRB5 was selected. I haven't read enough specs to understand if it's - # just this one case that works, or others could use the same stripping. - var switch_size: uint64; + # This is based on Wireshark output and example traffic we have. There's always + # a 4 byte length field followed by the krb5_tok_id field in messages after + # MS_KRB5 was selected. I haven't read enough specs to understand if it's + # just this one case that works, or others could use the same stripping. + var switch_size: uint64; - len: uint32; - krb5_tok_id: uint16; + len: uint32; + krb5_tok_id: uint16; - switch ( self.krb5_tok_id ) { - 0x0504 -> krb_wrap_token: KrbWrapToken; - * -> : void; - }; + switch (self.krb5_tok_id) { + 0x0504 -> krb_wrap_token: KrbWrapToken; + * -> : void; + }; - : skip bytes &size=0 { - self.switch_size = self.len - (self.offset() - 4); - if ( self?.krb_wrap_token ) - self.switch_size -= self.krb_wrap_token.trailer_ec; - } + : skip bytes &size=0 { + self.switch_size = self.len - (self.offset() - 4); + if (self?.krb_wrap_token) + self.switch_size -= self.krb_wrap_token.trailer_ec; + } - switch ( self?.krb_wrap_token && ! self.krb_wrap_token.ctx_flags.sealed ) { - True -> : Message(ctx)[] &eod; - * -> : skip bytes &eod; - } &size=self.switch_size; + switch (self?.krb_wrap_token && !self.krb_wrap_token.ctx_flags.sealed) { + True -> : Message(ctx)[] &eod; + * -> : skip bytes &eod; + } &size=self.switch_size; - # Consume the wrap token trailer, if any. - trailer_e: skip bytes &size=self.krb_wrap_token.trailer_ec if (self?.krb_wrap_token); + # Consume the wrap token trailer, if any. + trailer_e: skip bytes &size=self.krb_wrap_token.trailer_ec if(self?.krb_wrap_token); }; #----------------------------------------------------------------------------- public type Message = unit(ctx: Ctx&) { - var messageID: int64; - var opcode: ProtocolOpcode = ProtocolOpcode::Undef; - var unsetResultDefault: Result; - var result_: Result& = self.unsetResultDefault; - var obj: string = ""; - var arg: string = ""; - var seqHeaderLen: uint64; - var msgLen: uint64; - var opLen: uint64; + var messageID: int64; + var opcode: ProtocolOpcode = ProtocolOpcode::Undef; + var unsetResultDefault: Result; + var result_: Result& = self.unsetResultDefault; + var obj: string = ""; + var arg: string = ""; + var seqHeaderLen: uint64; + var msgLen: uint64; + var opLen: uint64; - seqHeader: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::Universal && $$.tag.type_ == ASN1::ASN1Type::Sequence) { - self.msgLen = $$.len.len; - } + seqHeader: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::Universal && $$.tag.type_ == ASN1::ASN1Type::Sequence) { + self.msgLen = $$.len.len; + } - # Use offset() to determine how many bytes the seqHeader took. This - # needs to be done after the seqHeader field hook. - : void { - self.seqHeaderLen = self.offset(); - } + # Use offset() to determine how many bytes the seqHeader took. This + # needs to be done after the seqHeader field hook. + : void { + self.seqHeaderLen = self.offset(); + } - messageID_header: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::Universal && $$.tag.type_ == ASN1::ASN1Type::Integer); - : ASN1::ASN1Body(self.messageID_header, False) { - self.messageID = $$.num_value; - } + messageID_header: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::Universal && $$.tag.type_ == ASN1::ASN1Type::Integer); + : ASN1::ASN1Body(self.messageID_header, False) { + self.messageID = $$.num_value; + } - protocolOp: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::Application) { - self.opcode = cast(cast($$.tag.type_)); - self.opLen = $$.len.len; - } + protocolOp: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::Application) { + self.opcode = cast(cast($$.tag.type_)); + self.opLen = $$.len.len; + } - switch ( self.opcode ) { - ProtocolOpcode::BIND_REQUEST -> BIND_REQUEST: BindRequest(self, ctx); - ProtocolOpcode::BIND_RESPONSE -> BIND_RESPONSE: BindResponse(self, ctx); - ProtocolOpcode::UNBIND_REQUEST -> UNBIND_REQUEST: UnbindRequest(self); - ProtocolOpcode::SEARCH_REQUEST -> SEARCH_REQUEST: SearchRequest(self); - ProtocolOpcode::SEARCH_RESULT_ENTRY -> SEARCH_RESULT_ENTRY: SearchResultEntry(self); - ProtocolOpcode::SEARCH_RESULT_DONE -> SEARCH_RESULT_DONE: SearchResultDone(self); - ProtocolOpcode::MODIFY_REQUEST -> MODIFY_REQUEST: ModifyRequest(self); - ProtocolOpcode::MODIFY_RESPONSE -> MODIFY_RESPONSE: ModifyResponse(self); - ProtocolOpcode::ADD_RESPONSE -> ADD_RESPONSE: AddResponse(self); - ProtocolOpcode::DEL_REQUEST -> DEL_REQUEST: DelRequest(self); - ProtocolOpcode::DEL_RESPONSE -> DEL_RESPONSE: DelResponse(self); - ProtocolOpcode::MOD_DN_RESPONSE -> MOD_DN_RESPONSE: ModDNResponse(self); - ProtocolOpcode::COMPARE_RESPONSE -> COMPARE_RESPONSE: CompareResponse(self); - ProtocolOpcode::ABANDON_REQUEST -> ABANDON_REQUEST: AbandonRequest(self); + switch (self.opcode) { + ProtocolOpcode::BIND_REQUEST -> BIND_REQUEST: BindRequest(self, ctx); + ProtocolOpcode::BIND_RESPONSE -> BIND_RESPONSE: BindResponse(self, ctx); + ProtocolOpcode::UNBIND_REQUEST -> UNBIND_REQUEST: UnbindRequest(self); + ProtocolOpcode::SEARCH_REQUEST -> SEARCH_REQUEST: SearchRequest(self); + ProtocolOpcode::SEARCH_RESULT_ENTRY -> SEARCH_RESULT_ENTRY: SearchResultEntry(self); + ProtocolOpcode::SEARCH_RESULT_DONE -> SEARCH_RESULT_DONE: SearchResultDone(self); + ProtocolOpcode::MODIFY_REQUEST -> MODIFY_REQUEST: ModifyRequest(self); + ProtocolOpcode::MODIFY_RESPONSE -> MODIFY_RESPONSE: ModifyResponse(self); + ProtocolOpcode::ADD_RESPONSE -> ADD_RESPONSE: AddResponse(self); + ProtocolOpcode::DEL_REQUEST -> DEL_REQUEST: DelRequest(self); + ProtocolOpcode::DEL_RESPONSE -> DEL_RESPONSE: DelResponse(self); + ProtocolOpcode::MOD_DN_RESPONSE -> MOD_DN_RESPONSE: ModDNResponse(self); + ProtocolOpcode::COMPARE_RESPONSE -> COMPARE_RESPONSE: CompareResponse(self); + ProtocolOpcode::ABANDON_REQUEST -> ABANDON_REQUEST: AbandonRequest(self); - # TODO: not yet implemented, redirect to NotImplemented because when we're - # just commenting this out, it will stop processing LDAP Messages in this connection - ProtocolOpcode::ADD_REQUEST -> ADD_REQUEST: NotImplemented(self); - ProtocolOpcode::COMPARE_REQUEST -> COMPARE_REQUEST: NotImplemented(self); - ProtocolOpcode::EXTENDED_REQUEST -> EXTENDED_REQUEST: ExtendedRequest(self, ctx); - ProtocolOpcode::EXTENDED_RESPONSE -> EXTENDED_RESPONSE: ExtendedResponse(self, ctx); - ProtocolOpcode::INTERMEDIATE_RESPONSE -> INTERMEDIATE_RESPONSE: NotImplemented(self); - ProtocolOpcode::MOD_DN_REQUEST -> MOD_DN_REQUEST: NotImplemented(self); - ProtocolOpcode::SEARCH_RESULT_REFERENCE -> SEARCH_RESULT_REFERENCE: NotImplemented(self); - } &size=self.opLen; + # TODO: not yet implemented, redirect to NotImplemented because when we're + # just commenting this out, it will stop processing LDAP Messages in this connection + ProtocolOpcode::ADD_REQUEST -> ADD_REQUEST: NotImplemented(self); + ProtocolOpcode::COMPARE_REQUEST -> COMPARE_REQUEST: NotImplemented(self); + ProtocolOpcode::EXTENDED_REQUEST -> EXTENDED_REQUEST: ExtendedRequest(self, ctx); + ProtocolOpcode::EXTENDED_RESPONSE -> EXTENDED_RESPONSE: ExtendedResponse(self, ctx); + ProtocolOpcode::INTERMEDIATE_RESPONSE -> INTERMEDIATE_RESPONSE: NotImplemented(self); + ProtocolOpcode::MOD_DN_REQUEST -> MOD_DN_REQUEST: NotImplemented(self); + ProtocolOpcode::SEARCH_RESULT_REFERENCE -> SEARCH_RESULT_REFERENCE: NotImplemented(self); + } &size=self.opLen; - # Ensure some invariants hold after parsing the command. - : void &requires=(self.offset() >= self.seqHeaderLen); - : void &requires=(self.msgLen >= (self.offset() - self.seqHeaderLen)); + # Ensure some invariants hold after parsing the command. + : void &requires=(self.offset() >= self.seqHeaderLen); + : void &requires=(self.msgLen >= (self.offset() - self.seqHeaderLen)); - # Eat the controls field if it exists. - : skip bytes &size=self.msgLen - (self.offset() - self.seqHeaderLen); + # Eat the controls field if it exists. + : skip bytes &size=self.msgLen - (self.offset() - self.seqHeaderLen); }; #----------------------------------------------------------------------------- @@ -363,198 +360,192 @@ public type Message = unit(ctx: Ctx&) { # https://tools.ietf.org/html/rfc4511#section-4.2 public type BindAuthType = enum { - BIND_AUTH_SIMPLE = 0, - BIND_AUTH_SASL = 3, + BIND_AUTH_SIMPLE = 0, + BIND_AUTH_SASL = 3, - # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/8b9dbfb2-5b6a-497a-a533-7e709cb9a982 - # 5.1.1.1.3 Sicily Authentication - SICILY_PACKAGE_DISCOVERY = 9, - SICILY_NEGOTIATE = 10, - SICILY_RESPONSE = 11, + # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/8b9dbfb2-5b6a-497a-a533-7e709cb9a982 + # 5.1.1.1.3 Sicily Authentication + SICILY_PACKAGE_DISCOVERY = 9, + SICILY_NEGOTIATE = 10, + SICILY_RESPONSE = 11, }; type GSS_SPNEGO_negTokenInit = unit { - oidHeader: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::Universal && $$.tag.type_ == ASN1::ASN1Type::ObjectIdentifier); - oid: ASN1::ASN1ObjectIdentifier(self.oidHeader.len.len) &requires=(self.oid.oidstring == "1.3.6.1.5.5.2"); + oidHeader: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::Universal && $$.tag.type_ == ASN1::ASN1Type::ObjectIdentifier); + oid: ASN1::ASN1ObjectIdentifier(self.oidHeader.len.len) &requires=(self.oid.oidstring == "1.3.6.1.5.5.2"); - # TODO: Parse the rest of negTokenInit. - : skip bytes &eod; + # TODO: Parse the rest of negTokenInit. + : skip bytes &eod; }; # Peak into GSS-SPNEGO payload and ensure it is indeed GSS-SPNEGO, # or GSS-SPNEGO with a NTMLSSP payload that starts with NTLMSSP. type GSS_SPNEGO_Init = unit { - # This is the optional octet string in SaslCredentials. - credentialsHeader: ASN1::ASN1Header &requires=($$.tag.type_ == ASN1::ASN1Type::OctetString); + # This is the optional octet string in SaslCredentials. + credentialsHeader: ASN1::ASN1Header &requires=($$.tag.type_ == ASN1::ASN1Type::OctetString); - # Now we either have the initial message as specified in RFC2743 or - # a continuation from RFC4178, or a "NTMLSSP" signature. - # - # 60 -> APPLICATION [0] https://datatracker.ietf.org/doc/html/rfc2743#page-81 - # a1 -> CHOICE [1] https://www.rfc-editor.org/rfc/rfc4178#section-4.2 - # "NTMLSSP" https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/907f519d-6217-45b1-b421-dca10fc8af0d - # - switch { - -> spnegoInitByte: uint8(0x60); - -> spnegoChoiceByte: uint8(0xa1); - -> ntlmSignature: skip b"NTLMSSP"; # Unsupported, should forward to child analyzer! - }; + # Now we either have the initial message as specified in RFC2743 or + # a continuation from RFC4178, or a "NTMLSSP" signature. + # + # 60 -> APPLICATION [0] https://datatracker.ietf.org/doc/html/rfc2743#page-81 + # a1 -> CHOICE [1] https://www.rfc-editor.org/rfc/rfc4178#section-4.2 + # "NTMLSSP" https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/907f519d-6217-45b1-b421-dca10fc8af0d + # + switch { + -> spnegoInitByte: uint8(0x60); + -> spnegoChoiceByte: uint8(0xa1); + -> ntlmSignature: skip b"NTLMSSP"; # Unsupported, should forward to child analyzer! + }; - spnegoLen: skip ASN1::LengthType if (self?.spnegoInitByte || self?.spnegoChoiceByte); + spnegoLen: skip ASN1::LengthType if(self?.spnegoInitByte || self?.spnegoChoiceByte); - # Peak into the SPNEGO_negTokenInit - spnegoInitial: skip GSS_SPNEGO_negTokenInit if (self?.spnegoInitByte); + # Peak into the SPNEGO_negTokenInit + spnegoInitial: skip GSS_SPNEGO_negTokenInit if(self?.spnegoInitByte); }; -type SaslCredentials = unit() { - mechanism: ASN1::ASN1Message(False) &convert=$$.body.str_value; +type SaslCredentials = unit { + mechanism: ASN1::ASN1Message(False) &convert=$$.body.str_value; - # Peak into GSS-SPNEGO payload if we have any. - switch ( self.mechanism ) { - "GSS-SPNEGO" -> gss_spnego: GSS_SPNEGO_Init; - * -> : skip bytes &eod; - }; + # Peak into GSS-SPNEGO payload if we have any. + switch (self.mechanism) { + "GSS-SPNEGO" -> gss_spnego: GSS_SPNEGO_Init; + * -> : skip bytes &eod; + }; }; -type SicilyMessage = unit() { - # Just ensure the signature matches. We could do more, - # but it'd be better to forward to an NTLM analyzer. - signature: skip b"NTLMSSP"; - var signature_decoded: string = "NTLMSSP"; +type SicilyMessage = unit { + # Just ensure the signature matches. We could do more, + # but it'd be better to forward to an NTLM analyzer. + signature: skip b"NTLMSSP"; + var signature_decoded: string = "NTLMSSP"; }; type GSS_SPNEGO_Subsequent = unit { - switch { - -> spnegoChoiceByte: uint8(0xa1); - -> ntmlSignature: skip b"NTLMSSP"; # Unsupported, should forward to NTLM! - }; + switch { + -> spnegoChoiceByte: uint8(0xa1); + -> ntmlSignature: skip b"NTLMSSP"; # Unsupported, should forward to NTLM! + }; - spnegoChoiceLen: skip ASN1::LengthType if (self?.spnegoChoiceByte); - negTokenResp: GSS_SPNEGO_negTokenResp if (self?.spnegoChoiceByte); + spnegoChoiceLen: skip ASN1::LengthType if(self?.spnegoChoiceByte); + negTokenResp: GSS_SPNEGO_negTokenResp if(self?.spnegoChoiceByte); }; type GSS_SPNEGO_negTokenResp = unit { - var accepted: bool; - var supportedMech: ASN1::ASN1Message; - var responseToken: optional; + var accepted: bool; + var supportedMech: ASN1::ASN1Message; + var responseToken: optional; - # Parse the contained Sequence. - seq: ASN1::ASN1Message(True) { - for ( msg in $$.body.seq.submessages ) { - # https://www.rfc-editor.org/rfc/rfc4178#section-4.2.2 - if ( msg.application_id == 0 ) { - self.accepted = msg.application_data == b"\x0a\x01\x00"; - } else if ( msg.application_id == 1 ) { - self.supportedMech = msg; - } else if ( msg.application_id == 2 ) { - self.responseToken = msg.application_data; - } else if ( msg.application_id == 3 ) { - # ignore mechListMec - } else { - throw "unhandled NegTokenResp id %s" % msg.application_id; - } + # Parse the contained Sequence. + seq: ASN1::ASN1Message(True) { + for (msg in $$.body.seq.submessages) { + # https://www.rfc-editor.org/rfc/rfc4178#section-4.2.2 + if (msg.application_id == 0) { + self.accepted = msg.application_data == b"\x0a\x01\x00"; + } else if (msg.application_id == 1) { + self.supportedMech = msg; + } else if (msg.application_id == 2) { + self.responseToken = msg.application_data; + } else if (msg.application_id == 3) { + # ignore mechListMec + } else { + throw "unhandled NegTokenResp id %s" % msg.application_id; + } + } } - } - switch ( self?.supportedMech ) { - True -> supportedMechOid: ASN1::ASN1Message(False) &convert=$$.body.str_value; - * -> : void; - } &parse-from=self.supportedMech.application_data; + switch (self?.supportedMech) { + True -> supportedMechOid: ASN1::ASN1Message(False) &convert=$$.body.str_value; + * -> : void; + } &parse-from=self.supportedMech.application_data; }; # TODO(fox-ds): A helper unit for requests for which no handling has been implemented. # Eventually all uses of this unit should be replaced with actual parsers so this unit can be removed. type NotImplemented = unit(inout message: Message) { - : skip bytes &eod; + : skip bytes &eod; }; type BindRequest = unit(inout message: Message, ctx: Ctx&) { - version: ASN1::ASN1Message(True) &convert=$$.body.num_value; - name: ASN1::ASN1Message(True) &convert=$$.body.str_value { - message.obj = self.name; - } - var authType: BindAuthType = BindAuthType::Undef; - var authData: bytes = b""; - var simpleCreds: string = ""; - - : ASN1::ASN1Message(True) { - if ($$?.application_id) { - self.authType = cast(cast($$.application_id)); - self.authData = $$.application_data; + version: ASN1::ASN1Message(True) &convert=$$.body.num_value; + name: ASN1::ASN1Message(True) &convert=$$.body.str_value { + message.obj = self.name; } - } + var authType: BindAuthType = BindAuthType::Undef; + var authData: bytes = b""; + var simpleCreds: string = ""; - if ( |self.authData| > 0 ) { - switch ( self.authType ) { - BindAuthType::BIND_AUTH_SIMPLE -> - : void { - self.simpleCreds = self.authData.decode(); - message.arg = self.simpleCreds; + : ASN1::ASN1Message(True) { + if ($$?.application_id) { + self.authType = cast(cast($$.application_id)); + self.authData = $$.application_data; } + } - BindAuthType::BIND_AUTH_SASL -> - saslCreds: SaslCredentials { - message.arg = self.saslCreds.mechanism; - ctx.saslMechanism = self.saslCreds.mechanism; - } - - BindAuthType::SICILY_NEGOTIATE, BindAuthType::SICILY_RESPONSE -> - sicilyMessage: SicilyMessage { - message.arg = self.sicilyMessage.signature_decoded; - } - - * -> : void; - } &parse-from=self.authData; - }; + if (|self.authData| > 0) { + switch (self.authType) { + BindAuthType::BIND_AUTH_SIMPLE -> : void { + self.simpleCreds = self.authData.decode(); + message.arg = self.simpleCreds; + } + BindAuthType::BIND_AUTH_SASL -> saslCreds: SaslCredentials { + message.arg = self.saslCreds.mechanism; + ctx.saslMechanism = self.saslCreds.mechanism; + } + BindAuthType::SICILY_NEGOTIATE, + BindAuthType::SICILY_RESPONSE -> sicilyMessage: SicilyMessage { + message.arg = self.sicilyMessage.signature_decoded; + } + * -> : void; + } &parse-from=self.authData; + }; } &requires=(self?.authType && (self.authType != BindAuthType::Undef)); type ServerSaslCreds = unit { - serverSaslCreds: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::ContextSpecific && $$.tag.type_ == ASN1::ASN1Type(7)); - payload: bytes &size=self.serverSaslCreds.len.len; + serverSaslCreds: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::ContextSpecific && $$.tag.type_ == ASN1::ASN1Type(7)); + payload: bytes &size=self.serverSaslCreds.len.len; }; type BindResponse = unit(inout message: Message, ctx: Ctx&) { - : Result { - message.result_ = $$; + : Result { + message.result_ = $$; - # The SASL authentication was successful. We do not actually - # know if the following messages are encrypted or not. This may be - # mechanism and parameter specific. For example SCRAM-SHA512 or NTLM - # will continue to be cleartext, while SRP or GSS-API would be encrypted. - # - # Switch messageMode into trial mode which is explored via MessageDispatch - # and the MaybeEncrypted unit. - # - # Note, messageMode may be changed to something more specific like - # MS_KRB5 below. - if ( |ctx.saslMechanism| > 0 && $$.code == ResultCode::SUCCESS ) { - ctx.messageMode = MessageMode::MAYBE_ENCRYPTED; - } - } - - # Try to parse serverSaslCreds if there's any input remaining. This - # unit is parsed with &size, so &eod here works. - # - # Technically we should be able to tell from the ASN.1 structure - # if the serverSaslCreds field exists or not. But, not sure we can - # check if there's any bytes left at this point outside of passing - # in the length and playing with offset(). - serverSaslCreds: ServerSaslCreds[] &eod; - - # If the client requested GSS-SPNEGO, try to parse the server's response - # to switch message mode. - gss_spnego: GSS_SPNEGO_Subsequent &parse-from=self.serverSaslCreds[0].payload - if (ctx.saslMechanism == "GSS-SPNEGO" && |self.serverSaslCreds| > 0) { - - if ( $$?.negTokenResp ) { - local token = $$.negTokenResp; - if ( token.accepted && token?.supportedMechOid ) { - if ( token.supportedMechOid == GSSAPI_MECH_MS_KRB5 && token.responseToken ) { - ctx.messageMode = MessageMode::MS_KRB5; + # The SASL authentication was successful. We do not actually + # know if the following messages are encrypted or not. This may be + # mechanism and parameter specific. For example SCRAM-SHA512 or NTLM + # will continue to be cleartext, while SRP or GSS-API would be encrypted. + # + # Switch messageMode into trial mode which is explored via MessageDispatch + # and the MaybeEncrypted unit. + # + # Note, messageMode may be changed to something more specific like + # MS_KRB5 below. + if (|ctx.saslMechanism| > 0 && $$.code == ResultCode::SUCCESS) { + ctx.messageMode = MessageMode::MAYBE_ENCRYPTED; + } + } + + # Try to parse serverSaslCreds if there's any input remaining. This + # unit is parsed with &size, so &eod here works. + # + # Technically we should be able to tell from the ASN.1 structure + # if the serverSaslCreds field exists or not. But, not sure we can + # check if there's any bytes left at this point outside of passing + # in the length and playing with offset(). + serverSaslCreds: ServerSaslCreds[] &eod; + + # If the client requested GSS-SPNEGO, try to parse the server's response + # to switch message mode. + gss_spnego: GSS_SPNEGO_Subsequent &parse-from=self.serverSaslCreds[0].payload if(ctx.saslMechanism == "GSS-SPNEGO" && |self.serverSaslCreds| > 0) { + + if ($$?.negTokenResp) { + local token = $$.negTokenResp; + if (token.accepted && token?.supportedMechOid) { + if (token.supportedMechOid == GSSAPI_MECH_MS_KRB5 && token.responseToken) { + ctx.messageMode = MessageMode::MS_KRB5; + } + } } - } } - } }; #----------------------------------------------------------------------------- @@ -562,7 +553,7 @@ type BindResponse = unit(inout message: Message, ctx: Ctx&) { # https://tools.ietf.org/html/rfc4511#section-4.3 type UnbindRequest = unit(inout message: Message) { - # this page intentionally left blank + # this page intentionally left blank }; #----------------------------------------------------------------------------- @@ -570,66 +561,63 @@ type UnbindRequest = unit(inout message: Message) { # https://tools.ietf.org/html/rfc4511#section-4.5 public type SearchScope = enum { - SEARCH_BASE = 0, - SEARCH_SINGLE = 1, - SEARCH_TREE = 2, + SEARCH_BASE = 0, + SEARCH_SINGLE = 1, + SEARCH_TREE = 2, }; public type SearchDerefAlias = enum { - DEREF_NEVER = 0, - DEREF_IN_SEARCHING = 1, - DEREF_FINDING_BASE = 2, - DEREF_ALWAYS = 3, + DEREF_NEVER = 0, + DEREF_IN_SEARCHING = 1, + DEREF_FINDING_BASE = 2, + DEREF_ALWAYS = 3, }; type FilterType = enum { - FILTER_AND = 0, - FILTER_OR = 1, - FILTER_NOT = 2, - FILTER_EQ = 3, - FILTER_SUBSTR = 4, - FILTER_GE = 5, - FILTER_LE = 6, - FILTER_PRESENT = 7, - FILTER_APPROX = 8, - FILTER_EXT = 9, - FILTER_INVALID = 254, + FILTER_AND = 0, + FILTER_OR = 1, + FILTER_NOT = 2, + FILTER_EQ = 3, + FILTER_SUBSTR = 4, + FILTER_GE = 5, + FILTER_LE = 6, + FILTER_PRESENT = 7, + FILTER_APPROX = 8, + FILTER_EXT = 9, + FILTER_INVALID = 254, }; public type AttributeSelection = unit { - var attributes: vector; + var attributes: vector; - # TODO: parse AttributeSelection as per - # https://tools.ietf.org/html/rfc4511#section-4.5.1 - # and decide how deep that should be fleshed out. - : ASN1::ASN1Message(True) { - if (($$.head.tag.type_ == ASN1::ASN1Type::Sequence) && - ($$.body?.seq)) { - for (i in $$.body.seq.submessages) { - if (i.body?.str_value) { - self.attributes.push_back(i.body.str_value); + # TODO: parse AttributeSelection as per + # https://tools.ietf.org/html/rfc4511#section-4.5.1 + # and decide how deep that should be fleshed out. + : ASN1::ASN1Message(True) { + if (($$.head.tag.type_ == ASN1::ASN1Type::Sequence) && ($$.body?.seq)) { + for (i in $$.body.seq.submessages) { + if (i.body?.str_value) { + self.attributes.push_back(i.body.str_value); + } + } } - } } - } }; type AttributeValueAssertion = unit { - var desc: string = ""; - var val: string = ""; + var desc: string = ""; + var val: string = ""; - : ASN1::ASN1Message(True) { - if (($$.head.tag.type_ == ASN1::ASN1Type::Sequence) && - ($$.body?.seq) && - (|$$.body.seq.submessages| >= 2)) { - if ($$.body.seq.submessages[0].body?.str_value) { - self.desc = $$.body.seq.submessages[0].body.str_value; - } - if ($$.body.seq.submessages[1].body?.str_value) { - self.val = $$.body.seq.submessages[1].body.str_value; - } + : ASN1::ASN1Message(True) { + if (($$.head.tag.type_ == ASN1::ASN1Type::Sequence) && ($$.body?.seq) && (|$$.body.seq.submessages| >= 2)) { + if ($$.body.seq.submessages[0].body?.str_value) { + self.desc = $$.body.seq.submessages[0].body.str_value; + } + if ($$.body.seq.submessages[1].body?.str_value) { + self.val = $$.body.seq.submessages[1].body.str_value; + } + } } - } }; # An AND or OR search filter can consist of many sub-searchfilters, so we try to parse these @@ -643,199 +631,184 @@ type ParseNestedNot = unit { # Helper functions to properly format some custom data structures -public function utf16_guid_to_hex_repr(bts: bytes) : string { - # Rather ugly workaround to pretty-print the CLDAP DomainGuid UTF16-LE encoded string - # in the same format as Wireshark (aabbccdd-eeff-gghh-iijj-kkllmmnnoopp) +public function utf16_guid_to_hex_repr(bts: bytes): string { + # Rather ugly workaround to pretty-print the CLDAP DomainGuid UTF16-LE encoded string + # in the same format as Wireshark (aabbccdd-eeff-gghh-iijj-kkllmmnnoopp) - # We need to have exactly 16 bytes... - if ( |bts| != 16 ) { - # ... and otherwise just return an error code - return "GUID_FORMAT_FAILED"; - } - - local ret = ""; - for ( i in [[3, 2, 1, 0], [5, 4], [7, 6], [8, 9], [10, 11, 12, 13, 14, 15]] ) { - for ( j in i ) { - local bt: uint8 = *bts.at(j); - ret = ret + "%02x" % bt; - if ( j in [0, 4, 6, 9] ) { - ret = ret + "-"; - } + # We need to have exactly 16 bytes... + if (|bts| != 16) { + # ... and otherwise just return an error code + return "GUID_FORMAT_FAILED"; } - } - return ret; + + local ret = ""; + for (i in [[3, 2, 1, 0], [5, 4], [7, 6], [8, 9], [10, 11, 12, 13, 14, 15]]) { + for (j in i) { + local bt: uint8 = *bts.at(j); + ret = ret + "%02x" % bt; + if (j in [0, 4, 6, 9]) { + ret = ret + "-"; + } + } + } + return ret; } -public function bytes_sid_to_hex_repr(bts: bytes) : string { - local ret = ""; - local cnt = 0; +public function bytes_sid_to_hex_repr(bts: bytes): string { + local ret = ""; + local cnt = 0; - while ( cnt < |bts| ) { - local bt: uint8 = *bts.at(cnt); - ret = ret + "%02x" % bt; + while (cnt < |bts|) { + local bt: uint8 = *bts.at(cnt); + ret = ret + "%02x" % bt; - if ( cnt < |bts|-1 ) { - ret = ret + ":"; + if (cnt < |bts| - 1) { + ret = ret + ":"; + } + cnt += 1; } - cnt += 1; - } - return ret; + return ret; } -public function bytes_sid_to_SID_repr(bts: bytes) : string { - # Example: SID -> S-1-5-21-1153942841-488947194-1912431946 +public function bytes_sid_to_SID_repr(bts: bytes): string { + # Example: SID -> S-1-5-21-1153942841-488947194-1912431946 - # Needs to be exactly 24 bytes - if ( |bts| != 24 ) { - # ... and otherwise just return an error code - return "SID_FORMAT_FAILED"; - } - - local ret = "S-"; - local cnt = 0; - - # Mixed little and big endian, so turn everything to big endian first... - # Byte 1 seems to be skipped when parsing the SID - for ( i in [[0], [2, 3, 4, 5, 6, 7], [11, 10, 9, 8], [15, 14, 13, 12], [19, 18, 17, 16], [23, 22, 21, 20]] ) { - local dec_val_rep: bytes = b""; - for ( j in i ) { - local bt: uint8 = *bts.at(j); - dec_val_rep += bt; - cnt += 1; + # Needs to be exactly 24 bytes + if (|bts| != 24) { + # ... and otherwise just return an error code + return "SID_FORMAT_FAILED"; } - # ... so we can represent this integer value in big endian - ret = ret + "%u" % dec_val_rep.to_uint(spicy::ByteOrder::Big); + local ret = "S-"; + local cnt = 0; - # Only print the dash when we're not at the end - if ( cnt < 23 ) { - ret = ret + "-"; + # Mixed little and big endian, so turn everything to big endian first... + # Byte 1 seems to be skipped when parsing the SID + for (i in [[0], [2, 3, 4, 5, 6, 7], [11, 10, 9, 8], [15, 14, 13, 12], [19, 18, 17, 16], [23, 22, 21, 20]]) { + local dec_val_rep: bytes = b""; + for (j in i) { + local bt: uint8 = *bts.at(j); + dec_val_rep += bt; + cnt += 1; + } + + # ... so we can represent this integer value in big endian + ret = ret + "%u" % dec_val_rep.to_uint(spicy::ByteOrder::Big); + + # Only print the dash when we're not at the end + if (cnt < 23) { + ret = ret + "-"; + } } - } - return ret; + return ret; } +public function uint32_to_hex_repr(bts: bytes): string { + # Needs to be exactly 4 bytes + if (|bts| != 4) { + # ... and otherwise just return an error code + return "HEX_FORMAT_FAILED"; + } -public function uint32_to_hex_repr(bts: bytes) : string { - # Needs to be exactly 4 bytes - if ( |bts| != 4 ) { - # ... and otherwise just return an error code - return "HEX_FORMAT_FAILED"; - } - - # Workaround to print the hex value of an uint32, prepended with '0x' - local ret = "0x"; - for ( i in [3, 2, 1, 0] ) { - local bt: uint8 = *bts.at(i); - ret = ret + "%02x" % bt; - } - return ret; + # Workaround to print the hex value of an uint32, prepended with '0x' + local ret = "0x"; + for (i in [3, 2, 1, 0]) { + local bt: uint8 = *bts.at(i); + ret = ret + "%02x" % bt; + } + return ret; } # Helper to compute a string representation of a `SearchFilter`. public function string_representation(search_filter: SearchFilter): string { local repr: string; - switch ( local fType = search_filter.filterType ) { - # The NOT, AND and OR filter types are trees and may hold many leaf nodes. So recursively get - # the stringPresentations for the leaf nodes and add them all in one final statement. + switch (local fType = search_filter.filterType) { + # The NOT, AND and OR filter types are trees and may hold many leaf nodes. So recursively get + # the stringPresentations for the leaf nodes and add them all in one final statement. + case FilterType::FILTER_NOT: { + repr = "(!%s)" % search_filter.FILTER_NOT.searchfilter.stringRepresentation; + } + case FilterType::FILTER_AND, + FilterType::FILTER_OR: { + local nestedObj: ParseNestedAndOr; + local printChar = ""; - case FilterType::FILTER_NOT: { - repr = "(!%s)" % search_filter.FILTER_NOT.searchfilter.stringRepresentation; - } + if (fType == FilterType::FILTER_AND) { + printChar = "&"; + nestedObj = search_filter.FILTER_AND; + } else { + printChar = "|"; + nestedObj = search_filter.FILTER_OR; + } - case FilterType::FILTER_AND, FilterType::FILTER_OR: { - local nestedObj: ParseNestedAndOr; - local printChar = ""; + # Build the nested AND/OR statement in this loop. When we encounter the first element, + # we open the statement. At the second element, we close our first complete statement. For every + # following statement, we extend the AND/OR statement by wrapping it around the already completed + # statement. Although it is also valid to not do this wrapping, which is logically equivalent, e.g: + # + # (1) (2) + # (?(a=b)(c=d)(e=f)) vs (?(?(a=b)(c=d))(e=f)) + # + # the latter version is also shown by Wireshark. So although the parsed structure actually represents + # version (1) of the query, we now choose to print version (2). If this is not desirable, swap the code + # for the following: + # + # # Construct the nested structure, like (1) + # for ( SF in nestedObj.searchfilters ) { + # self.stringRepresentation = self.stringRepresentation + SF.stringRepresentation + # } + # # Close it with brackets and put the correct printChar for AND/OR in the statement + # self.stringRepresentation = "(%s" % printChar + self.stringRepresentation + ")"; + # - if ( fType == FilterType::FILTER_AND ) { - printChar = "&"; - nestedObj = search_filter.FILTER_AND; - } else { - printChar = "|"; - nestedObj = search_filter.FILTER_OR; + local i = 0; + for (searchFilter in nestedObj.searchfilters) { + switch (i) { + case 0: { + repr = "(%s%s%s" % (printChar, searchFilter.stringRepresentation, + # If we have exactly one element immediately close the statement since we are done. + |nestedObj.searchfilters| == 1 ? ")" : ""); + } + case 1: { + repr = repr + searchFilter.stringRepresentation + ")"; + } + default: { + repr = "(%s" % printChar + repr + searchFilter.stringRepresentation + ")"; + } + } + i += 1; + } } - # Build the nested AND/OR statement in this loop. When we encounter the first element, - # we open the statement. At the second element, we close our first complete statement. For every - # following statement, we extend the AND/OR statement by wrapping it around the already completed - # statement. Although it is also valid to not do this wrapping, which is logically equivalent, e.g: - # - # (1) (2) - # (?(a=b)(c=d)(e=f)) vs (?(?(a=b)(c=d))(e=f)) - # - # the latter version is also shown by Wireshark. So although the parsed structure actually represents - # version (1) of the query, we now choose to print version (2). If this is not desirable, swap the code - # for the following: - # - # # Construct the nested structure, like (1) - # for ( SF in nestedObj.searchfilters ) { - # self.stringRepresentation = self.stringRepresentation + SF.stringRepresentation - # } - # # Close it with brackets and put the correct printChar for AND/OR in the statement - # self.stringRepresentation = "(%s" % printChar + self.stringRepresentation + ")"; - # - - local i = 0; - for ( searchFilter in nestedObj.searchfilters ) { - switch ( i ) { - case 0: { - repr = "(%s%s%s" % ( - printChar, - searchFilter.stringRepresentation, - # If we have exactly one element immediately close the statement since we are done. - |nestedObj.searchfilters| == 1 ? ")" : "" - ); - } - case 1: { - repr = repr + searchFilter.stringRepresentation + ")"; - } - default: { - repr = "(%s" % printChar + repr + searchFilter.stringRepresentation + ")"; - } - } - i += 1; + # The following FilterTypes are leaf nodes and can thus be represented in a statement + case FilterType::FILTER_EXT: { + # For extended search filters the meaning of the individual fields in + # `DecodedAttributeValue` is slightly different. + repr = "(%s:%s:=%s)" % (search_filter.FILTER_EXT.assertionValueDecoded, search_filter.FILTER_EXT.attributeDesc.decode(), search_filter.FILTER_EXT.matchValue); } - } + case FilterType::FILTER_APPROX: { + repr = "(%s~=%s)" % (search_filter.FILTER_APPROX.attributeDesc.decode(), search_filter.FILTER_APPROX.assertionValueDecoded); + } + case FilterType::FILTER_EQ: { + repr = "(%s=%s)" % (search_filter.FILTER_EQ.attributeDesc.decode(), search_filter.FILTER_EQ.assertionValueDecoded); + } + case FilterType::FILTER_GE: { + repr = "(%s>=%s)" % (search_filter.FILTER_GE.attributeDesc.decode(), search_filter.FILTER_GE.assertionValueDecoded); + } + case FilterType::FILTER_LE: { + repr = "(%s<=%s)" % (search_filter.FILTER_LE.attributeDesc.decode(), search_filter.FILTER_LE.assertionValueDecoded); + } + case FilterType::FILTER_SUBSTR: { + local anys: string = ""; + if (|search_filter.FILTER_SUBSTR.anys| > 0) + anys = b"*".join(search_filter.FILTER_SUBSTR.anys).decode() + "*"; - # The following FilterTypes are leaf nodes and can thus be represented in a statement - - case FilterType::FILTER_EXT: { - # For extended search filters the meaning of the individual fields in - # `DecodedAttributeValue` is slightly different. - repr = "(%s:%s:=%s)" % (search_filter.FILTER_EXT.assertionValueDecoded, - search_filter.FILTER_EXT.attributeDesc.decode(), - search_filter.FILTER_EXT.matchValue); - } - case FilterType::FILTER_APPROX: { - repr = "(%s~=%s)" % (search_filter.FILTER_APPROX.attributeDesc.decode(), - search_filter.FILTER_APPROX.assertionValueDecoded); - } - case FilterType::FILTER_EQ: { - repr = "(%s=%s)" % (search_filter.FILTER_EQ.attributeDesc.decode(), - search_filter.FILTER_EQ.assertionValueDecoded); - } - case FilterType::FILTER_GE: { - repr = "(%s>=%s)" % (search_filter.FILTER_GE.attributeDesc.decode(), - search_filter.FILTER_GE.assertionValueDecoded); - } - case FilterType::FILTER_LE: { - repr = "(%s<=%s)" % (search_filter.FILTER_LE.attributeDesc.decode(), - search_filter.FILTER_LE.assertionValueDecoded); - } - case FilterType::FILTER_SUBSTR: { - local anys: string = ""; - if ( |search_filter.FILTER_SUBSTR.anys| > 0 ) - anys = b"*".join(search_filter.FILTER_SUBSTR.anys).decode() + "*"; - - repr = "(%s=%s*%s%s)" % (search_filter.FILTER_SUBSTR.attributeDesc.decode(), - search_filter.FILTER_SUBSTR.initial, - anys, - search_filter.FILTER_SUBSTR.final); - } - case FilterType::FILTER_PRESENT: { - repr = "(%s=*)" % search_filter.FILTER_PRESENT; - } + repr = "(%s=%s*%s%s)" % (search_filter.FILTER_SUBSTR.attributeDesc.decode(), search_filter.FILTER_SUBSTR.initial, anys, search_filter.FILTER_SUBSTR.final); + } + case FilterType::FILTER_PRESENT: { + repr = "(%s=*)" % search_filter.FILTER_PRESENT; + } } return repr; @@ -843,165 +816,146 @@ public function string_representation(search_filter: SearchFilter): string { # Represents an (extended) key-value pair present in SearchFilters type DecodedAttributeValue = unit(fType: FilterType) { - var assertionValueDecoded: string = ""; + var assertionValueDecoded: string = ""; - : uint8; - attributeDesc_len: uint8; - attributeDesc: bytes &size=self.attributeDesc_len; + : uint8; + attributeDesc_len: uint8; + attributeDesc: bytes &size=self.attributeDesc_len; - : uint8; - assertionValue_len: uint8; - assertionValue: bytes &size=self.assertionValue_len; + : uint8; + assertionValue_len: uint8; + assertionValue: bytes &size=self.assertionValue_len; - # Only for the FILTER_EXT type, parse extra fields - : uint8 if ( fType == FilterType::FILTER_EXT ); - matchValue_len: uint8 if( fType == FilterType::FILTER_EXT ); - matchValue: bytes &size=self.matchValue_len if ( fType == FilterType::FILTER_EXT ); + # Only for the FILTER_EXT type, parse extra fields + : uint8 if(fType == FilterType::FILTER_EXT); + matchValue_len: uint8 if(fType == FilterType::FILTER_EXT); + matchValue: bytes &size=self.matchValue_len if(fType == FilterType::FILTER_EXT); - on %done { - switch ( self.attributeDesc ) { - # Special parsing required for some CLDAP attributes, - # see https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/895a7744-aff3-4f64-bcfa-f8c05915d2e9 + on %done { + switch (self.attributeDesc) { + # Special parsing required for some CLDAP attributes, + # see https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/895a7744-aff3-4f64-bcfa-f8c05915d2e9 + case b"DomainGuid": { + self.assertionValueDecoded = utf16_guid_to_hex_repr(self.assertionValue); + } + case b"objectSid", + b"AAC": { + self.assertionValueDecoded = bytes_sid_to_hex_repr(self.assertionValue); + } + case b"DomainSid": { + self.assertionValueDecoded = bytes_sid_to_SID_repr(self.assertionValue); + } + case b"NtVer": { + self.assertionValueDecoded = uint32_to_hex_repr(self.assertionValue); + } - case b"DomainGuid": { - self.assertionValueDecoded = utf16_guid_to_hex_repr(self.assertionValue); - } - - case b"objectSid", b"AAC": { - self.assertionValueDecoded = bytes_sid_to_hex_repr(self.assertionValue); - } - - case b"DomainSid": { - self.assertionValueDecoded = bytes_sid_to_SID_repr(self.assertionValue); - } - - case b"NtVer": { - self.assertionValueDecoded = uint32_to_hex_repr(self.assertionValue); - } - - # By default, decode with UTF-8 - default: { - self.assertionValueDecoded = self.assertionValue.decode(); - } + # By default, decode with UTF-8 + default: { + self.assertionValueDecoded = self.assertionValue.decode(); + } + } } - } }; type SubstringFilter = unit { - var initial: string; - var final: string; - var anys: vector; + var initial: string; + var final: string; + var anys: vector; - : uint8; # filter tag - attributeDesc_len: uint8; - attributeDesc: bytes &size=self.attributeDesc_len; + : uint8; # filter tag + attributeDesc_len: uint8; + attributeDesc: bytes &size=self.attributeDesc_len; - # Crunch through the sequence/choice of substrings. - # - # https://datatracker.ietf.org/doc/html/rfc4511#section-4.5.1 - header: ASN1::ASN1Header; - : ASN1::ASN1Message(False)[] &size=self.header.len.len foreach { - local data = $$.application_data.decode(); - if ( $$.application_id == 0 ) { - self.initial = data; - } else if ( $$.application_id == 1 ) { - self.anys.push_back(data); - } else if ( $$.application_id == 2 ) { - self.final = data; - } else { - throw "invalid substring choice %s" % $$.application_id; + # Crunch through the sequence/choice of substrings. + # + # https://datatracker.ietf.org/doc/html/rfc4511#section-4.5.1 + header: ASN1::ASN1Header; + : ASN1::ASN1Message(False)[] &size=self.header.len.len foreach { + local data = $$.application_data.decode(); + if ($$.application_id == 0) { + self.initial = data; + } else if ($$.application_id == 1) { + self.anys.push_back(data); + } else if ($$.application_id == 2) { + self.final = data; + } else { + throw "invalid substring choice %s" % $$.application_id; + } } - } }; type SearchFilter = unit { - var filterType: FilterType = FilterType::Undef; - var filterBytes: bytes = b""; - var filterLen: uint64 = 0; - var stringRepresentation: string = ""; + var filterType: FilterType = FilterType::Undef; + var filterBytes: bytes = b""; + var filterLen: uint64 = 0; + var stringRepresentation: string = ""; - : ASN1::ASN1Message(True) { - if ($$?.application_id) { - self.filterType = cast(cast($$.application_id)); - self.filterBytes = $$.application_data; - self.filterLen = $$.head.len.len; - } else { - self.filterType = FilterType::FILTER_INVALID; + : ASN1::ASN1Message(True) { + if ($$?.application_id) { + self.filterType = cast(cast($$.application_id)); + self.filterBytes = $$.application_data; + self.filterLen = $$.head.len.len; + } else { + self.filterType = FilterType::FILTER_INVALID; + } } - } - switch ( self.filterType ) { + switch (self.filterType) { - # FilterTypes that hold one or more SearchFilters inside them + # FilterTypes that hold one or more SearchFilters inside them + FilterType::FILTER_AND -> FILTER_AND: ParseNestedAndOr() &parse-from=self.filterBytes; + FilterType::FILTER_OR -> FILTER_OR: ParseNestedAndOr() &parse-from=self.filterBytes; + FilterType::FILTER_NOT -> FILTER_NOT: ParseNestedNot() &parse-from=self.filterBytes; - FilterType::FILTER_AND -> FILTER_AND: ParseNestedAndOr() - &parse-from=self.filterBytes; - FilterType::FILTER_OR -> FILTER_OR: ParseNestedAndOr() - &parse-from=self.filterBytes; - FilterType::FILTER_NOT -> FILTER_NOT: ParseNestedNot() - &parse-from=self.filterBytes; + # FilterTypes that we can actually convert to a string + FilterType::FILTER_EQ -> FILTER_EQ: DecodedAttributeValue(FilterType::FILTER_EQ) &parse-from=self.filterBytes; + FilterType::FILTER_SUBSTR -> FILTER_SUBSTR: SubstringFilter &parse-from=self.filterBytes; + FilterType::FILTER_GE -> FILTER_GE: DecodedAttributeValue(FilterType::FILTER_GE) &parse-from=self.filterBytes; + FilterType::FILTER_LE -> FILTER_LE: DecodedAttributeValue(FilterType::FILTER_LE) &parse-from=self.filterBytes; + FilterType::FILTER_APPROX -> FILTER_APPROX: DecodedAttributeValue(FilterType::FILTER_APPROX) &parse-from=self.filterBytes; + FilterType::FILTER_EXT -> FILTER_EXT: DecodedAttributeValue(FilterType::FILTER_EXT) &parse-from=self.filterBytes; + FilterType::FILTER_PRESENT -> FILTER_PRESENT: ASN1::ASN1OctetString(self.filterLen, False) &convert=$$.value.decode(spicy::Charset::ASCII) &parse-from=self.filterBytes; + }; - # FilterTypes that we can actually convert to a string + # So when you're done with recursively parsing the filters, we can now leverage the tree structure to + # recursively get the stringRepresentations for those leaves, which are SearchFilters - FilterType::FILTER_EQ -> FILTER_EQ: DecodedAttributeValue(FilterType::FILTER_EQ) - &parse-from=self.filterBytes; - FilterType::FILTER_SUBSTR -> FILTER_SUBSTR: SubstringFilter - &parse-from=self.filterBytes; - FilterType::FILTER_GE -> FILTER_GE: DecodedAttributeValue(FilterType::FILTER_GE) - &parse-from=self.filterBytes; - FilterType::FILTER_LE -> FILTER_LE: DecodedAttributeValue(FilterType::FILTER_LE) - &parse-from=self.filterBytes; - FilterType::FILTER_APPROX -> FILTER_APPROX: DecodedAttributeValue(FilterType::FILTER_APPROX) - &parse-from=self.filterBytes; - FilterType::FILTER_EXT -> FILTER_EXT: DecodedAttributeValue(FilterType::FILTER_EXT) - &parse-from=self.filterBytes; - FilterType::FILTER_PRESENT -> FILTER_PRESENT: ASN1::ASN1OctetString(self.filterLen, False) - &convert=$$.value.decode(spicy::Charset::ASCII) - &parse-from=self.filterBytes; - }; - - # So when you're done with recursively parsing the filters, we can now leverage the tree structure to - # recursively get the stringRepresentations for those leaves, which are SearchFilters - - on %done { - self.stringRepresentation = string_representation(self); - } - - on %error { - self.stringRepresentation = "FILTER_PARSING_ERROR"; - } + on %done { + self.stringRepresentation = string_representation(self); + } + on %error { + self.stringRepresentation = "FILTER_PARSING_ERROR"; + } }; -public type SearchRequest = unit(inout message: Message) { - baseObject: ASN1::ASN1Message(True) &convert=$$.body.str_value { - message.obj = self.baseObject; - } - scope: ASN1::ASN1Message(True) &convert=cast(cast($$.body.num_value)) - &default=SearchScope::Undef { - message.arg = "%s" % self.scope; - } - deref: ASN1::ASN1Message(True) &convert=cast(cast($$.body.num_value)) - &default=SearchDerefAlias::Undef; - sizeLimit: ASN1::ASN1Message(True) &convert=$$.body.num_value &default=0; - timeLimit: ASN1::ASN1Message(True) &convert=$$.body.num_value &default=0; - typesOnly: ASN1::ASN1Message(True) &convert=$$.body.bool_value &default=False; - filter: SearchFilter &convert=$$.stringRepresentation; - attributes: AttributeSelection &convert=$$.attributes; +public type SearchRequest = unit(inout message: Message) { + baseObject: ASN1::ASN1Message(True) &convert=$$.body.str_value { + message.obj = self.baseObject; + } + scope: ASN1::ASN1Message(True) &convert=cast(cast($$.body.num_value)) &default=SearchScope::Undef { + message.arg = "%s" % self.scope; + } + deref: ASN1::ASN1Message(True) &convert=cast(cast($$.body.num_value)) &default=SearchDerefAlias::Undef; + sizeLimit: ASN1::ASN1Message(True) &convert=$$.body.num_value &default=0; + timeLimit: ASN1::ASN1Message(True) &convert=$$.body.num_value &default=0; + typesOnly: ASN1::ASN1Message(True) &convert=$$.body.bool_value &default=False; + filter: SearchFilter &convert=$$.stringRepresentation; + attributes: AttributeSelection &convert=$$.attributes; }; type SearchResultEntry = unit(inout message: Message) { - objectName: ASN1::ASN1Message(True) &convert=$$.body.str_value { - message.obj = self.objectName; - } - # TODO: if we want to descend down into PartialAttributeList - attributes: ASN1::ASN1Message(True); + objectName: ASN1::ASN1Message(True) &convert=$$.body.str_value { + message.obj = self.objectName; + } + # TODO: if we want to descend down into PartialAttributeList + attributes: ASN1::ASN1Message(True); }; type SearchResultDone = unit(inout message: Message) { - : Result { - message.result_ = $$; - } + : Result { + message.result_ = $$; + } }; # TODO: implement SearchResultReference @@ -1014,17 +968,17 @@ type SearchResultDone = unit(inout message: Message) { # https://tools.ietf.org/html/rfc4511#section-4.6 type ModifyRequest = unit(inout message: Message) { - objectName: ASN1::ASN1Message(True) &convert=$$.body.str_value { - message.obj = self.objectName; - } + objectName: ASN1::ASN1Message(True) &convert=$$.body.str_value { + message.obj = self.objectName; + } - # TODO: parse changes + # TODO: parse changes }; type ModifyResponse = unit(inout message: Message) { - : Result { - message.result_ = $$; - } + : Result { + message.result_ = $$; + } }; #----------------------------------------------------------------------------- @@ -1038,9 +992,9 @@ type ModifyResponse = unit(inout message: Message) { # }; type AddResponse = unit(inout message: Message) { - : Result { - message.result_ = $$; - } + : Result { + message.result_ = $$; + } }; #----------------------------------------------------------------------------- @@ -1048,15 +1002,15 @@ type AddResponse = unit(inout message: Message) { # https://tools.ietf.org/html/rfc4511#section-4.8 type DelRequest = unit(inout message: Message) { - objectName: ASN1::ASN1Message(True) &convert=$$.body.str_value { - message.obj = self.objectName; - } + objectName: ASN1::ASN1Message(True) &convert=$$.body.str_value { + message.obj = self.objectName; + } }; type DelResponse = unit(inout message: Message) { - : Result { - message.result_ = $$; - } + : Result { + message.result_ = $$; + } }; #----------------------------------------------------------------------------- @@ -1069,9 +1023,9 @@ type DelResponse = unit(inout message: Message) { # }; type ModDNResponse = unit(inout message: Message) { - : Result { - message.result_ = $$; - } + : Result { + message.result_ = $$; + } }; #----------------------------------------------------------------------------- @@ -1084,9 +1038,9 @@ type ModDNResponse = unit(inout message: Message) { # }; type CompareResponse = unit(inout message: Message) { - : Result { - message.result_ = $$; - } + : Result { + message.result_ = $$; + } }; #----------------------------------------------------------------------------- @@ -1094,68 +1048,66 @@ type CompareResponse = unit(inout message: Message) { # https://tools.ietf.org/html/rfc4511#section-4.11 type AbandonRequest = unit(inout message: Message) { - messageID: ASN1::ASN1Message(True) &convert=$$.body.num_value { - message.obj = "%d" % (self.messageID); - } + messageID: ASN1::ASN1Message(True) &convert=$$.body.num_value { + message.obj = "%d" % (self.messageID); + } }; #----------------------------------------------------------------------------- # Extended Operation # https://tools.ietf.org/html/rfc4511#section-4.12 type ExtendedRequest = unit(inout message: Message, ctx: Ctx&) { - var requestValue: bytes; - header: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::ContextSpecific); - requestName: bytes &size=self.header.len.len &convert=$$.decode(spicy::Charset::ASCII) { - message.obj = $$; - } + var requestValue: bytes; + header: ASN1::ASN1Header &requires=($$.tag.class == ASN1::ASN1Class::ContextSpecific); + requestName: bytes &size=self.header.len.len &convert=$$.decode(spicy::Charset::ASCII) { + message.obj = $$; + } - # If there's more byte to parse, it's the requestValue. - : ASN1::ASN1Message(False) - &requires=($$.head.tag.class == ASN1::ASN1Class::ContextSpecific) - if ( message.opLen > self.offset() ) { + # If there's more byte to parse, it's the requestValue. + : ASN1::ASN1Message(False) &requires=($$.head.tag.class == ASN1::ASN1Class::ContextSpecific) if(message.opLen > self.offset()) { - self.requestValue = $$.application_data; - } + self.requestValue = $$.application_data; + } - on %done { - # Did the client request StartTLS? - # - # https://datatracker.ietf.org/doc/html/rfc4511#section-4.14.1 - if ( self.requestName == "1.3.6.1.4.1.1466.20037" ) - ctx.startTlsRequested = True; - } + on %done { + # Did the client request StartTLS? + # + # https://datatracker.ietf.org/doc/html/rfc4511#section-4.14.1 + if (self.requestName == "1.3.6.1.4.1.1466.20037") + ctx.startTlsRequested = True; + } }; #----------------------------------------------------------------------------- type ExtendedResponseEntry = unit(inout r: ExtendedResponse) { - : ASN1::ASN1Message(False) &requires=($$.head.tag.class == ASN1::ASN1Class::ContextSpecific) { - if ( $$.head.tag.type_ == ASN1::ASN1Type(10) ) - r.responseName = $$.application_data; - else if ( $$.head.tag.type_ == ASN1::ASN1Type(11) ) - r.responseValue = $$.application_data; - else - throw "Unhandled extended response tag %s" % $$.head.tag; - } + : ASN1::ASN1Message(False) &requires=($$.head.tag.class == ASN1::ASN1Class::ContextSpecific) { + if ($$.head.tag.type_ == ASN1::ASN1Type(10)) + r.responseName = $$.application_data; + else if ($$.head.tag.type_ == ASN1::ASN1Type(11)) + r.responseValue = $$.application_data; + else + throw "Unhandled extended response tag %s" % $$.head.tag; + } }; #----------------------------------------------------------------------------- type ExtendedResponse = unit(inout message: Message, ctx: Ctx&) { - var responseName: bytes; - var responseValue: bytes; - : Result { - message.result_ = $$; - } + var responseName: bytes; + var responseValue: bytes; + : Result { + message.result_ = $$; + } - # Try to parse two ASN1 entries if there are bytes left in the unit. - # Both are optional and identified by context specific tagging. - : ExtendedResponseEntry(self) if ( message.opLen > self.offset() ); - : ExtendedResponseEntry(self) if ( message.opLen > self.offset() ); + # Try to parse two ASN1 entries if there are bytes left in the unit. + # Both are optional and identified by context specific tagging. + : ExtendedResponseEntry(self) if(message.opLen > self.offset()); + : ExtendedResponseEntry(self) if(message.opLen > self.offset()); - on %done { - # Client had requested StartTLS and it was successful? Switch to SSL. - if ( ctx.startTlsRequested && message.result_.code == ResultCode::SUCCESS ) - ctx.messageMode = MessageMode::TLS; - } + on %done { + # Client had requested StartTLS and it was successful? Switch to SSL. + if (ctx.startTlsRequested && message.result_.code == ResultCode::SUCCESS) + ctx.messageMode = MessageMode::TLS; + } }; #----------------------------------------------------------------------------- @@ -1168,5 +1120,5 @@ type ExtendedResponse = unit(inout message: Message, ctx: Ctx&) { # }; on LDAP::Message::%done { - spicy::accept_input(); + spicy::accept_input(); } diff --git a/src/analyzer/protocol/ldap/ldap_zeek.spicy b/src/analyzer/protocol/ldap/ldap_zeek.spicy index 3795f1ff74..33fcd47dc9 100644 --- a/src/analyzer/protocol/ldap/ldap_zeek.spicy +++ b/src/analyzer/protocol/ldap/ldap_zeek.spicy @@ -6,9 +6,9 @@ import LDAP; import zeek; on LDAP::TlsForward::%init { - zeek::protocol_begin("SSL"); + zeek::protocol_begin("SSL"); } on LDAP::TlsForward::chunk { - zeek::protocol_data_in(zeek::is_orig(), self.chunk); + zeek::protocol_data_in(zeek::is_orig(), self.chunk); } diff --git a/src/analyzer/protocol/quic/QUIC.spicy b/src/analyzer/protocol/quic/QUIC.spicy index dbac41d761..a071d1ea19 100644 --- a/src/analyzer/protocol/quic/QUIC.spicy +++ b/src/analyzer/protocol/quic/QUIC.spicy @@ -7,131 +7,124 @@ import spicy; import zeek; # The interface to the C++ code that handles the decryption of the INITIAL packet payload using well-known keys -public function decrypt_crypto_payload( - version: uint32, - data: bytes, - connection_id: bytes, - encrypted_offset: uint64, - payload_offset: uint64, - from_client: bool -): bytes &cxxname="QUIC_decrypt_crypto_payload"; - +public function decrypt_crypto_payload(version: uint32, data: bytes, connection_id: bytes, encrypted_offset: uint64, payload_offset: uint64, from_client: bool): bytes &cxxname="QUIC_decrypt_crypto_payload"; # Can we decrypt? function can_decrypt(long_header: LongHeaderPacket, context: Context, crypto: CryptoSinkUnit&): bool { - if ( ! long_header.is_initial ) - return False; + if (!long_header.is_initial) + return False; - if ( crypto == Null ) - return False; + if (crypto == Null) + return False; - # Can only decrypt the responder if we've seen the initial destination conn id. - if ( ! crypto.is_orig && ! context.initial_destination_conn_id ) - return False; + # Can only decrypt the responder if we've seen the initial destination conn id. + if (!crypto.is_orig && !context.initial_destination_conn_id) + return False; - # Only attempt decryption if we haven't flushed some SSL data yet. - return ! crypto.finished; + # Only attempt decryption if we haven't flushed some SSL data yet. + return !crypto.finished; } function reset_crypto(context: Context&) { - # Recreate all the crypto state on the next %init of Packet. - zeek::protocol_handle_close(context.ssl_handle); - unset context.ssl_handle; - context.client_crypto = Null; - context.server_crypto = Null; - context.client_sink = Null; - context.server_sink = Null; - context.initial_destination_conn_id = Null; + # Recreate all the crypto state on the next %init of Packet. + zeek::protocol_handle_close(context.ssl_handle); + unset context.ssl_handle; + context.client_crypto = Null; + context.server_crypto = Null; + context.client_sink = Null; + context.server_sink = Null; + context.initial_destination_conn_id = Null; } # This unit is connected with the server and client sinks receiving # CRYPTO frames and forwards data to the SSL handle in the context. type CryptoSinkUnit = unit(is_orig: bool, context: Context&) { - var buffered: bytes; - var length: uint32 = 0; - var is_orig: bool = is_orig; - var finished: bool; + var buffered: bytes; + var length: uint32 = 0; + var is_orig: bool = is_orig; + var finished: bool; - # The first 4 bytes of crypto data contain the expected tag and a - # 24bit length from the TLS HandshakeMessage. Extract the length - # so we can determine when all CRYPTO frames have arrived. - # - # https://datatracker.ietf.org/doc/html/rfc8446#section-4 - # - # struct { - # HandshakeType msg_type; /* handshake type */ - # uint24 length; /* remaining bytes in message */ - # ... - # - : uint8 { - self.buffered += $$; - } - - len: uint8[3] { - self.length = (cast($$[0]) << 16) + (cast($$[1]) << 8) + cast($$[2]) + 4; - - self.buffered += $$[0]; - self.buffered += $$[1]; - self.buffered += $$[2]; - } - - : void &requires=(self.length <= 2**14 + 256) { # The length MUST NOT exceed 2^14 + 256 bytes (RFC 8446) - - # The client or server hello data is forwarded to the SSL analyzer as a - # TLSPlaintext record with legacy_record_version set to \x03\x03 (1.3). + # The first 4 bytes of crypto data contain the expected tag and a + # 24bit length from the TLS HandshakeMessage. Extract the length + # so we can determine when all CRYPTO frames have arrived. # - # enum { - # invalid(0), - # change_cipher_spec(20), - # alert(21), - # handshake(22), - # application_data(23), - # (255) - # } ContentType; + # https://datatracker.ietf.org/doc/html/rfc8446#section-4 # - # struct { - # ContentType type; - # ProtocolVersion legacy_record_version; - # uint16 length; - # opaque fragment[TLSPlaintext.length]; - # } TLSPlaintext; + # struct { + # HandshakeType msg_type; /* handshake type */ + # uint24 length; /* remaining bytes in message */ + # ... # - # https://datatracker.ietf.org/doc/html/rfc8446#section-5.1 - local length_bytes = pack(cast(self.length), spicy::ByteOrder::Big); - zeek::protocol_data_in(is_orig, b"\x16\x03\x03" + length_bytes + self.buffered, context.ssl_handle); - } + : uint8 { + self.buffered += $$; + } - : bytes &chunked &size=(self.length - 4) { - zeek::protocol_data_in(is_orig, $$, context.ssl_handle); - } + len: uint8[3] { + self.length = (cast($$[0]) << 16) + (cast($$[1]) << 8) + cast($$[2]) + 4; - : void { - self.finished = True; - } + self.buffered += $$[0]; + self.buffered += $$[1]; + self.buffered += $$[2]; + } + + : void &requires=(self.length <= 2**14 + 256) { + # The length MUST NOT exceed 2^14 + 256 bytes (RFC 8446) + + # The client or server hello data is forwarded to the SSL analyzer as a + # TLSPlaintext record with legacy_record_version set to \x03\x03 (1.3). + # + # enum { + # invalid(0), + # change_cipher_spec(20), + # alert(21), + # handshake(22), + # application_data(23), + # (255) + # } ContentType; + # + # struct { + # ContentType type; + # ProtocolVersion legacy_record_version; + # uint16 length; + # opaque fragment[TLSPlaintext.length]; + # } TLSPlaintext; + # + # https://datatracker.ietf.org/doc/html/rfc8446#section-5.1 + local length_bytes = pack(cast(self.length), spicy::ByteOrder::Big); + zeek::protocol_data_in(is_orig, b"\x16\x03\x03" + length_bytes + self.buffered, context.ssl_handle); + } + + : bytes &chunked &size=(self.length - 4) { + zeek::protocol_data_in(is_orig, $$, context.ssl_handle); + } + + : void { + self.finished = True; + } }; ############## ## Context ############## type Context = struct { - client_cid_len: uint8; - server_cid_len: uint8; + client_cid_len: uint8; + server_cid_len: uint8; - # The DCID used by the client is employed by client and - # server for packet protection. Packet re-ordering - # will make life miserable. - # - # https://quicwg.org/base-drafts/rfc9001.html#appendix-A - initial_destination_conn_id: optional; + # The DCID used by the client is employed by client and + # server for packet protection. Packet re-ordering + # will make life miserable. + # + # https://quicwg.org/base-drafts/rfc9001.html#appendix-A + initial_destination_conn_id: optional; - # Track crypto state. - client_crypto: CryptoSinkUnit&; - client_sink: sink&; - server_crypto: CryptoSinkUnit&; - server_sink: sink&; + # Track crypto state. + client_crypto: CryptoSinkUnit&; + client_sink: sink&; + server_crypto: CryptoSinkUnit&; + server_sink: sink&; - ssl_handle: zeek::ProtocolHandle &optional; + ssl_handle: zeek::ProtocolHandle &optional; }; ############## @@ -160,59 +153,59 @@ const Version1: uint32 = 0x00000001; const Version2: uint32 = 0x6b3343cf; type LongPacketTypeV1 = enum { - INITIAL = 0, - ZERO_RTT = 1, - HANDSHAKE = 2, - RETRY = 3, + INITIAL = 0, + ZERO_RTT = 1, + HANDSHAKE = 2, + RETRY = 3, }; # V2 changed packet types to avoid ossification. # # https://www.rfc-editor.org/rfc/rfc9369.html#name-long-header-packet-types type LongPacketTypeV2 = enum { - INITIAL = 1, - ZERO_RTT = 2, - HANDSHAKE = 3, - RETRY = 0, + INITIAL = 1, + ZERO_RTT = 2, + HANDSHAKE = 3, + RETRY = 0, }; type HeaderForm = enum { - SHORT = 0, - LONG = 1, + SHORT = 0, + LONG = 1, }; type FrameType = enum { - PADDING = 0x00, - PING = 0x01, - ACK1 = 0x02, - ACK2 = 0x03, - RESET_STREAM = 0x04, - STOP_SENDING = 0x05, - CRYPTO = 0x06, - NEW_TOKEN = 0x07, - STREAM1 = 0x08, - STREAM2 = 0x09, - STREAM3 = 0x0a, - STREAM4 = 0x0b, - STREAM5 = 0x0c, - STREAM6 = 0x0d, - STREAM7 = 0x0e, - STREAM8 = 0x0f, - MAX_DATA = 0x10, - MAX_STREAM_DATA = 0x11, - MAX_STREAMS1 = 0x12, - MAX_STREAMS2 = 0x13, - DATA_BLOCKED = 0x14, - STREAM_DATA_BLOCKED = 0x15, - STREAMS_BLOCKED1 = 0x16, - STREAMS_BLOCKED2 = 0x17, - NEW_CONNECTION_ID = 0x18, - RETIRE_CONNECTION_ID = 0x19, - PATH_CHALLENGE = 0x1a, - PATH_RESPONSE = 0x1b, - CONNECTION_CLOSE1 = 0x1c, - CONNECTION_CLOSE2 = 0x1d, - HANDSHAKE_DONE = 0x1e, + PADDING = 0x00, + PING = 0x01, + ACK1 = 0x02, + ACK2 = 0x03, + RESET_STREAM = 0x04, + STOP_SENDING = 0x05, + CRYPTO = 0x06, + NEW_TOKEN = 0x07, + STREAM1 = 0x08, + STREAM2 = 0x09, + STREAM3 = 0x0a, + STREAM4 = 0x0b, + STREAM5 = 0x0c, + STREAM6 = 0x0d, + STREAM7 = 0x0e, + STREAM8 = 0x0f, + MAX_DATA = 0x10, + MAX_STREAM_DATA = 0x11, + MAX_STREAMS1 = 0x12, + MAX_STREAMS2 = 0x13, + DATA_BLOCKED = 0x14, + STREAM_DATA_BLOCKED = 0x15, + STREAMS_BLOCKED1 = 0x16, + STREAMS_BLOCKED2 = 0x17, + NEW_CONNECTION_ID = 0x18, + RETIRE_CONNECTION_ID = 0x19, + PATH_CHALLENGE = 0x1a, + PATH_RESPONSE = 0x1b, + CONNECTION_CLOSE1 = 0x1c, + CONNECTION_CLOSE2 = 0x1d, + HANDSHAKE_DONE = 0x1e, }; ############## @@ -220,22 +213,22 @@ type FrameType = enum { ############## type VariableLengthInteger = unit { - var bytes_to_parse: uint64; - var result_: uint64; + var bytes_to_parse: uint64; + var result_: uint64; - # Value of the two most significant bits indicates number of bytes - # to parse for the variable length integer. - # - # https://datatracker.ietf.org/doc/rfc9000/ - # Section 16 and Appendix A - : uint8 { - self.bytes_to_parse = 2**((0xC0 & $$) >> 6); - self.result_ = $$ & 0x3F; - } + # Value of the two most significant bits indicates number of bytes + # to parse for the variable length integer. + # + # https://datatracker.ietf.org/doc/rfc9000/ + # Section 16 and Appendix A + : uint8 { + self.bytes_to_parse = 2**((0xC0 & $$) >> 6); + self.result_ = $$ & 0x3F; + } - : uint8[self.bytes_to_parse - 1] if (self.bytes_to_parse > 1) foreach { - self.result_ = (self.result_ << 8) | $$; - } + : uint8[self.bytes_to_parse - 1] if(self.bytes_to_parse > 1) foreach { + self.result_ = (self.result_ << 8) | $$; + } }; ############## @@ -243,171 +236,166 @@ type VariableLengthInteger = unit { # Generic units ############## public type LongHeaderPacketV1 = unit(inout outer: LongHeaderPacket) { - switch ( LongPacketTypeV1(outer.first_byte.packet_type) ) { - LongPacketTypeV1::INITIAL -> initial_hdr : InitialPacket(outer) { - outer.is_initial = True; - outer.encrypted_offset = outer.offset() + - self.initial_hdr.length.bytes_to_parse + - self.initial_hdr.token_length.bytes_to_parse + - self.initial_hdr.token_length.result_; - outer.payload_length = self.initial_hdr.length.result_; - } - - LongPacketTypeV1::ZERO_RTT -> zerortt_hdr : ZeroRTTPacket(outer); - LongPacketTypeV1::HANDSHAKE -> handshake_hdr : HandshakePacket(outer); - LongPacketTypeV1::RETRY -> retry_hdr : RetryPacket(outer) { - outer.is_retry = True; - } - }; + switch (LongPacketTypeV1(outer.first_byte.packet_type)) { + LongPacketTypeV1::INITIAL -> initial_hdr: InitialPacket(outer) { + outer.is_initial = True; + outer.encrypted_offset = outer.offset() + self.initial_hdr.length.bytes_to_parse + self.initial_hdr.token_length.bytes_to_parse + self.initial_hdr.token_length.result_; + outer.payload_length = self.initial_hdr.length.result_; + } + LongPacketTypeV1::ZERO_RTT -> zerortt_hdr: ZeroRTTPacket(outer); + LongPacketTypeV1::HANDSHAKE -> handshake_hdr: HandshakePacket(outer); + LongPacketTypeV1::RETRY -> retry_hdr: RetryPacket(outer) { + outer.is_retry = True; + } + }; }; public type LongHeaderPacketV2 = unit(inout outer: LongHeaderPacket) { - switch ( LongPacketTypeV2(outer.first_byte.packet_type) ) { - LongPacketTypeV2::INITIAL -> initial_hdr : InitialPacket(outer) { - outer.is_initial = True; - outer.encrypted_offset = outer.offset() + - self.initial_hdr.length.bytes_to_parse + - self.initial_hdr.token_length.bytes_to_parse + - self.initial_hdr.token_length.result_; - outer.payload_length = self.initial_hdr.length.result_; - } - - LongPacketTypeV2::ZERO_RTT -> zerortt_hdr : ZeroRTTPacket(outer); - LongPacketTypeV2::HANDSHAKE -> handshake_hdr : HandshakePacket(outer); - LongPacketTypeV2::RETRY -> retry_hdr : RetryPacket(outer) { - outer.is_retry = True; - } - }; + switch (LongPacketTypeV2(outer.first_byte.packet_type)) { + LongPacketTypeV2::INITIAL -> initial_hdr: InitialPacket(outer) { + outer.is_initial = True; + outer.encrypted_offset = outer.offset() + self.initial_hdr.length.bytes_to_parse + self.initial_hdr.token_length.bytes_to_parse + self.initial_hdr.token_length.result_; + outer.payload_length = self.initial_hdr.length.result_; + } + LongPacketTypeV2::ZERO_RTT -> zerortt_hdr: ZeroRTTPacket(outer); + LongPacketTypeV2::HANDSHAKE -> handshake_hdr: HandshakePacket(outer); + LongPacketTypeV2::RETRY -> retry_hdr: RetryPacket(outer) { + outer.is_retry = True; + } + }; }; # Just eat the data for event raising. public type UnhandledVersion = unit(header: LongHeaderPacket) { - var header: LongHeaderPacket = header; - payload: skip bytes &eod; + var header: LongHeaderPacket = header; + payload: skip bytes &eod; }; public type LongHeaderPacket = unit { - var encrypted_offset: uint64; - var payload_length: uint64; - var client_conn_id_length: uint8; - var server_conn_id_length: uint8; - var is_initial: bool; - var is_retry: bool; + var encrypted_offset: uint64; + var payload_length: uint64; + var client_conn_id_length: uint8; + var server_conn_id_length: uint8; + var is_initial: bool; + var is_retry: bool; - first_byte: bitfield(8) { - header_form: 7 &convert=cast(cast($$)); - fixed_bit: 6; - packet_type: 4..5; - type_specific_bits: 0..3 &convert=cast($$); - }; + first_byte: bitfield(8) { + header_form: 7 &convert=cast(cast($$)); + fixed_bit: 6; + packet_type: 4..5; + type_specific_bits: 0..3 &convert=cast($$); + }; - version: uint32; - dest_conn_id_len: uint8 { self.server_conn_id_length = $$; } - dest_conn_id: bytes &size=self.server_conn_id_length; - src_conn_id_len: uint8 { self.client_conn_id_length = $$; } - src_conn_id: bytes &size=self.client_conn_id_length; - - switch ( self.version ) { - VersionDraft22, - VersionDraft23, - VersionDraft24, - VersionDraft25, - VersionDraft26, - VersionDraft27, - VersionDraft28, - VersionDraft29, - VersionDraft30, - VersionDraft31, - VersionDraft32, - VersionDraft33, - VersionDraft34, - VersionFace001, - VersionFace002, - VersionFace00e, - VersionFace011, - VersionFace012, - VersionFace013, - Version1 -> v1: LongHeaderPacketV1(self); - Version2 -> v2: LongHeaderPacketV2(self); - * -> unknown: UnhandledVersion(self) { - throw "unhandled QUIC version 0x%x" % self.version; + version: uint32; + dest_conn_id_len: uint8 { + self.server_conn_id_length = $$; } - }; + dest_conn_id: bytes &size=self.server_conn_id_length; + src_conn_id_len: uint8 { + self.client_conn_id_length = $$; + } + src_conn_id: bytes &size=self.client_conn_id_length; + + switch (self.version) { + VersionDraft22, + VersionDraft23, + VersionDraft24, + VersionDraft25, + VersionDraft26, + VersionDraft27, + VersionDraft28, + VersionDraft29, + VersionDraft30, + VersionDraft31, + VersionDraft32, + VersionDraft33, + VersionDraft34, + VersionFace001, + VersionFace002, + VersionFace00e, + VersionFace011, + VersionFace012, + VersionFace013, + Version1 -> v1: LongHeaderPacketV1(self); + Version2 -> v2: LongHeaderPacketV2(self); + * -> unknown: UnhandledVersion(self) { + throw "unhandled QUIC version 0x%x" % self.version; + } + }; }; # A QUIC Frame. public type Frame = unit(header: LongHeaderPacket, from_client: bool, crypto: CryptoSinkUnit, crypto_sink: sink&) { - frame_type : uint8 &convert=cast($$); + frame_type: uint8 &convert=cast($$); - # TODO: add other FrameTypes as well - switch ( self.frame_type ) { - FrameType::ACK1 -> a: ACKPayload(FrameType::ACK1); - FrameType::ACK2 -> b: ACKPayload(FrameType::ACK2); - FrameType::CRYPTO -> c: CRYPTOPayload(from_client) { - # Have the sink re-assemble potentially out-of-order cryptodata - crypto_sink.write(self.c.cryptodata, self.c.offset.result_); + # TODO: add other FrameTypes as well + switch (self.frame_type) { + FrameType::ACK1 -> a: ACKPayload(FrameType::ACK1); + FrameType::ACK2 -> b: ACKPayload(FrameType::ACK2); + FrameType::CRYPTO -> c: CRYPTOPayload(from_client) { + # Have the sink re-assemble potentially out-of-order cryptodata + crypto_sink.write(self.c.cryptodata, self.c.offset.result_); - # If the crypto unit has determined a valid length, ensure we - # don't attempt to write more bytes into the sink. If it doesn't, - # use 2000 bytes as an arbitrary limit required to observe the - # length of the contained Client Hello or Server Hello. - if ( crypto.length > 0 ) { - if ( |crypto_sink| > crypto.length ) - throw "too much crypto data received %s > %s" % ( |crypto_sink|, crypto.length); - } else { - if ( |crypto_sink| > 2000 ) - throw "too much crypto data without length received %s" % |crypto_sink|; - } - } - FrameType::CONNECTION_CLOSE1 -> : ConnectionClosePayload(header); - FrameType::PADDING -> : skip /\x00*/; # eat the padding - FrameType::PING -> : void; - * -> : void { - throw "unhandled frame type %s in %s" % (self.frame_type, header.first_byte.packet_type); - } - }; + # If the crypto unit has determined a valid length, ensure we + # don't attempt to write more bytes into the sink. If it doesn't, + # use 2000 bytes as an arbitrary limit required to observe the + # length of the contained Client Hello or Server Hello. + if (crypto.length > 0) { + if (|crypto_sink| > crypto.length) + throw "too much crypto data received %s > %s" % (|crypto_sink|, crypto.length); + } else { + if (|crypto_sink| > 2000) + throw "too much crypto data without length received %s" % |crypto_sink|; + } + } + FrameType::CONNECTION_CLOSE1 -> : ConnectionClosePayload(header); + FrameType::PADDING -> : skip /\x00*/; # eat the padding + FrameType::PING -> : void; + * -> : void { + throw "unhandled frame type %s in %s" % (self.frame_type, header.first_byte.packet_type); + } + }; }; type CRYPTOPayload = unit(from_client: bool) { - offset: VariableLengthInteger; - length: VariableLengthInteger; - cryptodata: bytes &size=self.length.result_; + offset: VariableLengthInteger; + length: VariableLengthInteger; + cryptodata: bytes &size=self.length.result_; }; # https://datatracker.ietf.org/doc/html/rfc9000#ack-ranges type ACKRange = unit { - gap: VariableLengthInteger; - ack_range_length: VariableLengthInteger; + gap: VariableLengthInteger; + ack_range_length: VariableLengthInteger; }; type ACKECNCounts = unit { - ect0: VariableLengthInteger; - ect1: VariableLengthInteger; - ecn_ce: VariableLengthInteger; + ect0: VariableLengthInteger; + ect1: VariableLengthInteger; + ecn_ce: VariableLengthInteger; }; # https://datatracker.ietf.org/doc/html/rfc9000#name-ack-frames type ACKPayload = unit(frame_type: FrameType) { - latest_ack: VariableLengthInteger; - ack_delay: VariableLengthInteger; - ack_range_count: VariableLengthInteger; - first_ack_range: VariableLengthInteger; - ack_ranges: ACKRange[self.ack_range_count.result_]; - ecn_counts: ACKECNCounts if(frame_type == FrameType::ACK2); + latest_ack: VariableLengthInteger; + ack_delay: VariableLengthInteger; + ack_range_count: VariableLengthInteger; + first_ack_range: VariableLengthInteger; + ack_ranges: ACKRange[self.ack_range_count.result_]; + ecn_counts: ACKECNCounts if(frame_type == FrameType::ACK2); }; type ConnectionClosePayload = unit(header: LongHeaderPacket) { - var header: LongHeaderPacket = header; - error_code: VariableLengthInteger; - switch { - -> unknown_frame_type: b"\x00"; - -> frame_type: VariableLengthInteger; - }; - reason_phrase_length: VariableLengthInteger; - reason_phrase: bytes &size=self.reason_phrase_length.result_; + var header: LongHeaderPacket = header; + error_code: VariableLengthInteger; + switch { + -> unknown_frame_type: b"\x00"; + -> frame_type: VariableLengthInteger; + }; + reason_phrase_length: VariableLengthInteger; + reason_phrase: bytes &size=self.reason_phrase_length.result_; }; - ############## # Long packets # Specific long packet type units @@ -415,53 +403,52 @@ type ConnectionClosePayload = unit(header: LongHeaderPacket) { # Remainder of an Initial packet type InitialPacket = unit(header: LongHeaderPacket) { - var header: LongHeaderPacket = header; - token_length: VariableLengthInteger; - token: bytes &size=self.token_length.result_; + var header: LongHeaderPacket = header; + token_length: VariableLengthInteger; + token: bytes &size=self.token_length.result_; - # 5.4.2. Header Protection Sample - # - # That is, in sampling packet ciphertext for header - # protection, the Packet Number field is assumed to - # be 4 bytes long (its maximum possible encoded length). - # - # Enforce 4 bytes Packet Number length + 16 bytes sample - # ciphertext available. - length: VariableLengthInteger &requires=self.length.result_ >= 20; + # 5.4.2. Header Protection Sample + # + # That is, in sampling packet ciphertext for header + # protection, the Packet Number field is assumed to + # be 4 bytes long (its maximum possible encoded length). + # + # Enforce 4 bytes Packet Number length + 16 bytes sample + # ciphertext available. + length: VariableLengthInteger &requires=self.length.result_ >= 20; - # Consume the remainder of payload. This - # includes the packet number field, but we - # do not know its length yet. We need the - # payload for sampling, however. - payload: skip bytes &size=self.length.result_; + # Consume the remainder of payload. This + # includes the packet number field, but we + # do not know its length yet. We need the + # payload for sampling, however. + payload: skip bytes &size=self.length.result_; }; type ZeroRTTPacket = unit(header: LongHeaderPacket) { - var header: LongHeaderPacket = header; - length: VariableLengthInteger; - payload: skip bytes &size=self.length.result_; + var header: LongHeaderPacket = header; + length: VariableLengthInteger; + payload: skip bytes &size=self.length.result_; }; type HandshakePacket = unit(header: LongHeaderPacket) { - var header: LongHeaderPacket = header; - length: VariableLengthInteger; - payload: skip bytes &size=self.length.result_; + var header: LongHeaderPacket = header; + length: VariableLengthInteger; + payload: skip bytes &size=self.length.result_; }; - type RetryPacket = unit(header: LongHeaderPacket) { - var header: LongHeaderPacket = header; - var retry_token: bytes; - var integrity_tag: bytes; + var header: LongHeaderPacket = header; + var retry_token: bytes; + var integrity_tag: bytes; - # A retry packet ends with a 128bit / 16 byte integrity - # tag, but otherwise we do not know anything about the - # size of the retry_token. Slurp the whole datagram and - # post split it into the distinct parts. - data: bytes &eod { - self.retry_token = self.data.sub(0, |self.data| - 16); - self.integrity_tag = self.data.sub(|self.data| - 16, |self.data|); - } + # A retry packet ends with a 128bit / 16 byte integrity + # tag, but otherwise we do not know anything about the + # size of the retry_token. Slurp the whole datagram and + # post split it into the distinct parts. + data: bytes &eod { + self.retry_token = self.data.sub(0, |self.data| - 16); + self.integrity_tag = self.data.sub(|self.data| - 16, |self.data|); + } }; ############## @@ -470,18 +457,18 @@ type RetryPacket = unit(header: LongHeaderPacket) { # TODO: implement public type ShortHeader = unit(dest_conn_id_length: uint8) { - first_byte: bitfield(8) { - header_form: 7 &convert=cast(cast($$)); - fixed_bit: 6; - spin_bit: 5; - todo: 0..4; + first_byte: bitfield(8) { + header_form: 7 &convert=cast(cast($$)); + fixed_bit: 6; + spin_bit: 5; + todo: 0..4; }; dest_conn_id: bytes &size=dest_conn_id_length; }; # TODO: investigate whether we can parse something useful out of this public type ShortPacketPayload = unit { - payload: skip bytes &eod; + payload: skip bytes &eod; }; ############## @@ -490,144 +477,143 @@ public type ShortPacketPayload = unit { # A UDP datagram contains one or more QUIC packets. ############## type Packet = unit(from_client: bool, context: Context&) { - var decrypted_data: bytes; - var packet_size: uint64 = 0; - var start: iterator; + var decrypted_data: bytes; + var packet_size: uint64 = 0; + var start: iterator; - var crypto: CryptoSinkUnit&; - var crypto_sink: sink&; + var crypto: CryptoSinkUnit&; + var crypto_sink: sink&; - # Attach an SSL analyzer to this connection once. - on %init { - if ( ! context?.ssl_handle ) { - context.ssl_handle = zeek::protocol_handle_get_or_create("SSL"); - } - - self.start = self.input(); - - # Initialize crypto state in context for both sides if not already done. - if ( context.client_crypto == Null ) { - assert ! context.server_crypto; - context.client_crypto = new CryptoSinkUnit(True, context); - context.client_sink = new sink; - context.client_sink.connect(context.client_crypto); - - context.server_crypto = new CryptoSinkUnit(False, context); - context.server_sink = new sink; - context.server_sink.connect(context.server_crypto); - } - - if ( from_client ) { - self.crypto = context.client_crypto; - self.crypto_sink = context.client_sink; - } else { - self.crypto = context.server_crypto; - self.crypto_sink = context.server_sink; - } - } - - # Peek into the first byte and determine the header type. - first_byte: bitfield(8) { - header_form: 7 &convert=HeaderForm($$); - }; - - # TODO: Consider bitfield based look-ahead-parsing in the switch below - # to avoid this rewinding here. It's a hack. - : void { - self.set_input(self.start); # rewind - } - - # Depending on the header, parse it and update the src/dest ConnectionID's - switch ( self.first_byte.header_form ) { - HeaderForm::SHORT -> short_header: ShortHeader(context.client_cid_len); - HeaderForm::LONG -> long_header: LongHeaderPacket { - # For now, only allow a change of src/dest ConnectionID's for INITIAL packets. - - # If we see a retry packet from the responder, reset the decryption - # context such that the next DCID from the client is used for decryption. - if ( self.long_header.is_retry ) { - reset_crypto(context); - - self.crypto = Null; - self.crypto_sink = Null; + # Attach an SSL analyzer to this connection once. + on %init { + if (!context?.ssl_handle) { + context.ssl_handle = zeek::protocol_handle_get_or_create("SSL"); } - } - }; - : void { - if ( self?.long_header && can_decrypt(self.long_header, context, self.crypto ) ) - # If we have parsed an initial packet that we can decrypt the payload, - # determine the size to store into a buffer. - self.packet_size = self.offset(); - } + self.start = self.input(); - # Buffer the whole packet if we determined we have a chance to decrypt. - packet_data: bytes &parse-at=self.start &size=self.packet_size if ( self.packet_size > 0 ) { + # Initialize crypto state in context for both sides if not already done. + if (context.client_crypto == Null) { + assert !context.server_crypto; + context.client_crypto = new CryptoSinkUnit(True, context); + context.client_sink = new sink; + context.client_sink.connect(context.client_crypto); - if ( from_client ) { - context.server_cid_len = self.long_header.dest_conn_id_len; - context.client_cid_len = self.long_header.src_conn_id_len; + context.server_crypto = new CryptoSinkUnit(False, context); + context.server_sink = new sink; + context.server_sink.connect(context.server_crypto); + } - # This is the first INITIAL packet we attempt to decrypt and it is - # coming from the client. Use its destination connection ID for - # decryption purposes. - if ( ! context.initial_destination_conn_id ) { - context.initial_destination_conn_id = self.long_header.dest_conn_id; - } - - # This means that here, we can try to decrypt the initial packet! - # All data is accessible via the `long_header` unit - self.decrypted_data = decrypt_crypto_payload( - self.long_header.version, - self.packet_data, - *context.initial_destination_conn_id, - self.long_header.encrypted_offset, - self.long_header.payload_length, - from_client - ); - - } else { - context.server_cid_len = self.long_header.src_conn_id_len; - context.client_cid_len = self.long_header.dest_conn_id_len; - - self.decrypted_data = decrypt_crypto_payload( - self.long_header.version, - self.packet_data, - *context.initial_destination_conn_id, - self.long_header.encrypted_offset, - self.long_header.payload_length, - from_client - ); + if (from_client) { + self.crypto = context.client_crypto; + self.crypto_sink = context.client_sink; + } else { + self.crypto = context.server_crypto; + self.crypto_sink = context.server_sink; + } } - # We attempted decryption, but it failed. Just reject the - # input and assume Zeek will disable the analyzer for this - # connection. - if ( |self.decrypted_data| == 0 ) - throw "decryption failed"; + # Peek into the first byte and determine the header type. + first_byte: bitfield(8) { + header_form: 7 &convert=HeaderForm($$); + }; - # We were able to decrypt the INITIAL packet. Confirm QUIC! - spicy::accept_input(); - } + # TODO: Consider bitfield based look-ahead-parsing in the switch below + # to avoid this rewinding here. It's a hack. + : void { + self.set_input(self.start); # rewind + } - # If this packet has a SHORT header, consume until &eod, there's nothing - # we can do with it anyhow. - : ShortPacketPayload if (self.first_byte.header_form == HeaderForm::SHORT); + # Depending on the header, parse it and update the src/dest ConnectionID's + switch (self.first_byte.header_form) { + HeaderForm::SHORT -> short_header: ShortHeader(context.client_cid_len); + HeaderForm::LONG -> long_header: LongHeaderPacket { + # For now, only allow a change of src/dest ConnectionID's for INITIAL packets. - # If this was packet with a long header and decrypted data exists, attempt - # to parse the plain QUIC frames from it. - frames: Frame(self.long_header, from_client, self.crypto, self.crypto_sink)[] &parse-from=self.decrypted_data if (self.first_byte.header_form == HeaderForm::LONG && |self.decrypted_data| > 0); + # If we see a retry packet from the responder, reset the decryption + # context such that the next DCID from the client is used for decryption. + if (self.long_header.is_retry) { + reset_crypto(context); + + self.crypto = Null; + self.crypto_sink = Null; + } + } + }; + + : void { + if (self?.long_header && can_decrypt(self.long_header, context, self.crypto)) + # If we have parsed an initial packet that we can decrypt the payload, + # determine the size to store into a buffer. + self.packet_size = self.offset(); + } + + # Buffer the whole packet if we determined we have a chance to decrypt. + packet_data: bytes &parse-at=self.start &size=self.packet_size if(self.packet_size > 0) { + + if (from_client) { + context.server_cid_len = self.long_header.dest_conn_id_len; + context.client_cid_len = self.long_header.src_conn_id_len; + + # This is the first INITIAL packet we attempt to decrypt and it is + # coming from the client. Use its destination connection ID for + # decryption purposes. + if (!context.initial_destination_conn_id) { + context.initial_destination_conn_id = self.long_header.dest_conn_id; + } + + # This means that here, we can try to decrypt the initial packet! + # All data is accessible via the `long_header` unit + self.decrypted_data = decrypt_crypto_payload( + self.long_header.version, + self.packet_data, + *context.initial_destination_conn_id, + self.long_header.encrypted_offset, + self.long_header.payload_length, + from_client + ); + } else { + context.server_cid_len = self.long_header.src_conn_id_len; + context.client_cid_len = self.long_header.dest_conn_id_len; + + self.decrypted_data = decrypt_crypto_payload( + self.long_header.version, + self.packet_data, + *context.initial_destination_conn_id, + self.long_header.encrypted_offset, + self.long_header.payload_length, + from_client + ); + } + + # We attempted decryption, but it failed. Just reject the + # input and assume Zeek will disable the analyzer for this + # connection. + if (|self.decrypted_data| == 0) + throw "decryption failed"; + + # We were able to decrypt the INITIAL packet. Confirm QUIC! + spicy::accept_input(); + } + + # If this packet has a SHORT header, consume until &eod, there's nothing + # we can do with it anyhow. + : ShortPacketPayload if(self.first_byte.header_form == HeaderForm::SHORT); + + # If this was packet with a long header and decrypted data exists, attempt + # to parse the plain QUIC frames from it. + frames: Frame(self.long_header, from_client, self.crypto, self.crypto_sink)[] &parse-from=self.decrypted_data if(self.first_byte.header_form == HeaderForm::LONG && |self.decrypted_data| > 0); }; ############## # Entrypoints ############## public type RequestFrame = unit { - %context = Context; - : Packet(True, self.context())[]; + %context = Context; + : Packet(True, self.context())[]; }; public type ResponseFrame = unit { - %context = Context; - : Packet(False, self.context())[]; + %context = Context; + : Packet(False, self.context())[]; }; diff --git a/src/analyzer/protocol/websocket/websocket.spicy b/src/analyzer/protocol/websocket/websocket.spicy index c39aedbefe..8b2b9cf22b 100644 --- a/src/analyzer/protocol/websocket/websocket.spicy +++ b/src/analyzer/protocol/websocket/websocket.spicy @@ -6,114 +6,109 @@ import spicy; import zeek; const OPCODE_CONTINUATION = 0x00; -const OPCODE_TEXT = 0x01; -const OPCODE_BINARY = 0x02; -const OPCODE_CLOSE = 0x08; -const OPCODE_PING = 0x09; -const OPCODE_PONG = 0x0a; +const OPCODE_TEXT = 0x01; +const OPCODE_BINARY = 0x02; +const OPCODE_CLOSE = 0x08; +const OPCODE_PING = 0x09; +const OPCODE_PONG = 0x0a; -public function fast_unmask( - masking_key_idx: uint64, - masking_key: vector, - chunk: bytes -): bytes &cxxname="hlt_websocket::WebSocket::fast_unmask"; +public function fast_unmask(masking_key_idx: uint64, masking_key: vector, chunk: bytes): bytes &cxxname="hlt_websocket::WebSocket::fast_unmask"; type Frame = unit(m: Message) { - var payload_len: uint64; - var masking_key_idx: uint64; - var close_data: bytes; - var effective_opcode: uint8; + var payload_len: uint64; + var masking_key_idx: uint64; + var close_data: bytes; + var effective_opcode: uint8; - : bitfield(16) { - fin: 0 &convert=cast($$); - rsv: 1..3; - opcode: 4..7 &convert=cast($$); - mask: 8 &convert=cast($$); - payload_len1: 9..15; - } &bit-order=spicy::BitOrder::MSB0; + : bitfield(16) { + fin: 0 &convert=cast($$); + rsv: 1..3; + opcode: 4..7 &convert=cast($$); + mask: 8 &convert=cast($$); + payload_len1: 9..15; + } &bit-order=spicy::BitOrder::MSB0; - # Verify that this is either a continuation frame, or the Message does not - # yet have a non-continuation opcode, but this frame does. - : void &requires=(m.opcode != OPCODE_CONTINUATION && self.opcode == OPCODE_CONTINUATION || m.opcode == OPCODE_CONTINUATION && self.opcode != OPCODE_CONTINUATION); + # Verify that this is either a continuation frame, or the Message does not + # yet have a non-continuation opcode, but this frame does. + : void &requires=(m.opcode != OPCODE_CONTINUATION && self.opcode == OPCODE_CONTINUATION || m.opcode == OPCODE_CONTINUATION && self.opcode != OPCODE_CONTINUATION); - # Type/opcode to decide what to do with individual chunks. - : void { - self.effective_opcode = m.opcode != OPCODE_CONTINUATION ? m.opcode : self.opcode; - } - - payload_len2: uint16 if (self.payload_len1 == 126); - payload_len8: uint64 if (self.payload_len1 == 127); - - : void { - self.payload_len = self.payload_len1; - if ( self?.payload_len2 ) - self.payload_len = self.payload_len2; - else if ( self?.payload_len8 ) - self.payload_len = self.payload_len8; - } - - # This being an uint8[] allows masking_key[x] indexing, while a bytes - # object would require *masking_key.at(i) which took roughly 20% more - # runtime when I tested it. - masking_key: uint8[] &size=4 if (self.mask); - - chunk: bytes &size=self.payload_len &chunked { - # Don't use &convert with &chunked: https://github.com/zeek/spicy/issues/1661 - if ( self.mask ) { - self.chunk = fast_unmask(self.masking_key_idx, self.masking_key, $$); - self.masking_key_idx += |$$|; - } else { - self.chunk = $$; + # Type/opcode to decide what to do with individual chunks. + : void { + self.effective_opcode = m.opcode != OPCODE_CONTINUATION ? m.opcode : self.opcode; } - # Forward TEXT and BINARY data to dowstream analyzers. - if ( self.effective_opcode == OPCODE_TEXT || self.effective_opcode == OPCODE_BINARY ) - zeek::protocol_data_in(zeek::is_orig(), $$); + payload_len2: uint16 if(self.payload_len1 == 126); + payload_len8: uint64 if(self.payload_len1 == 127); - # Accumulate the unmasked data in close_data if this a close frame - # so it can be parsed by the outer Message. It's a bit of a hack. - if ( self.effective_opcode == OPCODE_CLOSE ) - self.close_data += $$; - } + : void { + self.payload_len = self.payload_len1; + if (self?.payload_len2) + self.payload_len = self.payload_len2; + else if (self?.payload_len8) + self.payload_len = self.payload_len8; + } + + # This being an uint8[] allows masking_key[x] indexing, while a bytes + # object would require *masking_key.at(i) which took roughly 20% more + # runtime when I tested it. + masking_key: uint8[] &size=4 if(self.mask); + + chunk: bytes &size=self.payload_len &chunked { + # Don't use &convert with &chunked: https://github.com/zeek/spicy/issues/1661 + if (self.mask) { + self.chunk = fast_unmask(self.masking_key_idx, self.masking_key, $$); + self.masking_key_idx += |$$|; + } else { + self.chunk = $$; + } + + # Forward TEXT and BINARY data to dowstream analyzers. + if (self.effective_opcode == OPCODE_TEXT || self.effective_opcode == OPCODE_BINARY) + zeek::protocol_data_in(zeek::is_orig(), $$); + + # Accumulate the unmasked data in close_data if this a close frame + # so it can be parsed by the outer Message. It's a bit of a hack. + if (self.effective_opcode == OPCODE_CLOSE) + self.close_data += $$; + } }; type CloseFrame = unit { - var status: uint16; - var reason: bytes; + var status: uint16; + var reason: bytes; - : bytes &eod { - if ( |$$| > 0 ) { - self.status = cast($$.sub(0, 2).to_uint(spicy::ByteOrder::Network)); - self.reason = $$.sub(2, 0); + : bytes &eod { + if (|$$| > 0) { + self.status = cast($$.sub(0, 2).to_uint(spicy::ByteOrder::Network)); + self.reason = $$.sub(2, 0); + } } - } }; - public type Message = unit { - # transient trickery - var done: bool = False; + # transient trickery + var done: bool = False; - var opcode: uint8; + var opcode: uint8; - # Keep the first one persistent to have access - # to the payload if it's a close frame. - first_frame: Frame(self) { - self.opcode = $$.opcode; - self.done = $$.fin; - } + # Keep the first one persistent to have access + # to the payload if it's a close frame. + first_frame: Frame(self) { + self.opcode = $$.opcode; + self.done = $$.fin; + } - : Frame(self)[] &until=(self.done) if (!self.done) foreach { - self.done = $$.fin; - } + : Frame(self)[] &until=(self.done) if(!self.done) foreach { + self.done = $$.fin; + } - : CloseFrame &parse-from=self.first_frame.close_data if (self.opcode == OPCODE_CLOSE); + : CloseFrame &parse-from=self.first_frame.close_data if(self.opcode == OPCODE_CLOSE); - on %done { - spicy::accept_input(); - } + on %done { + spicy::accept_input(); + } }; public type Messages = unit { - : Message[]; + : Message[]; };