mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 06:38:20 +00:00
quic: Initial implementation
This commit is contained in:
parent
ffc35d90ba
commit
44d7c45723
12 changed files with 848 additions and 0 deletions
1
scripts/base/protocols/quic/__load__.zeek
Normal file
1
scripts/base/protocols/quic/__load__.zeek
Normal file
|
@ -0,0 +1 @@
|
|||
@load ./main
|
2
scripts/base/protocols/quic/main.zeek
Normal file
2
scripts/base/protocols/quic/main.zeek
Normal file
|
@ -0,0 +1,2 @@
|
|||
module QUIC;
|
||||
|
5
src/analyzer/protocol/quic/CMakeLists.txt
Normal file
5
src/analyzer/protocol/quic/CMakeLists.txt
Normal file
|
@ -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)
|
10
src/analyzer/protocol/quic/QUIC.evt
Normal file
10
src/analyzer/protocol/quic/QUIC.evt
Normal file
|
@ -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);
|
411
src/analyzer/protocol/quic/QUIC.spicy
Normal file
411
src/analyzer/protocol/quic/QUIC.spicy
Normal file
|
@ -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<HeaderForm>(cast<uint8>($$));
|
||||
};
|
||||
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<uint8>($$) &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<HeaderForm>(cast<uint8>($$));
|
||||
fixed_bit: 6;
|
||||
packet_type: 4..5 &convert=cast<LongPacketType>(cast<uint8>($$));
|
||||
type_specific_bits: 0..3 &convert=cast<uint8>($$);
|
||||
};
|
||||
|
||||
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<FrameType>($$);
|
||||
|
||||
# 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<uint8>((self.length.result >> 8) & 0xff)).encode();
|
||||
self.length_in_byte2 = ("%c" % cast<uint8>(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<HeaderForm>(cast<uint8>($$));
|
||||
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());
|
||||
};
|
380
src/analyzer/protocol/quic/decrypt_crypto.cc
Normal file
380
src/analyzer/protocol/quic/decrypt_crypto.cc
Normal file
|
@ -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 <stdlib.h>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
// OpenSSL imports
|
||||
#include <openssl/kdf.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
// Import HILTI
|
||||
#include <hilti/rt/libhilti.h>
|
||||
|
||||
// Struct to store decryption info for this specific connection
|
||||
struct DecryptionInformation
|
||||
{
|
||||
std::vector<uint8_t> unprotected_header;
|
||||
std::vector<uint8_t> protected_header;
|
||||
uint64_t packet_number;
|
||||
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> CLIENT_INITIAL_INFO = {
|
||||
0x00, 0x20, 0x0f, 0x74, 0x6c,
|
||||
0x73, 0x31, 0x33, 0x20, 0x63,
|
||||
0x6c, 0x69, 0x65, 0x6e, 0x74,
|
||||
0x20, 0x69, 0x6e, 0x00};
|
||||
|
||||
std::vector<uint8_t> SERVER_INITIAL_INFO = {
|
||||
0x00, 0x20, 0x0f, 0x74, 0x6c,
|
||||
0x73, 0x31, 0x33, 0x20, 0x73,
|
||||
0x65, 0x72, 0x76, 0x65, 0x72,
|
||||
0x20, 0x69, 0x6e, 0x00};
|
||||
|
||||
std::vector<uint8_t> KEY_INFO = {
|
||||
0x00, 0x10, 0x0e, 0x74, 0x6c,
|
||||
0x73, 0x31, 0x33, 0x20, 0x71,
|
||||
0x75, 0x69, 0x63, 0x20, 0x6b,
|
||||
0x65, 0x79, 0x00};
|
||||
|
||||
std::vector<uint8_t> IV_INFO = {
|
||||
0x00, 0x0c, 0x0d, 0x74, 0x6c,
|
||||
0x73, 0x31, 0x33, 0x20, 0x71,
|
||||
0x75, 0x69, 0x63, 0x20, 0x69,
|
||||
0x76, 0x00};
|
||||
|
||||
std::vector<uint8_t> 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<uint8_t> hkdf_extract(std::vector<uint8_t> connection_id)
|
||||
{
|
||||
std::vector<uint8_t> 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<size_t *>(&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<uint8_t> hkdf_expand(size_t out_len,
|
||||
std::vector<uint8_t> key,
|
||||
std::vector<uint8_t> info)
|
||||
{
|
||||
std::vector<uint8_t> 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<uint8_t> client_hp, uint8_t encrypted_offset, std::vector<uint8_t> 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<uint8_t> sample(encrypted_packet.begin() +
|
||||
encrypted_offset +
|
||||
MAXIMUM_PACKET_NUMBER_LENGTH,
|
||||
|
||||
encrypted_packet.begin() +
|
||||
encrypted_offset +
|
||||
MAXIMUM_PACKET_NUMBER_LENGTH +
|
||||
AEAD_SAMPLE_LENGTH);
|
||||
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> calculate_nonce(std::vector<uint8_t> client_iv, uint64_t packet_number)
|
||||
{
|
||||
std::vector<uint8_t> 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<uint8_t> decrypt(std::vector<uint8_t> client_key,
|
||||
std::vector<uint8_t> encrypted_packet,
|
||||
uint64_t payload_offset,
|
||||
DecryptionInformation decryptInfo)
|
||||
{
|
||||
int out, out2, res;
|
||||
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint64_t> &encrypted_offset,
|
||||
const hilti::rt::integer::safe<uint64_t> &payload_offset,
|
||||
const hilti::rt::Bool &from_client)
|
||||
{
|
||||
|
||||
// Fill in the entire packet bytes
|
||||
std::vector<uint8_t> e_pkt;
|
||||
for (const auto &singlebyte : entire_packet)
|
||||
{
|
||||
e_pkt.push_back(singlebyte);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> cnnid;
|
||||
for (const auto &singlebyte : connection_id)
|
||||
{
|
||||
cnnid.push_back(singlebyte);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> initial_secret = hkdf_extract(cnnid);
|
||||
|
||||
std::vector<uint8_t> 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<uint8_t> key = hkdf_expand(AEAD_KEY_LEN,
|
||||
server_client_secret,
|
||||
KEY_INFO);
|
||||
std::vector<uint8_t> iv = hkdf_expand(AEAD_IV_LEN,
|
||||
server_client_secret,
|
||||
IV_INFO);
|
||||
std::vector<uint8_t> 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<uint8_t> 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;
|
||||
}
|
12
src/analyzer/protocol/quic/zeek_QUIC.spicy
Normal file
12
src/analyzer/protocol/quic/zeek_QUIC.spicy
Normal file
|
@ -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");
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
|
@ -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
|
BIN
testing/btest/Traces/quic/quic_win11_firefox_google.pcap
Normal file
BIN
testing/btest/Traces/quic/quic_win11_firefox_google.pcap
Normal file
Binary file not shown.
4
testing/btest/scripts/base/protocols/quic/run-pcap.zeek
Normal file
4
testing/btest/scripts/base/protocols/quic/run-pcap.zeek
Normal file
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue