Merge remote-tracking branch 'origin/topic/jsiwek/gh-566-fix-ssh-encrypted-packet'

* origin/topic/jsiwek/gh-566-fix-ssh-encrypted-packet:
  GH-566: fix cases where ssh_encrypted_packet event wasn't raised
This commit is contained in:
Robin Sommer 2019-09-17 17:21:58 +00:00
commit 6f9d1ec72d
10 changed files with 156 additions and 11 deletions

View file

@ -1,4 +1,12 @@
3.1.0-dev.118 | 2019-09-17 17:21:58 +0000
* GH-566: Fix cases where ssh_encrypted_packet event wasn't raised.
When encrypted data was bundled within the same segment as the
NewKeys message, it wasn't not reported via a
ssh_encrypted_package event as it should have been. (Jon Siwek,
Corelight)
3.1.0-dev.116 | 2019-09-17 10:08:38 -0700 3.1.0-dev.116 | 2019-09-17 10:08:38 -0700
* Switch from header guards to pragma once (Dominik Charousset, Corelight) * Switch from header guards to pragma once (Dominik Charousset, Corelight)

View file

@ -1 +1 @@
3.1.0-dev.116 3.1.0-dev.118

View file

@ -18,6 +18,7 @@ SSH_Analyzer::SSH_Analyzer(Connection* c)
had_gap = false; had_gap = false;
auth_decision_made = false; auth_decision_made = false;
skipped_banner = false; skipped_banner = false;
saw_encrypted_client_data = false;
service_accept_size = 0; service_accept_size = 0;
userauth_failure_size = 0; userauth_failure_size = 0;
} }
@ -56,16 +57,12 @@ void SSH_Analyzer::DeliverStream(int len, const u_char* data, bool orig)
if ( interp->get_state(orig) == binpac::SSH::ENCRYPTED ) if ( interp->get_state(orig) == binpac::SSH::ENCRYPTED )
{ {
if ( ssh_encrypted_packet ) ProcessEncryptedSegment(len, orig);
BifEvent::generate_ssh_encrypted_packet(interp->bro_analyzer(), interp->bro_analyzer()->Conn(),
orig, len);
if ( ! auth_decision_made )
ProcessEncrypted(len, orig);
return; return;
} }
interp->clear_encrypted_byte_count_in_current_segment();
try try
{ {
interp->NewData(orig, data, data + len); interp->NewData(orig, data, data + len);
@ -74,6 +71,14 @@ void SSH_Analyzer::DeliverStream(int len, const u_char* data, bool orig)
{ {
ProtocolViolation(fmt("Binpac exception: %s", e.c_msg())); ProtocolViolation(fmt("Binpac exception: %s", e.c_msg()));
} }
auto encrypted_len = interp->get_encrypted_bytes_in_current_segment();
if ( encrypted_len > 0 )
// We must have transitioned into the encrypted state during this
// delivery, but also had some portion of the segment be comprised
// of encrypted data, so process the encrypted segment length.
ProcessEncryptedSegment(encrypted_len, orig);
} }
void SSH_Analyzer::Undelivered(uint64_t seq, int len, bool orig) void SSH_Analyzer::Undelivered(uint64_t seq, int len, bool orig)
@ -83,11 +88,31 @@ void SSH_Analyzer::Undelivered(uint64_t seq, int len, bool orig)
interp->NewGap(orig, len); interp->NewGap(orig, len);
} }
void SSH_Analyzer::ProcessEncryptedSegment(int len, bool orig)
{
if ( ssh_encrypted_packet )
BifEvent::generate_ssh_encrypted_packet(interp->bro_analyzer(),
interp->bro_analyzer()->Conn(),
orig, len);
if ( ! auth_decision_made )
ProcessEncrypted(len, orig);
}
void SSH_Analyzer::ProcessEncrypted(int len, bool orig) void SSH_Analyzer::ProcessEncrypted(int len, bool orig)
{ {
// We're interested in messages from the server for SSH2 if ( interp->get_version() != binpac::SSH::SSH2 )
if ( ! orig && (interp->get_version() == binpac::SSH::SSH2) ) return;
if ( orig )
saw_encrypted_client_data = true;
else
{ {
// If the client hasn't sent any encrypted data yet, but the
// server is, just ignore it until seeing encrypted client data.
if ( ! saw_encrypted_client_data )
return;
// The first thing we see and want to know is the length of // The first thing we see and want to know is the length of
// SSH_MSG_SERVICE_REQUEST, which has a fixed (decrypted) size // SSH_MSG_SERVICE_REQUEST, which has a fixed (decrypted) size
// of 24 bytes (17 for content pad-aligned to 8-byte // of 24 bytes (17 for content pad-aligned to 8-byte

View file

@ -30,12 +30,14 @@ namespace analyzer {
binpac::SSH::SSH_Conn* interp; binpac::SSH::SSH_Conn* interp;
void ProcessEncrypted(int len, bool orig); void ProcessEncrypted(int len, bool orig);
void ProcessEncryptedSegment(int len, bool orig);
bool had_gap; bool had_gap;
// Packet analysis stuff // Packet analysis stuff
bool auth_decision_made; bool auth_decision_made;
bool skipped_banner; bool skipped_banner;
bool saw_encrypted_client_data;
int service_accept_size; int service_accept_size;
int userauth_failure_size; int userauth_failure_size;

View file

@ -9,9 +9,15 @@
# - Encrypted messages have no usable data, so we'll just ignore them as best we can. # - Encrypted messages have no usable data, so we'll just ignore them as best we can.
# - Finally, key exchange messages have a common format. # - Finally, key exchange messages have a common format.
type EncryptedByte(is_orig: bool) = record {
encrypted : bytestring &length=1 &transient;
} &let {
proc: bool = $context.connection.inc_encrypted_byte_count_in_current_segment();
};
type SSH_PDU(is_orig: bool) = case $context.connection.get_state(is_orig) of { type SSH_PDU(is_orig: bool) = case $context.connection.get_state(is_orig) of {
VERSION_EXCHANGE -> version : SSH_Version(is_orig); VERSION_EXCHANGE -> version : SSH_Version(is_orig);
ENCRYPTED -> encrypted : bytestring &length=1 &transient; ENCRYPTED -> encrypted : EncryptedByte(is_orig);
default -> kex : SSH_Key_Exchange(is_orig); default -> kex : SSH_Key_Exchange(is_orig);
} &byteorder=bigendian; } &byteorder=bigendian;
@ -265,6 +271,7 @@ refine connection SSH_Conn += {
int state_up_; int state_up_;
int state_down_; int state_down_;
int version_; int version_;
int encrypted_bytes_in_current_segment_;
bool kex_orig_; bool kex_orig_;
bool kex_seen_; bool kex_seen_;
@ -276,6 +283,7 @@ refine connection SSH_Conn += {
state_up_ = VERSION_EXCHANGE; state_up_ = VERSION_EXCHANGE;
state_down_ = VERSION_EXCHANGE; state_down_ = VERSION_EXCHANGE;
version_ = UNK; version_ = UNK;
encrypted_bytes_in_current_segment_ = 0;
kex_seen_ = false; kex_seen_ = false;
kex_orig_ = false; kex_orig_ = false;
@ -288,6 +296,23 @@ refine connection SSH_Conn += {
kex_algs_cache_.free(); kex_algs_cache_.free();
%} %}
function clear_encrypted_byte_count_in_current_segment() : bool
%{
encrypted_bytes_in_current_segment_ = 0;
return true;
%}
function inc_encrypted_byte_count_in_current_segment() : bool
%{
++encrypted_bytes_in_current_segment_;
return true;
%}
function get_encrypted_bytes_in_current_segment() : int
%{
return encrypted_bytes_in_current_segment_;
%}
function get_state(is_orig: bool) : int function get_state(is_orig: bool) : int
%{ %{
if ( is_orig ) if ( is_orig )

View file

@ -0,0 +1,18 @@
0, T, 64
1, F, 64
2, T, 80
3, F, 80
4, T, 272
5, F, 48
6, T, 80
7, F, 528
8, F, 64
9, T, 176
10, F, 160
11, F, 736
12, F, 96
13, T, 64
14, F, 64
15, F, 64
16, F, 48
17, F, 128

View file

@ -0,0 +1,46 @@
0, F, 172
1, T, 44
2, F, 44
3, T, 68
4, F, 52
5, T, 148
6, F, 28
7, T, 112
8, F, 500
9, F, 44
10, T, 460
11, F, 108
12, F, 100
13, F, 36
14, F, 36
15, F, 76
16, F, 36
17, F, 84
18, F, 36
19, F, 84
20, F, 36
21, F, 36
22, F, 36
23, F, 92
24, F, 36
25, F, 108
26, F, 36
27, F, 68
28, F, 36
29, F, 36
30, F, 68
31, F, 36
32, F, 68
33, F, 36
34, F, 36
35, F, 68
36, F, 36
37, F, 92
38, F, 36
39, F, 100
40, T, 36
41, F, 44
42, F, 36
43, F, 140
44, T, 36
45, T, 60

View file

@ -0,0 +1,21 @@
# In the pcaps used here, the first encrypted packet is sent along with NEWKEYS
# message of either the client (1st pcap) or the server (2nd pcap) instead of
# separately. The "ssh_encrypted_packet" should be raised for such encrypted
# data appearing within the same tcp segment delivery as other non-encrypted
# messages.
# @TEST-EXEC: zeek -b -C -r $TRACES/ssh/ssh_client_sends_first_enc_pkt_with_newkeys.pcap %INPUT > client.out
# @TEST-EXEC: zeek -b -C -r $TRACES/ssh/ssh_server_sends_first_enc_pkt_with_newkeys.pcap %INPUT > server.out
# @TEST-EXEC: btest-diff client.out
# @TEST-EXEC: btest-diff server.out
@load base/protocols/ssh
global pkts: count = 0;
redef SSH::disable_analyzer_after_detection = F;
event ssh_encrypted_packet(c: connection, orig: bool, len: count)
{
print pkts, orig, len;
++pkts;
}