Skip to content

Instantly share code, notes, and snippets.

@JustinAzoff
Last active February 20, 2017 15:52
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save JustinAzoff/80f97af4f4fbb91ae26492b919a50434 to your computer and use it in GitHub Desktop.
scan scripts
module Site;
export {
global is_initialized = F;
const initialization_duration = 1 mins &redef;
const initialization_threshold = 100 &redef;
# These should be figured out based on how large local_nets is
# if local_nets is a single /24, v4_aggregation_bits can be 32
const v4_aggregation_bits = 24 &redef;
const v6_aggregation_bits = 64 &redef;
global used_address_space: set[subnet] &synchronized &redef;
global darknet_address_space: set[subnet] &synchronized &redef;
global is_darknet: function(a: addr): bool;
type DarknetMode: enum {
DARKNET,
NOT_ALLOCATED,
DARKNET_OR_NOT_ALLOCATED,
DARKNET_AND_NOT_ALLOCATED,
};
const darknet_mode: DarknetMode=NOT_ALLOCATED &redef;
}
function aggregate_address(a: addr): subnet
{
if(is_v4_addr(a)) {
return mask_addr(a, v4_aggregation_bits);
} else {
return mask_addr(a, v6_aggregation_bits);
}
}
function add_host(a: addr)
{
if (a !in used_address_space) {
local masked = aggregate_address(a);
add used_address_space[masked];
Reporter::info(fmt("New used address space %s", masked));
flush_all();
}
}
function is_darknet(a: addr): bool
{
if (!is_initialized)
return F;
switch ( darknet_mode) {
case DARKNET:
return (a in darknet_address_space);
case NOT_ALLOCATED:
return (a in local_nets && a !in used_address_space);
case DARKNET_OR_NOT_ALLOCATED:
return (a in darknet_address_space || (a in local_nets && a !in used_address_space));
case DARKNET_AND_NOT_ALLOCATED:
return (a in darknet_address_space && (a in local_nets && a !in used_address_space));
}
Reporter::error(fmt("Invalid darknet_mode %s(%d)", darknet_mode, darknet_mode));
return F;
}
#Similar to how known hosts works, but this will also catch udp only hosts.
event Conn::log_conn(rec: Conn::Info)
{
if (|Site::local_nets| == 0)
return;
if (rec$local_orig && rec$orig_pkts > 0)
add_host(rec$id$orig_h);
if (rec$local_resp && rec$resp_pkts > 0)
add_host(rec$id$resp_h);
}
event check_if_initialized()
{
Reporter::info("Unused address tracking delay over");
if (|used_address_space| < initialization_threshold) {
Reporter::info(fmt("addresses set only contains %d items, less than %d, trying again after %s", |used_address_space|, initialization_threshold, initialization_duration));
schedule initialization_duration {check_if_initialized () };
return;
}
Reporter::info(fmt("tracking set initialized with %d values", |used_address_space|));
is_initialized = T;
}
event bro_init()
{
schedule 5secs {check_if_initialized () };
}
##! TCP Scan detection.
# ..Authors: Justin Azoff
# All the authors of the old scan.bro
@load base/frameworks/notice
@load base/utils/time
@load ./dark-nets
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 unique number of host+ports a scanning host has to
## have failed connections with on
const dark_host_threshold = 3 &redef;
const scan_threshold = 25 &redef;
const local_scan_threshold = 250 &redef;
const scan_threshold_with_darknet_hits = 10 &redef;
const local_scan_threshold_with_darknet_hits = 100 &redef;
const knockknock_threshold = 20 &redef;
const knockknock_threshold_with_darknet_hits = 3 &redef;
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(n: Notice::Info)
{
if (n$note == Address_Scan || n$note == Random_Scan || n$note == Port_Scan)
{
known_scanners[n$src] = n$suppress_for;
delete recent_scan_attempts[n$src];
}
}
function adjust_known_scanner_expiration(s: table[addr] of interval, idx: addr): interval
{
local duration = s[idx];
s[idx] = 0secs;
return duration;
}
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(|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| == 1)
{
#Extract the single victim
for (v in victims)
return [$note=Port_Scan, $msg=fmt("%s unique ports on host %s", |ports|, v)];
}
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;
}
}
@if ( Cluster::is_enabled() )
######################################
# Cluster mode
@ifdef ( Broker::__enable )
redef Cluster::worker2manager_events += { "Scan::scan_attempt" };
@else
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];
event Scan::scan_attempt(scanner, attempt);
# 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::MANAGER )
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" )
add_scan(c$id);
}
event connection_rejected(c: connection)
{
if ( c$history == "Sr" )
add_scan(c$id);
}
#event connection_reset(c: connection)
# {
# if ( c$history == "ShR" )
# add_scan(c$id);
# }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment