More extensive base script updating.

* This is basically another checkpoint, but the difference
  is that in this one all.bro loads just about all of the
  new scripts and functionality.
This commit is contained in:
Seth Hall 2011-04-20 23:14:54 -04:00
parent 11ca973a10
commit 5a868eefda
17 changed files with 583 additions and 406 deletions

View file

@ -1,9 +1,16 @@
# This script only aims at loading all of the base analysis scripts.
@load conn
@load dns
@load ftp
@load http
@load known-services
@load irc
@load known-hosts
@load ssh
@load known-services
@load smtp
@load ssl
@load ssh
@load mime
@load software
@load weird

View file

@ -259,12 +259,12 @@ type pcap_packet: record {
# GeoIP support.
type geo_location: record {
country_code: string &default="";
region: string &default="";
city: string &default="";
latitude: double &default=0.0;
longitude: double &default=0.0;
};
country_code: string &optional;
region: string &optional;
city: string &optional;
latitude: double &optional;
longitude: double &optional;
} &log;
type entropy_test_result: record {
entropy: double;
@ -928,7 +928,7 @@ type X509: record {
issuer: string;
subject: string;
orig_addr: addr;
};
} &log;
type http_stats_rec: record {
num_requests: count;

View file

@ -11,6 +11,7 @@ export {
trans_id: count &log &optional;
query: string &log &optional;
qtype: count &log &optional;
qtype_name: string &log &optional;
qclass: count &log &optional;
rcode: count &log &optional;
QR: bool &log &default=F;
@ -102,9 +103,9 @@ function set_session(c: connection, msg: dns_msg, is_query: bool)
if ( info?$total_answers &&
info$total_answers != msg$num_answers + msg$num_addl + msg$num_auth )
{
print "the total number of answers changed midstream on a dns response.";
print info;
print msg;
#print "the total number of answers changed midstream on a dns response.";
#print info;
#print msg;
}
else
{
@ -122,7 +123,15 @@ event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qcla
c$dns$RD = msg$RD;
c$dns$TC = msg$TC;
c$dns$qtype = qtype;
c$dns$qtype_name = query_types[qtype];
c$dns$qclass = qclass;
# Decode netbios name queries
# Note: I'm ignoring the name type for now. Not sure if this should be
# worked into the query/response in some fashion.
if ( c$id$resp_p == 137/udp )
query = decode_netbios_name(query);
c$dns$query = query;
}
@ -156,6 +165,24 @@ event dns_AAAA_reply(c: connection, msg: dns_msg, ans: dns_answer, a: addr,
add c$dns$replies[fmt("%s", a)];
}
event dns_NS_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string) &priority=5
{
set_session(c, msg, F);
if ( ! c$dns?$replies )
c$dns$replies = set();
add c$dns$replies[name];
}
event dns_CNAME_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string) &priority=5
{
set_session(c, msg, F);
if ( ! c$dns?$replies )
c$dns$replies = set();
add c$dns$replies[name];
}
event dns_MX_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string,
preference: count) &priority=5
@ -176,6 +203,43 @@ event dns_PTR_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string)
add c$dns$replies[name];
}
event dns_SOA_reply(c: connection, msg: dns_msg, ans: dns_answer, soa: dns_soa)
{
set_session(c, msg, F);
if ( ! c$dns?$replies )
c$dns$replies = set();
add c$dns$replies[soa$mname];
}
event dns_WKS_reply(c: connection, msg: dns_msg, ans: dns_answer)
{
set_session(c, msg, F);
}
event dns_SRV_reply(c: connection, msg: dns_msg, ans: dns_answer)
{
set_session(c, msg, F);
}
# TODO: figure out how to handle these
#event dns_EDNS(c: connection, msg: dns_msg, ans: dns_answer)
# {
#
# }
#
#event dns_EDNS_addl(c: connection, msg: dns_msg, ans: dns_edns_additional)
# {
#
# }
#
#event dns_TSIG_addl(c: connection, msg: dns_msg, ans: dns_tsig_additional)
# {
#
# }
event dns_rejected(c: connection, msg: dns_msg,
query: string, qtype: count, qclass: count)
{

View file

@ -1,10 +1,32 @@
##! Activates port-independent protocol detection.
@load functions
@load signatures
module DPD;
# Add the DPD signatures.
redef signature_files += "dpd.sig";
redef enum Log::ID += { DPD };
export {
type Info: record {
ts: time &log;
id: conn_id &log;
proto: transport_proto &log;
analyzer: string &log;
failure_reason: string &log;
};
}
redef record connection += {
dpd: Info &optional;
};
event bro_init()
{
Log::create_stream(DPD, [$columns=Info]);
for ( a in dpd_config )
{
for ( p in dpd_config[a]$ports )
@ -16,16 +38,25 @@ event bro_init()
}
}
event protocol_confirmation(c: connection, atype: count, aid: count)
event protocol_confirmation(c: connection, atype: count, aid: count) &priority=10
{
if ( fmt("-%s",analyzer_name(atype)) in c$service )
delete c$service[fmt("-%s",analyzer_name(atype))];
add c$service[analyzer_name(atype)];
}
event protocol_violation(c: connection, atype: count, aid: count,
reason: string) &priority = 10
reason: string) &priority=10
{
if ( analyzer_name(atype) in c$service )
delete c$service[analyzer_name(atype)];
add c$service[fmt("-%s",analyzer_name(atype))];
Log::write(DPD, [$ts=network_time(),
$id=c$id,
$proto=get_conn_transport_proto(c$id),
$analyzer=analyzer_name(atype),
$failure_reason=reason]);
}

View file

@ -3,6 +3,8 @@
@load http/base
@load software
module HTTP;
redef enum Software::Type += {
WEB_SERVER,
WEB_BROWSER,
@ -32,7 +34,7 @@ event http_header(c: connection, is_orig: bool, name: string, value: string) &pr
{
# Flash doesn't include it's name so we'll add it here since it
# simplifies the version parsing.
value = cat("Flash ", value);
value = cat("Flash/", value);
local flash_version = Software::parse(value, c$id$orig_h, WEB_BROWSER_PLUGIN);
Software::found(c$id, flash_version);
}

View file

@ -8,6 +8,8 @@ module IRC;
redef enum Log::ID += { IRC };
export {
type Tags: enum { EMPTY };
type Info: record {
ts: time &log;
id: conn_id &log;
@ -18,6 +20,7 @@ export {
command: string &log &optional;
value: string &log &optional;
addl: string &log &optional;
tags: set[Tags] &log &default=set();
};
const logged_commands = set("JOIN", "DCC SEND");
@ -148,10 +151,3 @@ event irc_join_message(c: connection, info_list: irc_join_list) &priority=-5
Log::write(IRC, c$irc);
}
}
event expected_connection_seen(c: connection, a: count) &priority=10
{
local id = c$id;
if ( [id$resp_h, id$resp_p] in dcc_expected_transfers )
add c$service["ftp-data"];
}

View file

@ -12,26 +12,75 @@
module IRC;
export {
redef enum Tags += { EXTRACTED_FILE };
## Pattern of file mime types to extract from IRC DCC file transfers.
const extract_file_types = /NO_DEFAULT/ &redef;
## The on-disk prefix for files to be extracted from IRC DCC file transfers.
const extraction_prefix = "irc-dcc-item" &redef;
redef record Info += {
file_name: string &optional;
file_size: count &optional;
dcc_file_name: string &log &optional;
dcc_file_size: count &log &optional;
dcc_mime_type: string &log &optional;
## The file handle for the file to be extracted
extraction_file: file &log &optional;
## A boolean to indicate if the current file transfer shoudl be transfered.
extract_file: bool &default=F;
};
}
global dcc_expected_transfers: table[addr, port] of Info = table();
event file_transferred(c: connection, prefix: string, descr: string,
mime_type: string) &priority=5
mime_type: string) &priority=3
{
local id = c$id;
if ( [id$resp_h, id$resp_p] in dcc_expected_transfers )
if ( [id$resp_h, id$resp_p] !in dcc_expected_transfers )
return;
local irc = dcc_expected_transfers[id$resp_h, id$resp_p];
irc$dcc_mime_type = mime_type;
if ( extract_file_types in mime_type )
irc$extract_file = T;
if ( irc$extract_file )
{
add irc$tags[EXTRACTED_FILE];
irc$extraction_file = open(fmt("%s.%s", extraction_prefix, id_string(c$id)));
}
}
event file_transferred(c: connection, prefix: string, descr: string,
mime_type: string) &priority=-4
{
local id = c$id;
if ( [id$resp_h, id$resp_p] !in dcc_expected_transfers )
return;
local irc = dcc_expected_transfers[id$resp_h, id$resp_p];
if ( irc$extract_file && irc?$extraction_file )
set_contents_file(id, CONTENTS_RESP, irc$extraction_file);
# Delete these values in case another DCC transfer
# happens during the IRC session.
# TODO: uncomment these when this operator works
# delete irc$extract_file;
# delete irc$extraction_file;
# delete irc$dcc_file_name;
# delete irc$dcc_file_size;
# delete irc$dcc_mime_type;
delete dcc_expected_transfers[id$resp_h, id$resp_p];
local fh = open("irc-dcc-item");
set_contents_file(id, CONTENTS_RESP, fh);
}
}
event irc_server(c: connection, prefix: string, data: string) &priority=5
{
local parts = split_all(data, / /);
@ -45,11 +94,17 @@ event irc_server(c: connection, prefix: string, data: string) &priority=5
c$irc$command = "DCC SEND";
#local ex_h = count_to_v4_addr(to_count(parts[|parts|-4]));
local ex_p = to_port(to_count(parts[|parts|-2]), tcp);
c$irc$file_name = parts[|parts|-6];
c$irc$file_size = to_count(parts[|parts|]);
c$irc$dcc_file_name = parts[|parts|-6];
c$irc$dcc_file_size = to_count(parts[|parts|]);
#print fmt("file! %s->%s:%d", c$id$orig_h, ex_h, ex_p);
#expect_connection(c$id$orig_h, ex_h, ex_p, ANALYZER_FILE, 5 min);
#dcc_expected_transfers[ex_h, ex_p];
}
}
event expected_connection_seen(c: connection, a: count) &priority=10
{
local id = c$id;
if ( [id$resp_h, id$resp_p] in dcc_expected_transfers )
add c$service["irc-dcc-data"];
}

View file

@ -21,13 +21,15 @@ export {
};
type Info: record {
ts: time &log &optional;
id: conn_id &log &optional; # connection-ID, if we don't have a connection handy
note: Type &log;
msg: string &default="" &log;
sub: string &log &optional; # sub-message
conn: connection &log &optional; # connection associated with notice
iconn: icmp_conn &log &optional; # associated ICMP "connection"
id: conn_id &log &optional; # connection-ID, if we don't have a connection handy
src: addr &log &optional; # source address, if we don't have a connection
dst: addr &log &optional; # destination address
p: port &log &optional; # associated port, if we don't have a conn.
@ -210,6 +212,8 @@ function execute_with_notice(cmd: string, n: Notice::Info)
function notice(n: Notice::Info)
{
# Fill in some defaults.
n$ts = network_time();
if ( ! n?$id && n?$conn )
n$id = n$conn$id;

View file

@ -0,0 +1,7 @@
@load smtp/base
export {
}

View file

@ -1,3 +1,6 @@
module SMTP;
function find_address_in_smtp_header(header: string): string
{
local ips = find_ip_addresses(header);

View file

@ -118,6 +118,7 @@ function parse_mozilla(unparsed_version: string,
if ( 2 in parts )
v = parse(parts[2], host, software_type)$version;
}
return [$ts=network_time(), $host=host, $name=software_name, $version=v,
$unparsed_version=unparsed_version];
}

View file

@ -1,274 +1,2 @@
@load functions
@load notice
@load software
module SSH;
redef enum Notice::Type += {
SSH_Login,
SSH_PasswordGuessing,
SSH_LoginByPasswordGuesser,
SSH_Login_From_Interesting_Hostname,
SSH_Bytecount_Inconsistency,
};
redef enum Log::ID += { SSH };
redef enum Software::Type += {
SSH_SERVER,
SSH_CLIENT,
};
# Configure DPD and the packet filter
redef capture_filters += { ["ssh"] = "tcp port 22" };
redef dpd_config += { [ANALYZER_SSH] = [$ports = set(22/tcp)] };
export {
type Log: record {
ts: time;
id: conn_id;
status: string &default="";
direction: string &default="";
remote_location: geo_location &optional;
client: string &default="";
server: string &default="";
resp_size: count &default=0;
};
# This is the prototype for the event that the logging framework tries
# to generate if there is a handler for it.
global log: event(rec: Log);
const password_guesses_limit = 30 &redef;
# The size in bytes at which the SSH connection is presumed to be
# successful.
const authentication_data_size = 5500 &redef;
# The amount of time to remember presumed non-successful logins to build
# model of a password guesser.
const guessing_timeout = 30 mins &redef;
# If you want to lookup and log geoip data in the event of a failed login.
const log_geodata_on_failure = F &redef;
# The set of countries for which you'd like to throw notices upon successful login
# requires Bro compiled with libGeoIP support
const watched_countries: set[string] = {"RO"} &redef;
# Strange/bad host names to originate successful SSH logins
const interesting_hostnames =
/^d?ns[0-9]*\./ |
/^smtp[0-9]*\./ |
/^mail[0-9]*\./ |
/^pop[0-9]*\./ |
/^imap[0-9]*\./ |
/^www[0-9]*\./ |
/^ftp[0-9]*\./ &redef;
# This is a table with orig subnet as the key, and subnet as the value.
const ignore_guessers: table[subnet] of subnet &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 (e.g., tracking connection size).
const skip_processing_after_detection = F &redef;
# Keeps count of how many rejections a host has had
global password_rejections: table[addr] of TrackCount
&default=default_track_count
&write_expire=guessing_timeout;
# Keeps track of hosts identified as guessing passwords
# TODO: guessing_timeout doesn't work correctly here. If a user redefs
# the variable, it won't take effect.
global password_guessers: set[addr] &read_expire=guessing_timeout+1hr;
# The list of active SSH connections and the associated session info.
global active_conns: table[conn_id] of Log &read_expire=2mins;
global log_ssh: event(rec: Log);
}
function local_filter(rec: record { id: conn_id; } ): bool
{
return is_local_addr(rec$id$resp_h);
}
event bro_init()
{
# Create the stream.
# First argument is the ID for the stream.
# Second argument is the log record type.
Log::create_stream(SSH, [$columns=SSH::Log, $ev=log_ssh]);
# Add a default filter that simply logs everything to "ssh.log" using the default writer.
Log::add_default_filter(SSH);
}
event check_ssh_connection(c: connection, done: bool)
{
# If this is no longer a known SSH connection, just return.
if ( c$id !in active_conns )
return;
# If this is still a live connection and the byte count has not
# crossed the threshold, just return and let the resheduled check happen later.
if ( !done && c$resp$size < authentication_data_size )
return;
# Make sure the server has sent back more than 50 bytes to filter out
# hosts that are just port scanning. Nothing is ever logged if the server
# doesn't send back at least 50 bytes.
if ( c$resp$size < 50 )
return;
local ssh_log = active_conns[c$id];
local status = "failure";
local direction = is_local_addr(c$id$orig_h) ? "to" : "from";
local location: geo_location;
if ( done && c$resp$size < authentication_data_size )
{
# presumed failure
if ( log_geodata_on_failure )
location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h);
if ( c$id$orig_h !in password_rejections )
password_rejections[c$id$orig_h] = default_track_count(c$id$orig_h);
# Track the number of rejections
if ( !(c$id$orig_h in ignore_guessers &&
c$id$resp_h in ignore_guessers[c$id$orig_h]) )
++password_rejections[c$id$orig_h]$n;
if ( default_check_threshold(password_rejections[c$id$orig_h]) )
{
add password_guessers[c$id$orig_h];
NOTICE([$note=SSH_PasswordGuessing,
$conn=c,
$msg=fmt("SSH password guessing by %s", c$id$orig_h),
$sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n),
$n=password_rejections[c$id$orig_h]$n]);
}
}
# TODO: This is to work around a quasi-bug in Bro which occasionally
# causes the byte count to be oversized.
# Watch for Gregors work that adds an actual counter of bytes transferred.
else if ( c$resp$size < 20000000 )
{
# presumed successful login
status = "success";
location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h);
if ( password_rejections[c$id$orig_h]$n > password_guesses_limit &&
c$id$orig_h !in password_guessers)
{
add password_guessers[c$id$orig_h];
NOTICE([$note=SSH_LoginByPasswordGuesser,
$conn=c,
$n=password_rejections[c$id$orig_h]$n,
$msg=fmt("Successful SSH login by password guesser %s", c$id$orig_h),
$sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n)]);
}
local message = fmt("SSH login %s %s \"%s\" \"%s\" %f %f %s (triggered with %d bytes)",
direction, location$country_code, location$region, location$city,
location$latitude, location$longitude,
id_string(c$id), c$resp$size);
# TODO: rewrite the message once a location variable can be put in notices
NOTICE([$note=SSH_Login,
$conn=c,
$msg=message,
$sub=location$country_code]);
# Check to see if this login came from an interesting hostname
when( local hostname = lookup_addr(c$id$orig_h) )
{
if ( interesting_hostnames in hostname )
{
NOTICE([$note=SSH_Login_From_Interesting_Hostname,
$conn=c,
$msg=fmt("Strange login from %s", hostname),
$sub=hostname]);
}
}
}
else if ( c$resp$size >= 200000000 )
{
NOTICE([$note=SSH_Bytecount_Inconsistency,
$conn=c,
$msg="During byte counting in SSH analysis, an overly large value was seen.",
$sub=fmt("%d",c$resp$size)]);
}
ssh_log$remote_location = location;
ssh_log$status = status;
ssh_log$direction = direction;
ssh_log$resp_size = c$resp$size;
Log::write(SSH, ssh_log);
delete active_conns[c$id];
# Stop watching this connection, we don't care about it anymore.
if ( skip_processing_after_detection )
{
skip_further_processing(c$id);
set_record_packets(c$id, F);
}
}
event connection_state_remove(c: connection)
{
event check_ssh_connection(c, T);
}
event ssh_watcher(c: connection)
{
local id = c$id;
# don't go any further if this connection is gone already!
if ( !connection_exists(id) )
{
delete active_conns[id];
return;
}
event check_ssh_connection(c, F);
if ( c$id in active_conns )
schedule +15secs { ssh_watcher(c) };
}
event ssh_client_version(c: connection, version: string)
{
if ( c$id in active_conns )
active_conns[c$id]$client = version;
else
{
active_conns[c$id] = [$ts=c$start_time, $id=c$id];
schedule +15secs { ssh_watcher(c) };
}
# Get rid of the protocol information when passing to the software framework.
local cleaned_version = sub(version, /^SSH[0-9\.\-]+/, "");
local si = Software::parse(cleaned_version, c$id$orig_h, SSH_CLIENT);
Software::found(c, si);
}
event ssh_server_version(c: connection, version: string)
{
if ( c$id in active_conns )
active_conns[c$id]$server = version;
# Get rid of the protocol information when passing to the software framework.
local cleaned_version = sub(version, /SSH[0-9\.\-]{2,}/, "");
local si = Software::parse(cleaned_version, c$id$resp_h, SSH_SERVER);
Software::found(c, si);
}
event protocol_confirmation(c: connection, atype: count, aid: count)
{
if ( atype == ANALYZER_SSH )
{
active_conns[c$id] = [$ts=c$start_time, $id=c$id];
schedule +15secs { ssh_watcher(c) };
}
}
@load ssh/base
@load ssh/software

263
policy/ssh/base.bro Normal file
View file

@ -0,0 +1,263 @@
@load functions
@load notice
module SSH;
redef enum Notice::Type += {
SSH_Login,
SSH_PasswordGuessing,
SSH_LoginByPasswordGuesser,
SSH_Login_From_Interesting_Hostname,
SSH_Bytecount_Inconsistency,
};
redef enum Log::ID += { SSH };
# Configure DPD and the packet filter
redef capture_filters += { ["ssh"] = "tcp port 22" };
redef dpd_config += { [ANALYZER_SSH] = [$ports = set(22/tcp)] };
export {
type Info: record {
ts: time &log;
id: conn_id &log;
status: string &log &optional;
direction: string &log &optional;
remote_location: geo_location &log &optional;
client: string &log &optional;
server: string &log &optional;
resp_size: count &log &default=0;
## Indicate if the SSH session is done being watched.
done: bool &default=F;
};
const password_guesses_limit = 30 &redef;
# The size in bytes at which the SSH connection is presumed to be
# successful.
const authentication_data_size = 5500 &redef;
# The amount of time to remember presumed non-successful logins to build
# model of a password guesser.
const guessing_timeout = 30 mins &redef;
# If you want to lookup and log geoip data in the event of a failed login.
const log_geodata_on_failure = F &redef;
# The set of countries for which you'd like to throw notices upon successful login
# requires Bro compiled with libGeoIP support
const watched_countries: set[string] = {"RO"} &redef;
# Strange/bad host names to originate successful SSH logins
const interesting_hostnames =
/^d?ns[0-9]*\./ |
/^smtp[0-9]*\./ |
/^mail[0-9]*\./ |
/^pop[0-9]*\./ |
/^imap[0-9]*\./ |
/^www[0-9]*\./ |
/^ftp[0-9]*\./ &redef;
# This is a table with orig subnet as the key, and subnet as the value.
const ignore_guessers: table[subnet] of subnet &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 (e.g., tracking connection size).
const skip_processing_after_detection = F &redef;
# Keeps count of how many rejections a host has had
global password_rejections: table[addr] of TrackCount
&default=default_track_count
&write_expire=guessing_timeout;
# Keeps track of hosts identified as guessing passwords
# TODO: guessing_timeout doesn't work correctly here. If a user redefs
# the variable, it won't take effect.
global password_guessers: set[addr] &read_expire=guessing_timeout+1hr;
global log_ssh: event(rec: Info);
}
# TODO: move this elsewhere
function local_filter(rec: record { id: conn_id; } ): bool
{
return is_local_addr(rec$id$resp_h);
}
redef record connection += {
ssh: Info &optional;
};
event bro_init()
{
Log::create_stream(SSH, [$columns=Info, $ev=log_ssh]);
}
function set_session(c: connection)
{
if ( ! c?$ssh )
{
local info: Info;
info$ts=network_time();
info$id=c$id;
c$ssh = info;
}
}
function check_ssh_connection(c: connection, done: bool)
{
# If done watching this connection, just return.
if ( c$ssh$done )
return;
# If this is still a live connection and the byte count has not
# crossed the threshold, just return and let the resheduled check happen later.
if ( !done && c$resp$size < authentication_data_size )
return;
# Make sure the server has sent back more than 50 bytes to filter out
# hosts that are just port scanning. Nothing is ever logged if the server
# doesn't send back at least 50 bytes.
if ( c$resp$size < 50 )
return;
local status = "failure";
local direction = is_local_addr(c$id$orig_h) ? "to" : "from";
local location: geo_location;
if ( done && c$resp$size < authentication_data_size )
{
# presumed failure
if ( log_geodata_on_failure )
location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h);
if ( c$id$orig_h !in password_rejections )
password_rejections[c$id$orig_h] = default_track_count(c$id$orig_h);
# Track the number of rejections
if ( !(c$id$orig_h in ignore_guessers &&
c$id$resp_h in ignore_guessers[c$id$orig_h]) )
++password_rejections[c$id$orig_h]$n;
if ( default_check_threshold(password_rejections[c$id$orig_h]) )
{
add password_guessers[c$id$orig_h];
NOTICE([$note=SSH_PasswordGuessing,
$conn=c,
$msg=fmt("SSH password guessing by %s", c$id$orig_h),
$sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n),
$n=password_rejections[c$id$orig_h]$n]);
}
}
# TODO: This is to work around a quasi-bug in Bro which occasionally
# causes the byte count to be oversized.
# Watch for Gregors work that adds an actual counter of bytes transferred.
else if ( c$resp$size < 20000000 )
{
# presumed successful login
status = "success";
location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h);
if ( password_rejections[c$id$orig_h]$n > password_guesses_limit &&
c$id$orig_h !in password_guessers)
{
add password_guessers[c$id$orig_h];
NOTICE([$note=SSH_LoginByPasswordGuesser,
$conn=c,
$n=password_rejections[c$id$orig_h]$n,
$msg=fmt("Successful SSH login by password guesser %s", c$id$orig_h),
$sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n)]);
}
local message = fmt("SSH login %s %s \"%s\" \"%s\" %f %f %s (triggered with %d bytes)",
direction, location$country_code, location$region, location$city,
location$latitude, location$longitude,
id_string(c$id), c$resp$size);
# TODO: rewrite the message once a location variable can be put in notices
NOTICE([$note=SSH_Login,
$conn=c,
$msg=message,
$sub=location$country_code]);
# Check to see if this login came from an interesting hostname
when( local hostname = lookup_addr(c$id$orig_h) )
{
if ( interesting_hostnames in hostname )
{
NOTICE([$note=SSH_Login_From_Interesting_Hostname,
$conn=c,
$msg=fmt("Strange login from %s", hostname),
$sub=hostname]);
}
}
}
else if ( c$resp$size >= 200000000 )
{
NOTICE([$note=SSH_Bytecount_Inconsistency,
$conn=c,
$msg="During byte counting in SSH analysis, an overly large value was seen.",
$sub=fmt("%d",c$resp$size)]);
}
c$ssh$remote_location = location;
c$ssh$status = status;
c$ssh$direction = direction;
c$ssh$resp_size = c$resp$size;
Log::write(SSH, c$ssh);
# Set the "done" flag to prevent the watching event from rescheduling
# after detection is done.
c$ssh$done;
# Stop watching this connection, we don't care about it anymore.
if ( skip_processing_after_detection )
{
skip_further_processing(c$id);
set_record_packets(c$id, F);
}
}
event connection_state_remove(c: connection)
{
if ( c?$ssh )
check_ssh_connection(c, T);
}
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;
check_ssh_connection(c, F);
if ( ! c$ssh$done )
schedule +15secs { ssh_watcher(c) };
}
event ssh_server_version(c: connection, version: string) &priority=5
{
set_session(c);
c$ssh$server = version;
}
event ssh_client_version(c: connection, version: string) &priority=5
{
set_session(c);
c$ssh$client = version;
schedule +15secs { ssh_watcher(c) };
}
#event protocol_confirmation(c: connection, atype: count, aid: count) &priority=5
# {
# if ( atype == ANALYZER_SSH )
# {
# if ( ! c?$ssh )
# schedule +15secs { ssh_watcher(c) };
# set_session(c);
# }
# }

25
policy/ssh/software.bro Normal file
View file

@ -0,0 +1,25 @@
@load ssh/base
@load software
module SSH;
redef enum Software::Type += {
SSH_SERVER,
SSH_CLIENT,
};
event ssh_client_version(c: connection, version: string) &priority=4
{
# Get rid of the protocol information when passing to the software framework.
local cleaned_version = sub(version, /^SSH[0-9\.\-]+/, "");
local si = Software::parse(cleaned_version, c$id$orig_h, SSH_CLIENT);
Software::found(c$id, si);
}
event ssh_server_version(c: connection, version: string) &priority=4
{
# Get rid of the protocol information when passing to the software framework.
local cleaned_version = sub(version, /SSH[0-9\.\-]{2,}/, "");
local si = Software::parse(cleaned_version, c$id$resp_h, SSH_SERVER);
Software::found(c$id, si);
}

View file

@ -465,7 +465,7 @@ const ssl_cipher_desc: table[count] of string = {
[SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA] = "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA",
[SSL_RSA_FIPS_WITH_DES_CBC_SHA_2] = "SSL_RSA_FIPS_WITH_DES_CBC_SHA_2",
[SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA_2] = "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA_2",
};
} &default="UNKNOWN";
# --- the following sets are provided for convenience

View file

@ -9,7 +9,7 @@
module SSL;
redef enum Notice::Type += {
# Blanket X509 error
## Blanket X509 error
SSL_X509Violation,
## Session data not consistent with connection
SSL_SessConIncon,
@ -18,33 +18,31 @@ redef enum Notice::Type += {
redef enum Log::ID += { SSL };
export {
type Log: record {
ts: time;
id: conn_id;
## This is the session ID. It's optional because SSLv2 doesn't have it.
sid: string &optional;
# TODO: dga 3/11 The following 2 fields are not yet picked up
#not_valid_before: time; # certificate valid time constraint
#not_valid_after: time; # certificate valid time constraint
version: string &default="UNKNOWN"; # version number
weak_client_cipher: bool &default = F; # true if client offered insecure ciphers
weak_server_cipher: bool &default = F; # true if server offered insecure ciphers
weak_cipher_agreed: bool &default = F; # true if insecure cipher agreed upon for use
version: string &default=""; # version associated with connection
client_cert: X509 &optional; # client certificate
server_cert: X509 &optional; # server certificate
handshake_cipher: string &default=""; # agreed-upon cipher for session/conn.
type Tags: enum {
WEAK_CLIENT_CIPHER,
WEAK_SERVER_CIPHER,
WEAK_CIPHER_AGREED
};
type ConnectionInfo: record {
log: Log;
type Info: record {
ts: time &log;
id: conn_id &log;
## This is the session ID. It's optional because SSLv2 doesn't have it.
sid: string &log &optional;
# TODO: dga 3/11 The following 2 fields are not yet picked up
#not_valid_before: time &log &optional; ##< certificate valid time constraint
#not_valid_after: time &log &optional; ##< certificate valid time constraint
version: string &log &default="UNKNOWN"; ##< SSL/TLS version number
client_cert: X509 &log &optional; ##< client certificate
server_cert: X509 &log &optional; ##< server certificate
handshake_cipher: string &log &optional; ##< agreed-upon cipher for session/conn.
tags: set[Tags] &log;
};
type SessionInfo: record {
## This tracks the number of times this session has been reused.
num_reuse: count &default=1;
## This tracks the number of times this session has been used.
num_use: count &default=1;
version: string &default=""; # version associated with connection
client_cert: X509 &optional; # client certificate
@ -89,13 +87,10 @@ export {
## The list of all detected X509 certs.
global certs: set[addr, port, string] &create_expire=1day &synchronized;
## All active SSL/TLS connections
global active_conns: table[conn_id] of ConnectionInfo &read_expire=1hr;
## Recent TLS session IDs
global recent_sessions: table[string] of SessionInfo &read_expire=1hr;
global log_ssl: event(rec: Log);
global log_ssl: event(rec: Info);
## This is the set of SSL/TLS ciphers are are seen as weak to attack.
const weak_ciphers: set[count] = {
@ -140,6 +135,10 @@ export {
}
redef record connection += {
ssl: Info &optional;
};
# NOTE: this is a 'local' port format for your site
# --- well-known ports for ssl ---------
redef capture_filters += {
@ -167,8 +166,7 @@ redef dpd_config += {
event bro_init()
{
Log::create_stream(SSL, [$columns=SSL::Log, $ev=log_ssl] );
Log::add_default_filter(SSL);
Log::create_stream(SSL, [$columns=Info, $ev=log_ssl] );
# The event engine will generate a run-time if this fails for
# reasons other than that the directory already exists.
@ -199,24 +197,16 @@ const x509_hot_errors: set[int] = {
};
@endif
function ssl_get_cipher_name(cipherSuite: count): string
{
return cipherSuite in ssl_cipher_desc ?
ssl_cipher_desc[cipherSuite] : "UNKNOWN";
}
function get_connection_info(c: connection): ConnectionInfo
function set_session(c: connection)
{
local id = c$id;
if ( id in active_conns )
return active_conns[id];
else
if ( ! c?$ssl )
{
local log: Log = [$ts=network_time(), $id=id];
local conn_info: ConnectionInfo = [$log=log];
active_conns[id] = conn_info;
return conn_info;
local info: Info;
info$ts=network_time();
info$id=id;
c$ssl = info;
}
}
@ -231,44 +221,43 @@ function get_session_info(s: SSL_sessionID): SessionInfo
event ssl_certificate(c: connection, cert: X509, is_server: bool)
{
#if ( is_server )
# event protocol_confirmation(c, ANALYZER_SSL, 0);
local conn = get_connection_info(c);
print "hello?";
set_session(c);
if ( [c$id$resp_h, c$id$resp_p, cert$subject] !in certs )
add certs[c$id$resp_h, c$id$resp_p, cert$subject];
if( is_server )
{
conn$log$server_cert = cert;
c$ssl$server_cert = cert;
# We have not filled in the field for the master session
# for this connection. Do it now, but only if this is not a
# SSLv2 connection (no session information in that case).
if ( conn$log$sid in recent_sessions &&
recent_sessions[conn$log$sid]?$server_cert )
recent_sessions[conn$log$sid]$server_cert$subject = cert$subject;
if ( c$ssl$sid in recent_sessions &&
recent_sessions[c$ssl$sid]?$server_cert )
recent_sessions[c$ssl$sid]$server_cert$subject = cert$subject;
}
else
{
conn$log$client_cert = cert;
c$ssl$client_cert = cert;
}
}
event ssl_conn_attempt(c: connection, version: count, ciphers: cipher_suites_list)
{
local conn = get_connection_info(c);
set_session(c);
conn$log$version = version_strings[version];
c$ssl$version = version_strings[version];
for ( cs in ciphers )
{
if ( cs in weak_ciphers )
{
conn$log$weak_client_cipher = T;
add c$ssl$tags[WEAK_CLIENT_CIPHER];
#event ssl_conn_weak(
# fmt("SSL client supports weak cipher: %s (0x%x)",
# ssl_get_cipher_name(cs), cs), c);
# ssl_cipher_desc[cs], cs), c);
}
}
}
@ -276,7 +265,7 @@ event ssl_conn_attempt(c: connection, version: count, ciphers: cipher_suites_lis
event ssl_conn_server_reply(c: connection, version: count,
ciphers: cipher_suites_list)
{
local conn = get_connection_info(c);
set_session(c);
#conn$log$version = version_strings[version];
@ -284,31 +273,27 @@ event ssl_conn_server_reply(c: connection, version: count,
{
if ( cs in weak_ciphers )
{
conn$log$weak_server_cipher = T;
#event ssl_conn_weak(
# fmt("SSLv2 server supports weak cipher: %s (0x%x)",
# ssl_get_cipher_name(cs), cs), c);
add c$ssl$tags[WEAK_SERVER_CIPHER];
}
}
}
event ssl_conn_established(c: connection, version: count, cipher_suite: count) &priority=1
{
local conn = get_connection_info(c);
set_session(c);
conn$log$ts = network_time();
#conn$log$version = version_strings[version];
c$ssl$version = version_strings[version];
if ( cipher_suite in weak_ciphers )
conn$log$weak_cipher_agreed = T;
add c$ssl$tags[WEAK_CIPHER_AGREED];
# log the connection
Log::write(SSL, conn$log);
Log::write(SSL, c$ssl);
}
event process_X509_extensions(c: connection, ex: X509_extension)
{
local conn = get_connection_info(c);
set_session(c);
#local msg = fmt( "%.6f X.509 extensions: ", network_time() );
#for ( i in ex )
@ -317,33 +302,34 @@ event process_X509_extensions(c: connection, ex: X509_extension)
event ssl_session_insertion(c: connection, id: SSL_sessionID)
{
set_session(c);
local cid = c$id;
local conn = get_connection_info(c);
conn$log$sid=md5_hash(id);
c$ssl$sid=md5_hash(id);
# This will create a new session if one doesn't already exist.
local session = get_session_info(id);
session$version=conn$log$version;
if ( conn$log?$client_cert ) session$client_cert=conn$log$client_cert;
if ( conn$log?$server_cert ) session$server_cert=conn$log$server_cert;
session$handshake_cipher=conn$log$handshake_cipher;
session$version=c$ssl$version;
if ( c$ssl?$client_cert ) session$client_cert=c$ssl$client_cert;
if ( c$ssl?$server_cert ) session$server_cert=c$ssl$server_cert;
if ( c$ssl?$handshake_cipher )session$handshake_cipher=c$ssl$handshake_cipher;
}
event ssl_conn_reused(c: connection, session_id: SSL_sessionID)
{
local conn = get_connection_info(c);
set_session(c);
# We cannot track sessions with SSLv2.
if ( conn$log$version == version_strings[SSLv2] )
if ( c$ssl$version == version_strings[SSLv2] )
return;
local session = get_session_info(session_id);
++session$num_reuse;
++session$num_use;
# At this point, the connection values have been set. We can then
# compare session and connection values with some confidence.
if ( session$version != conn$log$version ||
session$handshake_cipher != conn$log$handshake_cipher )
if ( session$version != c$ssl$version ||
session$handshake_cipher != c$ssl$handshake_cipher )
{
NOTICE([$note=SSL_SessConIncon, $conn=c, $msg="session violation"]);
}
@ -354,7 +340,8 @@ event ssl_X509_error(c: connection, err: int, err_string: string)
if ( err in x509_ignore_errors )
return;
local conn = get_connection_info(c);
set_session(c);
local error =
err in x509_errors ? x509_errors[err] : "unknown X.509 error";
@ -362,13 +349,7 @@ event ssl_X509_error(c: connection, err: int, err_string: string)
if ( err in x509_hot_errors )
{
NOTICE([$note=SSL_X509Violation, $conn=c, $msg=error]);
++c$hot;
severity = "error";
}
}
event connection_state_remove(c: connection)
{
delete active_conns[c$id];
}

View file

@ -47,6 +47,16 @@ global matched_software: table[string] of Software::Info = {
[$name="Zope/(Zope", $version=[$major=2,$minor=7,$minor2=8,$addl="final"], $host=0.0.0.0, $ts=ts],
["The Bat! (v2.00.9) Personal"] =
[$name="The Bat!", $version=[$major=2,$minor=0,$minor2=9,$addl="Personal"], $host=0.0.0.0, $ts=ts],
["Flash/10,2,153,1"] =
[$name="Flash", $version=[$major=10,$minor=2,$minor2=153,$addl="1"], $host=0.0.0.0, $ts=ts],
["Apache/2.0.46 (Win32) mod_ssl/2.0.46 OpenSSL/0.9.7b mod_jk2/2.0.4"] =
[$name="Apache", $version=[$major=2,$minor=0,$minor2=46,$addl="Win32"], $host=0.0.0.0, $ts=ts],
["Apple iPhone v4.3.1 Weather v1.0.0.8G4"] =
[$name="Apple iPhone", $version=[$major=4,$minor=3,$minor2=1,$addl="Weather"], $host=0.0.0.0, $ts=ts],
["Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 Safari/6533.18.5"] =
[$name="Safari", $version=[$major=5,$minor=0,$minor2=2,$addl="Mobile"], $host=0.0.0.0, $ts=ts],
};
event bro_init()