mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 06:38:20 +00:00
485 lines
13 KiB
Text
485 lines
13 KiB
Text
# $Id: stepping.bro 6481 2008-12-15 00:47:57Z vern $
|
|
|
|
@load notice
|
|
@load port-name
|
|
@load demux
|
|
@load login
|
|
@load alarm
|
|
|
|
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");
|
|
}
|
|
}
|