zeek/policy.old/stepping.bro
Robin Sommer 9709b1d522 Merge remote branch 'origin/topic/robin/reporting'
* origin/topic/robin/reporting:
  Syslog BiF now goes through the reporter as well.
  Avoiding infinite loops when an error message handlers triggers errors itself.
  Renaming the Logger to Reporter.
  Overhauling the internal reporting of messages to the user.

Updating a bunch of tests/baselines as well.

Conflicts:
	aux/broccoli
	policy.old/alarm.bro
	policy/all.bro
	policy/bro.init
	policy/frameworks/notice/weird.bro
	policy/notice.bro
	src/SSL-binpac.cc
	src/bro.bif
	src/main.cc
2011-07-01 13:59:21 -07:00

484 lines
13 KiB
Text

# $Id: stepping.bro 6481 2008-12-15 00:47:57Z vern $
@load notice
@load port-name
@load demux
@load login
module Stepping;
export {
redef enum Notice += {
# A stepping stone was seen in which the first part of
# the chain is a clear-text connection but the second part
# is encrypted. This often means that a password or
# passphrase has been exposed in the clear, and may also
# mean that the user has an incomplete notion that their
# connection is protected from eavesdropping.
ClearToEncrypted_SS,
};
}
global step_log = open_log_file("step") &redef;
# The following must be defined for the event engine to generate
# stepping stone events.
redef stp_delta = 0.08 sec;
redef stp_idle_min = 0.5 sec;
global stepping_stone: event(c1: connection, c2: connection, method: string);
#### First, tag-based schemes - $DISPLAY, Last Login ####
# If <conn> was a login to <dst> propagating a $DISPLAY of <display>,
# then we make an entry of [<dst>, <display>] = <conn>.
global display_pairs: table[addr, string] of connection;
# Maps login tags like "Last login ..." to connections.
global tag_to_conn_map: table[string] of connection;
type tag_info: record {
display: string; # $DISPLAY, if any
tag: string; # login tag, e.g. "Last login ..."
};
global conn_tag_info: table[conn_id] of tag_info;
const STONE_DISPLAY = 1;
const STONE_LOGIN_BANNER = 2;
const STONE_TIMING = 4;
### fixme
global detected_stones: table[addr, port, addr, port, addr, port, addr, port]
of count &default = 0;
global did_stone_summary: table[addr, port, addr, port, addr, port, addr, port]
of count &default = 0;
function new_tag_info(c: connection)
{
local ti: tag_info;
ti$tag = ti$display = "";
conn_tag_info[c$id] = ti;
}
event login_display(c: connection, display: string)
{
local id = c$id;
if ( id !in conn_tag_info )
new_tag_info(c);
conn_tag_info[id]$display = display;
display_pairs[id$resp_h, display] = c;
if ( [id$orig_h, display] in display_pairs )
event Stepping::stepping_stone(display_pairs[id$orig_h, display], c, "display");
}
event login_output_line(c: connection, line: string)
{
if ( /^([Ll]ast +(successful)? *login)/ | /^Last interactive login/
!in line ||
# Some finger output includes "Last login ..." but luckily
# appears to be terminated by ctrl-A.
/\001/ in line )
return;
if ( c$id !in conn_tag_info )
new_tag_info(c);
local ti = conn_tag_info[c$id];
local tag = line;
if ( ti$tag == "" )
ti$tag = tag;
if ( tag in tag_to_conn_map )
{
local c2 = tag_to_conn_map[tag];
### Would really like this taken care of by having
# tag_to_conn_map[tag] deleted when c2 goes away.
if ( active_connection(c2$id) )
event Stepping::stepping_stone(c2, c, "login-tag");
}
else
tag_to_conn_map[tag] = c;
}
event connection_finished(c: connection)
{
### would really like some automatic destructors invoked
### whenever a connection goes away
local id = c$id;
if ( id in conn_tag_info )
{
local ti = conn_tag_info[id];
delete display_pairs[id$resp_h, ti$display];
delete tag_to_conn_map[ti$tag];
delete conn_tag_info[id];
}
}
#### Now, timing-based correlation ####
const stp_ratio_thresh = 0.3 &redef; # prop. of idle times that must coincide
# Time scale to which following thresholds apply.
const stp_scale = 100.0 &redef;
const stp_common_host_thresh = 2 &redef; # must be <= stp_random_pair_thresh
const stp_random_pair_thresh = 4 &redef;
const stp_demux_disabled = T &redef;
# Indexed by the center host (or destination of the first connection,
# for ABCD stepping stones) and the $addl information associated with
# the connection (i.e., often username). If present in the set, then
# we shouldn't bother generating a report for a clear->ssh stepping stone.
const skip_clear_ssh_reports: set[addr, string] &redef;
global num_stp_pairs = 0;
type endp_info: record {
conn: connection;
id: conn_id;
resume_time: time; # time when resuming from most recent idle period
old_resume_time: time; # time when resuming from penultimate idle period
idle_cnt: count; # number of idle periods for this endpoint (flow)
};
type pair_info: record {
is_stp: bool; # true if flow pair considered a stepping stone pair
hit: count; # number of coincidences
hit_two_in_row: count; # number of coincidences two-in-row
};
# For connection k:
# stp_endps[2k] is the orig endpoint
# stp_endps[2k+1] is the resp endpoint
global stp_endps: table[int] of endp_info;
# Some endpoint pairs are weird, e.g., when two endp's share a common port.
# Such weird endp pairs may be correlated, but are unlikely to be stepping
# stone pairs.
global stp_weird_pairs: set[int, int];
# Normal (i.e., not weird) endp pairs.
global stp_normal_pairs: table[int, int] of pair_info;
function is_orig(e: int): bool
{
return (e % 2) == 0;
}
function peer(e: int): int
{
return (e % 2) == 0 ? (e + 1): (e - 1);
}
function orig_host(e: int): addr
{
return stp_endps[e]$id$orig_h;
}
function resp_host(e: int): addr
{
return stp_endps[e]$id$resp_h;
}
function orig_port(e: int): port
{
return stp_endps[e]$id$orig_p;
}
function resp_port(e: int): port
{
return stp_endps[e]$id$resp_p;
}
function build_conn(e: int): connection
{ # return the id of the orig, not the resp
return stp_endps[e]$conn;
}
function stp_id_string(id: conn_id): string
{
return fmt("%s.%d > %s.%d", id$orig_h, id$orig_p, id$resp_h, id$resp_p);
}
function stp_create_weird_pair(e1: int, e2: int)
{
add stp_weird_pairs[e1, e2];
}
function stp_create_normal_pair(e1: int, e2: int)
{
local pair: pair_info;
pair$is_stp = F;
pair$hit = pair$hit_two_in_row = 0;
stp_normal_pairs[e1, e2] = pair;
}
function stp_correlate_weird_pair(e1: int, e2: int)
{ # do nothing right now
}
global stp_check_normal_pair: function(e1: int, e2: int): bool;
function stp_correlate_normal_pair(e1: int, e2: int)
{
if ( stp_normal_pairs[e1, e2]$is_stp )
return; # already classified as stepping stone pair
++stp_normal_pairs[e1, e2]$hit;
if ( stp_endps[e1]$old_resume_time != 0.0 &&
stp_endps[e2]$old_resume_time != 0.0 )
{
local dt = stp_endps[e2]$old_resume_time -
stp_endps[e1]$old_resume_time;
if ( dt >= 0.0 sec && dt <= stp_delta )
++stp_normal_pairs[e1, e2]$hit_two_in_row;
}
stp_check_normal_pair(e1, e2);
}
function stp_check_weird_pair(e1: int, e2: int)
{ # do nothing right now
}
function stp_check_normal_pair(e1: int, e2: int): bool
{
if ( stp_normal_pairs[e1, e2]$is_stp )
return T; # already classified as stepping stone pair
local p1 = peer(e1);
local p2 = peer(e2);
local reverse_exists = [p2, p1] in stp_normal_pairs;
if ( reverse_exists && stp_normal_pairs[p2, p1]$is_stp )
{ # already classified as stepping stone pair
stp_normal_pairs[e1, e2]$is_stp = T;
return T;
}
local hit_two_in_row = stp_normal_pairs[e1, e2]$hit_two_in_row;
if ( reverse_exists )
hit_two_in_row = hit_two_in_row +
stp_normal_pairs[p2, p1]$hit_two_in_row;
# Criteria 1:
# if ( e1 and e2 share a common host )
# hit_two_in_row >= stp_common_host_thresh
# else
# hit_two_in_row >= stp_random_pair_thresh
local factor = max_double(1.0,
min_count(stp_endps[e1]$idle_cnt,
stp_endps[e2]$idle_cnt) / stp_scale);
if ( hit_two_in_row < factor * stp_common_host_thresh )
return F;
if ( hit_two_in_row < factor * stp_random_pair_thresh &&
orig_host(e1) != orig_host(e2) && orig_host(e1) != resp_host(e2) &&
resp_host(e1) != orig_host(e2) && resp_host(e1) != resp_host(e2) )
return F;
# Criteria 2:
# hit_ratio >= stp_ratio_thresh
local hit_ratio: double;
if ( reverse_exists &&
stp_normal_pairs[p2, p1]$hit > stp_normal_pairs[e1, e2]$hit )
hit_ratio = (1.0 * stp_normal_pairs[p2, p1]$hit) /
min_count(stp_endps[p1]$idle_cnt,
stp_endps[p2]$idle_cnt);
else
hit_ratio = (1.0 * stp_normal_pairs[e1, e2]$hit) /
min_count(stp_endps[e1]$idle_cnt,
stp_endps[e2]$idle_cnt);
if ( hit_ratio < stp_ratio_thresh )
return F;
stp_normal_pairs[e1, e2]$is_stp = T;
event Stepping::stepping_stone(build_conn(e1), build_conn(e2), "timing");
return T;
}
function reverse_id(id: conn_id): conn_id
{
local rid: conn_id;
rid$orig_h = id$resp_h;
rid$orig_p = id$resp_p;
rid$resp_h = id$orig_h;
rid$resp_p = id$orig_p;
return rid;
}
event stp_create_endp(c: connection, e: int, is_orig: bool)
{
local end_i: endp_info;
end_i$conn = c;
end_i$id = is_orig ? c$id : reverse_id(c$id);
end_i$resume_time = end_i$old_resume_time = 0.0;
end_i$idle_cnt = 0;
stp_endps[e] = end_i;
}
event stp_resume_endp(e: int)
{
stp_endps[e]$old_resume_time = stp_endps[e]$resume_time;
stp_endps[e]$resume_time = network_time();
++stp_endps[e]$idle_cnt;
}
event stp_correlate_pair(e1: int, e2: int)
{
local normal = T;
if ( [e1, e2] in stp_normal_pairs )
;
else if ( [e1, e2] in stp_weird_pairs )
normal = F;
else
{
# An endpoint pair is considered weird, iff:
# the two flows both originated at same host, or
# both terminated at same host, or
# at least one flow is within a single host, or
# two flows share an endpoint (host, port)
if ( orig_host(e1) == orig_host(e2) || resp_host(e1) == resp_host(e2) ||
orig_host(e1) == resp_host(e1) || orig_host(e2) == resp_host(e2) ||
(orig_host(e1) == resp_host(e2) && orig_port(e1) == resp_port(e2)) ||
(resp_host(e1) == orig_host(e2) && resp_port(e1) == orig_port(e2)) )
{
stp_create_weird_pair(e1, e2);
normal = F;
}
else
stp_create_normal_pair(e1, e2);
}
if ( normal )
stp_correlate_normal_pair(e1, e2);
else
stp_correlate_weird_pair(e1, e2);
}
event stp_remove_pair(e1: int, e2: int)
{
delete stp_normal_pairs[e1, e2];
delete stp_weird_pairs[e1, e2];
}
event stp_remove_endp(e: int)
{
delete stp_endps[e];
}
function report_stone(id1: conn_id, addl1: string, id2: conn_id, addl2: string)
: string
{
if ( id1$resp_h == id2$orig_h )
# A single-intermediary stepping stone.
return fmt("%s -> %s %s-> %s %s",
id1$orig_h,
endpoint_id(id1$resp_h, id1$resp_p), addl1,
endpoint_id(id2$resp_h, id2$resp_p), addl2);
else
# A multi-intermediary stepping stone.
return fmt("%s -> %s %s... %s -> %s %s",
id1$orig_h,
endpoint_id(id1$resp_h, id1$resp_p), addl1,
id2$orig_h,
endpoint_id(id2$resp_h, id2$resp_p), addl2);
}
event stone_summary(id1: conn_id, id2: conn_id)
{
if ( ++did_stone_summary[id1$orig_h, id1$orig_p, id1$resp_h, id1$resp_p, id2$orig_h, id2$orig_p, id2$resp_h, id2$resp_p] > 1 )
return;
local detection_type = detected_stones[id1$orig_h, id1$orig_p, id1$resp_h, id1$resp_p, id2$orig_h, id2$orig_p, id2$resp_h, id2$resp_p];
local report: string;
if ( detection_type == STONE_DISPLAY )
report = "only-display";
else if ( detection_type == STONE_LOGIN_BANNER )
report = "only-banner";
else if ( detection_type == STONE_TIMING )
report = "only-timing";
else if ( detection_type == STONE_LOGIN_BANNER + STONE_TIMING )
report = "stone-both";
else
report = fmt("stone-other-%d", detection_type);
print step_log, fmt("%s detected %s %s %d %s %d %s %d %s %d",
network_time(), report, id1$orig_h, id1$orig_p, id1$resp_h,
id1$resp_p, id2$orig_h, id2$orig_p, id2$resp_h, id2$resp_p);
}
event stepping_stone(c1: connection, c2: connection, method: string)
{
# Put into canonical form: make #1 be the earlier of the two
# connections.
local id1 = c1$start_time < c2$start_time ? c1$id : c2$id;
local id2 = c1$start_time < c2$start_time ? c2$id : c1$id;
local addl1 = c1$start_time < c2$start_time ? c1$addl : c2$addl;
local addl2 = c1$start_time < c2$start_time ? c2$addl : c1$addl;
if ( id1$orig_h == id2$orig_h || id1$resp_h == id2$resp_h )
# of the form A->B, A->C ; or B->A, C->A ; uninteresting.
return;
local tag = fmt("stp.%d", ++num_stp_pairs);
local prelude = fmt("%.6f step %s (%s)", network_time(), num_stp_pairs, method);
local stone_type = (method == "display" ? STONE_DISPLAY :
(method == "login-tag" ? STONE_LOGIN_BANNER :
STONE_TIMING));
local current_stones = detected_stones[id1$orig_h, id1$orig_p, id1$resp_h, id1$resp_p, id2$orig_h, id2$orig_p, id2$resp_h, id2$resp_p];
if ( (current_stones / stone_type) % 2 == 0 )
detected_stones[id1$orig_h, id1$orig_p, id1$resp_h, id1$resp_p, id2$orig_h, id2$orig_p, id2$resp_h, id2$resp_p] = current_stones + stone_type;
schedule 1 day { stone_summary(id1, id2) };
print step_log, fmt("%s: %s", prelude, report_stone(id1, addl1, id2, addl2));
local is_ssh1 = id1$orig_p == ssh || id1$resp_p == ssh;
local is_ssh2 = id2$orig_p == ssh || id2$resp_p == ssh;
if ( ! is_ssh1 && is_ssh2 )
{ # Inbound clear-text, outbound ssh.
if ( [id1$resp_h, addl1] !in skip_clear_ssh_reports )
NOTICE([$note=ClearToEncrypted_SS,
# The following isn't sufficient for
# A->(B->C)->D stepping stones, only A->B->C.
$src=c1$id$orig_h, $conn=c2,
$user=addl1, $sub=addl2,
$msg=fmt("clear -> ssh: %s", report_stone(id1, addl1, id2, addl2))]);
}
if ( ! stp_demux_disabled )
{
demux_conn(id1, tag, "keys", "server");
demux_conn(id2, tag, "keys", "server");
}
}