mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 14:48:21 +00:00
TLS analyzer: change logic to track TLS 1.3 connection establishment
This commit changes the logic that is used to tracks connection establishment - and moves it from scriptland into the core. TLS 1.3 connection establishment is much more finnicky for us than the establishment of earlier versions - since we cannot rely on the CCS message anymore (which is meaningless and not sent in a lot of cases). With this commit, the ssl_encrypted_data message gets raised for encrypted TLS 1.3 handshake messages - which is much more correct than the behavior before that just interpreted them as plaintext messages. I will refine this a bit more - at the moment the connection established event happens a bit too early - earlier than TLS 1.3 connections actually can be estasblished. Part of GH-1323
This commit is contained in:
parent
f5d306a732
commit
bea3075c1f
5 changed files with 68 additions and 44 deletions
|
@ -46,12 +46,6 @@ export {
|
||||||
## by the client. This value is used to determine if a session
|
## by the client. This value is used to determine if a session
|
||||||
## is being resumed. It's not logged.
|
## is being resumed. It's not logged.
|
||||||
client_key_exchange_seen: bool &default=F;
|
client_key_exchange_seen: bool &default=F;
|
||||||
## Count to track if the server already sent an application data
|
|
||||||
## packet for TLS 1.3. Used to track when a session was established.
|
|
||||||
server_appdata: count &default=0;
|
|
||||||
## Flag to track if the client already sent an application data
|
|
||||||
## packet for TLS 1.3. Used to track when a session was established.
|
|
||||||
client_appdata: bool &default=F;
|
|
||||||
|
|
||||||
## Last alert that was seen during the connection.
|
## Last alert that was seen during the connection.
|
||||||
last_alert: string &log &optional;
|
last_alert: string &log &optional;
|
||||||
|
@ -370,41 +364,8 @@ event ssl_plaintext_data(c: connection, is_orig: bool, record_version: count, co
|
||||||
if ( ! c$ssl?$version || c$ssl$established || content_type != APPLICATION_DATA )
|
if ( ! c$ssl?$version || c$ssl$established || content_type != APPLICATION_DATA )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( c$ssl$version_num/0xFF != 0x7F && c$ssl$version_num != TLSv13 )
|
local wi = Weird::Info($ts=network_time(), $name="ssl_early_application_data", $uid=c$uid, $id=c$id);
|
||||||
{
|
Weird::weird(wi);
|
||||||
local wi = Weird::Info($ts=network_time(), $name="ssl_early_application_data", $uid=c$uid, $id=c$id);
|
|
||||||
Weird::weird(wi);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( is_orig )
|
|
||||||
{
|
|
||||||
c$ssl$client_appdata = T;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( c$ssl$client_appdata && c$ssl$server_appdata == 0 )
|
|
||||||
{
|
|
||||||
# something went wrong in the handshake here - we can't say if it was established. Just abort.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if ( ! c$ssl$client_appdata && c$ssl$server_appdata == 0 )
|
|
||||||
{
|
|
||||||
c$ssl$server_appdata = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if ( c$ssl$client_appdata && c$ssl$server_appdata == 1 )
|
|
||||||
{
|
|
||||||
# wait for one more packet before we believe it was established. This one could be an encrypted alert.
|
|
||||||
c$ssl$server_appdata = 2;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if ( c$ssl$client_appdata && c$ssl$server_appdata == 2 )
|
|
||||||
{
|
|
||||||
set_ssl_established(c);
|
|
||||||
event ssl_established(c);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
event protocol_violation(c: connection, atype: Analyzer::Tag, aid: count,
|
event protocol_violation(c: connection, atype: Analyzer::Tag, aid: count,
|
||||||
|
|
|
@ -48,6 +48,11 @@ void SSL_Analyzer::StartEncryption()
|
||||||
interp->setEstablished();
|
interp->setEstablished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint16_t SSL_Analyzer::GetNegotiatedVersion() const
|
||||||
|
{
|
||||||
|
return handshake_interp->chosen_version();
|
||||||
|
}
|
||||||
|
|
||||||
void SSL_Analyzer::DeliverStream(int len, const u_char* data, bool orig)
|
void SSL_Analyzer::DeliverStream(int len, const u_char* data, bool orig)
|
||||||
{
|
{
|
||||||
analyzer::tcp::TCP_ApplicationAnalyzer::DeliverStream(len, data, orig);
|
analyzer::tcp::TCP_ApplicationAnalyzer::DeliverStream(len, data, orig);
|
||||||
|
|
|
@ -24,6 +24,8 @@ public:
|
||||||
|
|
||||||
// Tell the analyzer that encryption has started.
|
// Tell the analyzer that encryption has started.
|
||||||
void StartEncryption();
|
void StartEncryption();
|
||||||
|
// Get the TLS version that the server chose. 0 if not yet known.
|
||||||
|
uint16_t GetNegotiatedVersion() const;
|
||||||
|
|
||||||
// Overriden from analyzer::tcp::TCP_ApplicationAnalyzer.
|
// Overriden from analyzer::tcp::TCP_ApplicationAnalyzer.
|
||||||
void EndpointEOF(bool is_orig) override;
|
void EndpointEOF(bool is_orig) override;
|
||||||
|
|
|
@ -30,10 +30,10 @@ type SSLRecord(is_orig: bool) = record {
|
||||||
UNKNOWN_VERSION -> 0;
|
UNKNOWN_VERSION -> 0;
|
||||||
SSLv20 -> (((head0 & 0x7f) << 8) | head1) - 3;
|
SSLv20 -> (((head0 & 0x7f) << 8) | head1) - 3;
|
||||||
default -> (head3 << 8) | head4;
|
default -> (head3 << 8) | head4;
|
||||||
};
|
} &requires(version);
|
||||||
};
|
};
|
||||||
|
|
||||||
type RecordText(rec: SSLRecord) = case $context.connection.state(rec.is_orig) of {
|
type RecordText(rec: SSLRecord) = case $context.connection.determine_state(rec.is_orig, rec.content_type) of {
|
||||||
STATE_ENCRYPTED
|
STATE_ENCRYPTED
|
||||||
-> ciphertext : CiphertextRecord(rec);
|
-> ciphertext : CiphertextRecord(rec);
|
||||||
default
|
default
|
||||||
|
@ -137,6 +137,49 @@ type SSLPDU(is_orig: bool) = record {
|
||||||
|
|
||||||
refine connection SSL_Conn += {
|
refine connection SSL_Conn += {
|
||||||
|
|
||||||
|
## So - this falls a bit under the envelope of dirty hack - but I don't
|
||||||
|
## really have a better idea. This function determines if a packet should
|
||||||
|
## be handled as an encrypted or as a plaintext packet.
|
||||||
|
##
|
||||||
|
## For TLS 1.2 and below - this is relatively straightforward. Everything
|
||||||
|
## that arrives before CCS (Change Cipher Spec) is a plaintext record. And
|
||||||
|
## everything that arrives after CCS will be encrypted.
|
||||||
|
##
|
||||||
|
## TLS 1.3, however, messes this up a bunch. Some clients still choose to
|
||||||
|
## send a CCS message. The message, however, is pretty much meaningless
|
||||||
|
## from a protocol perspective - and just ignored by the other side. Also -
|
||||||
|
## it is not necessary to send it and some implementations just don't.
|
||||||
|
##
|
||||||
|
## So - what we do here is that we enable the encrypted flag when we get
|
||||||
|
## the first application data in a connection that negotiated TLS 1.3.
|
||||||
|
##
|
||||||
|
## This is correct insofar as the packet will be encrypted. We sadly loose
|
||||||
|
## a bit of context here - we can't really say when we get the first packet
|
||||||
|
## that uses the final cryptographic key material - and will contain content
|
||||||
|
## data. We just don't have that information available in TLS 1.3 anymore.
|
||||||
|
function determine_state(is_orig: bool, content_type: int) : int
|
||||||
|
%{
|
||||||
|
int current_state = state(is_orig);
|
||||||
|
if ( current_state == STATE_ENCRYPTED || content_type != APPLICATION_DATA )
|
||||||
|
return current_state;
|
||||||
|
|
||||||
|
// state = STATE_CLEAR && content_type == APPLICATION_DATA
|
||||||
|
uint16_t negotiated_version = zeek_analyzer()->GetNegotiatedVersion();
|
||||||
|
|
||||||
|
// in theory, we should check for TLS13 or draft-TLS13 instead of doing the reverse.
|
||||||
|
// But - people use weird version numbers. And all of those weird version numbers are
|
||||||
|
// some sort of TLS1.3. So - let's do it this way round instead.
|
||||||
|
if ( negotiated_version != SSLv20 && negotiated_version != SSLv30 && negotiated_version != TLSv10 && negotiated_version != TLSv11 && negotiated_version != TLSv12 )
|
||||||
|
{
|
||||||
|
// well, it seems like this is a TLS 1.3 (or equivalent) applicatio data packet. Let's enable encryption
|
||||||
|
// and handle it as encrypted.
|
||||||
|
startEncryption(is_orig);
|
||||||
|
return STATE_ENCRYPTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current_state; // has to be STATE_CLEAR
|
||||||
|
%}
|
||||||
|
|
||||||
function determine_ssl_record_layer(head0 : uint8, head1 : uint8,
|
function determine_ssl_record_layer(head0 : uint8, head1 : uint8,
|
||||||
head2 : uint8, head3: uint8, head4: uint8, is_orig: bool) : int
|
head2 : uint8, head3: uint8, head4: uint8, is_orig: bool) : int
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -795,10 +795,14 @@ type SupportedVersions(rec: HandshakeRecord) = record {
|
||||||
versions: uint16[] &until($input.length() == 0);
|
versions: uint16[] &until($input.length() == 0);
|
||||||
} &length=length+1;
|
} &length=length+1;
|
||||||
|
|
||||||
|
# If the server sends it, this is the authorative version. Set it.
|
||||||
type OneSupportedVersion(rec: HandshakeRecord) = record {
|
type OneSupportedVersion(rec: HandshakeRecord) = record {
|
||||||
version: uint16;
|
version: uint16;
|
||||||
|
} &let {
|
||||||
|
version_set : bool = $context.connection.set_version(version);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
type PSKKeyExchangeModes(rec: HandshakeRecord) = record {
|
type PSKKeyExchangeModes(rec: HandshakeRecord) = record {
|
||||||
length: uint8;
|
length: uint8;
|
||||||
modes: uint8[] &until($input.length() == 0);
|
modes: uint8[] &until($input.length() == 0);
|
||||||
|
@ -944,6 +948,7 @@ refine connection Handshake_Conn += {
|
||||||
%init{
|
%init{
|
||||||
chosen_cipher_ = NO_CHOSEN_CIPHER;
|
chosen_cipher_ = NO_CHOSEN_CIPHER;
|
||||||
chosen_version_ = UNKNOWN_VERSION;
|
chosen_version_ = UNKNOWN_VERSION;
|
||||||
|
|
||||||
record_version_ = 0;
|
record_version_ = 0;
|
||||||
%}
|
%}
|
||||||
|
|
||||||
|
@ -955,10 +960,18 @@ refine connection Handshake_Conn += {
|
||||||
return true;
|
return true;
|
||||||
%}
|
%}
|
||||||
|
|
||||||
function chosen_version() : int %{ return chosen_version_; %}
|
function chosen_version() : uint16 %{ return chosen_version_; %}
|
||||||
|
|
||||||
|
# This function is called several times in certain circumstances.
|
||||||
|
# If it is called twice, it is first called due to the supported_versions
|
||||||
|
# field in the server hello - and then again due to the outer version in
|
||||||
|
# the server hello. So - once we have a version here, let's just stick
|
||||||
|
# with it.
|
||||||
function set_version(version: uint16) : bool
|
function set_version(version: uint16) : bool
|
||||||
%{
|
%{
|
||||||
|
if ( chosen_version_ != UNKNOWN_VERSION )
|
||||||
|
return false;
|
||||||
|
|
||||||
chosen_version_ = version;
|
chosen_version_ = version;
|
||||||
return true;
|
return true;
|
||||||
%}
|
%}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue