##! TCP Scan detection. # ..Authors: Justin Azoff # All the authors of the old scan.bro @load base/frameworks/notice @load base/frameworks/sumstats @load base/utils/time 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]; }; ## 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 scan_threshold = 25.0 &redef; global Scan::scan_policy: hook(scanner: addr, victim: addr, scanned_port: port); global scan_attempt: event(scanner: addr, attempt: Attempt); global scanner_detected: event(scanner: addr); global attacks: table[addr] of Scan_Info &read_expire=scan_timeout &redef; global recent_scanners: set[addr] &create_expire=5mins; } 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 recent_scanners ) return; local si: Scan_Info; local attempts: set[Attempt]; if ( scanner !in attacks) { attempts = set(); si = Scan_Info($first_seen=network_time(), $attempts=attempts); attacks[scanner] = si; } else { si = attacks[scanner]; attempts = si$attempts; } add attempts[attempt]; if ( |attempts| >= scan_threshold) { local note = generate_notice(scanner, si); NOTICE(note); delete attacks[scanner]; add recent_scanners[scanner]; event Scan::scanner_detected(scanner); } } @if ( Cluster::is_enabled() ) ###################################### # Cluster mode redef Cluster::worker2manager_events += /Scan::scan_attempt/; redef Cluster::manager2worker_events += /Scan::scanner_detected/; global recent_scan_attempts: table[addr] of set[Attempt] &create_expire=5mins; 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 recent_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 scanner_detected event if ( |recent_scan_attempts[scanner]| >= scan_threshold ) { add recent_scanners[scanner]; delete recent_scan_attempts[scanner]; } } event Scan::scanner_detected(scanner: addr) { add recent_scanners[scanner]; 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); }