diff --git a/scripts/base/protocols/quic/__load__.zeek b/scripts/base/protocols/quic/__load__.zeek new file mode 100644 index 0000000000..a10fe855df --- /dev/null +++ b/scripts/base/protocols/quic/__load__.zeek @@ -0,0 +1 @@ +@load ./main diff --git a/scripts/base/protocols/quic/main.zeek b/scripts/base/protocols/quic/main.zeek new file mode 100644 index 0000000000..0d54e52618 --- /dev/null +++ b/scripts/base/protocols/quic/main.zeek @@ -0,0 +1,2 @@ +module QUIC; + diff --git a/src/analyzer/protocol/quic/CMakeLists.txt b/src/analyzer/protocol/quic/CMakeLists.txt new file mode 100644 index 0000000000..4d9e0f6964 --- /dev/null +++ b/src/analyzer/protocol/quic/CMakeLists.txt @@ -0,0 +1,5 @@ +spicy_add_analyzer( + NAME QUIC + PACKAGE_NAME QUIC + SOURCES decrypt_crypto.cc QUIC.spicy QUIC.evt zeek_QUIC.spicy + SCRIPTS __load__.zeek main.zeek) diff --git a/src/analyzer/protocol/quic/QUIC.evt b/src/analyzer/protocol/quic/QUIC.evt new file mode 100644 index 0000000000..7a1650bf31 --- /dev/null +++ b/src/analyzer/protocol/quic/QUIC.evt @@ -0,0 +1,10 @@ +protocol analyzer spicy::QUIC over UDP: + parse originator with QUIC::RequestFrame, + parse responder with QUIC::ResponseFrame, + ports { 443/udp }; + +import QUIC; +import Zeek_QUIC; + +# TODO: Add actual events, instead of this dummy event +on QUIC::ResponseFrame -> event QUIC::example($conn); diff --git a/src/analyzer/protocol/quic/QUIC.spicy b/src/analyzer/protocol/quic/QUIC.spicy new file mode 100644 index 0000000000..7781a3875c --- /dev/null +++ b/src/analyzer/protocol/quic/QUIC.spicy @@ -0,0 +1,411 @@ +module QUIC; + +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(entire_packet: bytes, connection_id: bytes, encrypted_offset: uint64, payload_offset: uint64, from_client: bool): bytes &cxxname="decrypt_crypto_payload"; + + +############## +## Context - tracked in one connection +############## + +type ConnectionIDInfo = unit { + var client_cid_len: uint8; + var server_cid_len: uint8; + var initial_destination_conn_id: bytes; + var initial_packets_exchanged: bool; + var initialized: bool; + + on %init { + self.client_cid_len = 0; + self.server_cid_len = 0; + self.initial_packets_exchanged = False; + self.initialized = False; + } +}; + +############## +# Definitions +############## + +type LongPacketType = enum { + INITIAL = 0, + ZERO_RTT = 1, + HANDSHAKE = 2, + RETRY = 3, +}; + +type HeaderForm = enum { + 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, +}; + +############## +# Helper units +############## + +# Used to peek into the next byte and determine if it's a long or short packet +public type InitialByte = unit { + initialbyte: bitfield(8) { + header_form: 7 &convert=cast(cast($$)); + }; + on %done{ + self.backtrack(); + } +}; + +# Used to peek into the next byte and check it's value +type InitialUint8 = unit { + var bt: uint8; + : uint8 { + self.bt = $$; + } + + on %done{ + self.backtrack(); + } +}; + +# https://datatracker.ietf.org/doc/rfc9000/ +# Section 16 and Appendix A +type VariableLengthIntegerLength = unit { + var length: uint8; + + a: bitfield(8) { + length: 6..7 &convert=cast($$) &byte-order=spicy::ByteOrder::Big; + }; + + on %done { + self.length = self.a.length; + self.backtrack(); + } +}; + +type VariableLengthInteger = unit { + var bytes_to_parse: uint64; + var result: uint64; + var result_bytes: bytes; + + : VariableLengthIntegerLength &try { + switch ( $$.length ) { + case 0: + self.bytes_to_parse = 1; + case 1: + self.bytes_to_parse = 2; + case 2: + self.bytes_to_parse = 4; + case 3: + self.bytes_to_parse = 8; + } + } + + # Parse the required amount of bytes and apply a mask to clear the + # first two bits, leaving the actual length + remainder: bytes &size=self.bytes_to_parse { + switch ( self.bytes_to_parse ) { + case 1: + self.result = $$.to_uint(spicy::ByteOrder::Big) & 0x3f; + case 2: + self.result = $$.to_uint(spicy::ByteOrder::Big) & 0x3fff; + case 4: + self.result = $$.to_uint(spicy::ByteOrder::Big) & 0x3fffffff; + case 8: + self.result = $$.to_uint(spicy::ByteOrder::Big) & 0x3fffffffffffffff; + } + } +}; + +############## +# Long packets +# Generic units +############## + +# Used to capture all data form the entire frame. May be inefficient, but works for now. +# This is passed to the decryption function, as this function needs both the header and the payload +# Performs a backtrack() at the end +type AllData = unit { + var data: bytes; + + : bytes &eod { + self.data = $$; + } + + on %done { + self.backtrack(); + } +}; + +public type LongHeader = unit { + var encrypted_offset: uint64; + var payload_length: uint64; + var client_conn_id_length: uint8; + var server_conn_id_length: uint8; + + first_byte: bitfield(8) { + header_form: 7 &convert=cast(cast($$)); + fixed_bit: 6; + packet_type: 4..5 &convert=cast(cast($$)); + 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; + + # We pass the type specific 4 bits too and don't parse them again + switch ( self.first_byte.packet_type ) { + LongPacketType::INITIAL -> initial_hdr : InitialLongPacketHeader(self.first_byte.type_specific_bits) { + self.encrypted_offset = self.offset() + + self.initial_hdr.payload_length.bytes_to_parse + + self.initial_hdr.token_length.bytes_to_parse + + self.initial_hdr.token_length.result; + self.payload_length = self.initial_hdr.payload_length.result; + } + LongPacketType::ZERO_RTT -> zerortt_hdr : ZeroRTTLongPacketHeader(self.first_byte.type_specific_bits); + LongPacketType::HANDSHAKE -> handshake_hdr : HandshakeLongPacketHeader(self.first_byte.type_specific_bits); + LongPacketType::RETRY -> retry_hdr : RetryLongPacketHeader(self.first_byte.type_specific_bits); + }; +}; + +# Decrypted long packet payload that can actually be parsed +public type DecryptedLongPacketPayload = unit(packet_type: LongPacketType, from_client: bool) { + frame_type : uint8 &convert=cast($$); + + # TODO: add other FrameTypes as well + switch ( self.frame_type ) { + FrameType::ACK1 -> a: ACKPayload; + FrameType::ACK2 -> b: ACKPayload; + FrameType::CRYPTO -> c: CRYPTOPayload(from_client); + FrameType::PADDING -> d: PADDINGPayload; + }; +}; + +# TODO: investigate whether we can do something useful with this +public type EncryptedLongPacketPayload = unit { + payload: bytes &eod; +}; + +# Determines how to parse the long packet payload, depending on whether is was decrypted or not +public type LongPacketPayload = unit(packet_type: LongPacketType, from_client: bool, encrypted: bool) { + : DecryptedLongPacketPayload(packet_type, from_client) if (encrypted == False); + : EncryptedLongPacketPayload if (encrypted == True); +}; + +type CRYPTOPayload = unit(from_client: bool) { + var length_in_byte1: bytes; + var length_in_byte2: bytes; + + offset: uint8; + length: VariableLengthInteger; + cryptodata: bytes &size=self.length.result; + + on %done { + # As of 5 Sept. 2022 there is no function to convert a unsigned integer back to bytes. + # Therefore, the following (quite dirty) method is used. Should be fixed/improved whenever + # a better alternative is available. + # It converts a uint16 to its two-byte representation. + self.length_in_byte1 = ("%c" % cast((self.length.result >> 8) & 0xff)).encode(); + self.length_in_byte2 = ("%c" % cast(self.length.result & 0xff)).encode(); + + # The data is passed to the SSL analyzer as part of a HANDSHAKE (0x16) message with TLS1.3 (\x03\x03). + # The 2 length bytes are also passed, followed by the actual CRYPTO blob which contains a CLIENT HELLO or SERVER HELLO + zeek::protocol_data_in(from_client, b"\x16\x03\x03" + self.length_in_byte1 + self.length_in_byte2 + self.cryptodata); + } +}; + +type ACKPayload = unit { + latest_ack: uint8; + ack_delay: uint8; + ack_range_count: uint8; + first_ack_range: uint8; +}; + +public type NullBytes = unit { + : (b"\x00")[]; + x: InitialUint8 &try; +}; + +type PADDINGPayload = unit { + var padding_length: uint64 = 0; + + # Simply consume all next nullbytes + : NullBytes; +}; + + +############## +# Long packets +# Specific long packet type units +############## + +type InitialLongPacketHeader = unit(type_specific_bits: uint8) { + var packet_number_length_full: uint8; + + token_length: VariableLengthInteger; + token: bytes &size=self.token_length.result; + payload_length: VariableLengthInteger; + packet_number: bytes &size=self.packet_number_length_full &convert=$$.to_uint(spicy::ByteOrder::Big); + + on %init { + # Calculate the packet number length while the initial byte is still encoded. + # Will result in 0, 1, 2 or 3. So we need to read n+1 bytes to properly parse the header. + self.packet_number_length_full = (type_specific_bits & 0x03) + 1; + } +}; + +# TODO: implement +type ZeroRTTLongPacketHeader = unit(type_specific_bits: uint8) {}; +type HandshakeLongPacketHeader = unit(type_specific_bits: uint8) {}; +type RetryLongPacketHeader = unit(type_specific_bits: uint8) {}; + +############## +# Short packets +############## + +# 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; + }; + 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: bytes &eod; +}; + +############## +# QUIC frame parsing +############## +type Frame = unit(from_client: bool, context: ConnectionIDInfo&) { + var hdr_form: HeaderForm; + var decrypted_data: bytes; + var full_packet: bytes; + + # Peek into the header to check if it's a SHORT or LONG header + : InitialByte &try { + self.hdr_form = $$.initialbyte.header_form; + } + + # Capture all the packet bytes if we're still have a chance of decrypting the INITIAL PACKETS + fpack: AllData &try if (context.initial_packets_exchanged == False); + + # Depending on the header, parse it and update the src/dest ConnectionID's + switch ( self.hdr_form ) { + HeaderForm::SHORT -> short_header: ShortHeader(context.client_cid_len); + HeaderForm::LONG -> long_header: LongHeader { + # For now, only allow a change of src/dest ConnectionID's for INITIAL packets. + # TODO: allow this for Retry packets + + if ( self.long_header.first_byte.packet_type == LongPacketType::INITIAL + && context.initial_packets_exchanged == False ) { + + 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 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.fpack.data, + self.long_header.dest_conn_id, + self.long_header.encrypted_offset, + self.long_header.payload_length, + from_client); + + # Set this to be the seed for the decryption + if ( ! context.initial_packets_exchanged ) { + context.initial_destination_conn_id = self.long_header.dest_conn_id; + } + + } else { + context.server_cid_len = self.long_header.src_conn_id_len; + context.client_cid_len = self.long_header.dest_conn_id_len; + + # Assuming that the client set up the connection, this can be considered the first + # received Initial from the client. So disable change of ConnectionID's afterwards + self.decrypted_data = decrypt_crypto_payload(self.fpack.data, + context.initial_destination_conn_id, + self.long_header.encrypted_offset, + self.long_header.payload_length, + from_client); + } + } + + # If it's a reply from the server and it's not a REPLY, we assume the keys are restablished and decryption is no longer possible + # TODO: verify if this is actually correct per RFC + if (self.long_header.first_byte.packet_type != LongPacketType::RETRY && ! from_client) { + context.initial_packets_exchanged = True; + } + } + }; + + # Depending on the type of header, we parse the remaining payload. + switch ( self.hdr_form ) { + HeaderForm::SHORT -> remaining_short_payload: ShortPacketPayload; + HeaderForm::LONG -> remaining_long_payload : LongPacketPayload(self.long_header.first_byte.packet_type, from_client, context.initial_packets_exchanged)[] &parse-from=self.decrypted_data; + }; + + on %init { + # Make sure to only attach the SSL analyzer once per QUIC connection + if ( ! context.initialized ) { + context.initialized = True; + zeek::protocol_begin("SSL"); + } + } +}; + +############## +# Entrypoints +############## +public type RequestFrame = unit { + %context = ConnectionIDInfo; + : Frame(True, self.context()); +}; + +public type ResponseFrame = unit { + %context = ConnectionIDInfo; + : Frame(False, self.context()); +}; diff --git a/src/analyzer/protocol/quic/decrypt_crypto.cc b/src/analyzer/protocol/quic/decrypt_crypto.cc new file mode 100644 index 0000000000..94282dc67d --- /dev/null +++ b/src/analyzer/protocol/quic/decrypt_crypto.cc @@ -0,0 +1,380 @@ +// See the file "COPYING" in the main distribution directory for copyright. + +/* +WORK-IN-PROGRESS +Initial working version of decrypting the INITIAL packets from +both client & server to be used by the Spicy parser. Might need a few more +refactors as C++ development is not our main profession. +*/ + +// Default imports +#include +#include +#include +#include +#include + +// OpenSSL imports +#include +#include +#include + +// Import HILTI +#include + +// Struct to store decryption info for this specific connection +struct DecryptionInformation +{ + std::vector unprotected_header; + std::vector protected_header; + uint64_t packet_number; + std::vector nonce; + uint8_t packet_number_length; +}; + +/* +Constants used in the HKDF functions. HKDF-Expand-Label uses labels +such as 'quic key' and 'quic hp'. These labels can obviously be +calculated dynamically, but are incluced statically for now, as the +goal of this analyser is only to analyze the INITIAL packets. +*/ + +std::vector INITIAL_SALT_V1 = { + 0x38, 0x76, 0x2c, 0xf7, 0xf5, + 0x59, 0x34, 0xb3, 0x4d, 0x17, + 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, + 0xad, 0xcc, 0xbb, 0x7f, 0x0a}; + +std::vector CLIENT_INITIAL_INFO = { + 0x00, 0x20, 0x0f, 0x74, 0x6c, + 0x73, 0x31, 0x33, 0x20, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x20, 0x69, 0x6e, 0x00}; + +std::vector SERVER_INITIAL_INFO = { + 0x00, 0x20, 0x0f, 0x74, 0x6c, + 0x73, 0x31, 0x33, 0x20, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, + 0x20, 0x69, 0x6e, 0x00}; + +std::vector KEY_INFO = { + 0x00, 0x10, 0x0e, 0x74, 0x6c, + 0x73, 0x31, 0x33, 0x20, 0x71, + 0x75, 0x69, 0x63, 0x20, 0x6b, + 0x65, 0x79, 0x00}; + +std::vector IV_INFO = { + 0x00, 0x0c, 0x0d, 0x74, 0x6c, + 0x73, 0x31, 0x33, 0x20, 0x71, + 0x75, 0x69, 0x63, 0x20, 0x69, + 0x76, 0x00}; + +std::vector HP_INFO = { + 0x00, 0x10, 0x0d, 0x74, 0x6c, + 0x73, 0x31, 0x33, 0x20, 0x71, + 0x75, 0x69, 0x63, 0x20, 0x68, + 0x70, 0x00}; + +/* +Constants used by the different functions +*/ +const size_t INITIAL_SECRET_LEN = 32; +const size_t AEAD_KEY_LEN = 16; +const size_t AEAD_IV_LEN = 12; +const size_t AEAD_HP_LEN = 16; +const size_t AEAD_SAMPLE_LENGTH = 16; +const size_t AEAD_TAG_LENGTH = 16; +const size_t MAXIMUM_PACKET_LENGTH = 1500; +const size_t MAXIMUM_PACKET_NUMBER_LENGTH = 4; + +/* +HKDF-Extract as decribed in https://www.rfc-editor.org/rfc/rfc8446.html#section-7.1 +*/ +std::vector hkdf_extract(std::vector connection_id) +{ + std::vector out_temp(INITIAL_SECRET_LEN); + size_t initial_secret_len = out_temp.size(); + const EVP_MD *digest = EVP_sha256(); + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL); + EVP_PKEY_derive_init(pctx); + EVP_PKEY_CTX_hkdf_mode(pctx, EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY); + EVP_PKEY_CTX_set_hkdf_md(pctx, digest); + EVP_PKEY_CTX_set1_hkdf_key(pctx, + connection_id.data(), + connection_id.size()); + EVP_PKEY_CTX_set1_hkdf_salt(pctx, + INITIAL_SALT_V1.data(), + INITIAL_SALT_V1.size()); + EVP_PKEY_derive(pctx, + out_temp.data(), + reinterpret_cast(&initial_secret_len)); + EVP_PKEY_CTX_free(pctx); + return out_temp; +} + +/* +HKDF-Expand-Label as decribed in https://www.rfc-editor.org/rfc/rfc8446.html#section-7.1 +that uses the global constant labels such as 'quic hp'. +*/ +std::vector hkdf_expand(size_t out_len, + std::vector key, + std::vector info) +{ + std::vector out_temp(out_len); + const EVP_MD *digest = EVP_sha256(); + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL); + EVP_PKEY_derive_init(pctx); + EVP_PKEY_CTX_hkdf_mode(pctx, EVP_PKEY_HKDEF_MODE_EXPAND_ONLY); + EVP_PKEY_CTX_set_hkdf_md(pctx, digest); + EVP_PKEY_CTX_set1_hkdf_key(pctx, key.data(), key.size()); + EVP_PKEY_CTX_add1_hkdf_info(pctx, info.data(), info.size()); + EVP_PKEY_derive(pctx, out_temp.data(), &out_len); + EVP_PKEY_CTX_free(pctx); + return out_temp; +} + +/* +Removes the header protection from the INITIAL packet and returns a DecryptionInformation struct that is partially filled +*/ +DecryptionInformation remove_header_protection(std::vector client_hp, uint8_t encrypted_offset, std::vector encrypted_packet) +{ + DecryptionInformation decryptInfo; + int outlen; + auto cipher = EVP_aes_128_ecb(); + auto ctx = EVP_CIPHER_CTX_new(); + EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, 1); + EVP_CIPHER_CTX_set_key_length(ctx, client_hp.size()); + // Passing an 1 means ENCRYPT + EVP_CipherInit_ex(ctx, NULL, NULL, client_hp.data(), NULL, 1); + + std::vector sample(encrypted_packet.begin() + + encrypted_offset + + MAXIMUM_PACKET_NUMBER_LENGTH, + + encrypted_packet.begin() + + encrypted_offset + + MAXIMUM_PACKET_NUMBER_LENGTH + + AEAD_SAMPLE_LENGTH); + std::vector mask(sample.size()); + EVP_CipherUpdate(ctx, mask.data(), &outlen, sample.data(), AEAD_SAMPLE_LENGTH); + + // To determine the actual packet number length, + // we have to remove the mask from the first byte + uint8_t first_byte = encrypted_packet[0]; + + if (first_byte & 0x80) + { + first_byte ^= mask[0] & 0x0F; + } + else + { + first_byte ^= first_byte & 0x1F; + } + + // And now we can fully recover the correct packet number length... + int recovered_packet_number_length = (first_byte & 0x03) + 1; + + // .. and use this to reconstruct the (partially) unprotected header + std::vector unprotected_header( + encrypted_packet.begin(), + + encrypted_packet.begin() + + encrypted_offset + + recovered_packet_number_length); + + uint32_t decoded_packet_number = 0; + + unprotected_header[0] = first_byte; + for (int i = 0; i < recovered_packet_number_length; ++i) + { + unprotected_header[encrypted_offset + i] ^= mask[1 + i]; + decoded_packet_number = + unprotected_header[encrypted_offset + i] | + (decoded_packet_number << 8); + } + std::vector protected_header(encrypted_packet.begin(), + encrypted_packet.begin() + + encrypted_offset + + recovered_packet_number_length); + + // Store the information back in the struct + decryptInfo.packet_number = decoded_packet_number; + decryptInfo.packet_number_length = recovered_packet_number_length; + decryptInfo.protected_header = protected_header; + decryptInfo.unprotected_header = unprotected_header; + return decryptInfo; +} + +/* +Calculate the nonce for the AEAD by XOR'ing the CLIENT_IV and the +decoded packet number, and returns the nonce +*/ +std::vector calculate_nonce(std::vector client_iv, uint64_t packet_number) +{ + std::vector nonce = client_iv; + + for (int i = 0; i < 8; ++i) + { + nonce[AEAD_IV_LEN - 1 - i] ^= + (uint8_t)(packet_number >> 8 * i); + } + + // Return the nonce + return nonce; +} + +/* +Function that calls the AEAD decryption routine, and returns the +decrypted data +*/ +std::vector decrypt(std::vector client_key, + std::vector encrypted_packet, + uint64_t payload_offset, + DecryptionInformation decryptInfo) +{ + int out, out2, res; + std::vector encrypted_payload( + encrypted_packet.begin() + + decryptInfo.protected_header.size(), + + encrypted_packet.begin() + + decryptInfo.protected_header.size() + + payload_offset - + decryptInfo.packet_number_length - + AEAD_TAG_LENGTH); + + std::vector tag_to_check( + encrypted_packet.begin() + + decryptInfo.protected_header.size() + + payload_offset - + decryptInfo.packet_number_length - + AEAD_TAG_LENGTH, + + encrypted_packet.begin() + + decryptInfo.protected_header.size() + + payload_offset - + decryptInfo.packet_number_length); + + unsigned char decrypt_buffer[MAXIMUM_PACKET_LENGTH]; + + // Setup context + auto cipher = EVP_aes_128_gcm(); + auto ctx = EVP_CIPHER_CTX_new(); + + EVP_CipherInit_ex(ctx, + cipher, + NULL, + NULL, + NULL, + 0); + + // Set the sizes for the IV and KEY + EVP_CIPHER_CTX_ctrl(ctx, + EVP_CTRL_CCM_SET_IVLEN, + decryptInfo.nonce.size(), + NULL); + + EVP_CIPHER_CTX_set_key_length(ctx, + client_key.size()); + + // Set the KEY and IV + EVP_CipherInit_ex(ctx, + NULL, + NULL, + client_key.data(), + decryptInfo.nonce.data(), + 0); + + // Set the tag to be validated after decryption + EVP_CIPHER_CTX_ctrl(ctx, + EVP_CTRL_CCM_SET_TAG, + tag_to_check.size(), + tag_to_check.data()); + + // Setting the second parameter to NULL will pass it as Associated Data + EVP_CipherUpdate(ctx, + NULL, + &out, + decryptInfo.unprotected_header.data(), + decryptInfo.unprotected_header.size()); + + // Set the actual data to decrypt data into the decrypt_buffer. The amount of + // byte decrypted is stored into `out` + EVP_CipherUpdate(ctx, + decrypt_buffer, + &out, + encrypted_payload.data(), + encrypted_payload.size()); + + // Validate whether the decryption was successful or not + EVP_CipherFinal_ex(ctx, NULL, &out2); + + // Copy the decrypted data from the decrypted buffer into a new vector and return this + // Use the `out` variable to only include relevant bytes + std::vector decrypted_data(decrypt_buffer, decrypt_buffer + out); + return decrypted_data; +} + +/* +Function that is called from Spicy. It's a wrapper around `process_data`; +it stores all the passed data in a global struct and then calls `process_data`, +which will eventually return the decrypted data and pass it back to Spicy. +*/ +hilti::rt::Bytes decrypt_crypto_payload( + const hilti::rt::Bytes &entire_packet, + const hilti::rt::Bytes &connection_id, + const hilti::rt::integer::safe &encrypted_offset, + const hilti::rt::integer::safe &payload_offset, + const hilti::rt::Bool &from_client) +{ + + // Fill in the entire packet bytes + std::vector e_pkt; + for (const auto &singlebyte : entire_packet) + { + e_pkt.push_back(singlebyte); + } + + std::vector cnnid; + for (const auto &singlebyte : connection_id) + { + cnnid.push_back(singlebyte); + } + + std::vector initial_secret = hkdf_extract(cnnid); + + std::vector server_client_secret; + if (from_client) + { + server_client_secret = hkdf_expand(INITIAL_SECRET_LEN, + initial_secret, + CLIENT_INITIAL_INFO); + } + else + { + server_client_secret = hkdf_expand(INITIAL_SECRET_LEN, + initial_secret, + SERVER_INITIAL_INFO); + } + + std::vector key = hkdf_expand(AEAD_KEY_LEN, + server_client_secret, + KEY_INFO); + std::vector iv = hkdf_expand(AEAD_IV_LEN, + server_client_secret, + IV_INFO); + std::vector hp = hkdf_expand(AEAD_HP_LEN, + server_client_secret, + HP_INFO); + + DecryptionInformation decryptInfo = remove_header_protection(hp, (uint8_t)encrypted_offset, e_pkt); + + // Calculate the correct nonce for the decryption + decryptInfo.nonce = calculate_nonce(iv, decryptInfo.packet_number); + + std::vector decrypted_data = decrypt(key, e_pkt, payload_offset, decryptInfo); + + // Return it as hilti Bytes again + hilti::rt::Bytes decr(decrypted_data.begin(), decrypted_data.end()); + return decr; +} diff --git a/src/analyzer/protocol/quic/zeek_QUIC.spicy b/src/analyzer/protocol/quic/zeek_QUIC.spicy new file mode 100644 index 0000000000..c2013335d3 --- /dev/null +++ b/src/analyzer/protocol/quic/zeek_QUIC.spicy @@ -0,0 +1,12 @@ +module Zeek_QUIC; + +import zeek; +import QUIC; + +on QUIC::ResponseFrame::%done { + zeek::confirm_protocol(); +} + +on QUIC::ResponseFrame::%error { + zeek::reject_protocol("error while parsing QUIC message"); +} diff --git a/testing/btest/Baseline/scripts.base.protocols.quic.run-pcap/conn.log b/testing/btest/Baseline/scripts.base.protocols.quic.run-pcap/conn.log new file mode 100644 index 0000000000..7e2c337793 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.quic.run-pcap/conn.log @@ -0,0 +1,11 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path conn +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service duration orig_bytes resp_bytes conn_state local_orig local_resp missed_bytes history orig_pkts orig_ip_bytes resp_pkts resp_ip_bytes tunnel_parents +#types time string addr port addr port enum string interval count count string bool bool count string count count count count set[string] +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 1.2.3.4 49369 4.3.2.1 443 udp spicy_quic,ssl 18.071102 14371 394242 SF - - 0 Dd 96 17059 345 403902 - +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.quic.run-pcap/output b/testing/btest/Baseline/scripts.base.protocols.quic.run-pcap/output new file mode 100644 index 0000000000..49d861c74c --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.quic.run-pcap/output @@ -0,0 +1 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. diff --git a/testing/btest/Baseline/scripts.base.protocols.quic.run-pcap/ssl.log b/testing/btest/Baseline/scripts.base.protocols.quic.run-pcap/ssl.log new file mode 100644 index 0000000000..496f6a47f8 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.quic.run-pcap/ssl.log @@ -0,0 +1,11 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path ssl +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p version cipher curve server_name resumed last_alert next_protocol established ssl_history cert_chain_fps client_cert_chain_fps sni_matches_cert +#types time string addr port addr port string string string string bool string string bool string vector[string] vector[string] bool +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 1.2.3.4 49369 4.3.2.1 443 - - - www.google.com F - - F C - - - +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Traces/quic/quic_win11_firefox_google.pcap b/testing/btest/Traces/quic/quic_win11_firefox_google.pcap new file mode 100644 index 0000000000..a18d821d07 Binary files /dev/null and b/testing/btest/Traces/quic/quic_win11_firefox_google.pcap differ diff --git a/testing/btest/scripts/base/protocols/quic/run-pcap.zeek b/testing/btest/scripts/base/protocols/quic/run-pcap.zeek new file mode 100644 index 0000000000..763993316c --- /dev/null +++ b/testing/btest/scripts/base/protocols/quic/run-pcap.zeek @@ -0,0 +1,4 @@ +# @TEST-DOC: Test that runs the pcap +# @TEST-EXEC: zeek -Cr $TRACES/quic/quic_win11_firefox_google.pcap base/protocols/quic >output +# @TEST-EXEC: btest-diff conn.log +# @TEST-EXEC: btest-diff ssl.log \ No newline at end of file