changeset 0:fec14263801d

Initial import of maltfilter development version.
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 13 Aug 2009 15:15:18 +0300
parents
children ff602dc88d9e
files COPYING README example.conf example.init maltfilter
diffstat 5 files changed, 682 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/COPYING	Thu Aug 13 15:15:18 2009 +0300
@@ -0,0 +1,32 @@
+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)
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in
+    the documentation and/or other materials provided with the
+    distribution.
+
+ 3. The name of the author may not be used to endorse or promote
+    products derived from this software without specific prior written
+    permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Thu Aug 13 15:15:18 2009 +0300
@@ -0,0 +1,70 @@
+Malicious Attack Livid Termination Filter daemon (maltfilter) v0.7
+==================================================================
+Programmed by Matti 'ccr' Hämäläinen <ccr@tnsp.org>
+(C) Copyright 2009 Tecnic Software productions (TNSP)
+
+Distributed under the modified ("3-clause") BSD license. Please see
+included file COPYING for more information.
+
+About
+=====
+Automagic management script for adding and removing Netfilter/iptables
+filtering rules based on continuous logfile parsing for certain break-in
+and exploitation scanning attempts.
+
+Maltfilter daemon script continuously scans various system logfiles
+including auth.log, httpd logs, etc. for signs of malicious connections
+break-in and exploitation attempts. The originating IP addresses of
+these connections are then blocked via Netfilter (iptables).
+
+Requirements:
+
+ - Perl 5.8 or later
+ - Date::Parse (libtimedate-perl)
+ - Net::IP (libnet-ip-perl)
+
+
+Installation
+============
+Copy maltfilter script to /usr/sbin and set permissions
+
+$ cp maltfilter /usr/sbin/maltfilter
+$ chmod 755 /usr/sbin/maltfilter
+$ chown root:root /usr/sbin/maltfilter
+
+Copy example configuration under /etc (you may not want to
+to have the configuration readable to regular users, so below
+example sets mode 600 to it.)
+
+$ cp example.conf /etc/maltfilter.conf
+$ chmod 600 /etc/maltfilter.conf
+$ chown root:root /etc/maltfilter.conf
+
+
+Optional
+========
+Additionally you can set up the provided Debian style init script:
+
+$ cp example.init /etc/init.d/maltfilter
+$ chmod 755 /etc/init.d/maltfilter
+$ chown root:root /etc/init.d/maltfilter
+
+You need to edit the script, if you didn't install the configuration
+and maltfilter to paths described in installation section.
+
+
+Configuration and usage
+=======================
+See example.conf or /etc/maltfilter.conf for general settings.
+I HIGHLY recommend that you carefully think which 
+
+The script itself contains additional information about what
+certain scan options actually do.
+
+Start maltfilter either via the init script or through commandline:
+
+$ maltfilter /var/run/maltfilter.pid /etc/maltfilter.conf
+
+If you want to use the init script, you need to edit your init runlevel
+settings to enable it, for example in Debian/Ubuntu you can use rcconf(8)
+or chkconfig(8).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example.conf	Thu Aug 13 15:15:18 2009 +0300
@@ -0,0 +1,51 @@
+## Maltfilter configuration file.
+## PLEASE GO THROUGH THIS FILE VERY CAREFULLY!
+
+# Verbosity level (0 = quiet, bigger values add noise. valid range 0 - 4)
+VERBOSITY = 4
+
+# Dry-run: 1 = disables daemonization/forking to background, disables
+# modification of netfilter/iptables, printing the iptables commands to
+# stdout instead.
+# NOTICE! IF YOU DON'T CHANGE THIS TO 0, MALTFILTER WILL NOT DAEMONIZE!
+DRY_RUN = 1
+
+# Define system log files to scan. Only auth.log and Apache errorlog /
+# common log format files are supported for now. You can have as many
+# of SCANFILE settings as you wish.
+SCANFILE = "/var/log/auth.log"
+SCANFILE = "/var/log/httpd/error.log"
+SCANFILE = "/var/log/httpd/access.log"
+
+
+# Weeding treshold in hours. Entries older than this will be "weeded"
+# off from current netfilter settings.
+WEEDPERIOD = 72
+
+# How many "hits" the IP needs until it is eligible to be blocked.
+# (the "hits" can be from any "source", e.g. sshd crack, httpd, etc.)
+TRESHOLD = 3
+
+# Target iptables action for added entries, default is DROP, but you
+# can use whatever rule chain name you want to here.
+ACTION = "DROP"
+
+# Enabled checks (1 = enabled, 0 = disabled). Please read the test
+# descriptions from "check_log_line" function in the maltfilter script.
+CHK_SSHD            = 1
+CHK_KNOWN_CGI       = 1
+CHK_PHP_XSS         = 1
+CHK_PROXY_SCAN      = 1
+CHK_GOOD_HOSTS      = "example.org|google.com|74.125.45.100"
+
+# Maltfilter logfile path and name (set empty "" if you don't want logging)
+LOGFILE = "/var/log/maltfilter"
+  
+# Full path to iptables binary
+IPTABLES = "/sbin/iptables"
+  
+# IP(s) NOT to be blocked under any circumstances, separated by pipes (|).
+# You should set this if you wish to have a surefire open channel from
+# somewhere, even in case someone tries to spoof IPs for denial of service.
+# NOTICE! This setting supports only IPv4 addresses, no IPv6 or DNS names.
+NOBLOCK_HOSTS = "127.0.0.1|74.125.45.100"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example.init	Thu Aug 13 15:15:18 2009 +0300
@@ -0,0 +1,121 @@
+#! /bin/sh
+### BEGIN INIT INFO
+# Provides:          maltfilter
+# Required-Start:    $syslog $remote_fs
+# Required-Stop:     $syslog $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      1
+# Short-Description: Malicious Attack Livid Termination Filter
+### END INIT INFO
+# Author: Matti Hamalainen <ccr@tnsp.org>
+
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC="MALicious Termination Filter daemon (maltfilter)"
+NAME=maltfilter
+DAEMON="/usr/sbin/$NAME"
+CONFIG="/etc/maltfilter.conf"
+PIDFILE="/var/run/$NAME.pid"
+SCRIPTNAME="/etc/init.d/$NAME"
+
+# Exit if the package is not installed
+[ -x "$DAEMON" ] || exit 0
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+	# Return
+	#   0 if daemon has been started
+	#   1 if daemon was already running
+	#   2 if daemon could not be started
+	start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null || return 1
+	start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- $PIDFILE $CONFIG || return 2
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+	# Return
+	#   0 if daemon has been stopped
+	#   1 if daemon was already stopped
+	#   2 if daemon could not be stopped
+	#   other if a failure occurred
+	start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
+	RETVAL="$?"
+	[ "$RETVAL" = 2 ] && return 2
+
+	# Many daemons don't delete their pidfiles when they exit.
+	rm -f $PIDFILE
+	return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+	#
+	# If the daemon can reload its configuration without
+	# restarting (for example, when it is sent a SIGHUP),
+	# then implement that here.
+	#
+	start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
+	return 0
+}
+
+case "$1" in
+  start)
+	[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+	do_start
+	case "$?" in
+		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+	esac
+	;;
+  stop)
+	[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+	do_stop
+	case "$?" in
+		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+	esac
+	;;
+  reload|force-reload)
+	log_daemon_msg "Reloading $DESC" "$NAME"
+	do_reload
+	log_end_msg $?
+	;;
+  restart)
+	log_daemon_msg "Restarting $DESC" "$NAME"
+	do_stop
+	case "$?" in
+	  0|1)
+		do_start
+		case "$?" in
+			0) log_end_msg 0 ;;
+			1) log_end_msg 1 ;; # Old process is still running
+			*) log_end_msg 1 ;; # Failed to start
+		esac
+		;;
+	  *)
+	  	# Failed to stop
+		log_end_msg 1
+		;;
+	esac
+	;;
+  *)
+	echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
+	exit 3
+	;;
+esac
+
+:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/maltfilter	Thu Aug 13 15:15:18 2009 +0300
@@ -0,0 +1,408 @@
+#!/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 $progbanner =
+"Malicious Attack Livid Termination Filter daemon (maltfilter) v0.7\n".
+"Programmed by Matti 'ccr' Hamalainen <ccr\@tnsp.org>\n".
+"(C) Copyright 2009 Tecnic Software productions (TNSP)\n";
+
+#############################################################################
+### Settings / configuration
+#############################################################################
+my %settings = (
+  "VERBOSITY" => 4,
+  "DRY_RUN" => 1,
+  "WEEDPERIOD" => 72,
+  "TRESHOLD" => 3,
+  "ACTION" => "DROP",
+  "LOGFILE" => "/var/log/maltfilter",
+  "IPTABLES" => "/sbin/iptables",
+  "NOBLOCK_HOSTS" => "127.0.0.1",
+
+  "CHK_SSHD"            => 1,
+  "CHK_KNOWN_CGI"       => 1,
+  "CHK_PHP_XSS"         => 1,
+  "CHK_PROXY_SCAN"      => 1,
+  "CHK_GOOD_HOSTS"      => "",
+);
+
+# 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 @scanfiles = ();
+
+#############################################################################
+### Script code
+#############################################################################
+my %hitcount = ();
+my %iplist = ();
+my $pid_file = "";
+my $LOGFILE;
+
+sub mlog
+{
+  my $level = shift;
+  my $msg = shift;
+  if (defined($LOGFILE)) {
+    print $LOGFILE "[".scalar localtime()."] ".$msg if ($settings{"VERBOSITY"} > $level);
+  } else {
+    print $msg if ($settings{"VERBOSITY"} > $level);
+  }
+}
+
+sub check_hosts($$)
+{
+  my $host = $_[1];
+  my $ip = new Net::IP($host);
+  foreach my $test (split(/\s*\|\s*/, $_[0])) {
+    my $test_ip = new Net::IP($test);
+    if ($host eq $test) {
+      return 1;
+    }
+    if (defined($ip) && defined($test_ip)) {
+      if ($ip->binip() eq $test_ip->binip()) {
+        return 1;
+      }
+    }
+  }
+  return 0;
+}
+
+### Execute iptables
+sub exec_iptables(@)
+{
+  my @args = ($settings{"IPTABLES"}, @_);
+  if ($settings{"DRY_RUN"}) {
+    log(3, ":: ".join(" ", @args)."\n");
+  } else {
+    system(@args) == 0 or print join(" ", @args)." failed: $?\n";
+  }
+}
+
+### Get current Netfilter INPUT table entries we manage
+sub update_iplist($)
+{
+  open(STATUS, $settings{"IPTABLES"}." -v -n -L INPUT |") or
+    die("Could not execute ".$settings{"IPTABLES"}."\n");
+  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*$/) {
+      if (!defined($iplist{$2})) {
+        $hitcount{$2} = $settings{"TRESHOLD"};
+        $iplist{$2} = $_[0];
+        if ($_[0] >= 0) { mlog(2, "* $2 appeared in iptables, adding.\n"); }
+      }
+    }
+  }
+  close(STATUS);
+}
+
+### Weed out old entries
+sub check_time($)
+{
+  return ($_[0] >= time() - ($settings{"WEEDPERIOD"} * 60 * 60));
+}
+
+sub weed_do($)
+{
+  if (defined($iplist{$_[0]})) {
+    mlog(2, "* Weeding $_[0] ($iplist{$_[0]})\n");
+    exec_iptables("-D", "INPUT", "-s", $_[0], "-d", "0.0.0.0/0", "-j", $settings{"ACTION"});
+    undef($iplist{$_[0]});
+  }
+}
+
+sub weed_entries()
+{
+  foreach my $mip (keys %iplist) {
+    if (defined($iplist{$mip})) {
+      if ($iplist{$mip} >= 0) {
+        if (!check_time($iplist{$mip})) { weed_do($mip); }
+      } else {
+        weed_do($mip);
+      }
+    }
+  }
+}
+
+### Check if given "try count" exceeds treshold and if entry
+### is NOT in Netfilter already, then add it if so.
+sub check_add_entry($$$$)
+{
+  my $mip = $_[0];
+  my $mdate = str2time($_[1]);
+  my $mreason = $_[2];
+  my $mcond = $_[3];
+  
+  my $cnt = $hitcount{$mip}++;
+  if ($cnt >= $settings{"TRESHOLD"} && check_time($mdate)) {
+    my $pat;
+    if (!$mcond) {
+      mlog(2, "* Ignoring $mip: $mreason\n");
+      return;
+    }
+    if (!defined($iplist{$mip})) {
+      if (!check_hosts($settings{"NOBLOCK_HOSTS"}, $mip)) {
+        # Add entry that has >= treshold hits and is not added yet
+        mlog(1, "* Adding $mip ($mdate): $mreason\n");
+        exec_iptables("-I", "INPUT", "1", "-s", $mip, "-j", $settings{"ACTION"});
+      }
+      $iplist{$mip} = $mdate;
+    } else {
+      # Over treshold, but is added, check if we can update the timedate
+      if ($iplist{$mip} >= 0) {
+        if ($mdate > $iplist{$mip}) {
+          $iplist{$mip} = $mdate;
+        }
+      } else {
+        # Empty date, set it now.
+        $iplist{$mip} = $mdate;
+      }
+    }
+  }
+}
+
+### Check given logfile line for matches
+sub check_log_line($)
+{
+  # (1) SSH login scan attempts
+  if (/^(\S+\s+\d+\s+\d\d:\d\d:\d\d)\s+\S+\s+sshd\S*?: Failed password for invalid user \S+ from (\d+\.\d+\.\d+\.\d+)/) {
+    check_add_entry($2, $1, "SSHD", $settings{"CHK_SSHD"});
+  }
+  # (2) Common/known exploitable CGI/PHP software scans (like phpMyAdmin)
+  # NOTICE! This matches ERRORLOG, thus it only works if you DO NOT have
+  # any or some of these installed. Preferably none, or use uncommon
+  # paths and prefixes.
+  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_entry($mip, $mdate, "CGI: $tmp", $settings{"CHK_KNOWN_CGI"});
+      }
+    }
+  }
+  # Match Apache common logging format GET requests here
+  elsif (/(\d+\.\d+\.\d+\.\d+)\s+-\s+-\s+\[(.+?)\]\s+\"GET (\S*?) HTTP\//) {
+    my $mdate = $2;
+    my $mip = $1;
+    my $merr = $3;
+    
+    # (3) Simple match for generic PHP XSS vulnerability scans
+    # NOTICE! If your site genuinely uses (checked) PHP parameters with
+    # URIs, you should set CHK_GOOD_HOSTS to match your hostname(s)/IP(s)
+    # used in the URIs.
+    if ($merr =~ /\.php\?\S*?=http:\/\/([^\/]+)/) {
+      if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) {
+        check_add_entry($mip, $mdate, "PHP XSS: $merr", $settings{"CHK_PHP_XSS"});
+      }
+    }
+    # (4) Try to match proxy scanning attempts
+    elsif ($merr =~ /^http:\/\/([^\/]+)/) {
+      if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) {
+        check_add_entry($mip, $mdate, "Proxy scan: $merr", $settings{"CHK_PROXY_SCAN"});
+      }
+    }
+  }
+}
+
+
+###
+### Utility functions
+###
+my %logfile = ();
+
+sub malt_init {
+  foreach my $logname (@scanfiles) {
+    local *INFILE;
+    mlog(0, "- Parsing ".$logname." ...\n");
+    open(INFILE, "<", $logname) or die("Could not open '".$logname."'!\n");
+    $logfile{$logname} = *INFILE;
+    while (<INFILE>) {
+      chomp;
+      check_log_line($_);
+    }
+  }
+
+  mlog(0, "- Weeding out old entries.\n");
+  weed_entries();
+}
+
+sub malt_cleanup {
+  mlog(0, "- Closing open filehandles.\n");
+  foreach my $logname (keys %logfile) {
+    close($logfile{$logname});
+  }
+}
+
+sub malt_scan {
+  ### Keep on reading
+  mlog(1, "- Entering main scanning loop.\n");
+  my $counter = 0;
+  while (1) {
+    my %logpos = ();
+    foreach my $logname (keys %logfile) {
+      for ($logpos{$logname} = tell($logfile{$logname}); $_ = <$logfile{$logname}>; $logpos{$logname} = tell($logfile{$logname})) {
+        chomp;
+        check_log_line($_);
+      }
+    }
+    sleep(5);
+    if ($counter++ >= 5) {
+      # 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_iplist(time());
+      weed_entries();
+    }
+    foreach my $logname (keys %logfile) {
+      seek($logfile{$logname}, $logpos{$logname}, 0);
+    }
+  }
+}
+
+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);
+}
+
+sub malt_int {
+  mlog(-1, "\nCaught Interrupt (^C), aborting.\n");
+  malt_cleanup();
+  malt_finish();
+  exit(1);
+}
+
+sub malt_term {
+  mlog(-1, "Receinved TERM, quitting.\n");
+  malt_cleanup();
+  malt_finish();
+  exit(1);
+}
+
+sub malt_hup {
+  mlog(-1, "Received HUP, reinitializing.\n");
+  malt_cleanup();
+  malt_init();
+  mlog(-1, "Reinitialization finished, resuming scanning.\n");
+}
+
+
+###
+### Main program
+###
+# Setup signal handlers
+$SIG{'INT'} = 'malt_int';
+$SIG{'TERM'} = 'malt_term';
+$SIG{'HUP'} = 'malt_hup';
+
+# Banner
+my $argc = $#ARGV + 1;
+if ($argc < 1) {
+  print $progbanner.
+  "\nUsage: maltfilter <pid filename> [config filename]\n";
+  exit;
+}
+
+# Test pid file existence
+$pid_file = shift;
+die("'$pid_file' already exists, not starting.\nIf the daemon is NOT running, remove the pid-file and re-start.\n") if (-e $pid_file);
+
+# Read configuration file
+if (defined(my $config_file = shift)) {
+  my $errors = 0;
+
+  # Let user define his/her own logfiles to scan
+  undef(@scanfiles_def);
+
+  open(CONFFILE, "<", $config_file) or die("Could not open configuration '".$config_file."'!\n");
+  while (<CONFFILE>) {
+    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 {
+        print STDERR "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 (defined($settings{$key})) {
+        $settings{$key} = $value;
+      } else {
+        print STDERR "Unknown setting '$key' = '$value'\n";
+        $errors = 1;
+      }
+    } else {
+      print STDERR "Syntax error: $_\n";
+      $errors = 1;
+    }
+  }
+  close(CONFFILE);
+  die("Errors in configuration file '$config_file', bailing out.\n") unless ($errors == 0);
+}
+
+# Clean up scanfiles from duplicate entries
+my %saw = ();
+@scanfiles = grep(!$saw{$_}++, @scanfiles_def);
+
+
+# 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");
+}
+
+
+# Initialize
+update_iplist(-1);
+malt_init();
+
+# Fork to background, unless dry-running
+if ($settings{"DRY_RUN"}) {
+  malt_scan();
+  malt_cleanup();
+} else {
+  if (my $pid = fork) {
+    open(PIDFILE, ">", $pid_file) or die("Could not open pid file '".$pid_file."' for writing!\n");
+    print PIDFILE "$pid\n";
+    close(PIDFILE);
+  } else {
+    malt_scan();
+    malt_cleanup();
+  }
+}