Permalink
Fetching contributors…
Cannot retrieve contributors at this time
executable file 11563 lines (10005 sloc) 413 KB
#!/usr/bin/perl -w
#
################################################################################
#
# File: psad (/usr/sbin/psad)
#
# URL: http://www.cipherdyne.org/psad/
#
# Purpose: psad makes use of iptables logs to detect port scans,
# probes for backdoors and DDoS tools, and other suspect traffic
# (many signatures were adapted from the SNORT intrusion
# detection system). Data is provided by parsing syslog
# firewall messages out of /var/log/messages (or wherever syslog
# is configured to write iptables logs to).
#
# For more information read the psad man page or view the
# documentation provided at: http://www.cipherdyne.org/psad/
#
# Author: Michael Rash (mbr@cipherdyne.org)
#
# Credits: (see the CREDITS file bundled with the psad sources.)
#
# Version: 2.4.5
#
# Copyright (C) 1999-2017 Michael Rash (mbr@cipherdyne.org)
#
# Reference: SNORT is a registered trademark of Sourcefire, Inc.
#
# License (GNU Public License):
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
# TODO: (see the TODO file bundled with the psad sources.)
#
# Default behavior is as follows. Each of these features can be disabled
# with command line arguments:
#
# - passive OS fingerprinting = yes
# - snort rule matching = yes
# - write fw errors to error log = yes
# - daemon mode = yes
# - reverse dns lookups = yes
# - validate firewall rules = yes
# - whois lookups of scanning IPs = yes
# - parse netstat output for local server ports = yes
#
# Coding Style: All configuration variables from psad.conf are stored in
# the %config hash by keys that are in capital letters. This is
# the only place in the code where capital letters will be used in
# variables names. There are several variables with file-scope, and
# these variables are clearly commented near the top of each of the
# psad daemons. Lines are generally limited to 80 characters for easy
# reading.
#
# Scan hash key explanation:
# absnum - Total number of packets from $src to $dst
# tot_protocols - Total number of IP protocols scanned by $src against $dst
# chain - iptables chain under which the scan packets appear in the
# logs.
# s_time - Start time for the first packet seen from src to dst.
# alerted - An alert has been sent.
# pkts - Number of packets (used for signatures and a packet counter
# for the current interval.
# flags - Keeps track of tcp flags.
# sid - Signature tracking
# abs_sp - Absolute starting port.
# abs_ep - Absolute ending port.
# strtp - Starting port.
# endp - Ending port.
#
# Sample iptables log messages:
#
# Sample tcp packet (rejected by iptables... --log-prefix = "DROP ")
#
# Mar 11 13:15:52 orthanc kernel: DROP IN=lo OUT= MAC=00:00:00:00:00:00:00:00:
# 00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.1 LEN=60 TOS=0x00 PREC=0x00
# TTL=64 ID=0 DF PROTO=TCP SPT=44847 DPT=35 WINDOW=32304 RES=0x00 SYN URGP=0
#
# Sample icmp packet rejected by iptables INPUT chain:
#
# Nov 27 15:45:51 orthanc kernel: DROP IN=eth1 OUT= MAC=00:a0:cc:e2:1f:f2:00:
# 20:78:10:70:e7:08:00 SRC=192.168.10.20 DST=192.168.10.1 LEN=84 TOS=0x00
# PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=61055 SEQ=256
#
# Sample icmp packet logged through FORWARD chain:
#
# Aug 20 21:23:32 orthanc kernel: SID365 IN=eth2 OUT=eth1 SRC=192.168.20.25
# DST=192.168.10.15 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=ICMP TYPE=8
# CODE=0 ID=19467 SEQ=256
#
# Occasionally the kernel klogd ring buffer must become full since log
# entries are sometimes generated by a long port scan like this (note
# there is no 'DPT' field):
#
# Mar 16 23:50:25 orthanc kernel: DROP IN=lo OUT= MAC=00:00:00:00:00:00:00:
# 00:00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.1 LEN=60 TOS=0x00 PREC=0x00
# TTL=64 ID=0 DF PROTO=TCP SPT=39935 DINDOW=32304 RES=0x00 SYN URGP=0
#
# Note on iptables tcp log messages:
#
# iptables reports tcp flags in the following order:
#
# URG ACK PSH RST SYN FIN
#
# Files specification for /var/log/psad/<srcip> directories:
#
# psad creates a new directory "/var/log/psad/<src>" for each new <src>
# from which a scan is detected. Under this directory several files are
# created:
#
# danger_level - Overall danger level aggregated for all scans.
# p0f_guess - Passive OS fingerprint guess for <src>.
# <src/dst>_whois - Whois information for <src> (or <dst> if src
# is a local IP).
# <dst>_email_ctr - Total email alerts sent for <src>.
# <dst>_email_alert - The most recent email alert for <dst>.
# <dst>_packet_ctr - Packet counters for <dst>.
# <dst>_signatures - Signatures detected against <dst>.
#
# Note that some of the files above contain the destination address since a
# single source address may scan several destination addresses.
#
###############################################################################
#
### modules used by psad
use File::Copy;
use File::Path;
use IO::Socket;
use IO::Select;
use Socket;
use POSIX;
use IO::Handle;
use Data::Dumper;
use Getopt::Long 'GetOptions';
use strict;
### ========================== main =================================
### set the current version
my $version = '2.4.5';
### default config file for psad (can be changed with
### --config switch)
my $config_file = '/etc/psad/psad.conf';
### this will be set to either FW_DATA_FILE, ULOG_DATA_FILE
### or IPT_SYSLOG_FILE
my $fw_data_file = '';
### disable debugging by default
my $debug = 0;
my $debug_sid = 0; ### debug a specific signature
my $flush_fw = 0;
### build the iptables blocking configuration out of the
### IPT_AUTO_CHAIN variable
my @ipt_config = ();
### main configuration hash
my %config = ();
my $override_config_str = '';
### local subnets
my @local_nets = ();
### fw search string array
my @fw_search = ();
### socket for --fw-block
my $ipt_sock = '';
### commands hash
my %cmds = ();
### main psad data structure; contains ips, port ranges,
### protocol info, tcp flags, etc.
my %scan = ();
### cache scan danger levels
my %scan_dl = ();
### cache scan email counters
my %scan_email_ctrs = ();
### cache executions of external script (only used if
### ENABLE_EXT_SCRIPT_EXEC is set to 'Y');
my %scan_ext_exec = ();
### for Mirai botnet scanning phase detection
my %mirai_scan = ();
### cache p0f-based passive os fingerprinting information
my %p0f = ();
### cache p0f-based passive os fingerprinting signature information
my %p0f_ipv4_sigs = ();
my %p0f_ipv6_sigs = ();
### cache TOS-based passive os fingerprinting information
my %posf = ();
### cache TOS-based passive os fingerprinting signature information
my %posf_sigs = ();
### cache valid icmp types and corresponding codes
my %valid_icmp_types = ();
my %valid_icmp6_types = ();
### Cache snort rule messages unless --no-snort-sids switch was
### given. This is only useful if iptables includes rules
### that log things like "SID123". "fwsnort"
### (http://www.cipherdyne.org/fwsnort/) will automatically
### build such a ruleset from snort signatures.
my %fwsnort_sigs = ();
### Cache snort classification.config file for class priorities
my %snort_class_dl = ();
### Cache any individual Snort rule priority definitions from
### the snort_rule_dl file
my %snort_rule_dl = ();
### Cache Snort rule reference configuration
my %snort_ref_baseurl = ();
### cache all scan signatures from /etc/psad/signatures file
my %sigs = ();
my %sig_search = ();
my %sig_ip_objs = ();
### cache iptables prefixes
my %ipt_prefixes = ();
### ignore ports
my %ignore_ports = ();
### ignore protocols
my %ignore_protocols = ();
### ignore interfaces
my %ignore_interfaces = ();
### data array used for dshield.org logs
my @dshield_data = ();
### track the last time we sent an alert to dshield.org
my $last_dshield_alert = '';
### calculate how often a dshield alert will be sent
my $dshield_alert_interval = '';
### dshield stats counters
my $dshield_email_ctr = 0;
my $dshield_lines_ctr = 0;
### get the current timezone for dshield (this is calculated
### and re-calculated since the timezone may change).
my $timezone = '';
### get the current year for dshield
my $year = '';
### keep track of how many CHECK_INTERVALS have elapsed; this is
### useful for TOP_SCANS_CTR_THRESHOLD
my $check_interval_ctr = 0;
### track the number of scan IP pairs for MAX_SCAN_IP_PAIRS thresholding
my $scan_ip_pairs = 0;
### %auto_dl holds all ip addresses that should automatically
### be assigned a danger level (or ignored).
my %auto_dl = ();
my %auto_dl_ip_objs = ();
my %auto_assigned_msg = ();
my $PERMANENT = 0;
### cache the source ips that we have automatically blocked
### (if ENABLE_AUTO_IDS == 'Y')
my %auto_blocked_ips = ();
### counter to check psad iptables chains and jump rules
my $iptables_prereq_check = 0;
### cache the addresses we have issued dns lookups against.
my %dns_cache = ();
### cache the addresses we have executed whois lookups against.
my %whois_cache = ();
### cache ports the local machine is listening on (periodically
### updated by get_listening_ports()).
my %local_ports = ();
### cache the ip addresses associated with each interface on the
### local machine.
my %local_ips = ();
### Top attacking statistics
my %top_tcp_ports = ();
my %top_udp_ports = ();
my %top_udplite_ports = ();
my %top_sigs = ();
my %sig_sources = ();
my %top_sig_counts = ();
my %top_packet_counts = ();
my %local_src = ();
### regex to match IP addresses
my $ipv4_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|; ### IPv4
### IPv6 - full version in ip6tables logs
my $ipv6_re = qr|(?:[a-f0-9]{4}:){7}(?:[a-f0-9]{4})|i;
### ttl values are decremented depending on the number of hops
### the packet has taken before it hits the firewall. We will
### assume packets will not jump through more than 20 hops on
### average.
my $max_hops = 20;
### initial set of protocol packet counters (may get expanded through
### things like protocol scan detection)
my %protocols = (
'tcp' => '',
'udp' => '',
'udplite' => '',
'icmp' => '',
'icmp6' => ''
);
my %proto_ctrs = ();
my %protocol_strings = ();
### pid file hash
my %pidfiles = ();
### for the IPTables::ChainMgr module
my %ipt_opts = ();
### initialize and scope some default variables (command
### line args can override some default values)
my $sigs_file = '';
my $posf_file = '';
my $auto_dl_file = '';
my $snort_rules_dir = '';
my $srules_type = '';
my $cmdline_file = '';
my $analyze_mode = 0;
my $analysis_fields = '';
my $analysis_tokens_ar = [];
my $analysis_match_criteria_ar = [];
my $analyze_mode_auto_block = 0;
my $get_next_rule_id = 0;
my $test_mode = 0;
my $syslog_server = 0;
my $kill = 0;
my $restart = 0;
my $restrict_ip = '';
my $restrict_ip_cmdline = '';
my $status_mode = 0;
my $status_ip = '';
my $status_min_dl = 0;
my $status_summary = 0;
my $fw_list_auto = 0;
my $fw_block_ip = '';
my $fw_rm_block_ip = '';
my $fw_del_chains = 0;
my $gnuplot_mode = 0;
my $gnuplot_year = 0;
my $gnuplot_prev_mon = 0;
my $gnuplot_title = '';
my $gnuplot_legend_title = '';
my $gnuplot_grayscale = 0;
my $gnuplot_x_label = '';
my $gnuplot_x_range = '';
my $gnuplot_y_label = '';
my $gnuplot_y_range = '';
my $gnuplot_z_label = '';
my $gnuplot_z_range = '';
my $gnuplot_3d = 0;
my $gnuplot_view = '';
my $gnuplot_sort_style = 'value';
my $gnuplot_graph_style = '';
my $gnuplot_count_type = '';
my $gnuplot_count_element = -1;
my %gnuplot_cache_uniq = ();
my @gnuplot_data = ();
my $gnuplot_data_file = 'psad_iptables.dat';
my $gnuplot_plot_file = 'psad_iptables.gnu';
my $gnuplot_png_file = 'psad_iptables.png';
my $gnuplot_file_prefix = '';
my $gnuplot_template_file = '';
my $store_file = '';
my $gnuplot_interactive = 0;
my $plot_separator = ', '; ### default to CSV format for plot data
my $csv_mode = 0;
my $csv_stdin = 0;
my $csv_fields = '';
my $csv_print_uniq = 0;
my $csv_line_limit = 0;
my $csv_start_line = 0;
my $csv_end_line = 0;
my $csv_regex = '';
my $csv_neg_regex = '';
my $csv_have_timestamp = 0;
my $pkts_from_stdin = 0;
my $dump_ipt_policy = 0;
my $fw_include_ips = 0;
my $benchmark = 0;
my $num_packets = 0;
my $usr1 = 0;
my $hup = 0;
my $usr1_flag = 0;
my $hup_flag = 0;
my $verbose = 0;
my $print_ver = 0;
my $help = 0;
my $dump_conf = 0;
my $download_sigs = 0;
my $chk_interval = 0;
my $log_len = 23; ### used in scan_logr()
my $fw_analyze = 0;
my $fw_file = '';
my $lib_dir = '';
my $rm_data_ctr = 0;
my $analysis_emails = 0;
my $analysis_whois = 0;
my $enable_analysis_dns = 0;
my $netstat_lkup_ctr = 0;
my $kmsgsd_started = 0;
my $warn_msg = '';
my $die_msg = '';
my $skip_first_loop = 1;
my $fw_msg_read_continue = 1;
my $fw_msg_read_do_sleep = 1;
my $fw_read_cmd = '';
my $cmdl_interface = '';
my $analyze_write_data = 0;
my $local_ips_lkup_ctr = 0;
my $num_hash_marks = 76; ### for gnuplot output
my $imported_syslog_module = 0;
my $TRUNCATE = 1;
my $NO_TRUNCATE = 0;
my $s = ''; ### for IO::Select
### these flags are used to disable several features
### in psad if specified from the command line
my $no_snort_sids = 0;
my $no_signatures = 0;
my $no_icmp_types = 0;
my $no_icmp6_types = 0;
my $no_auto_dl = 0;
my $no_posf = 0;
my $no_daemon = 0;
my $no_ipt_errors = 0;
my $no_rdns = 0;
my $no_whois = 0;
my $no_netstat = 0;
my $no_fwcheck = 0;
my $no_kmsgsd = 0;
my $no_email_alerts = 0;
my $no_syslog_alerts = 0;
### tcp option types
my $tcp_nop_type = 1;
my $tcp_mss_type = 2;
my $tcp_win_scale_type = 3;
my $tcp_sack_type = 4;
my $tcp_timestamp_type = 8;
### for EXPECT_TCP_OPTIONS handling
my $found_one_tcp_options_field = 0;
my %tcp_p0f_opt_types = (
'N' => $tcp_nop_type,
'M' => $tcp_mss_type,
'W' => $tcp_win_scale_type,
'S' => $tcp_sack_type,
'T' => $tcp_timestamp_type
);
my %ip_options = ();
### for ICMP
my $ICMP_ECHO_REQUEST = 8;
my $ICMP_ECHO_REPLY = 0;
my $ICMP6_ECHO_REQUEST = 128;
my $ICMP6_ECHO_REPLY = 129;
### These are not directly support by psad because they
### do not appear in iptables logs; however, several of
### these options are supported if fwsnort is also running.
my @unsupported_snort_opts = qw(
pcre
fragbits
content-list
rpc
byte_test
byte_jump
distance
within
flowbits
rawbytes
regex
isdataat
uricontent
content
offset
replace
resp
flowbits
ip_proto
); ### the ip_proto keyword could be supported, but would require
### refactoring parse_NF_pkt_str().
### for Snort signature sp/dp matching
my @port_types = (
{'sp' => 'norm', 'dp' => 'norm'},
{'sp' => 'norm', 'dp' => 'neg'},
{'sp' => 'neg', 'dp' => 'norm'},
{'sp' => 'neg', 'dp' => 'neg'},
);
### main packet data structure
my %pkt_NF_init = (
### data link layer
'src_mac' => '',
'dst_mac' => '',
'intf' => '', ### FIXME in and out interfaces?
### network layer
'src' => '',
's_obj' => '',
'dst' => '',
'd_obj' => '',
'proto' => '',
'ip_len' => -1,
'ip_opts' => '', ### v4 or v6
### IPv4
'ip_id' => -1,
'ttl' => -1,
'tos' => '',
'frag_bit' => 0,
### IPv6
'is_ipv6' => 0,
'tc' => -1,
'hop_limit' => -1,
'flow_label' => -1,
### ICMP
'itype' => -1,
'icode' => -1,
'icmp_seq' => -1,
'icmp_id' => -1,
### transport layer
'sp' => -1,
'dp' => -1,
'win' => -1,
'flags' => -1,
'tcp_seq' => -1,
'tcp_ack' => -1,
'tcp_opts' => '',
'udp_len' => -1,
### extra fields for psad internals (DShield reporting, fwsnort
### sid matching, iptables logging prefixes and chains, etc.)
'fwsnort_sid' => 0,
'fwsnort_rnum' => 0,
'fwsnort_estab' => 0,
'is_topera' => 0, ### Topera IPv6 scanner detection, requires --log-ip-options
'is_masscan' => 0, ### Masscan detection, requires --log-tcp-options
'is_mirai' => 0, ### Mirai botnet scanning phase detection
'is_port_sweep' => 0,
'chain' => '',
'log_prefix' => '',
'dshield_str' => '',
'syslog_host' => '',
'timestamp' => ''
);
my %gnuplot_non_digit_packet_fields = (
### 'hashentry' - maps the field to an integer based on whether
### it has been seen before
### 'intf2int' - converts interface number to an integer (e.g. eth0 -> 0)
### 'ip2int' - converts IP address to integer representation
### data link layer
'src_mac' => 'hashentry',
'dst_mac' => 'hashentry',
'intf' => 'intf2int',
### network layer
'src' => 'ip2int',
'dst' => 'ip2int',
'proto' => 'proto2int',
'tos' => 'hashentry',
'ip_opts' => 'hashentry',
'frag_bit' => 'hashentry',
### transport layer
'flags' => 'hashentry',
'tcp_opts' => 'hashentry',
### extra fields for psad internals (DShield reporting, fwsnort
### sid matching, iptables logging prefixes and chains, etc.)
'chain' => 'hashentry',
'log_prefix' => 'hashentry',
'dshield_str' => 'hashentry',
'syslog_host' => 'hashentry',
);
my %gnuplot_non_digit_map = ();
my %ip2int_cache = ();
my %gnuplot_ip2int = ();
my $EXIT_SUCCESS = 0;
my $EXIT_FAILURE = 1;
### packet parsing return values
my $PKT_ERROR = 0;
my $PKT_SUCCESS = 1;
my $PKT_IGNORE = 2;
### icmp header validation
my $BAD_ICMP_TYPE = 1;
my $BAD_ICMP_CODE = 2;
my $SIG_MATCH = 1;
my $NO_SIG_MATCH = 0;
### header lengths
my $TCP_HEADER_LEN = 20; ### excludes options
my $TCP_MAX_OPTS_LEN = 44;
my $UDP_HEADER_LEN = 8;
my $ICMP_HEADER_LEN = 4;
my $IP_HEADER_LEN = 20; ### excludes options
### save a copy of the command line arguments
my @args_cp = @ARGV;
### handle command line args
&getopt_wrapper();
### Everthing after this point must be executed as root (psad
### only needs root if run in auto-blocking mode; should take
### this into account and drop privileges).
&is_root();
### Import all psad configuration and signatures files
### (psad.conf, posf, signatures, psad_icmp_types,
### and auto_dl), and call setup().
&psad_init();
### check to make sure another psad process is not already running.
&unique_pid($config{'PSAD_PID_FILE'});
### get the ip addresses that are local to this machine
&get_local_ips();
### get the current services running on this machine
&get_listening_ports() unless $no_netstat;
### daemonize psad unless running with --no-daemon or an
### analysis mode
unless ($no_daemon or $debug) {
my $pid = fork();
exit 0 if $pid;
die "[*] $0: Couldn't fork: $!" unless defined $pid;
POSIX::setsid() or die "[*] $0: Can't start a new session: $!";
}
### write the current pid associated with psad to the psad pid file
&write_pid($config{'PSAD_PID_FILE'});
### write the command line args used to start psad to $cmdline_file
&write_cmd_line(\@args_cp, $cmdline_file)
unless $debug;
### psad requires that kmsgsd is running to receive any data (unless
### SYSLOG_DAEMON is set to ulogd or psad is configured to acquire data
### from a normal file via IPT_SYSLOG_FILE), so let's start it here for good
### measure (as of 0.9.2 it makes use of the pid files and unique_pid(),
### so we don't have to worry about starting a duplicate copy). While
### we're at it, start psadwatchd as well. Note that this is the best
### place to start the other daemons since we just wrote the psad pid
### to PID_FILE above.
my $cmd = '';
unless ($config{'ENABLE_SYSLOG_FILE'} eq 'Y'
or $no_kmsgsd
or $config{'SYSLOG_DAEMON'} =~ /ulog/i
or $kmsgsd_started) {
$cmd = $cmds{'kmsgsd'};
$cmd .= " -c $config_file";
$cmd .= " -O $override_config_str"
if $override_config_str;
open KMSGSD, "| $cmd" or die "[*] Could not execute $cmds{'kmsgsd'}";
close KMSGSD;
$kmsgsd_started = 1;
}
unless ($kmsgsd_started) {
my $pid = &is_running($pidfiles{'kmsgsd'});
if ($pid) {
kill 9, $pid unless kill 15, $pid;
}
unlink $pidfiles{'kmsgsd'} if -e $pidfiles{'kmsgsd'};
}
unless ($debug or $no_daemon or $config{'ENABLE_PSADWATCHD'} eq 'N') {
$cmd = $cmds{'psadwatchd'};
$cmd .= " -c $config_file";
$cmd .= " -O $override_config_str"
if $override_config_str;
open PSADWATCHD, "| $cmd" or die "[*] Could not ",
"execute $cmds{'psadwatchd'}";
close PSADWATCHD;
}
if ($config{'ENABLE_AUTO_IDS'} eq 'Y') {
### always flush old rules (the subsequent renew_auto_blocked_ips()
### will re-instantiate any that should not have been expired).
&flush_auto_blocked_ips() if $config{'FLUSH_IPT_AT_INIT'} eq 'Y';
### Check to see if psad automatically blocked some IPs from
### a previous run. This feature is most useful for preserving
### auto-block rules for IPs after a reboot or after restarting
### psad. (Note that ENABLE_AUTO_IDS is disabled by psad_init()
### if we are running on a syslog server or if we are running
### in -A mode).
&renew_auto_blocked_ips();
}
### Install signal handlers for debugging %scan with Data::Dumper,
### and for reaping zombie whois processes.
$SIG{'__WARN__'} = \&warn_handler;
$SIG{'__DIE__'} = \&die_handler;
$SIG{'USR1'} = \&usr1_handler;
$SIG{'HUP'} = \&hup_handler;
if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y') {
$last_dshield_alert = time() unless $last_dshield_alert;
}
### Initialize current time for disk space checking.
my $last_disk_check = time();
if ($config{'IMPORT_OLD_SCANS'} eq 'Y') {
### import old scans and counters from /var/log/psad/
&import_old_scans();
} elsif ($config{'ENABLE_SCAN_ARCHIVE'} eq 'Y') {
&archive_data();
} else {
&remove_old_scans();
}
### zero out the packet counter file (the counters
### are all zero at this point anyway unless we
### imported old scans).
&write_global_packet_counters();
### zero out prefix counters
&write_prefix_counters();
### zero out dshield alert stats (note we do this here regardless of
### whether DShield alerting is enabled since if it isn't we will
### just zero out the counters).
&write_dshield_stats();
my $fw_data_file_size = -s $fw_data_file;
my $fw_data_file_inode = (stat($fw_data_file))[1];
my $fw_data_file_check_ctr = 0;
### main firewall packets array
my @fw_packets = ();
### Get an open filehandle for the main firewall data file FW_DATA_FILE.
my $fwdata_fh = &open_fw_data();
&get_auto_response_domain_sock()
if $config{'ENABLE_AUTO_IDS'} eq 'Y';
###=========================================================###
###### MAIN LOOP ######
###=========================================================###
MAIN: for (;;) {
### for --fw-block <ip>
my @add_ipt_addrs = ();
$fwdata_fh = &hup_re_init($fwdata_fh) if $hup_flag;
&usr1_print_scan() if $usr1_flag;
### allow the contents of the fwdata file to be processed only after
### the first loop has been executed.
if ($skip_first_loop) {
$skip_first_loop = 0;
unless ($config{'ENABLE_FW_MSG_READ_CMD'} eq 'Y') {
seek $fwdata_fh,0,SEEK_END; ### seek to the end of the file
}
next MAIN;
} else {
### Get any new packets have been written to syslog
if ($config{'ENABLE_FW_MSG_READ_CMD'} eq 'Y') {
if ($s->can_read(.5)) {
my $syslog_line = <$fwdata_fh>;
$fwdata_fh->flush();
push @fw_packets, $syslog_line unless $fw_msg_read_continue;
$fw_msg_read_do_sleep = 0;
} else {
$fw_msg_read_continue = 0;
$fw_msg_read_do_sleep = 1;
}
} else {
@fw_packets = <$fwdata_fh>;
}
if ($config{'ENABLE_AUTO_IDS'} eq 'Y') {
### get IP from the domain socket
my $ipt_add_connection = $ipt_sock->accept();
if ($ipt_add_connection) {
@add_ipt_addrs = <$ipt_add_connection>;
}
}
}
$fwdata_fh = &fw_data_file_check($fwdata_fh)
unless $config{'ENABLE_FW_MSG_READ_CMD'} eq 'Y';
### sleep for the check interval seconds
if ($config{'ENABLE_FW_MSG_READ_CMD'} eq 'Y') {
if ($fw_msg_read_do_sleep
or (($#fw_packets+1) % $config{'FW_MSG_READ_MIN_PKTS'} == 0)) {
if (@fw_packets) {
### Extract data and summarize scan packets, assign danger
### level, send email/syslog alerts.
&check_scan(\@fw_packets);
}
&post_scan_processing($#fw_packets+1, \@add_ipt_addrs);
@fw_packets = ();
}
unless (&look_for_process(quotemeta($fw_read_cmd))) {
&sys_log("firewall logs read command '$fw_read_cmd' " .
"is not running, restarting");
close $fwdata_fh;
$s->remove($fwdata_fh);
undef $s;
$fwdata_fh = &open_fw_data();
$fw_msg_read_continue = 1;
}
sleep $config{'CHECK_INTERVAL'} if $fw_msg_read_do_sleep;
} else {
if (@fw_packets) {
### Extract data and summarize scan packets, assign danger
### level, send email/syslog alerts.
&check_scan(\@fw_packets);
}
&post_scan_processing($#fw_packets+1, \@add_ipt_addrs);
@fw_packets = ();
### clearerr() on the $fwdata_fh file handle to be ready
### for new packets
$fwdata_fh->clearerr();
sleep $config{'CHECK_INTERVAL'};
}
$check_interval_ctr++;
$fw_data_file_check_ctr++;
} ### MAIN
### for completeness
close $fwdata_fh;
exit 0;
###=========================================================###
###### END MAIN ######
###=========================================================###
#=================== BEGIN SUBROUTINES ========================
sub post_scan_processing() {
my ($num_packets, $add_ipt_addrs_ar) = @_;
### log top scans data
my $do_log = 0;
if ($config{'TOP_SCANS_CTR_THRESHOLD'} == 0) {
$do_log = 1;
} elsif ($check_interval_ctr % $config{'TOP_SCANS_CTR_THRESHOLD'} == 0) {
$do_log = 1;
}
if ($do_log) {
### log the top port and signature matches
&log_top_scans();
}
### Write the number of tcp/udp/icmp packets out
### to the global packet counters file
&write_global_packet_counters();
### Write out log prefix counters
&write_prefix_counters();
if ($config{'ENABLE_AUTO_IDS'} eq 'Y') {
### Timeout any auto-blocked IPs that are past due (need to
### check the timeouts against existing IPs in the scan hash
### even if new packets are not found).
&timeout_auto_blocked_ips();
### see if we need to add any IP addresses from the domain
### socket
&check_ipt_cmd($add_ipt_addrs_ar) if $#$add_ipt_addrs_ar > -1;
}
### Send logs to dshield in dshield format
&dshield_email_log() if $config{'ENABLE_DSHIELD_ALERTS'} eq 'Y';
### Allow disk space utilization checks to be disabled by
### setting DISK_MAX_PERCENTAGE = 0.
if ($config{'DISK_MAX_PERCENTAGE'} > 0
and (time() - $last_disk_check) > $config{'DISK_CHECK_INTERVAL'}) {
$fwdata_fh = &disk_check($fwdata_fh);
}
&check_auto_response_sock()
if $config{'ENABLE_AUTO_IDS'} eq 'Y';
&notifications($num_packets);
### see if we need to timeout any old scans
&scan_timeouts() if $config{'ENABLE_PERSISTENCE'} eq 'N';
return;
}
### Keeps track of scanning ip's, increments packet counters,
### keeps track of tcp flags for each scan, test for snort sid
### values in iptables packets (if fwsnort is being used). This
### is the main detection function.
sub check_scan() {
my $fw_packets_ar = shift;
print STDERR "[+] check_scan()...\n" if $debug;
if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y') {
### calculate the timezone offset
$timezone = sprintf("%.2d", (Timezone())[3]) . ':00';
$year = This_Year();
}
unless ($no_netstat) {
### we don't expect the list of ports the machine is listening
### on to change very often.
if ($netstat_lkup_ctr == 10) {
&get_listening_ports();
$netstat_lkup_ctr = 0;
}
$netstat_lkup_ctr++;
}
### the local machine ip addresses could change (dhcp, etc.)
### but not that often.
if ($local_ips_lkup_ctr == 30) {
&get_local_ips();
$local_ips_lkup_ctr = 0;
}
$local_ips_lkup_ctr++;
my %curr_scan = ();
my %curr_sigs_dl = ();
my %curr_sids_dl = ();
my @err_pkts = ();
my %auto_block_regex_match = ();
my $pkt_ctr = 0;
my $log_scan_ip_pair_max = 0;
my $print_scale_factor = &get_scale_factor($#$fw_packets_ar);
### loop through all of the packet log messages we have just acquired
### from iptables
PKT: for my $pkt_str (@$fw_packets_ar) {
### main packet data structure
my %pkt = %pkt_NF_init;
if ($analyze_mode) {
$pkt_ctr++;
if ($pkt_ctr % $print_scale_factor == 0) {
print "[+] Processed $pkt_ctr packets...\n";
}
}
### main parsing routine for the iptables packet logging message
my $pkt_parse_rv = &parse_NF_pkt_str(\%pkt, $pkt_str);
print STDERR Dumper \%pkt if $debug and $verbose;
if ($pkt_parse_rv == $PKT_ERROR) {
push @err_pkts, $pkt_str unless $no_ipt_errors;
next PKT;
} elsif ($pkt_parse_rv == $PKT_IGNORE) {
next PKT;
}
if ($analyze_mode and $analysis_fields) {
my ($matched_fields_ar, $gnuplot_comment_str)
= &ipt_match_criteria(\%pkt, $analysis_tokens_ar,
$analysis_match_criteria_ar);
next PKT unless $#$matched_fields_ar > -1;
}
$proto_ctrs{$pkt{'proto'}}++;
$protocols{$pkt{'proto'}} = '';
if ($pkt{'proto'} eq 'tcp') {
$top_tcp_ports{$pkt{'dp'}}++;
} elsif ($pkt{'proto'} eq 'udp') {
$top_udp_ports{$pkt{'dp'}}++;
} elsif ($pkt{'proto'} eq 'udplite') {
$top_udplite_ports{$pkt{'dp'}}++;
}
### Mirai scanning phase detection
if ($pkt{'proto'} eq 'tcp' and $pkt{'flags'} =~ /SYN/) {
if ($pkt{'dp'} == 23) {
$mirai_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'dp'}}++;
} elsif ($pkt{'dp'} == 2323) {
if (defined $mirai_scan{$pkt{'src'}}
and defined $mirai_scan{$pkt{'src'}}{$pkt{'dst'}}
and defined $mirai_scan{$pkt{'src'}}{$pkt{'dst'}}{'23'}) {
### if 10 connects to port 23 have also been seen from the
### same source IP, and now we have a connect to port 2323 then
### it is most likely a Mirai botnet scan looking for default
### credentials
if ($mirai_scan{$pkt{'src'}}{$pkt{'dst'}}{'23'} >= 10) {
$pkt{'is_mirai'} = 1;
}
}
}
}
### If we made it here then we correctly matched packets
### that the firewall logged.
print STDERR "[+] valid packet: $pkt{'src'} ($pkt{'sp'}) -> ",
"$pkt{'dst'} ($pkt{'dp'}) $pkt{'proto'}\n" if $debug;
### see if we have hit the MAX_SCAN_IP_PAIRS threshold
if ($config{'MAX_SCAN_IP_PAIRS'} > 0) {
if ($scan_ip_pairs >= $config{'MAX_SCAN_IP_PAIRS'}) {
unless (defined $scan{$pkt{'src'}}
and defined $scan{$pkt{'src'}}{$pkt{'dst'}}) {
print STDERR "[-] excluding $pkt{'src'} -> $pkt{'dst'}, ",
"scan IP pairs too high: $scan_ip_pairs\n"
if $debug;
$log_scan_ip_pair_max = 1;
next PKT;
}
}
if (not defined $scan{$pkt{'src'}}) {
$scan_ip_pairs++;
} elsif (not defined $scan{$pkt{'src'}}{$pkt{'dst'}}) {
$scan_ip_pairs++;
}
}
### track packet counts for this source
$top_packet_counts{$pkt{'src'}}++;
if ($config{'HOME_NET'} ne 'any') {
if ($pkt{'chain'} eq 'INPUT') {
$local_src{$pkt{'dst'}} = '';
} elsif ($pkt{'chain'} eq 'OUTPUT') {
$local_src{$pkt{'src'}} = '';
} elsif ($pkt{'chain'} eq 'FORWARD') {
$local_src{$pkt{'src'}} = ''
if &is_local($pkt{'src'}, $pkt{'s_obj'});
}
}
### initialize the danger level to 0 if it is not already defined
### (note the same source address might have already scanned a
### different destination IP, so the danger level represents the
### aggregate danger level).
unless (defined $scan_dl{$pkt{'src'}}) {
$scan_dl{$pkt{'src'}} = 0;
$scan{$pkt{'src'}}{$pkt{'dst'}}{'alerted'} = 0
if $config{'ALERT_ALL'} eq 'N';
}
### see if we need to assign a danger level according to the auto_dl
### file. The return value is the auto-assigned danger level (or
### -1 if there is no auto-assigned danger level.
unless ($no_auto_dl) {
my $rv = &assign_auto_danger_level(\%pkt);
print STDERR "[+] assign_auto_danger_level() returned: $rv\n"
if $debug;
if ($rv == 0) {
print STDERR "[+] ignoring $pkt{'src'} $pkt{'proto'} ",
"$pkt{'dp'} scan.\n" if $debug;
next PKT;
}
}
if ($pkt{'proto'} eq 'icmp') {
### validate icmp type and code fields against the official values
### in RFC 792. See %inval_type_code for corresponding signature
### message text and danger levels.
my $type_code_rv = &check_icmp_type(
'icmp', \%valid_icmp_types,
$pkt{'itype'}, $pkt{'icode'});
my $update_dl = 0;
if ($type_code_rv == $BAD_ICMP_TYPE) {
$scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp'}
{'invalid_type'}{$pkt{'itype'}}
{$pkt{'chain'}}{'pkts'}++;
$update_dl = 1;
} elsif ($type_code_rv == $BAD_ICMP_CODE) {
$scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp'}
{'invalid_code'}{$pkt{'itype'}}{$pkt{'icode'}}
{$pkt{'chain'}}{'pkts'}++;
$update_dl = 1;
}
if ($update_dl) {
if (defined $scan_dl{$pkt{'src'}}) {
if ($scan_dl{$pkt{'src'}} < 2) {
$scan_dl{$pkt{'src'}} = 2;
}
} else {
$scan_dl{$pkt{'src'}} = 2;
}
}
} elsif ($pkt{'proto'} eq 'icmp6') {
### validate icmp6 type and code fields against the official values
### defined by IANA. See %inval_type_code for corresponding signature
### message text and danger levels.
my $type_code_rv = &check_icmp_type(
'icmp6', \%valid_icmp6_types,
$pkt{'itype'}, $pkt{'icode'});
my $update_dl = 0;
if ($type_code_rv == $BAD_ICMP_TYPE) {
$scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp6'}
{'invalid_type'}{$pkt{'itype'}}
{$pkt{'chain'}}{'pkts'}++;
$update_dl = 1;
} elsif ($type_code_rv == $BAD_ICMP_CODE) {
$scan{$pkt{'src'}}{$pkt{'dst'}}{'icmp6'}
{'invalid_code'}{$pkt{'itype'}}{$pkt{'icode'}}
{$pkt{'chain'}}{'pkts'}++;
$update_dl = 1;
}
if ($update_dl) {
if (defined $scan_dl{$pkt{'src'}}) {
if ($scan_dl{$pkt{'src'}} < 2) {
$scan_dl{$pkt{'src'}} = 2;
}
} else {
$scan_dl{$pkt{'src'}} = 2;
}
}
}
unless ($no_snort_sids) {
if ($pkt{'fwsnort_sid'}) {
### found a fwsnort sid in the packet log message
my ($dl, $is_sig_match) = &add_fwsnort_sid(\%pkt);
if ($dl) {
$curr_sids_dl{$pkt{'src'}} = $dl;
} else {
### a signature matched but is supposed
### to be ignored
next PKT if $is_sig_match == $SIG_MATCH;
}
} else {
### attempt to match any tcp/udp/icmp signatures in the
### main signatures hash
unless ($no_signatures) {
my ($dl, $is_sig_match) = &match_sigs(\%pkt);
print STDERR " match_sigs() returned DL: $dl\n"
if $debug and $verbose;
if ($dl) {
$curr_sigs_dl{$pkt{'src'}} = $dl;
} else {
### a signature matched but is supposed
### to be ignored
next PKT if $is_sig_match == $SIG_MATCH;
}
}
}
}
### note that we send this packet data off to DShield regardless
### of whether psad decides that it is associated with a scan so
### that DShield can make its own determination
if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y'
and not $benchmark
and not $analyze_mode
and $pkt{'dshield_str'}) {
if ($pkt{'timestamp'} =~ /^\s*(\w+)\s+(\d+)\s+(\S+)/) {
my $m_tmp = $1; ### kludge for Decode_Month() call
my $month = Decode_Month($m_tmp);
my $day = sprintf("%.2d", $2);
my $time_24 = $3;
push @dshield_data, "$year-$month-$day $time_24 " .
"$timezone\t$config{'DSHIELD_USER_ID'}\t1" .
"\t$pkt{'dshield_str'}\n";
}
}
### record the absolute starting time of the scan
unless (defined $scan{$pkt{'src'}}{$pkt{'dst'}}{'s_time'}) {
if ($analyze_mode) {
if ($pkt_str =~ /^(.*?)\s+\S+\s+kernel:/) {
$scan{$pkt{'src'}}{$pkt{'dst'}}{'s_time'}
= &date_time($1);
} elsif ($pkt_str =~ /^\s*(\S+\s+\S+\s+\S+)/) {
$scan{$pkt{'src'}}{$pkt{'dst'}}{'s_time'}
= &date_time($1);
} else {
die "[*] Could not extract time from packet: $pkt_str\n",
" Please send a bug report to: ",
"mbr\@cipherdyne.org\n";
}
} else {
$scan{$pkt{'src'}}{$pkt{'dst'}}{'s_time'} = time();
}
}
### increment hash values
$scan{$pkt{'src'}}{$pkt{'dst'}}{'absnum'}++;
$scan{$pkt{'src'}}{$pkt{'dst'}}{'tot_protocols'} = 0
unless defined $scan{$pkt{'src'}}{$pkt{'dst'}}{'tot_protocols'};
$scan{$pkt{'src'}}{$pkt{'dst'}}{'tot_protocols'}++
unless defined $scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}};
$scan{$pkt{'src'}}{$pkt{'dst'}}{'chain'}
{$pkt{'chain'}}{$pkt{'intf'}}{$pkt{'proto'}}++;
### keep track of MAC addresses
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'s_mac'} = $pkt{'src_mac'};
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'d_mac'} = $pkt{'dst_mac'};
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'tot_protocols'} = 0
unless defined $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'tot_protocols'};
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'tot_protocols'}++
unless defined $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}};
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'pkts'}++;
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}
{$pkt{'proto'}}{'flags'}{$pkt{'flags'}}++ if $pkt{'flags'};
### keep track of which syslog daemon reported the message.
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'syslog_host'}
{$pkt{'syslog_host'}} = '' if $pkt{'syslog_host'};
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'is_topera'} = $pkt{'is_topera'};
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'is_masscan'} = $pkt{'is_masscan'};
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{'is_mirai'} = $pkt{'is_mirai'};
if ($pkt{'log_prefix'}) {
### see if the logging prefix matches the blocking
### regex, and if not the IP will not be blocked
if ($config{'ENABLE_AUTO_IDS'} eq 'Y'
and $config{'ENABLE_AUTO_IDS_REGEX'} eq 'Y'
and $config{'AUTO_BLOCK_REGEX'} ne 'NONE') {
### we require a match
if (not defined $auto_block_regex_match{$pkt{'src'}}
and $pkt{'log_prefix'} =~ /$config{'AUTO_BLOCK_REGEX'}/) {
$auto_block_regex_match{$pkt{'src'}} = '';
}
}
} else {
$pkt{'log_prefix'} = '*noprfx*';
}
### keep track of iptables chain and logging prefix
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'chain'}
{$pkt{'chain'}}{$pkt{'log_prefix'}}++;
if ($pkt{'proto'} eq 'tcp' or $pkt{'proto'} eq 'udp'
or $pkt{'proto'} eq 'udplite') {
### initialize the start and end port for the scanned port range
if (not defined $curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'strtp'}) {
### make sure the initial start port is not too low
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'strtp'} = 65535;
### make sure the initial end port is not too high
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'endp'} = 0;
}
unless (defined $scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}
and defined $scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_sp'}) {
### This is the absolute starting port since the
### first packet was detected. Make sure the initial
### start port is not too low
$scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_sp'} = 65535;
### make sure the initial end port is not too high
$scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_ep'} = 0;
}
### see if the destination port lies outside our current range
### and change if needed
($curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'strtp'},
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'endp'}) =
&check_range($pkt{'dp'},
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'strtp'},
$curr_scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'endp'});
($scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_sp'},
$scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_ep'}) =
&check_range($pkt{'dp'},
$scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_sp'},
$scan{$pkt{'src'}}{$pkt{'dst'}}{$pkt{'proto'}}{'abs_ep'});
}
if ($debug and $verbose) {
print STDERR " print Dumper scan hash $pkt{'src'} -> $pkt{'dst'}\n";
print STDERR Dumper $scan{$pkt{'src'}}{$pkt{'dst'}};
}
### attempt to passively guess the remote operating
### system based on the ttl, id, len, window, and tos
### fields in tcp syn packets (this technique is based
### on the paper "Passive OS Fingerprinting: Details
### and Techniques" by Toby Miller). Also attempt to
### fingerprint with a re-implementation of Michal Zalewski's
### p0f that only requires iptables log messages
unless ($no_posf) {
### make sure we have not already guessed the OS,
### and if we have been unsuccessful in guessing
### the OS after 100 packets don't keep trying.
if ($pkt{'proto'} eq 'tcp' and $pkt{'flags'} =~ /SYN/) {
if ($pkt{'tcp_opts'}) { ### got the tcp options portion of the header
### p0f based fingerprinting
&p0f(\%pkt);
} elsif (not defined $posf{$pkt{'src'}}{'guess'}
and $scan{$pkt{'src'}}{$pkt{'dst'}}{'absnum'} < 100) {
&posf(\%pkt);
}
}
}
%pkt = ();
}
### write bogus packets to the error log.
if ($benchmark) {
print scalar localtime(), ' [+] Err packets: ' .
($#err_pkts+1) . ".\n";
} else {
&collect_errors(\@err_pkts) unless $no_ipt_errors;
}
### Assign a danger level to the scan
print "[+] Assigning scan danger levels...\n" if $analyze_mode;
&assign_danger_level(\%curr_scan, \%curr_sigs_dl, \%curr_sids_dl);
my $tot_scan_ips = 0;
if ($analyze_mode) {
for (my $dl=1; $dl <= 5; $dl++) {
my $num_ips = 0;
for my $src (keys %curr_scan) {
$num_ips++ if $scan_dl{$src} == $dl;
}
$tot_scan_ips += $num_ips;
print " Level $dl: $num_ips IP addresses\n";
}
print "\n Tracking $tot_scan_ips total IP addresses\n";
}
### display the scan analysis
&print_scan_status() if $analyze_mode;
### log scan data to the filesystem
&scan_logr(\%curr_scan);
### remember that ENABLE_AUTO_IDS may have been set to 'N' if we
### are running on a syslog server, of if we are running in -A mode.
&auto_psad_response(\%curr_scan, \%auto_block_regex_match)
if $config{'ENABLE_AUTO_IDS'} eq 'Y'
and (not $analyze_mode or $analyze_mode_auto_block);
if ($config{'ENABLE_AUTO_IDS'} eq 'Y' and $analyze_mode_auto_block) {
sleep $config{'AUTO_BLOCK_TIMEOUT'} + 1;
&timeout_auto_blocked_ips();
}
if ($log_scan_ip_pair_max) {
&sys_log("scan IP pairs threshold reached");
}
return;
}
sub parse_NF_pkt_str() {
my ($pkt_hr, $pkt_str) = @_;
my $is_ipv6 = 0;
my $is_tcp = 0;
my $is_udp = 0;
my $is_udplite = 0;
my $is_icmp = 0;
my $is_icmp6 = 0;
my $proto_num = -1;
my $proto_str = '';
my $is_proto_num = 0;
### with ENABLE_SYSLOG_FILE enabled, psad sees all sorts of syslog
### messages that aren't just from iptables (kmsgsd is not running
### to filter them), so require a preliminary match
if ($config{'ENABLE_SYSLOG_FILE'} eq 'Y') {
return $PKT_IGNORE unless $pkt_str =~ /IN=.*OUT=/;
}
print STDERR "\n", $pkt_str if $debug;
$pkt_hr->{'raw'} = $pkt_str if $csv_mode or $gnuplot_mode;
### see if there is a logging prefix (used for scan email alert even
### if we are running with FW_SEARCH_ALL = Y). Note that sometimes
### there is a buffering issue in the kernel ring buffer that is used
### to hold the iptables log message, so we want to get only the
### very last possible candidate for the log prefix (this is why the
### "kernel:" string is preceded by .*).
if ($pkt_str =~ /.*kernel:\s+(.*?)\s*IN=/) {
$pkt_hr->{'log_prefix'} = $1;
$pkt_hr->{'log_prefix'} =~ s|\[\s*\d+\.\d+\s*\]\s*||
if ($config{'IGNORE_KERNEL_TIMESTAMP'} eq 'Y');
if ($pkt_hr->{'log_prefix'} =~ /\S/) {
if ($config{'IGNORE_LOG_PREFIXES'} ne 'NONE') {
return $PKT_IGNORE if $pkt_hr->{'log_prefix'}
=~ m|$config{'IGNORE_LOG_PREFIXES'}|;
}
$ipt_prefixes{$pkt_hr->{'log_prefix'}}++;
}
}
### get the in/out interface and iptables chain (the code below
### allows the iptables log message to contain the PHYSDEV stuff):
### Feb 25 12:13:27 bridge kernel: INBOUND TCP: IN=br0 PHYSIN=eth0 OUT=br0
### PHYSOUT=eth1 SRC=63.147.183.21 DST=11.11.79.100 LEN=48 TOS=0x00
### PREC=0x00 TTL=113 ID=19664 DF PROTO=TCP SPT=4918 DPT=135 WINDOW=64240
### RES=0x00 SYN URGP=0
### Note the lack of whitespace requirement before the IN= interface
### because the logging prefix might not have contained it.
if ($pkt_str =~ /IN=(\S+)\s+PHYSIN=.*?\sOUT=\s/
or $pkt_str =~ /IN=(\S+).*?\sOUT=\s/) {
$pkt_hr->{'intf'} = $1;
$pkt_hr->{'chain'} = 'INPUT';
} elsif ($pkt_str =~ /IN=(\S+)\s+PHYSIN=.*?\sOUT=\S/
or $pkt_str =~ /IN=(\S+).*?\sOUT=\S/) {
$pkt_hr->{'intf'} = $1;
$pkt_hr->{'chain'} = 'FORWARD';
} elsif ($pkt_str =~ /IN=\s+PHYSIN=.*?\sOUT=(\S+)/
or $pkt_str =~ /IN=\s+OUT=(\S+)/) {
$pkt_hr->{'intf'} = $1;
$pkt_hr->{'chain'} = 'OUTPUT';
}
### -I was used on the command line to require a specific interface
if ($cmdl_interface) {
return $PKT_IGNORE unless $pkt_hr->{'intf'} eq $cmdl_interface;
}
if ($pkt_str =~ /\sMAC=(\S+)/) {
my $mac_str = $1;
if ($mac_str =~ /^((?:\w{2}\:){6})((?:\w{2}\:){6})/) {
$pkt_hr->{'dst_mac'} = $1;
$pkt_hr->{'src_mac'} = $2;
}
}
if ($pkt_hr->{'src_mac'}) {
$pkt_hr->{'src_mac'} =~ s/:$//;
print STDERR "[+] src mac addr: $pkt_hr->{'src_mac'}\n" if $debug;
}
if ($pkt_hr->{'dst_mac'}) {
$pkt_hr->{'dst_mac'} =~ s/:$//;
print STDERR "[+] dst mac addr: $pkt_hr->{'dst_mac'}\n" if $debug;
}
unless ($pkt_hr->{'intf'} and $pkt_hr->{'chain'}) {
print STDERR "[-] err packet: could not determine ",
"interface and chain.\n" if $debug;
return $PKT_ERROR;
}
if (%ignore_interfaces) {
for my $ignore_intf (keys %ignore_interfaces) {
return $PKT_IGNORE if $pkt_hr->{'intf'} eq $ignore_intf;
}
}
$pkt_hr->{'syslog_host'} = '';
### get the syslog logging host and timestamp for this packet
if ($config{'ENABLE_CUSTOM_SYSLOG_TS_RE'} eq 'Y') {
if ($pkt_str =~ /$config{'CUSTOM_SYSLOG_TS_RE'}/) {
$pkt_hr->{'timestamp'} = $1;
$pkt_hr->{'syslog_host'} = $2;
}
} else {
if ($pkt_str =~ /^\s*((?:\S+\s+){2}\S+)\s+(\S+)\s+kernel\:/) {
$pkt_hr->{'timestamp'} = $1;
$pkt_hr->{'syslog_host'} = $2;
} elsif ($pkt_str =~ /^\s*([\d\-]+T(?:\d{2}\:){2}\d{2}\S+)\s+(\S+)\s+kernel\:/) {
### 2015-03-08T02:25:11.444012+02:00 servername kernel: ...
$pkt_hr->{'timestamp'} = $1;
$pkt_hr->{'syslog_host'} = $2;
}
}
unless ($pkt_hr->{'syslog_host'}) {
$pkt_hr->{'timestamp'} = localtime();
$pkt_hr->{'syslog_host'} = 'unknown';
}
if ($debug) {
print "[+] timestamp: $pkt_hr->{'timestamp'}\n",
" syslog hostname: $pkt_hr->{'syslog_host'}\n";
}
### try to extract a snort sid (generated by fwsnort) from
### the packet
unless ($no_snort_sids) {
if ($pkt_hr->{'log_prefix'}) {
if ($pkt_hr->{'log_prefix'} =~ /$config{'SNORT_SID_STR'}(\d+)/) {
$pkt_hr->{'fwsnort_sid'} = $1;
### try to extract the fwsnort rule number (must be
### fwsnort-0.9.0 or greater)
if ($pkt_hr->{'log_prefix'} =~ /\[(\d+)\]/) {
$pkt_hr->{'fwsnort_rnum'} = $1;
}
if ($pkt_hr->{'log_prefix'} =~ /ESTAB/) {
$pkt_hr->{'fwsnort_estab'} = 1;
}
}
}
}
unless ($pkt_hr->{'fwsnort_sid'} or $config{'FW_SEARCH_ALL'} eq 'Y') {
### note that this is not _too_ strict since people
### have different ways of writing --log-prefix strings
my $matched = 0;
for my $fw_search_str (@fw_search) {
$matched = 1 if $pkt_str =~ /$fw_search_str/;
}
return $PKT_IGNORE unless $matched;
}
### test for IPv6
if ($pkt_str =~ /HOPLIMIT=\d+\s+FLOWLBL=\d+/) {
return $PKT_IGNORE unless $config{'ENABLE_IPV6_DETECTION'} eq 'Y';
$is_ipv6 = 1;
$pkt_hr->{'is_ipv6'} = 1;
}
### test for IPv4 "don't fragment" bit
unless ($is_ipv6) {
$pkt_hr->{'frag_bit'} = 1 if $pkt_str =~ /\sDF\s+PROTO/;
}
### get IP options if --log-ip-options is used - they appear before the
### PROTO= field for either IPv4 or IPv6 packets
if ($pkt_str =~ /OPT\s+\((\S+)\)\s+PROTO=/) {
$pkt_hr->{'ip_opts'} = $1;
}
if ($is_ipv6 and $pkt_str =~ /PROTO=ICMPv6\s/) {
### test for ICMP before TCP or UDP because ICMP destination
### unreachable messages can contain embedded TCP/UDP specifics like
### so:
### Jul 21 19:07:39 minastirith kernel: [1912155.755921] IPv6 Packet
### IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:86:dd
### SRC=0000:0000:0000:0000:0000:0000:0000:0001
### DST=0000:0000:0000:0000:0000:0000:0000:0001 LEN=107 TC=0
### HOPLIMIT=64 FLOWLBL=0 PROTO=ICMPv6 TYPE=1 CODE=4
### [SRC=0000:0000:0000:0000:0000:0000:0000:0001
### DST=0000:0000:0000:0000:0000:0000:0000:0001 LEN=59 TC=0
### HOPLIMIT=64 FLOWLBL=0 PROTO=UDP SPT=35186 DPT=12345 LEN=19 ]
$is_icmp6 = 1;
$proto_str = 'icmp6'
} elsif ($pkt_str =~ /PROTO=ICMP\s/) {
### test for ICMP before TCP or UDP because ICMP destination
### unreachable messages can contain embedded TCP/UDP specifics like
### so:
### Sep 8 18:04:26 minastirith kernel: [28241.572876] IN_DROP
### IN=wlan0 OUT= MAC=00:1a:9f:91:df:ae:00:12:27:12:0a:a0:12:00
### SRC=10.0.0.138 DST=192.168.1.103 LEN=96 TOS=0x00 PREC=0xC0
### TTL=254 ID=63642 PROTO=ICMP TYPE=3 CODE=3 [SRC=192.168.1.103
### DST=10.0.0.138 LEN=68 TOS=0x00 PREC=0x00 TTL=0 ID=22458 PROTO=UDP
### SPT=35080 DPT=33434 LEN=48 ]
$is_icmp = 1;
$proto_str = 'icmp';
} elsif ($pkt_str =~ /PROTO=TCP\s/) {
$is_tcp = 1;
$proto_str = 'tcp';
} elsif ($pkt_str =~ /PROTO=UDPLITE\s/) {
$is_udplite = 1;
$proto_str = 'udplite';
} elsif ($pkt_str =~ /PROTO=UDP\s/) {
$is_udp = 1;
$proto_str = 'udp';
} elsif ($pkt_str =~ /PROTO=(\d+)\s/) {
$proto_num = $1;
$is_proto_num = 1;
} else {
print STDERR "[-] err packet: unrecognized protocol\n" if $debug;
return $PKT_ERROR;
}
### see if we need to ignore this packet based on the
### IGNORE_PROTOCOLS config keyword.
return $PKT_IGNORE if &check_ignore_proto($proto_str,
$proto_num, $is_proto_num);
if ($is_tcp) {
if ($is_ipv6) {
### Jul 18 19:19:08 lorien kernel: [ 1835.131574] IPV6 packet IN=lo
### OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:86:dd
### SRC=0000:0000:0000:0000:0000:0000:0000:0001
### DST=0000:0000:0000:0000:0000:0000:0000:0001 LEN=72 TC=0
### HOPLIMIT=255 FLOWLBL=0 PROTO=TCP SPT=22 DPT=57005 WINDOW=512
### RES=0x00 ACK FIN URGP=0
### Topera SYN scan with --log-ip-options turned on:
### Dec 20 20:10:51 rohan kernel: [ 499.178765] DROP IN=eth0 OUT=
### MAC=00:1b:f9:76:9c:e4:0a:13:33:3a:33:36:86:ed
### SRC=2012:1234:1234:0000:0000:0000:0000:0001
### DST=2012:1234:1234:0000:0000:0000:0000:0002 LEN=132 TC=0 HOPLIMIT=64
### FLOWLBL=0 OPT ( ) OPT ( ) OPT ( ) OPT ( ) OPT ( ) OPT ( ) OPT ( )
### OPT ( ) OPT ( ) PROTO=TCP SPT=26732 DPT=78 WINDOW=8192 RES=0x00 SYN URGP=0
if ($pkt_str =~ /SRC=($ipv6_re)\s+DST=($ipv6_re)\s+LEN=(\d+)\s+
TC=(\d+)\s+HOPLIMIT=(\d+)\s+FLOWLBL=(\d+)\s+.*PROTO=TCP\s+
SPT=(\d+)\s+DPT=(\d+)\s+WINDOW=(\d+)\s+
(.*)\s+URGP=/x) {
($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
$pkt_hr->{'tc'}, $pkt_hr->{'hop_limit'},
$pkt_hr->{'flow_label'}, $pkt_hr->{'sp'}, $pkt_hr->{'dp'},
$pkt_hr->{'win'}, $pkt_hr->{'flags'})
= ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);
$pkt_hr->{'s_obj'} = new6 NetAddr::IP($pkt_hr->{'src'})
or return $PKT_ERROR;
$pkt_hr->{'d_obj'} = new6 NetAddr::IP($pkt_hr->{'dst'})
or return $PKT_ERROR;
if ($pkt_str =~ /(?:OPT\s+\(\s+\)\s+){7,}/) {
$pkt_hr->{'is_topera'} = 1;
print STDERR " Topera IPv6 scan\n"
if $debug;
}
} else {
print STDERR "[-] err packet: strange IPv6 TCP format\n"
if $debug;
return $PKT_ERROR;
}
} else {
### May 18 22:21:26 orthanc kernel: DROP IN=eth2 OUT=
### MAC=00:60:1d:23:d0:01:00:60:1d:23:d3:0e:08:00 SRC=192.168.20.25
### DST=192.168.20.1 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=47300 DF
### PROTO=TCP SPT=34111 DPT=6345 WINDOW=5840 RES=0x00 SYN URGP=0
if ($pkt_str =~ /SRC=($ipv4_re)\s+DST=($ipv4_re)\s+LEN=(\d+)\s+TOS=(\S+)
\s*.*\s+TTL=(\d+)\s+ID=(\d+)\s*.*\s+PROTO=TCP\s+
SPT=(\d+)\s+DPT=(\d+)\s.*\s*WINDOW=(\d+)\s+
(.*)\s+URGP=/x) {
($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
$pkt_hr->{'tos'}, $pkt_hr->{'ttl'}, $pkt_hr->{'ip_id'},
$pkt_hr->{'sp'}, $pkt_hr->{'dp'}, $pkt_hr->{'win'},
$pkt_hr->{'flags'})
= ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);
$pkt_hr->{'s_obj'} = new NetAddr::IP($pkt_hr->{'src'})
or return $PKT_ERROR;
$pkt_hr->{'d_obj'} = new NetAddr::IP($pkt_hr->{'dst'})
or return $PKT_ERROR;
} else {
print STDERR "[-] err packet: strange IPv4 TCP format\n"
if $debug;
return $PKT_ERROR;
}
}
$pkt_hr->{'proto'} = 'tcp';
### the reserve bits are not reported by ulogd, but normal
### iptables syslog messages contain them.
$pkt_hr->{'flags'} =~ s/\s*RES=\S+\s*//;
### default to NULL
$pkt_hr->{'flags'} = 'NULL' unless $pkt_hr->{'flags'};
if (not $pkt_hr->{'fwsnort_sid'}
and $config{'IGNORE_CONNTRACK_BUG_PKTS'} eq 'Y' &&
($pkt_hr->{'flags'} =~ /ACK/ || $pkt_hr->{'flags'} =~ /RST/)) {
### $dp > 1024 && ($pkt_hr->{'flags'} =~ /ACK/ ||
### FIXME: ignore TCP packets that have the ACK or RST
### bits set (unless we matched a snort sid) since
### _usually_ we see these packets as a result of the
### iptables connection tracking bug. Also, note that
### no signatures make use of the RST flag.
print STDERR "[-] err packet: matched ACK or RST flag.\n"
if $debug;
return $PKT_IGNORE;
}
### per page 595 of the Camel book, "if /blah1|blah2/"
### can be slower than "if /blah1/ || /blah2/
unless ($pkt_hr->{'flags'} !~ /WIN/ &&
$pkt_hr->{'flags'} =~ /ACK/ ||
$pkt_hr->{'flags'} =~ /SYN/ ||
$pkt_hr->{'flags'} =~ /RST/ ||
$pkt_hr->{'flags'} =~ /URG/ ||
$pkt_hr->{'flags'} =~ /PSH/ ||
$pkt_hr->{'flags'} =~ /FIN/ ||
$pkt_hr->{'flags'} eq 'NULL') {
print STDERR "[-] err packet: bad tcp flags.\n" if $debug;
return $PKT_ERROR;
}
### look for TCP options, but don't pickup IP options if
### also --log-ip-options is used (IP options appear before
### the PROTO= field).
if ($pkt_str =~ /URGP=\S+\s+OPT\s+\((\S+)\)/) {
$pkt_hr->{'tcp_opts'} = $1;
$found_one_tcp_options_field = 1
if $config{'EXPECT_TCP_OPTIONS'} eq 'Y';
}
if ($pkt_hr->{'flags'}
and $pkt_hr->{'flags'} eq 'SYN'
and $found_one_tcp_options_field
and not $pkt_hr->{'tcp_opts'}) {
$pkt_hr->{'is_masscan'} = 1;
print STDERR " Masscan SYN scan\n"
if $debug;
}
if ($pkt_str =~ /\sSEQ=(\d+)\s+ACK=(\d+)/) {
$pkt_hr->{'tcp_seq'} = $1;
$pkt_hr->{'tcp_ack'} = $2;
}
### see if we need to ignore this packet based on the
### IGNORE_PORTS config keyword
return $PKT_IGNORE if &check_ignore_port($pkt_hr->{'dp'},
$pkt_hr->{'proto'});
if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y'
and not $benchmark
and not $analyze_mode) {
my $dflags = $pkt_hr->{'flags'};
$dflags =~ s/\s/,/g;
$pkt_hr->{'dshield_str'} = "$pkt_hr->{'src'}\t$pkt_hr->{'sp'}\t" .
"$pkt_hr->{'dst'}\t$pkt_hr->{'dp'}\t$pkt_hr->{'proto'}\t" .
"$dflags";
}
} elsif ($is_udp or $is_udplite) {
if ($is_udplite) {
$pkt_hr->{'proto'} = 'udplite';
} else {
$pkt_hr->{'proto'} = 'udp';
}
if ($is_ipv6) {
### Jul 21 21:07:39 minastirith kernel: [1912155.755862] IPv6 Packet IN=lo
### OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:86:dd
### SRC=0000:0000:0000:0000:0000:0000:0000:0001
### DST=0000:0000:0000:0000:0000:0000:0000:0001 LEN=59 TC=0 HOPLIMIT=64
### FLOWLBL=0 PROTO=UDP SPT=35186 DPT=12345 LEN=19
if ($pkt_str =~ /SRC=($ipv6_re)\s+DST=($ipv6_re)\s+LEN=(\d+)\s+
TC=(\d+)\s+HOPLIMIT=(\d+)\s+FLOWLBL=(\d+)\s+
PROTO=UDP(?:LITE)?\s+SPT=(\d+)\s+DPT=(\d+)\s+LEN=(\d+)/x) {
($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
$pkt_hr->{'tc'}, $pkt_hr->{'hop_limit'},
$pkt_hr->{'flow_label'}, $pkt_hr->{'sp'}, $pkt_hr->{'dp'},
$pkt_hr->{'udp_len'}) = ($1,$2,$3,$4,$5,$6,$7,$8,$9);
$pkt_hr->{'s_obj'} = new6 NetAddr::IP($pkt_hr->{'src'})
or return $PKT_ERROR;
$pkt_hr->{'d_obj'} = new6 NetAddr::IP($pkt_hr->{'dst'})
or return $PKT_ERROR;
} else {
print STDERR "[-] err packet: strange IPv6 UDP format\n"
if $debug;
return $PKT_ERROR;
}
} else {
### May 18 22:21:26 orthanc kernel: DROP IN=eth2 OUT=
### MAC=00:60:1d:23:d0:01:00:60:1d:23:d3:0e:08:00
### SRC=192.168.20.25 DST=192.168.20.1 LEN=28 TOS=0x00 PREC=0x00
### TTL=40 ID=47523 PROTO=UDP SPT=57339 DPT=305 LEN=8
if ($pkt_str =~ /SRC=($ipv4_re)\s+DST=($ipv4_re)\s+LEN=(\d+)\s+TOS=(\S+)
\s.*TTL=(\d+)\s+ID=(\d+)\s*.*\s+PROTO=UDP\s+
SPT=(\d+)\s+DPT=(\d+)\s+LEN=(\d+)/x) {
($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
$pkt_hr->{'tos'}, $pkt_hr->{'ttl'}, $pkt_hr->{'ip_id'},
$pkt_hr->{'sp'}, $pkt_hr->{'dp'}, $pkt_hr->{'udp_len'})
= ($1,$2,$3,$4,$5,$6,$7,$8,$9);
$pkt_hr->{'s_obj'} = new NetAddr::IP($pkt_hr->{'src'})
or return $PKT_ERROR;
$pkt_hr->{'d_obj'} = new NetAddr::IP($pkt_hr->{'dst'})
or return $PKT_ERROR;
} else {
print STDERR "[-] err packet: strange IPv4 UDP format\n"
if $debug;
return $PKT_ERROR;
}
}
### see if we need to ignore this packet based on the
### IGNORE_PORTS config keyword
return $PKT_IGNORE if &check_ignore_port($pkt_hr->{'dp'},
$pkt_hr->{'proto'});
if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y'
and not $benchmark
and not $analyze_mode) {
$pkt_hr->{'dshield_str'} = "$pkt_hr->{'src'}\t$pkt_hr->{'sp'}\t" .
"$pkt_hr->{'dst'}\t$pkt_hr->{'dp'}\t$pkt_hr->{'proto'}";
}
} elsif ($is_icmp6 or $is_icmp) {
$pkt_hr->{'sp'} = $pkt_hr->{'dp'} = 0;
if ($is_ipv6) {
### Jul 18 17:18:19 lorien kernel: [ 1786.520508] IPV6 packet IN=lo
### OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:86:dd
### SRC=0000:0000:0000:0000:0000:0000:0000:0001
### DST=0000:0000:0000:0000:0000:0000:0000:0001 LEN=104 TC=0
### HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=129 CODE=0 ID=14997 SEQ=1
if ($pkt_str =~ /SRC=($ipv6_re)\s+DST=($ipv6_re)\s+LEN=(\d+)\s+TC=(\d+)\s+
HOPLIMIT=(\d+)\s+FLOWLBL=(\d+)\s+PROTO=ICMPv6\s+TYPE=(\d+)\s+
CODE=(\d+)/x) {
($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
$pkt_hr->{'tc'}, $pkt_hr->{'hop_limit'},
$pkt_hr->{'flow_label'}, $pkt_hr->{'itype'},
$pkt_hr->{'icode'}) = ($1,$2,$3,$4,$5,$6,$7,$8);
$pkt_hr->{'s_obj'} = new6 NetAddr::IP($pkt_hr->{'src'})
or return $PKT_ERROR;
$pkt_hr->{'d_obj'} = new6 NetAddr::IP($pkt_hr->{'dst'})
or return $PKT_ERROR;
} else {
print STDERR "[-] err packet: strange IPv6 ICMP format\n"
if $debug;
return $PKT_ERROR;
}
$pkt_hr->{'proto'} = 'icmp6';
} else {
### Nov 27 15:45:51 orthanc kernel: DROP IN=eth1 OUT= MAC=00:a0:cc:e2:1f:f2:00:
### 20:78:10:70:e7:08:00 SRC=192.168.10.20 DST=192.168.10.1 LEN=84 TOS=0x00
### PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=61055 SEQ=256
if ($pkt_str =~ /SRC=($ipv4_re)\s+DST=($ipv4_re)\s+LEN=(\d+).*
TTL=(\d+)\s+ID=(\d+).*PROTO=ICMP\s+TYPE=(\d+)\s+
CODE=(\d+)/x) {
($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
$pkt_hr->{'ttl'}, $pkt_hr->{'ip_id'}, $pkt_hr->{'itype'},
$pkt_hr->{'icode'}) = ($1,$2,$3,$4,$5,$6,$7);
$pkt_hr->{'s_obj'} = new NetAddr::IP($pkt_hr->{'src'})
or return $PKT_ERROR;
$pkt_hr->{'d_obj'} = new NetAddr::IP($pkt_hr->{'dst'})
or return $PKT_ERROR;
} else {
print STDERR "[-] err packet: strange IPv4 ICMP format\n"
if $debug;
return $PKT_ERROR;
}
$pkt_hr->{'proto'} = 'icmp';
if ($pkt_hr->{'itype'} == $ICMP_ECHO_REQUEST
or $pkt_hr->{'itype'} == $ICMP_ECHO_REPLY) {
### we expect the ICMP ID and SEQ fields to be populated
if ($pkt_str =~ /CODE=(\d+)\s+ID=(\d+)\s+SEQ=(\d+)/) {
$pkt_hr->{'icmp_id'} = $1;
$pkt_hr->{'icmp_seq'} = $2;
} else {
return $PKT_ERROR;
}
}
}
if ($config{'ENABLE_DSHIELD_ALERTS'} eq 'Y'
and not $benchmark
and not $analyze_mode) {
$pkt_hr->{'dshield_str'} = "$pkt_hr->{'src'}\t$pkt_hr->{'itype'}" .
"\t$pkt_hr->{'dst'}\t$pkt_hr->{'icode'}\t$pkt_hr->{'proto'}";
}
} elsif ($is_proto_num) {
if ($is_ipv6) {
### FIXME future
} else {
### Mar 15 20:59:04 linux kernel: [810523.812773] DROP IN=eth0 OUT=
### MAC=23:87:fc:c6:24:58:00:21:3f:98:99:78:09:00 SRC=192.168.10.55
### DST=192.168.10.1 LEN=20 TOS=0x00 PREC=0x00 TTL=59 ID=3052 PROTO=29
if ($pkt_str =~ /SRC=($ipv4_re)\s+DST=($ipv4_re)\s+LEN=(\d+).*
TTL=(\d+)\s+ID=(\d+).*PROTO=/x) {
($pkt_hr->{'src'}, $pkt_hr->{'dst'}, $pkt_hr->{'ip_len'},
$pkt_hr->{'ttl'}, $pkt_hr->{'ip_id'}) = ($1,$2,$3,$4,$5);
$pkt_hr->{'s_obj'} = new NetAddr::IP($pkt_hr->{'src'})
or return $PKT_ERROR;
$pkt_hr->{'d_obj'} = new NetAddr::IP($pkt_hr->{'dst'})
or return $PKT_ERROR;
}
$pkt_hr->{'proto'} = $proto_num;
}
} else {
### Sometimes the iptables log entry gets messed up due to
### buffering issues so we write it to the error log.
print STDERR "[-] err packet: no regex match.\n" if $debug;
return $PKT_ERROR;
}
if ($restrict_ip) {
### we are looking to analyze packets from a specific IP/subnet
if ($pkt_hr->{'is_ipv6'}) {
if ($restrict_ip->version() == 6) {
return $PKT_IGNORE unless
$pkt_hr->{'s_obj'}->within($restrict_ip) or
$pkt_hr->{'d_obj'}->within($restrict_ip);
}
} else {
if ($restrict_ip->version() == 4) {
return $PKT_IGNORE unless
$pkt_hr->{'s_obj'}->within($restrict_ip) or
$pkt_hr->{'d_obj'}->within($restrict_ip);
}
}
}
return $PKT_SUCCESS;
}
sub check_ignore_proto() {
my ($proto_str, $proto_num, $is_proto_num) = @_;
return 0 unless %ignore_protocols;
if ($is_proto_num) {
return 1 if defined $ignore_protocols{$proto_num};
} else {
return 1 if defined $ignore_protocols{$proto_str};
}
return 0;
}
sub match_sigs() {
my $pkt_hr = shift;
my $dl = 0;
my $is_sig_match = $NO_SIG_MATCH;
print STDERR "[+] match_sigs()\n" if $debug and $verbose;
### always run the IP protocol sigs
PROTO: for my $proto ($pkt_hr->{'proto'}, 'ip') {
next PROTO unless defined $sig_search{$proto};
SRC: for my $src (keys %{$sig_search{$proto}}) {
print STDERR "[+] match_sigs() pkt src: $pkt_hr->{'src'} within sig src: $src ?..."
if $debug and $verbose;
if ($pkt_hr->{'s_obj'}->within($sig_ip_objs{$src})) {
print STDERR "yes\n"
if $debug and $verbose;
} else {
print STDERR "no\n"
if $debug and $verbose;
next SRC;
}
DST: for my $dst (keys %{$sig_search{$proto}{$src}}) {
print STDERR "[+] match_sigs() pkt dst: $pkt_hr->{'dst'} within sig dst: $dst ?..."
if $debug and $verbose;
if ($pkt_hr->{'d_obj'}->within($sig_ip_objs{$dst})) {
print STDERR "yes\n"
if $debug and $verbose;
} else {
print STDERR "no\n"
if $debug and $verbose;
next DST;
}
print STDERR " Matched sig IP criteria.\n"
if $debug and $verbose;
if ($proto eq 'tcp' or $proto eq 'udp' or $proto eq 'udplite') {
TYPE: for my $hr (@port_types) {
my $sp_type = $hr->{'sp'};
my $dp_type = $hr->{'dp'};
next TYPE unless
defined $sig_search{$proto}{$src}{$dst}{$sp_type};
my $sp_hr = $sig_search{$proto}{$src}{$dst}{$sp_type};
SP_S: for my $sp_s (keys %{$sp_hr}) {
if ($sp_type eq 'norm') {
### normal match on the starting port value
next SP_S unless $pkt_hr->{'sp'} >= $sp_s;
}
SP_E: for my $sp_e (keys %{$sp_hr->{$sp_s}}) {
if ($sp_type eq 'norm') {
### normal match on the ending port value
next SP_E unless $pkt_hr->{'sp'} <= $sp_e;
} else {
### negative match on the ending port value
### (note the "or" condition)
next SP_E unless ($pkt_hr->{'sp'} > $sp_e
or $pkt_hr->{'sp'} < $sp_s);
}
next TYPE unless defined
$sp_hr->{$sp_s}->{$sp_e}->{$dp_type};
my $dp_hr = $sp_hr->{$sp_s}->{$sp_e}->{$dp_type};
DP_S: for my $dp_s (keys %$dp_hr) {
if ($dp_type eq 'norm') {
next DP_S unless $pkt_hr->{'dp'} >= $dp_s;
}
DP_E: for my $dp_e (keys %{$dp_hr->{$dp_s}}) {
if ($dp_type eq 'norm') {
next DP_E unless $pkt_hr->{'dp'} <= $dp_e;
} else {
### negative match on the ending port value
### (note the "or" condition)
next DP_E unless ($pkt_hr->{'dp'} > $dp_e
or $pkt_hr->{'dp'} < $dp_s);
}
### now we have the set of applicable
### signatures that match the sip/dip
### and sp/dp, so match any Snort
### keywords
my ($dl_tmp, $sig_match_tmp) =
&match_snort_keywords($pkt_hr,
$dp_hr->{$dp_s}->{$dp_e});
print STDERR " match_snort_keywords() ",
" return DL: $dl_tmp\n" if $debug;
### return maximal danger level from all
### signature matches
$dl = $dl_tmp if $dl_tmp > $dl;
$is_sig_match = $SIG_MATCH
if $sig_match_tmp == $SIG_MATCH;
}
}
}
}
}
} else {
### now we have the set of applicable icmp
### signatures that match the sip/dip
my ($dl_tmp, $sig_match_tmp) = &match_snort_keywords(
$pkt_hr, $sig_search{$proto}{$src}{$dst});
print STDERR " match_snort_keywords() ",
" return DL: $dl_tmp\n" if $debug;
### return maximal danger level from all signature matches
$dl = $dl_tmp if $dl_tmp > $dl;
$is_sig_match = $SIG_MATCH if $sig_match_tmp == $SIG_MATCH;
}
}
}
}
return $dl, $is_sig_match;
}
sub match_snort_keywords() {
my ($pkt_hr, $sigs_ids_hr) = @_;
print STDERR "[+] match_snort_keywords()\n" if $debug;
my $dl = 0;
my $matched_sig = $NO_SIG_MATCH;
### see if all Snort keywords match the packet - the sigs hash has
### been restricted to the appropriate protocol
SIG: for my $sid (keys %$sigs_ids_hr) {
next SIG unless defined $sigs{$sid}; ### should never happen
my $sig_hr = $sigs{$sid};
### iptables logging messages always include TTL and IP ID
### values (at least for ipv4, see
### linux/net/ipv4/netfilter/ipt_LOG.c)
my $dl_tmp = 0;
my ($rv, $sig_match_rv) = &match_snort_ip_keywords($pkt_hr, $sig_hr);
if ($sig_match_rv == $SIG_MATCH) {
$matched_sig = $SIG_MATCH;
$dl_tmp = $rv;
if ($rv == 0) {
next SIG; ### ignore signature
}
} elsif ($sig_match_rv == $NO_SIG_MATCH) {
### there were network-layer keywords that did not match
next SIG unless $rv;
### else there were no network-layer keywords so continue on
}
$dl = $dl_tmp if $dl_tmp > $dl;
if ($debug and $debug_sid == $sid) {
print STDERR "[+] SID: $sid, passed match_snort_ip_keywords() ",
"tests.\n";
}
if ($sig_hr->{'proto'} eq 'tcp') {
($rv, $sig_match_rv) = &match_snort_tcp_keywords($pkt_hr, $sig_hr);
if ($sig_match_rv == $SIG_MATCH) {
$matched_sig = $SIG_MATCH;
next SIG if $rv == 0;
$dl = $rv if $rv > $dl;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'} = 0
unless defined $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'}++
unless defined $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'dp'}
= $pkt_hr->{'dp'};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'flags'}
= $pkt_hr->{'flags'};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tcp'}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'time'} = time();
$sig_sources{$sid}{$pkt_hr->{'src'}} = '';
$top_sigs{$sid}++;
$top_sig_counts{$pkt_hr->{'src'}}++;
}
} elsif ($sig_hr->{'proto'} eq 'udp') {
($rv, $sig_match_rv) = &match_snort_udp_keywords($pkt_hr, $sig_hr);
if ($sig_match_rv == $SIG_MATCH) {
$matched_sig = $SIG_MATCH;
next SIG if $rv == 0;
$dl = $rv if $rv > $dl;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'} = 0
unless defined $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'}++
unless defined $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'dp'}
= $pkt_hr->{'dp'};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'udp'}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'time'} = time();
$sig_sources{$sid}{$pkt_hr->{'src'}} = 0; ### not fwsnort
$top_sigs{$sid}++;
$top_sig_counts{$pkt_hr->{'src'}}++;
}
} elsif ($sig_hr->{'proto'} eq 'icmp') {
($rv, $sig_match_rv) = &match_snort_icmp_keywords($pkt_hr, $sig_hr);
if ($sig_match_rv == $SIG_MATCH) {
$matched_sig = $SIG_MATCH;
next SIG if $rv == 0;
$dl = $rv if $rv > $dl;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'} = 0
unless defined $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'}++
unless defined $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'icmp'};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'icmp'}{'sid'}
{$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'icmp'}{'sid'}
{$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'icmp'}{'sid'}
{$sid}{$pkt_hr->{'chain'}}{'time'} = time();
$sig_sources{$sid}{$pkt_hr->{'src'}} = 0; ### not fwsnort
$top_sigs{$sid}++;
$top_sig_counts{$pkt_hr->{'src'}}++;
}
} elsif ($sig_hr->{'proto'} eq 'icmp6') {
### FIXME icmp6 specifc Snort matches?
}
}
return $dl, $matched_sig;
}
sub match_snort_tcp_keywords() {
my ($pkt_hr, $sig_hr) = @_;
if ($debug and $debug_sid == $sig_hr->{'sid'}) {
print STDERR "[+] SID: $sig_hr->{'sid'} match_snort_tcp_keywords()\n";
}
if (defined $sig_hr->{'flags'}) {
unless ($pkt_hr->{'flags'} eq $sig_hr->{'flags'}) {
if ($debug and $debug_sid == $sig_hr->{'sid'}) {
print STDERR "[-] SID: $sig_hr->{'sid'} ",
"$pkt_hr->{'flags'} != $sig_hr->{'flags'}\n";
}
return 0, $NO_SIG_MATCH;
}
}
my $header_len = $IP_HEADER_LEN + $TCP_HEADER_LEN;
if ($pkt_hr->{'flags'} =~ m|SYN|) {
### extend the header length to compensate for TCP options
$header_len += $TCP_MAX_OPTS_LEN;
}
if (defined $sig_hr->{'dsize'} and defined $sig_hr->{'dsize_s'}) {
return 0, $NO_SIG_MATCH unless &check_sig_int_range(
($pkt_hr->{'ip_len'}-$header_len), 'dsize', $sig_hr);
}
if (defined $sig_hr->{'psad_dsize'} and defined $sig_hr->{'psad_dsize_s'}) {
return 0, $NO_SIG_MATCH unless &check_sig_int_range(
($pkt_hr->{'ip_len'}-$header_len), 'psad_dsize', $sig_hr);
}
if (defined $sig_hr->{'window'} and defined $sig_hr->{'window_s'}) {
return 0, $NO_SIG_MATCH
unless &check_sig_int_range($pkt_hr->{'win'}, 'window', $sig_hr);
}
if (defined $sig_hr->{'seq'} and defined $sig_hr->{'seq_s'}) {
return 0, $NO_SIG_MATCH
unless &check_sig_int_range($pkt_hr->{'tcp_seq'}, 'seq', $sig_hr);
}
if (defined $sig_hr->{'ack'} and defined $sig_hr->{'ack_s'}) {
return 0, $NO_SIG_MATCH
unless &check_sig_int_range($pkt_hr->{'tcp_ack'}, 'ack', $sig_hr);
}
### matched the signature
if ($debug) {
print STDERR "[+] packet matched tcp keywords for sid: ",
"$sig_hr->{'sid'} (psad_id: $sig_hr->{'psad_id'})\n",
qq| "$sig_hr->{'msg'}"\n|;
}
return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
}
sub match_snort_udp_keywords() {
my ($pkt_hr, $sig_hr) = @_;
if (defined $sig_hr->{'dsize'} and defined $sig_hr->{'dsize_s'}) {
return 0, $NO_SIG_MATCH unless &check_sig_int_range(
($pkt_hr->{'udp_len'}-$UDP_HEADER_LEN),
'dsize', $sig_hr);
}
if (defined $sig_hr->{'psad_dsize'} and defined $sig_hr->{'psad_dsize_s'}) {
return 0, $NO_SIG_MATCH unless &check_sig_int_range(
($pkt_hr->{'udp_len'}-$UDP_HEADER_LEN),
'psad_dsize', $sig_hr);
}
### matched the signature
if ($debug) {
print STDERR "[+] packet matched udp keywords for sid: ",
"$sig_hr->{'sid'} (psad_id: $sig_hr->{'psad_id'})\n",
qq| "$sig_hr->{'msg'}"\n|;
}
return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
}
sub match_snort_icmp_keywords() {
my ($pkt_hr, $sig_hr) = @_;
if (defined $sig_hr->{'dsize'} and defined $sig_hr->{'dsize_s'}) {
return 0, $NO_SIG_MATCH unless &check_sig_int_range(
($pkt_hr->{'ip_len'}-$IP_HEADER_LEN-$ICMP_HEADER_LEN),
'dsize', $sig_hr);
}
if (defined $sig_hr->{'psad_dsize'} and defined $sig_hr->{'psad_dsize_s'}) {
return 0, $NO_SIG_MATCH unless &check_sig_int_range(
($pkt_hr->{'ip_len'}-$IP_HEADER_LEN-$ICMP_HEADER_LEN),
'psad_dsize', $sig_hr);
}
if (defined $sig_hr->{'itype'} and defined $sig_hr->{'itype_s'}) {
return 0, $NO_SIG_MATCH
unless &check_sig_int_range($pkt_hr->{'itype'}, 'itype', $sig_hr);
}
if (defined $sig_hr->{'icode'} and defined $sig_hr->{'icode_s'}) {
return 0, $NO_SIG_MATCH
unless &check_sig_int_range($pkt_hr->{'icode'}, 'icode', $sig_hr);
}
if (defined $sig_hr->{'icmp_seq'} and defined $sig_hr->{'icmp_seq_s'}) {
return 0, $NO_SIG_MATCH
unless &check_sig_int_range($pkt_hr->{'icmp_seq'}, 'icmp_seq', $sig_hr);
}
if (defined $sig_hr->{'icmp_id'} and defined $sig_hr->{'icmp_id_s'}) {
return 0, $NO_SIG_MATCH
unless &check_sig_int_range($pkt_hr->{'icmp_id'}, 'icmp_id', $sig_hr);
}
### matched the signature
if ($debug) {
print STDERR "[+] packet matched icmp keywords for sid: ",
"$sig_hr->{'sid'} (psad_id: $sig_hr->{'psad_id'})\n",
qq| "$sig_hr->{'msg'}"\n|;
}
return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
}
sub match_snort_ip_keywords() {
my ($pkt_hr, $sig_hr) = @_;
if ($pkt_hr->{'is_ipv6'}) {
### we need to build IPv6 signature keywords
return 1, $NO_SIG_MATCH;
}
if (defined $sig_hr->{'ttl'} and defined $sig_hr->{'ttl_s'}) {
return 0, $NO_SIG_MATCH
unless &check_sig_int_range($pkt_hr->{'ttl'}, 'ttl', $sig_hr);
}
if (defined $sig_hr->{'id'} and defined $sig_hr->{'id_s'}) {
return 0, $NO_SIG_MATCH
unless &check_sig_int_range($pkt_hr->{'ip_id'}, 'id', $sig_hr);
}
if (defined $sig_hr->{'psad_ip_len'} and defined $sig_hr->{'psad_ip_len_s'}) {
return 0, $NO_SIG_MATCH
unless &check_sig_int_range($pkt_hr->{'ip_len'},
'psad_ip_len', $sig_hr);
}
### to handle the ip_proto keyword parse_NF_pkt_str() would have to be
### modified to handle packets besides TCP, UDP, and ICMP.
return 0, $NO_SIG_MATCH if defined $sig_hr->{'ip_proto'};
### handle the sameip keyword
if (defined $sig_hr->{'sameip'} and $sig_hr->{'sameip'}) {
return 0, $NO_SIG_MATCH if $pkt_hr->{'intf'} eq 'lo';
return 0, $NO_SIG_MATCH unless $pkt_hr->{'src'} eq $pkt_hr->{'dst'};
}
return 0, $NO_SIG_MATCH
unless &check_sig_ipopts($pkt_hr->{'ip_opts'}, 'ipopts', $sig_hr);
if ($sig_hr->{'proto'} eq 'ip') {
### signature match
if ($debug) {
print STDERR "[+] packet matched ip keywords for sid: ",
"$sig_hr->{'sid'} (psad_id: $sig_hr->{'psad_id'})\n",
qq| "$sig_hr->{'msg'}"\n|;
}
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'ip'}{'sid'}
{$sig_hr->{'sid'}}{$pkt_hr->{'chain'}}{'pkts'}++;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'ip'}{'sid'}
{$sig_hr->{'sid'}}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 0;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'ip'}{'sid'}
{$sig_hr->{'sid'}}{$pkt_hr->{'chain'}}{'time'} = time();
$sig_sources{$sig_hr->{'sid'}}{$pkt_hr->{'src'}} = 0; ### not fwsnort
$top_sigs{$sig_hr->{'sid'}}++;
$top_sig_counts{$pkt_hr->{'src'}}++;
return &assign_sid_dl($sig_hr->{'sid'}, $sig_hr->{'dl'}), $SIG_MATCH;
}
return 1, $NO_SIG_MATCH;
}
sub check_sig_int_range() {
my ($pkt_val, $keyword, $sig_hr) = @_;
$pkt_val = 0 if $pkt_val < 0;
if ($sig_hr->{"${keyword}_neg"}) {
if ($pkt_val <= $sig_hr->{"${keyword}_e"}
and $pkt_val >= $sig_hr->{"${keyword}_s"}) {
if ($debug) {
if ($verbose or $debug_sid == $sig_hr->{'sid'}) {
print STDERR "[-] SID: $sig_hr->{'sid'} failed ",
"$keyword test, $pkt_val <= ",
qq|$sig_hr->{"${keyword}_e"} and $pkt_val |,
qq|>= $sig_hr->{"${keyword}_s"}\n|;
}
}
return 0;
}
} else {
### normal match
if ($pkt_val < $sig_hr->{"${keyword}_s"}) {
if ($debug) {
if ($verbose or $debug_sid == $sig_hr->{'sid'}) {
print STDERR "[-] SID: $sig_hr->{'sid'} failed ",
"$keyword test, $pkt_val < ",
qq|$sig_hr->{"${keyword}_s"} (range start)\n|;
}
}
return 0;
}
if ($pkt_val > $sig_hr->{"${keyword}_e"}) {
if ($debug) {
if ($verbose or $debug_sid == $sig_hr->{'sid'}) {
print STDERR "[-] SID: $sig_hr->{'sid'} failed ",
"$keyword test, $pkt_val > ",
qq|$sig_hr->{"${keyword}_e"} (range end)\n|;
}
}
return 0;
}
}
return 1;
}
sub check_sig_ipopts() {
my ($pkt_val, $keyword, $sig_hr) = @_;
return 1 unless defined $sig_hr->{$keyword};
return 0 unless $pkt_val;
return 1 if $sig_hr->{$keyword} eq 'any';
my $pkt_opts_hr = &parse_ip_options($pkt_val);
return 0 unless defined $pkt_opts_hr->{$sig_hr->{$keyword}};
return 1;
}
sub check_ignore_port() {
my ($port, $proto) = @_;
return 0 unless defined $ignore_ports{$proto};
return &match_port(\%{$ignore_ports{$proto}}, $port);
}
sub match_port() {
my ($hr, $port) = @_;
if (defined $hr->{'port'}) {
return 1 if defined $hr->{'port'}->{$port};
}
if (defined $hr->{'range'}) {
for my $low_port (keys %{$hr->{'range'}}) {
my $high_port = $hr->{'range'}->{$low_port};
return 1 if ($port >= $low_port and $port <= $high_port);
}
}
return 0;
}
sub p0f() {
my $pkt_hr = shift;
if ($pkt_hr->{'is_ipv6'}) {
&p0f_ipv6($pkt_hr);
} else {
&p0f_ipv4($pkt_hr);
}
return;
}
sub p0f_ipv6() {
my $pkt_hr = shift;
return;
}
sub p0f_ipv4() {
my $pkt_hr = shift;
print STDERR "[+] p0f_ipv4(): $pkt_hr->{'src'} len: $pkt_hr->{'ip_len'}, ",
"frag_bit: $pkt_hr->{'frag_bit'}, ttl: $pkt_hr->{'ttl'}, ",
"win: $pkt_hr->{'win'}\n" if $debug;
# p0f Fingerprint entry format:
#
# wwww:ttt:D:ss:OOO...:OS:Version:Subtype:Details
#
# wwww - window size (can be *, %nnn, Snn or Tnn). The special values
# "S" and "T" which are a multiple of MSS or a multiple of MTU
# respectively.
# ttt - initial TTL
# D - don't fragment bit (0 - not set, 1 - set)
# ss - overall SYN packet size
# OOO - option value and order specification (see below)
# OS - OS genre (Linux, Solaris, Windows)
# Version - OS Version (2.0.27 on x86, etc)
# Subtype - OS subtype or patchlevel (SP3, lo0)
# details - Generic OS details
#
### S4:64:1:60:M*,S,T,N,W7: Linux:2.6:8:Linux 2.6.8 and newer (?)
my $options_ar = &parse_tcp_options($pkt_hr->{'src'},
$pkt_hr->{'tcp_opts'});
unless ($options_ar) {
print STDERR "[-] Could not fingerprint remote OS.\n" if $debug;
return;
}
my $matched_os = 0;
### try to match SYN packet length
LEN: for my $sig_len (keys %p0f_ipv4_sigs) {
my $matched_len = 0;
if ($sig_len eq '*') { ### len can be wildcarded in pf.os
$matched_len = 1;
} elsif ($sig_len =~ /^\%(\d+)/) {
if (($pkt_hr->{'ip_len'} % $1) == 0) {
$matched_len = 1;
}
} elsif ($pkt_hr->{'ip_len'} == $sig_len) {
$matched_len = 1;
}
next LEN unless $matched_len;
### try to match fragmentation bit
FRAG: for my $test_frag_bit ($pkt_hr->{'frag_bit'}, '*') { ### don't need "%nnn" check
next FRAG unless defined $p0f_ipv4_sigs{$sig_len}{$test_frag_bit};
### find out for which p0f sigs the TTL is within range
TTL: for my $sig_ttl (keys %{$p0f_ipv4_sigs{$sig_len}{$test_frag_bit}}) {
unless ($pkt_hr->{'ttl'} > $sig_ttl - $config{'MAX_HOPS'}
and $pkt_hr->{'ttl'} <= $sig_ttl) {
next TTL;
}
### match tcp window size
WIN: for my $sig_win_size (keys
%{$p0f_ipv4_sigs{$sig_len}{$test_frag_bit}{$sig_ttl}}) {
my $matched_win_size = 0;
if ($sig_win_size eq '*') {
$matched_win_size = 1;
} elsif ($sig_win_size =~ /^\%(\d+)/) {
if (($pkt_hr->{'win'} % $1) == 0) {
$matched_win_size = 1;
}
} elsif ($sig_win_size =~ /^S(\d+)/) {
### window size must be a multiple of maximum
### seqment size
my $multiple = $1;
for my $opt_hr (@$options_ar) {
if (defined $opt_hr->{$tcp_p0f_opt_types{'M'}}) {
my $mss_val = $opt_hr->{$tcp_p0f_opt_types{'M'}};
if ($pkt_hr->{'win'} == $mss_val * $multiple) {
$matched_win_size = 1;
}
}
last;
}
} elsif ($sig_win_size == $pkt_hr->{'win'}) {
$matched_win_size = 1;
}
next WIN unless $matched_win_size;
TCPOPTS: for my $sig_opts (keys %{$p0f_ipv4_sigs{$sig_len}
{$test_frag_bit}{$sig_ttl}{$sig_win_size}}) {
my @sig_opts = split /\,/, $sig_opts;
for (my $i=0; $i<=$#sig_opts; $i++) {
### tcp option order is important. Check to see if
### the option order in the packet matches the order we
### expect to see in the signature
if ($sig_opts[$i] =~ /^([NMWST])/) {
my $sig_letter = $1;
unless (defined $options_ar->[$i]->
{$tcp_p0f_opt_types{$sig_letter}}) {
next TCPOPTS; ### could not match tcp option order
}
### MSS, window scale, and timestamp have
### specific signatures requirements on values
if ($sig_letter eq 'M') {
if ($sig_opts[$i] =~ /M(\d+)/) {
my $sig_mss_val = $1;
next TCPOPTS unless $options_ar->[$i]->
{$tcp_p0f_opt_types{$sig_letter}}
== $sig_mss_val;
} elsif ($sig_opts[$i] =~ /M\%(\d+)/) {
my $sig_mss_mod_val = $1;
next TCPOPTS unless (($options_ar->[$i]->
{$tcp_p0f_opt_types{$sig_letter}}
% $sig_mss_mod_val) == 0);
} ### else it is "M*" which always matches
} elsif ($sig_letter eq 'W') {
if ($sig_opts[$i] =~ /W(\d+)/) {
my $sig_win_val = $1;
next TCPOPTS unless $options_ar->[$i]->
{$tcp_p0f_opt_types{$sig_letter}}
== $sig_win_val;
} elsif ($sig_opts[$i] =~ /W\%(\d+)/) {
my $sig_win_mod_val = $1;
next TCPOPTS unless (($options_ar->[$i]->
{$tcp_p0f_opt_types{$sig_letter}}
% $sig_win_mod_val) == 0);
} ### else it is "W*" which always matches
} elsif ($sig_letter eq 'T') {
if ($sig_opts[$i] =~ /T0/) {
next TCPOPTS unless $options_ar->[$i]->
{$tcp_p0f_opt_types{$sig_letter}}
== 0;
} ### else it is just "T" which matches
}
}
}
OS: for my $os (keys %{$p0f_ipv4_sigs{$sig_len}
{$test_frag_bit}{$sig_ttl}{$sig_win_size}
{$sig_opts}}) {
my $sig = $p0f_ipv4_sigs{$sig_len}
{$test_frag_bit}{$sig_ttl}{$sig_win_size}
{$sig_opts}{$os};
print STDERR "[+] os: $os, $sig\n" if $debug;
$matched_os = 1;
$p0f{$pkt_hr->{'src'}}{$os} = '';
}
}
}
}
}
}
if ($debug and not $matched_os) {
print STDERR " Could not match $pkt_hr->{'src'} against any p0f signature.\n";
}
return;
}
sub parse_tcp_options() {
my ($src, $tcp_options) = @_;
my @opts = ();
my @hex_nums = ();
my $debug_str = '';
unless ($tcp_options) {
print STDERR 'no tcp options' if $debug;
return [];
}
if (length($tcp_options) % 2 != 0) { ### make sure length is a multiple of two
print STDERR 'tcp options length not a multiple of two.' if $debug;
return [];
}
### $tcp_options is a hex string like "020405B401010402" from the iptables
### log message
my @chars = split //, $tcp_options;
for (my $i=0; $i <= $#chars; $i += 2) {
my $str = $chars[$i] . $chars[$i+1];
push @hex_nums, $str;
}
my $max_parse_attempts = $#chars;
my $parse_ctr = 0;
OPT: for (my $opt_kind=0; $opt_kind <= $#hex_nums;) {
$parse_ctr++;
if ($parse_ctr > $max_parse_attempts) {
print STDERR " p0f() parse_ctr $parse_ctr > ",
"max_parse_attempts $max_parse_attempts\n" if $debug;
return [];
}
last OPT unless defined $hex_nums[$opt_kind+1];
my $is_nop = 0;
my $len = hex($hex_nums[$opt_kind+1]);
if (hex($hex_nums[$opt_kind]) == $tcp_nop_type) {
$debug_str .= 'NOP, ' if $debug;
push @opts, {$tcp_nop_type => ''};
$is_nop = 1;
} elsif (hex($hex_nums[$opt_kind]) == $tcp_mss_type) { ### MSS
my $mss_hex = '';
for (my $i=$opt_kind+2; $i < ($opt_kind+$len); $i++) {
$mss_hex .= $hex_nums[$i];
}
my $mss = hex($mss_hex);
push @opts, {$tcp_mss_type => $mss};
$debug_str .= 'MSS: ' . hex($mss_hex) . ', ' if $debug;
} elsif (hex($hex_nums[$opt_kind]) == $tcp_win_scale_type) {
my $window_scale_hex = '';
for (my $i=$opt_kind+2; $i < ($opt_kind+$len); $i++) {
$window_scale_hex .= $hex_nums[$i];
}
my $win_scale = hex($window_scale_hex);
push @opts, {$tcp_win_scale_type => $win_scale};
$debug_str .= 'Win Scale: ' . hex($window_scale_hex) . ', ' if $debug;
} elsif (hex($hex_nums[$opt_kind]) == $tcp_sack_type) {
push @opts, {$tcp_sack_type => ''};
$debug_str .= 'SACK, ' if $debug;
} elsif (hex($hex_nums[$opt_kind]) == $tcp_timestamp_type) {
my $timestamp_hex = '';
for (my $i=$opt_kind+2; $i < ($opt_kind+$len) - 4; $i++) {
$timestamp_hex .= $hex_nums[$i];
}
my $timestamp = hex($timestamp_hex);
push @opts, {$tcp_timestamp_type => $timestamp};
$debug_str .= 'Timestamp: ' . hex($timestamp_hex) . ', ' if $debug;
} elsif (hex($hex_nums[$opt_kind]) == 0) { ### End of option list
last OPT;
}
if ($is_nop) {
$opt_kind += 1;
} else {
if ($len == 0 or $len == 1) {
### this should never happen; it indicates a broken TCP stack
### or maliciously constructed options since the len field is
### not large enough to accomodate the TLV encoding
my $msg = "broken $len-byte len field within TCP options " .
"string: $tcp_options from source IP: $src";
print STDERR " $msg\n" if $debug;
&sys_log($msg);
return [];
}
### get to the next option-kind field
$opt_kind += $len;
}
}
if ($debug) {
$debug_str =~ s/\,$//;
print STDERR "[+] $debug_str\n" if $debug;
}
return \@opts;
}
sub parse_ip_options() {
my $ip_opts_str = shift;
my %ip_opts = ();
my @hex_nums = ();
if (length($ip_opts_str) % 2 != 0) { ### make sure length is a multiple of two
print STDERR 'IP options length not a multiple of two.' if $debug;
return '';
}
print STDERR "[+] parse_ip_options(): matched " if $debug;
push @hex_nums, $1 while $ip_opts_str =~ m|(.{2})|g;
OPT: for (my $i=0; $i <= $#hex_nums; $i++) {
my $val = hex($hex_nums[$i]);
for my $rfc_opt_val (keys %ip_options) {
next unless $val == $rfc_opt_val;
if ($ip_options{$rfc_opt_val}{'len'} ne '-1') {
$i += $ip_options{$rfc_opt_val}{'len'}
unless $ip_options{$rfc_opt_val}{'len'} == 1;
} else {
return \%ip_opts if ($i+1 > $#hex_nums);
### subtract out the option and length fields
my $pkt_opt_len = hex($hex_nums[$i+1]) - 2;
if ($i + $pkt_opt_len > $#hex_nums) {
### this should not happen unless the IP packet
### was truncated (i.e. the length argument for
### this option is past the IP options portion
### of the header).
return \%ip_opts;
}
$i += $pkt_opt_len;
}
if ($debug) {
printf STDERR ("$ip_options{$rfc_opt_val}{'sig_keyword'} " .
"(0x%x) ", $val) unless defined $ip_opts{$ip_options
{$rfc_opt_val}{'sig_keyword'}};
}
$ip_opts{$ip_options{$rfc_opt_val}{'sig_keyword'}} = '';
}
}
print STDERR "\n" if $debug;
return \%ip_opts;
}
sub posf() {
my $pkt_hr = shift;
if ($pkt_hr->{'is_ipv6'}) {
&posf_ipv6($pkt_hr);
} else {
&posf_ipv4($pkt_hr);
}
return;
}
sub posf_ipv6() {
my $pkt_hr = shift;
return;
}
sub posf_ipv4() {
my $pkt_hr = shift;
my $src = $pkt_hr->{'src'};
my $len = $pkt_hr->{'ip_len'};
my $tos = $pkt_hr->{'tos'};
my $ttl = $pkt_hr->{'ttl'};
my $id = $pkt_hr->{'ip_id'};
my $win = $pkt_hr->{'win'};
my $min_ttl;
my $max_ttl;
my $id_str;
$posf{$src}{'len'}{$len}++;
$posf{$src}{'tos'}{$tos}++;
$posf{$src}{'ttl'}{$ttl}++;
$posf{$src}{'win'}{$win}++;
$posf{$src}{'ctr'}++;
push @{$posf{$src}{'id'}}, $id; ### need to maintain ordering
print STDERR "[+] posf(): $src LEN: $len, TOS: $tos, TTL: $ttl, ",
"ID: $id, WIN: $win\n" if $debug;
$id_str = &id_incr(\@{$posf{$src}{'id'}});
for my $os (keys %posf_sigs) {
if ($posf{$src}{'ctr'} >= $posf_sigs{$os}{'numpkts'}) {
($min_ttl, $max_ttl) = &ttl_range($posf{$src}{'ttl'});
if (defined $posf{$src}{'win'}{$posf_sigs{$os}{'win'}}
# and defined $posf{$src}{'tos'}{$posf_sigs{$os}{'tos'}}
and defined $posf{$src}{'len'}{$posf_sigs{$os}{'len'}}
### ttl's only decrease
and ($min_ttl > ($posf_sigs{$os}{'ttl'}-$max_hops))
and ($max_ttl <= $posf_sigs{$os}{'ttl'})
and $id_str eq $posf_sigs{$os}{'id'}) {
$posf{$src}{'guess'} = $os;
print STDERR "[+] posf(): matched OS: $os\n" if $debug;
return;
}
}
}
return;
}
sub id_incr() {
my $aref = shift;
for (my $i=0; $i<$#$aref; $i++) {
return 'RANDOM'
unless ($aref->[$i] < $aref->[$i+1]
and ($aref->[$i+1] - $aref->[$i]) < 1000);
}
return 'SMALLINCR';
}
sub ttl_range() {
my $hr = shift;
my $min_ttl = 256;
my $max_ttl = 0;
for my $ttl (keys %$hr) {
$min_ttl = $ttl if $ttl < $min_ttl;
$max_ttl = $ttl if $ttl > $max_ttl;
}
return $min_ttl, $max_ttl;
}
sub assign_sid_dl() {
my ($sid, $dl) = @_;
### see if /etc/psad/snort_rule_dl assigns a DL (may be
### zero).
if (defined $snort_rule_dl{$sid}) {
$dl = $snort_rule_dl{$sid};
}
print STDERR "[+] assign_sid_dl(): snort_rule_dl ",
"assigning SID $sid a danger level of ",
"$dl\n" if $debug;
return $dl;
}
sub add_fwsnort_sid() {
my $pkt_hr = shift;
my $sid = $pkt_hr->{'fwsnort_sid'};
if (defined $fwsnort_sigs{$sid}) {
### see if we need to ignore this signature match
my $dl = &assign_sid_dl($sid, 2);
unless ($dl) {
print "[+] add_fwsnort_sid(): ignoring fwsnort signature ",
"match for SID: $sid (DL=0)\n" if $debug;
return 0, $SIG_MATCH;
}
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'} = 0
unless defined $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{'tot_protocols'}++
unless defined $scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'pkts'}++;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'is_fwsnort'} = 1;
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'time'} = time();
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'fwsnort_rnum'}
= $pkt_hr->{'fwsnort_rnum'};
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'fwsnort_estab'}
= $pkt_hr->{'fwsnort_estab'};
$sig_sources{$sid}{$pkt_hr->{'src'}} = 1; ### is an fwsnort sid
$top_sigs{$sid}++;
$top_sig_counts{$pkt_hr->{'src'}}++;
if ($pkt_hr->{'proto'} eq 'tcp' or $pkt_hr->{'proto'} eq 'udp') {
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'dp'}
= $pkt_hr->{'dp'};
if ($pkt_hr->{'proto'} eq 'tcp') {
$scan{$pkt_hr->{'src'}}{$pkt_hr->{'dst'}}{$pkt_hr->{'proto'}}
{'sid'}{$sid}{$pkt_hr->{'chain'}}{'flags'}
= $pkt_hr->{'flags'};
}
}
return $dl, $SIG_MATCH;
} else {
print "[-] Found sid: $sid in packet, but no ",
"corresponding fwsnort rule.\n" if $debug;
}
return 0, $NO_SIG_MATCH;
}
sub delete_old_scans() {
my $current_time = time();
print STDERR "[+] delete_old_scans()\n" if $debug;
### see if we need to timeout any old scans
for my $src (keys %scan) {
for my $dst (keys %{$scan{$src}}) {
next unless defined $scan{$src}{$dst}{'s_time'};
if (($current_time - $scan{$src}{$dst}{'s_time'})
>= $config{'SCAN_TIMEOUT'}) {
print STDERR " delete old scan $src -> $dst\n"
if $debug;
delete $scan{$src}{$dst};
if ($config{'MAX_SCAN_IP_PAIRS'} > 0) {
$scan_ip_pairs-- if $scan_ip_pairs > 0;
}
}
}
}
return;
}
sub dshield_email_log() {
### dshield alert interval is in hours. Check to see if there are more
### than 10,000 lines of log data (and if the last alert was sent more than
### two hours later than the previous alert), and if yes send the alert
### email.
if (@dshield_data and ((time() - $last_dshield_alert)
>= $dshield_alert_interval)
or (($#dshield_data > 10000)
and ((time() - $last_dshield_alert) >= 2*3600))) {
my $dshield_version = $version;
$dshield_version =~ s/^(\d+\.\d+)\.\d+/$1/;
$dshield_version =~ s/-pre\d+//;
my $subject = "FORMAT DSHIELD USERID $config{'DSHIELD_USER_ID'} " .
"TZ $timezone psad Version $dshield_version";
if ($config{'DSHIELD_USER_EMAIL'} eq 'NONE') {
open MAIL, qq(| $cmds{'mail'} -s "$subject" ) .
$config{'DSHIELD_ALERT_EMAIL'} or die '[*] Could not send ',
'dshield alert email.';
### save this email to disk also
open DSSAVE, "> $config{'DSHIELD_EMAIL_FILE'}" or die '[*] ',
"Could not open $config{'DSHIELD_EMAIL_FILE'}: $!";
if ($config{'DSHIELD_DL_THRESHOLD'} > 0) {
for my $line (@dshield_data) {
if ($line =~ /^.*?($ipv4_re)/) {
my $src = $1;
if (defined $scan_dl{$src}
and ($scan_dl{$src}
>= $config{'DSHIELD_DL_THRESHOLD'})) {
print MAIL $line;
print DSSAVE $line;
}
}
}
} else {
print MAIL for @dshield_data;
print DSSAVE for @dshield_data;
}
close MAIL;
close DSSAVE;
} else {
open MAIL, "| $cmds{'sendmail'} -oi -t" or die '[*] Could not ',
'send dshield alert email.';
### save this email to disk also
open DSSAVE, "> $config{'DSHIELD_EMAIL_FILE'}" or die '[*] ',
"Could not open $config{'DSHIELD_EMAIL_FILE'}: $!";
print MAIL "From: $config{'DSHIELD_USER_EMAIL'}\n",
"To: $config{'DSHIELD_ALERT_EMAIL'}\n",
"Subject: $subject\n";
print DSSAVE "From: $config{'DSHIELD_USER_EMAIL'}\n",
"To: $config{'DSHIELD_ALERT_EMAIL'}\n",
"Subject: $subject\n";
if ($config{'DSHIELD_DL_THRESHOLD'} > 0) {
for my $line (@dshield_data) {
if ($line =~ /^.*?($ipv4_re)/) {
my $src = $1;
if (defined $scan_dl{$src}
and ($scan_dl{$src}
>= $config{'DSHIELD_DL_THRESHOLD'})) {
print MAIL $line;
print DSSAVE $line;
}
}
}
} else {
print MAIL for @dshield_data;
print DSSAVE for @dshield_data;
}
close MAIL;
close DSSAVE;
}
&sys_log("sent $#dshield_data lines of log data to " .
$config{'DSHIELD_ALERT_EMAIL'});
### store the current time
$last_dshield_alert = time();
### increment stats counters
$dshield_email_ctr++;
$dshield_lines_ctr += $#dshield_data;
### clear the dshield data array so we don't re-send
### any old data.
@dshield_data = ();