338 lines
11 KiB
Plaintext
338 lines
11 KiB
Plaintext
##! TCP Scan detection.
|
|
|
|
# ..Authors: Justin Azoff
|
|
# All the authors of the old scan.bro
|
|
|
|
@load base/frameworks/notice
|
|
|
|
@load base/utils/time
|
|
|
|
@ifndef(Site::darknet_mode)
|
|
@load ./bro-is-darknet
|
|
@endif
|
|
|
|
module Scan;
|
|
|
|
export {
|
|
redef enum Notice::Type += {
|
|
## Address scans detect that a host appears to be scanning some
|
|
## number of destinations on a single port. This notice is
|
|
## generated when more than :bro:id:`Scan::scan_threshold`
|
|
## unique hosts are seen over the previous
|
|
## :bro:id:`Scan::scan_interval` time range.
|
|
Address_Scan,
|
|
|
|
## Port scans detect that an attacking host appears to be
|
|
## scanning a single victim host on several ports. This notice
|
|
## is generated when an attacking host attempts to connect to
|
|
## :bro:id:`Scan::scan_threshold`
|
|
## unique ports on a single host over the previous
|
|
## :bro:id:`Scan::scan_interval` time range.
|
|
Port_Scan,
|
|
|
|
## Random scans detect that an attacking host appears to be
|
|
## scanning multiple victim hosts on several ports. This notice
|
|
## is generated when an attacking host attempts to connect to
|
|
## :bro:id:`Scan::scan_threshold`
|
|
## unique hosts and ports over the previous
|
|
## :bro:id:`Scan::scan_interval` time range.
|
|
Random_Scan,
|
|
};
|
|
|
|
## An individual scan destination
|
|
type Attempt: record {
|
|
victim: addr;
|
|
scanned_port: port;
|
|
};
|
|
|
|
## Information tracked for each scanner
|
|
type Scan_Info: record {
|
|
first_seen: time;
|
|
attempts: set[Attempt];
|
|
port_counts: table[port] of count;
|
|
dark_hosts: set[addr];
|
|
};
|
|
|
|
## Failed connection attempts are tracked until not seen for this interval.
|
|
## A higher interval will detect slower scanners, but may also yield more
|
|
## false positives.
|
|
const scan_timeout = 15min &redef;
|
|
|
|
## The threshold of the number of darknet hosts a scanning host has to have
|
|
## scanned in order for the scan to be considered a darknet scan
|
|
const dark_host_threshold = 3 &redef;
|
|
|
|
## The threshold of the unique number of host+ports a remote scanning host
|
|
## has to have failed connections with
|
|
const scan_threshold = 25 &redef;
|
|
|
|
## The threshold of the unique number of host+ports a local scanning host
|
|
## has to have failed connections with
|
|
const local_scan_threshold = 250 &redef;
|
|
|
|
## The threshold of the unique number of host+ports a remote scanning host
|
|
## has to have failed connections with if it has passed dark_host_threshold
|
|
const scan_threshold_with_darknet_hits = 10 &redef;
|
|
|
|
## The threshold of the unique number of host+ports a local scanning host
|
|
## has to have failed connections with if it has passed dark_host_threshold
|
|
const local_scan_threshold_with_darknet_hits = 100 &redef;
|
|
|
|
## The threshold of the number of unique hosts a remote scanning host has
|
|
## to have failed connections with
|
|
const knockknock_threshold = 20 &redef;
|
|
|
|
## The threshold of the number of unique hosts a remote scanning host has
|
|
## to have failed connections with if it has passed dark_host_threshold
|
|
const knockknock_threshold_with_darknet_hits = 3 &redef;
|
|
|
|
## Override this hook to ignore particular scan connections
|
|
global Scan::scan_policy: hook(scanner: addr, victim: addr, scanned_port: port);
|
|
|
|
|
|
global scan_attempt: event(scanner: addr, attempt: Attempt);
|
|
global attacks: table[addr] of Scan_Info &read_expire=scan_timeout &redef;
|
|
global recent_scan_attempts: table[addr] of set[Attempt] &create_expire=1mins;
|
|
|
|
global adjust_known_scanner_expiration: function(s: table[addr] of interval, idx: addr): interval;
|
|
global known_scanners: table[addr] of interval &create_expire=10secs &expire_func=adjust_known_scanner_expiration;
|
|
}
|
|
|
|
# There's no way to set a key to expire at a specific time, so we
|
|
# First set the keys value to the duration we want, and then
|
|
# use expire_func to adjust it to the desired time.
|
|
event Notice::begin_suppression(ts: time, suppress_for: interval, note: Notice::Type, identifier: string)
|
|
{
|
|
if (note == Address_Scan || note == Random_Scan || note == Port_Scan)
|
|
{
|
|
local src = to_addr(identifier);
|
|
known_scanners[src] = suppress_for;
|
|
delete recent_scan_attempts[src];
|
|
}
|
|
}
|
|
|
|
function adjust_known_scanner_expiration(s: table[addr] of interval, idx: addr): interval
|
|
{
|
|
local duration = s[idx];
|
|
s[idx] = 0secs;
|
|
return duration;
|
|
}
|
|
|
|
@if ( !Cluster::is_enabled() || Cluster::local_node_type() != Cluster::WORKER )
|
|
function analyze_unique_hostports(attempts: set[Attempt]): Notice::Info
|
|
{
|
|
local ports: set[port];
|
|
local victims: set[addr];
|
|
|
|
local ports_str: set[string];
|
|
local victims_str: set[string];
|
|
|
|
for ( a in attempts )
|
|
{
|
|
add victims[a$victim];
|
|
add ports[a$scanned_port];
|
|
|
|
add victims_str[cat(a$victim)];
|
|
add ports_str[cat(a$scanned_port)];
|
|
}
|
|
|
|
if(|ports| == 1)
|
|
{
|
|
#Extract the single port
|
|
for (p in ports)
|
|
{
|
|
return [$note=Address_Scan, $msg=fmt("%s unique hosts on port %s", |victims|, p), $p=p];
|
|
}
|
|
}
|
|
if(|victims| == 1)
|
|
{
|
|
#Extract the single victim
|
|
for (v in victims)
|
|
return [$note=Port_Scan, $msg=fmt("%s unique ports on host %s", |ports|, v)];
|
|
}
|
|
if(|ports| <= 5)
|
|
{
|
|
local ports_string = join_string_set(ports_str, ", ");
|
|
return [$note=Address_Scan, $msg=fmt("%s unique hosts on ports %s", |victims|, ports_string)];
|
|
}
|
|
if(|victims| <= 5)
|
|
{
|
|
local victims_string = join_string_set(victims_str, ", ");
|
|
return [$note=Port_Scan, $msg=fmt("%s unique ports on hosts %s", |ports|, victims_string)];
|
|
}
|
|
return [$note=Random_Scan, $msg=fmt("%d hosts on %d ports", |victims|, |ports|)];
|
|
}
|
|
|
|
function generate_notice(scanner: addr, si: Scan_Info): Notice::Info
|
|
{
|
|
local side = Site::is_local_addr(scanner) ? "local" : "remote";
|
|
local dur = duration_to_mins_secs(network_time() - si$first_seen);
|
|
local n = analyze_unique_hostports(si$attempts);
|
|
n$msg = fmt("%s scanned at least %s in %s", scanner, n$msg, dur);
|
|
n$src = scanner;
|
|
n$sub = side;
|
|
n$identifier=cat(scanner);
|
|
return n;
|
|
}
|
|
|
|
function add_scan_attempt(scanner: addr, attempt: Attempt)
|
|
{
|
|
# If this is a recent scanner, do nothing
|
|
if ( scanner in known_scanners )
|
|
return;
|
|
|
|
local si: Scan_Info;
|
|
local attempts: set[Attempt];
|
|
local dark_hosts: set[addr];
|
|
local port_counts: table[port] of count;
|
|
|
|
# Accounting
|
|
if ( scanner !in attacks)
|
|
{
|
|
attempts = set();
|
|
port_counts = table();
|
|
dark_hosts = set();
|
|
si = Scan_Info($first_seen=network_time(), $attempts=attempts, $port_counts=port_counts, $dark_hosts=dark_hosts);
|
|
attacks[scanner] = si;
|
|
}
|
|
else
|
|
{
|
|
si = attacks[scanner];
|
|
attempts = si$attempts;
|
|
port_counts = si$port_counts;
|
|
dark_hosts = si$dark_hosts;
|
|
}
|
|
|
|
if ( attempt in attempts )
|
|
return;
|
|
|
|
add attempts[attempt];
|
|
if (attempt$scanned_port !in port_counts)
|
|
port_counts[attempt$scanned_port] = 1;
|
|
else
|
|
++port_counts[attempt$scanned_port];
|
|
|
|
# See if we need more dark hosts, otherwise add the new one if we can
|
|
if(|dark_hosts| < dark_host_threshold && attempt$victim !in dark_hosts && Site::is_darknet(attempt$victim)) {
|
|
add dark_hosts[attempt$victim];
|
|
}
|
|
# End of accounting
|
|
|
|
# Determine thresholds and if they were crossed
|
|
local thresh: count;
|
|
local is_local = Site::is_local_addr(scanner);
|
|
|
|
local is_darknet_scan = |dark_hosts| >= dark_host_threshold;
|
|
|
|
if ( is_darknet_scan )
|
|
thresh = is_local ? local_scan_threshold_with_darknet_hits : scan_threshold_with_darknet_hits;
|
|
else
|
|
thresh = is_local ? local_scan_threshold : scan_threshold;
|
|
|
|
local is_scan = |attempts| >= thresh;
|
|
local is_knockkock = F;
|
|
if ( !is_local )
|
|
{
|
|
local knock_thresh = is_darknet_scan ? knockknock_threshold_with_darknet_hits : knockknock_threshold;
|
|
# This should probably check all port counts if is_darknet_scan
|
|
is_knockkock = port_counts[attempt$scanned_port] >= knock_thresh;
|
|
}
|
|
|
|
#The above 17 lines needs to be factored out into functions/hooks/something plugable.
|
|
if ( is_scan || is_knockkock)
|
|
{
|
|
local note = generate_notice(scanner, si);
|
|
if ( is_knockkock )
|
|
note$msg = fmt("kk: %s", note$msg);
|
|
NOTICE(note);
|
|
delete attacks[scanner];
|
|
known_scanners[scanner] = 1hrs;
|
|
}
|
|
}
|
|
@endif
|
|
|
|
@if ( Cluster::is_enabled() )
|
|
######################################
|
|
# Cluster mode
|
|
@ifdef (Cluster::worker2manager_events)
|
|
redef Cluster::worker2manager_events += /Scan::scan_attempt/;
|
|
@endif
|
|
|
|
function add_scan(id: conn_id)
|
|
{
|
|
local scanner = id$orig_h;
|
|
local victim = id$resp_h;
|
|
local scanned_port = id$resp_p;
|
|
|
|
# If this is a recent scanner, do nothing
|
|
if ( scanner in known_scanners )
|
|
return;
|
|
|
|
if ( hook Scan::scan_policy(scanner, victim, scanned_port) )
|
|
{
|
|
local attempt = Attempt($victim=victim, $scanned_port=scanned_port);
|
|
if ( scanner !in recent_scan_attempts)
|
|
recent_scan_attempts[scanner] = set();
|
|
if ( attempt in recent_scan_attempts[scanner] )
|
|
return;
|
|
add recent_scan_attempts[scanner][attempt];
|
|
@ifdef (Cluster::worker2manager_events)
|
|
event Scan::scan_attempt(scanner, attempt);
|
|
@else
|
|
Cluster::publish_hrw(Cluster::proxy_pool, scanner, Scan::scan_attempt, scanner, attempt);
|
|
@endif
|
|
|
|
# Check to see if we have already sent enough attempts
|
|
# this is mostly reduntant due to the notice begin_suppression event
|
|
local thresh = Site::is_local_addr(scanner) ? local_scan_threshold : scan_threshold;
|
|
if ( |recent_scan_attempts[scanner]| >= thresh )
|
|
{
|
|
known_scanners[scanner] = 1hrs;
|
|
delete recent_scan_attempts[scanner];
|
|
}
|
|
}
|
|
}
|
|
|
|
@if ( Cluster::local_node_type() != Cluster::WORKER )
|
|
event Scan::scan_attempt(scanner: addr, attempt: Attempt)
|
|
{
|
|
add_scan_attempt(scanner, attempt);
|
|
}
|
|
@endif
|
|
######################################
|
|
|
|
@else
|
|
######################################
|
|
# Standalone mode
|
|
function add_scan(id: conn_id)
|
|
{
|
|
local scanner = id$orig_h;
|
|
local victim = id$resp_h;
|
|
local scanned_port = id$resp_p;
|
|
|
|
if ( hook Scan::scan_policy(scanner, victim, scanned_port) )
|
|
{
|
|
add_scan_attempt(scanner, Attempt($victim=victim, $scanned_port=scanned_port));
|
|
}
|
|
}
|
|
@endif
|
|
######################################
|
|
|
|
event connection_attempt(c: connection)
|
|
{
|
|
if ( c$history == "S" || c$history == "SW")
|
|
add_scan(c$id);
|
|
}
|
|
|
|
event connection_rejected(c: connection)
|
|
{
|
|
if ( c$history == "Sr" || c$history == "SWr")
|
|
add_scan(c$id);
|
|
}
|
|
|
|
#event connection_reset(c: connection)
|
|
# {
|
|
# if ( c$history == "ShR" )
|
|
# add_scan(c$id);
|
|
# }
|