zeek/policy/icmp.bro

306 lines
7.5 KiB
Text

# $Id: icmp.bro 6883 2009-08-19 21:08:09Z vern $
@load hot
@load weird
@load conn
@load scan
global icmp_file = open_log_file("icmp");
redef capture_filters += { ["icmp"] = "icmp" };
module ICMP;
export {
redef enum Notice += {
ICMPAsymPayload, # payload in echo req-resp not the same
ICMPConnectionPair, # too many ICMPs between hosts
ICMPAddressScan,
# The following isn't presently sufficiently useful due
# to cold start and packet drops.
# ICMPUnpairedEchoReply, # no EchoRequest seen for EchoReply
};
# Whether to log detailed information icmp.log.
const log_details = T &redef;
# ICMP scan detection.
const detect_scans = T &redef;
const scan_threshold = 25 &redef;
# Analysis of connection pairs.
const detect_conn_pairs = F &redef; # switch for connection pair
const detect_payload_asym = F &redef; # switch for echo payload
const conn_pair_threshold = 200 &redef;
}
global conn_pair:table[addr] of set[addr] &create_expire = 1 day;
global conn_pair_thresh_reached: table[addr] of bool &default=F;
type flow_id: record {
orig_h: addr;
resp_h: addr;
id: count;
};
type flow_info: record {
start_time: time;
last_time: time;
orig_bytes: count;
resp_bytes: count;
payload: string;
};
const names: table[count] of string = {
[0] = "echo_reply",
[3] = "unreach",
[4] = "quench",
[5] = "redirect",
[8] = "echo_req",
[9] = "router_adv",
[10] = "router_sol",
[11] = "time_xcd",
[12] = "param_prob",
[13] = "tstamp_req",
[14] = "tstamp_reply",
[15] = "info_req",
[16] = "info_reply",
[17] = "mask_req",
[18] = "mask_reply",
} &default = function(n: count): string { return fmt("icmp-%d", n); };
# Map IP protocol number to the protocol's name.
const IP_proto_name: table[count] of string = {
[1] = "ICMP",
[2] = "IGMP",
[6] = "TCP",
[17] = "UDP",
[41] = "IPV6",
} &default = function(n: count): string { return fmt("%s", n); }
&redef;
# Print a report for the given ICMP flow.
function generate_flow_summary(flow: flow_id, fi: flow_info)
{
local local_init = is_local_addr(flow$orig_h);
local local_addr = local_init ? flow$orig_h : flow$resp_h;
local remote_addr = local_init ? flow$resp_h : flow$orig_h;
local flags = local_init ? "L" : "";
local state: string;
if ( fi$orig_bytes > 0 )
{
if ( fi$resp_bytes > 0 )
state = "SF";
else
state = "SH";
}
else if ( fi$resp_bytes > 0 )
state = "SHR";
else
state = "OTH";
print icmp_file, fmt("%.6f %.6f %s %s %s %s %s %s %s",
fi$start_time, fi$last_time - fi$start_time,
flow$orig_h, flow$resp_h, "icmp_echo",
fi$orig_bytes, fi$resp_bytes, state, flags);
}
# Called when a flow is expired in order to generate a report for it.
function flush_flow(ft: table[flow_id] of flow_info, fi: flow_id): interval
{
generate_flow_summary(fi, ft[fi]);
return 0 sec;
}
# Table to track each active flow.
global flows: table[flow_id] of flow_info
&read_expire = 45 sec
&expire_func = flush_flow;
event icmp_sent(c: connection, icmp: icmp_conn)
{
print icmp_file, fmt("%.6f %.6f %s %s %s %s %s %s %s %s %s",
network_time(), 0.0, icmp$orig_h, icmp$resp_h,
names[icmp$itype], icmp$itype, icmp$icode, "icmp",
icmp$len, "0", "SH");
}
event flow_summary(flow: flow_id, last_time: time)
{
if ( flow !in flows )
return;
local fi = flows[flow];
if ( fi$last_time == last_time )
{
generate_flow_summary(flow, fi);
delete flows[flow];
}
}
function update_flow(icmp: icmp_conn, id: count, is_orig: bool, payload: string)
{
local fid: flow_id;
fid$orig_h = is_orig ? icmp$orig_h : icmp$resp_h;
fid$resp_h = is_orig ? icmp$resp_h : icmp$orig_h;
fid$id = id;
if ( fid !in flows )
{
local info: flow_info;
info$start_time = network_time();
info$orig_bytes = info$resp_bytes = 0;
info$payload = payload; # checked in icmp_echo_reply
flows[fid] = info;
}
local fi = flows[fid];
fi$last_time = network_time();
if ( is_orig )
fi$orig_bytes = fi$orig_bytes + byte_len(payload);
else
fi$resp_bytes = fi$resp_bytes + byte_len(payload);
schedule +30sec { flow_summary(fid, fi$last_time) };
}
event icmp_echo_request(c: connection, icmp: icmp_conn, id: count, seq: count, payload: string)
{
update_flow(icmp, id, T, payload);
local orig = icmp$orig_h;
local resp = icmp$resp_h;
# Simple ping scan detector.
if ( detect_scans &&
(orig !in Scan::distinct_peers ||
resp !in Scan::distinct_peers[orig]) )
{
if ( orig !in Scan::distinct_peers )
{
local empty_peer_set: set[addr] &mergeable;
Scan::distinct_peers[orig] = empty_peer_set;
}
if ( resp !in Scan::distinct_peers[orig] )
add Scan::distinct_peers[orig][resp];
if ( ! Scan::shut_down_thresh_reached[orig] &&
orig !in Scan::skip_scan_sources &&
orig !in Scan::skip_scan_nets &&
|Scan::distinct_peers[orig]| >= scan_threshold )
{
NOTICE([$note=ICMPAddressScan, $src=orig,
$n=scan_threshold,
$msg=fmt("%s has icmp echo scanned %s hosts",
orig, scan_threshold)]);
Scan::shut_down_thresh_reached[orig] = T;
}
}
if ( detect_conn_pairs )
{
if ( orig !in conn_pair )
{
local empty_peer_set2: set[addr] &mergeable;
conn_pair[orig] = empty_peer_set2;
}
if ( resp !in conn_pair[orig] )
add conn_pair[orig][resp];
if ( ! conn_pair_thresh_reached[orig] &&
|conn_pair[orig]| >= conn_pair_threshold )
{
NOTICE([$note=ICMPConnectionPair,
$msg=fmt("ICMP connection threshold exceeded : %s -> %s",
orig, resp)]);
conn_pair_thresh_reached[orig] = T;
}
}
}
event icmp_echo_reply(c: connection, icmp: icmp_conn, id: count,
seq: count, payload: string)
{
# Check payload with the associated flow.
local fid: flow_id;
fid$orig_h = icmp$resp_h; # We know the expected results since
fid$resp_h = icmp$orig_h; # it's an echo reply.
fid$id = id;
if ( fid !in flows )
{
# NOTICE([$note=ICMPUnpairedEchoReply,
# $msg=fmt("ICMP echo reply w/o request: %s -> %s",
# icmp$orig_h, icmp$resp_h)]);
}
else
{
if ( detect_payload_asym )
{
local fi = flows[fid];
local pl = fi$payload;
if ( pl != payload )
{
NOTICE([$note=ICMPAsymPayload,
$msg=fmt("ICMP payload inconsistancy: %s(%s) -> %s(%s)",
icmp$orig_h, byte_len(fi$payload),
icmp$resp_h, byte_len(payload))]);
}
}
}
update_flow(icmp, id, F, payload);
}
event icmp_unreachable(c: connection, icmp: icmp_conn, code: count,
context: icmp_context)
{
if ( active_connection(context$id) )
{
# This section allows Bro to act on ICMP-unreachable packets
# that happen in the context of an active connection. It is
# not currently used.
local c2 = connection_record(context$id);
local os = c2$orig$state;
local rs = c2$resp$state;
local is_attempt =
is_tcp_port(c2$id$orig_p) ?
(os == TCP_SYN_SENT && rs == TCP_INACTIVE) :
(os == UDP_ACTIVE && rs == UDP_INACTIVE);
# Insert action here.
}
if ( log_details )
{
# ICMP unreachable packets are the only ones currently
# logged. Due to the connection data contained *within*
# them, each log line will contain two connections' worth
# of data. The initial ICMP connection info is the same
# as logged for connections.
print icmp_file, fmt("%.6f %.6f %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s",
network_time(), 0.0, icmp$orig_h, icmp$resp_h,
names[icmp$itype], icmp$itype, icmp$icode, "icmp",
icmp$len, "0", "EncapPkt:",
# This is the encapsulated packet:
context$id$orig_h, context$id$orig_p,
context$id$resp_h, context$id$resp_p,
context$len, IP_proto_name[context$proto],
context$len, context$bad_hdr_len,
context$bad_checksum);
}
}