##! 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; }; ## 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 distinct_attacks: table[addr] of set[Attempt] &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, attempts: set[Attempt]): Notice::Info { local side = Site::is_local_addr(scanner) ? "local" : "remote"; local dur = "TODO"; local n = analyze_unique_hostports(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 ( scanner in recent_scanners ) return; if ( scanner !in distinct_attacks) distinct_attacks[scanner] = set(); add distinct_attacks[scanner][attempt]; if ( |distinct_attacks[scanner]| >= scan_threshold) { local note = generate_notice(scanner, distinct_attacks[scanner]); NOTICE(note); delete distinct_attacks[scanner]; add recent_scanners[scanner]; } } @if ( Cluster::is_enabled() ) ###################################### # Cluster mode redef Cluster::worker2manager_events += /Scan::scan_attempt/; global recent_scan_keys: 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 ( hook Scan::scan_policy(scanner, victim, scanned_port) ) local attempt = Attempt($victim=victim, $scanned_port=scanned_port); if( scanner !in recent_scan_keys) recent_scan_keys[scanner] = set(); if( attempt in recent_scan_keys[scanner] || |recent_scan_keys[scanner]| > scan_threshold) return; add recent_scan_keys[scanner][attempt]; event Scan::scan_attempt(scanner, attempt); } @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 ###################################### function is_failed_conn(c: connection): bool { # Sr || ( (hR || ShR) && (data not sent in any direction) ) if ( (c$orig$state == TCP_SYN_SENT && c$resp$state == TCP_RESET) || (((c$orig$state == TCP_RESET && c$resp$state == TCP_SYN_ACK_SENT) || (c$orig$state == TCP_RESET && c$resp$state == TCP_ESTABLISHED && "S" in c$history ) ) && /[Dd]/ !in c$history ) ) return T; return F; } event connection_attempt(c: connection) { if ( "H" !in c$history ) add_scan(c$id); } event connection_rejected(c: connection) { if ( "S" in c$history ) add_scan(c$id); } event connection_reset(c: connection) { if ( is_failed_conn(c) ) add_scan(c$id); }