/dark-nets.bro Secret
Last active
February 20, 2017 15:52
Star
You must be signed in to star a gist
scan scripts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 () }; | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
##! 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