Merge remote-tracking branch 'origin/topic/bernhard/ssl-analyzer'

* origin/topic/bernhard/ssl-analyzer:
  Fix a few failing tests
  Add very basic ocsp stapling support.
  Add documentation, consts and tests for the new events.
  Support parsing of several TLS extensions.
  Make SSL/TLS version detection less brittle.
  Nicer notices for heartbleed.
  rip out state handline from ssl analyzer.
  enable detection of encrypted heartbleeds.
  also extract payload data in ssl_heartbeat
  add to local.bro, add disclaimer
  make tls heartbeat messages a bit better.
  fix tabs.
  polish script and probably detect encrypted attacks too.
  detect and alert on simple case of heartbleed
  default to TLS when not being able to determine version
  add is_orig to heartbeat event
  Throw new event for heartbeat messages.

BIT-1178 #merged
This commit is contained in:
Robin Sommer 2014-04-24 16:57:54 -07:00
commit 201fc7b25a
28 changed files with 800 additions and 376 deletions

View file

@ -34,26 +34,23 @@ type SSLRecord(is_orig: bool) = record {
head4 : uint8;
rec : RecordText(this)[] &length=length, &requires(content_type);
} &length = length+5, &byteorder=bigendian,
&let {
&let {
version : int =
$context.connection.determine_ssl_version(head0, head1, head2);
$context.connection.determine_ssl_record_layer(head0, head1, head2, head3, head4);
content_type : int = case version of {
UNKNOWN_VERSION -> 0;
SSLv20 -> head2+300;
default -> head0;
};
length : int = case version of {
UNKNOWN_VERSION -> 0;
SSLv20 -> (((head0 & 0x7f) << 8) | head1) - 3;
default -> (head3 << 8) | head4;
};
};
type RecordText(rec: SSLRecord) = case $context.connection.state() of {
STATE_ABBREV_SERVER_ENCRYPTED, STATE_CLIENT_ENCRYPTED,
STATE_COMM_ENCRYPTED, STATE_CONN_ESTABLISHED
type RecordText(rec: SSLRecord) = case $context.connection.state(rec.is_orig) of {
STATE_ENCRYPTED
-> ciphertext : CiphertextRecord(rec);
default
-> plaintext : PlaintextRecord(rec);
@ -63,6 +60,7 @@ type PlaintextRecord(rec: SSLRecord) = case rec.content_type of {
CHANGE_CIPHER_SPEC -> ch_cipher : ChangeCipherSpec(rec);
ALERT -> alert : Alert(rec);
HANDSHAKE -> handshake : Handshake(rec);
HEARTBEAT -> heartbeat: Heartbeat(rec);
APPLICATION_DATA -> app_data : ApplicationData(rec);
V2_ERROR -> v2_error : V2Error(rec);
V2_CLIENT_HELLO -> v2_client_hello : V2ClientHello(rec);
@ -71,75 +69,98 @@ type PlaintextRecord(rec: SSLRecord) = case rec.content_type of {
default -> unknown_record : UnknownRecord(rec);
};
######################################################################
# TLS Extensions
######################################################################
type SSLExtension(rec: SSLRecord) = record {
type: uint16;
data_len: uint16;
data: bytestring &length=data_len;
# Pretty code ahead. Deal with the fact that perhaps extensions are
# not really present and we do not want to fail because of that.
ext: case type of {
EXT_APPLICATION_LAYER_PROTOCOL_NEGOTIATION -> apnl: ApplicationLayerProtocolNegotiationExtension(rec)[] &until($element == 0 || $element != 0);
EXT_ELLIPTIC_CURVES -> elliptic_curves: EllipticCurves(rec)[] &until($element == 0 || $element != 0);
EXT_EC_POINT_FORMATS -> ec_point_formats: EcPointFormats(rec)[] &until($element == 0 || $element != 0);
# EXT_STATUS_REQUEST -> status_request: StatusRequest(rec)[] &until($element == 0 || $element != 0);
EXT_SERVER_NAME -> server_name: ServerNameExt(rec)[] &until($element == 0 || $element != 0);
default -> data: bytestring &restofdata;
};
} &length=data_len+4 &exportsourcedata;
type ServerNameHostName() = record {
length: uint16;
host_name: bytestring &length=length;
};
type ServerName() = record {
name_type: uint8; # has to be 0 for host-name
name: case name_type of {
0 -> host_name: ServerNameHostName;
default -> data : bytestring &restofdata; # unknown name
};
};
type ServerNameExt(rec: SSLRecord) = record {
length: uint16;
server_names: ServerName[] &until($input.length() == 0);
} &length=length+2;
# Do not parse for now. Structure is correct, but only contains asn.1 data that we would not use further.
#type OcspStatusRequest(rec: SSLRecord) = record {
# responder_id_list_length: uint16;
# responder_id_list: bytestring &length=responder_id_list_length;
# request_extensions_length: uint16;
# request_extensions: bytestring &length=request_extensions_length;
#};
#
#type StatusRequest(rec: SSLRecord) = record {
# status_type: uint8; # 1 -> ocsp
# req: case status_type of {
# 1 -> ocsp_status_request: OcspStatusRequest(rec);
# default -> data : bytestring &restofdata; # unknown
# };
#};
type EcPointFormats(rec: SSLRecord) = record {
length: uint8;
point_format_list: uint8[length];
};
type EllipticCurves(rec: SSLRecord) = record {
length: uint16;
elliptic_curve_list: uint16[length/2];
};
type ProtocolName() = record {
length: uint8;
name: bytestring &length=length;
};
type ApplicationLayerProtocolNegotiationExtension(rec: SSLRecord) = record {
length: uint16;
protocol_name_list: ProtocolName[] &until($input.length() == 0);
} &length=length+2;
######################################################################
# state management according to Section 7.3. in spec
# Encryption Tracking
######################################################################
enum AnalyzerState {
STATE_INITIAL,
STATE_CLIENT_HELLO_RCVD,
STATE_IN_SERVER_HELLO,
STATE_SERVER_HELLO_DONE,
STATE_CLIENT_CERT,
STATE_CLIENT_KEY_WITH_CERT,
STATE_CLIENT_KEY_NO_CERT,
STATE_CLIENT_CERT_VERIFIED,
STATE_CLIENT_ENCRYPTED,
STATE_CLIENT_FINISHED,
STATE_ABBREV_SERVER_ENCRYPTED,
STATE_ABBREV_SERVER_FINISHED,
STATE_COMM_ENCRYPTED,
STATE_CONN_ESTABLISHED,
STATE_V2_CL_MASTER_KEY_EXPECTED,
STATE_TRACK_LOST,
STATE_ANY
STATE_CLEAR,
STATE_ENCRYPTED
};
%code{
string state_label(int state_nr)
{
switch ( state_nr ) {
case STATE_INITIAL:
return string("INITIAL");
case STATE_CLIENT_HELLO_RCVD:
return string("CLIENT_HELLO_RCVD");
case STATE_IN_SERVER_HELLO:
return string("IN_SERVER_HELLO");
case STATE_SERVER_HELLO_DONE:
return string("SERVER_HELLO_DONE");
case STATE_CLIENT_CERT:
return string("CLIENT_CERT");
case STATE_CLIENT_KEY_WITH_CERT:
return string("CLIENT_KEY_WITH_CERT");
case STATE_CLIENT_KEY_NO_CERT:
return string("CLIENT_KEY_NO_CERT");
case STATE_CLIENT_CERT_VERIFIED:
return string("CLIENT_CERT_VERIFIED");
case STATE_CLIENT_ENCRYPTED:
return string("CLIENT_ENCRYPTED");
case STATE_CLIENT_FINISHED:
return string("CLIENT_FINISHED");
case STATE_ABBREV_SERVER_ENCRYPTED:
return string("ABBREV_SERVER_ENCRYPTED");
case STATE_ABBREV_SERVER_FINISHED:
return string("ABBREV_SERVER_FINISHED");
case STATE_COMM_ENCRYPTED:
return string("COMM_ENCRYPTED");
case STATE_CONN_ESTABLISHED:
return string("CONN_ESTABLISHED");
case STATE_V2_CL_MASTER_KEY_EXPECTED:
return string("STATE_V2_CL_MASTER_KEY_EXPECTED");
case STATE_TRACK_LOST:
return string("TRACK_LOST");
case STATE_ANY:
return string("ANY");
case STATE_CLEAR:
return string("CLEAR");
case STATE_ENCRYPTED:
return string("ENCRYPTED");
default:
return string(fmt("UNKNOWN (%d)", state_nr));
@ -176,21 +197,7 @@ type ChangeCipherSpec(rec: SSLRecord) = record {
type : uint8;
} &length = 1, &let {
state_changed : bool =
$context.connection.transition(STATE_CLIENT_FINISHED,
STATE_COMM_ENCRYPTED, rec.is_orig, false) ||
$context.connection.transition(STATE_IN_SERVER_HELLO,
STATE_ABBREV_SERVER_ENCRYPTED, rec.is_orig, false) ||
$context.connection.transition(STATE_CLIENT_KEY_NO_CERT,
STATE_CLIENT_ENCRYPTED, rec.is_orig, true) ||
$context.connection.transition(STATE_CLIENT_CERT_VERIFIED,
STATE_CLIENT_ENCRYPTED, rec.is_orig, true) ||
$context.connection.transition(STATE_CLIENT_CERT,
STATE_CLIENT_ENCRYPTED, rec.is_orig, true) ||
$context.connection.transition(STATE_CLIENT_KEY_WITH_CERT,
STATE_CLIENT_ENCRYPTED, rec.is_orig, true) ||
$context.connection.transition(STATE_ABBREV_SERVER_FINISHED,
STATE_COMM_ENCRYPTED, rec.is_orig, true) ||
$context.connection.lost_track();
$context.connection.startEncryption(rec.is_orig);
};
@ -209,7 +216,7 @@ type Alert(rec: SSLRecord) = record {
######################################################################
type V2Error(rec: SSLRecord) = record {
data: bytestring &restofdata &transient;
data : bytestring &restofdata &transient;
} &let {
error_code : uint16 = ((rec.head3 << 8) | rec.head4);
};
@ -226,17 +233,21 @@ type ApplicationData(rec: SSLRecord) = record {
};
######################################################################
# Handshake Protocol (7.4.)
# V3 Heartbeat
######################################################################
type Heartbeat(rec: SSLRecord) = record {
type : uint8;
payload_length : uint16;
data : bytestring &restofdata;
};
######################################################################
# V3 Hello Request (7.4.1.1.)
######################################################################
# Hello Request is empty
type HelloRequest(rec: SSLRecord) = empty &let {
hr: bool = $context.connection.set_hello_requested(true);
};
type HelloRequest(rec: SSLRecord) = empty;
######################################################################
@ -257,16 +268,8 @@ type ClientHello(rec: SSLRecord) = record {
# of the following fields.
ext_len: uint16[] &until($element == 0 || $element != 0);
extensions : SSLExtension(rec)[] &until($input.length() == 0);
} &let {
state_changed : bool =
$context.connection.transition(STATE_INITIAL,
STATE_CLIENT_HELLO_RCVD, rec.is_orig, true) ||
($context.connection.hello_requested() &&
$context.connection.transition(STATE_ANY, STATE_CLIENT_HELLO_RCVD, rec.is_orig, true)) ||
$context.connection.lost_track();
};
######################################################################
# V2 Client Hello (SSLv2 2.5.)
######################################################################
@ -279,13 +282,6 @@ type V2ClientHello(rec: SSLRecord) = record {
session_id : uint8[session_len];
challenge : bytestring &length = chal_len;
} &length = 6 + csuit_len + session_len + chal_len, &let {
state_changed : bool =
$context.connection.transition(STATE_INITIAL,
STATE_CLIENT_HELLO_RCVD, rec.is_orig, true) ||
($context.connection.hello_requested() &&
$context.connection.transition(STATE_ANY, STATE_CLIENT_HELLO_RCVD, rec.is_orig, true)) ||
$context.connection.lost_track();
client_version : int = rec.version;
};
@ -306,11 +302,6 @@ type ServerHello(rec: SSLRecord) = record {
# of the following fields.
ext_len: uint16[] &until($element == 0 || $element != 0);
extensions : SSLExtension(rec)[] &until($input.length() == 0);
} &let {
state_changed : bool =
$context.connection.transition(STATE_CLIENT_HELLO_RCVD,
STATE_IN_SERVER_HELLO, rec.is_orig, false) ||
$context.connection.lost_track();
};
@ -329,14 +320,6 @@ type V2ServerHello(rec: SSLRecord) = record {
ciphers : uint24[ciph_len/3];
conn_id_data : bytestring &length = conn_id_len;
} &let {
state_changed : bool =
(session_id_hit > 0 ?
$context.connection.transition(STATE_CLIENT_HELLO_RCVD,
STATE_CONN_ESTABLISHED, rec.is_orig, false) :
$context.connection.transition(STATE_CLIENT_HELLO_RCVD,
STATE_V2_CL_MASTER_KEY_EXPECTED, rec.is_orig, false)) ||
$context.connection.lost_track();
session_id_hit : uint8 = rec.head3;
cert_type : uint8 = rec.head4;
};
@ -351,20 +334,18 @@ type X509Certificate = record {
certificate : bytestring &length = to_int()(length);
};
type CertificateList = X509Certificate[] &until($input.length() == 0);
type Certificate(rec: SSLRecord) = record {
length : uint24;
certificates : CertificateList &length = to_int()(length);
} &let {
state_changed : bool =
$context.connection.transition(STATE_IN_SERVER_HELLO,
STATE_IN_SERVER_HELLO, rec.is_orig, false) ||
$context.connection.transition(STATE_SERVER_HELLO_DONE,
STATE_CLIENT_CERT, rec.is_orig, true) ||
$context.connection.lost_track();
};
certificates : X509Certificate[] &until($input.length() == 0);
} &length = to_int()(length)+3;
# OCSP Stapling
type CertificateStatus(rec: SSLRecord) = record {
status_type: uint8; # 1 = ocsp, everything else is undefined
length : uint24;
response: bytestring &restofdata;
};
######################################################################
# V3 Server Key Exchange Message (7.4.3.)
@ -373,11 +354,6 @@ type Certificate(rec: SSLRecord) = record {
# For now ignore details; just eat up complete message
type ServerKeyExchange(rec: SSLRecord) = record {
key : bytestring &restofdata &transient;
} &let {
state_changed : bool =
$context.connection.transition(STATE_IN_SERVER_HELLO,
STATE_IN_SERVER_HELLO, rec.is_orig, false) ||
$context.connection.lost_track();
};
@ -388,11 +364,6 @@ type ServerKeyExchange(rec: SSLRecord) = record {
# For now, ignore Certificate Request Details; just eat up message.
type CertificateRequest(rec: SSLRecord) = record {
cont : bytestring &restofdata &transient;
} &let {
state_changed : bool =
$context.connection.transition(STATE_IN_SERVER_HELLO,
STATE_IN_SERVER_HELLO, rec.is_orig, false) ||
$context.connection.lost_track();
};
@ -401,12 +372,7 @@ type CertificateRequest(rec: SSLRecord) = record {
######################################################################
# Server Hello Done is empty
type ServerHelloDone(rec: SSLRecord) = empty &let {
state_changed : bool =
$context.connection.transition(STATE_IN_SERVER_HELLO,
STATE_SERVER_HELLO_DONE, rec.is_orig, false) ||
$context.connection.lost_track();
};
type ServerHelloDone(rec: SSLRecord) = empty;
######################################################################
@ -425,15 +391,6 @@ type ServerHelloDone(rec: SSLRecord) = empty &let {
# encrypted anyway); just eat up message.
type ClientKeyExchange(rec: SSLRecord) = record {
key : bytestring &restofdata &transient;
} &let {
state_changed : bool =
$context.connection.transition(STATE_SERVER_HELLO_DONE,
STATE_CLIENT_KEY_NO_CERT, rec.is_orig, true) ||
$context.connection.transition(STATE_CLIENT_CERT,
STATE_CLIENT_KEY_WITH_CERT, rec.is_orig, true) ||
$context.connection.transition(STATE_CLIENT_CERT,
STATE_CLIENT_KEY_WITH_CERT, rec.is_orig, true) ||
$context.connection.lost_track();
};
######################################################################
@ -449,12 +406,10 @@ type V2ClientMasterKey(rec: SSLRecord) = record {
en_key_data : bytestring &length = en_key_len &transient;
key_arg_data : bytestring &length = key_arg_len &transient;
} &length = 7 + cl_key_len + en_key_len + key_arg_len, &let {
state_changed : bool =
$context.connection.transition(STATE_V2_CL_MASTER_KEY_EXPECTED,
STATE_CONN_ESTABLISHED, rec.is_orig, true) ||
$context.connection.lost_track();
cipher_kind : int = (((rec.head3 << 16) | (rec.head4 << 8)) | cipher_kind_8);
# encryption starts for both sides after this message.
state_changed_client : bool = $context.connection.startEncryption(true);
state_changed_server : bool = $context.connection.startEncryption(false);
};
@ -465,11 +420,6 @@ type V2ClientMasterKey(rec: SSLRecord) = record {
# For now, ignore Certificate Verify; just eat up the message.
type CertificateVerify(rec: SSLRecord) = record {
cont : bytestring &restofdata &transient;
} &let {
state_changed : bool =
$context.connection.transition(STATE_CLIENT_KEY_WITH_CERT,
STATE_CLIENT_CERT_VERIFIED, rec.is_orig, true) ||
$context.connection.lost_track();
};
@ -481,13 +431,6 @@ type CertificateVerify(rec: SSLRecord) = record {
# so we will not be able to read those messages.
type Finished(rec: SSLRecord) = record {
cont : bytestring &restofdata &transient;
} &let {
state_changed : bool =
$context.connection.transition(STATE_SERVER_HELLO_DONE,
STATE_COMM_ENCRYPTED, rec.is_orig, true) ||
$context.connection.transition(STATE_CLIENT_FINISHED,
STATE_COMM_ENCRYPTED, rec.is_orig, false) ||
$context.connection.lost_track();
};
type SessionTicketHandshake(rec: SSLRecord) = record {
@ -499,10 +442,8 @@ type SessionTicketHandshake(rec: SSLRecord) = record {
# V3 Handshake Protocol (7.)
######################################################################
type UnknownHandshake(hs: Handshake, is_orig: bool) = record {
type UnknownHandshake(hs: Handshake, is_orig: bool) = record {
data : bytestring &restofdata &transient;
} &let {
state_changed : bool = $context.connection.lost_track();
};
type Handshake(rec: SSLRecord) = record {
@ -522,7 +463,7 @@ type Handshake(rec: SSLRecord) = record {
CLIENT_KEY_EXCHANGE -> client_key_exchange : ClientKeyExchange(rec);
FINISHED -> finished : Finished(rec);
CERTIFICATE_URL -> certificate_url : bytestring &restofdata &transient;
CERTIFICATE_STATUS -> certificate_status : bytestring &restofdata &transient;
CERTIFICATE_STATUS -> certificate_status : CertificateStatus(rec);
default -> unknown_handshake : UnknownHandshake(this, rec.is_orig);
} &length = to_int()(length);
};
@ -532,33 +473,12 @@ type Handshake(rec: SSLRecord) = record {
# Fragmentation (6.2.1.)
######################################################################
type UnknownRecord(rec: SSLRecord) = record {
type UnknownRecord(rec: SSLRecord) = record {
cont : bytestring &restofdata &transient;
} &let {
state_changed : bool = $context.connection.lost_track();
};
type CiphertextRecord(rec: SSLRecord) = record {
cont : bytestring &restofdata &transient;
} &let {
state_changed : bool =
$context.connection.transition(STATE_CLIENT_FINISHED,
STATE_CLIENT_FINISHED, rec.is_orig, false) ||
$context.connection.transition(STATE_CLIENT_FINISHED,
STATE_CLIENT_FINISHED, rec.is_orig, true) ||
$context.connection.transition(STATE_ABBREV_SERVER_ENCRYPTED,
STATE_ABBREV_SERVER_FINISHED, rec.is_orig, false) ||
$context.connection.transition(STATE_CLIENT_ENCRYPTED,
STATE_CLIENT_FINISHED, rec.is_orig, true) ||
$context.connection.transition(STATE_COMM_ENCRYPTED,
STATE_CONN_ESTABLISHED, rec.is_orig, false) ||
$context.connection.transition(STATE_COMM_ENCRYPTED,
STATE_CONN_ESTABLISHED, rec.is_orig, true) ||
$context.connection.transition(STATE_CONN_ESTABLISHED,
STATE_CONN_ESTABLISHED, rec.is_orig, false) ||
$context.connection.transition(STATE_CONN_ESTABLISHED,
STATE_CONN_ESTABLISHED, rec.is_orig, true) ||
$context.connection.lost_track();
};
@ -578,67 +498,88 @@ type SSLPDU(is_orig: bool) = record {
refine connection SSL_Conn += {
%member{
int state_;
int old_state_;
bool hello_requested_;
int client_state_;
int server_state_;
int record_layer_version_;
%}
%init{
state_ = STATE_INITIAL;
old_state_ = STATE_INITIAL;
hello_requested_ = false;
server_state_ = STATE_CLEAR;
client_state_ = STATE_CLEAR;
record_layer_version_ = UNKNOWN_VERSION;
%}
function determine_ssl_version(head0 : uint8, head1 : uint8,
head2 : uint8) : int
function determine_ssl_record_layer(head0 : uint8, head1 : uint8,
head2 : uint8, head3: uint8, head4: uint8) : int
%{
if ( head0 >= 20 && head0 <= 23 &&
head1 == 0x03 && head2 <= 0x03 )
// This is most probably SSL version 3.
return (head1 << 8) | head2;
if ( record_layer_version_ != UNKNOWN_VERSION )
return record_layer_version_;
else if ( head0 >= 128 && head2 < 5 && head2 != 3 )
// Not very strong evidence, but we suspect
// this to be SSLv2.
return SSLv20;
if ( head0 & 0x80 )
{
if ( head2 == 0x01 ) // SSLv2 client hello.
{
uint16 version = (head3 << 8) | head4;
if ( version != SSLv20 && version != SSLv30 && version != TLSv10 &&
version != TLSv11 && version != TLSv12 )
{
bro_analyzer()->ProtocolViolation(fmt("Invalid version in SSL client hello. Version: %d", version));
return UNKNOWN_VERSION;
}
else
else
return SSLv20;
}
else if ( head2 == 0x04 ) // SSLv2 server hello. This connection will continue using SSLv2.
{
record_layer_version_ = SSLv20;
return SSLv20;
}
else // this is not SSL or TLS.
{
bro_analyzer()->ProtocolViolation(fmt("Invalid headers in SSL connection. Head1: %d, head2: %d, head3: %d", head1, head2, head3));
return UNKNOWN_VERSION;
}
}
uint16 version = (head1<<8) | head2;
if ( version != SSLv30 && version != TLSv10 &&
version != TLSv11 && version != TLSv12 )
{
bro_analyzer()->ProtocolViolation(fmt("Invalid version in TLS connection. Version: %d", version));
return UNKNOWN_VERSION;
}
if ( head0 >=20 && head0 <= 30 )
{ // ok, set record layer version, this never can be downgraded to v2
record_layer_version_ = version;
return version;
}
bro_analyzer()->ProtocolViolation(fmt("Invalid type in TLS connection. Version: %d, Type: %d", version, head0));
return UNKNOWN_VERSION;
%}
function state() : int %{ return state_; %}
function old_state() : int %{ return old_state_; %}
function client_state() : int %{ return client_state_; %}
function transition(olds : AnalyzerState, news : AnalyzerState,
current_record_is_orig : bool, is_orig : bool) : bool
function server_state() : int %{ return client_state_; %}
function state(is_orig: bool) : int
%{
if ( (olds != STATE_ANY && olds != state_) ||
current_record_is_orig != is_orig )
return false;
if ( is_orig )
return client_state_;
else
return server_state_;
%}
old_state_ = state_;
state_ = news;
//printf("transitioning from %s to %s\n", state_label(old_state()).c_str(), state_label(state()).c_str());
function startEncryption(is_orig: bool) : bool
%{
if ( is_orig )
client_state_ = STATE_ENCRYPTED;
else
server_state_ = STATE_ENCRYPTED;
return true;
%}
function lost_track() : bool
%{
state_ = STATE_TRACK_LOST;
return false;
%}
function hello_requested() : bool
%{
bool ret = hello_requested_;
hello_requested_ = false;
return ret;
%}
function set_hello_requested(val : bool) : bool
%{
hello_requested_ = val;
return val;
%}
};