##! 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); # }