3 ################################################################################
5 # File: psad (/usr/sbin/psad)
7 # URL: http://www.cipherdyne.org/psad/
9 # Purpose: psad makes use of iptables logs to detect port scans,
10 # probes for backdoors and DDoS tools, and other suspect traffic
11 # (many signatures were adapted from the Snort intrusion
12 # detection system). Data is provided by parsing syslog
13 # firewall messages out of /var/log/messages (or wherever syslog
14 # is configured to write iptables logs to).
16 # For more information read the psad man page or view the
17 # documentation provided at: http://www.cipherdyne.org/psad/
19 # Author: Michael Rash (mbr@cipherdyne.org)
21 # Credits: (see the CREDITS file bundled with the psad sources.)
25 # Copyright (C) 1999-2012 Michael Rash (mbr@cipherdyne.org)
27 # Reference: Snort is a registered trademark of Sourcefire, Inc.
29 # License (GNU Public License):
31 # This program is distributed in the hope that it will be useful,
32 # but WITHOUT ANY WARRANTY; without even the implied warranty of
33 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
34 # GNU General Public License for more details.
36 # You should have received a copy of the GNU General Public License
37 # along with this program; if not, write to the Free Software
38 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
41 # TODO: (see the TODO file bundled with the psad sources.)
43 # Default behavior is as follows. Each of these features can be disabled
44 # with command line arguments:
46 # - passive OS fingerprinting = yes
47 # - snort rule matching = yes
48 # - write fw errors to error log = yes
50 # - reverse dns lookups = yes
51 # - validate firewall rules = yes
52 # - whois lookups of scanning IPs = yes
53 # - parse netstat output for local server ports = yes
55 # Coding Style: All configuration variables from psad.conf are stored in
56 # the %config hash by keys that are in capital letters. This is
57 # the only place in the code where capital letters will be used in
58 # variables names. There are several variables with file-scope, and
59 # these variables are clearly commented near the top of each of the
60 # psad daemons. Lines are generally limited to 80 characters for easy
63 # Scan hash key explanation:
64 # absnum - Total number of packets from $src to $dst
65 # chain - iptables chain under which the scan packets appear in the
67 # s_time - Start time for the first packet seen from src to dst.
68 # alerted - An alert has been sent.
69 # pkts - Number of packets (used for signatures and a packet counter
70 # for the current interval.
71 # flags - Keeps track of tcp flags.
72 # sid - Signature tracking
73 # abs_sp - Absolute starting port.
74 # abs_ep - Absolute ending port.
75 # strtp - Starting port.
78 # Sample iptables log messages:
80 # Sample tcp packet (rejected by iptables... --log-prefix = "DROP ")
82 # Mar 11 13:15:52 orthanc kernel: DROP IN=lo OUT= MAC=00:00:00:00:00:00:00:00:
83 # 00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.1 LEN=60 TOS=0x00 PREC=0x00
84 # TTL=64 ID=0 DF PROTO=TCP SPT=44847 DPT=35 WINDOW=32304 RES=0x00 SYN URGP=0
86 # Sample icmp packet rejected by iptables INPUT chain:
88 # Nov 27 15:45:51 orthanc kernel: DROP IN=eth1 OUT= MAC=00:a0:cc:e2:1f:f2:00:
89 # 20:78:10:70:e7:08:00 SRC=192.168.10.20 DST=192.168.10.1 LEN=84 TOS=0x00
90 # PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=61055 SEQ=256
92 # Sample icmp packet logged through FORWARD chain:
94 # Aug 20 21:23:32 orthanc kernel: SID365 IN=eth2 OUT=eth1 SRC=192.168.20.25
95 # DST=192.168.10.15 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=ICMP TYPE=8
96 # CODE=0 ID=19467 SEQ=256
98 # Occasionally the kernel klogd ring buffer must become full since log
99 # entries are sometimes generated by a long port scan like this (note
100 # there is no 'DPT' field):
102 # Mar 16 23:50:25 orthanc kernel: DROP IN=lo OUT= MAC=00:00:00:00:00:00:00:
103 # 00:00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.1 LEN=60 TOS=0x00 PREC=0x00
104 # TTL=64 ID=0 DF PROTO=TCP SPT=39935 DINDOW=32304 RES=0x00 SYN URGP=0
106 # Note on iptables tcp log messages:
108 # iptables reports tcp flags in the following order:
110 # URG ACK PSH RST SYN FIN
112 # Files specification for /var/log/psad/<srcip> directories:
114 # psad creates a new directory "/var/log/psad/<src>" for each new <src>
115 # from which a scan is detected. Under this directory several files are
118 # danger_level - Overall danger level aggregated for all scans.
119 # p0f_guess - Passive OS fingerprint guess for <src>.
120 # <src/dst>_whois - Whois information for <src> (or <dst> if src
122 # <dst>_email_ctr - Total email alerts sent for <src>.
123 # <dst>_email_alert - The most recent email alert for <dst>.
124 # <dst>_packet_ctr - Packet counters for <dst>.
125 # <dst>_signatures - Signatures detected against <dst>.
127 # Note that some of the files above contain the destination address since a
128 # single source address may scan several destination addresses.
130 ###############################################################################
133 ### modules used by psad
141 use Getopt::Long 'GetOptions';
144 ### ========================== main =================================
146 ### set the current version
149 ### default config file for psad (can be changed with
151 my $config_file = '/etc/psad/psad.conf';
153 ### this will be set to either FW_DATA_FILE, ULOG_DATA_FILE
154 ### or IPT_SYSLOG_FILE
155 my $fw_data_file = '';
157 ### disable debugging by default
159 my $debug_sid = 0; ### debug a specific signature
163 ### build the iptables blocking configuration out of the
164 ### IPT_AUTO_CHAIN variable
167 ### main configuration hash
169 my $override_config_str = '';
174 ### fw search string array
177 ### socket for --fw-block
183 ### main psad data structure; contains ips, port ranges,
184 ### protocol info, tcp flags, etc.
187 ### cache scan danger levels
190 ### cache scan email counters
191 my %scan_email_ctrs = ();
193 ### cache executions of external script (only used if
194 ### ENABLE_EXT_SCRIPT_EXEC is set to 'Y');
195 my %scan_ext_exec = ();
197 ### cache p0f-based passive os fingerprinting information
200 ### cache p0f-based passive os fingerprinting signature information
201 my %p0f_ipv4_sigs = ();
202 my %p0f_ipv6_sigs = ();
204 ### cache TOS-based passive os fingerprinting information
207 ### cache TOS-based passive os fingerprinting signature information
210 ### cache valid icmp types and corresponding codes
211 my %valid_icmp_types = ();
212 my %valid_icmp6_types = ();
214 ### Cache snort rule messages unless --no-snort-sids switch was
215 ### given. This is only useful if iptables includes rules
216 ### that log things like "SID123". "fwsnort"
217 ### (http://www.cipherdyne.org/fwsnort/) will automatically
218 ### build such a ruleset from snort signatures.
219 my %fwsnort_sigs = ();
221 ### Cache snort classification.config file for class priorities
222 my %snort_class_dl = ();
224 ### Cache any individual Snort rule priority definitions from
225 ### the snort_rule_dl file
226 my %snort_rule_dl = ();
228 ### Cache Snort rule reference configuration
229 my %snort_ref_baseurl = ();
231 ### cache all scan signatures from /etc/psad/signatures file
234 my %sig_ip_objs = ();
236 ### cache iptables prefixes
237 my %ipt_prefixes = ();
240 my %ignore_ports = ();
243 my %ignore_protocols = ();
245 ### ignore interfaces
246 my %ignore_interfaces = ();
248 ### data array used for dshield.org logs
249 my @dshield_data = ();
251 ### track the last time we sent an alert to dshield.org
252 my $last_dshield_alert = '';
254 ### calculate how often a dshield alert will be sent
255 my $dshield_alert_interval = '';
257 ### dshield stats counters
258 my $dshield_email_ctr = 0;
259 my $dshield_lines_ctr = 0;
261 ### get the current timezone for dshield (this is calculated
262 ### and re-calculated since the timezone may change).
265 ### get the current year for dshield
268 ### keep track of how many CHECK_INTERVALS have elapsed; this is
269 ### useful for TOP_SCANS_CTR_THRESHOLD
270 my $check_interval_ctr = 0;
272 ### track the number of scan IP pairs for MAX_SCAN_IP_PAIRS thresholding
273 my $scan_ip_pairs = 0;
275 ### %auto_dl holds all ip addresses that should automatically
276 ### be assigned a danger level (or ignored).
278 my %auto_dl_ip_objs = ();
279 my %auto_assigned_msg = ();
281 ### cache the source ips that we have automatically blocked
282 ### (if ENABLE_AUTO_IDS == 'Y')
283 my %auto_blocked_ips = ();
285 ### counter to check psad iptables chains and jump rules
286 my $iptables_prereq_check = 0;
288 ### cache the addresses we have issued dns lookups against.
291 ### cache the addresses we have executed whois lookups against.
292 my %whois_cache = ();
294 ### cache ports the local machine is listening on (periodically
295 ### updated by get_listening_ports()).
296 my %local_ports = ();
298 ### cache the ip addresses associated with each interface on the
302 ### Top attacking statistics
303 my %top_tcp_ports = ();
304 my %top_udp_ports = ();
305 my %top_udplite_ports = ();
307 my %sig_sources = ();
308 my %top_sig_counts = ();
309 my %top_packet_counts = ();
312 ### regex to match IP addresses
313 my $ipv4_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|; ### IPv4
315 ### IPv6 - full version in ip6tables logs
316 my $ipv6_re = qr|(?:[a-f0-9]{4}:){7}(?:[a-f0-9]{4})|i;
318 ### ttl values are decremented depending on the number of hops
319 ### the packet has taken before it hits the firewall. We will
320 ### assume packets will not jump through more than 20 hops on
324 ### per protocol packet counters
325 my @protocols = qw/tcp udp udplite icmp icmp6/;
331 ### initialize and scope some default variables (command
332 ### line args can override some default values)
335 my $auto_dl_file = '';
336 my $snort_rules_dir = '';
337 my $srules_type = '';
338 my $cmdline_file = '';
339 my $analyze_mode = 0;
340 my $analysis_fields = '';
341 my $analysis_tokens_ar = [];
342 my $analysis_match_criteria_ar = [];
343 my $get_next_rule_id = 0;
345 my $syslog_server = 0;
348 my $restrict_ip = '';
349 my $restrict_ip_cmdline = '';
352 my $status_min_dl = 0;
353 my $status_summary = 0;
354 my $fw_list_auto = 0;
355 my $fw_block_ip = '';
356 my $fw_rm_block_ip = '';
357 my $fw_del_chains = 0;
358 my $gnuplot_mode = 0;
359 my $gnuplot_year = 0;
360 my $gnuplot_prev_mon = 0;
361 my $gnuplot_title = '';
362 my $gnuplot_legend_title = '';
363 my $gnuplot_grayscale = 0;
364 my $gnuplot_x_label = '';
365 my $gnuplot_x_range = '';
366 my $gnuplot_y_label = '';
367 my $gnuplot_y_range = '';
368 my $gnuplot_z_label = '';
369 my $gnuplot_z_range = '';
371 my $gnuplot_view = '';
372 my $gnuplot_sort_style = 'value';
373 my $gnuplot_graph_style = '';
374 my $gnuplot_count_type = '';
375 my $gnuplot_count_element = -1;
376 my %gnuplot_cache_uniq = ();
377 my @gnuplot_data = ();
378 my $gnuplot_data_file = 'psad_iptables.dat';
379 my $gnuplot_plot_file = 'psad_iptables.gnu';
380 my $gnuplot_png_file = 'psad_iptables.png';
381 my $gnuplot_file_prefix = '';
382 my $gnuplot_template_file = '';
384 my $gnuplot_interactive = 0;
385 my $plot_separator = ', '; ### default to CSV format for plot data
389 my $csv_print_uniq = 0;
390 my $csv_line_limit = 0;
391 my $csv_start_line = 0;
392 my $csv_end_line = 0;
394 my $csv_neg_regex = '';
395 my $csv_have_timestamp = 0;
396 my $dump_ipt_policy = 0;
397 my $fw_include_ips = 0;
408 my $download_sigs = 0;
409 my $chk_interval = 0;
410 my $log_len = 23; ### used in scan_logr()
415 my $analysis_emails = 0;
416 my $analysis_whois = 0;
417 my $enable_analysis_dns = 0;
418 my $netstat_lkup_ctr = 0;
419 my $kmsgsd_started = 0;
422 my $skip_first_loop = 1;
423 my $cmdl_interface = '';
424 my $analyze_write_data = 0;
425 my $local_ips_lkup_ctr = 0;
426 my $num_hash_marks = 76; ### for gnuplot output
427 my $imported_syslog_module = 0;
429 ### these flags are used to disable several features
430 ### in psad if specified from the command line
431 my $no_snort_sids = 0;
432 my $no_signatures = 0;
433 my $no_icmp_types = 0;
434 my $no_icmp6_types = 0;
438 my $no_ipt_errors = 0;
444 my $no_email_alerts = 0;
445 my $no_syslog_alerts = 0;
448 my $tcp_nop_type = 1;
449 my $tcp_mss_type = 2;
450 my $tcp_win_scale_type = 3;
451 my $tcp_sack_type = 4;
452 my $tcp_timestamp_type = 8;
454 my %tcp_p0f_opt_types = (
455 'N' => $tcp_nop_type,
456 'M' => $tcp_mss_type,
457 'W' => $tcp_win_scale_type,
458 'S' => $tcp_sack_type,
459 'T' => $tcp_timestamp_type
465 my $ICMP_ECHO_REQUEST = 8;
466 my $ICMP_ECHO_REPLY = 0;
467 my $ICMP6_ECHO_REQUEST = 128;
468 my $ICMP6_ECHO_REPLY = 129;
470 ### These are not directly support by psad because they
471 ### do not appear in iptables logs; however, several of
472 ### these options are supported if fwsnort is also running.
473 my @unsupported_snort_opts = qw(
493 ); ### the ip_proto keyword could be supported, but would require
494 ### refactoring parse_NF_pkt_str().
496 ### for Snort signature sp/dp matching
498 {'sp' => 'norm', 'dp' => 'norm'},
499 {'sp' => 'norm', 'dp' => 'neg'},
500 {'sp' => 'neg', 'dp' => 'norm'},
501 {'sp' => 'neg', 'dp' => 'neg'},
504 ### main packet data structure
510 'intf' => '', ### FIXME in and out interfaces?
519 'ip_opts' => '', ### v4 or v6
549 ### extra fields for psad internals (DShield reporting, fwsnort
550 ### sid matching, iptables logging prefixes and chains, etc.)
553 'fwsnort_estab' => 0,
561 my %gnuplot_non_digit_packet_fields = (
562 ### 'hashentry' - maps the field to an integer based on whether
563 ### it has been seen before
564 ### 'intf2int' - converts interface number to an integer (e.g. eth0 -> 0)
565 ### 'ip2int' - converts IP address to integer representation
568 'src_mac' => 'hashentry',
569 'dst_mac' => 'hashentry',
570 'intf' => 'intf2int',
575 'proto' => 'proto2int',
576 'tos' => 'hashentry',
577 'ip_opts' => 'hashentry',
578 'frag_bit' => 'hashentry',
581 'flags' => 'hashentry',
582 'tcp_opts' => 'hashentry',
584 ### extra fields for psad internals (DShield reporting, fwsnort
585 ### sid matching, iptables logging prefixes and chains, etc.)
586 'chain' => 'hashentry',
587 'log_prefix' => 'hashentry',
588 'dshield_str' => 'hashentry',
589 'syslog_host' => 'hashentry',
591 my %gnuplot_non_digit_map = ();
592 my %ip2int_cache = ();
593 my %gnuplot_ip2int = ();
595 ### packet parsing return values
600 ### icmp header validation
601 my $BAD_ICMP_TYPE = 1;
602 my $BAD_ICMP_CODE = 2;
605 my $NO_SIG_MATCH = 0;
608 my $TCP_HEADER_LEN = 20; ### excludes options
609 my $TCP_MAX_OPTS_LEN = 44;
610 my $UDP_HEADER_LEN = 8;
611 my $ICMP_HEADER_LEN = 4;
612 my $IP_HEADER_LEN = 20; ### excludes options
614 ### save a copy of the command line arguments
617 ### handle command line args
620 ### Everthing after this point must be executed as root (psad
621 ### only needs root if run in auto-blocking mode; should take
622 ### this into account and drop privileges).
625 ### Import all psad configuration and signatures files
626 ### (psad.conf, posf, signatures, psad_icmp_types,
627 ### and auto_dl), and call setup().
630 ### check to make sure another psad process is not already running.
631 &unique_pid($config{'PSAD_PID_FILE'});
633 ### get the ip addresses that are local to this machine
636 ### get the current services running on this machine
637 &get_listening_ports() unless ($no_netstat);
639 ### daemonize psad unless running with --no-daemon or an
641 unless ($no_daemon or $debug) {
644 die "[*] $0: Couldn't fork: $!" unless defined $pid;
645 POSIX::setsid() or die "[*] $0: Can't start a new session: $!";
648 ### write the current pid associated with psad to the psad pid file
649 &write_pid($config{'PSAD_PID_FILE'});
651 ### write the command line args used to start psad to $cmdline_file
652 &write_cmd_line(\@args_cp, $cmdline_file)
655 ### psad requires that kmsgsd is running to receive any data (unless
656 ### SYSLOG_DAEMON is set to ulogd or psad is configured to acquire data
657 ### from a normal file via IPT_SYSLOG_FILE), so let's start it here for good
658 ### measure (as of 0.9.2 it makes use of the pid files and unique_pid(),
659 ### so we don't have to worry about starting a duplicate copy). While
660 ### we're at it, start psadwatchd as well. Note that this is the best
661 ### place to start the other daemons since we just wrote the psad pid
662 ### to PID_FILE above.
664 unless ($config{'ENABLE_SYSLOG_FILE'} eq 'Y'
666 or $config{'SYSLOG_DAEMON'} =~ /ulog/i
667 or $kmsgsd_started) {
668 $cmd = $cmds{'kmsgsd'};
669 $cmd .= " -c $config_file";
670 $cmd .= " -O $override_config_str"
671 if $override_config_str;
672 open KMSGSD, "| $cmd" or die "[*] Could not execute $cmds{'kmsgsd'}";
677 unless ($kmsgsd_started) {
678 my $pid = &is_running($pidfiles{'kmsgsd'});
681 kill 9, $pid unless kill 15, $pid;
683 unlink $pidfiles{'kmsgsd'} if -e $pidfiles{'kmsgsd'};
686 unless ($debug or $no_daemon) {
687 $cmd = $cmds{'psadwatchd'};
688 $cmd .= " -c $config_file";
689 $cmd .= " -O $override_config_str"
690 if $override_config_str;
691 open PSADWATCHD, "| $cmd" or die "[*] Could not ",
692 "execute $cmds{'psadwatchd'}";
696 if ($config{'ENABLE_AUTO_IDS'} eq 'Y') {
697 ### always flush old rules (the subsequent renew_auto_blocked_ips()
698 ### will re-instantiate any that should not have been expired).
699 &flush_auto_blocked_ips() if $config{'FLUSH_IPT_AT_INIT'} eq 'Y';
701 ### Check to see if psad automatically blocked some IPs from
702 ### a previous run. This feature is most useful for preserving
703 ### auto-block rules for IPs after a reboot or after restarting
704 ### psad. (Note that ENABLE_AUTO_IDS is disabled by psad_init()
705 ### if we are running on a syslog server or if we are running
707 &renew_auto_blocked_ips();
710 ### Install signal handlers for debugging %scan with Data::Dumper,
711 ### and for reaping zombie whois processes.
712 $SIG{'__WARN__'} = \&warn_handler;
713 $SIG{'__DIE__'} = \&die_handler;
714 $SIG{'CHLD'} = \&REAPER;
715 $SIG{'USR1'} = \&usr1_handler;
716 $SIG{'HUP'} = \&hup_handler;
718 if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y') {
719 $last_dshield_alert = time() unless $last_dshield_alert;
722 ### Initialize current time for disk space checking.
723 my $last_disk_check = time();
725 if ($config{'IMPORT_OLD_SCANS'} eq 'Y') {
726 ### import old scans and counters from /var/log/psad/
728 } elsif ($config{'ENABLE_SCAN_ARCHIVE'} eq 'Y') {
734 ### zero out the packet counter file (the counters
735 ### are all zero at this point anyway unless we
736 ### imported old scans).
737 &write_global_packet_counters();
739 ### zero out prefix counters
740 &write_prefix_counters();
742 ### zero out dshield alert stats (note we do this here regardless of
743 ### whether DShield alerting is enabled since if it isn't we will
744 ### just zero out the counters).
745 &write_dshield_stats();
747 my $fw_data_file_size = -s $fw_data_file;
748 my $fw_data_file_inode = (stat($fw_data_file))[1];
750 my $fw_data_file_check_ctr = 0;
752 ### Get an open filehandle for the main firewall data file FW_DATA_FILE.
753 ### All firewall drop/reject log messages are written to FW_DATA_FILE
754 ### by kmsgsd (or by ulogd directly).
755 print STDERR "[+] Opening iptables data file: $fw_data_file\n" if $debug;
756 open FWDATA, $fw_data_file or die '[*] Could not open ',
759 &get_auto_response_domain_sock()
760 if $config{'ENABLE_AUTO_IDS'} eq 'Y';
762 ###=========================================================###
763 ###### MAIN LOOP ######
764 ###=========================================================###
767 ### scope and clear the firewall data array
770 ### for --fw-block <ip>
771 my @add_ipt_addrs = ();
775 &sys_log('received HUP signal, ' .
776 're-importing psad.conf');
778 print STDERR "[+] Received HUP signal, re-importing config...\n"
781 my $orig_fwdata = $fw_data_file;
782 my $orig_ipt_sockfile = '';
784 $orig_ipt_sockfile = $config{'AUTO_IPT_SOCK'}
785 if $config{'ENABLE_AUTO_IDS'} eq 'Y';
787 ### Re-import all used config files (psad.conf, auto_dl,
788 ### posf, signatures) if we received a HUP signal.
791 if ($orig_fwdata ne $fw_data_file) {
794 ### re-open the fwdata file
795 open FWDATA, $fw_data_file or die
796 "[*] Could not open $fw_data_file: $!";
798 $skip_first_loop = 1;
801 if ($config{'ENABLE_AUTO_IDS'} eq 'Y') {
802 if ($orig_ipt_sockfile ne $config{'AUTO_IPT_SOCK'}) {
805 &get_auto_response_domain_sock();
807 $skip_first_loop = 1;
810 $hup_flag = 0; ### clear the HUP flag
813 ### See if we need to print out the %scan datastructure
814 ### (we received a USR1 signal)
816 $usr1_flag = 0; ### clear the USR1 flag
818 &sys_log('received USR1 signal, printing scan ' .
819 "hashes to $config{'PSAD_DIR'}/scan_hash.$$");
821 ### dump scan hash to filesystem
825 ### allow the contents of the fwdata file to be processed only after
826 ### the first loop has been executed.
827 if ($skip_first_loop) {
829 $skip_first_loop = 0;
830 seek FWDATA,0,2; ### seek to the end of the file
835 ### Get any new packets have been written to
836 ### FW_DATA_FILE by kmsgsd for psad analysis.
837 @fw_packets = <FWDATA>;
839 if ($config{'ENABLE_AUTO_IDS'} eq 'Y') {
841 ### get IP from the domain socket
842 my $ipt_add_connection = $ipt_sock->accept();
843 if ($ipt_add_connection) {
844 @add_ipt_addrs = <$ipt_add_connection>;
849 if ($fw_data_file_check_ctr == 10) {
850 if (-e $fw_data_file) {
851 my $size_tmp = -s $fw_data_file;
852 my $inode_tmp = (stat($fw_data_file))[1];
853 if ($inode_tmp != $fw_data_file_inode
854 or $size_tmp < $fw_data_file_size) {
858 &sys_log('[+]', "iptables syslog file $fw_data_file " .
859 "shrank or was rotated, so re-opening");
861 ### re-open the fwdata file
862 open FWDATA, $fw_data_file or die
863 "[*] Could not open $fw_data_file: $!";
865 $skip_first_loop = 1;
867 ### set file size and inode
868 $fw_data_file_size = $size_tmp;
869 $fw_data_file_inode = $inode_tmp;
872 $fw_data_file_check_ctr = 0;
877 if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y') {
878 ### calculate the timezone offset
879 $timezone = sprintf("%.2d", (Timezone())[3]) . ':00';
883 unless ($no_netstat) {
884 ### we don't expect the list of ports the machine is listening
885 ### on to change very often.
886 if ($netstat_lkup_ctr == 10) {
887 &get_listening_ports();
888 $netstat_lkup_ctr = 0;
892 ### the local machine ip addresses could change (dhcp, etc.)
893 ### but not that often.
894 if ($local_ips_lkup_ctr == 30) {
896 $local_ips_lkup_ctr = 0;
898 $local_ips_lkup_ctr++;
900 ### Extract data and summarize scan packets, assign danger level,
901 ### send email/syslog alerts.
902 &check_scan(\@fw_packets);
906 ### log top scans data
908 if ($config{'TOP_SCANS_CTR_THRESHOLD'} == 0) {
910 } elsif ($check_interval_ctr % $config{'TOP_SCANS_CTR_THRESHOLD'} == 0) {
916 ### log the top port and signature matches
920 ### Write the number of tcp/udp/icmp packets out
921 ### to the global packet counters file
922 &write_global_packet_counters();
924 ### Write out log prefix counters
925 &write_prefix_counters();
927 if ($config{'ENABLE_AUTO_IDS'} eq 'Y') {
928 ### Timeout any auto-blocked IPs that are past due (need to
929 ### check the timeouts against existing IPs in the scan hash
930 ### even if new packets are not found).
931 if ($config{'AUTO_BLOCK_TIMEOUT'} > 0) {
932 &timeout_auto_blocked_ips();
935 ### see if we need to add any IP address from the domain
937 &check_ipt_cmd(\@add_ipt_addrs) if @add_ipt_addrs;
940 ### Send logs to dshield in dshield format
941 if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y') {
942 &dshield_email_log();
945 ### Allow disk space utilization checks to be disabled by
946 ### setting DISK_MAX_PERCENTAGE = 0.
947 if ($config{'DISK_MAX_PERCENTAGE'} > 0
948 and (time() - $last_disk_check) > $config{'DISK_CHECK_INTERVAL'}) {
949 ### See how we are doing on disk space, and remove data
951 if (&disk_space_exceeded() and $config{'ENABLE_SYSLOG_FILE'} ne 'Y') {
954 ### truncate fwdata file
955 &truncate_file($fw_data_file);
957 ### re-open the fwdata file
958 open FWDATA, $fw_data_file or die
959 "[*] Could not open $fw_data_file: $!";
961 $last_disk_check = time();
964 &check_auto_response_sock()
965 if $config{'ENABLE_AUTO_IDS'} eq 'Y';
967 ### Print the number of new packets we saw in FW_DATA_FILE if we are
968 ### running in debug mode
970 print STDERR "[+] MAIN: number of new packets: " .
971 ($#fw_packets+1) . "\n";
975 &print_sys_msg($die_msg, "$config{'PSAD_ERR_DIR'}/psad.die");
980 &print_sys_msg($warn_msg, "$config{'PSAD_ERR_DIR'}/psad.warn");
984 ### see if we need to timeout any old scans
985 if ($config{'ENABLE_PERSISTENCE'} eq 'N') {
988 if ($config{'PERSISTENCE_CTR_THRESHOLD'} == 0) {
990 } elsif ($check_interval_ctr % $config{'PERSISTENCE_CTR_THRESHOLD'} == 0) {
994 &delete_old_scans() if $do_timeout;
997 $check_interval_ctr++;
999 $fw_data_file_check_ctr++;
1001 ### clearerr() on the FWDATA filehandle to be ready for new packets
1004 ### sleep for the check interval seconds
1005 sleep $config{'CHECK_INTERVAL'};
1008 ### for completeness
1011 ###=========================================================###
1012 ###### END MAIN ######
1013 ###=========================================================###
1015 #=================== BEGIN SUBROUTINES ========================
1017 ### Keeps track of scanning ip's, increments packet counters,
1018 ### keeps track of tcp flags for each scan, test for snort sid
1019 ### values in iptables packets (if fwsnort is being used).
1021 my $fw_packets_ar = shift;
1024 my %curr_sigs_dl = ();
1025 my %curr_sids_dl = ();
1027 my %auto_block_regex_match = ();
1030 my $log_scan_ip_pair_max = 0;
1032 my $print_scale_factor = &get_scale_factor($#$fw_packets_ar);
1034 ### loop through all of the packet log messages we have just acquired
1037 PKT: for my $pkt_str (@$fw_packets_ar) {
1039 ### main packet data structure
1040 my %pkt = %pkt_NF_init;
1042 if ($analyze_mode) {
1044 if ($pkt_ctr % $print_scale_factor == 0) {
1045 print "[+] Processed $pkt_ctr packets...\n";
1049 ### main parsing routine for the iptables packet logging message
1050 my $pkt_parse_rv = &parse_NF_pkt_str(\%pkt, $pkt_str);
1051 print STDERR Dumper \%pkt if $debug and $verbose;
1052 if ($pkt_parse_rv == $PKT_ERROR) {
1053 push @err_pkts, $pkt_str unless $no_ipt_errors;
1055 } elsif ($pkt_parse_rv == $PKT_IGNORE) {
1059 if ($analyze_mode and $analysis_fields) {
1060 my ($matched_fields_ar, $gnuplot_comment_str)
1061 = &ipt_match_criteria(\%pkt, $analysis_tokens_ar,
1062 $analysis_match_criteria_ar);
1063 next PKT unless $#$matched_fields_ar > -1;
1066 $proto_ctrs{$pkt{'proto'}}++;
1067 if ($pkt{'proto'} eq 'tcp') {
1068 $top_tcp_ports{$pkt{'dp'}}++;
1069 } elsif ($pkt{'proto'} eq 'udp') {
1070 $top_udp_ports{$pkt{'dp'}}++;
1071 } elsif ($pkt{'proto'} eq 'udplite') {
1072 $top_udplite_ports{$pkt{'dp'}}++;
1075 ### If we made it here then we correctly matched packets
1076 ### that the firewall logged.
1077 print STDERR "[+] valid packet: $pkt{'src'} ($pkt{'sp'}) -> ",
1078 "$pkt{'dst'} ($pkt{'dp'}) $pkt{'proto'}\n" if $debug;
1080 ### see if we have hit the MAX_SCAN_IP_PAIRS threshold
1081 if ($config{'MAX_SCAN_IP_PAIRS'} > 0) {
1082 if ($scan_ip_pairs >= $config{'MAX_SCAN_IP_PAIRS'}) {
1083 unless (defined $scan{$pkt{'src'}}
1084 and defined $scan{$pkt{'src'}}{$pkt{'dst'}}) {
1085 print STDERR "[-] excluding $pkt{'src'} -> $pkt{'dst'}, ",
1086 "scan IP pairs too high: $scan_ip_pairs\n"
1088 $log_scan_ip_pair_max = 1;
1092 if (not defined $scan{$pkt{'src'}}) {
1094 } elsif (not defined $scan{$pkt{'src'}}{$pkt{'dst'}}) {
1099 ### track packet counts for this source
1100 $top_packet_counts{$pkt{'src'}}++;
1102 if ($config{'HOME_NET'} ne 'any') {
1103 if ($pkt{'chain'} eq 'INPUT') {
1104 $local_src{$pkt{'dst'}} = '';
1105 } elsif ($pkt{'chain'} eq 'OUTPUT') {
1106 $local_src{$pkt{'src'}} = '';
1107 } elsif ($pkt{'chain'} eq 'FORWARD') {
1108 $local_src{$pkt{'src'}} = ''
1109 if &is_local($pkt{'src'}, $pkt{'s_obj'});
1113 ### initialize the danger level to 0 if it is not already defined
1114 ### (note the same source address might have already scanned a
1115 ### different destination IP, so the danger level represents the
1116 ### aggregate danger level).
1117 unless (defined $scan_dl{$pkt{'src'}}) {
1118 $scan_dl{$pkt{'src'}} = 0;
1119 $scan{$pkt{'src'}}{$pkt{'dst'}}{'alerted'} = 0
1120 if $config{'ALERT_ALL'} eq 'N';
1123 ### see if we need to assign a danger level according to the auto_dl
1124 ### file. The return value is the auto-assigned danger level (or
1125 ### -1 if there is no auto-assigned danger level.
1126 unless ($no_auto_dl) {
1127 my $rv = &assign_auto_danger_level(\%pkt);
1129 print STDERR "[+] assign_auto_danger_level() returned: $rv\n"
1132 print STDERR "[+] ignoring $pkt{'src'} $pkt{'proto'} ",
1133 "$pkt{'dp'} scan.\n" if $debug;
1138 if ($pkt{'proto'} eq 'icmp') {
1140 ### validate icmp type and code fields against the official values
1141 ### in RFC 792. See %inval_type_code for corresponding signature
1142 ### message text and danger levels.
1143 my $type_code_rv = &check_icmp_type(
1144 'icmp', \%valid_icmp_types,
1145 $pkt{'itype'}, $pkt{'icode'});
1148 if ($type_code_rv == $BAD_ICMP_TYPE) {
1150 $scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp'}
1151 {'invalid_type'}{$pkt{'itype'}}
1152 {$pkt{'chain'}}{'pkts'}++;
1156 } elsif ($type_code_rv == $BAD_ICMP_CODE) {
1158 $scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp'}
1159 {'invalid_code'}{$pkt{'itype'}}{$pkt{'icode'}}
1160 {$pkt{'chain'}}{'pkts'}++;
1166 if (defined $scan_dl{$pkt{'src'}}) {
1167 if ($scan_dl{$pkt{'src'}} < 2) {
1168 $scan_dl{$pkt{'src'}} = 2;
1171 $scan_dl{$pkt{'src'}} = 2;
1175 } elsif ($pkt{'proto'} eq 'icmp6') {
1177 ### validate icmp6 type and code fields against the official values
1178 ### defined by IANA. See %inval_type_code for corresponding signature
1179 ### message text and danger levels.
1180 my $type_code_rv = &check_icmp_type(
1181 'icmp6', \%valid_icmp6_types,
1182 $pkt{'itype'}, $pkt{'icode'});
1185 if ($type_code_rv == $BAD_ICMP_TYPE) {
1187 $scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp6'}
1188 {'invalid_type'}{$pkt{'itype'}}
1189 {$pkt{'chain'}}{'pkts'}++;
1193 } elsif ($type_code_rv == $BAD_ICMP_CODE) {
1195 $scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp6'}
1196 {'invalid_code'}{$pkt{'itype'}}{$pkt{'icode'}}
1197 {$pkt{'chain'}}{'pkts'}++;
1203 if (defined $scan_dl{$pkt{'src'}}) {
1204 if ($scan_dl{$pkt{'src'}} < 2) {
1205 $scan_dl{$pkt{'src'}} = 2;
1208 $scan_dl{$pkt{'src'}} = 2;
1213 unless ($no_snort_sids) {
1214 if ($pkt{'fwsnort_sid'}) {
1216 ### found a fwsnort sid in the packet log message
1217 my ($dl, $is_sig_match) = &add_fwsnort_sid(\%pkt);
1220 $curr_sids_dl{$pkt{'src'}} = $dl;
1222 ### a signature matched but is supposed
1224 next PKT if $is_sig_match == $SIG_MATCH;
1228 ### attempt to match any tcp/udp/icmp signatures in the
1229 ### main signatures hash
1230 unless ($no_signatures) {
1232 my ($dl, $is_sig_match) = &match_sigs(\%pkt);
1234 print STDERR " match_sigs() returned DL: $dl\n"
1235 if $debug and $verbose;
1238 $curr_sigs_dl{$pkt{'src'}} = $dl;
1240 ### a signature matched but is supposed
1242 next PKT if $is_sig_match == $SIG_MATCH;
1248 ### note that we send this packet data off to DShield regardless
1249 ### of whether psad decides that it is associated with a scan so
1250 ### that DShield can make its own determination
1251 if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y'
1253 and not $analyze_mode
1254 and $pkt{'dshield_str'}) {
1255 if ($pkt{'timestamp'} =~ /^\s*(\w+)\s+(\d+)\s+(\S+)/) {
1256 my $m_tmp = $1; ### kludge for Decode_Month() call
1257 my $month = Decode_Month($m_tmp);
1258 my $day = sprintf("%.2d", $2);
1260 push @dshield_data, "$year-$month-$day $time_24 " .
1261 "$timezone\t$config{'DSHIELD_USER_ID'}\t1" .
1262 "\t$pkt{'dshield_str'}\n";
1266 ### record the absolute starting time of the scan
1267 unless (defined $scan{$pkt{'src'}}{$pkt{'dst'}}{'s_time'}) {
1268 if ($analyze_mode) {
1269 if ($pkt_str =~ /^(.*?)\s+\S+\s+kernel:/) {
1270 $scan{$pkt{'src'}}{$pkt{'dst'}}{'s_time'}
1272 } elsif ($pkt_str =~ /^\s*(\S+\s+\S+\s+\S+)/) {
1273 $scan{$pkt{'src'}}{$pkt{'dst'}}{'s_time'}
1276 die "[*] Could not extract time from packet: $pkt_str\n",
1277 " Please send a bug report to: ",
1278 "mbr\@cipherdyne.org\n";
1281 $scan{$pkt{'src'}}{$pkt{'dst'}}{'s_time'} = time();
1285 ### increment hash values
1286 $scan{$pkt{'src'}}{$pkt{'dst'}}{'absnum'}++;
1287 $scan{$pkt{'src'}}{$pkt{'dst'}}{'chain'}
1288 {$pkt{'chain'}}{$pkt{'intf'}}{$pkt{'proto'}}++;
1289 $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'pkts'}++;
1290 $curr_scan{$pkt{'src'}}{$pkt{'dst'}}
1291 {$pkt{'proto'}}{'flags'}{$pkt{'flags'}}++ if $pkt{'flags'};
1293 ### keep track of MAC addresses
1294 $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'s_mac'} = $pkt{'src_mac'};
1295 $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'d_mac'} = $pkt{'dst_mac'};
1297 ### keep track of which syslog daemon reported the message.
1298 $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'syslog_host'}
1299 {$pkt{'syslog_host'}} = '' if $pkt{'syslog_host'};
1301 if ($pkt{'log_prefix'}) {
1302 ### see if the logging prefix matches the blocking
1303 ### regex, and if not the IP will not be blocked
1304 if ($config{'ENABLE_AUTO_IDS'} eq 'Y'
1305 and $config{'ENABLE_AUTO_IDS_REGEX'} eq 'Y'
1306 and $config{'AUTO_BLOCK_REGEX'} ne 'NONE') {
1307 ### we require a match
1308 if (not defined $auto_block_regex_match{$pkt{'src'}}
1309 and $pkt{'log_prefix'} =~ /$config{'AUTO_BLOCK_REGEX'}/) {
1310 $auto_block_regex_match{$pkt{'src'}} = '';
1314 $pkt{'log_prefix'} = '*noprfx*';
1317 ### keep track of iptables chain and logging prefix
1318 $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'chain'}
1319 {$pkt{'chain'}}{$pkt{'log_prefix'}}++;
1321 if ($pkt{'proto'} eq 'tcp' or $pkt{'proto'} eq 'udp'
1322 or $pkt{'proto'} eq 'udplite') {
1323 ### initialize the start and end port for the scanned port range
1324 if (not defined $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'strtp'}) {
1325 ### make sure the initial start port is not too low
1326 $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'strtp'} = 65535;
1327 ### make sure the initial end port is not too high
1328 $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'endp'} = 0;
1330 if (not defined $scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_sp'}) {
1331 ### This is the absolute starting port since the
1332 ### first packet was detected. Make sure the initial
1333 ### start port is not too low
1334 $scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_sp'} = 65535;
1335 ### make sure the initial end port is not too high
1336 $scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_ep'} = 0;
1339 ### see if the destination port lies outside our current range
1340 ### and change if needed
1341 ($curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'strtp'},
1342 $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'endp'}) =
1343 &check_range($pkt{'dp'},
1344 $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'strtp'},
1345 $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'endp'});
1346 ($scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_sp'},
1347 $scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_ep'}) =
1348 &check_range($pkt{'dp'},
1349 $scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_sp'},
1350 $scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_ep'});
1353 print STDERR Dumper $scan{$pkt{'src'}}{$pkt{'dst'}} if $debug and $verbose;
1355 ### attempt to passively guess the remote operating
1356 ### system based on the ttl, id, len, window, and tos
1357 ### fields in tcp syn packets (this technique is based
1358 ### on the paper "Passive OS Fingerprinting: Details
1359 ### and Techniques" by Toby Miller). Also attempt to
1360 ### fingerprint with a re-implementation of Michal Zalewski's
1361 ### p0f that only requires iptables log messages
1363 ### make sure we have not already guessed the OS,
1364 ### and if we have been unsuccessful in guessing
1365 ### the OS after 100 packets don't keep trying.
1366 if ($pkt{'proto'} eq 'tcp' and $pkt{'flags'} =~ /SYN/) {
1367 if ($pkt{'tcp_opts'}) { ### got the tcp options portion of the header
1369 ### p0f based fingerprinting
1372 } elsif (not defined $posf{$pkt{'src'}}{'guess'}
1373 and $scan{$pkt{'src'}}{$pkt{'dst'}}{'absnum'} < 100) {
1382 ### write bogus packets to the error log.
1384 print scalar localtime(), ' [+] Err packets: ' .
1385 ($#err_pkts+1) . ".\n";
1387 &collect_errors(\@err_pkts) unless $no_ipt_errors;
1390 ### Assign a danger level to the scan
1391 print "[+] Assigning scan danger levels...\n" if $analyze_mode;
1392 &assign_danger_level(\%curr_scan, \%curr_sigs_dl, \%curr_sids_dl);
1394 my $tot_scan_ips = 0;
1395 if ($analyze_mode) {
1396 for (my $dl=1; $dl <= 5; $dl++) {
1398 for my $src (keys %curr_scan) {
1399 $num_ips++ if $scan_dl{$src} == $dl;
1401 $tot_scan_ips += $num_ips;
1402 print " Level $dl: $num_ips IP addresses\n";
1404 print "\n Tracking $tot_scan_ips total IP addresses\n";
1407 ### display the scan analysis
1408 &print_scan_status() if $analyze_mode;
1410 ### log scan data to the filesystem
1411 &scan_logr(\%curr_scan);
1413 ### remember that ENABLE_AUTO_IDS may have been set to 'N' if we
1414 ### are running on a syslog server, of if we are running in -A mode.
1415 &auto_psad_response(\%curr_scan, \%auto_block_regex_match)
1416 if $config{'ENABLE_AUTO_IDS'} eq 'Y' and not $analyze_mode;
1418 if ($log_scan_ip_pair_max) {
1419 &sys_log("scan IP pairs threshold reached");
1425 sub parse_NF_pkt_str() {
1426 my ($pkt_hr, $pkt_str) = @_;
1435 ### with ENABLE_SYSLOG_FILE enabled, psad sees all sorts of syslog
1436 ### messages that aren't just from iptables (kmsgsd is not running
1437 ### to filter them), so require a preliminary match
1438 if ($config{'ENABLE_SYSLOG_FILE'} eq 'Y') {
1439 return $PKT_IGNORE unless $pkt_str =~ /IN=.*OUT=/;
1442 print STDERR "\n", $pkt_str if $debug;
1444 $pkt_hr->{'raw'} = $pkt_str if $csv_mode or $gnuplot_mode;
1446 ### see if there is a logging prefix (used for scan email alert even
1447 ### if we are running with FW_SEARCH_ALL = Y). Note that sometimes
1448 ### there is a buffering issue in the kernel ring buffer that is used
1449 ### to hold the iptables log message, so we want to get only the
1450 ### very last possible candidate for the log prefix (this is why the
1451 ### "kernel:" string is preceded by .*).
1452 if ($pkt_str =~ /.*kernel:\s+(.*?)\s*IN=/) {
1453 $pkt_hr->{'log_prefix'} = $1;
1454 $pkt_hr->{'log_prefix'} =~ s|\[\s*\d+\.\d+\s*\]\s*||
1455 if ($config{'IGNORE_KERNEL_TIMESTAMP'} eq 'Y');
1456 if ($pkt_hr->{'log_prefix'} =~ /\S/) {
1457 if ($config{'IGNORE_LOG_PREFIXES'} ne 'NONE') {
1458 return $PKT_IGNORE if $pkt_hr->{'log_prefix'}
1459 =~ m|$config{'IGNORE_LOG_PREFIXES'}|;
1461 $ipt_prefixes{$pkt_hr->{'log_prefix'}}++;
1465 ### get the in/out interface and iptables chain (the code below
1466 ### allows the iptables log message to contain the PHYSDEV stuff):
1467 ### Feb 25 12:13:27 bridge kernel: INBOUND TCP: IN=br0 PHYSIN=eth0 OUT=br0
1468 ### PHYSOUT=eth1 SRC=63.147.183.21 DST=11.11.79.100 LEN=48 TOS=0x00
1469 ### PREC=0x00 TTL=113 ID=19664 DF PROTO=TCP SPT=4918 DPT=135 WINDOW=64240
1470 ### RES=0x00 SYN URGP=0
1471 ### Note the lack of whitespace requirement before the IN= interface
1472 ### because the logging prefix might not have contained it.
1473 if ($pkt_str =~ /IN=(\S+)\s+PHYSIN=.*?\sOUT=\s/
1474 or $pkt_str =~ /IN=(\S+).*?\sOUT=\s/) {
1475 $pkt_hr->{'intf'} = $1;
1476 $pkt_hr->{'chain'} = 'INPUT';
1477 } elsif ($pkt_str =~ /IN=(\S+)\s+PHYSIN=.*?\sOUT=\S/
1478 or $pkt_str =~ /IN=(\S+).*?\sOUT=\S/) {
1479 $pkt_hr->{'intf'} = $1;
1480 $pkt_hr->{'chain'} = 'FORWARD';
1481 } elsif ($pkt_str =~ /IN=\s+PHYSIN=.*?\sOUT=(\S+)/
1482 or $pkt_str =~ /IN=\s+OUT=(\S+)/) {
1483 $pkt_hr->{'intf'} = $1;
1484 $pkt_hr->{'chain'} = 'OUTPUT';
1487 ### -I was used on the command line to require a specific interface
1488 if ($cmdl_interface) {
1489 return $PKT_IGNORE unless $pkt_hr->{'intf'} eq $cmdl_interface;
1492 if ($pkt_str =~ /\sMAC=(\S+)/) {
1494 if ($mac_str =~ /^((?:\w{2}\:){6})((?:\w{2}\:){6})/) {
1495 $pkt_hr->{'dst_mac'} = $1;
1496 $pkt_hr->{'src_mac'} = $2;
1499 if ($pkt_hr->{'src_mac'}) {
1500 $pkt_hr->{'src_mac'} =~ s/:$//;
1501 print STDERR "[+] src mac addr: $pkt_hr->{'src_mac'}\n" if $debug;
1503 if ($pkt_hr->{'dst_mac'}) {
1504 $pkt_hr->{'dst_mac'} =~ s/:$//;
1505 print STDERR "[+] dst mac addr: $pkt_hr->{'dst_mac'}\n" if $debug;
1508 unless ($pkt_hr->{'intf'} and $pkt_hr->{'chain'}) {
1509 print STDERR "[-] err packet: could not determine ",
1510 "interface and chain.\n" if $debug;
1514 if (%ignore_interfaces) {
1515 for my $ignore_intf (keys %ignore_interfaces) {
1516 return $PKT_IGNORE if $pkt_hr->{'intf'} eq $ignore_intf;
1520 ### get the syslog logging host and timestamp for this packet
1521 if ($pkt_str =~ /^\s*((?:\S+\s+){2}\S+)\s+(\S+)\s+kernel:/) {
1522 $pkt_hr->{'timestamp'} = $1;
1523 $pkt_hr->{'syslog_host'} = $2;
1525 $pkt_hr->{'timestamp'} = localtime();
1526 $pkt_hr->{'syslog_host'} = 'unknown';
1529 ### try to extract a snort sid (generated by fwsnort) from
1531 unless ($no_snort_sids) {
1532 if ($pkt_hr->{'log_prefix'}) {
1534 if ($pkt_hr->{'log_prefix'} =~ /$config{'SNORT_SID_STR'}(\d+)/) {
1535 $pkt_hr->{'fwsnort_sid'} = $1;
1537 ### try to extract the fwsnort rule number (must be
1538 ### fwsnort-0.9.0 or greater)
1539 if ($pkt_hr->{'log_prefix'} =~ /\[(\d+)\]/) {
1540 $pkt_hr->{'fwsnort_rnum'} = $1;
1543 if ($pkt_hr->{'log_prefix'} =~ /ESTAB/) {
1544 $pkt_hr->{'fwsnort_estab'} = 1;
1550 unless ($pkt_hr->{'fwsnort_sid'} or $config{'FW_SEARCH_ALL'} eq 'Y') {
1551 ### note that this is not _too_ strict since people
1552 ### have different ways of writing --log-prefix strings
1554 for my $fw_search_str (@fw_search) {
1555 $matched = 1 if $pkt_str =~ /$fw_search_str/;
1557 return $PKT_IGNORE unless $matched;
1561 if ($pkt_str =~ /HOPLIMIT=\d+\s+FLOWLBL=\d+/) {
1562 return $PKT_IGNORE unless $config{'ENABLE_IPV6_DETECTION'} eq 'Y';
1564 $pkt_hr->{'is_ipv6'} = 1;
1567 ### test for IPv4 "don't fragment" bit
1569 $pkt_hr->{'frag_bit'} = 1 if $pkt_str =~ /\sDF\s+PROTO/;
1572 ### get IP options if --log-ip-options is used - they appear before the
1573 ### PROTO= field for either IPv4 or IPv6 packets
1574 if ($pkt_str =~ /OPT\s+\((\S+)\)\s+PROTO=/) {
1575 $pkt_hr->{'ip_opts'} = $1;
1578 if ($is_ipv6 and $pkt_str =~ /PROTO=ICMPv6\s/) {
1579 ### test for ICMP before TCP or UDP because ICMP destination
1580 ### unreachable messages can contain embedded TCP/UDP specifics like
1583 ### Jul 21 19:07:39 minastirith kernel: [1912155.755921] IPv6 Packet
1584 ### IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:86:dd
1585 ### SRC=0000:0000:0000:0000:0000:0000:0000:0001
1586 ### DST=0000:0000:0000:0000:0000:0000:0000:0001 LEN=107 TC=0
1587 ### HOPLIMIT=64 FLOWLBL=0 PROTO=ICMPv6 TYPE=1 CODE=4
1588 ### [SRC=0000:0000:0000:0000:0000:0000:0000:0001
1589 ### DST=0000:0000:0000:0000:0000:0000:0000:0001 LEN=59 TC=0
1590 ### HOPLIMIT=64 FLOWLBL=0 PROTO=UDP SPT=35186 DPT=12345 LEN=19 ]
1593 } elsif ($pkt_str =~ /PROTO=ICMP\s/) {
1594 ### test for ICMP before TCP or UDP because ICMP destination
1595 ### unreachable messages can contain embedded TCP/UDP specifics like
1598 ### Sep 8 18:04:26 minastirith kernel: [28241.572876] IN_DROP
1599 ### IN=wlan0 OUT= MAC=00:1a:9f:91:df:ae:00:12:27:12:0a:a0:12:00
1600 ### SRC=10.0.0.138 DST=192.168.1.103 LEN=96 TOS=0x00 PREC=0xC0
1601 ### TTL=254 ID=63642 PROTO=ICMP TYPE=3 CODE=3 [SRC=192.168.1.103
1602 ### DST=10.0.0.138 LEN=68 TOS=0x00 PREC=0x00 TTL=0 ID=22458 PROTO=UDP
1603 ### SPT=35080 DPT=33434 LEN=48 ]
1606 } elsif ($pkt_str =~ /PROTO=TCP\s/) {
1608 } elsif ($pkt_str =~ /PROTO=UDPLITE\s/) {
1610 } elsif ($pkt_str =~ /PROTO=UDP\s/) {
1613 print STDERR "[-] err packet: unrecognized protocol\n" if $debug;
1620 ### Jul 18 19:19:08 lorien kernel: [ 1835.131574] IPV6 packet IN=lo
1621 ### OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:86:dd
1622 ### SRC=0000:0000:0000:0000:0000:0000:0000:0001
1623 ### DST=0000:0000:0000:0000:0000:0000:0000:0001 LEN=72 TC=0
1624 ### HOPLIMIT=255 FLOWLBL=0 PROTO=TCP SPT=22 DPT=57005 WINDOW=512
1625 ### RES=0x00 ACK FIN URGP=0
1626 if ($pkt_str =~ /SRC=($ipv6_re)\s+DST=($ipv6_re)\s+LEN=(\d+)\s+
1627 TC=(\d+)\s+HOPLIMIT=(\d+)\s+FLOWLBL=(\d+)\s+PROTO=TCP\s+
1628 SPT=(\d+)\s+DPT=(\d+)\s+WINDOW=(\d+)\s+
1631 ($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
1632 $pkt_hr->{'tc'}, $pkt_hr->{'hop_limit'},
1633 $pkt_hr->{'flow_label'}, $pkt_hr->{'sp'}, $pkt_hr->{'dp'},
1634 $pkt_hr->{'win'}, $pkt_hr->{'flags'})
1635 = ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);
1637 $pkt_hr->{'s_obj'} = new6 NetAddr::IP($pkt_hr->{'src'})
1638 or return $PKT_ERROR;
1639 $pkt_hr->{'d_obj'} = new6 NetAddr::IP($pkt_hr->{'dst'})
1640 or return $PKT_ERROR;
1643 print STDERR "[-] err packet: strange IPv6 TCP format\n"
1650 ### May 18 22:21:26 orthanc kernel: DROP IN=eth2 OUT=
1651 ### MAC=00:60:1d:23:d0:01:00:60:1d:23:d3:0e:08:00 SRC=192.168.20.25
1652 ### DST=192.168.20.1 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=47300 DF
1653 ### PROTO=TCP SPT=34111 DPT=6345 WINDOW=5840 RES=0x00 SYN URGP=0
1655 if ($pkt_str =~ /SRC=($ipv4_re)\s+DST=($ipv4_re)\s+LEN=(\d+)\s+TOS=(\S+)
1656 \s*.*\s+TTL=(\d+)\s+ID=(\d+)\s*.*\s+PROTO=TCP\s+
1657 SPT=(\d+)\s+DPT=(\d+)\s.*\s*WINDOW=(\d+)\s+
1660 ($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
1661 $pkt_hr->{'tos'}, $pkt_hr->{'ttl'}, $pkt_hr->{'ip_id'},
1662 $pkt_hr->{'sp'}, $pkt_hr->{'dp'}, $pkt_hr->{'win'},
1664 = ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);
1666 $pkt_hr->{'s_obj'} = new NetAddr::IP($pkt_hr->{'src'})
1667 or return $PKT_ERROR;
1668 $pkt_hr->{'d_obj'} = new NetAddr::IP($pkt_hr->{'dst'})
1669 or return $PKT_ERROR;
1672 print STDERR "[-] err packet: strange IPv4 TCP format\n"
1678 $pkt_hr->{'proto'} = 'tcp';
1680 ### the reserve bits are not reported by ulogd, but normal
1681 ### iptables syslog messages contain them.
1682 $pkt_hr->{'flags'} =~ s/\s*RES=\S+\s*//;
1685 $pkt_hr->{'flags'} = 'NULL' unless $pkt_hr->{'flags'};
1687 if (not $pkt_hr->{'fwsnort_sid'}
1688 and $config{'IGNORE_CONNTRACK_BUG_PKTS'} eq 'Y' &&
1689 ($pkt_hr->{'flags'} =~ /ACK/ || $pkt_hr->{'flags'} =~ /RST/)) {
1691 ### $dp > 1024 && ($pkt_hr->{'flags'} =~ /ACK/ ||
1693 ### FIXME: ignore TCP packets that have the ACK or RST
1694 ### bits set (unless we matched a snort sid) since
1695 ### _usually_ we see these packets as a result of the
1696 ### iptables connection tracking bug. Also, note that
1697 ### no signatures make use of the RST flag.
1699 print STDERR "[-] err packet: matched ACK or RST flag.\n"
1704 ### per page 595 of the Camel book, "if /blah1|blah2/"
1705 ### can be slower than "if /blah1/ || /blah2/
1706 unless ($pkt_hr->{'flags'} !~ /WIN/ &&
1707 $pkt_hr->{'flags'} =~ /ACK/ ||
1708 $pkt_hr->{'flags'} =~ /SYN/ ||
1709 $pkt_hr->{'flags'} =~ /RST/ ||
1710 $pkt_hr->{'flags'} =~ /URG/ ||
1711 $pkt_hr->{'flags'} =~ /PSH/ ||
1712 $pkt_hr->{'flags'} =~ /FIN/ ||
1713 $pkt_hr->{'flags'} eq 'NULL') {
1715 print STDERR "[-] err packet: bad tcp flags.\n" if $debug;
1719 ### don't pickup IP options if --log-ip-options is used
1720 ### (they appear before the PROTO= field).
1721 if ($pkt_str =~ /URGP=\S+\s+OPT\s+\((\S+)\)/) {
1722 $pkt_hr->{'tcp_opts'} = $1;
1725 if ($pkt_str =~ /\sSEQ=(\d+)\s+ACK=(\d+)/) {
1726 $pkt_hr->{'tcp_seq'} = $1;
1727 $pkt_hr->{'tcp_ack'} = $2;
1730 ### see if we need to ignore this packet based on the
1731 ### IGNORE_PROTOCOLS config keyword.
1732 return $PKT_IGNORE if &check_ignore_proto($pkt_hr->{'proto'});
1734 ### see if we need to ignore this packet based on the
1735 ### IGNORE_PORTS config keyword
1736 return $PKT_IGNORE if &check_ignore_port($pkt_hr->{'dp'},
1737 $pkt_hr->{'proto'});
1739 if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y'
1741 and not $analyze_mode) {
1743 my $dflags = $pkt_hr->{'flags'};
1744 $dflags =~ s/\s/,/g;
1746 $pkt_hr->{'dshield_str'} = "$pkt_hr->{'src'}\t$pkt_hr->{'sp'}\t" .
1747 "$pkt_hr->{'dst'}\t$pkt_hr->{'dp'}\t$pkt_hr->{'proto'}\t" .
1751 } elsif ($is_udp or $is_udplite) {
1754 $pkt_hr->{'proto'} = 'udplite';
1756 $pkt_hr->{'proto'} = 'udp';
1761 ### Jul 21 21:07:39 minastirith kernel: [1912155.755862] IPv6 Packet IN=lo
1762 ### OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:86:dd
1763 ### SRC=0000:0000:0000:0000:0000:0000:0000:0001
1764 ### DST=0000:0000:0000:0000:0000:0000:0000:0001 LEN=59 TC=0 HOPLIMIT=64
1765 ### FLOWLBL=0 PROTO=UDP SPT=35186 DPT=12345 LEN=19
1766 if ($pkt_str =~ /SRC=($ipv6_re)\s+DST=($ipv6_re)\s+LEN=(\d+)\s+
1767 TC=(\d+)\s+HOPLIMIT=(\d+)\s+FLOWLBL=(\d+)\s+
1768 PROTO=UDP(?:LITE)?\s+SPT=(\d+)\s+DPT=(\d+)\s+LEN=(\d+)/x) {
1770 ($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
1771 $pkt_hr->{'tc'}, $pkt_hr->{'hop_limit'},
1772 $pkt_hr->{'flow_label'}, $pkt_hr->{'sp'}, $pkt_hr->{'dp'},
1773 $pkt_hr->{'udp_len'}) = ($1,$2,$3,$4,$5,$6,$7,$8,$9);
1775 $pkt_hr->{'s_obj'} = new6 NetAddr::IP($pkt_hr->{'src'})
1776 or return $PKT_ERROR;
1777 $pkt_hr->{'d_obj'} = new6 NetAddr::IP($pkt_hr->{'dst'})
1778 or return $PKT_ERROR;
1781 print STDERR "[-] err packet: strange IPv6 UDP format\n"
1788 ### May 18 22:21:26 orthanc kernel: DROP IN=eth2 OUT=
1789 ### MAC=00:60:1d:23:d0:01:00:60:1d:23:d3:0e:08:00
1790 ### SRC=192.168.20.25 DST=192.168.20.1 LEN=28 TOS=0x00 PREC=0x00
1791 ### TTL=40 ID=47523 PROTO=UDP SPT=57339 DPT=305 LEN=8
1793 if ($pkt_str =~ /SRC=($ipv4_re)\s+DST=($ipv4_re)\s+LEN=(\d+)\s+TOS=(\S+)
1794 \s.*TTL=(\d+)\s+ID=(\d+)\s*.*\s+PROTO=UDP\s+
1795 SPT=(\d+)\s+DPT=(\d+)\s+LEN=(\d+)/x) {
1797 ($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
1798 $pkt_hr->{'tos'}, $pkt_hr->{'ttl'}, $pkt_hr->{'ip_id'},
1799 $pkt_hr->{'sp'}, $pkt_hr->{'dp'}, $pkt_hr->{'udp_len'})
1800 = ($1,$2,$3,$4,$5,$6,$7,$8,$9);
1802 $pkt_hr->{'s_obj'} = new NetAddr::IP($pkt_hr->{'src'})
1803 or return $PKT_ERROR;
1804 $pkt_hr->{'d_obj'} = new NetAddr::IP($pkt_hr->{'dst'})
1805 or return $PKT_ERROR;
1808 print STDERR "[-] err packet: strange IPv4 UDP format\n"
1814 ### see if we need to ignore this packet based on the
1815 ### IGNORE_PROTOCOLS config keyword.
1816 return $PKT_IGNORE if &check_ignore_proto($pkt_hr->{'proto'});
1818 ### see if we need to ignore this packet based on the
1819 ### IGNORE_PORTS config keyword
1820 return $PKT_IGNORE if &check_ignore_port($pkt_hr->{'dp'},
1821 $pkt_hr->{'proto'});
1823 if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y'
1825 and not $analyze_mode) {
1827 $pkt_hr->{'dshield_str'} = "$pkt_hr->{'src'}\t$pkt_hr->{'sp'}\t" .
1828 "$pkt_hr->{'dst'}\t$pkt_hr->{'dp'}\t$pkt_hr->{'proto'}";
1831 } elsif ($is_icmp6 or $is_icmp) {
1833 $pkt_hr->{'sp'} = $pkt_hr->{'dp'} = 0;
1837 ### Jul 18 17:18:19 lorien kernel: [ 1786.520508] IPV6 packet IN=lo
1838 ### OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:86:dd
1839 ### SRC=0000:0000:0000:0000:0000:0000:0000:0001
1840 ### DST=0000:0000:0000:0000:0000:0000:0000:0001 LEN=104 TC=0
1841 ### HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=129 CODE=0 ID=14997 SEQ=1
1843 if ($pkt_str =~ /SRC=($ipv6_re)\s+DST=($ipv6_re)\s+LEN=(\d+)\s+TC=(\d+)\s+
1844 HOPLIMIT=(\d+)\s+FLOWLBL=(\d+)\s+PROTO=ICMPv6\s+TYPE=(\d+)\s+
1847 ($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
1848 $pkt_hr->{'tc'}, $pkt_hr->{'hop_limit'},
1849 $pkt_hr->{'flow_label'}, $pkt_hr->{'itype'},
1850 $pkt_hr->{'icode'}) = ($1,$2,$3,$4,$5,$6,$7,$8);
1852 $pkt_hr->{'s_obj'} = new6 NetAddr::IP($pkt_hr->{'src'})
1853 or return $PKT_ERROR;
1854 $pkt_hr->{'d_obj'} = new6 NetAddr::IP($pkt_hr->{'dst'})
1855 or return $PKT_ERROR;
1858 print STDERR "[-] err packet: strange IPv6 ICMP format\n"
1863 $pkt_hr->{'proto'} = 'icmp6';
1867 ### Nov 27 15:45:51 orthanc kernel: DROP IN=eth1 OUT= MAC=00:a0:cc:e2:1f:f2:00:
1868 ### 20:78:10:70:e7:08:00 SRC=192.168.10.20 DST=192.168.10.1 LEN=84 TOS=0x00
1869 ### PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=61055 SEQ=256
1871 if ($pkt_str =~ /SRC=($ipv4_re)\s+DST=($ipv4_re)\s+LEN=(\d+).*
1872 TTL=(\d+)\s+ID=(\d+).*PROTO=ICMP\s+TYPE=(\d+)\s+
1875 ($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
1876 $pkt_hr->{'ttl'}, $pkt_hr->{'ip_id'}, $pkt_hr->{'itype'},
1877 $pkt_hr->{'icode'}) = ($1,$2,$3,$4,$5,$6,$7);
1879 $pkt_hr->{'s_obj'} = new NetAddr::IP($pkt_hr->{'src'})
1880 or return $PKT_ERROR;
1881 $pkt_hr->{'d_obj'} = new NetAddr::IP($pkt_hr->{'dst'})
1882 or return $PKT_ERROR;
1885 print STDERR "[-] err packet: strange IPv4 ICMP format\n"
1890 $pkt_hr->{'proto'} = 'icmp';
1892 if ($pkt_hr->{'itype'} == $ICMP_ECHO_REQUEST
1893 or $pkt_hr->{'itype'} == $ICMP_ECHO_REPLY) {
1895 ### we expect the ICMP ID and SEQ fields to be populated
1896 if ($pkt_str =~ /CODE=(\d+)\s+ID=(\d+)\s+SEQ=(\d+)/) {
1897 $pkt_hr->{'icmp_id'} = $1;
1898 $pkt_hr->{'icmp_seq'} = $2;
1905 ### see if we need to ignore this packet based on the
1906 ### IGNORE_PROTOCOLS config keyword.
1907 return $PKT_IGNORE if &check_ignore_proto($pkt_hr->{'proto'});
1909 if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y'
1911 and not $analyze_mode) {
1913 $pkt_hr->{'dshield_str'} = "$pkt_hr->{'src'}\t$pkt_hr->{'itype'}" .
1914 "\t$pkt_hr->{'dst'}\t$pkt_hr->{'icode'}\t$pkt_hr->{'proto'}";
1918 ### Sometimes the iptables log entry gets messed up due to
1919 ### buffering issues so we write it to the error log.
1920 print STDERR "[-] err packet: no regex match.\n" if $debug;
1925 ### we are looking to analyze packets from a specific IP/subnet
1926 if ($pkt_hr->{'is_ipv6'}) {
1927 if ($restrict_ip->version() == 6) {
1928 return $PKT_IGNORE unless
1929 $pkt_hr->{'s_obj'}->within($restrict_ip) or
1930 $pkt_hr->{'d_obj'}->within($restrict_ip);
1933 if ($restrict_ip->version() == 4) {
1934 return $PKT_IGNORE unless
1935 $pkt_hr->{'s_obj'}->within($restrict_ip) or
1936 $pkt_hr->{'d_obj'}->within($restrict_ip);
1941 return $PKT_SUCCESS;
1944 sub check_ignore_proto() {
1945 my $pkt_proto = shift;
1947 return 0 unless %ignore_protocols;
1949 return 1 if defined $ignore_protocols{$pkt_proto};
1957 my $is_sig_match = $NO_SIG_MATCH;
1959 print STDERR "[+] match_sigs()\n" if $debug and $verbose;
1961 ### always run the IP protocol sigs
1962 PROTO: for my $proto ($pkt_hr->{'proto'}, 'ip') {
1964 next PROTO unless defined $sig_search{$proto};
1966 SRC: for my $src (keys %{$sig_search{$proto}}) {
1968 print STDERR "[+] match_sigs() pkt src: $pkt_hr->{'src'} within sig src: $src ?..."
1969 if $debug and $verbose;
1971 if ($pkt_hr->{'s_obj'}->within($sig_ip_objs{$src})) {
1972 print STDERR "yes\n"
1973 if $debug and $verbose;
1976 if $debug and $verbose;
1980 DST: for my $dst (keys %{$sig_search{$proto}{$src}}) {
1982 print STDERR "[+] match_sigs() pkt dst: $pkt_hr->{'dst'} within sig dst: $dst ?..."
1983 if $debug and $verbose;
1985 if ($pkt_hr->{'d_obj'}->within($sig_ip_objs{$dst})) {
1986 print STDERR "yes\n"
1987 if $debug and $verbose;
1990 if $debug and $verbose;
1994 print STDERR " Matched sig IP criteria.\n"
1995 if $debug and $verbose;
1997 if ($proto eq 'tcp' or $proto eq 'udp' or $proto eq 'udplite') {
1999 TYPE: for my $hr (@port_types) {
2000 my $sp_type = $hr->{'sp'};
2001 my $dp_type = $hr->{'dp'};
2004 defined $sig_search{$proto}{$src}{$dst}{$sp_type};
2006 my $sp_hr = $sig_search{$proto}{$src}{$dst}{$sp_type};
2008 SP_S: for my $sp_s (keys %{$sp_hr}) {
2010 if ($sp_type eq 'norm') {
2011 ### normal match on the starting port value
2012 next SP_S unless $pkt_hr->{'sp'} >= $sp_s;
2015 SP_E: for my $sp_e (keys %{$sp_hr->{$sp_s}}) {
2017 if ($sp_type eq 'norm') {
2018 ### normal match on the ending port value
2019 next SP_E unless $pkt_hr->{'sp'} <= $sp_e;
2021 ### negative match on the ending port value
2022 ### (note the "or" condition)
2023 next SP_E unless ($pkt_hr->{'sp'} > $sp_e
2024 or $pkt_hr->{'sp'} < $sp_s);
2027 next TYPE unless defined
2028 $sp_hr->{$sp_s}->{$sp_e}->{$dp_type};
2030 my $dp_hr = $sp_hr->{$sp_s}->{$sp_e}->{$dp_type};
2032 DP_S: for my $dp_s (keys %$dp_hr) {
2033 if ($dp_type eq 'norm') {
2034 next DP_S unless $pkt_hr->{'dp'} >= $dp_s;
2037 DP_E: for my $dp_e (keys %{$dp_hr->{$dp_s}}) {
2038 if ($dp_type eq 'norm') {
2039 next DP_E unless $pkt_hr->{'dp'} <= $dp_e;
2041 ### negative match on the ending port value
2042 ### (note the "or" condition)
2043 next DP_E unless ($pkt_hr->{'dp'} > $dp_e
2044 or $pkt_hr->{'dp'} < $dp_s);
2047 ### now we have the set of applicable
2048 ### signatures that match the sip/dip
2049 ### and sp/dp, so match any Snort
2051 my ($dl_tmp, $sig_match_tmp) =
2052 &match_snort_keywords($pkt_hr,
2053 $dp_hr->{$dp_s}->{$dp_e});
2055 print STDERR " match_snort_keywords() ",
2056 " return DL: $dl_tmp\n" if $debug;
2058 ### return maximal danger level from all
2059 ### signature matches
2060 $dl = $dl_tmp if $dl_tmp > $dl;
2061 $is_sig_match = $SIG_MATCH
2062 if $sig_match_tmp == $SIG_MATCH;
2069 ### now we have the set of applicable icmp
2070 ### signatures that match the sip/dip
2071 my ($dl_tmp, $sig_match_tmp) = &match_snort_keywords(
2072 $pkt_hr, $sig_search{$proto}{$src}{$dst});
2074 print STDERR " match_snort_keywords() ",
2075 " return DL: $dl_tmp\n" if $debug;
2076 ### return maximal danger level from all signature matches
2077 $dl = $dl_tmp if $dl_tmp > $dl;
2078 $is_sig_match = $SIG_MATCH if $sig_match_tmp == $SIG_MATCH;
2083 return $dl, $is_sig_match;
2086 sub match_snort_keywords() {
2087 my ($pkt_hr, $sigs_ids_hr) = @_;
2089 print STDERR "[+] match_snort_keywords()\n" if $debug;
2092 my $matched_sig = $NO_SIG_MATCH;
2094 ### see if all Snort keywords match the packet - the sigs hash has
2095 ### been restricted to the appropriate protocol
2096 SIG: for my $sid (keys %$sigs_ids_hr) {
2098 next SIG unless defined $sigs{$sid}; ### should never happen
2100 my $sig_hr = $sigs{$sid};
2102 ### iptables logging messages always include TTL and IP ID
2103 ### values (at least for ipv4, see
2104 ### linux/net/ipv4/netfilter/ipt_LOG.c)
2107 my ($rv, $sig_match_rv) = &match_snort_ip_keywords($pkt_hr, $sig_hr);
2109 if ($sig_match_rv == $SIG_MATCH) {
2110 $matched_sig = $SIG_MATCH;
2113 next SIG; ### ignore signature
2115 } elsif ($sig_match_rv == $NO_SIG_MATCH) {
2116 ### there were network-layer keywords that did not match
2117 next SIG unless $rv;
2118 ### else there were no network-layer keywords so continue on
2121 $dl = $dl_tmp if $dl_tmp > $dl;
2123 if ($debug and $debug_sid == $sid) {
2124 print STDERR "[+] SID: $sid, passed match_snort_ip_keywords() ",
2128 if ($sig_hr->{'proto'} eq 'tcp') {
2130 ($rv, $sig_match_rv) = &match_snort_tcp_keywords($pkt_hr, $sig_hr);
2132 if ($sig_match_rv == $SIG_MATCH) {
2134 $matched_sig = $SIG_MATCH;
2135 next SIG if $rv == 0;
2137 $dl = $rv if $rv > $dl;
2139 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
2140 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'dp'}
2143 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
2144 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'flags'}
2145 = $pkt_hr->{'flags'};
2147 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
2148 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
2150 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
2151 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
2153 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
2154 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'time'} = time();
2156 $sig_sources{$sid}{$pkt_hr->{'src'}} = '';
2158 $top_sig_counts{$pkt_hr->{'src'}}++;
2161 } elsif ($sig_hr->{'proto'} eq 'udp') {
2163 ($rv, $sig_match_rv) = &match_snort_udp_keywords($pkt_hr, $sig_hr);
2165 if ($sig_match_rv == $SIG_MATCH) {
2167 $matched_sig = $SIG_MATCH;
2168 next SIG if $rv == 0;
2170 $dl = $rv if $rv > $dl;
2172 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
2173 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'dp'}
2176 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
2177 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
2179 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
2180 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
2182 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
2183 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'time'} = time();
2185 $sig_sources{$sid}{$pkt_hr->{'src'}} = 0; ### not fwsnort
2187 $top_sig_counts{$pkt_hr->{'src'}}++;
2190 } elsif ($sig_hr->{'proto'} eq 'icmp') {
2192 ($rv, $sig_match_rv) = &match_snort_icmp_keywords($pkt_hr, $sig_hr);
2194 if ($sig_match_rv == $SIG_MATCH) {
2196 $matched_sig = $SIG_MATCH;
2197 next SIG if $rv == 0;
2199 $dl = $rv if $rv > $dl;
2201 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'icmp'}{'sid'}
2202 {$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
2204 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'icmp'}{'sid'}
2205 {$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
2207 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'icmp'}{'sid'}
2208 {$sid}{$pkt_hr->{'chain'}}{'time'} = time();
2210 $sig_sources{$sid}{$pkt_hr->{'src'}} = 0; ### not fwsnort
2212 $top_sig_counts{$pkt_hr->{'src'}}++;
2215 } elsif ($sig_hr->{'proto'} eq 'icmp6') {
2216 ### FIXME icmp6 specifc Snort matches?
2219 return $dl, $matched_sig;
2222 sub match_snort_tcp_keywords() {
2223 my ($pkt_hr, $sig_hr) = @_;
2225 if ($debug and $debug_sid == $sig_hr->{'sid'}) {
2226 print STDERR "[+] SID: $sig_hr->{'sid'} match_snort_tcp_keywords()\n";
2229 if (defined $sig_hr->{'flags'}) {
2230 unless ($pkt_hr->{'flags'} eq $sig_hr->{'flags'}) {
2231 if ($debug and $debug_sid == $sig_hr->{'sid'}) {
2232 print STDERR "[-] SID: $sig_hr->{'sid'} ",
2233 "$pkt_hr->{'flags'} != $sig_hr->{'flags'}\n";
2235 return 0, $NO_SIG_MATCH;
2239 my $header_len = $IP_HEADER_LEN + $TCP_HEADER_LEN;
2241 if ($pkt_hr->{'flags'} =~ m|SYN|) {
2242 ### extend the header length to compensate for TCP options
2243 $header_len += $TCP_MAX_OPTS_LEN;
2246 if (defined $sig_hr->{'dsize'} and defined $sig_hr->{'dsize_s'}) {
2247 return 0, $NO_SIG_MATCH unless &check_sig_int_range(
2248 ($pkt_hr->{'ip_len'}-$header_len), 'dsize', $sig_hr);
2251 if (defined $sig_hr->{'psad_dsize'} and defined $sig_hr->{'psad_dsize_s'}) {
2252 return 0, $NO_SIG_MATCH unless &check_sig_int_range(
2253 ($pkt_hr->{'ip_len'}-$header_len), 'psad_dsize', $sig_hr);
2256 if (defined $sig_hr->{'window'} and defined $sig_hr->{'window_s'}) {
2257 return 0, $NO_SIG_MATCH
2258 unless &check_sig_int_range($pkt_hr->{'win'}, 'window', $sig_hr);
2261 if (defined $sig_hr->{'seq'} and defined $sig_hr->{'seq_s'}) {
2262 return 0, $NO_SIG_MATCH
2263 unless &check_sig_int_range($pkt_hr->{'tcp_seq'}, 'seq', $sig_hr);
2266 if (defined $sig_hr->{'ack'} and defined $sig_hr->{'ack_s'}) {
2267 return 0, $NO_SIG_MATCH
2268 unless &check_sig_int_range($pkt_hr->{'tcp_ack'}, 'ack', $sig_hr);
2271 ### matched the signature
2273 print STDERR "[+] packet matched tcp keywords for sid: ",
2274 "$sig_hr->{'sid'} (psad_id: $sig_hr->{'psad_id'})\n",
2275 qq| "$sig_hr->{'msg'}"\n|;
2278 return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
2281 sub match_snort_udp_keywords() {
2282 my ($pkt_hr, $sig_hr) = @_;
2284 if (defined $sig_hr->{'dsize'} and defined $sig_hr->{'dsize_s'}) {
2285 return 0, $NO_SIG_MATCH unless &check_sig_int_range(
2286 ($pkt_hr->{'udp_len'}-$UDP_HEADER_LEN),
2290 if (defined $sig_hr->{'psad_dsize'} and defined $sig_hr->{'psad_dsize_s'}) {
2291 return 0, $NO_SIG_MATCH unless &check_sig_int_range(
2292 ($pkt_hr->{'udp_len'}-$UDP_HEADER_LEN),
2293 'psad_dsize', $sig_hr);
2296 ### matched the signature
2298 print STDERR "[+] packet matched udp keywords for sid: ",
2299 "$sig_hr->{'sid'} (psad_id: $sig_hr->{'psad_id'})\n",
2300 qq| "$sig_hr->{'msg'}"\n|;
2303 return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
2306 sub match_snort_icmp_keywords() {
2307 my ($pkt_hr, $sig_hr) = @_;
2309 if (defined $sig_hr->{'dsize'} and defined $sig_hr->{'dsize_s'}) {
2310 return 0, $NO_SIG_MATCH unless &check_sig_int_range(
2311 ($pkt_hr->{'ip_len'}-$IP_HEADER_LEN-$ICMP_HEADER_LEN),
2315 if (defined $sig_hr->{'psad_dsize'} and defined $sig_hr->{'psad_dsize_s'}) {
2316 return 0, $NO_SIG_MATCH unless &check_sig_int_range(
2317 ($pkt_hr->{'ip_len'}-$IP_HEADER_LEN-$ICMP_HEADER_LEN),
2318 'psad_dsize', $sig_hr);
2321 if (defined $sig_hr->{'itype'} and defined $sig_hr->{'itype_s'}) {
2322 return 0, $NO_SIG_MATCH
2323 unless &check_sig_int_range($pkt_hr->{'itype'}, 'itype', $sig_hr);
2325 if (defined $sig_hr->{'icode'} and defined $sig_hr->{'icode_s'}) {
2326 return 0, $NO_SIG_MATCH
2327 unless &check_sig_int_range($pkt_hr->{'icode'}, 'icode', $sig_hr);
2329 if (defined $sig_hr->{'icmp_seq'} and defined $sig_hr->{'icmp_seq_s'}) {
2330 return 0, $NO_SIG_MATCH
2331 unless &check_sig_int_range($pkt_hr->{'icmp_seq'}, 'icmp_seq', $sig_hr);
2333 if (defined $sig_hr->{'icmp_id'} and defined $sig_hr->{'icmp_id_s'}) {
2334 return 0, $NO_SIG_MATCH
2335 unless &check_sig_int_range($pkt_hr->{'icmp_id'}, 'icmp_id', $sig_hr);
2338 ### matched the signature
2340 print STDERR "[+] packet matched icmp keywords for sid: ",
2341 "$sig_hr->{'sid'} (psad_id: $sig_hr->{'psad_id'})\n",
2342 qq| "$sig_hr->{'msg'}"\n|;
2345 return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
2348 sub match_snort_ip_keywords() {
2349 my ($pkt_hr, $sig_hr) = @_;
2351 if ($pkt_hr->{'is_ipv6'}) {
2352 ### we need to build IPv6 signature keywords
2353 return 1, $NO_SIG_MATCH;
2356 if (defined $sig_hr->{'ttl'} and defined $sig_hr->{'ttl_s'}) {
2357 return 0, $NO_SIG_MATCH
2358 unless &check_sig_int_range($pkt_hr->{'ttl'}, 'ttl', $sig_hr);
2361 if (defined $sig_hr->{'id'} and defined $sig_hr->{'id_s'}) {
2362 return 0, $NO_SIG_MATCH
2363 unless &check_sig_int_range($pkt_hr->{'ip_id'}, 'id', $sig_hr);
2366 if (defined $sig_hr->{'psad_ip_len'} and defined $sig_hr->{'psad_ip_len_s'}) {
2367 return 0, $NO_SIG_MATCH
2368 unless &check_sig_int_range($pkt_hr->{'ip_len'},
2369 'psad_ip_len', $sig_hr);
2372 ### to handle the ip_proto keyword parse_NF_pkt_str() would have to be
2373 ### modified to handle packets besides TCP, UDP, and ICMP.
2374 return 0, $NO_SIG_MATCH if defined $sig_hr->{'ip_proto'};
2376 ### handle the sameip keyword
2377 if (defined $sig_hr->{'sameip'} and $sig_hr->{'sameip'}) {
2378 return 0, $NO_SIG_MATCH if $pkt_hr->{'intf'} eq 'lo';
2379 return 0, $NO_SIG_MATCH unless $pkt_hr->{'src'} eq $pkt_hr->{'dst'};
2382 return 0, $NO_SIG_MATCH
2383 unless &check_sig_ipopts($pkt_hr->{'ip_opts'}, 'ipopts', $sig_hr);
2385 if ($sig_hr->{'proto'} eq 'ip') {
2388 print STDERR "[+] packet matched ip keywords for sid: ",
2389 "$sig_hr->{'sid'} (psad_id: $sig_hr->{'psad_id'})\n",
2390 qq| "$sig_hr->{'msg'}"\n|;
2393 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'ip'}{'sid'}
2394 {$sig_hr->{'sid'}}{$pkt_hr->{'chain'}}{'pkts'}++;
2396 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'ip'}{'sid'}
2397 {$sig_hr->{'sid'}}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
2399 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'ip'}{'sid'}
2400 {$sig_hr->{'sid'}}{$pkt_hr->{'chain'}}{'time'} = time();
2402 $sig_sources{$sig_hr->{'sid'}}{$pkt_hr->{'src'}} = 0; ### not fwsnort
2403 $top_sigs{$sig_hr->{'sid'}}++;
2404 $top_sig_counts{$pkt_hr->{'src'}}++;
2406 return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
2408 return 1, $NO_SIG_MATCH;
2411 sub check_sig_int_range() {
2412 my ($pkt_val, $keyword, $sig_hr) = @_;
2414 $pkt_val = 0 if $pkt_val < 0;
2416 if ($sig_hr->{"${keyword}_neg"}) {
2417 if ($pkt_val <= $sig_hr->{"${keyword}_e"}
2418 and $pkt_val >= $sig_hr->{"${keyword}_s"}) {
2420 if ($verbose or $debug_sid == $sig_hr->{'sid'}) {
2421 print STDERR "[-] SID: $sig_hr->{'sid'} failed ",
2422 "$keyword test, $pkt_val <= ",
2423 qq|$sig_hr->{"${keyword}_e"} and $pkt_val |,
2424 qq|>= $sig_hr->{"${keyword}_s"}\n|;
2431 if ($pkt_val < $sig_hr->{"${keyword}_s"}) {
2433 if ($verbose or $debug_sid == $sig_hr->{'sid'}) {
2434 print STDERR "[-] SID: $sig_hr->{'sid'} failed ",
2435 "$keyword test, $pkt_val < ",
2436 qq|$sig_hr->{"${keyword}_s"} (range start)\n|;
2441 if ($pkt_val > $sig_hr->{"${keyword}_e"}) {
2443 if ($verbose or $debug_sid == $sig_hr->{'sid'}) {
2444 print STDERR "[-] SID: $sig_hr->{'sid'} failed ",
2445 "$keyword test, $pkt_val > ",
2446 qq|$sig_hr->{"${keyword}_e"} (range end)\n|;
2455 sub check_sig_ipopts() {
2456 my ($pkt_val, $keyword, $sig_hr) = @_;
2458 return 1 unless defined $sig_hr->{$keyword};
2459 return 0 unless $pkt_val;
2460 return 1 if $sig_hr->{$keyword} eq 'any';
2462 my $pkt_opts_hr = &parse_ip_options($pkt_val);
2464 return 0 unless defined $pkt_opts_hr->{$sig_hr->{$keyword}};
2468 sub check_ignore_port() {
2469 my ($port, $proto) = @_;
2470 return 0 unless defined $ignore_ports{$proto};
2471 return &match_port(\%{$ignore_ports{$proto}}, $port);
2475 my ($hr, $port) = @_;
2476 if (defined $hr->{'port'}) {
2477 return 1 if defined $hr->{'port'}->{$port};
2479 if (defined $hr->{'range'}) {
2480 for my $low_port (keys %{$hr->{'range'}}) {
2481 my $high_port = $hr->{'range'}->{$low_port};
2482 return 1 if ($port >= $low_port and $port <= $high_port);
2491 if ($pkt_hr->{'is_ipv6'}) {
2507 print STDERR "[+] p0f_ipv4(): $pkt_hr->{'src'} len: $pkt_hr->{'ip_len'}, ",
2508 "frag_bit: $pkt_hr->{'frag_bit'}, ttl: $pkt_hr->{'ttl'}, ",
2509 "win: $pkt_hr->{'win'}\n" if $debug;
2511 # p0f Fingerprint entry format:
2513 # wwww:ttt:D:ss:OOO...:OS:Version:Subtype:Details
2515 # wwww - window size (can be *, %nnn, Snn or Tnn). The special values
2516 # "S" and "T" which are a multiple of MSS or a multiple of MTU
2519 # D - don't fragment bit (0 - not set, 1 - set)
2520 # ss - overall SYN packet size
2521 # OOO - option value and order specification (see below)
2522 # OS - OS genre (Linux, Solaris, Windows)
2523 # Version - OS Version (2.0.27 on x86, etc)
2524 # Subtype - OS subtype or patchlevel (SP3, lo0)
2525 # details - Generic OS details
2527 ### S4:64:1:60:M*,S,T,N,W7: Linux:2.6:8:Linux 2.6.8 and newer (?)
2529 my $options_ar = &parse_tcp_options($pkt_hr->{'src'},
2530 $pkt_hr->{'tcp_opts'});
2532 unless ($options_ar) {
2533 print STDERR "[-] Could not fingerprint remote OS.\n" if $debug;
2539 ### try to match SYN packet length
2540 LEN: for my $sig_len (keys %p0f_ipv4_sigs) {
2541 my $matched_len = 0;
2542 if ($sig_len eq '*') { ### len can be wildcarded in pf.os
2544 } elsif ($sig_len =~ /^\%(\d+)/) {
2545 if (($pkt_hr->{'ip_len'} % $1) == 0) {
2548 } elsif ($pkt_hr->{'ip_len'} == $sig_len) {
2551 next LEN unless $matched_len;
2553 ### try to match fragmentation bit
2554 FRAG: for my $test_frag_bit ($pkt_hr->{'frag_bit'}, '*') { ### don't need "%nnn" check
2555 next FRAG unless defined $p0f_ipv4_sigs{$sig_len}{$test_frag_bit};
2557 ### find out for which p0f sigs the TTL is within range
2558 TTL: for my $sig_ttl (keys %{$p0f_ipv4_sigs{$sig_len}{$test_frag_bit}}) {
2559 unless ($pkt_hr->{'ttl'} > $sig_ttl - $config{'MAX_HOPS'}
2560 and $pkt_hr->{'ttl'} <= $sig_ttl) {
2564 ### match tcp window size
2565 WIN: for my $sig_win_size (keys
2566 %{$p0f_ipv4_sigs{$sig_len}{$test_frag_bit}{$sig_ttl}}) {
2567 my $matched_win_size = 0;
2568 if ($sig_win_size eq '*') {
2569 $matched_win_size = 1;
2570 } elsif ($sig_win_size =~ /^\%(\d+)/) {
2571 if (($pkt_hr->{'win'} % $1) == 0) {
2572 $matched_win_size = 1;
2574 } elsif ($sig_win_size =~ /^S(\d+)/) {
2575 ### window size must be a multiple of maximum
2578 for my $opt_hr (@$options_ar) {
2579 if (defined $opt_hr->{$tcp_p0f_opt_types{'M'}}) {
2580 my $mss_val = $opt_hr->{$tcp_p0f_opt_types{'M'}};
2581 if ($pkt_hr->{'win'} == $mss_val * $multiple) {
2582 $matched_win_size = 1;
2587 } elsif ($sig_win_size == $pkt_hr->{'win'}) {
2588 $matched_win_size = 1;
2591 next WIN unless $matched_win_size;
2592 TCPOPTS: for my $sig_opts (keys %{$p0f_ipv4_sigs{$sig_len}
2593 {$test_frag_bit}{$sig_ttl}{$sig_win_size}}) {
2594 my @sig_opts = split /\,/, $sig_opts;
2595 for (my $i=0; $i<=$#sig_opts; $i++) {
2596 ### tcp option order is important. Check to see if
2597 ### the option order in the packet matches the order we
2598 ### expect to see in the signature
2599 if ($sig_opts[$i] =~ /^([NMWST])/) {
2600 my $sig_letter = $1;
2602 unless (defined $options_ar->[$i]->
2603 {$tcp_p0f_opt_types{$sig_letter}}) {
2604 next TCPOPTS; ### could not match tcp option order
2607 ### MSS, window scale, and timestamp have
2608 ### specific signatures requirements on values
2609 if ($sig_letter eq 'M') {
2610 if ($sig_opts[$i] =~ /M(\d+)/) {
2611 my $sig_mss_val = $1;
2612 next TCPOPTS unless $options_ar->[$i]->
2613 {$tcp_p0f_opt_types{$sig_letter}}
2615 } elsif ($sig_opts[$i] =~ /M\%(\d+)/) {
2616 my $sig_mss_mod_val = $1;
2617 next TCPOPTS unless (($options_ar->[$i]->
2618 {$tcp_p0f_opt_types{$sig_letter}}
2619 % $sig_mss_mod_val) == 0);
2620 } ### else it is "M*" which always matches
2621 } elsif ($sig_letter eq 'W') {
2622 if ($sig_opts[$i] =~ /W(\d+)/) {
2623 my $sig_win_val = $1;
2624 next TCPOPTS unless $options_ar->[$i]->
2625 {$tcp_p0f_opt_types{$sig_letter}}
2627 } elsif ($sig_opts[$i] =~ /W\%(\d+)/) {
2628 my $sig_win_mod_val = $1;
2629 next TCPOPTS unless (($options_ar->[$i]->
2630 {$tcp_p0f_opt_types{$sig_letter}}
2631 % $sig_win_mod_val) == 0);
2632 } ### else it is "W*" which always matches
2633 } elsif ($sig_letter eq 'T') {
2634 if ($sig_opts[$i] =~ /T0/) {
2635 next TCPOPTS unless $options_ar->[$i]->
2636 {$tcp_p0f_opt_types{$sig_letter}}
2638 } ### else it is just "T" which matches
2643 OS: for my $os (keys %{$p0f_ipv4_sigs{$sig_len}
2644 {$test_frag_bit}{$sig_ttl}{$sig_win_size}
2646 my $sig = $p0f_ipv4_sigs{$sig_len}
2647 {$test_frag_bit}{$sig_ttl}{$sig_win_size}
2649 print STDERR "[+] os: $os, $sig\n" if $debug;
2651 $p0f{$pkt_hr->{'src'}}{$os} = '';
2658 if ($debug and not $matched_os) {
2659 print STDERR " Could not match $pkt_hr->{'src'} against any p0f signature.\n";
2664 sub parse_tcp_options() {
2665 my ($src, $tcp_options) = @_;
2670 unless ($tcp_options) {
2671 print STDERR 'no tcp options' if $debug;
2675 if (length($tcp_options) % 2 != 0) { ### make sure length is a multiple of two
2676 print STDERR 'tcp options length not a multiple of two.' if $debug;
2679 ### $tcp_options is a hex string like "020405B401010402" from the iptables
2681 my @chars = split //, $tcp_options;
2682 for (my $i=0; $i <= $#chars; $i += 2) {
2683 my $str = $chars[$i] . $chars[$i+1];
2684 push @hex_nums, $str;
2687 my $max_parse_attempts = $#chars;
2690 OPT: for (my $opt_kind=0; $opt_kind <= $#hex_nums;) {
2693 if ($parse_ctr > $max_parse_attempts) {
2694 print STDERR " p0f() parse_ctr $parse_ctr > ",
2695 "max_parse_attempts $max_parse_attempts\n" if $debug;
2699 last OPT unless defined $hex_nums[$opt_kind+1];
2702 my $len = hex($hex_nums[$opt_kind+1]);
2703 if (hex($hex_nums[$opt_kind]) == $tcp_nop_type) {
2704 $debug_str .= 'NOP, ' if $debug;
2705 push @opts, {$tcp_nop_type => ''};
2707 } elsif (hex($hex_nums[$opt_kind]) == $tcp_mss_type) { ### MSS
2709 for (my $i=$opt_kind+2; $i < ($opt_kind+$len); $i++) {
2710 $mss_hex .= $hex_nums[$i];
2712 my $mss = hex($mss_hex);
2713 push @opts, {$tcp_mss_type => $mss};
2714 $debug_str .= 'MSS: ' . hex($mss_hex) . ', ' if $debug;
2715 } elsif (hex($hex_nums[$opt_kind]) == $tcp_win_scale_type) {
2716 my $window_scale_hex = '';
2717 for (my $i=$opt_kind+2; $i < ($opt_kind+$len); $i++) {
2718 $window_scale_hex .= $hex_nums[$i];
2720 my $win_scale = hex($window_scale_hex);
2721 push @opts, {$tcp_win_scale_type => $win_scale};
2722 $debug_str .= 'Win Scale: ' . hex($window_scale_hex) . ', ' if $debug;
2723 } elsif (hex($hex_nums[$opt_kind]) == $tcp_sack_type) {
2724 push @opts, {$tcp_sack_type => ''};
2725 $debug_str .= 'SACK, ' if $debug;
2726 } elsif (hex($hex_nums[$opt_kind]) == $tcp_timestamp_type) {
2727 my $timestamp_hex = '';
2728 for (my $i=$opt_kind+2; $i < ($opt_kind+$len) - 4; $i++) {
2729 $timestamp_hex .= $hex_nums[$i];
2731 my $timestamp = hex($timestamp_hex);
2732 push @opts, {$tcp_timestamp_type => $timestamp};
2733 $debug_str .= 'Timestamp: ' . hex($timestamp_hex) . ', ' if $debug;
2734 } elsif (hex($hex_nums[$opt_kind]) == 0) { ### End of option list
2740 if ($len == 0 or $len == 1) {
2741 ### this should never happen; it indicates a broken TCP stack
2742 ### or maliciously constructed options since the len field is
2743 ### not large enough to accomodate the TLV encoding
2744 my $msg = "broken $len-byte len field within TCP options " .
2745 "string: $tcp_options from source IP: $src";
2746 print STDERR " $msg\n" if $debug;
2750 ### get to the next option-kind field
2755 $debug_str =~ s/\,$//;
2756 print STDERR "[+] $debug_str\n" if $debug;
2761 sub parse_ip_options() {
2762 my $ip_opts_str = shift;
2767 if (length($ip_opts_str) % 2 != 0) { ### make sure length is a multiple of two
2768 print STDERR 'IP options length not a multiple of two.' if $debug;
2771 print STDERR "[+] parse_ip_options(): matched " if $debug;
2773 push @hex_nums, $1 while $ip_opts_str =~ m|(.{2})|g;
2775 OPT: for (my $i=0; $i <= $#hex_nums; $i++) {
2776 my $val = hex($hex_nums[$i]);
2778 for my $rfc_opt_val (keys %ip_options) {
2779 next unless $val == $rfc_opt_val;
2780 if ($ip_options{$rfc_opt_val}{'len'} ne '-1') {
2781 $i += $ip_options{$rfc_opt_val}{'len'}
2782 unless $ip_options{$rfc_opt_val}{'len'} == 1;
2784 return \%ip_opts if ($i+1 > $#hex_nums);
2785 ### subtract out the option and length fields
2786 my $pkt_opt_len = hex($hex_nums[$i+1]) - 2;
2787 if ($i + $pkt_opt_len > $#hex_nums) {
2788 ### this should not happen unless the IP packet
2789 ### was truncated (i.e. the length argument for
2790 ### this option is past the IP options portion
2797 printf STDERR ("$ip_options{$rfc_opt_val}{'sig_keyword'} " .
2798 "(0x%x) ", $val) unless defined $ip_opts{$ip_options
2799 {$rfc_opt_val}{'sig_keyword'}};
2801 $ip_opts{$ip_options{$rfc_opt_val}{'sig_keyword'}} = '';
2804 print STDERR "\n" if $debug;
2812 if ($pkt_hr->{'is_ipv6'}) {
2813 &posf_ipv6($pkt_hr);
2815 &posf_ipv4($pkt_hr);
2828 my $src = $pkt_hr->{'src'};
2829 my $len = $pkt_hr->{'ip_len'};
2830 my $tos = $pkt_hr->{'tos'};
2831 my $ttl = $pkt_hr->{'ttl'};
2832 my $id = $pkt_hr->{'ip_id'};
2833 my $win = $pkt_hr->{'win'};
2839 $posf{$src}{'len'}{$len}++;
2840 $posf{$src}{'tos'}{$tos}++;
2841 $posf{$src}{'ttl'}{$ttl}++;
2842 $posf{$src}{'win'}{$win}++;
2843 $posf{$src}{'ctr'}++;
2844 push @{$posf{$src}{'id'}}, $id; ### need to maintain ordering
2846 print STDERR "[+] posf(): $src LEN: $len, TOS: $tos, TTL: $ttl, ",
2847 "ID: $id, WIN: $win\n" if $debug;
2849 $id_str = &id_incr(\@{$posf{$src}{'id'}});
2850 for my $os (keys %posf_sigs) {
2851 if ($posf{$src}{'ctr'} >= $posf_sigs{$os}{'numpkts'}) {
2852 ($min_ttl, $max_ttl) = &ttl_range($posf{$src}{'ttl'});
2853 if (defined $posf{$src}{'win'}{$posf_sigs{$os}{'win'}}
2854 # and defined $posf{$src}{'tos'}{$posf_sigs{$os}{'tos'}}
2855 and defined $posf{$src}{'len'}{$posf_sigs{$os}{'len'}}
2856 ### ttl's only decrease
2857 and ($min_ttl > ($posf_sigs{$os}{'ttl'}-$max_hops))
2858 and ($max_ttl <= $posf_sigs{$os}{'ttl'})
2859 and $id_str eq $posf_sigs{$os}{'id'}) {
2860 $posf{$src}{'guess'} = $os;
2861 print STDERR "[+] posf(): matched OS: $os\n" if $debug;
2871 for (my $i=0; $i<$#$aref; $i++) {
2873 unless ($aref->[$i] < $aref->[$i+1]
2874 and ($aref->[$i+1] - $aref->[$i]) < 1000);
2883 for my $ttl (keys %$hr) {
2884 $min_ttl = $ttl if $ttl < $min_ttl;
2885 $max_ttl = $ttl if $ttl > $max_ttl;
2887 return $min_ttl, $max_ttl;
2890 sub assign_sid_dl() {
2891 my ($sid, $dl) = @_;
2893 ### see if /etc/psad/snort_rule_dl assigns a DL (may be
2895 if (defined $snort_rule_dl{$sid}) {
2896 $dl = $snort_rule_dl{$sid};
2899 print STDERR "[+] assign_sid_dl(): snort_rule_dl ",
2900 "assigning SID $sid a danger level of ",
2906 sub add_fwsnort_sid() {
2909 my $sid = $pkt_hr->{'fwsnort_sid'};
2911 if (defined $fwsnort_sigs{$sid}) {
2913 ### see if we need to ignore this signature match
2914 my $dl = &assign_sid_dl($sid, 2);
2917 print "[+] add_fwsnort_sid(): ignoring fwsnort signature ",
2918 "match for SID: $sid (DL=0)\n" if $debug;
2919 return 0, $SIG_MATCH;
2922 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2923 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
2925 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2926 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 1;
2928 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2929 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'time'} = time();
2931 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2932 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'fwsnort_rnum'}
2933 = $pkt_hr->{'fwsnort_rnum'};
2935 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2936 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'fwsnort_estab'}
2937 = $pkt_hr->{'fwsnort_estab'};
2939 $sig_sources{$sid}{$pkt_hr->{'src'}} = 1; ### is an fwsnort sid
2941 $top_sig_counts{$pkt_hr->{'src'}}++;
2943 if ($pkt_hr->{'proto'} eq 'tcp' or $pkt_hr->{'proto'} eq 'udp') {
2945 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2946 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'dp'}
2949 if ($pkt_hr->{'proto'} eq 'tcp') {
2950 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2951 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'flags'}
2952 = $pkt_hr->{'flags'};
2956 return $dl, $SIG_MATCH;
2959 print "[-] Found sid: $sid in packet, but no ",
2960 "corresponding fwsnort rule.\n" if $debug;
2962 return 0, $NO_SIG_MATCH;
2965 sub delete_old_scans() {
2967 my $current_time = time();
2969 print STDERR "[+] delete_old_scans()\n" if $debug;
2971 ### see if we need to timeout any old scans
2972 for my $src (keys %scan) {
2973 for my $dst (keys %{$scan{$src}}) {
2974 next unless defined $scan{$src}{$dst}{'s_time'};
2975 if (($current_time - $scan{$src}{$dst}{'s_time'})
2976 >= $config{'SCAN_TIMEOUT'}) {
2977 print STDERR " delete old scan $src -> $dst\n"
2980 delete $scan{$src}{$dst};
2982 if ($config{'MAX_SCAN_IP_PAIRS'} > 0) {
2983 $scan_ip_pairs-- if $scan_ip_pairs > 0;
2991 sub dshield_email_log() {
2992 ### dshield alert interval is in hours. Check to see if there are more
2993 ### than 10,000 lines of log data (and if the last alert was sent more than
2994 ### two hours later than the previous alert), and if yes send the alert
2996 if (@dshield_data and ((time() - $last_dshield_alert)
2997 >= $dshield_alert_interval)
2998 or (($#dshield_data > 10000)
2999 and ((time() - $last_dshield_alert) >= 2*3600))) {
3000 my $dshield_version = $version;
3001 $dshield_version =~ s/^(\d+\.\d+)\.\d+/$1/;
3002 $dshield_version =~ s/-pre\d+//;
3003 my $subject = "FORMAT DSHIELD USERID $config{'DSHIELD_USER_ID'} " .
3004 "TZ $timezone psad Version $dshield_version";
3005 if ($config{'DSHIELD_USER_EMAIL'} eq 'NONE') {
3006 open MAIL, qq(| $cmds{'mail'} -s "$subject" ) .
3007 $config{'DSHIELD_ALERT_EMAIL'} or die '[*] Could not send ',
3008 'dshield alert email.';
3009 ### save this email to disk also
3010 open DSSAVE, "> $config{'DSHIELD_EMAIL_FILE'}" or die '[*] ',
3011 "Could not open $config{'DSHIELD_EMAIL_FILE'}: $!";
3012 if ($config{'DSHIELD_DL_THRESHOLD'} > 0) {
3013 for my $line (@dshield_data) {
3014 if ($line =~ /^.*?($ipv4_re)/) {
3016 if (defined $scan_dl{$src}
3018 >= $config{'DSHIELD_DL_THRESHOLD'})) {
3025 print MAIL for @dshield_data;
3026 print DSSAVE for @dshield_data;
3031 open MAIL, "| $cmds{'sendmail'} -oi -t" or die '[*] Could not ',
3032 'send dshield alert email.';
3033 ### save this email to disk also
3034 open DSSAVE, "> $config{'DSHIELD_EMAIL_FILE'}" or die '[*] ',
3035 "Could not open $config{'DSHIELD_EMAIL_FILE'}: $!";
3036 print MAIL "From: $config{'DSHIELD_USER_EMAIL'}\n",
3037 "To: $config{'DSHIELD_ALERT_EMAIL'}\n",
3038 "Subject: $subject\n";
3039 print DSSAVE "From: $config{'DSHIELD_USER_EMAIL'}\n",
3040 "To: $config{'DSHIELD_ALERT_EMAIL'}\n",
3041 "Subject: $subject\n";
3042 if ($config{'DSHIELD_DL_THRESHOLD'} > 0) {
3043 for my $line (@dshield_data) {
3044 if ($line =~ /^.*?($ipv4_re)/) {
3046 if (defined $scan_dl{$src}
3048 >= $config{'DSHIELD_DL_THRESHOLD'})) {
3055 print MAIL for @dshield_data;
3056 print DSSAVE for @dshield_data;
3062 &sys_log("sent $#dshield_data lines of log data to " .
3063 $config{'DSHIELD_ALERT_EMAIL'});
3065 ### store the current time
3066 $last_dshield_alert = time();
3068 ### increment stats counters
3069 $dshield_email_ctr++;
3070 $dshield_lines_ctr += $#dshield_data;
3072 ### clear the dshield data array so we don't re-send
3076 ### Write Dshield stats to disk
3077 &write_dshield_stats();
3082 sub check_icmp_type() {
3083 my ($proto, $valid_types_hr, $type, $code) = @_;
3084 print STDERR " check_icmp_type(type: $type, code: $code)\n" if $debug;
3085 if (not defined $valid_types_hr->{$type}) {
3086 print STDERR " bad $proto type\n" if $debug;
3087 return $BAD_ICMP_TYPE;
3088 } elsif (not defined $valid_types_hr->{$type}->{'codes'}->{$code}) {
3089 print STDERR " bad $proto code\n" if $debug;
3090 return $BAD_ICMP_CODE;
3092 print STDERR " valid $proto type/code\n" if $debug;
3096 sub import_perl_modules() {
3098 my $mod_paths_ar = &get_mod_paths();
3100 if ($#$mod_paths_ar > -1) { ### /usr/lib/psad/ exists
3101 push @$mod_paths_ar, @INC;
3102 splice @INC, 0, $#$mod_paths_ar+1, @$mod_paths_ar;
3106 print STDERR "[+] import_perl_modules(): The \@INC array:\n";
3107 print STDERR "$_\n" for @INC;
3110 require IPTables::ChainMgr;
3111 require NetAddr::IP;
3113 require Unix::Syslog;
3115 require Storable if $store_file;
3117 Date::Calc->import(qw(Timezone This_Year Decode_Month
3118 Today Date_to_Time This_Year Mktime Localtime));
3119 Unix::Syslog->import(qw(:subs :macros));
3120 Storable->import(qw(retrieve store)) if $store_file;
3122 $imported_syslog_module = 1;
3127 sub get_mod_paths() {
3131 $config{'PSAD_LIBS_DIR'} = $lib_dir if $lib_dir;
3133 unless (-d $config{'PSAD_LIBS_DIR'}) {
3134 my $dir_tmp = $config{'PSAD_LIBS_DIR'};
3135 $dir_tmp =~ s|lib/|lib64/|;
3137 $config{'PSAD_LIBS_DIR'} = $dir_tmp;
3143 opendir D, $config{'PSAD_LIBS_DIR'}
3144 or die "[*] Could not open $config{'PSAD_LIBS_DIR'}: $!";
3145 my @dirs = readdir D;
3148 push @paths, $config{'PSAD_LIBS_DIR'};
3150 for my $dir (@dirs) {
3151 ### get directories like "/usr/lib/psad/x86_64-linux"
3152 next unless -d "$config{'PSAD_LIBS_DIR'}/$dir";
3153 push @paths, "$config{'PSAD_LIBS_DIR'}/$dir"
3154 if $dir =~ m|linux| or $dir =~ m|thread|
3155 or (-d "$config{'PSAD_LIBS_DIR'}/$dir/auto");
3165 ### set umask to -rw-------
3168 ### turn off buffering
3171 $no_syslog_alerts = 1 if $analyze_mode or $status_mode or $test_mode;
3172 $no_email_alerts = 1 if $test_mode;
3173 $debug = 1 if $test_mode;
3175 ### import any override config files first
3176 &import_override_configs() if $override_config_str;
3178 ### import psad.conf
3179 &import_config($config_file);
3181 ### import FW_MSG_SEARCH strings
3182 &import_fw_search($config_file);
3184 ### expand any embedded vars within config values
3189 'psadwatchd' => $config{'PSADWATCHD_PID_FILE'},
3190 'psad' => $config{'PSAD_PID_FILE'},
3191 'kmsgsd' => $config{'KMSGSD_PID_FILE'},
3194 ### dump configuration to STDOUT
3195 if ($dump_conf or $dump_ipt_policy) {
3198 $rv = &dump_conf() if $dump_conf;
3199 $rv_tmp = &dump_ipt_policy() if $dump_ipt_policy;
3200 $rv += $rv_tmp if $rv_tmp != 0;
3204 ### make sure all necessary configuration variables
3208 ### store the psad command line.
3209 $cmdline_file = $config{'PSAD_CMDLINE_FILE'};
3211 ### make sure the values in the config file make sense
3214 ### setup the appropriate iptables data file depending on whether
3215 ### SYSLOG_DAEMON is set to ulogd, or if ENABLE_SYSLOG_FILE is set
3217 unless ($fw_data_file) {
3218 if ($config{'SYSLOG_DAEMON'} =~ /ulog/i) {
3219 $fw_data_file = $config{'ULOG_DATA_FILE'};
3220 } elsif ($config{'ENABLE_SYSLOG_FILE'} eq 'Y') {
3221 $fw_data_file = $config{'IPT_SYSLOG_FILE'};
3223 $fw_data_file = $config{'FW_DATA_FILE'};
3227 ### check to make sure the commands specified in the config section
3228 ### are in the right place, and attempt to correct automatically if not.
3229 ### (wget is only needed in --sig-update mode)
3230 &check_commands({'wget' => ''});
3232 ### import psad perl modules
3233 &import_perl_modules();
3235 ### download latest signatures from
3236 ### http://www.cipherdyne.org/psad/signatures
3237 exit &download_signatures() if $download_sigs;
3239 ### set some config variables based on command line input
3242 ### build iptables block config hash out of IPT_AUTO_CHAIN keywords
3243 ### (we don't check ENABLE_AUTO_IDS here since someone may have turned
3244 ### it off but still want to run --Status checks or use --Flush).
3245 &build_ipt_config() unless $syslog_server;
3247 ### The --Kill command line switch was given.
3248 exit &stop_psad() if $kill;
3250 ### The --HUP command line switch was given.
3251 exit &hup() if $hup;
3253 ### The --USR1 command line switch was given.
3254 exit &usr1() if $usr1;
3256 ### The --Flush command line switch was given.
3257 exit &sockwrite_flush_auto_rules() if $flush_fw;
3259 ### the --Restart command line switch was given
3260 exit &restart() if $restart;
3262 ### list any existing iptables IPT_AUTO_CHAIN chains
3263 exit &ipt_list_auto_chains() if $fw_list_auto;
3265 ### add an IP/network to the psad auto blocking chains via the
3266 ### domain socket (note that &sockwrite_add_ipt_block_ip() calls
3267 ### &import_auto_dl() to make sure we don't add an IP that should
3269 exit &sockwrite_add_ipt_block_ip() if $fw_block_ip;
3271 ### delete IP/network from psad auto blocking chains
3272 exit &sockwrite_rm_ipt_block_ip() if $fw_rm_block_ip;
3274 ### send a warning via syslog if the HOME_NET variable definition
3275 ### appears to include a subnet that is not directly connected to
3276 ### the local system.
3277 &validate_home_net();
3279 ### import icmp types and codes from psad_icmp_types; icmp "type"
3280 ### and "code" fields will be validated against the values in this
3282 &import_icmp_types('icmp', \%valid_icmp_types, $config{'ICMP_TYPES_FILE'})
3283 unless $no_icmp_types;
3284 &import_icmp_types('icmp6', \%valid_icmp6_types,
3285 $config{'ICMP6_TYPES_FILE'}) unless $no_icmp6_types;
3287 ### import p0f-based passive OS fingerprinting signatures
3288 &import_p0f_ipv4_sigs() unless $no_posf;
3290 ### import TOS-based passive OS fingerprinting signatures
3291 &import_posf_sigs() unless $no_posf;
3293 ### import auto_dl file for automatic ip/network danger
3294 ### level assignment
3295 &import_auto_dl() unless $no_auto_dl;
3297 ### parse snort rules if we enable psad to match on iptables log
3298 ### messages that include snort SID's (see "fwsnort":
3299 ### http://www.cipherdyne.org/fwsnort).
3300 &import_snort_rules() unless $no_snort_sids;
3302 ### import psad signatures (note that these signatures have been
3303 ### adapted from the Snort IDS and contain several keywords that
3304 ### were added by the psad project).
3305 &import_signatures() unless $no_signatures;
3307 ### the --Status command line switch was given
3308 exit &status() if $status_mode;
3310 ### there is a set of ports that should be ignored
3311 &parse_ignore_ports();
3313 ### there is a set of protocols that should be ignored
3314 &parse_ignore_protocols();
3316 ### there is a set of interfaces that should be ignored
3317 &parse_ignore_interfaces();
3319 ### enter iptables analysis mode.
3320 exit &analysis_mode() if $analyze_mode;
3322 ### enter CSV output mode.
3323 exit &csv_mode() if $csv_mode or $gnuplot_mode;
3325 ### enter benchmarking mode
3326 exit &benchmark_mode() if $benchmark;
3328 ### analyze the iptables policy and exit
3329 my $rv = &fw_analyze_mode();
3330 exit $rv if $fw_analyze;
3332 ### make sure we are setup to run
3336 &dump_conf() if $debug;
3338 if ($restrict_ip_cmdline) {
3339 $restrict_ip = new NetAddr::IP $restrict_ip_cmdline
3340 or die "[*] Could not acquire NetAddr::IP object for $restrict_ip";
3346 sub validate_config() {
3348 &check_enable_vars_value();
3350 &is_digit_range('PORT_RANGE_SCAN_THRESHOLD', 0, 65535);
3351 &is_digit_range('TOP_SCANS_CTR_THRESHOLD', 0, 500);
3352 &is_digit_range('DSHIELD_ALERT_INTERVAL', 1, 24);
3353 &is_digit_range('TOP_PORTS_LOG_THRESHOLD', 0, 65535);
3354 &is_digit_range('STATUS_PORTS_THRESHOLD', 0, 65535);
3355 &is_digit_range('TOP_IP_LOG_THRESHOLD', 0, 10000);
3356 &is_digit_range('STATUS_IP_THRESHOLD', 0, 10000);
3358 ### it will be a long time before there are 100000 signatures
3359 &is_digit_range('TOP_SIGS_LOG_THRESHOLD', 0, 100000);
3360 &is_digit_range('STATUS_SIGS_THRESHOLD', 0, 10000);
3362 die qq([*] Invalid EMAIL_ADDRESSES value: "$config{'EMAIL_ADDRESSES'}")
3363 unless $config{'EMAIL_ADDRESSES'} =~ /\S+\@\S+/;
3365 ### translate commas into spaces
3366 $config{'EMAIL_ADDRESSES'} =~ s/\s*\,\s/ /g;
3368 if ($config{'ENABLE_AUTO_IDS'} eq 'Y'
3369 and $config{'IPTABLES_BLOCK_METHOD'} eq 'N'
3370 and $config{'TCPWRAPPERS_BLOCK_METHOD'} eq 'N') {
3371 die 'config warning, ENABLE_AUTO_IDS=Y, but ' .
3372 'both IPTABLES_BLOCK_METHOD and TCPWRAPPERS_BLOCK_METHOD are ' .
3375 if ($status_min_dl and $status_min_dl > 5) {
3376 die '[*] The --status-dl must be between 1 and 5.';
3378 if ($no_kmsgsd and not $debug) {
3379 die '[*] The --no-kmsgsd option can only be used with --debug.';
3382 if ($fw_del_chains and not $flush_fw) {
3383 die '[*] The --fw-del-chains option can only be used with --Flush.';
3387 unless ($fw_block_ip =~ m|^\s*$ipv4_re\s*$|
3388 or $fw_block_ip =~ m|^\s*$ipv4_re/\d+\s*$|
3389 or $fw_block_ip =~ m|^\s*$ipv4_re/$ipv4_re\s*$|) {
3390 die '[*] The --fw-block-ip argument accepts ' .
3391 'an IP address or network.';
3395 if ($fw_rm_block_ip) {
3396 unless ($fw_rm_block_ip =~ m|^\s*$ipv4_re\s*$|
3397 or $fw_rm_block_ip =~ m|^\s*$ipv4_re/\d+\s*$|
3398 or $fw_rm_block_ip =~ m|^\s*$ipv4_re/$ipv4_re\s*$|) {
3399 die '[*] The --fw-rm-block-ip argument accepts ' .
3400 'an IP address or network.';
3404 if ($config{'ENABLE_SYSLOG_FILE'} eq 'Y') {
3405 die "[*] Cannot set IPT_SYSLOG_FILE and FW_DATA_FILE to point ",
3406 "at the same file." if $config{'IPT_SYSLOG_FILE'}
3407 eq $config{'FW_DATA_FILE'};
3410 unless ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL7/i
3411 or $config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL6/i
3412 or $config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL5/i
3413 or $config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL4/i
3414 or $config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL3/i
3415 or $config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL2/i
3416 or $config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL1/i
3417 or $config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL0/i) {
3418 die "[*] Unrecognized SYSLOG_FACILITY, see psad.conf";
3421 unless ($config{'SYSLOG_PRIORITY'} =~ /LOG_INFO/i
3422 or $config{'SYSLOG_PRIORITY'} =~ /LOG_DEBUG/i
3423 or $config{'SYSLOG_PRIORITY'} =~ /LOG_NOTICE/i
3424 or $config{'SYSLOG_PRIORITY'} =~ /LOG_WARNING/i
3425 or $config{'SYSLOG_PRIORITY'} =~ /LOG_ERR/i
3426 or $config{'SYSLOG_PRIORITY'} =~ /LOG_CRIT/i
3427 or $config{'SYSLOG_PRIORITY'} =~ /LOG_ALERT/i
3428 or $config{'SYSLOG_PRIORITY'} =~ /LOG_EMERG/i) {
3429 die "[*] Unrecognized SYSLOG_PRIORITY, see psad.conf";
3432 if ($analyze_mode or $gnuplot_mode or $csv_mode) {
3433 $fw_data_file = $config{'IPT_SYSLOG_FILE'}
3434 unless $fw_data_file;
3437 if ($gnuplot_mode and not $csv_fields) {
3438 die "[*] Must specify which iptables fields to plot with the ",
3439 "--CSV-fields argument."
3445 sub check_enable_vars_value() {
3446 for my $var (keys %config) {
3447 next unless $var =~ /^ENABLE_/;
3448 ### make sure that all of the "ENABLE_" vars have a value of
3450 $config{$var} = uc($config{$var});
3451 unless ($config{$var} eq 'Y' or $config{$var} eq 'N'
3452 or $config{$var} eq 'YES' or $config{$var} eq 'NO') {
3453 die "[*] $var variable must be 'Y' or 'N'";
3459 sub is_digit_range() {
3460 my ($key, $min, $max) = @_;
3461 die "[*] $key must be an integer >= $min and <= $max"
3462 unless $config{$key} =~ m|^\d+$| and $config{$key} >= $min
3463 and $config{$key} <= $max;
3467 sub get_connected_subnets() {
3469 my @connected_subnets = ();
3471 if ($config{'IFCFGTYPE'} =~ /iproute2/i) {
3472 my @ip_out = @{&run_command($cmds{'ip'}, 'addr')};
3474 my $home_net_str = '';
3475 for my $line (@ip_out) {
3476 if ($line =~ /^\d+:\s+(\S+): </) {
3480 next if $intf_name eq 'lo';
3481 next if $intf_name =~ /dummy/i;
3482 if ($line =~ /^\s+inet.*?($ipv4_re)\/(\d+)/i) {
3483 push @connected_subnets, new NetAddr::IP($1, $2);
3484 } elsif ($line =~ /inet6\s(\S+)/) {
3485 push @connected_subnets, new6 NetAddr::IP($1);
3489 my @ifconfig_out = @{&run_command($cmds{'ifconfig'}, '-a')};
3491 my $home_net_str = '';
3492 for my $line (@ifconfig_out) {
3493 if ($line =~ /^(\S+)\s+Link/) {
3497 next if $intf_name eq 'lo';
3498 next if $intf_name =~ /dummy/i;
3499 if ($line =~ /^\s+inet.*?:($ipv4_re).*:($ipv4_re)/i) {
3500 push @connected_subnets, new NetAddr::IP($1, $2);
3501 } elsif ($line =~ /^\s+inet6\saddr:\s+(\S+)/) {
3502 push @connected_subnets, new6 NetAddr::IP($1);
3506 return \@connected_subnets;
3509 sub validate_home_net() {
3513 return if $config{'HOME_NET'} eq 'any'
3514 and $config{'ENABLE_INTF_LOCAL_NETS'} eq 'N';
3516 my $connected_subnets_ar = &get_connected_subnets();
3518 if ($config{'ENABLE_INTF_LOCAL_NETS'} eq 'Y' and not $analyze_mode) {
3520 my $connected_str = '';
3521 for my $net (@$connected_subnets_ar) {
3522 push @local_nets, $net;
3523 $connected_str .= $net->network()->cidr() . ", ";
3525 $connected_str =~ s|,\s*$||;
3527 $config{'HOME_NET'} = $connected_str;
3528 $config{'HOME_NET'} = 'any' unless $connected_str;
3532 if ($config{'HOME_NET'} =~ /CHANGEME/) {
3533 &sys_log('config warning: the HOME_NET ' .
3534 'variable has not been set, defaulting to "any"');
3536 $config{'HOME_NET'} = 'any';
3539 my @home_nets = split /\s*\,\s*/, $config{'HOME_NET'};
3540 my $found_one_net = 0;
3541 for my $net (@home_nets) {
3543 if ($net =~ m|($ipv4_re/$ipv4_re)|) {
3544 $home_net = new NetAddr::IP $1;
3545 } elsif ($net =~ m|($ipv4_re/\d+)|) {
3546 $home_net = new NetAddr::IP $1;
3547 } elsif ($net =~ m|($ipv4_re)|) {
3548 $home_net = new NetAddr::IP $1;
3552 push @local_nets, new NetAddr::IP $net;
3554 for my $net (@$connected_subnets_ar) {
3555 $found = $found_one_net = 1
3556 if $home_net->within($net);
3559 ### note that this might be ok if psad is running on a syslog
3560 ### server, but the most likely explanation is that there was a
3561 ### typo in the HOME_NET variable defintion.
3562 &sys_log('config warning: HOME_NET definition ' .
3563 qq|in psad.conf contains "$home_net" which does not appear | .
3564 "to be directly connected to the local system.");
3567 $config{'HOME_NET'} = 'any' unless $found_one_net;
3573 my ($ip, $ip_obj) = @_;
3575 print STDERR "[+] is_local(): $ip..." if $debug;
3578 print STDERR "(test mode enabled) no\n";
3583 for my $net (@local_nets) {
3584 if ($ip_obj->within($net)) {
3590 $found == 0 ? print STDERR "no\n" : print STDERR "yes\n";
3595 sub import_ip_options() {
3599 open O, "< $config{'IP_OPTS_FILE'}" or die
3600 "[*] Couild not open IP options file $config{'IP_OPTS_FILE'}: $!";
3604 ### 136 4 satid Stream Identifier
3605 ### 145 -1 extproto Extended Internet Proto
3606 if (/^\s*(\d+)\s+(\S+)\s+(\w+)\s+(.*)\s*/) {
3607 $ip_options{$1}{'len'} = $2;
3608 $ip_options{$1}{'sig_keyword'} = $3;
3609 $ip_options{$1}{'desc'} = $4;
3615 print STDERR "[+] IP options:\n", Dumper(\%ip_options)
3616 if $debug and $verbose;
3621 sub import_fw_search() {
3622 my $config_file = shift;
3626 open F, "< $config_file" or die "[*] Could not open config ",
3627 "string file $config_file: $!";
3630 my $found_fw_search = 0;
3631 for my $line (@lines) {
3632 next unless $line =~ /\S/;
3633 next if $line =~ /^\s*#/;
3634 if ($line =~ /^\s*FW_MSG_SEARCH\s+(.*?);/) {
3635 push @fw_search, $1;
3636 $found_fw_search = 1;
3637 } elsif ($line =~ /^\s*FW_SEARCH_ALL\s+(\w+);/) {
3639 if ($strategy eq 'Y' or $strategy eq 'N') {
3640 $config{'FW_SEARCH_ALL'} = $strategy;
3644 unless (defined $config{'FW_SEARCH_ALL'}) {
3645 $config{'FW_SEARCH_ALL'} = 'Y';
3648 unless ($config{'FW_SEARCH_ALL'} eq 'Y' or
3649 $config{'FW_SEARCH_ALL'} eq 'N') {
3650 $config{'FW_SEARCH_ALL'} = 'Y';
3653 if ($config{'FW_SEARCH_ALL'} eq 'N' and not $found_fw_search) {
3654 push @fw_search, 'DROP';
3659 sub parse_ignore_ports() {
3661 ### zero out the hash since a HUP signal may have been received
3664 return if $config{'IGNORE_PORTS'} eq 'NONE';
3666 &parse_port_range(\%ignore_ports, $config{'IGNORE_PORTS'});
3671 sub parse_port_range() {
3672 my ($hr, $line) = @_;
3674 my @fields = split /\s*,\s*/, $line;
3675 for my $field (@fields) {
3676 if ($field =~ m/(tcp|udp)\/(\d+)\s*-\s*(\d+)/i) {
3681 my $existing_high = 0;
3682 if (defined $hr->{$proto}
3683 and defined $hr->{$proto}->{'range'}
3684 and defined $hr->{$proto}->{'range'}->{$low}) {
3685 $existing_high = $hr->{$proto}->{'range'}->{$low};
3687 if ($existing_high) {
3688 if ($high > $existing_high) {
3689 $hr->{$proto}->{'range'}->{$low} = $high;
3692 $hr->{$proto}->{'range'}->{$low} = $high;
3695 } elsif ($field =~ m/(tcp|udp)\/(\d+)/i) {
3698 $hr->{$proto}->{'port'}->{$port} = '';
3704 sub parse_ignore_protocols() {
3706 ### zero out the hash since a HUP signal may have been received
3707 %ignore_protocols = ();
3709 return if $config{'IGNORE_PROTOCOLS'} eq 'NONE';
3711 my @protos = split /\s*,\s*/, $config{'IGNORE_PROTOCOLS'};
3712 for my $proto (@protos) {
3713 if ($proto =~ /\W/) {
3714 &sys_log('invalid protocol in IGNORE_PROTOCOLS var');
3716 if ($proto =~ /^\d+$/) {
3717 ### IP protocol number
3718 $ignore_protocols{$proto} = '';
3720 $ignore_protocols{lc($proto)} = '';
3727 sub parse_ignore_interfaces() {
3729 ### zero out the hash since a HUP signal may have been received
3730 %ignore_interfaces = ();
3732 return if $config{'IGNORE_INTERFACES'} eq 'NONE';
3734 my @interfaces = split /\s*,\s*/, $config{'IGNORE_INTERFACES'};
3735 for my $intf (@interfaces) {
3736 if ($intf =~ /\W/) {
3737 &sys_log('invalid interface in IGNORE_INTERFACES var');
3739 $ignore_interfaces{$intf} = '';
3745 sub import_snort_rules() {
3749 opendir D, $config{'SNORT_RULES_DIR'}
3750 or die "[*] Could not open $config{'SNORT_RULES_DIR'}";
3751 my @rfiles = readdir D;
3754 FILE: for my $rfile (@rfiles) {
3755 next FILE unless $rfile =~ /\.rules$/;
3757 next FILE unless $rfile =~ /^${srules_type}\.rules$/;
3759 my ($type) = ($rfile =~ /(\w+)\.rules/);
3760 open R, "< ${config{'SNORT_RULES_DIR'}}/${rfile}" or
3761 die "[*] Could not open: ${srules_type}/${rfile}";
3764 RULE: for my $line (@lines) {
3765 next RULE unless $line =~ /^\s*alert/;
3768 my $sid; ### snort rule id
3769 if ($line =~ /[\s;]sid:\s*(\d+)\s*;/) {
3775 $fwsnort_sigs{$sid}{'msg'} = $1
3776 if $line =~ /msg:\s*\"(.*?)\"\s*;/;
3777 $fwsnort_sigs{$sid}{'is_psad_id'} = 0;
3779 if ($line =~ /^\s*alert\s+(\w+)/) {
3780 $fwsnort_sigs{$sid}{'proto'} = lc($1);
3783 if ($line =~ /[\s;]classtype:\s*(.*?)\s*;/) {
3784 $fwsnort_sigs{$sid}{'classtype'} = $1;
3786 $fwsnort_sigs{$sid}{'classtype'} = '';
3789 $fwsnort_sigs{$sid}{'priority'} = &convert_snort_priority($1)
3790 if $line =~ /[\s;]priority:\s*(\d+)\s*;/;
3792 ### import multiple content fields; someone could have built
3793 ### a series of custom iptables chains in order to detect
3794 ### multiple content strings.
3795 while ($line =~ /[\s;](?:uri)?content:\s*\"(.*?)\"\s*;/g) {
3796 push @{$fwsnort_sigs{$sid}{'content'}}, $1;
3799 while ($line =~ /[\s;]reference:\s*(.*?)\s*;/g) {
3801 if ($ref =~ /^(\w+),(\S+)/) {
3802 ### reference:bugtraq,9732;
3803 push @{$fwsnort_sigs{$sid}{'reference'}{lc($1)}}, $2;
3807 next RULE unless defined $fwsnort_sigs{$sid}{'msg'}
3808 and defined $fwsnort_sigs{$sid}{'classtype'}
3809 and defined $fwsnort_sigs{$sid}{'content'};
3813 ### import the Snort classification.config file
3814 &import_snort_class_priorities();
3816 ### import the reference.config file
3817 &import_snort_reference_config();
3819 ### import any specific SID -> DL mappings from the
3820 ### snort_rule_dl file
3821 &import_snort_rule_dl();
3823 print STDERR Dumper %fwsnort_sigs if $debug and $verbose;
3824 &sys_log("imported original Snort rules in " .
3825 "$config{'SNORT_RULES_DIR'}/ for reference info");
3829 sub import_snort_class_priorities() {
3831 my $snort_class_file = "$config{'SNORT_RULES_DIR'}/classification.config";
3833 return unless -e $snort_class_file;
3834 open F, "< $snort_class_file" or die $!;
3836 ### config classification: rpc-portmap-decode,Decode of an RPC Query,2
3837 if (/config\s+classification:\s+(\S+),.*(\d+)/) {
3838 ### the snort priority value can go from 1 to 10, with 1 being the
3839 ### worst offense and 10 being the least. Most priorities are
3840 ### from 1 to 4. We need to map these into the psad danger levels
3841 ### (reversed). NOTE: the Snort engine does not enforce the 1-10
3843 $snort_class_dl{$1} = &convert_snort_priority($2);
3847 &sys_log('imported Snort classification.config');
3851 sub convert_snort_priority() {
3852 my $snort_priority = shift;
3855 if ($snort_priority == 1) {
3857 } elsif ($snort_priority == 2) {
3859 } elsif ($snort_priority == 3) {
3861 } elsif ($snort_priority == 4) {
3867 sub import_snort_reference_config() {
3869 my $ref_file = "$config{'SNORT_RULES_DIR'}/reference.config";
3870 return unless -e $ref_file;
3872 open F, "< $ref_file" or die $!;
3874 if (/^\s*config\s+reference:\s+(\w+)\s+(\S+)/) {
3875 ### config reference: bugtraq http://www.securityfocus.com/bid/
3876 $snort_ref_baseurl{lc($1)} = $2;
3883 sub import_snort_rule_dl() {
3885 %snort_rule_dl = ();
3887 ### parse the snort_rule_dl file
3888 return unless -e $config{'SNORT_RULE_DL_FILE'};
3889 open F, "< $config{'SNORT_RULE_DL_FILE'}" or die $!;
3891 next unless /^\s*\d/;
3892 if (/^\s*(\d+)\s+(\d+)/) {
3896 unless ($dl >= 0 and $dl < 6) {
3899 $snort_rule_dl{$sid} = $dl;
3906 ### for signatures that psad is able to detect with iptables logs that do
3907 ### not contain "SIDnnn" messages generated by fwsnort (and hence have no
3908 ### application layer matching criteria)
3910 sub import_signatures() {
3912 ### import the ip_options file so that psad can make use of the
3913 ### ipopts keyword in Snort rules (requires --log-ip-options to
3915 &import_ip_options();
3917 ### undef so we don't leave old signatures around if
3918 ### we execute this code after receiving a HUP signal.
3923 ### make sure no duplicate psad_id and sid fields exist
3927 open SIGS, "< $config{'SIGS_FILE'}" or die
3928 "[*] Could not open the signatures file $config{'SIGS_FILE'}: $!";
3932 my $next_available_sid = 1;
3934 SIG: while (<SIGS>) {
3937 next SIG unless /\S/;
3938 next SIG if /^\s*#/;
3942 ### alert tcp $HOME_NET 12345:12346 -> $EXTERNAL_NET any
3943 ### (msg:"BACKDOOR netbus active"; flow:from_server,established;
3944 ### content:"NetBus"; reference:arachnids,401; classtype:misc-activity;
3945 ### sid:109; rev:4; psad_dlevel:2)
3948 my $rule_options = '';
3950 if (m|^(.*?)\s+\((.*)\)|) {
3954 die "[*] import_signatures(): bad signature on line: ",
3958 ### parse rule header (routine taken from fwsnort).
3959 if ($rule_hdr =~ m|^\s*alert\s+(\S+)\s+(\S+)\s+(\S+)
3960 \s+(\S+)\s+(\S+)\s+(\S+)|x) {
3963 if ($direction eq '<>') {
3971 if ($direction eq '<-') {
3972 $sig{'proto'} = lc($1);
3973 $src = $5; ### switch src and dst
3978 $sig{'proto'} = lc($1);
3979 $src = $2; ### normal src -> dst
3985 $sig{'src'} = &expand_sig_ips($src, $line_num);
3986 $sig{'dst'} = &expand_sig_ips($dst, $line_num);
3988 ### assign the source and destination port ranges
3989 &build_sig_int_range(\%sig, 'sp', 1, 65535, $line_num);
3990 &build_sig_int_range(\%sig, 'dp', 1, 65535, $line_num);
3993 die "[*] import_signatures(): bad rule ",
3994 "header on line: $line_num";
3997 ### make sure the signature does not contain any unsupported
3998 ### Snort rule options
3999 unless (&check_supported_options($rule_options, $line_num)) {
4003 ### parse rule options
4004 if ($rule_options =~ /[\s;]psad_id:\s*(\d+)\s*;/) {
4006 if (defined $psad_ids{$psad_id}) {
4007 die "[*] import_signatures(): Duplicate psad_id: $psad_id ",
4008 qq|on line: $line_num|;
4009 } elsif (defined $sids{$psad_id}) {
4010 die "[*] import_signatures(): Duplicate psad_id: $psad_id ",
4011 qq|to sid on line: $line_num|;
4013 $psad_ids{$psad_id} = '';
4014 $sig{'psad_id'} = $psad_id;
4015 $next_available_sid = $psad_id if $psad_id > $next_available_sid;
4017 my $msg = "[*] import_signatures(): could not find signature" .
4018 qq| "psad_id" on line: $line_num|;
4019 if ($config{'ENABLE_SNORT_SIG_STRICT'} eq 'Y') {
4027 ### original Snort sid
4028 if ($rule_options =~ /[\s;]sid:\s*(\d+)\s*;/) {
4030 if (defined $sids{$sid}) {
4031 die "[*] import_signatures(): Duplicate sid: $sid ",
4032 qq|on line: $line_num|;
4033 } elsif (defined $psad_ids{$sid}) {
4034 die "[*] import_signatures(): Duplicate sid: $sid ",
4035 qq|to psad_id on line: $line_num|;
4039 $sig{'is_psad_id'} = 0;
4040 $next_available_sid = $sid if $sid > $next_available_sid;
4042 ### the signature was derived from several Snort rules
4043 $sig{'sid'} = $sig{'psad_id'};
4044 $sig{'is_psad_id'} = 1;
4048 if ($rule_options =~ /msg:\s*\"(.+?)\"\s*;/) {
4051 die "[*] import_signatures(): could not find ",
4052 qq|"msg" keyword on line: $line_num|;
4056 if ($rule_options =~ /[\s;]classtype:\s*(.+?)\s*;/) {
4057 $sig{'classtype'} = $1;
4059 $sig{'classtype'} = '';
4063 while ($rule_options =~ /[\s;]reference:\s*(.*?)\s*;/g) {
4065 if ($ref =~ /^(\w+),(\S+)/) {
4066 ### reference:bugtraq,9732;
4067 push @{$sig{'reference'}{lc($1)}}, $2;
4071 ### psad danger level
4072 $sig{'dl'} = 2; ### default danger level
4073 if ($rule_options =~ /[\s;]psad_dl:\s*(\d+)/) {
4075 } elsif ($sig{'classtype'}) {
4076 ### assign the danger level from the classification.config
4077 ### file if the psad_dl field does not exist
4078 if (defined $snort_class_dl{$sig{'classtype'}}) {
4079 $sig{'dl'} = $snort_class_dl{$sig{'classtype'}};
4083 ### see the signature was derived from a set of Snort rules
4084 if ($rule_options =~ /[\s;]psad_derived_sids:\s*(.+?)\s*;/) {
4085 $sig{'psad_derived_sids'} = [split /\s*,\s*/, $1];
4089 if ($rule_options =~ /[\s;]sameip;/) {
4093 ### psad_dsize keyword
4094 if ($rule_options =~ /[\s;]psad_dsize:\s*(.+?)\s*;/i) {
4095 $sig{'psad_dsize'} = $1;
4096 &build_sig_int_range(\%sig, 'psad_dsize', 1, 1514, $line_num);
4099 ### psad_ip_len keyword
4100 if ($rule_options =~ /[\s;]psad_ip_len:\s*(.+?)\s*;/i) {
4101 $sig{'psad_ip_len'} = $1;
4102 ### technically, the minimum length must be $IP_HEADER_LEN
4103 &build_sig_int_range(\%sig, 'psad_ip_len', 1, 65536, $line_num);
4107 if ($rule_options =~ /[\s;]dsize:\s*(.+?)\s*;/i) {
4109 &build_sig_int_range(\%sig, 'dsize', 1, 1514, $line_num);
4113 if ($rule_options =~ /[\s;]ttl:\s*(.+?)\s*;/i) {
4115 &build_sig_int_range(\%sig, 'ttl', 1, 255, $line_num);
4118 ### id keyword (for IP ID value)
4119 if ($rule_options =~ /[\s;]id:\s*(.+?)\s*;/i) {
4121 &build_sig_int_range(\%sig, 'id', 1, 65535, $line_num);
4124 ### ipopts keyword (for IP options)
4125 if ($rule_options =~ /[\s;]ipopts:\s*(.+?)\s*;/i) {
4126 $sig{'ipopts'} = lc($1);
4128 ### make sure the IP option is defined in the ip_options
4131 for my $opt_val (keys %ip_options) {
4133 if $sig{'ipopts'} eq $ip_options{$opt_val}{'sig_keyword'};
4136 print STDERR qq|[-] Invalid argument "$sig{'ipopts'}" to |,
4137 "ipopts keyword\n" if $debug;
4142 if ($sig{'proto'} eq 'tcp') {
4143 my $require_ack = 0;
4144 if ($rule_options =~ /[\s;]flow:\s*established\s*\;/i) {
4149 if ($rule_options =~ /[\s;]flags:\s*(.+?)\s*;/) {
4152 ### make flags identical to what iptables log messages
4153 ### would report (check in iptables flag reporting order).
4154 if ($sig_flags =~ /U/) {
4156 $flags = 'URG ' . $flags;
4161 if ($sig_flags =~ /A/ or $require_ack) {
4164 $flags .= 'PSH ' if $sig_flags =~ /P/;
4165 $flags .= 'RST ' if $sig_flags =~ /R/;
4166 $flags .= 'SYN ' if $sig_flags =~ /S/;
4167 $flags .= 'FIN ' if $sig_flags =~ /F/;
4169 ### if no flags are set iptables simply reports no flags
4170 ### at all instead of reporting "NULL".
4171 $flags = 'NULL' if $sig_flags =~ /N/;
4172 $flags =~ s/\s*$// if $flags;
4174 $sig{'flags'} = $flags;
4177 ### seq keyword (TCP sequence number)