bumped version to psad-2.3-pre1
[psad.git] / psad
1 #!/usr/bin/perl -w
2 #
3 ################################################################################
4 #
5 # File: psad (/usr/sbin/psad)
6 #
7 # URL: http://www.cipherdyne.org/psad/
8 #
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).
15 #
16 #          For more information read the psad man page or view the
17 #          documentation provided at: http://www.cipherdyne.org/psad/
18 #
19 # Author: Michael Rash (mbr@cipherdyne.org)
20 #
21 # Credits: (see the CREDITS file bundled with the psad sources.)
22 #
23 # Version: psad-2.3-pre1
24 #
25 # Copyright (C) 1999-2012 Michael Rash (mbr@cipherdyne.org)
26 #
27 # Reference: Snort is a registered trademark of Sourcefire, Inc.
28 #
29 # License (GNU Public License):
30 #
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.
35 #
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
39 #    USA
40 #
41 # TODO: (see the TODO file bundled with the psad sources.)
42 #
43 # Default behavior is as follows.  Each of these features can be disabled
44 # with command line arguments:
45 #
46 #       - passive OS fingerprinting                   = yes
47 #       - snort rule matching                         = yes
48 #       - write fw errors to error log                = yes
49 #       - daemon mode                                 = 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
54 #
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
61 #   reading.
62 #
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
66 #               logs.
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.
76 #   endp      - Ending port.
77 #
78 # Sample iptables log messages:
79 #
80 #  Sample tcp packet (rejected by iptables... --log-prefix = "DROP ")
81 #
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
85 #
86 #  Sample icmp packet rejected by iptables INPUT chain:
87 #
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
91 #
92 #  Sample icmp packet logged through FORWARD chain:
93 #
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
97 #
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):
101 #
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
105 #
106 # Note on iptables tcp log messages:
107 #
108 #   iptables reports tcp flags in the following order:
109 #
110 #       URG ACK PSH RST SYN FIN
111 #
112 # Files specification for /var/log/psad/<srcip> directories:
113 #
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
116 #   created:
117 #
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
121 #                            is a local IP).
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>.
126 #
127 #   Note that some of the files above contain the destination address since a
128 #   single source address may scan several destination addresses.
129 #
130 ###############################################################################
131 #
132
133 ### modules used by psad
134 use File::Copy;
135 use File::Path;
136 use IO::Socket;
137 use Socket;
138 use POSIX;
139 use IO::Handle;
140 use Data::Dumper;
141 use Getopt::Long 'GetOptions';
142 use strict;
143
144 ### ========================== main =================================
145
146 ### set the current version
147 my $version = 'psad-2.3-pre1';
148
149 ### default config file for psad (can be changed with
150 ### --config switch)
151 my $config_file  = '/etc/psad/psad.conf';
152
153 ### this will be set to either FW_DATA_FILE, ULOG_DATA_FILE
154 ### or IPT_SYSLOG_FILE
155 my $fw_data_file = '';
156
157 ### disable debugging by default
158 my $debug     = 0;
159 my $debug_sid = 0;  ### debug a specific signature
160
161 my $flush_fw = 0;
162
163 ### build the iptables blocking configuration out of the
164 ### IPT_AUTO_CHAIN variable
165 my @ipt_config = ();
166
167 ### main configuration hash
168 my %config = ();
169 my $override_config_str = '';
170
171 ### local subnets
172 my @local_nets = ();
173
174 ### fw search string array
175 my @fw_search = ();
176
177 ### socket for --fw-block
178 my $ipt_sock = '';
179
180 ### commands hash
181 my %cmds = ();
182
183 ### main psad data structure; contains ips, port ranges,
184 ### protocol info, tcp flags, etc.
185 my %scan = ();
186
187 ### cache scan danger levels
188 my %scan_dl = ();
189
190 ### cache scan email counters
191 my %scan_email_ctrs = ();
192
193 ### cache executions of external script (only used if
194 ### ENABLE_EXT_SCRIPT_EXEC is set to 'Y');
195 my %scan_ext_exec = ();
196
197 ### cache p0f-based passive os fingerprinting information
198 my %p0f = ();
199
200 ### cache p0f-based passive os fingerprinting signature information
201 my %p0f_ipv4_sigs = ();
202 my %p0f_ipv6_sigs = ();
203
204 ### cache TOS-based passive os fingerprinting information
205 my %posf = ();
206
207 ### cache TOS-based passive os fingerprinting signature information
208 my %posf_sigs = ();
209
210 ### cache valid icmp types and corresponding codes
211 my %valid_icmp_types  = ();
212 my %valid_icmp6_types = ();
213
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 = ();
220
221 ### Cache snort classification.config file for class priorities
222 my %snort_class_dl = ();
223
224 ### Cache any individual Snort rule priority definitions from
225 ### the snort_rule_dl file
226 my %snort_rule_dl = ();
227
228 ### Cache Snort rule reference configuration
229 my %snort_ref_baseurl = ();
230
231 ### cache all scan signatures from /etc/psad/signatures file
232 my %sigs = ();
233 my %sig_search = ();
234 my %sig_ip_objs = ();
235
236 ### cache iptables prefixes
237 my %ipt_prefixes = ();
238
239 ### ignore ports
240 my %ignore_ports = ();
241
242 ### ignore protocols
243 my %ignore_protocols = ();
244
245 ### ignore interfaces
246 my %ignore_interfaces = ();
247
248 ### data array used for dshield.org logs
249 my @dshield_data = ();
250
251 ### track the last time we sent an alert to dshield.org
252 my $last_dshield_alert = '';
253
254 ### calculate how often a dshield alert will be sent
255 my $dshield_alert_interval = '';
256
257 ### dshield stats counters
258 my $dshield_email_ctr = 0;
259 my $dshield_lines_ctr = 0;
260
261 ### get the current timezone for dshield (this is calculated
262 ### and re-calculated since the timezone may change).
263 my $timezone = '';
264
265 ### get the current year for dshield
266 my $year = '';
267
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;
271
272 ### track the number of scan IP pairs for MAX_SCAN_IP_PAIRS thresholding
273 my $scan_ip_pairs = 0;
274
275 ### %auto_dl holds all ip addresses that should automatically
276 ### be assigned a danger level (or ignored).
277 my %auto_dl = ();
278 my %auto_dl_ip_objs = ();
279 my %auto_assigned_msg = ();
280
281 ### cache the source ips that we have automatically blocked
282 ### (if ENABLE_AUTO_IDS == 'Y')
283 my %auto_blocked_ips = ();
284
285 ### counter to check psad iptables chains and jump rules
286 my $iptables_prereq_check = 0;
287
288 ### cache the addresses we have issued dns lookups against.
289 my %dns_cache = ();
290
291 ### cache the addresses we have executed whois lookups against.
292 my %whois_cache = ();
293
294 ### cache ports the local machine is listening on (periodically
295 ### updated by get_listening_ports()).
296 my %local_ports = ();
297
298 ### cache the ip addresses associated with each interface on the
299 ### local machine.
300 my %local_ips = ();
301
302 ### Top attacking statistics
303 my %top_tcp_ports = ();
304 my %top_udp_ports = ();
305 my %top_udplite_ports = ();
306 my %top_sigs    = ();
307 my %sig_sources = ();
308 my %top_sig_counts = ();
309 my %top_packet_counts = ();
310 my %local_src = ();
311
312 ### regex to match IP addresses
313 my $ipv4_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;  ### IPv4
314
315 ### IPv6 - full version in ip6tables logs
316 my $ipv6_re = qr|(?:[a-f0-9]{4}:){7}(?:[a-f0-9]{4})|i;
317
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
321 ### average.
322 my $max_hops = 20;
323
324 ### per protocol packet counters
325 my @protocols = qw/tcp udp udplite icmp icmp6/;
326 my %proto_ctrs = ();
327
328 ### pid file hash
329 my %pidfiles = ();
330
331 ### initialize and scope some default variables (command
332 ### line args can override some default values)
333 my $sigs_file        = '';
334 my $posf_file        = '';
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;
344 my $test_mode        = 0;
345 my $syslog_server    = 0;
346 my $kill             = 0;
347 my $restart          = 0;
348 my $restrict_ip      = '';
349 my $restrict_ip_cmdline = '';
350 my $status_mode      = 0;
351 my $status_ip        = '';
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 = '';
370 my $gnuplot_3d   = 0;
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 = '';
383 my $store_file       = '';
384 my $gnuplot_interactive = 0;
385 my $plot_separator   = ', ';  ### default to CSV format for plot data
386 my $csv_mode         = 0;
387 my $csv_stdin        = 0;
388 my $csv_fields       = '';
389 my $csv_print_uniq   = 0;
390 my $csv_line_limit   = 0;
391 my $csv_start_line   = 0;
392 my $csv_end_line     = 0;
393 my $csv_regex        = '';
394 my $csv_neg_regex    = '';
395 my $csv_have_timestamp = 0;
396 my $dump_ipt_policy  = 0;
397 my $fw_include_ips   = 0;
398 my $benchmark        = 0;
399 my $num_packets      = 0;
400 my $usr1             = 0;
401 my $hup              = 0;
402 my $usr1_flag        = 0;
403 my $hup_flag         = 0;
404 my $verbose          = 0;
405 my $print_ver        = 0;
406 my $help             = 0;
407 my $dump_conf        = 0;
408 my $download_sigs    = 0;
409 my $chk_interval     = 0;
410 my $log_len          = 23;  ### used in scan_logr()
411 my $fw_analyze       = 0;
412 my $fw_file          = '';
413 my $lib_dir          = '';
414 my $rm_data_ctr      = 0;
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;
420 my $warn_msg         = '';
421 my $die_msg          = '';
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;
428
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;
435 my $no_auto_dl    = 0;
436 my $no_posf       = 0;
437 my $no_daemon     = 0;
438 my $no_ipt_errors = 0;
439 my $no_rdns       = 0;
440 my $no_whois      = 0;
441 my $no_netstat    = 0;
442 my $no_fwcheck    = 0;
443 my $no_kmsgsd     = 0;
444 my $no_email_alerts  = 0;
445 my $no_syslog_alerts = 0;
446
447 ### tcp option types
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;
453
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
460 );
461
462 my %ip_options = ();
463
464 ### for ICMP
465 my $ICMP_ECHO_REQUEST  = 8;
466 my $ICMP_ECHO_REPLY    = 0;
467 my $ICMP6_ECHO_REQUEST = 128;
468 my $ICMP6_ECHO_REPLY   = 129;
469
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(
474     pcre
475     fragbits
476     content-list
477     rpc
478     byte_test
479     byte_jump
480     distance
481     within
482     flowbits
483     rawbytes
484     regex
485     isdataat
486     uricontent
487     content
488     offset
489     replace
490     resp
491     flowbits
492     ip_proto
493 );  ### the ip_proto keyword could be supported, but would require
494     ### refactoring parse_NF_pkt_str().
495
496 ### for Snort signature sp/dp matching
497 my @port_types = (
498     {'sp' => 'norm', 'dp' => 'norm'},
499     {'sp' => 'norm', 'dp' => 'neg'},
500     {'sp' => 'neg',  'dp' => 'norm'},
501     {'sp' => 'neg',  'dp' => 'neg'},
502 );
503
504 ### main packet data structure
505 my %pkt_NF_init = (
506
507     ### data link layer
508     'src_mac' => '',
509     'dst_mac' => '',
510     'intf'    => '',   ### FIXME in and out interfaces?
511
512     ### network layer
513     'src'    => '',
514     's_obj'  => '',
515     'dst'    => '',
516     'd_obj'  => '',
517     'proto'  => '',
518     'ip_len' => -1,
519     'ip_opts'  => '',  ### v4 or v6
520
521     ### IPv4
522     'ip_id'  => -1,
523     'ttl'    => -1,
524     'tos'    => '',
525     'frag_bit' => 0,
526
527     ### IPv6
528     'is_ipv6' => 0,
529     'tc'      => -1,
530     'hop_limit'  => -1,
531     'flow_label' => -1,
532
533     ### ICMP
534     'itype'    => -1,
535     'icode'    => -1,
536     'icmp_seq' => -1,
537     'icmp_id'  => -1,
538
539     ### transport layer
540     'sp'  => -1,
541     'dp'  => -1,
542     'win' => -1,
543     'flags' => -1,
544     'tcp_seq'  => -1,
545     'tcp_ack'  => -1,
546     'tcp_opts' => '',
547     'udp_len'  => -1,
548
549     ### extra fields for psad internals (DShield reporting, fwsnort
550     ### sid matching, iptables logging prefixes and chains, etc.)
551     'fwsnort_sid'   => 0,
552     'fwsnort_rnum'  => 0,
553     'fwsnort_estab' => 0,
554     'chain'         => '',
555     'log_prefix'    => '',
556     'dshield_str'   => '',
557     'syslog_host'   => '',
558     'timestamp'     => ''
559 );
560
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
566
567     ### data link layer
568     'src_mac' => 'hashentry',
569     'dst_mac' => 'hashentry',
570     'intf'    => 'intf2int',
571
572     ### network layer
573     'src'      => 'ip2int',
574     'dst'      => 'ip2int',
575     'proto'    => 'proto2int',
576     'tos'      => 'hashentry',
577     'ip_opts'  => 'hashentry',
578     'frag_bit' => 'hashentry',
579
580     ### transport layer
581     'flags'    => 'hashentry',
582     'tcp_opts' => 'hashentry',
583
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',
590 );
591 my %gnuplot_non_digit_map = ();
592 my %ip2int_cache   = ();
593 my %gnuplot_ip2int = ();
594
595 ### packet parsing return values
596 my $PKT_ERROR   = 0;
597 my $PKT_SUCCESS = 1;
598 my $PKT_IGNORE  = 2;
599
600 ### icmp header validation
601 my $BAD_ICMP_TYPE = 1;
602 my $BAD_ICMP_CODE = 2;
603
604 my $SIG_MATCH    = 1;
605 my $NO_SIG_MATCH = 0;
606
607 ### header lengths
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
613
614 ### save a copy of the command line arguments
615 my @args_cp = @ARGV;
616
617 ### handle command line args
618 &getopt_wrapper();
619
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).
623 &is_root();
624
625 ### Import all psad configuration and signatures files
626 ### (psad.conf, posf, signatures, psad_icmp_types,
627 ### and auto_dl), and call setup().
628 &psad_init();
629
630 ### check to make sure another psad process is not already running.
631 &unique_pid($config{'PSAD_PID_FILE'});
632
633 ### get the ip addresses that are local to this machine
634 &get_local_ips();
635
636 ### get the current services running on this machine
637 &get_listening_ports() unless ($no_netstat);
638
639 ### daemonize psad unless running with --no-daemon or an
640 ### analysis mode
641 unless ($no_daemon or $debug) {
642     my $pid = fork();
643     exit 0 if $pid;
644     die "[*] $0: Couldn't fork: $!" unless defined $pid;
645     POSIX::setsid() or die "[*] $0: Can't start a new session: $!";
646 }
647
648 ### write the current pid associated with psad to the psad pid file
649 &write_pid($config{'PSAD_PID_FILE'});
650
651 ### write the command line args used to start psad to $cmdline_file
652 &write_cmd_line(\@args_cp, $cmdline_file)
653     unless $debug;
654
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.
663 my $cmd;
664 unless ($config{'ENABLE_SYSLOG_FILE'} eq 'Y'
665         or $no_kmsgsd
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'}";
673     close KMSGSD;
674     $kmsgsd_started = 1;
675 }
676
677 unless ($kmsgsd_started) {
678     my $pid = &is_running($pidfiles{'kmsgsd'});
679
680     if ($pid) {
681         kill 9, $pid unless kill 15, $pid;
682     }
683     unlink $pidfiles{'kmsgsd'} if -e $pidfiles{'kmsgsd'};
684 }
685
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'}";
693     close PSADWATCHD;
694 }
695
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';
700
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
706     ### in -A mode).
707     &renew_auto_blocked_ips();
708 }
709
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;
717
718 if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y') {
719     $last_dshield_alert = time() unless $last_dshield_alert;
720 }
721
722 ### Initialize current time for disk space checking.
723 my $last_disk_check = time();
724
725 if ($config{'IMPORT_OLD_SCANS'} eq 'Y') {
726     ### import old scans and counters from /var/log/psad/
727     &import_old_scans();
728 } elsif ($config{'ENABLE_SCAN_ARCHIVE'} eq 'Y') {
729     &archive_data();
730 } else {
731     &remove_old_scans();
732 }
733
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();
738
739 ### zero out prefix counters
740 &write_prefix_counters();
741
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();
746
747 my $fw_data_file_size  = -s $fw_data_file;
748 my $fw_data_file_inode = (stat($fw_data_file))[1];
749
750 my $fw_data_file_check_ctr = 0;
751
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 ',
757     "$fw_data_file: $!";
758
759 &get_auto_response_domain_sock()
760     if $config{'ENABLE_AUTO_IDS'} eq 'Y';
761
762 ###=========================================================###
763 ######                    MAIN LOOP                      ######
764 ###=========================================================###
765 MAIN: for (;;) {
766
767     ### scope and clear the firewall data array
768     my @fw_packets = ();
769
770     ### for --fw-block <ip>
771     my @add_ipt_addrs = ();
772
773     if ($hup_flag) {
774
775         &sys_log('received HUP signal, ' .
776             're-importing psad.conf');
777
778         print STDERR "[+] Received HUP signal, re-importing config...\n"
779             if $debug;
780
781         my $orig_fwdata = $fw_data_file;
782         my $orig_ipt_sockfile = '';
783
784         $orig_ipt_sockfile = $config{'AUTO_IPT_SOCK'}
785             if $config{'ENABLE_AUTO_IDS'} eq 'Y';
786
787         ### Re-import all used config files (psad.conf, auto_dl,
788         ### posf, signatures) if we received a HUP signal.
789         &psad_init();
790
791         if ($orig_fwdata ne $fw_data_file) {
792             close FWDATA;
793
794             ### re-open the fwdata file
795             open FWDATA, $fw_data_file or die
796                 "[*] Could not open $fw_data_file: $!";
797
798             $skip_first_loop = 1;
799         }
800
801         if ($config{'ENABLE_AUTO_IDS'} eq 'Y') {
802             if ($orig_ipt_sockfile ne $config{'AUTO_IPT_SOCK'}) {
803                 close $ipt_sock;
804
805                 &get_auto_response_domain_sock();
806
807                 $skip_first_loop = 1;
808             }
809         }
810         $hup_flag = 0;  ### clear the HUP flag
811     }
812
813     ### See if we need to print out the %scan datastructure
814     ### (we received a USR1 signal)
815     if ($usr1_flag) {
816         $usr1_flag = 0;  ### clear the USR1 flag
817
818         &sys_log('received USR1 signal, printing scan ' .
819             "hashes to $config{'PSAD_DIR'}/scan_hash.$$");
820
821         ### dump scan hash to filesystem
822         &print_scan();
823     }
824
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) {
828
829         $skip_first_loop = 0;
830         seek FWDATA,0,2;  ### seek to the end of the file
831         next MAIN;
832
833     } else {
834
835         ### Get any new packets have been written to
836         ### FW_DATA_FILE by kmsgsd for psad analysis.
837         @fw_packets = <FWDATA>;
838
839         if ($config{'ENABLE_AUTO_IDS'} eq 'Y') {
840
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>;
845             }
846         }
847     }
848
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) {
855
856                 close FWDATA;
857
858                 &sys_log('[+]', "iptables syslog file $fw_data_file " .
859                     "shrank or was rotated, so re-opening");
860
861                 ### re-open the fwdata file
862                 open FWDATA, $fw_data_file or die
863                     "[*] Could not open $fw_data_file: $!";
864
865                 $skip_first_loop = 1;
866
867                 ### set file size and inode
868                 $fw_data_file_size  = $size_tmp;
869                 $fw_data_file_inode = $inode_tmp;
870             }
871         }
872         $fw_data_file_check_ctr = 0;
873     }
874
875     if (@fw_packets) {
876
877         if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y') {
878             ### calculate the timezone offset
879             $timezone = sprintf("%.2d", (Timezone())[3]) . ':00';
880             $year     = This_Year();
881         }
882
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;
889             }
890             $netstat_lkup_ctr++;
891         }
892         ### the local machine ip addresses could change (dhcp, etc.)
893         ### but not that often.
894         if ($local_ips_lkup_ctr == 30) {
895             &get_local_ips();
896             $local_ips_lkup_ctr = 0;
897         }
898         $local_ips_lkup_ctr++;
899
900         ### Extract data and summarize scan packets, assign danger level,
901         ### send email/syslog alerts.
902         &check_scan(\@fw_packets);
903
904     }
905
906     ### log top scans data
907     my $do_log = 0;
908     if ($config{'TOP_SCANS_CTR_THRESHOLD'} == 0) {
909         $do_log = 1;
910     } elsif ($check_interval_ctr % $config{'TOP_SCANS_CTR_THRESHOLD'} == 0) {
911         $do_log = 1;
912     }
913
914     if ($do_log) {
915
916         ### log the top port and signature matches
917         &log_top_scans();
918     }
919
920     ### Write the number of tcp/udp/icmp packets out
921     ### to the global packet counters file
922     &write_global_packet_counters();
923
924     ### Write out log prefix counters
925     &write_prefix_counters();
926
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();
933         }
934
935         ### see if we need to add any IP address from the domain
936         ### socket
937         &check_ipt_cmd(\@add_ipt_addrs) if @add_ipt_addrs;
938     }
939
940     ### Send logs to dshield in dshield format
941     if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y') {
942         &dshield_email_log();
943     }
944
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
950         ### if necessary.
951         if (&disk_space_exceeded() and $config{'ENABLE_SYSLOG_FILE'} ne 'Y') {
952             close FWDATA;
953
954             ### truncate fwdata file
955             &truncate_file($fw_data_file);
956
957             ### re-open the fwdata file
958             open FWDATA, $fw_data_file or die
959                 "[*] Could not open $fw_data_file: $!";
960         }
961         $last_disk_check = time();
962     }
963
964     &check_auto_response_sock()
965         if $config{'ENABLE_AUTO_IDS'} eq 'Y';
966
967     ### Print the number of new packets we saw in FW_DATA_FILE if we are
968     ### running in debug mode
969     if ($debug) {
970         print STDERR "[+] MAIN: number of new packets: " .
971             ($#fw_packets+1) . "\n";
972     }
973
974     if ($die_msg) {
975         &print_sys_msg($die_msg, "$config{'PSAD_ERR_DIR'}/psad.die");
976         $die_msg = '';
977     }
978
979     if ($warn_msg) {
980         &print_sys_msg($warn_msg, "$config{'PSAD_ERR_DIR'}/psad.warn");
981         $warn_msg = '';
982     }
983
984     ### see if we need to timeout any old scans
985     if ($config{'ENABLE_PERSISTENCE'} eq 'N') {
986
987         my $do_timeout = 0;
988         if ($config{'PERSISTENCE_CTR_THRESHOLD'} == 0) {
989             $do_timeout = 1;
990         } elsif ($check_interval_ctr % $config{'PERSISTENCE_CTR_THRESHOLD'} == 0) {
991             $do_timeout = 1;
992         }
993
994         &delete_old_scans() if $do_timeout;
995     }
996
997     $check_interval_ctr++;
998
999     $fw_data_file_check_ctr++;
1000
1001     ### clearerr() on the FWDATA filehandle to be ready for new packets
1002     FWDATA->clearerr();
1003
1004     ### sleep for the check interval seconds
1005     sleep $config{'CHECK_INTERVAL'};
1006 }
1007
1008 ### for completeness
1009 close FWDATA;
1010 exit 0;
1011 ###=========================================================###
1012 ######                    END MAIN                       ######
1013 ###=========================================================###
1014
1015 #=================== BEGIN SUBROUTINES ========================
1016
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).
1020 sub check_scan() {
1021     my $fw_packets_ar = shift;
1022
1023     my %curr_scan = ();
1024     my %curr_sigs_dl = ();
1025     my %curr_sids_dl = ();
1026     my @err_pkts     = ();
1027     my %auto_block_regex_match = ();
1028
1029     my $pkt_ctr = 0;
1030     my $log_scan_ip_pair_max = 0;
1031
1032     my $print_scale_factor = &get_scale_factor($#$fw_packets_ar);
1033
1034     ### loop through all of the packet log messages we have just acquired
1035     ### from iptables
1036
1037     PKT: for my $pkt_str (@$fw_packets_ar) {
1038
1039         ### main packet data structure
1040         my %pkt = %pkt_NF_init;
1041
1042         if ($analyze_mode) {
1043             $pkt_ctr++;
1044             if ($pkt_ctr % $print_scale_factor == 0) {
1045                 print "[+] Processed $pkt_ctr packets...\n";
1046             }
1047         }
1048
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;
1054             next PKT;
1055         } elsif ($pkt_parse_rv == $PKT_IGNORE) {
1056             next PKT;
1057         }
1058
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;
1064         }
1065
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'}}++;
1073         }
1074
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;
1079
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"
1087                         if $debug;
1088                     $log_scan_ip_pair_max = 1;
1089                     next PKT;
1090                 }
1091             }
1092             if (not defined $scan{$pkt{'src'}}) {
1093                 $scan_ip_pairs++;
1094             } elsif (not defined $scan{$pkt{'src'}}{$pkt{'dst'}}) {
1095                 $scan_ip_pairs++;
1096             }
1097         }
1098
1099         ### track packet counts for this source
1100         $top_packet_counts{$pkt{'src'}}++;
1101
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'});
1110             }
1111         }
1112
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';
1121         }
1122
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);
1128
1129             print STDERR "[+] assign_auto_danger_level() returned: $rv\n"
1130                 if $debug;
1131             if ($rv == 0) {
1132                 print STDERR "[+] ignoring $pkt{'src'} $pkt{'proto'} ",
1133                     "$pkt{'dp'} scan.\n" if $debug;
1134                 next PKT;
1135             }
1136         }
1137
1138         if ($pkt{'proto'} eq 'icmp') {
1139
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'});
1146
1147             my $update_dl = 0;
1148             if ($type_code_rv == $BAD_ICMP_TYPE) {
1149
1150                 $scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp'}
1151                     {'invalid_type'}{$pkt{'itype'}}
1152                     {$pkt{'chain'}}{'pkts'}++;
1153
1154                 $update_dl = 1;
1155
1156             } elsif ($type_code_rv == $BAD_ICMP_CODE) {
1157
1158                 $scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp'}
1159                     {'invalid_code'}{$pkt{'itype'}}{$pkt{'icode'}}
1160                     {$pkt{'chain'}}{'pkts'}++;
1161
1162                 $update_dl = 1;
1163             }
1164
1165             if ($update_dl) {
1166                 if (defined $scan_dl{$pkt{'src'}}) {
1167                     if ($scan_dl{$pkt{'src'}} < 2) {
1168                         $scan_dl{$pkt{'src'}} = 2;
1169                     }
1170                 } else {
1171                     $scan_dl{$pkt{'src'}} = 2;
1172                 }
1173             }
1174
1175         } elsif ($pkt{'proto'} eq 'icmp6') {
1176
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'});
1183
1184             my $update_dl = 0;
1185             if ($type_code_rv == $BAD_ICMP_TYPE) {
1186
1187                 $scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp6'}
1188                     {'invalid_type'}{$pkt{'itype'}}
1189                     {$pkt{'chain'}}{'pkts'}++;
1190
1191                 $update_dl = 1;
1192
1193             } elsif ($type_code_rv == $BAD_ICMP_CODE) {
1194
1195                 $scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp6'}
1196                     {'invalid_code'}{$pkt{'itype'}}{$pkt{'icode'}}
1197                     {$pkt{'chain'}}{'pkts'}++;
1198
1199                 $update_dl = 1;
1200             }
1201
1202             if ($update_dl) {
1203                 if (defined $scan_dl{$pkt{'src'}}) {
1204                     if ($scan_dl{$pkt{'src'}} < 2) {
1205                         $scan_dl{$pkt{'src'}} = 2;
1206                     }
1207                 } else {
1208                     $scan_dl{$pkt{'src'}} = 2;
1209                 }
1210             }
1211         }
1212
1213         unless ($no_snort_sids) {
1214             if ($pkt{'fwsnort_sid'}) {
1215
1216                 ### found a fwsnort sid in the packet log message
1217                 my ($dl, $is_sig_match) = &add_fwsnort_sid(\%pkt);
1218
1219                 if ($dl) {
1220                     $curr_sids_dl{$pkt{'src'}} = $dl;
1221                 } else {
1222                     ### a signature matched but is supposed
1223                     ### to be ignored
1224                     next PKT if $is_sig_match == $SIG_MATCH;
1225                 }
1226
1227             } else {
1228                 ### attempt to match any tcp/udp/icmp signatures in the
1229                 ### main signatures hash
1230                 unless ($no_signatures) {
1231
1232                     my ($dl, $is_sig_match) = &match_sigs(\%pkt);
1233
1234                     print STDERR "    match_sigs() returned DL: $dl\n"
1235                         if $debug and $verbose;
1236
1237                     if ($dl) {
1238                         $curr_sigs_dl{$pkt{'src'}} = $dl;
1239                     } else {
1240                         ### a signature matched but is supposed
1241                         ### to be ignored
1242                         next PKT if $is_sig_match == $SIG_MATCH;
1243                     }
1244                 }
1245             }
1246         }
1247
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'
1252                 and not $benchmark
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);
1259                 my $time_24 = $3;
1260                 push @dshield_data, "$year-$month-$day $time_24 " .
1261                     "$timezone\t$config{'DSHIELD_USER_ID'}\t1" .
1262                     "\t$pkt{'dshield_str'}\n";
1263             }
1264         }
1265
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'}
1271                         = &date_time($1);
1272                 } elsif ($pkt_str =~ /^\s*(\S+\s+\S+\s+\S+)/) {
1273                     $scan{$pkt{'src'}}{$pkt{'dst'}}{'s_time'}
1274                         = &date_time($1);
1275                 } else {
1276                     die "[*] Could not extract time from packet: $pkt_str\n",
1277                         "    Please send a bug report to: ",
1278                         "mbr\@cipherdyne.org\n";
1279                 }
1280             } else {
1281                 $scan{$pkt{'src'}}{$pkt{'dst'}}{'s_time'} = time();
1282             }
1283         }
1284
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'};
1292
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'};
1296
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'};
1300
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'}} = '';
1311                 }
1312             }
1313         } else {
1314             $pkt{'log_prefix'} = '*noprfx*';
1315         }
1316
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'}}++;
1320
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;
1329             }
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;
1337             }
1338
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'});
1351         }
1352
1353         print STDERR Dumper $scan{$pkt{'src'}}{$pkt{'dst'}} if $debug and $verbose;
1354
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
1362         unless ($no_posf) {
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
1368
1369                     ### p0f based fingerprinting
1370                     &p0f(\%pkt);
1371
1372                 } elsif (not defined $posf{$pkt{'src'}}{'guess'}
1373                         and $scan{$pkt{'src'}}{$pkt{'dst'}}{'absnum'} < 100) {
1374                     &posf(\%pkt);
1375                 }
1376             }
1377         }
1378
1379         %pkt = ();
1380     }
1381
1382     ### write bogus packets to the error log.
1383     if ($benchmark) {
1384         print scalar localtime(), ' [+] Err packets: ' .
1385             ($#err_pkts+1) . ".\n";
1386     } else {
1387         &collect_errors(\@err_pkts) unless $no_ipt_errors;
1388     }
1389
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);
1393
1394     my $tot_scan_ips = 0;
1395     if ($analyze_mode) {
1396         for (my $dl=1; $dl <= 5; $dl++) {
1397             my $num_ips = 0;
1398             for my $src (keys %curr_scan) {
1399                 $num_ips++ if $scan_dl{$src} == $dl;
1400             }
1401             $tot_scan_ips += $num_ips;
1402             print "    Level $dl: $num_ips IP addresses\n";
1403         }
1404         print "\n    Tracking $tot_scan_ips total IP addresses\n";
1405     }
1406
1407     ### display the scan analysis
1408     &print_scan_status() if $analyze_mode;
1409
1410     ### log scan data to the filesystem
1411     &scan_logr(\%curr_scan);
1412
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;
1417
1418     if ($log_scan_ip_pair_max) {
1419         &sys_log("scan IP pairs threshold reached");
1420     }
1421
1422     return;
1423 }
1424
1425 sub parse_NF_pkt_str() {
1426     my ($pkt_hr, $pkt_str) = @_;
1427
1428     my $is_ipv6    = 0;
1429     my $is_tcp     = 0;
1430     my $is_udp     = 0;
1431     my $is_udplite = 0;
1432     my $is_icmp    = 0;
1433     my $is_icmp6   = 0;
1434
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=/;
1440     }
1441
1442     print STDERR "\n", $pkt_str if $debug;
1443
1444     $pkt_hr->{'raw'} = $pkt_str if $csv_mode or $gnuplot_mode;
1445
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'}|;
1460             }
1461             $ipt_prefixes{$pkt_hr->{'log_prefix'}}++;
1462         }
1463     }
1464
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';
1485     }
1486
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;
1490     }
1491
1492     if ($pkt_str =~ /\sMAC=(\S+)/) {
1493         my $mac_str = $1;
1494         if ($mac_str =~ /^((?:\w{2}\:){6})((?:\w{2}\:){6})/) {
1495             $pkt_hr->{'dst_mac'} = $1;
1496             $pkt_hr->{'src_mac'} = $2;
1497         }
1498     }
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;
1502     }
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;
1506     }
1507
1508     unless ($pkt_hr->{'intf'} and $pkt_hr->{'chain'}) {
1509         print STDERR "[-] err packet: could not determine ",
1510             "interface and chain.\n" if $debug;
1511         return $PKT_ERROR;
1512     }
1513
1514     if (%ignore_interfaces) {
1515         for my $ignore_intf (keys %ignore_interfaces) {
1516             return $PKT_IGNORE if $pkt_hr->{'intf'} eq $ignore_intf;
1517         }
1518     }
1519
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;
1524     } else {
1525         $pkt_hr->{'timestamp'}   = localtime();
1526         $pkt_hr->{'syslog_host'} = 'unknown';
1527     }
1528
1529     ### try to extract a snort sid (generated by fwsnort) from
1530     ### the packet
1531     unless ($no_snort_sids) {
1532         if ($pkt_hr->{'log_prefix'}) {
1533
1534             if ($pkt_hr->{'log_prefix'} =~ /$config{'SNORT_SID_STR'}(\d+)/) {
1535                 $pkt_hr->{'fwsnort_sid'} = $1;
1536
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;
1541                 }
1542
1543                 if ($pkt_hr->{'log_prefix'} =~ /ESTAB/) {
1544                     $pkt_hr->{'fwsnort_estab'} = 1;
1545                 }
1546             }
1547         }
1548     }
1549
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
1553         my $matched = 0;
1554         for my $fw_search_str (@fw_search) {
1555             $matched = 1 if $pkt_str =~ /$fw_search_str/;
1556         }
1557         return $PKT_IGNORE unless $matched;
1558     }
1559
1560     ### test for IPv6
1561     if ($pkt_str =~ /HOPLIMIT=\d+\s+FLOWLBL=\d+/) {
1562         return $PKT_IGNORE unless $config{'ENABLE_IPV6_DETECTION'} eq 'Y';
1563         $is_ipv6 = 1;
1564         $pkt_hr->{'is_ipv6'} = 1;
1565     }
1566
1567     ### test for IPv4 "don't fragment" bit
1568     unless ($is_ipv6) {
1569         $pkt_hr->{'frag_bit'} = 1 if $pkt_str =~ /\sDF\s+PROTO/;
1570     }
1571
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;
1576     }
1577
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
1581         ### so:
1582
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 ]
1591
1592         $is_icmp6 = 1;
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
1596         ### so:
1597
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 ]
1604
1605         $is_icmp = 1;
1606     } elsif ($pkt_str =~ /PROTO=TCP\s/) {
1607         $is_tcp = 1;
1608     } elsif ($pkt_str =~ /PROTO=UDPLITE\s/) {
1609         $is_udplite = 1;
1610     } elsif ($pkt_str =~ /PROTO=UDP\s/) {
1611         $is_udp = 1;
1612     } else {
1613         print STDERR "[-] err packet: unrecognized protocol\n" if $debug;
1614         return $PKT_ERROR;
1615     }
1616
1617     if ($is_tcp) {
1618         if ($is_ipv6) {
1619
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+
1629                     (.*)\s+URGP=/x) {
1630
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);
1636
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;
1641
1642             } else {
1643                 print STDERR "[-] err packet: strange IPv6 TCP format\n"
1644                     if $debug;
1645                 return $PKT_ERROR;
1646             }
1647
1648         } else {
1649
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
1654
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+
1658                     (.*)\s+URGP=/x) {
1659
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'},
1663                     $pkt_hr->{'flags'})
1664                         = ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);
1665
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;
1670
1671             } else {
1672                 print STDERR "[-] err packet: strange IPv4 TCP format\n"
1673                     if $debug;
1674                 return $PKT_ERROR;
1675             }
1676         }
1677
1678         $pkt_hr->{'proto'} = 'tcp';
1679
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*//;
1683
1684         ### default to NULL
1685         $pkt_hr->{'flags'} = 'NULL' unless $pkt_hr->{'flags'};
1686
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/)) {
1690
1691             ### $dp > 1024 && ($pkt_hr->{'flags'} =~ /ACK/ ||
1692
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.
1698
1699             print STDERR "[-] err packet: matched ACK or RST flag.\n"
1700                 if $debug;
1701             return $PKT_IGNORE;
1702         }
1703
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') {
1714
1715             print STDERR "[-] err packet: bad tcp flags.\n" if $debug;
1716             return $PKT_ERROR;
1717         }
1718
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;
1723         }
1724
1725         if ($pkt_str =~ /\sSEQ=(\d+)\s+ACK=(\d+)/) {
1726             $pkt_hr->{'tcp_seq'} = $1;
1727             $pkt_hr->{'tcp_ack'} = $2;
1728         }
1729
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'});
1733
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'});
1738
1739         if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y'
1740                 and not $benchmark
1741                 and not $analyze_mode) {
1742
1743             my $dflags = $pkt_hr->{'flags'};
1744             $dflags =~ s/\s/,/g;
1745
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" .
1748                 "$dflags";
1749         }
1750
1751     } elsif ($is_udp or $is_udplite) {
1752
1753         if ($is_udplite) {
1754             $pkt_hr->{'proto'} = 'udplite';
1755         } else {
1756             $pkt_hr->{'proto'} = 'udp';
1757         }
1758
1759         if ($is_ipv6) {
1760
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) {
1769
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);
1774
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;
1779
1780             } else {
1781                 print STDERR "[-] err packet: strange IPv6 UDP format\n"
1782                     if $debug;
1783                 return $PKT_ERROR;
1784             }
1785
1786         } else {
1787
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
1792
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) {
1796
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);
1801
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;
1806
1807             } else {
1808                 print STDERR "[-] err packet: strange IPv4 UDP format\n"
1809                     if $debug;
1810                 return $PKT_ERROR;
1811             }
1812         }
1813
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'});
1817
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'});
1822
1823         if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y'
1824                 and not $benchmark
1825                 and not $analyze_mode) {
1826
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'}";
1829         }
1830
1831     } elsif ($is_icmp6 or $is_icmp) {
1832
1833         $pkt_hr->{'sp'} = $pkt_hr->{'dp'} = 0;
1834
1835         if ($is_ipv6) {
1836
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 
1842
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+
1845                     CODE=(\d+)/x) {
1846
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);
1851
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;
1856
1857             } else {
1858                 print STDERR "[-] err packet: strange IPv6 ICMP format\n"
1859                     if $debug;
1860                 return $PKT_ERROR;
1861             }
1862
1863             $pkt_hr->{'proto'} = 'icmp6';
1864
1865         } else {
1866
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
1870
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+
1873                       CODE=(\d+)/x) {
1874
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);
1878
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;
1883
1884             } else {
1885                 print STDERR "[-] err packet: strange IPv4 ICMP format\n"
1886                     if $debug;
1887                 return $PKT_ERROR;
1888             }
1889
1890             $pkt_hr->{'proto'} = 'icmp';
1891
1892             if ($pkt_hr->{'itype'} == $ICMP_ECHO_REQUEST
1893                     or $pkt_hr->{'itype'} == $ICMP_ECHO_REPLY) {
1894
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;
1899                 } else {
1900                     return $PKT_ERROR;
1901                 }
1902             }
1903         }
1904
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'});
1908
1909         if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y'
1910                 and not $benchmark
1911                 and not $analyze_mode) {
1912
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'}";
1915         }
1916
1917     } else {
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;
1921         return $PKT_ERROR;
1922     }
1923
1924     if ($restrict_ip) {
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);
1931             }
1932         } else {
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);
1937             }
1938         }
1939     }
1940
1941     return $PKT_SUCCESS;
1942 }
1943
1944 sub check_ignore_proto() {
1945     my $pkt_proto = shift;
1946
1947     return 0 unless %ignore_protocols;
1948
1949     return 1 if defined $ignore_protocols{$pkt_proto};
1950     return 0;
1951 }
1952
1953 sub match_sigs() {
1954     my $pkt_hr = shift;
1955
1956     my $dl = 0;
1957     my $is_sig_match = $NO_SIG_MATCH;
1958
1959     print STDERR "[+] match_sigs()\n" if $debug and $verbose;
1960
1961     ### always run the IP protocol sigs
1962     PROTO: for my $proto ($pkt_hr->{'proto'}, 'ip') {
1963
1964         next PROTO unless defined $sig_search{$proto};
1965
1966         SRC: for my $src (keys %{$sig_search{$proto}}) {
1967
1968             print STDERR "[+] match_sigs() pkt src: $pkt_hr->{'src'} within sig src: $src ?..."
1969                 if $debug and $verbose;
1970
1971             if ($pkt_hr->{'s_obj'}->within($sig_ip_objs{$src})) {
1972                 print STDERR "yes\n"
1973                     if $debug and $verbose;
1974             } else {
1975                 print STDERR "no\n"
1976                     if $debug and $verbose;
1977                 next SRC;
1978             }
1979
1980             DST: for my $dst (keys %{$sig_search{$proto}{$src}}) {
1981
1982                 print STDERR "[+] match_sigs() pkt dst: $pkt_hr->{'dst'} within sig dst: $dst ?..."
1983                     if $debug and $verbose;
1984
1985                 if ($pkt_hr->{'d_obj'}->within($sig_ip_objs{$dst})) {
1986                     print STDERR "yes\n"
1987                         if $debug and $verbose;
1988                 } else {
1989                     print STDERR "no\n"
1990                         if $debug and $verbose;
1991                     next DST;
1992                 }
1993
1994                 print STDERR "    Matched sig IP criteria.\n"
1995                     if $debug and $verbose;
1996
1997                 if ($proto eq 'tcp' or $proto eq 'udp' or $proto eq 'udplite') {
1998
1999                     TYPE: for my $hr (@port_types) {
2000                         my $sp_type = $hr->{'sp'};
2001                         my $dp_type = $hr->{'dp'};
2002
2003                         next TYPE unless
2004                             defined $sig_search{$proto}{$src}{$dst}{$sp_type};
2005
2006                         my $sp_hr = $sig_search{$proto}{$src}{$dst}{$sp_type};
2007
2008                         SP_S: for my $sp_s (keys %{$sp_hr}) {
2009
2010                             if ($sp_type eq 'norm') {
2011                                 ### normal match on the starting port value
2012                                 next SP_S unless $pkt_hr->{'sp'} >= $sp_s;
2013                             }
2014
2015                             SP_E: for my $sp_e (keys %{$sp_hr->{$sp_s}}) {
2016
2017                                 if ($sp_type eq 'norm') {
2018                                     ### normal match on the ending port value
2019                                     next SP_E unless $pkt_hr->{'sp'} <= $sp_e;
2020                                 } else {
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);
2025                                 }
2026
2027                                 next TYPE unless defined
2028                                     $sp_hr->{$sp_s}->{$sp_e}->{$dp_type};
2029
2030                                 my $dp_hr = $sp_hr->{$sp_s}->{$sp_e}->{$dp_type};
2031
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;
2035                                     }
2036
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;
2040                                         } else {
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);
2045                                         }
2046
2047                                         ### now we have the set of applicable
2048                                         ### signatures that match the sip/dip
2049                                         ### and sp/dp, so match any Snort
2050                                         ### keywords
2051                                         my ($dl_tmp, $sig_match_tmp) =
2052                                                 &match_snort_keywords($pkt_hr,
2053                                                     $dp_hr->{$dp_s}->{$dp_e});
2054
2055                                         print STDERR "    match_snort_keywords() ",
2056                                             " return DL: $dl_tmp\n" if $debug;
2057
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;
2063                                     }
2064                                 }
2065                             }
2066                         }
2067                     }
2068                 } else {
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});
2073
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;
2079                 }
2080             }
2081         }
2082     }
2083     return $dl, $is_sig_match;
2084 }
2085
2086 sub match_snort_keywords() {
2087     my ($pkt_hr, $sigs_ids_hr) = @_;
2088
2089     print STDERR "[+] match_snort_keywords()\n" if $debug;
2090
2091     my $dl = 0;
2092     my $matched_sig = $NO_SIG_MATCH;
2093
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) {
2097
2098         next SIG unless defined $sigs{$sid};  ### should never happen
2099
2100         my $sig_hr = $sigs{$sid};
2101
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)
2105         my $dl_tmp = 0;
2106
2107         my ($rv, $sig_match_rv) = &match_snort_ip_keywords($pkt_hr, $sig_hr);
2108
2109         if ($sig_match_rv == $SIG_MATCH) {
2110             $matched_sig = $SIG_MATCH;
2111             $dl_tmp = $rv;
2112             if ($rv == 0) {
2113                 next SIG;  ### ignore signature
2114             }
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
2119         }
2120
2121         $dl = $dl_tmp if $dl_tmp > $dl;
2122
2123         if ($debug and $debug_sid == $sid) {
2124             print STDERR "[+] SID: $sid, passed match_snort_ip_keywords() ",
2125                 "tests.\n";
2126         }
2127
2128         if ($sig_hr->{'proto'} eq 'tcp') {
2129
2130             ($rv, $sig_match_rv) = &match_snort_tcp_keywords($pkt_hr, $sig_hr);
2131
2132             if ($sig_match_rv == $SIG_MATCH) {
2133
2134                 $matched_sig = $SIG_MATCH;
2135                 next SIG if $rv == 0;
2136
2137                 $dl = $rv if $rv > $dl;
2138
2139                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
2140                     {'sid'}{$sid}{$pkt_hr->{'chain'}}{'dp'}
2141                         = $pkt_hr->{'dp'};
2142
2143                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
2144                     {'sid'}{$sid}{$pkt_hr->{'chain'}}{'flags'}
2145                         = $pkt_hr->{'flags'};
2146
2147                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
2148                     {'sid'}{$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
2149
2150                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
2151                     {'sid'}{$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
2152
2153                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
2154                     {'sid'}{$sid}{$pkt_hr->{'chain'}}{'time'} = time();
2155
2156                 $sig_sources{$sid}{$pkt_hr->{'src'}} = '';
2157                 $top_sigs{$sid}++;
2158                 $top_sig_counts{$pkt_hr->{'src'}}++;
2159             }
2160
2161         } elsif ($sig_hr->{'proto'} eq 'udp') {
2162
2163             ($rv, $sig_match_rv) = &match_snort_udp_keywords($pkt_hr, $sig_hr);
2164
2165             if ($sig_match_rv == $SIG_MATCH) {
2166
2167                 $matched_sig = $SIG_MATCH;
2168                 next SIG if $rv == 0;
2169
2170                 $dl = $rv if $rv > $dl;
2171
2172                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
2173                     {'sid'}{$sid}{$pkt_hr->{'chain'}}{'dp'}
2174                          = $pkt_hr->{'dp'};
2175
2176                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
2177                     {'sid'}{$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
2178
2179                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
2180                     {'sid'}{$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
2181
2182                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
2183                     {'sid'}{$sid}{$pkt_hr->{'chain'}}{'time'} = time();
2184
2185                 $sig_sources{$sid}{$pkt_hr->{'src'}} = 0; ### not fwsnort
2186                 $top_sigs{$sid}++;
2187                 $top_sig_counts{$pkt_hr->{'src'}}++;
2188             }
2189
2190         } elsif ($sig_hr->{'proto'} eq 'icmp') {
2191
2192             ($rv, $sig_match_rv) = &match_snort_icmp_keywords($pkt_hr, $sig_hr);
2193
2194             if ($sig_match_rv == $SIG_MATCH) {
2195
2196                 $matched_sig = $SIG_MATCH;
2197                 next SIG if $rv == 0;
2198
2199                 $dl = $rv if $rv > $dl;
2200
2201                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'icmp'}{'sid'}
2202                     {$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
2203
2204                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'icmp'}{'sid'}
2205                     {$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
2206
2207                 $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'icmp'}{'sid'}
2208                     {$sid}{$pkt_hr->{'chain'}}{'time'} = time();
2209
2210                 $sig_sources{$sid}{$pkt_hr->{'src'}} = 0; ### not fwsnort
2211                 $top_sigs{$sid}++;
2212                 $top_sig_counts{$pkt_hr->{'src'}}++;
2213             }
2214
2215         } elsif ($sig_hr->{'proto'} eq 'icmp6') {
2216             ### FIXME icmp6 specifc Snort matches?
2217         }
2218     }
2219     return $dl, $matched_sig;
2220 }
2221
2222 sub match_snort_tcp_keywords() {
2223     my ($pkt_hr, $sig_hr) = @_;
2224
2225     if ($debug and $debug_sid == $sig_hr->{'sid'}) {
2226         print STDERR "[+] SID: $sig_hr->{'sid'} match_snort_tcp_keywords()\n";
2227     }
2228
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";
2234             }
2235             return 0, $NO_SIG_MATCH;
2236         }
2237     }
2238
2239     my $header_len = $IP_HEADER_LEN + $TCP_HEADER_LEN;
2240
2241     if ($pkt_hr->{'flags'} =~ m|SYN|) {
2242         ### extend the header length to compensate for TCP options
2243         $header_len += $TCP_MAX_OPTS_LEN;
2244     }
2245
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);
2249     }
2250
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);
2254     }
2255
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);
2259     }
2260
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);
2264     }
2265
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);
2269     }
2270
2271     ### matched the signature
2272     if ($debug) {
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|;
2276     }
2277
2278     return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
2279 }
2280
2281 sub match_snort_udp_keywords() {
2282     my ($pkt_hr, $sig_hr) = @_;
2283
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),
2287                 'dsize', $sig_hr);
2288     }
2289
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);
2294     }
2295
2296     ### matched the signature
2297     if ($debug) {
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|;
2301     }
2302
2303     return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
2304 }
2305
2306 sub match_snort_icmp_keywords() {
2307     my ($pkt_hr, $sig_hr) = @_;
2308
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),
2312                 'dsize', $sig_hr);
2313     }
2314
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);
2319     }
2320
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);
2324     }
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);
2328     }
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);
2332     }
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);
2336     }
2337
2338     ### matched the signature
2339     if ($debug) {
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|;
2343     }
2344
2345     return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
2346 }
2347
2348 sub match_snort_ip_keywords() {
2349     my ($pkt_hr, $sig_hr) = @_;
2350
2351     if ($pkt_hr->{'is_ipv6'}) {
2352         ### we need to build IPv6 signature keywords
2353         return 1, $NO_SIG_MATCH;
2354     }
2355
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);
2359     }
2360
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);
2364     }
2365
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);
2370     }
2371
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'};
2375
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'};
2380     }
2381
2382     return 0, $NO_SIG_MATCH
2383         unless &check_sig_ipopts($pkt_hr->{'ip_opts'}, 'ipopts', $sig_hr);
2384
2385     if ($sig_hr->{'proto'} eq 'ip') {
2386         ### signature match
2387         if ($debug) {
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|;
2391         }
2392
2393         $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'ip'}{'sid'}
2394             {$sig_hr->{'sid'}}{$pkt_hr->{'chain'}}{'pkts'}++;
2395
2396         $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'ip'}{'sid'}
2397             {$sig_hr->{'sid'}}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
2398
2399         $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'ip'}{'sid'}
2400             {$sig_hr->{'sid'}}{$pkt_hr->{'chain'}}{'time'} = time();
2401
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'}}++;
2405
2406         return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
2407     }
2408     return 1, $NO_SIG_MATCH;
2409 }
2410
2411 sub check_sig_int_range() {
2412     my ($pkt_val, $keyword, $sig_hr) = @_;
2413
2414     $pkt_val = 0 if $pkt_val < 0;
2415
2416     if ($sig_hr->{"${keyword}_neg"}) {
2417         if ($pkt_val <= $sig_hr->{"${keyword}_e"}
2418                 and $pkt_val >= $sig_hr->{"${keyword}_s"}) {
2419             if ($debug) {
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|;
2425                 }
2426             }
2427             return 0;
2428         }
2429     } else {
2430         ### normal match
2431         if ($pkt_val < $sig_hr->{"${keyword}_s"}) {
2432             if ($debug) {
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|;
2437                 }
2438             }
2439             return 0;
2440         }
2441         if ($pkt_val > $sig_hr->{"${keyword}_e"}) {
2442             if ($debug) {
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|;
2447                 }
2448             }
2449             return 0;
2450         }
2451     }
2452     return 1;
2453 }
2454
2455 sub check_sig_ipopts() {
2456     my ($pkt_val, $keyword, $sig_hr) = @_;
2457
2458     return 1 unless defined $sig_hr->{$keyword};
2459     return 0 unless $pkt_val;
2460     return 1 if $sig_hr->{$keyword} eq 'any';
2461
2462     my $pkt_opts_hr = &parse_ip_options($pkt_val);
2463
2464     return 0 unless defined $pkt_opts_hr->{$sig_hr->{$keyword}};
2465     return 1;
2466 }
2467
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);
2472 }
2473
2474 sub match_port() {
2475     my ($hr, $port) = @_;
2476     if (defined $hr->{'port'}) {
2477         return 1 if defined $hr->{'port'}->{$port};
2478     }
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);
2483         }
2484     }
2485     return 0;
2486 }
2487
2488 sub p0f() {
2489     my $pkt_hr = shift;
2490
2491     if ($pkt_hr->{'is_ipv6'}) {
2492         &p0f_ipv6($pkt_hr);
2493     } else {
2494         &p0f_ipv4($pkt_hr);
2495     }
2496     return;
2497 }
2498
2499 sub p0f_ipv6() {
2500     my $pkt_hr = shift;
2501     return;
2502 }
2503
2504 sub p0f_ipv4() {
2505     my $pkt_hr = shift;
2506
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;
2510
2511     # p0f Fingerprint entry format:
2512     #
2513     # wwww:ttt:D:ss:OOO...:OS:Version:Subtype:Details
2514     #
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
2517     #            respectively.
2518     # ttt      - initial TTL
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
2526     #
2527     ### S4:64:1:60:M*,S,T,N,W7:     Linux:2.6:8:Linux 2.6.8 and newer (?)
2528
2529     my $options_ar = &parse_tcp_options($pkt_hr->{'src'},
2530             $pkt_hr->{'tcp_opts'});
2531
2532     unless ($options_ar) {
2533         print STDERR "[-] Could not fingerprint remote OS.\n" if $debug;
2534         return;
2535     }
2536
2537     my $matched_os = 0;
2538
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
2543             $matched_len = 1;
2544         } elsif ($sig_len =~ /^\%(\d+)/) {
2545             if (($pkt_hr->{'ip_len'} % $1) == 0) {
2546                 $matched_len = 1;
2547             }
2548         } elsif ($pkt_hr->{'ip_len'} == $sig_len) {
2549             $matched_len = 1;
2550         }
2551         next LEN unless $matched_len;
2552
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};
2556
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) {
2561                     next TTL;
2562                 }
2563
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;
2573                         }
2574                     } elsif ($sig_win_size =~ /^S(\d+)/) {
2575                         ### window size must be a multiple of maximum
2576                         ### seqment size
2577                         my $multiple = $1;
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;
2583                                 }
2584                             }
2585                             last;
2586                         }
2587                     } elsif ($sig_win_size == $pkt_hr->{'win'}) {
2588                         $matched_win_size = 1;
2589                     }
2590
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;
2601
2602                                 unless (defined $options_ar->[$i]->
2603                                         {$tcp_p0f_opt_types{$sig_letter}}) {
2604                                     next TCPOPTS;  ### could not match tcp option order
2605                                 }
2606
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}}
2614                                                 == $sig_mss_val;
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}}
2626                                                 == $sig_win_val;
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}}
2637                                                 == 0;
2638                                     }  ### else it is just "T" which matches
2639                                 }
2640
2641                             }
2642                         }
2643                         OS: for my $os (keys %{$p0f_ipv4_sigs{$sig_len}
2644                                 {$test_frag_bit}{$sig_ttl}{$sig_win_size}
2645                                 {$sig_opts}}) {
2646                             my $sig = $p0f_ipv4_sigs{$sig_len}
2647                                 {$test_frag_bit}{$sig_ttl}{$sig_win_size}
2648                                 {$sig_opts}{$os};
2649                             print STDERR "[+] os: $os, $sig\n" if $debug;
2650                             $matched_os = 1;
2651                             $p0f{$pkt_hr->{'src'}}{$os} = '';
2652                         }
2653                     }
2654                 }
2655             }
2656         }
2657     }
2658     if ($debug and not $matched_os) {
2659         print STDERR "    Could not match $pkt_hr->{'src'} against any p0f signature.\n";
2660     }
2661     return;
2662 }
2663
2664 sub parse_tcp_options() {
2665     my ($src, $tcp_options) = @_;
2666     my @opts = ();
2667     my @hex_nums = ();
2668     my $debug_str = '';
2669
2670     unless ($tcp_options) {
2671         print STDERR 'no tcp options' if $debug;
2672         return [];
2673     }
2674
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;
2677         return [];
2678     }
2679     ### $tcp_options is a hex string like "020405B401010402" from the iptables
2680     ### log message
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;
2685     }
2686
2687     my $max_parse_attempts = $#chars;
2688     my $parse_ctr = 0;
2689
2690     OPT: for (my $opt_kind=0; $opt_kind <= $#hex_nums;) {
2691
2692         $parse_ctr++;
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;
2696             return [];
2697         }
2698
2699         last OPT unless defined $hex_nums[$opt_kind+1];
2700
2701         my $is_nop = 0;
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 => ''};
2706             $is_nop = 1;
2707         } elsif (hex($hex_nums[$opt_kind]) == $tcp_mss_type) {  ### MSS
2708             my $mss_hex = '';
2709             for (my $i=$opt_kind+2; $i < ($opt_kind+$len); $i++) {
2710                 $mss_hex .= $hex_nums[$i];
2711             }
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];
2719             }
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];
2730             }
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
2735             last OPT;
2736         }
2737         if ($is_nop) {
2738             $opt_kind += 1;
2739         } else {
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;
2747                 &sys_log($msg);
2748                 return [];
2749             }
2750             ### get to the next option-kind field
2751             $opt_kind += $len;
2752         }
2753     }
2754     if ($debug) {
2755         $debug_str =~ s/\,$//;
2756         print STDERR "[+] $debug_str\n" if $debug;
2757     }
2758     return \@opts;
2759 }
2760
2761 sub parse_ip_options() {
2762     my $ip_opts_str = shift;
2763
2764     my %ip_opts  = ();
2765     my @hex_nums = ();
2766
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;
2769         return '';
2770     }
2771     print STDERR "[+] parse_ip_options(): matched " if $debug;
2772
2773     push @hex_nums, $1 while $ip_opts_str =~ m|(.{2})|g;
2774
2775     OPT: for (my $i=0; $i <= $#hex_nums; $i++) {
2776         my $val = hex($hex_nums[$i]);
2777
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;
2783             } else {
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
2791                     ### of the header).
2792                     return \%ip_opts;
2793                 }
2794                 $i += $pkt_opt_len;
2795             }
2796             if ($debug) {
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'}};
2800             }
2801             $ip_opts{$ip_options{$rfc_opt_val}{'sig_keyword'}} = '';
2802         }
2803     }
2804     print STDERR "\n" if $debug;
2805
2806     return \%ip_opts;
2807 }
2808
2809 sub posf() {
2810     my $pkt_hr = shift;
2811
2812     if ($pkt_hr->{'is_ipv6'}) {
2813         &posf_ipv6($pkt_hr);
2814     } else {
2815         &posf_ipv4($pkt_hr);
2816     }
2817     return;
2818 }
2819
2820 sub posf_ipv6() {
2821     my $pkt_hr = shift;
2822     return;
2823 }
2824
2825 sub posf_ipv4() {
2826     my $pkt_hr = shift;
2827
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'};
2834
2835     my $min_ttl;
2836     my $max_ttl;
2837     my $id_str;
2838
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
2845
2846     print STDERR "[+] posf():  $src  LEN: $len, TOS: $tos, TTL: $ttl, ",
2847         "ID: $id, WIN: $win\n" if $debug;
2848
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;
2862                 return;
2863             }
2864         }
2865     }
2866     return;
2867 }
2868
2869 sub id_incr() {
2870     my $aref = shift;
2871     for (my $i=0; $i<$#$aref; $i++) {
2872         return 'RANDOM'
2873             unless ($aref->[$i] < $aref->[$i+1]
2874                 and ($aref->[$i+1] - $aref->[$i]) < 1000);
2875     }
2876     return 'SMALLINCR';
2877 }
2878
2879 sub ttl_range() {
2880     my $hr = shift;
2881     my $min_ttl = 256;
2882     my $max_ttl = 0;
2883     for my $ttl (keys %$hr) {
2884         $min_ttl = $ttl if $ttl < $min_ttl;
2885         $max_ttl = $ttl if $ttl > $max_ttl;
2886     }
2887     return $min_ttl, $max_ttl;
2888 }
2889
2890 sub assign_sid_dl() {
2891     my ($sid, $dl) = @_;
2892
2893     ### see if /etc/psad/snort_rule_dl assigns a DL (may be
2894     ### zero).
2895     if (defined $snort_rule_dl{$sid}) {
2896         $dl = $snort_rule_dl{$sid};
2897     }
2898
2899     print STDERR "[+] assign_sid_dl(): snort_rule_dl ",
2900         "assigning SID $sid a danger level of ",
2901         "$dl\n" if $debug;
2902
2903     return $dl;
2904 }
2905
2906 sub add_fwsnort_sid() {
2907     my $pkt_hr = shift;
2908
2909     my $sid = $pkt_hr->{'fwsnort_sid'};
2910
2911     if (defined $fwsnort_sigs{$sid}) {
2912
2913         ### see if we need to ignore this signature match
2914         my $dl = &assign_sid_dl($sid, 2);
2915
2916         unless ($dl) {
2917             print "[+] add_fwsnort_sid(): ignoring fwsnort signature ",
2918                 "match for SID: $sid (DL=0)\n" if $debug;
2919             return 0, $SIG_MATCH;
2920         }
2921
2922         $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2923             {'sid'}{$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
2924
2925         $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2926             {'sid'}{$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 1;
2927
2928         $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2929             {'sid'}{$sid}{$pkt_hr->{'chain'}}{'time'} = time();
2930
2931         $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2932             {'sid'}{$sid}{$pkt_hr->{'chain'}}{'fwsnort_rnum'}
2933                 = $pkt_hr->{'fwsnort_rnum'};
2934
2935         $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2936             {'sid'}{$sid}{$pkt_hr->{'chain'}}{'fwsnort_estab'}
2937                 = $pkt_hr->{'fwsnort_estab'};
2938
2939         $sig_sources{$sid}{$pkt_hr->{'src'}} = 1; ### is an fwsnort sid
2940         $top_sigs{$sid}++;
2941         $top_sig_counts{$pkt_hr->{'src'}}++;
2942
2943         if ($pkt_hr->{'proto'} eq 'tcp' or $pkt_hr->{'proto'} eq 'udp') {
2944
2945             $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
2946                 {'sid'}{$sid}{$pkt_hr->{'chain'}}{'dp'}
2947                     = $pkt_hr->{'dp'};
2948
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'};
2953             }
2954         }
2955
2956         return $dl, $SIG_MATCH;
2957
2958     } else {
2959         print "[-] Found sid: $sid in packet, but no ",
2960             "corresponding fwsnort rule.\n" if $debug;
2961     }
2962     return 0, $NO_SIG_MATCH;
2963 }
2964
2965 sub delete_old_scans() {
2966
2967     my $current_time = time();
2968
2969     print STDERR "[+] delete_old_scans()\n" if $debug;
2970
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"
2978                     if $debug;
2979
2980                 delete $scan{$src}{$dst};
2981
2982                 if ($config{'MAX_SCAN_IP_PAIRS'} > 0) {
2983                     $scan_ip_pairs-- if $scan_ip_pairs > 0;
2984                 }
2985             }
2986         }
2987     }
2988     return;
2989 }
2990
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
2995     ### email.
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)/) {
3015                         my $src = $1;
3016                         if (defined $scan_dl{$src}
3017                                 and ($scan_dl{$src}
3018                                     >= $config{'DSHIELD_DL_THRESHOLD'})) {
3019                             print MAIL $line;
3020                             print DSSAVE $line;
3021                         }
3022                     }
3023                 }
3024             } else {
3025                 print MAIL for @dshield_data;
3026                 print DSSAVE for @dshield_data;
3027             }
3028             close MAIL;
3029             close DSSAVE;
3030         } else {
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)/) {
3045                         my $src = $1;
3046                         if (defined $scan_dl{$src}
3047                                 and ($scan_dl{$src}
3048                                     >= $config{'DSHIELD_DL_THRESHOLD'})) {
3049                             print MAIL $line;
3050                             print DSSAVE $line;
3051                         }
3052                     }
3053                 }
3054             } else {
3055                 print MAIL for @dshield_data;
3056                 print DSSAVE for @dshield_data;
3057             }
3058             close MAIL;
3059             close DSSAVE;
3060         }
3061
3062         &sys_log("sent $#dshield_data lines of log data to " .
3063             $config{'DSHIELD_ALERT_EMAIL'});
3064
3065         ### store the current time
3066         $last_dshield_alert = time();
3067
3068         ### increment stats counters
3069         $dshield_email_ctr++;
3070         $dshield_lines_ctr += $#dshield_data;
3071
3072         ### clear the dshield data array so we don't re-send
3073         ### any old data.
3074         @dshield_data = ();
3075
3076         ### Write Dshield stats to disk
3077         &write_dshield_stats();
3078     }
3079     return;
3080 }
3081
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;
3091     }
3092     print STDERR "        valid $proto type/code\n" if $debug;
3093     return 0;
3094 }
3095
3096 sub import_perl_modules() {
3097
3098     my $mod_paths_ar = &get_mod_paths();
3099
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;
3103     }
3104
3105     if ($debug) {
3106         print STDERR "[+] import_perl_modules(): The \@INC array:\n";
3107         print STDERR "$_\n" for @INC;
3108     }
3109
3110     require IPTables::ChainMgr;
3111     require NetAddr::IP;
3112     require Date::Calc;
3113     require Unix::Syslog;
3114     require Cwd;
3115     require Storable if $store_file;
3116
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;
3121
3122     $imported_syslog_module = 1;
3123
3124     return;
3125 }
3126
3127 sub get_mod_paths() {
3128
3129     my @paths = ();
3130
3131     $config{'PSAD_LIBS_DIR'} = $lib_dir if $lib_dir;
3132
3133     unless (-d $config{'PSAD_LIBS_DIR'}) {
3134         my $dir_tmp = $config{'PSAD_LIBS_DIR'};
3135         $dir_tmp =~ s|lib/|lib64/|;
3136         if (-d $dir_tmp) {
3137             $config{'PSAD_LIBS_DIR'} = $dir_tmp;
3138         } else {
3139             return [];
3140         }
3141     }
3142
3143     opendir D, $config{'PSAD_LIBS_DIR'}
3144         or die "[*] Could not open $config{'PSAD_LIBS_DIR'}: $!";
3145     my @dirs = readdir D;
3146     closedir D;
3147
3148     push @paths, $config{'PSAD_LIBS_DIR'};
3149
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");
3156     }
3157     return \@paths;
3158 }
3159
3160 sub psad_init() {
3161
3162     %config = ();
3163     %cmds   = ();
3164
3165     ### set umask to -rw-------
3166     umask 0077;
3167
3168     ### turn off buffering
3169     $| = 1;
3170
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;
3174
3175     ### import any override config files first
3176     &import_override_configs() if $override_config_str;
3177
3178     ### import psad.conf
3179     &import_config($config_file);
3180
3181     ### import FW_MSG_SEARCH strings
3182     &import_fw_search($config_file);
3183
3184     ### expand any embedded vars within config values
3185     &expand_vars();
3186
3187     ### pid file hash
3188     %pidfiles = (
3189         'psadwatchd' => $config{'PSADWATCHD_PID_FILE'},
3190         'psad'       => $config{'PSAD_PID_FILE'},
3191         'kmsgsd'     => $config{'KMSGSD_PID_FILE'},
3192     );
3193
3194     ### dump configuration to STDOUT
3195     if ($dump_conf or $dump_ipt_policy) {
3196         my $rv = 0;
3197         my $rv_tmp = 0;
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;
3201         exit $rv;
3202     }
3203
3204     ### make sure all necessary configuration variables
3205     ### are defined
3206     &required_vars();
3207
3208     ### store the psad command line.
3209     $cmdline_file = $config{'PSAD_CMDLINE_FILE'};
3210
3211     ### make sure the values in the config file make sense
3212     &validate_config();
3213
3214     ### setup the appropriate iptables data file depending on whether
3215     ### SYSLOG_DAEMON is set to ulogd, or if ENABLE_SYSLOG_FILE is set
3216     ### to 'Y'
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'};
3222         } else {
3223             $fw_data_file = $config{'FW_DATA_FILE'};
3224         }
3225     }
3226
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' => ''});
3231
3232     ### import psad perl modules
3233     &import_perl_modules();
3234
3235     ### download latest signatures from
3236     ### http://www.cipherdyne.org/psad/signatures
3237     exit &download_signatures() if $download_sigs;
3238
3239     ### set some config variables based on command line input
3240     &handle_cmdline();
3241
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;
3246
3247     ### The --Kill command line switch was given.
3248     exit &stop_psad() if $kill;
3249
3250     ### The --HUP command line switch was given.
3251     exit &hup() if $hup;
3252
3253     ### The --USR1 command line switch was given.
3254     exit &usr1() if $usr1;
3255
3256     ### The --Flush command line switch was given.
3257     exit &sockwrite_flush_auto_rules() if $flush_fw;
3258
3259     ### the --Restart command line switch was given
3260     exit &restart() if $restart;
3261
3262     ### list any existing iptables IPT_AUTO_CHAIN chains
3263     exit &ipt_list_auto_chains() if $fw_list_auto;
3264
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
3268     ### be ignored).
3269     exit &sockwrite_add_ipt_block_ip() if $fw_block_ip;
3270
3271     ### delete IP/network from psad auto blocking chains
3272     exit &sockwrite_rm_ipt_block_ip() if $fw_rm_block_ip;
3273
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();
3278
3279     ### import icmp types and codes from psad_icmp_types; icmp "type"
3280     ### and "code" fields will be validated against the values in this
3281     ### file.
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;
3286
3287     ### import p0f-based passive OS fingerprinting signatures
3288     &import_p0f_ipv4_sigs() unless $no_posf;
3289
3290     ### import TOS-based passive OS fingerprinting signatures
3291     &import_posf_sigs() unless $no_posf;
3292
3293     ### import auto_dl file for automatic ip/network danger
3294     ### level assignment
3295     &import_auto_dl() unless $no_auto_dl;
3296
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;
3301
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;
3306
3307     ### the --Status command line switch was given
3308     exit &status() if $status_mode;
3309
3310     ### there is a set of ports that should be ignored
3311     &parse_ignore_ports();
3312
3313     ### there is a set of protocols that should be ignored
3314     &parse_ignore_protocols();
3315
3316     ### there is a set of interfaces that should be ignored
3317     &parse_ignore_interfaces();
3318
3319     ### enter iptables analysis mode.
3320     exit &analysis_mode() if $analyze_mode;
3321
3322     ### enter CSV output mode.
3323     exit &csv_mode() if $csv_mode or $gnuplot_mode;
3324
3325     ### enter benchmarking mode
3326     exit &benchmark_mode() if $benchmark;
3327
3328     ### analyze the iptables policy and exit
3329     my $rv = &fw_analyze_mode();
3330     exit $rv if $fw_analyze;
3331
3332     ### make sure we are setup to run
3333     &setup();
3334
3335     ### dump config
3336     &dump_conf() if $debug;
3337
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";
3341     }
3342
3343     return;
3344 }
3345
3346 sub validate_config() {
3347
3348     &check_enable_vars_value();
3349
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);
3357
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);
3361
3362     die qq([*] Invalid EMAIL_ADDRESSES value: "$config{'EMAIL_ADDRESSES'}")
3363         unless $config{'EMAIL_ADDRESSES'} =~ /\S+\@\S+/;
3364
3365     ### translate commas into spaces
3366     $config{'EMAIL_ADDRESSES'} =~ s/\s*\,\s/ /g;
3367
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 ' .
3373             'set to N.';
3374     }
3375     if ($status_min_dl and $status_min_dl > 5) {
3376         die '[*] The --status-dl must be between 1 and 5.';
3377     }
3378     if ($no_kmsgsd and not $debug) {
3379         die '[*] The --no-kmsgsd option can only be used with --debug.';
3380     }
3381
3382     if ($fw_del_chains and not $flush_fw) {
3383         die '[*] The --fw-del-chains option can only be used with --Flush.';
3384     }
3385
3386     if ($fw_block_ip) {
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.';
3392         }
3393     }
3394
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.';
3401         }
3402     }
3403
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'};
3408     }
3409
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";
3419     }
3420
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";
3430     }
3431
3432     if ($analyze_mode or $gnuplot_mode or $csv_mode) {
3433         $fw_data_file = $config{'IPT_SYSLOG_FILE'}
3434             unless $fw_data_file;
3435     }
3436
3437     if ($gnuplot_mode and not $csv_fields) {
3438         die "[*] Must specify which iptables fields to plot with the ",
3439             "--CSV-fields argument."
3440     }
3441
3442     return;
3443 }
3444
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
3449         ### 'Y' or 'N'
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'";
3454         }
3455     }
3456     return;
3457 }
3458
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;
3464     return;
3465 }
3466
3467 sub get_connected_subnets() {
3468
3469     my @connected_subnets = ();
3470
3471     if ($config{'IFCFGTYPE'} =~ /iproute2/i) {
3472         my @ip_out = @{&run_command($cmds{'ip'}, 'addr')};
3473         my $intf_name    = '';
3474         my $home_net_str = '';
3475         for my $line (@ip_out) {
3476             if ($line =~ /^\d+:\s+(\S+): </) {
3477                 $intf_name = $1;
3478                 next;
3479             }
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);
3486             }
3487         }
3488     } else {
3489         my @ifconfig_out = @{&run_command($cmds{'ifconfig'}, '-a')};
3490         my $intf_name    = '';
3491         my $home_net_str = '';
3492         for my $line (@ifconfig_out) {
3493             if ($line =~ /^(\S+)\s+Link/) {
3494                 $intf_name = $1;
3495                 next;
3496             }
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);
3503             }
3504         }
3505     }
3506     return \@connected_subnets;
3507 }
3508
3509 sub validate_home_net() {
3510
3511     @local_nets = ();
3512
3513     return if $config{'HOME_NET'} eq 'any'
3514         and $config{'ENABLE_INTF_LOCAL_NETS'} eq 'N';
3515
3516     my $connected_subnets_ar = &get_connected_subnets();
3517
3518     if ($config{'ENABLE_INTF_LOCAL_NETS'} eq 'Y' and not $analyze_mode) {
3519
3520         my $connected_str = '';
3521         for my $net (@$connected_subnets_ar) {
3522             push @local_nets, $net;
3523             $connected_str .= $net->network()->cidr() . ", ";
3524         }
3525         $connected_str =~ s|,\s*$||;
3526
3527         $config{'HOME_NET'} = $connected_str;
3528         $config{'HOME_NET'} = 'any' unless $connected_str;
3529
3530     } else {
3531
3532         if ($config{'HOME_NET'} =~ /CHANGEME/) {
3533             &sys_log('config warning: the HOME_NET ' .
3534                 'variable has not been set, defaulting to "any"');
3535
3536             $config{'HOME_NET'} = 'any';
3537             return;
3538         }
3539         my @home_nets = split /\s*\,\s*/, $config{'HOME_NET'};
3540         my $found_one_net = 0;
3541         for my $net (@home_nets) {
3542             my $home_net = '';
3543             if ($net =~ m|($ipv4_re/$ipv4_re)|) {
3544                 $home_net = new NetAddr::IP $1;
3545             } elsif ($net =~ m|($ipv4_re/\d+)|) {