Mercurial > hg > maltfilter
view maltfilter @ 52:8cfb71b296da
Added colour-coded grouping of IP addresses in summary table.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Sun, 16 Aug 2009 03:36:27 +0300 |
parents | 13e6507ec1bb |
children | dc072a56f343 |
line wrap: on
line source
#!/usr/bin/perl -w ############################################################################# # # Malicious Attack Livid Termination Filter daemon (maltfilter) # Programmed by Matti 'ccr' Hämäläinen <ccr@tnsp.org> # (C) Copyright 2009 Tecnic Software productions (TNSP) # ############################################################################# use strict; use Date::Parse; use Net::IP; my $progversion = "0.12.3"; my $progbanner = "Malicious Attack Livid Termination Filter daemon (maltfilter) v$progversion\n". "Programmed by Matti 'ccr' Hamalainen <ccr\@tnsp.org>\n". "(C) Copyright 2009 Tecnic Software productions (TNSP)\n"; ############################################################################# ### Default settings and configuration ############################################################################# my %settings = ( "VERBOSITY" => 3, "DRY_RUN" => 1, "WEED_BLOCK" => 168, "WEED_GLOBAL" => 336, "TRESHOLD" => 3, "ACTION" => "DROP", "LOGFILE" => "", "IPTABLES" => "/sbin/iptables", "STATUS_FILE_PLAIN" => "", "STATUS_FILE_HTML" => "", "STATUS_FILE_CSS" => "", "WHOIS_URL" => "http://whois.domaintools.com/", "CHK_SSHD" => 1, "CHK_KNOWN_CGI" => 1, "CHK_PHP_XSS" => 1, "CHK_PROXY_SCAN" => 1, "CHK_ROOT_SSH_PWD" => 0, "CHK_SYSACCT_SSH_PWD" => 0, "CHK_GOOD_HOSTS" => "", "SYSACCT_MIN_UID" => 1, "SYSACCT_MAX_UID" => 100, "FULL_TIME" => 1, "PASSWD" => "/etc/passwd", ); # Default logfiles to monitor (SCANFILES setting of configuration overrides these) my @scanfiles_def = ( "/var/log/auth.log", "/var/log/httpd/error.log", "/var/log/httpd/access.log" ); my @noblock_ips_def = ( "127.0.0.1", ); my %systemacct = (); ############################################################################# ### Check given logfile line for matches ############################################################################# sub check_log_line($) { # (1) SSHD scans if (/^(\S+\s+\d+\s+\d\d:\d\d:\d\d)\s+\S+\s+sshd\S*?: (.*)/) { my $mdate = $1; my $merr = $2; # (1.1) Generic login scan attempts if ($merr =~ /^Failed password for invalid user (\S+) from (\d+\.\d+\.\d+\.\d+)/) { check_add_hit($2, $mdate, "SSH login scan", "", $settings{"CHK_SSHD"}); } # (1.2) Root account SSH login password bruteforcing attempts. elsif (/^Failed password for root from (\d+\.\d+\.\d+\.\d+)/) { check_add_hit($1, $mdate, "Root SSH password bruteforce", "", $settings{"CHK_ROOT_SSH_PWD"}); } # (1.3) System account SSH login password bruteforcing attempts. if ($merr =~ /^Failed password for (\S+) from (\d+\.\d+\.\d+\.\d+)/) { my $mip = $2; my $macct = $1; if (defined($systemacct{$macct})) { check_add_hit($mip, $mdate, "SSH system account bruteforce", $macct, $settings{"CHK_SYSACCT_SSH_PWD"}); } } } # (2) Common/known vulnerable CGI/PHP software scans (like phpMyAdmin) elsif (/^\[(.+?)\]\s+\[error\]\s+\[client\s+(\d+\.\d+\.\d+\.\d+)\]\s+(.+)$/) { my $mdate = $1; my $mip = $2; my $merr = $3; if ($merr =~ /^File does not exist: (.+)$/) { my $tmp = $1; if ($tmp =~ /\/mss2|\/pma|admin|sql|\/roundcube|\/webmail|\/bin|\/mail|xampp|zen|mailto:|appserv|cube|round|_vti_bin|wiki/i) { check_add_hit($mip, $mdate, "CGI vuln scan", $tmp, $settings{"CHK_KNOWN_CGI"}); } } } # (3) Apache common logging format checks elsif (/(\d+\.\d+\.\d+\.\d+)\s+-\s+-\s+\[(.+?)\]\s+\"GET (\S*?) HTTP\//) { my $mdate = $2; my $mip = $1; my $merr = $3; # (3.1) Simple match for generic PHP XSS vulnerability scans if ($merr =~ /\.php\?\S*?=http:\/\/([^\/]+)/) { if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) { check_add_hit($mip, $mdate, "PHP XSS", $merr, $settings{"CHK_PHP_XSS"}); } } # (3.2) Try to match proxy scanning attempts elsif ($merr =~ /^http:\/\/([^\/]+)/) { if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) { check_add_hit($mip, $mdate, "Proxy scan", $merr, $settings{"CHK_PROXY_SCAN"}); } } } } ############################################################################# ### Global variables ############################################################################# my $reportmode = 0; # Full report mode my @scanfiles = (); # Files to scan my @noblock_ips = (); # IPs not to block my %filehandles = (); # Global hash holding opened scanned log filehandles my $pid_file = ""; # Name of Maltfilter daemon pid file my @configfiles = (); # Array of configuration file names my $LOGFILE; # Maltfilter logfile handle # IPs currently blocked in Netfilter $blocklist{$ip} = date my %blocklist = (); # Gathered information about hosts # $statlist{$ip}-> # "date1" = timestamp of first hit # "date2" = timestamp of latest hit # "hits" = number of hits to this IP # $statlist{$ip}{"reason"}{$class}-> # "msg" = reason message (array if $reportmode) # "hits" = hits to this class # "date1" = timestamp of first hit # "date2" = timestamp of latest hit my %statlist = (); # Gathered information about ignored hits (e.g. hits for tests that are not enabled) # Same fields as in %statlist my %ignorelist = (); ############################################################################# ### Status output functionality ############################################################################# sub urlencode($) { my $value = $_[0]; $value =~ s/([^a-zA-Z_0-9 ])/"%" . uc(sprintf "%lx" , unpack("C", $1))/eg; $value =~ tr/ /+/; return $value; } my %entities = ( "<" => "lt", ">" => "gt", "&" => "amp", ); sub htmlentities($) { my $value = $_[0]; # $value =~ s/([keys %entities])/"&".$entities{$1}.";"/eg; foreach my $val (keys %entities) { $value =~ s/$val/\&$entities{$val}\;/g; } return $value; } sub get_time_str($) { if ($_[0] >= 0) { return scalar localtime($_[0]); } else { return "?"; } } my @paskat = (30*24*60*60, 7*24*60*60, 24*60*60, 60*60, 60); my @opaskat = ("months", "weeks", "days", "hours", "minutes"); my @upaskat = ("month", "week", "day", "hour", "minute"); sub get_ago_str($) { return get_time_str($_[0]) if ($settings{"FULL_TIME"}); if ($_[0] >= 0) { my $str = ""; my $cur = time() - $_[0]; my ($r, $k, $p, $n); $n = 0; foreach my $div (@paskat) { $r = int($cur / $div); $k = ($cur % $div); if ($r > 0) { $p = ($r > 1) ? $opaskat[$n] : $upaskat[$n]; $str .= ", " if ($str ne ""); $str .= sprintf("%d %s", $r, $p); } $cur = $k; $n++; } return $str." ago"; } else { return "?"; } } sub printH($$$$) { my $fh = $_[1]; if ($_[0]) { print $fh "<h".$_[2].">".$_[3]."</h".$_[2].">\n"; } else { my $c = ($_[2] <= 1) ? "=" : "-"; print $fh $_[3]."\n". $c x length($_[3]) ."\n"; } } sub printTD { my $fh = $_[1]; if ($_[0]) { my $s = defined($_[3]) ? " ".$_[3]." " : ""; print $fh "<td".$s.">".$_[2]."</td>"; } else { print $fh $_[2]; } } sub printP($$$) { my $fh = $_[1]; if ($_[0]) { print $fh "<p>\n".$_[2]."</p>\n"; } else { print $fh $_[2]."\n"; } } sub printElem { my $fh = $_[1]; if ($_[0]) { print $fh $_[2]; } elsif (defined($_[3])) { print $fh $_[3]; } } sub bb($) { return $_[0] ? "<b>" : ""; } sub eb($) { return $_[0] ? "</b>" : ""; } sub pe($$) { return $_[0] ? "<$_[1]>" : ""; } sub get_link($$) { if ($settings{"WHOIS_URL"} ne "") { return $_[0] ? "<a href=\"".$settings{"WHOIS_URL"}.$_[1]. "\">".htmlentities($_[1])."</a>" : $_[1]; } else { return $_[0]; } } sub print_table1($$$$$$) { my ($m, $f, $table, $keys, $func, $class) = @_; my $ntotal = 0; printElem($m, $f, "<table class=\"".$class."\">\n". "<tr><th>Hits</th><th>IP-address</th><th>First hit</th><th>Latest hit</th><th>Reason(s)</th></tr>\n", "Hits | IP-address | First hit | Latest hit | Reason(s)\n" ); foreach my $mip (sort { $func->($table, $a, $b) } keys %{$keys}) { my $blocked = defined($blocklist{$mip}) ? "blocked" : "unblocked"; printElem($m, $f, " <tr class=\"$blocked\">"); printTD($m, $f, sprintf(bb($m)."%-10d".eb($m), $table->{$mip}{"hits"})); printElem(!$m, $f, " | "); printTD($m, $f, sprintf("%-15s", get_link($m, $mip))); printElem(!$m, $f, " | "); printTD($m, $f, get_ago_str($table->{$mip}{"date1"})); printElem(!$m, $f, " | "); printTD($m, $f, get_ago_str($table->{$mip}{"date2"})); printElem(!$m, $f, " | "); my @reasons = (); foreach my $class (sort keys %{$table->{$mip}{"reason"}}) { my $msgs; if ($class ne "IPTABLES") { if ($reportmode) { my @tmp = reverse(@{$table->{$mip}{"reason"}{$class}{"msg"}}); if ($#tmp > 5) { $#tmp = 5; } foreach (@tmp) { $_ = htmlentities($_); } $msgs = join(" ".bb($m)."|".eb($m)." ", @tmp); } else { $msgs = $table->{$mip}{"reason"}{$class}{"msg"}; } push(@reasons, bb($m).$class.eb($m)." #".$table->{$mip}{"reason"}{$class}{"hits"}. " ( ".$msgs." )"); } } printTD($m, $f, join(", ", @reasons)); printElem($m, $f, "</tr>\n", "\n"); $ntotal++; } printElem($m, $f, "</table>\n"); printP($m, $f, bb($m).$ntotal.eb($m)." entries total.\n"); } sub cmp_ips($$$) { my @ipa = split(/\./, $_[1]); my @ipb = split(/\./, $_[2]); for (my $i = 0; $i < 4; $i++) { return -1 if ($ipa[$i] > $ipb[$i]); return 1 if ($ipa[$i] < $ipb[$i]); } return 0; } sub test_ips($$) { my @ipa = split(/\./, $_[0]); my @ipb = split(/\./, $_[1]); for (my $i = 0; $i < 3; $i++) { return $i if ($ipa[$i] != $ipb[$i]); } return 4; } my @ipcolors = ( "#666", "#777", ); sub print_table2($$$$$$) { my ($m, $f, $table, $keys, $func, $class) = @_; my $nhits = 0; my $str = "<th>IP-address</th><th>Hits</th><th>First hit</th><th>Latest hit</th><th>Class</th>"; my $str2 = "IP-address | Hits | First hit | Latest hit | Class "; printElem($m, $f, "<table class=\"".$class."\">\n<tr>". $str."<th> </th>".$str ."</tr>\n", $str2." || ".$str2."\n"); my @previp = ("0.0.0.0", "0.0.0.0"); my @ncolor = (0, 0); my $printEntry = sub { my $blocked = "class=\"".(defined($blocklist{$_[0]}) ? "blocked" : "unblocked")."\""; if (test_ips($previp[$_[1]], $_[0]) < 3) { $ncolor[$_[1]]++; } $previp[$_[1]] = $_[0]; my $str = "style=\"background: ".$ipcolors[$ncolor[$_[1]] % scalar @ipcolors].";\""; printTD($m, $f, sprintf("%-15s", get_link($m, $_[0])), $str); printElem(!$m, $f, " | "); printTD($m, $f, sprintf("%-8d ", $table->{$_[0]}{"hits"}), $blocked); printElem(!$m, $f, " | "); printTD($m, $f, get_ago_str($table->{$_[0]}{"date1"}), $blocked); printElem(!$m, $f, " | "); printTD($m, $f, get_ago_str($table->{$_[0]}{"date2"}), $blocked); printElem(!$m, $f, " | "); my $tmp = join(", ", sort keys %{$table->{$_[0]}{"reason"}}); printTD($m, $f, sprintf("%-30s", $tmp), $blocked); $nhits += $table->{$_[0]}{"hits"}; }; my @mkeys = sort { $func->($table, $a, $b) } keys %{$keys}; my $nkeys = scalar @mkeys; my $kmax = $nkeys / 2; for (my $i = 0; $i <= $kmax; $i++) { printElem($m, $f, " <tr>"); if ($i < $kmax) { $printEntry->($mkeys[$i], 0); printElem($m, $f, "<th> </th>", " || "); } if ($i + $kmax + 1 < $nkeys) { $printEntry->($mkeys[$i + $kmax + 1], 1); } printElem($m, $f, "</tr>\n", "\n"); } printElem($m, $f, "</table>\n"); printP($m, $f, bb($m).$nkeys.eb($m)." entries total, ".bb($m).$nhits.eb($m)." hits total.\n"); } sub cmp_hits($$$) { return $_[0]->{$_[2]}{"hits"} <=> $_[0]->{$_[1]}{"hits"}; } sub get_period($) { my ($str, $r, $k); if ($_[0] > 30 * 24) { $r = $_[0] / (30 * 24); $k = $_[0] % (30 * 24); $str = sprintf("%d months", $r); $str .= sprintf(", %d days", $k) if ($k > 0); } elsif ($_[0] > 24 * 7) { $str = sprintf("%1.1f weeks", $_[0] / (24.0 * 7.0)); } elsif ($_[0] > 24) { $r = $_[0] / 24; $k = $_[0] % 24; $str = sprintf("%d days", $r); $str .= sprintf(", %d hours", $k) if ($k > 0); } else { $str = sprintf("%d hours", $_[0]); } return $str; } sub generate_status($$) { my $filename = shift; my $m = shift; return unless ($filename ne ""); open(STATUS, ">", $filename) or mdie("Could not open '".$filename."'!\n"); my $f = \*STATUS; printElem($m, $f, " <html> <head> <title>Maltfilter status report</title> "); printElem($m, $f, "<link href=\"".$settings{"STATUS_FILE_CSS"}."\" rel=\"stylesheet\" type=\"text/css\" />") if ($settings{"STATUS_FILE_CSS"}); printElem($m, $f, " </head> <body> "); printH($m, $f, 1, "Maltfilter v$progversion status report"); my $period = get_period($settings{"WEED_GLOBAL"}); printP($m, $f, "Generated ".bb($m).get_time_str(time()).eb($m).". Data computed from ". ($reportmode ? "complete logfile scan" : "a period of last $period").".\n"); printP($m, $f, "The hit classes marked as 'IPTABLES' are a pseudo-class meaning an\n". "blocked IP that was in Netfilter before Maltfilter was started.\n"); printH($m, $f, 2, "Currently blocked entries"); $period = get_period($settings{"WEED_BLOCK"}); printP($m, $f, "List of IPs that are currently blocked (or would be, if this is\n". "a report-only mode). Data from period of $period.\n"); print_table1($m, $f, \%statlist, \%blocklist, \&cmp_hits, "blocked"); printH($m, $f, 2, "Summary of non-ignored entries"); printP($m, $f, "List of 'hits' of suspicious activity noticed by Maltfilter, but not\n". "necessarily acted upon. Sorted by descending IP address.\n"); print_table2($m, $f, \%statlist, \%statlist, \&cmp_ips, "global"); printH($m, $f, 2, "Ignored entries"); printP($m, $f, "List of hits that were ignored (not acted upon), because the test was disabled.\n". "Notice that the entry may be blocked due to other checks, however.\n"); print_table1($m, $f, \%ignorelist, \%ignorelist, \&cmp_hits, "ignored"); printElem($m, $f, "</body>\n</html>\n"); close(STATUS); } ############################################################################# ### Entry management / handling functions ############################################################################# ### Check if given IP or host exists in array sub check_hosts_array($$) { my $chk_host = $_[1]; my $chk_ip = new Net::IP($chk_host); foreach my $host (@{$_[0]}) { if ($chk_host eq $host) { return 1; } my $ip = new Net::IP($host); if (defined($chk_ip) && defined($ip)) { if ($chk_ip->binip() eq $ip->binip()) { return 1; } } } return 0; } ### Check IP/host against | separated list of IPs/hosts sub check_hosts($$) { my @tmp = split(/\s*\|\s*/, $_[0]); return check_hosts_array(\@tmp, $_[1]); } ### Execute iptables sub exec_iptables(@) { my @args = ($settings{"IPTABLES"}, @_); if ($settings{"DRY_RUN"}) { mlog(3, ":: ".join(" ", @args)."\n"); } else { system(@args) == 0 or print join(" ", @args)." failed: $?\n"; } } ### Get current Netfilter INPUT table entries that match ### entry types we manage, e.g. blocklist sub update_blocklist() { $ENV{"PATH"} = ""; open(STATUS, $settings{"IPTABLES"}." -v -n -L INPUT |") or mdie("Could not execute ".$settings{"IPTABLES"}."\n"); my %newlist = (); undef(%newlist); while (<STATUS>) { chomp; if (/^\s*(\d+)\s+\d+\s+$settings{"ACTION"}\s+all\s+--\s+\*\s+\*\s+(\d+\.\d+\.\d+\.\d+)\s+0\.0\.0\.0\/0\s*$/) { my $mip = $2; my $mdate = time(); if (!defined($blocklist{$mip})) { mlog(2, "* $mip appeared in iptables.\n"); $blocklist{$2} = $mdate; } $newlist{$2} = $mdate; update_entry(\%statlist, $mip, -1, "IPTABLES", ""); } } close(STATUS); foreach my $mip (keys %blocklist) { if (!defined($newlist{$mip})) { mlog(2, "* $mip removed from iptables.\n"); delete($blocklist{$mip}); } } } ### Check if given timestamp is _newer_ than weedperiod threshold. ### Returns false if timestamp is over weed period, e.g. needs weeding. sub check_time1($) { return ($_[0] >= time() - ($settings{"WEED_BLOCK"} * 60 * 60)); } sub check_time2($) { return ($_[0] >= time() - ($settings{"WEED_GLOBAL"} * 60 * 60)); } ### Weed out old entries sub weed_do($) { my $mtime = $blocklist{$_[0]}; mlog(2, "* Weeding $_[0] (".get_time_str($mtime)."\n"); exec_iptables("-D", "INPUT", "-s", $_[0], "-d", "0.0.0.0/0", "-j", $settings{"ACTION"}); delete($blocklist{$_[0]}); delete($statlist{$_[0]}); delete($ignorelist{$_[0]}); } sub weed_entries() { # Don't weed in report mode. return if ($reportmode); # Weed blocked entries. my @mips = keys %blocklist; foreach my $mip (@mips) { if (defined($blocklist{$mip})) { if ($blocklist{$mip} >= 0) { weed_do($mip) unless check_time1($blocklist{$mip}); } else { weed_do($mip); } } } # Clean up old entries from other lists foreach my $mip (keys %statlist) { if (defined($statlist{$mip})) { my $mtime = $statlist{$mip}{"date2"}; if (!check_time2($mtime) && !defined($blocklist{$mip})) { mlog(3, "* Deleting stale $mip (".get_time_str($mtime).")\n"); delete($statlist{$mip}); } } } foreach my $mip (keys %ignorelist) { if (defined($ignorelist{$mip})) { my $mtime = $ignorelist{$mip}{"date2"}; if (!check_time2($mtime)) { mlog(3, "* Deleting stale ignored $mip (".get_time_str($mtime).")\n"); delete($ignorelist{$mip}); } } } } ### Update one entry of sub update_entry($$$$$) { my ($struct, $mip, $mdate, $mclass, $mreason) = @_; my $cnt = $struct->{$mip}{"hits"}++; $struct->{$mip}{"reason"}{$mclass}{"hits"}++; if ($reportmode) { push(@{$struct->{$mip}{"reason"}{$mclass}{"msg"}}, $mreason); } else { $struct->{$mip}{"reason"}{$mclass}{"msg"} = $mreason; } if (!defined($struct->{$mip}{"date1"}) || ($mdate > 0 && $struct->{$mip}{"date1"} < 0)) { $struct->{$mip}{"date1"} = $mdate; } if (!defined($struct->{$mip}{"date2"}) || $mdate > $struct->{$mip}{"date2"}) { $struct->{$mip}{"date2"} = $mdate; } if (!defined($struct->{$mip}{"reason"}{$mclass}{"date2"}) || ($mdate > 0 && $struct->{$mip}{"reason"}{$mclass}{"date2"} < 0)) { $struct->{$mip}{"reason"}{$mclass}{"date2"} = $mdate; } if (!defined($struct->{$mip}{"reason"}{$mclass}{"date2"}) || $mdate > $struct->{$mip}{"reason"}{$mclass}{"date2"}) { $struct->{$mip}{"reason"}{$mclass}{"date2"} = $mdate; } return $cnt; } ### Check if given "try count" exceeds treshold and if entry ### is NOT in Netfilter already, then add it if so. sub check_add_hit($$$$$) { my $mip = $_[0]; my $mdate = str2time($_[1]); my $mclass = $_[2]; my $mreason = $_[3]; my $mcond = $_[4]; my $cnt; if (check_hosts_array(\@noblock_ips, $mip)) { mlog(3, "Hit to NOBLOCK_IPS($mip): [$mclass] $mreason\n"); return; } # If condition is true, we add to regular statlist if ($mcond) { $cnt = update_entry(\%statlist, $mip, $mdate, $mclass, $mreason); } else { # This is an ignored hit (for disabled test), add to ignorelist update_entry(\%ignorelist, $mip, $mdate, $mclass, $mreason); return; } # Check if we have exceeded treshold etc. if ($cnt >= $settings{"TRESHOLD"} && check_time1($mdate)) { # Add to blocklist, unless already there. if (!defined($blocklist{$mip})) { mlog(1, "* Adding $mip ($mdate): [$mclass] $mreason\n"); exec_iptables("-I", "INPUT", "1", "-s", $mip, "-j", $settings{"ACTION"}); } # Update date of last hit $blocklist{$mip} = $mdate; } } ############################################################################# ### Main helper functions ############################################################################# ### Print log entry sub mlog($$) { my $level = shift; my $msg = shift; if ($LOGFILE) { print $LOGFILE "[".get_time_str(time())."] ".$msg if ($settings{"VERBOSITY"} > $level); } elsif ($settings{"DRY_RUN"}) { print STDERR $msg if ($settings{"VERBOSITY"} > $level); } } ### Like Perl's die(), but also print a logfile entry. sub mdie($) { mlog(-1, $_[0]); die($_[0]); } ### Initialize sub malt_init { mlog(0, "Updating initial blocklist from netfilter.\n"); update_blocklist(); foreach my $filename (@scanfiles) { local *INFILE; mlog(0, "Parsing ".$filename." ...\n"); open(INFILE, "<", $filename) or mdie("Could not open '".$filename."'!\n"); $filehandles{$filename} = *INFILE; while (<INFILE>) { chomp; check_log_line($_); } } mlog(0, "Weeding old entries.\n"); weed_entries(); } ### Quick cleanup (not complete shutdown) sub malt_cleanup { foreach my $filename (keys %filehandles) { close($filehandles{$filename}); } } sub malt_finish { # Unlink pid-file if ($pid_file ne "" && -e $pid_file) { unlink $pid_file; } # Close logfile close($LOGFILE) if (defined($LOGFILE)); undef($LOGFILE); } ### Signal handlers sub malt_int { mlog(-1, "\nCaught Interrupt (^C), aborting.\n"); malt_cleanup(); malt_finish(); exit(1); } sub malt_term { mlog(-1, "Received TERM, quitting.\n"); malt_cleanup(); malt_finish(); exit(1); } sub malt_hup { mlog(-1, "Received HUP, reinitializing.\n"); malt_cleanup(); malt_configure(); malt_init(); mlog(-1, "Reinitialization finished, resuming scanning.\n"); } ### Main scanning function sub malt_scan { mlog(1, "Entering main scanning loop.\n"); my $counter = -1; while (1) { my %filepos = (); foreach my $filename (keys %filehandles) { for ($filepos{$filename} = tell($filehandles{$filename}); $_ = <$filehandles{$filename}>; $filepos{$filename} = tell($filehandles{$filename})) { chomp; check_log_line($_); } } if ($counter < 0 || $counter++ >= 30) { # Every once in a while, update known IP list from iptables # (in case entries have appeared there from "outside") # and perform weeding of old entries. $counter = 0; update_blocklist(); weed_entries(); generate_status($settings{"STATUS_FILE_PLAIN"}, 0); generate_status($settings{"STATUS_FILE_HTML"}, 1); } sleep(5); foreach my $filename (keys %filehandles) { seek($filehandles{$filename}, $filepos{$filename}, 0); } } } ### Read one configuration file sub malt_read_config($) { my $filename = $_[0]; my $errors = 0; my $line = 0; open(CONFFILE, "<", $filename) or mdie("Could not open configuration '".$filename."'!\n"); while (<CONFFILE>) { $line++; chomp; if (/(^\s*#|^\s*$)/) { # Ignore comments and empty lines } elsif (/^\s*\"?([a-zA-Z0-9_]+)\"?\s*=>?\s*(\d+),?\s*$/) { my $key = uc($1); my $value = $2; if (defined($settings{$key})) { $settings{$key} = $value; } else { mlog(-1, "[$filename:$line] Unknown setting '$key' = $value\n"); $errors = 1; } } elsif (/^\s*\"?([a-zA-Z0-9_]+)\"?\s*=>?\s*\"(.*?)\",?\s*$/) { my $key = uc($1); my $value = $2; if ($key eq "SCANFILE") { push(@scanfiles_def, $value); } elsif ($key eq "NOBLOCK_IPS") { push(@noblock_ips_def, $value); } elsif (defined($settings{$key})) { $settings{$key} = $value; } else { mlog(-1, "[$filename:$line] Unknown setting '$key' = '$value'\n"); $errors = 1; } # Force dry run mode if we are reporting only if ($reportmode) { $settings{"DRY_RUN"} = 1; } } else { mlog(-1, "[$filename:$line] Syntax error: $_\n"); $errors = 1; } } close(CONFFILE); return $errors; } ### Read all configuration files sub malt_configure { # Let user define his/her own logfiles to scan @scanfiles_def = (); undef(@scanfiles_def); foreach my $filename (@configfiles) { mdie("Errors in configuration file '$filename', bailing out.\n") unless (malt_read_config($filename) == 0); } mdie("SYSACCT_MIN_UID must be >= 1.\n") unless ($settings{"SYSACCT_MIN_UID"} >= 1); mdie("SYSACCT_MAX_UID must be >= SYSACCT_MIN_UID.\n") unless ($settings{"SYSACCT_MAX_UID"} >= $settings{"SYSACCT_MIN_UID"}); open(PASSWD, "<", $settings{"PASSWD"}) or mdie("Could not open '".$settings{"PASSWD"}."' for reading!\n"); while (<PASSWD>) { my @fields = split(/\s*:\s*/); if ($fields[2] >= $settings{"SYSACCT_MIN_UID"} && $fields[2] <= $settings{"SYSACCT_MAX_UID"}) { $systemacct{$fields[0]} = $fields[2]; } } close(PASSWD); } ############################################################################# ### ### Main program ### ############################################################################# # Setup signal handlers $SIG{'INT'} = 'malt_int'; $SIG{'TERM'} = 'malt_term'; $SIG{'HUP'} = 'malt_hup'; # Print banner and help if no arguments my $argc = $#ARGV + 1; if ($argc < 1) { print $progbanner. "\n". "Usage: maltfilter <pid filename> [config filename] [config filename...]\n". " maltfilter -f [config filename] [config filename...]\n". "-f turns on the full report mode.\n"; exit; } # Test pid file existence unless report mode $pid_file = shift; if ($pid_file eq "-f") { $reportmode = 1; } else { mdie("'$pid_file' already exists, not starting.\n". "If the daemon is NOT running, remove the pid-file and re-start.\n") if (-e $pid_file); } # Read configuration files while (defined(my $filename = shift)) { push(@configfiles, $filename); } malt_configure(); # Clean up certain arrays duplicate entries my %saw = (); @scanfiles = grep(!$saw{$_}++, @scanfiles_def); %saw = (); @noblock_ips = grep(!$saw{$_}++, @noblock_ips_def); undef(%saw); # Open logfile if ($settings{"DRY_RUN"}) { print $progbanner. "*********************************************\n". "* NOTICE! DRY-RUN MODE ENABLED! No changes *\n". "* will actually get committed to netfilter! *\n". "*********************************************\n"; } elsif ($settings{"LOGFILE"} ne "") { open($LOGFILE, ">>", $settings{"LOGFILE"}) or die("Could not open logfile '".$settings{"LOGFILE"}."' for writing!\n"); mlog(-1, "Log started\n"); } # Test existence of iptables if (! -e $settings{"IPTABLES"} || ! -x $settings{"IPTABLES"}) { mdie("iptables binary does not exist or is not executable: ".$settings{"IPTABLES"}."\n"); } mlog(-1, "Not blocking following IPs: ".join(", ", @noblock_ips)."\n"); # Initialize malt_init(); # Fork to background, unless dry-running if ($settings{"DRY_RUN"}) { if ($reportmode) { mlog(-1, "Outputting report files.\n"); generate_status($settings{"STATUS_FILE_PLAIN"}, 0); generate_status($settings{"STATUS_FILE_HTML"}, 1); malt_cleanup(); } else { malt_scan(); malt_cleanup(); } } else { if (my $pid = fork) { open(PIDFILE, ">", $pid_file) or mdie("Could not open pid file '".$pid_file."' for writing!\n"); print PIDFILE "$pid\n"; close(PIDFILE); } else { malt_scan(); malt_cleanup(); } }