################################################################# # # TCP Attack & Anomaly Filter # ################################################################# # Author: Erwan Lemonnier (erwan.lemonnier@defcom.com) # # Created: 12-03-2001 # Last Modified: 25-04-2001 # # REM: for best readability, set tab to 8 chars # for detailed explanation, see 'tcp.desc' # ################################################################# ################################################################## # configuration ################################################################## # Recognized TCP option number SHORT_TCP_OPTIONS = [0,1,100]; #tcp options that take only 1 byte LONG_TCP_OPTIONS = [2,3,4,5,6,7,8,9,10,11,12,13,105]; #tcp options that take 2 or more bytes (size in 2nd byte) # internal values to distinguish between packets in log FIN = 1; SYN = 2; RST = 4; PSH = 8; ACK = 16; URG = 32; #special or composed types SYN_ACK = 18; XMAS = 41; NO_FLAG = 0; # Log upp to the 4 first packets of each new tcp connection # each element of START_LOG is a list with in 1st position the time at which # the last packet was received, then the type of each packet received, as # defined above. # { ipsrc, ipdst, portsrc, portdst, time_last, nbr_FIN_exchanged+RST_got, last_ack_src2dst, ack_src2dst_count, last_ack_dst2src, ack_dst2src_count, count_nul_window_src2dst, count_nul_window_dst2src, packet1, [packet2 [packet3 [packet4]]] } # FIN+RST -> 0 = no RST|FIN got, 1 = 1 FIN got, 2 = 2 FIN got, 1x = + a RST got # key = string based on ip_src, ip_dst, port_src, port_dst LOG = NULL; # flag used by the end of session checking filter (LOG Analyser) # it indicates when it is about to run wether the last occurence # of it has finished to run or not PREVIOUS_FINISHED = 1; ############################################################### # Some alarms are internaly listed by the filter, then processed # by a second filter runing every 3 minutes, which extract # interesting info from these alarms # messages codes of internaly filtered alarms MSG["TCP_RES_FLAG"] = "TCP reserved flag not null (hping, QUESO, NMAP OS or ECN ?)."; MSG["URG_F_NOT_P"] = "TCP URG flag set but urgent pointer null."; MSG["URG_P_NOT_F"] = "TCP non null urgent pointer while URG flag not set."; MSG["XMAS"] = "XMAS tree scan (FIN-URG-PSH, RST)."; MSG["F_R"] = "Stealth FIN scan (FIN, RST)."; MSG["S_R"] = "TCP half open scan: closed port (SYN, RST)."; MSG["NULL"] = "Null scan (NO_FLAG, RST)."; MSG["A_R"] = "ACK scan (ACK,RST)."; MSG["S_SA"] = "SYN, SYN-ACK (SYN flood|scan ?)."; MSG["S_SA_R"] = "TCP half open scan: open port (SYN, SYN-ACK, RST)."; MSG["S_SA_F"] = "TCP half open scan (SYN, SYN-ACK, FIN)."; MSG["Unused_TCP"] = "unused TCP connection (DoS ?)."; # alarm codes of internaly filtered alarms NUM["TCP_RES_FLAG"] = 7500021; NUM["URG_F_NOT_P"] = 7500022; NUM["URG_P_NOT_F"] = 7500022; NUM["XMAS"] = 7500028; NUM["F_R"] = 7500028; NUM["S_R"] = 7500028; NUM["NULL"] = 7500028; NUM["A_R"] = 7500028; NUM["S_SA"] = 7500028; NUM["S_SA_R"] = 7500028; NUM["S_SA_F"] = 7500028; NUM["Unused_TCP"] = 7500028; # internal queue of alerts: each entry is formated as follow: # $key = "--" # QUEUE[$key] = [list of each destination ports of each packet that issued this alarm] QUEUE = NULL; ################################################################## # start_alert() ################################################################## # Send an alert each time the filter is started func start_alert { $gm["ATi"] = "TCP filter v3.2 just started"; alertGMF(1, 7500020, $gm); } start_alert(); ################################################################## # alert_gmf() ################################################################## # Just built a GMF alert packet and send it func alert_gmf { $gm["AT[]SA[]Dp"] = tcp.destport; $gm["AT[]SA[]Sp"] = tcp.sourceport; $gm["AT[]HA[]D"] = ip.dest; $gm["AS[]HA[]D"] = ip.source; $gm["ATi"] = $2; alertGMF(1, $1, $gm); } ################################################################## # queue_alert() ################################################################## # Just built a GMF alert packet and send it func queue_alert { $key = cat($1,"+",$2,"+",$3); #key = "++" if (QUEUE[$key] == NULL) { QUEUE[$key] = [$4]; } else { QUEUE[$key] = listadd(QUEUE[$key],$4); } } ################################################################## # header_sanity_checker() ################################################################## # Does some basic checks on the TCP header to ensure its # compliance with TCP related RFCs: # - check that reserved flags are set to 0 # - check that if urg flag is set, then urgent pointer too # and that if urg flag not set, urgent pointer is indeed null # - reverse the TCP options, comparing them to the list of # known options, and alerting if anything unknown|malformed # occurs func header_sanity_checker { $window = $1; $reserved = $2; $flag = $3; $urgent = $4; $options = $5; #-------------------------------------- #check that reserved flags are set to 0 (avoid tunnels) if ( CHECK_RESERVED_TCP_FLAGS && ($reserved != 0) ) { queue_alert(ip.src, ip.dst, "TCP_RES_FLAG", tcp.destport); } #-------------------------------------- #urgent pointer check if (CHECK_URGENT) { $urg_flag = $flag & 32; if ($urg_flag && !$urgent ) { queue_alert(ip.src, ip.dst, "URG_F_NOT_P", tcp.destport); } if (!$urg_flag && $urgent ) { queue_alert(ip.src, ip.dst, "URG_P_NOT_F", tcp.destport); } } #-------------------------------------- #TCP options checker $l = strlen($options); $i = 0; while ($i<$l) { $number = ubyte($options,$i); if ($number inside SHORT_TCP_OPTIONS) { $i = $i + 1; } else if ($number inside LONG_TCP_OPTIONS) { if ($i+1 == $l) { alert_gmf(7500023,"incomplete TCP option: header only"); break; } $size = ubyte($options,$i+1); $i = $i +$size; if ( $i > $l ) { alert_gmf(7500024,"invalid TCP option size"); break; } } else { $msg = cat("unknown TCP option used: ",$number); alert_gmf(7500025,$msg); break; } } } ################################################################## # dos_checker() ################################################################## # Check this packet for a few known TCP Denial of Service: # - land & latierra # - winuke func dos_checker { $ip_src = $1; $ip_dst = $2; $port_src = $3; $port_dst = $4; $flag = $5; #-------------------------------------- # latierra|land check if ( ($ip_src == $ip_dst) && ($port_src == $port_dst) ) { alert_gmf(7500026,"TCP DoS: latierra|land detected"); } #-------------------------------------- # winuke check if ( ($flag & URG) && ($port_dst == 139) ) { alert_gmf(7500027,"TCP DoS: possible winuke detected"); } } ################################################################## # check_session() ################################################################## # Receives the flag field of up to the 4 first TCP packets of # a connection, and analyses them to identify a DoS or scan func check_session { $packets = $5; #packet list (up to 3 first packets) $timeout = $6; #1 if check_session called after a timeout on session #0 if it is called after receiving 2 FIN | 1 RST $len = listlen($packets); if ($len == 2) { $p1 = elem($packets,0); $p2 = elem($packets,1); # 'fin-urg-psh, rst, 0' (Xmas tree scan) if ( ($p1 == XMAS) && ($p2 & RST) ) { queue_alert($1, $2, "XMAS", $4); } # 'fin, rst, 0' (stealth FIN scan) else if ( ($p1 & FIN) && ($p2 & RST) ) { queue_alert($1, $2, "F_R", $4); } # 'syn, rst, 0' (Half Open scan) else if ( ($p1 & SYN) && ($p2 & RST) ) { queue_alert($1, $2, "S_R", $4); } # 'all-flag-off, rst, 0' (Null scan) else if ( ($p1 == NO_FLAG) && ($p2 & RST)) { queue_alert($1, $2, "NULL", $4); } # 'ack, rst, 0' (ACK scan) else if ( ($p1 & ACK) && ($p2 & RST) ) { queue_alert($1, $2, "A_R", $4); } # 'syn, syn-ack, 0' (single SYN) else if ( ($p1 & SYN) && ($p2 & SYN_ACK) && $timeout) { queue_alert($1, $2, "S_SA", $4); } } else if ($len == 3) { $p1 = elem($packets,0); $p2 = elem($packets,1); $p3 = elem($packets,2); if ( ($p1 & SYN) && ($p2 & SYN_ACK) ) { # 'syn, syn-ack, rst, 0' (Half Open scan) if ($p3 & RST) { queue_alert($1, $2, "S_SA_R", $4); } # 'syn, syn-ack, fin, 0' (half open bis) else if ($p3 & FIN) { queue_alert($1, $2, "S_SA_F", $4); } # 'syn, syn-ack, ack, 0' (connect dos) else if ( ($p3 & ACK) && $timeout ) { queue_alert($1, $2, "Unused TCP", $4); } } } } ################################################################## # session_logger() ################################################################## # Monitor a few parameters in order to monitor the session: # - the time of th last packet received, and the number of FIN # got, in order to detect the end of session # - the last ack number for both side of the connection, with the # number of ack carrying these same value -> detect ack storms # - the number of consecutive nul window size acknowledgments # - the 4 first packets of a tcp transaction. # # This log is analysed every minutes by 'log_analyser' to detect # timedout connections coreesponding to scans and DoS func session_logger { $ip_src = $1; $ip_dst = $2; $port_src = $3; $port_dst = $4; $flag = $5; $ack = $6; $window = $7; $data_length = $8; #don't log sessions that are running on UNCHECKED_PORTS if ( ($port_src inside UNCHECKED_PORTS) || ($port_dst inside UNCHECKED_PORTS) ) { return; } #log start of session, timestamps $src2dst = 1; # =1 if packet goes from source to dest of connection $key = cat($ip_src,"-",$ip_dst,"-",$port_src,"-",$port_dst); $key2 = cat($ip_dst,"-",$ip_src,"-",$port_dst,"-",$port_src); #-------------------------------------- # check if this session is already registered # and if not, create an entry for it if (LOG[$key] == NULL) { if (LOG[$key2] == NULL) #create a log entry for new session { LOG[$key] = [ $ip_src, $ip_dst, $port_src, $port_dst, system.time, 0, 0,0,0,0, 0,0]; } else #session already logged, but from dest to source { $key = $key2; $src2dst = 0; } } #-------------------------------------- # extract the previous log for this packet $list = LOG[$key]; $ip_src = elem($list,0); $ip_dst = elem($list,1); $port_src = elem($list,2); $port_dst = elem($list,3); $time = elem($list,4); #time at which last packet was received. to find timedout connections $fin_rst = elem($list,5); #count FIN & RST, to find ended connections $ack_s2d = elem($list,6); #count consecutive similar ACKs $ack_s2d_cnt = elem($list,7); #to detect ACK storms $ack_d2s = elem($list,8); $ack_d2s_cnt = elem($list,9); $wnd_s2d_cnt = elem($list,10); #count consecutive nul windows $wnd_d2s_cnt = elem($list,11); #to detect TCP buffers exhaustion $packets = sublist($list,12); #the flags of the up to 3 first packets in connection #-------------------------------------- # update this log # update time $time = system.time; # update FIN|RST count if ($flag & FIN) { $fin_rst = $fin_rst +1; } if ($flag & RST) { $fin_rst = $fin_rst +10;} # update ACK counts # for ACK storm detector if ($data_length) #if ACK in data packet -> can't belong to an ack storm { $ack_s2d == 0; $ack_d2s == 0; } else { if ($src2dst) { if ($ack == $ack_s2d) { $ack_s2d_cnt = $ack_s2d_cnt + 1; } else { $ack_s2d = $ack; $ack_s2d_cnt = 0; } } else { if ($ack == $ack_d2s) { $ack_d2s_cnt = $ack_d2s_cnt + 1; } else { $ack_d2s = $ack; $ack_d2s_cnt = 0; } } } # update window if ($window == 0) { if ($src2dst) { $wnd_s2d_cnt = $wnd_s2d_cnt + 1; } else { $wnd_d2s_cnt = $wnd_d2s_cnt + 1; } } else { if ($src2dst) { $wnd_s2d_cnt = 0; } else { $wnd_d2s_cnt = 0; } } # update packet log if (listlen($packets)<3) { $packets = listadd($packets, $flag); } #-------------------------------------- # test ACK storms & nul window size #test ACK storm if ( ($ack_s2d_cnt > MAX_ACK) && ($ack_d2s_cnt > MAX_ACK) ) { $ack_s2d_cnt = 0; $ack_d2s_cnt = 0; alert_gmf(7500029,"ACK storm between src and dst (TCP hijacking ?)"); } #test window size if ($wnd_s2d_cnt > MAX_NUL_WINDOWS) { $wnd_s2d_cnt = 0; $st = "TCP buffer exhaustion at source host"; alert_gmf(7500030,$st); } else if ($wnd_d2s_cnt > MAX_NUL_WINDOWS) { $wnd_d2s_cnt = 0; $st = "TCP buffer exhaustion at destination host"; alert_gmf(7500030,$st); } #-------------------------------------- # check if connection closed # ie if 1 RST or 2 FIN got # and wether this was a scan|dos or not if ($fin_rst > 1) { check_session($ip_src, $ip_dst, $port_src, $port_dst, $packets,0); LOG[$key] = NULL; } else { #-------------------------------------- # register new log # { ipsrc, ipdst, portsrc, portdst, time_last, nbr_FIN_exchanged+RST_got, last_ack_src2dst, ack_src2dst_count, last_ack_dst2src, ack_dst2src_count, count_nul_window_src2dst, count_nul_window_dst2src, packet1, [packet2 [packet3 [packet4]]] } $list = [$ip_src, $ip_dst, $port_src, $port_dst, $time, $fin_rst, $ack_s2d, $ack_s2d_cnt, $ack_d2s, $ack_d2s_cnt, $wnd_s2d_cnt, $wnd_d2s_cnt]; foreach $e inside ($packets) { $list = listadd($list,$e); } LOG[$key] = $list; } } ################################################################## # tcp_filter ################################################################## # Hook on every TCP packet. # For each packets, the following actions are taken: # - a check on the header compliancy with RFCs # - a check for known TCP based DoS # - some properties of this packet are logged for further # analysis by the various periodic 'analyser' filters filter tcp_filter ip (proto: 6) #hook on ip packets carrying tcp { #-------------------------------------- # extract tcp header #-------------------------------------- $length = (ubyte(ip.blob, 12) & 240)/4; #header length in bytes $header = substr(ip.blob,0,$length); $data_length = ip.len; #length of the data in ip packet #-------------------------------------- # disassemble header #-------------------------------------- $ip_src = ip.src; $ip_dst = ip.dst; $port_src = ubyte($header,0)*256 + ubyte($header,1); $port_dst = ubyte($header,2)*256 + ubyte($header,3); #we consider only the first 3 bytes in the 4 byte value of the ACK-SEQ: reasonable assumption #necessary because of problems with too large integers $seq = ubyte($header,5)*65536 + ubyte($header,6)*256 + ubyte($header,7); #$seq = ubyte($header,4)*16777216 + ubyte($header,5)*65536 + ubyte($header,6)*256 + ubyte($header,7); $ack = ubyte($header,9)*65536 + ubyte($header,10)*256 + ubyte($header,11); #$ack = ubyte($header,8)*16777216 + ubyte($header,9)*65536 + ubyte($header,10)*256 + ubyte($header,11); $flag = ubyte($header,13) & 63; $reserved = (ubyte($header,12) & 15) + (ubyte($header,13) & 192); $window = ubyte($header,14)*256 + ubyte($header,15); #cheksum not interesting: too often corrupted $urgent = ubyte($header,18)*256 + ubyte($header,19); if ($length > 20) { $options = substr($header,20); } else { $options = ""; } #-------------------------------------- # check header compliance with RFCs #-------------------------------------- header_sanity_checker($window,$reserved,$flag,$urgent,$options); #-------------------------------------- # check for known DoS #-------------------------------------- dos_checker($ip_src,$ip_dst,$port_src,$port_dst,$flag); #-------------------------------------- # log session parameters, to detect scans, # DoS & TCP hijacking #-------------------------------------- session_logger($ip_src,$ip_dst,$port_src,$port_dst,$flag,$ack,$window,$data_length); } ################################################################# # LOG Analyser # detect timed-out TCP connections ################################################################# filter log_analyser timeout ( sec: 300, repeat ) { if (!PREVIOUS_FINISHED) { LOG = NULL; PREVIOUS_FINISHED = 1; return; } PREVIOUS_FINISHED = 0; $now = system.time; #find timed out connections and check if they are known scans|dos $count = 0; foreach $key inside (LOG) { $count = $count + 1; $list = LOG[$key]; $ip_src = elem($list,0); $ip_dst = elem($list,1); $port_src = elem($list,2); $port_dst = elem($list,3); $time = elem($list,4); #time at which last packet was received. to find timedout connections $packets = sublist($list,12); if ( ($now - $time) > TIME_OUT) { #the last variable in call: 1 tells 'check_session' that it is called after a timeout check_session($ip_src, $ip_dst, $port_src, $port_dst, $packets, 1); LOG[$key] = NULL; } } if (COUNT_CONNECTIONS) { $gm["ATi"] = cat($count," opened connections"); alertGMF(7500031, $1, $gm); } PREVIOUS_FINISHED = 1; } ################################################################# # port_count() ################################################################# # given a list of port numbers, return an array with as keys # the different port numbers, and as values the amount of # occurence of each of these port numbers func port_count { $ports = NULL; foreach $elem inside ($1) { if ($ports[$elem] == NULL) { $ports[$elem] = 1; } else { $ports[$elem] = $ports[$elem] + 1; } } return $ports; } ################################################################# # port_list() ################################################################# # returns a string containing the list of the target ports # take in input the result of 'port_count()' func port_list { $st = ""; foreach $i inside ($1) { $st = cat($st,",",$i); } return $st; } ################################################################# # Alarm Queue Analyser # process the last queued alarms, and send alarms if dangerous # activity detected ################################################################# filter log_analyser timeout ( sec: 120, repeat ) { foreach $key inside (QUEUE) { $list = split($key,"+"); $ipsrc = elem($list,0); $ipdst = elem($list,1); $alarm = elem($list,2); $count = listlen(QUEUE[$key]); ##debugging alert #$gm["ATi"] = cat("count: ",$count," key: ",$key," alarm: ",$alarm," msg: ",MSG[$alarm]," num: ",NUM[$alarm]); #alertGMF(1, 2, $gm); #TCP reserved flag or TCP URG flag|pointer desynch if ( (NUM[$alarm] == 7500021) | (NUM[$alarm] == 7500022) ) { #send a summary $gm["AS[]HA[]D"] = $ipsrc; #ip source $gm["AT[]HA[]D"] = $ipdst; #ip dest #build list of destination ports $ports = port_count(QUEUE[$key]); $st = port_list($ports); $gm["ATi"] = cat($count," ",MSG[$alarm]," to ports: ",$st); alertGMF(1, NUM[$alarm], $gm); } else if (NUM[$alarm] == 7500028) #Scan { #if more than 10 'Unused TCP connections' on same connect -> alert DoS if ( ($alarm == "Unused_TCP") && ($count > 10) ) { $gm["AS[]HA[]D"] = $ipsrc; #ip source $gm["AT[]HA[]D"] = $ipdst; #ip dest #need to limit size of array processed by port_count, otherwise NFR crashes $st = "too many ports."; if ($count < 300) { $ports = port_count(QUEUE[$key]); $st = port_list($ports); } $gm["ATi"] = cat($count," unused TCP connections (DoS?). To port: ",$st); alertGMF(1, 7500028, $gm); } else #it was some other scan pattern { #need to limit size of array processed by port_count, otherwise NFR crashes if ($count > 300) { $gm["AS[]HA[]D"] = $ipsrc; #ip source $gm["AT[]HA[]D"] = $ipdst; #ip dest $st = port_list($ports); $gm["ATi"] = cat($count," ",MSG[$alarm]," ports scanned. Too many to be listed."); alertGMF(1, 7500028, $gm); } else { $ports = port_count(QUEUE[$key]); #if more than 2 connect to same port, it's probably an application trying #to connect to a closed server #-> look for scan patterns repeated less 3 times $closed_server = 0; foreach $p inside ($ports) { if ($ports[$p] > 2) { $closed_server = 1; break; } } #if it was not a closed server, it might be a scan ! if (!$closed_server) { $gm["AS[]HA[]D"] = $ipsrc; #ip source $gm["AT[]HA[]D"] = $ipdst; #ip dest $st = port_list($ports); $gm["ATi"] = cat($count," ",MSG[$alarm]," ports scanned: ",$st); alertGMF(1, 7500028, $gm); } } } } #remove entry from alarm queue QUEUE[$key] = NULL; } } ################################################################# # NFR related TCP exceptions ################################################################# filter ex1 except (tcpdatalength) { alert_gmf(7500032,"truncated data in reassembled TCP data stream"); } filter ex2 except (tcpheaderlength) { alert_gmf(7500033,"truncated header in reassembled TCP data stream"); }