Merge remote-tracking branch 'origin/topic/vladg/ssh'

I replaced a few strcmps with either calls to std::str.compare
or with the == operator of BroString.

Also changed two of the input framework tests that did not pass
anymore after the merge. The new SSH analyzer no longer loads the
scripts that let network time run, hence those tests failed because
updates were not propagated from the threads (that took a while
to find.)

* origin/topic/vladg/ssh: (25 commits)
  SSH: Register analyzer for 22/tcp.
  SSH: Add 22/tcp to likely_server_ports
  SSH: Ignore encrypted packets by default.
  SSH: Fix some edge-cases which created BinPAC exceptions
  SSH: Add memleak btest
  SSH: Update baselines
  SSH: Added some more events for SSH2
  SSH: Intel framework integration (PUBKEY_HASH)
  Update baselines for new SSH analyzer.
  Update SSH policy scripts with new events.
  SSH: Add documentation
  Refactoring ssh-protocol.pac:
  SSH: Use the compression_algorithms const in another place.
  Some cleanup and refactoring on SSH main.bro.
  SSH: A bit of code cleanup.
  Move SSH constants to consts.pac
  SSH: Cleanup code style.
  SSH: Fix some memleaks.
  Refactored the SSH analyzer. Added supported for algorithm detection and more key exchange message types.
  Add host key support for SSH1.
  Add support for SSH1
  Move SSH analyzer to new plugin architecture.
  ...

Conflicts:
  scripts/base/protocols/ssh/main.bro
  testing/btest/Baseline/core.print-bpf-filters/output2
  testing/btest/Baseline/plugins.hooks/output

BIT-1344: #merged
This commit is contained in:
Johanna Amann 2015-03-25 10:58:13 -07:00
commit f79b5adc08
38 changed files with 1494 additions and 324 deletions

View file

@ -1 +0,0 @@
Support for Secure Shell (SSH) protocol analysis.

View file

@ -1,3 +1,2 @@
@load ./main
@load-sigs ./dpd.sig
@load-sigs ./dpd.sig

View file

@ -1,6 +1,6 @@
signature dpd_ssh_client {
ip-proto == tcp
payload /^[sS][sS][hH]-/
payload /^[sS][sS][hH]-[12]\./
requires-reverse-signature dpd_ssh_server
enable "ssh"
tcp-state originator
@ -8,6 +8,6 @@ signature dpd_ssh_client {
signature dpd_ssh_server {
ip-proto == tcp
payload /^[sS][sS][hH]-/
payload /^[sS][sS][hH]-[12]\./
tcp-state responder
}
}

View file

@ -1,15 +1,5 @@
##! Base SSH analysis script. The heuristic to blindly determine success or
##! failure for SSH connections is implemented here. At this time, it only
##! uses the size of the data being returned from the server to make the
##! heuristic determination about success of the connection.
##! Requires that :bro:id:`use_conn_size_analyzer` is set to T! The heuristic
##! is not attempted if the connection size analyzer isn't enabled.
##! Implements base functionality for SSH analysis. Generates the ssh.log file.
@load base/protocols/conn
@load base/frameworks/notice
@load base/utils/site
@load base/utils/thresholds
@load base/utils/conn-ids
@load base/utils/directions-and-hosts
module SSH;
@ -25,45 +15,63 @@ export {
uid: string &log;
## The connection's 4-tuple of endpoint addresses/ports.
id: conn_id &log;
## Indicates if the login was heuristically guessed to be
## "success", "failure", or "undetermined".
status: string &log &default="undetermined";
## Direction of the connection. If the client was a local host
## SSH major version (1 or 2)
version: count &log;
## Authentication result (T=success, F=failure, unset=unknown)
auth_success: bool &log &optional;
## Direction of the connection. If the client was a local host
## logging into an external host, this would be OUTBOUND. INBOUND
## would be set for the opposite situation.
# TODO: handle local-local and remote-remote better.
# TODO - handle local-local and remote-remote better.
direction: Direction &log &optional;
## Software string from the client.
## The client's version string
client: string &log &optional;
## Software string from the server.
## The server's version string
server: string &log &optional;
## Indicate if the SSH session is done being watched.
done: bool &default=F;
## The encryption algorithm in use
cipher_alg: string &log &optional;
## The signing (MAC) algorithm in use
mac_alg: string &log &optional;
## The compression algorithm in use
compression_alg: string &log &optional;
## The key exchange algorithm in use
kex_alg: string &log &optional;
## The server host key's algorithm
host_key_alg: string &log &optional;
## The server's key fingerprint
host_key: string &log &optional;
};
## The size in bytes of data sent by the server at which the SSH
## connection is presumed to be successful.
const authentication_data_size = 4000 &redef;
## The set of compression algorithms. We can't accurately determine
## authentication success or failure when compression is enabled.
const compression_algorithms = set("zlib", "zlib@openssh.com") &redef;
## If true, we tell the event engine to not look at further data
## packets after the initial SSH handshake. Helps with performance
## (especially with large file transfers) but precludes some
## kinds of analyses.
const skip_processing_after_detection = F &redef;
## kinds of analyses. Defaults to T.
const skip_processing_after_detection = T &redef;
## Event that is generated when the heuristic thinks that a login
## was successful.
global heuristic_successful_login: event(c: connection);
## Event that is generated when the heuristic thinks that a login
## failed.
global heuristic_failed_login: event(c: connection);
## Event that can be handled to access the :bro:type:`SSH::Info`
## record as it is sent on to the logging framework.
## Event that can be handled to access the SSH record as it is sent on
## to the logging framework.
global log_ssh: event(rec: Info);
## Event that can be handled when the analyzer sees an SSH server host
## key. This abstracts :bro:id:`SSH::ssh1_server_host_key` and
## :bro:id:`SSH::ssh2_server_host_key`.
global ssh_server_host_key: event(c: connection, hash: string);
}
redef record Info += {
# This connection has been logged (internal use)
logged: bool &default=F;
# Number of failures seen (internal use)
num_failures: count &default=0;
# Store capabilities from the first host for
# comparison with the second (internal use)
capabilities: Capabilities &optional;
};
redef record connection += {
ssh: Info &optional;
};
@ -72,133 +80,152 @@ const ports = { 22/tcp };
redef likely_server_ports += { ports };
event bro_init() &priority=5
{
Log::create_stream(SSH::LOG, [$columns=Info, $ev=log_ssh, $path="ssh"]);
{
Analyzer::register_for_ports(Analyzer::ANALYZER_SSH, ports);
}
Log::create_stream(SSH::LOG, [$columns=Info, $ev=log_ssh, $path="ssh"]);
}
function set_session(c: connection)
{
if ( ! c?$ssh )
{
local info: Info;
info$ts=network_time();
info$uid=c$uid;
info$id=c$id;
local info: SSH::Info;
info$ts = network_time();
info$uid = c$uid;
info$id = c$id;
c$ssh = info;
}
}
function check_ssh_connection(c: connection, done: bool)
{
# If already done watching this connection, just return.
if ( c$ssh$done )
return;
if ( done )
{
# If this connection is done, then we can look to see if
# this matches the conditions for a failed login. Failed
# logins are only detected at connection state removal.
if ( # Require originators and responders to have sent at least 50 bytes.
c$orig$size > 50 && c$resp$size > 50 &&
# Responders must be below 4000 bytes.
c$resp$size < authentication_data_size &&
# Responder must have sent fewer than 40 packets.
c$resp$num_pkts < 40 &&
# If there was a content gap we can't reliably do this heuristic.
c?$conn && c$conn$missed_bytes == 0 )# &&
# Only "normal" connections can count.
#c$conn?$conn_state && c$conn$conn_state in valid_states )
{
c$ssh$status = "failure";
event SSH::heuristic_failed_login(c);
}
if ( c$resp$size >= authentication_data_size )
{
c$ssh$status = "success";
event SSH::heuristic_successful_login(c);
}
}
else
{
# If this connection is still being tracked, then it's possible
# to watch for it to be a successful connection.
if ( c$resp$size >= authentication_data_size )
{
c$ssh$status = "success";
event SSH::heuristic_successful_login(c);
}
else
# This connection must be tracked longer. Let the scheduled
# check happen again.
return;
}
# Set the direction for the log.
c$ssh$direction = Site::is_local_addr(c$id$orig_h) ? OUTBOUND : INBOUND;
# Set the "done" flag to prevent the watching event from rescheduling
# after detection is done.
c$ssh$done=T;
if ( skip_processing_after_detection )
{
# Stop watching this connection, we don't care about it anymore.
skip_further_processing(c$id);
set_record_packets(c$id, F);
}
}
event heuristic_successful_login(c: connection) &priority=-5
{
Log::write(SSH::LOG, c$ssh);
}
event heuristic_failed_login(c: connection) &priority=-5
{
Log::write(SSH::LOG, c$ssh);
}
event connection_state_remove(c: connection) &priority=-5
{
if ( c?$ssh )
{
check_ssh_connection(c, T);
if ( c$ssh$status == "undetermined" )
Log::write(SSH::LOG, c$ssh);
}
}
event ssh_watcher(c: connection)
{
local id = c$id;
# don't go any further if this connection is gone already!
if ( ! connection_exists(id) )
return;
lookup_connection(c$id);
check_ssh_connection(c, F);
if ( ! c$ssh$done )
schedule +15secs { ssh_watcher(c) };
}
event ssh_server_version(c: connection, version: string) &priority=5
event ssh_server_version(c: connection, version: string)
{
set_session(c);
c$ssh$server = version;
}
event ssh_client_version(c: connection, version: string) &priority=5
event ssh_client_version(c: connection, version: string)
{
set_session(c);
c$ssh$client = version;
# The heuristic detection for SSH relies on the ConnSize analyzer.
# Don't do the heuristics if it's disabled.
if ( use_conn_size_analyzer )
schedule +15secs { ssh_watcher(c) };
if ( ( |version| > 3 ) && ( version[4] == "1" ) )
c$ssh$version = 1;
if ( ( |version| > 3 ) && ( version[4] == "2" ) )
c$ssh$version = 2;
}
event ssh_auth_successful(c: connection, auth_method_none: bool)
{
# TODO - what to do here?
if ( !c?$ssh || ( c$ssh?$auth_success && c$ssh$auth_success ) )
return;
# We can't accurately tell for compressed streams
if ( c$ssh?$compression_alg && ( c$ssh$compression_alg in compression_algorithms ) )
return;
c$ssh$auth_success = T;
if ( skip_processing_after_detection)
{
skip_further_processing(c$id);
set_record_packets(c$id, F);
}
}
event ssh_auth_successful(c: connection, auth_method_none: bool) &priority=-5
{
if ( c?$ssh && !c$ssh$logged )
{
c$ssh$logged = T;
Log::write(SSH::LOG, c$ssh);
}
}
event ssh_auth_failed(c: connection)
{
if ( !c?$ssh || ( c$ssh?$auth_success && !c$ssh$auth_success ) )
return;
# We can't accurately tell for compressed streams
if ( c$ssh?$compression_alg && ( c$ssh$compression_alg in compression_algorithms ) )
return;
c$ssh$auth_success = F;
c$ssh$num_failures += 1;
}
# Determine the negotiated algorithm
function find_alg(client_algorithms: vector of string, server_algorithms: vector of string): string
{
for ( i in client_algorithms )
for ( j in server_algorithms )
if ( client_algorithms[i] == server_algorithms[j] )
return client_algorithms[i];
return "Algorithm negotiation failed";
}
# This is a simple wrapper around find_alg for cases where client to server and server to client
# negotiate different algorithms. This is rare, but provided for completeness.
function find_bidirectional_alg(client_prefs: Algorithm_Prefs, server_prefs: Algorithm_Prefs): string
{
local c_to_s = find_alg(client_prefs$client_to_server, server_prefs$client_to_server);
local s_to_c = find_alg(client_prefs$server_to_client, server_prefs$server_to_client);
# Usually these are the same, but if they're not, return the details
return c_to_s == s_to_c ? c_to_s : fmt("To server: %s, to client: %s", c_to_s, s_to_c);
}
event ssh_capabilities(c: connection, cookie: string, capabilities: Capabilities)
{
if ( !c?$ssh || ( c$ssh?$capabilities && c$ssh$capabilities$is_server == capabilities$is_server ) )
return;
if ( !c$ssh?$capabilities )
{
c$ssh$capabilities = capabilities;
return;
}
local client_caps = capabilities$is_server ? c$ssh$capabilities : capabilities;
local server_caps = capabilities$is_server ? capabilities : c$ssh$capabilities;
c$ssh$cipher_alg = find_bidirectional_alg(client_caps$encryption_algorithms,
server_caps$encryption_algorithms);
c$ssh$mac_alg = find_bidirectional_alg(client_caps$mac_algorithms,
server_caps$mac_algorithms);
c$ssh$compression_alg = find_bidirectional_alg(client_caps$compression_algorithms,
server_caps$compression_algorithms);
c$ssh$kex_alg = find_alg(client_caps$kex_algorithms, server_caps$kex_algorithms);
c$ssh$host_key_alg = find_alg(client_caps$server_host_key_algorithms,
server_caps$server_host_key_algorithms);
}
event connection_state_remove(c: connection) &priority=-5
{
if ( c?$ssh && !c$ssh$logged && c$ssh?$client && c$ssh?$server )
{
c$ssh$logged = T;
Log::write(SSH::LOG, c$ssh);
}
}
function generate_fingerprint(c: connection, key: string)
{
if ( !c?$ssh )
return;
local lx = str_split(md5_hash(key), vector(2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30));
lx[0] = "";
c$ssh$host_key = sub(join_string_vec(lx, ":"), /:/, "");
}
event ssh1_server_host_key(c: connection, p: string, e: string) &priority=5
{
generate_fingerprint(c, e + p);
}
event ssh2_server_host_key(c: connection, key: string) &priority=5
{
generate_fingerprint(c, key);
}