#!/usr/bin/perl -Tw # Active IP blocker on iptables firewall with Snort, # and a general syslog parser and responder # Yunliang Yu 2/2005. # Based on guardian-1.7. http://www.chaotic.org/guardian/ # # Copyright (C) 2005, Yunliang Yu # # This program is released under GPL. # # 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. # package Guardian2; sub usage { print <<'EOF'; Guardian2 $Id: guardian2.pl,v 1.267 2008/05/15 14:57:32 yu Exp yu $ guardian2.pl [-hdDqa] [-c conf_file] [-r rule_file] -h shows help & version -d run in debug mode (doesn't fork, output goes to STDOUT) -D run in debug mode, and don't actually block any host -q a little bit quiet when in debug mode -f run in the foreground, do not fork -c specifiy a configuration file other than the default conf -r specifiy a rule file other than the default one -a scan all lines in the alert file, not just snort logs, for matches This program runs on your firewall or syslog server to watch over the logs and blocks any remote hosts (or runs any command) whenever a match occurs. Please see guardian.conf for configure info, or run the command 'guardian2.pl -D' to experiment with it without blocking anything. Please email bugs/ideas to yu@math.duke.edu . EOF exit; } use strict; $ENV{'PATH'} = '/bin:/usr/bin:/sbin:/usr/sbin'; $ENV{'IFS'} = " \t\n"; use Getopt::Std; use Sys::Syslog; use Socket; use Term::ANSIColor; my $dir = $0; $dir =~ s,/[^/]*$,,; $dir = '.' if $dir eq $0; if ( $dir =~ /(.+)/ ) { $dir = $1; } my %opts = (); &getopts( 'hc:r:dDqfa', \%opts ); my $Debug = $opts{D}; my $debug = $opts{d} || $Debug; my $all = $opts{a}; my $quiet = $opts{q}; my $conf = $opts{c} ? $opts{c} : '/etc/guardian.conf'; &usage() if $opts{h}; my ( $interface, $logfile, $alertfile, @alertfiles, $commandfile, $timelimit, $maxblocks, $pullcommand, @pullcommand, $pulldumpfile, $pullinterval, $pullsec, $blockpath, $unblockpath, $resetpath, $listblocked, $perlinit, %ignoreIPs, @ignoreRegex, @ignorehostRegex, $ignorehostDefaultRegex, @rules, $nrules, %blocks, %def, %dns, $cmdinode, $cmdsize, $counter, $r, @match, $time ); my $ipRegex = qr/([1-9]\d*\.\d+\.\d+\.\d+)(?::(\d+))?/o; my $snortLogRegex = '\ssnort(?:\[\d+\])?:\s+'; my $hostid = `hostid`; chop $hostid; $hostid = hex($hostid); $hostid = $$ if !$hostid; $| = 1 if $debug; my $reload = 0; RELOAD: $interface = ''; $logfile = ''; $alertfile = ''; @alertfiles = (); $commandfile = ''; $timelimit = 0; $maxblocks = 0; $pullcommand = ''; @pullcommand = (); $pullinterval = 0; $pullsec = 0; $blockpath = ''; $unblockpath = ''; $resetpath = ''; $listblocked = 0; $perlinit = ''; %ignoreIPs = (); @ignoreRegex = (); @ignorehostRegex = (); $ignorehostDefaultRegex = ''; @rules = (); $nrules = 0; %blocks = (); %def = (); %dns = (); $pulldumpfile = ''; $cmdinode = 0; $cmdsize = 0; $counter = 0; $time = time; &init_env(); &load_confs(); &sig_handler_setup(); $r = $reload ? 'restart' : 'start'; if ($Debug) { print "\n*** Perl init to eval: $perlinit" if $perlinit && !$reload; print "\n*** Reset command to run: $resetpath $r\n\n"; } else { if ( $perlinit && $perlinit =~ /(.+)/s && !$reload ) { package Eavl; eval($1); } system("$resetpath $r 2>/dev/null"); } if ( !$debug ) { if ( !$opts{f} ) { print "Becoming a daemon..\n"; &daemonize() if !$reload; } else { print "Run in the foreground...\n"; &write_log("Guardian2 process id $$"); } } else { $all = $all ? 'scan all lines...' : ''; print "Running in debug mode...$all\n"; } $reload = 0; while (1) { $time = time; foreach my $ah (@alertfiles) { my $fh = $ah->{h}; if ( seek( $fh, 0, 1 ) ) { while (<$fh>) { print "." if $debug && !$quiet; if ( !$ah->{t} && !$all ) { if (/\[\*\*\]\s+(.*)\s+\[\*\*\]/) { $ah->{t} = 1; print " type=snort.alert " if $debug; } elsif (/$snortLogRegex/o) { $ah->{t} = 2; print " type=syslog " if $debug; } else { next; } } next if $all && / guardian: /o; foreach $r (@rules) { if ( &matchRegex( $_, $r->{rule}->[ $ah->{t} ], 1 ) ) { my $i = 0; my @a = ( '', '', 0, '', '', 0, $match[0] . '#' . $r->{i} ); unshift @match, $_; if ( $r->{src} || $r->{sport} || $r->{dst} || $r->{dport} ) { if ( $r->{src} && /$r->{src}/ && $1 ) { $a[0] = $1; if ( $a[0] =~ /[^\d\.]/ ) { $a[1] = $a[0]; if ( exists $dns{ $a[1] } ) { $a[0] = $dns{ $a[1] }; } else { $a[0] = $dns{ $a[1] } = join ".", unpack( "C4", gethostbyname( $a[1] ) ); } } } if ( $r->{sport} && /$r->{sport}/ && $1 ) { $a[2] = $1; } if ( $r->{dst} && /$r->{dst}/ && $1 ) { $a[3] = $1; if ( $a[3] =~ /[^\d\.]/ ) { $a[4] = $a[3]; if ( exists $dns{ $a[4] } ) { $a[3] = $dns{ $a[4] }; } else { $a[3] = $dns{ $a[4] } = join ".", unpack( "C4", gethostbyname( $a[4] ) ); } } } if ( $r->{dport} && /$r->{dport}/ && $1 ) { $a[5] = $1; } } else { s%$ipRegex% $a[$i++]=$1; $a[$i++]=''; $a[$i++]=$2?$2:0; $&%seg ; } print "\n$_==>$a[0]($a[1]):$a[2] -> $a[3]($a[4]):$a[5] $a[6]\n" if $debug && !$quiet; &blockit( $r, @a ); last; } } } } } if ( $commandfile && seek( CMD, 0, 1 ) ) { while () { print "c" if $debug && !$quiet; if (/^\s*(\w+)\s+$ipRegex\s+(\d+)\s+(.*)/) { my $act = $1; my $src = $2; my $prt = $3 ? $3 : 0; my $sec = $4 ? $4 : 0; my $msg = $5 ? $5 : ''; my $ri = 0; print "Cmd: $act s:$src p:$prt s:$sec m:$msg\n" if $debug && !$quiet; $msg =~ s%#i(\d+)%$ri=$1;''%seg; if ( !$ri ) { $ri = ++$nrules; } &blockit( { act => $act, sec => $sec, i => $ri, cmd => 1 }, ( $src, '', $prt, '', '', 0, 'CMD: ' . $msg ) ); } elsif (/\S/) { &write_log( "CMD error: " . $_ ); } } } if ($reload) { &write_log("Reloading conf files...."); goto RELOAD; } elsif ( $counter++ >= 30 ) { local $SIG{INT} = 'IGNORE'; local $SIG{HUP} = 'IGNORE'; local $SIG{USR1} = 'IGNORE'; local $SIG{USR2} = 'IGNORE'; Sys::Syslog::disconnect if ( !$logfile && !$debug ); &remove_blocks(); &pull_command() if $pullinterval; &check_log_name(); if ($listblocked) { &list_blocks2(); $listblocked = 0; } $counter = 0; } sleep 1; } sub blockit { my ( $r, $source, $host, $sport, $dest, $desthost, $dport, $mesage ) = @_; &init( $source, $host, $sport, $dest, $desthost, $dport ); return 1 if $source && $ignoreIPs{$source}; $time = time; my $blockit = 1; my $ignoreit = ''; my $dest1 = ''; if ($dest) { if ( !$desthost ) { if ( exists $dns{$dest} ) { $desthost = $dns{$dest}; } else { $desthost = $dns{$dest} = gethostbyaddr( inet_aton($dest), AF_INET ); } } $dest1 = ' -> ' . $dest; $dest1 .= " ($desthost)" if $desthost; } if ( &matchRegex( $source, \@ignoreRegex ) ) { $ignoreit = 'source'; } if ( !$blocks{$source} ) { if ( $source && !$host ) { if ( exists $dns{$source} ) { $host = $dns{$source}; } else { $host = $dns{$source} = gethostbyaddr( inet_aton($source), AF_INET ); } } if ( $host && !$ignoreit && &matchRegex( $host, \@ignorehostRegex ) ) { $ignoreit = 'host' if ( !$r->{cmd} || $host !~ /$ignorehostDefaultRegex/ ) ; } $blocks{$source} = { host => $host ? $host : '', blk => 0, sec => 0, msg => '', r => {}, }; } my $b = $blocks{$source}; my $i = $r->{i}; if ( !$b->{r}->{$i} ) { my %h = %$r; delete $h{rule}; delete $h{over}; delete $h{trak}; $b->{r}->{$i} = { r => \%h, cnttime => {}, }; $h{blk} = 0 if !$h{k}; $h{c} = 0; foreach my $o ( @{ $r->{over} } ) { if ( !$o->{dest} && ( &matchRegex( $source, $o->{rule} ) || $b->{host} && &matchRegex( $b->{host}, $o->{rule} ) ) || $o->{dest} && $dest && ( &matchRegex( $dest, $o->{rule} ) || $desthost && &matchRegex( $desthost, $o->{rule} ) ) ) { $h{act} = $o->{act} if $o->{act}; $h{sec} = $o->{sec} if $o->{sec}; $h{tag} = $r->{tag} ? $r->{tag} . ':' . $o->{tag} : $o->{tag} if $o->{tag}; $h{cti} = $o->{cti} if ref( $o->{cti} ) && @{ $o->{cti} }; $h{o} = 1; $mesage .= '.' . $o->{i}; last; } } } my %m = (); $m{act} = $r->{act}; $m{sec} = $r->{sec}; $m{src} = $source; $m{shost} = $host; $m{sport} = $sport; $m{dst} = $dest; $m{dhost} = $desthost; $m{dport} = $dport; $m{tag} = $r->{tag}; if ( 1 < @match ) { $m{_} = $match[0]; $m{_} =~ s/\s+$//s; $m{0} = $match[1]; map { $m{ $_ - 1 } = $match[$_] } ( 2 .. $#match ); $m{rn} = sprintf( "#i%d%02d", $hostid, $i ); } if ( $blockit && $r->{perl} ) { my $x = $r->{perl}; $x =~ s/\$\{(_|\d+|src|shost|sport|dst|dhost|dport|act|sec|rn|tag)\}/\$m{$1}/sg; if ($Debug) { print "Perl to eval: $r->{perl}\n"; } elsif ( $x =~ /(.+)/s ) { package Eavl; return 1 if !eval($1); } } my $bri = $b->{r}->{$i}; my $brir = $bri->{r}; my $srchost = $source ? " $source ($b->{host})" : ""; if ( ref( $brir->{cti} ) && @{ $brir->{cti} } ) { $blockit = 0; my %x = (); $brir->{c}++; foreach my $x ( @{ $brir->{cti} } ) { my $cnp = $x->{cnp}; my $k = $cnp =~ /H/i ? $dest : $cnp =~ /P/i ? $dport : $brir->{c}; if ( !$x{$cnp}{$k} ) { $x{$cnp}{$k} = 1; $bri->{cnttime}->{$cnp}->{$k} = $time; } my $l = $x->{int} ? $time - $x->{int} : 0; my @a = grep( $_ >= $l, values %{ $bri->{cnttime}->{$cnp} } ); if ( $x->{cnt} <= @a ) { $blockit = 1 if !$brir->{k}; $bri->{cnttime} = {}; $brir->{c} = 0; last; } elsif ( $brir->{k} && 1 == @a ) { $blockit = 1; last; } elsif ( $debug && !$quiet ) { my $s = $x->{cnt} - @a; &write_log( "Not to block yet, wait for $s more $cnp in $x->{int} seconds" ); } } } if ( $ignoreit && !$brir->{o} && $blockit ) { &write_log("Ignore[$ignoreit]$srchost: $mesage$dest1"); delete $blocks{$source}; return 1; } $b->{msg} .= $match[0] if $match[0]; if ($blockit) { my $trak = $brir->{k} ? "Trak#$brir->{k} " : ''; my $per = $trak ? 'for' : 'per'; if ( !$trak && $brir->{sec} != 1 && $brir->{blk} && ( $brir->{blk} + $brir->{sec} > $time || $brir->{act} =~ /DROP/ ) ) { my $s = $brir->{blk} + $brir->{sec} - $time; if ( $brir->{act} =~ /DROP/ ) { $brir->{blk} = $b->{blk} = $time; $b->{sec} = $time + $brir->{sec}; &write_log( "Already blocked for another $s seconds, just update blocking time" ) if $debug && !$quiet; } else { &write_log("Already blocked for another $s seconds") if $debug && !$quiet; } return 1; } if ( !$brir->{t} && $source && $r->{trak} && @{ $r->{trak} } && !$r->{k} ) { foreach my $o ( @{ $r->{trak} } ) { my $kadd = 1; foreach my $l (@rules) { if ( $l->{k} && $l->{i} == $o->{i} && $l->{ip} && $l->{ip} eq $source ) { $l->{blk} = $time; $kadd = 0; last; } } $brir->{t} = 1; next if !$kadd; my %t = %$o; $t{ip} = $source; $t{host} = $host; delete $t{rule}; foreach my $l ( @{ $o->{rule} } ) { my %k = %$l; $k{r} =~ s/\$(src|shost|sport|dst|dhost|dport|act|sec)\b/\Q$m{$1}\E/g; push @{ $t{rule} }, \%k; } push @rules, \%t; $nrules++; $t{blk} = $time; } } if ( $brir->{msg} ) { $mesage =~ s/^.*(#.*)$/$brir->{msg}$1/s; } if (1) { if ( $brir->{tag} ) { $m{tag} = $brir->{tag}; $m{tag} =~ s/\$(_|\d+|src|shost|sport|dst|dhost|dport|act|sec|rn)\b/$m{$1}/sg; } $mesage =~ s/\$(_|\d+|src|shost|sport|dst|dhost|dport|act|sec|rn|tag)\b/$m{$1}/sg ; } if ( $r->{final} ) { my $x = $r->{final}; $x =~ s/\$\{(_|\d+|src|shost|sport|dst|dhost|dport|act|sec|rn|tag)\}/\$m{$1}/sg; if ($Debug) { print "Perl final to eval: $r->{final}\n"; } elsif ( $x =~ /(.+)/s ) { package Eavl; eval($1); } } if ( $brir->{act} =~ /DROP/ ) { &write_log( "${trak}Block$srchost for $brir->{sec} seconds: $mesage$dest1"); if ( $source =~ /(.+)/ ) { $source = $1; } ; $m{tag} = '' if !$m{tag}; $m{tag} =~ s/'/'\\''/sg; if ( $m{tag} =~ /(.+)/ ) { $m{tag} = $1; } ; if ($Debug) { print "*** Block command to run: $blockpath $source $interface '$m{tag}'\n"; } else { system("$blockpath $source $interface '$m{tag}' 2>/dev/null") if $source; } $b->{blk} = $time; $b->{sec} = $time + $brir->{sec}; $listblocked++; } elsif ( $brir->{act} =~ /LOG/ ) { &write_log( "${trak}Log$srchost $per $brir->{sec} seconds: $mesage$dest1"); } elsif ( $brir->{act} =~ /SYS/ && @{ $brir->{sys} } ) { my @a = @{ $brir->{sys} }; my $s = ''; if ( 1 < @match ) { map { s/\$(_|\d+|src|shost|sport|dst|dhost|dport|act|sec|rn|tag)\b/$m{$1}/sg; } @a; } if ( $a[0] =~ /^(ECHO|BELL)$/ ) { $s = $a[2] ? $a[2] . "\n" : "${trak}$a[0]$srchost $mesage$dest1\n"; if ( $a[0] eq 'BELL' ) { my $n = ( $a[1] && $a[1] =~ /^\d+$/ ) ? $a[1] : 1; for ( ; $n > 0 ; $n-- ) { print "\a"; } $a[1] = 'red'; } if ( $a[1] ) { print colored( $s, $a[1] ); } else { print $s; } } elsif ($Debug) { print "*** Sys command to run: " . ( join ' ', @a ) . "\n"; } else { map { $_ = $1 if /(.*)/; } @a; eval { if ( $a[0] eq '|' ) { shift @a; if ( open( PIN, '|-', @a ) ) { print PIN $b->{msg}; close(PIN); } } else { system(@a); } }; $s = $@ ? "Failed $@" : 'OK'; &write_log( "${trak}SYS $a[0] $s$srchost $per $brir->{sec} seconds: $mesage$dest1" ); } } else { &write_log( "${trak}Noop$srchost $per $brir->{sec} seconds: $mesage$dest1") ; } $brir->{blk} = $time if !$trak; $b->{msg} = ''; } } sub src_cmp_sub { my $k = shift; my $b = $blocks{$k}; return 0 if $b->{blk} >= $time; return 99 if $b->{sec} <= $b->{blk}; ( $time - $b->{blk} ) / ( $b->{sec} - $b->{blk} ); } sub remove_blocks { my $n = 0; my ( $source, $i, $j, $k, $l, $brir, $blksec ); foreach $source ( sort { &src_cmp_sub($a) <=> &src_cmp_sub($b) } keys %blocks ) { my $b = $blocks{$source}; my $br = $b->{r}; my $blocked = 0; foreach $i ( keys %$br ) { $brir = $br->{$i}->{r}; $blksec = $brir->{blk} + $brir->{sec}; if ( $brir->{act} =~ /DROP/ ) { if ( $brir->{blk} && $blksec > $blocked ) { $blocked = $blksec; }; } if ( $brir->{blk} && $blksec <= $time || !$brir->{blk} && ( !ref( $brir->{cti} ) || !@{ $brir->{cti} } ) ) { delete $br->{$i}; } } if ($blocked) { if ( $n++ > $maxblocks || $blocked <= $time ) { if ( $n > $maxblocks ) { &write_log( "Unblock $source ($b->{host}) because of maxblocks $n>$maxblocks" ) if !$quiet; } else { &write_log("Unblock $source ($b->{host})") if !$quiet; } if ($Debug) { print "*** Unblock command to run: $unblockpath $source $interface\n"; } else { system("$unblockpath $source $interface 2>/dev/null") if $source; } foreach $i ( keys %$br ) { next if $br->{$i}->{r}->{act} !~ /DROP/; delete $br->{$i}; } $n--; $listblocked++; } else { $k = $time; $j = 0; foreach $i ( keys %$br ) { next if $br->{$i}->{r}->{act} !~ /DROP/; $brir = $br->{$i}->{r}; $blksec = $brir->{blk} + $brir->{sec}; if ( $brir->{blk} < $k ) { $k = $brir->{blk}; } if ( $blksec > $j ) { $j = $blksec; }; } $b->{blk} = $k; $b->{sec} = $j; } } foreach $i ( keys %$br ) { next if ( !ref( $br->{$i}->{r}->{cti} ) || !@{ $br->{$i}->{r}->{cti} } || !ref( $br->{$i}->{cnttime} ) || !%{ $br->{$i}->{cnttime} } ); $l = 0; foreach my $x ( @{ $br->{$i}->{r}->{cti} } ) { $l = $x->{int} if $x->{int} > $l; } $l = $time - $l if $l; foreach $j ( keys %{ $br->{$i}->{cnttime} } ) { foreach $k ( keys %{ $br->{$i}->{cnttime}->{$j} } ) { delete $br->{$i}->{cnttime}->{$j}->{$k} if $br->{$i}->{cnttime}->{$j}->{$k} < $l; } delete $br->{$i}->{cnttime}->{$j} if !%{ $br->{$i}->{cnttime}->{$j} }; } delete $br->{$i} if !%{ $br->{$i}->{cnttime} }; } delete $blocks{$source} if !%{$br}; } $i = 0; foreach $k (@rules) { if ( $k->{k} && $k->{blk} + $k->{sec} <= $time ) { $i++; last; } } if ($i) { my @k = @rules; @rules = (); $nrules = 0; foreach $k (@k) { if ( !$k->{k} || $k->{blk} + $k->{sec} > $time ) { push @rules, $k; $nrules++; } else { &init( $k->{host}, $k->{ip} ); print "*** clear out track#$k->{i} for $k->{ip} ($k->{host}) of rule#$k->{k}\n" if $debug && !$quiet; } } } %dns = () if $time % 86400 < 30; if ($n) { &write_log("There are $n blocks") if ( !$quiet && $time % 14400 < 30 ); } } sub list_blocks { my ( $source, $l, $i ); &dump( \@rules, \%blocks ) if !$quiet; &write_log('***** List blocks started *****'); foreach $source ( sort { $blocks{$b}->{blk} <=> $blocks{$a}->{blk} } keys %blocks ) { my $b = $blocks{$source}; my $s = ''; if ( !$quiet ) { &write_log( $source ? qq{$source ($b->{host}):} : 'Default:' ); foreach $l ( sort keys %{ $b->{r} } ) { my $brir = $b->{r}->{$l}->{r}; my $tak = $brir->{k} ? $brir->{k} . '.' : ""; if ( $brir->{blk} ) { $s = ( $brir->{blk} + $brir->{sec} ) - $time; } else { $s = 'wait...'; next if !ref( $brir->{cti} ); foreach my $x ( @{ $brir->{cti} } ) { my $cnp = $x->{cnp}; my $bri = $b->{r}->{$l}; my $lt = $x->{int} ? $time - $x->{int} : 0; my @a = grep( $_ >= $lt, values %{ $bri->{cnttime}->{$cnp} } ); $s .= ( $x->{cnt} - scalar(@a) ) . $cnp . '/' . $x->{int} . ' '; } } $s = sprintf( " #%-5s %-7s %-24s : %-14s seconds", $tak . $l, $brir->{act}, $brir->{blk} ? scalar( localtime( $brir->{blk} ) ) : '', $s ); &write_log($s); } } else { if ( $b->{blk} ) { $s = $source ? "$source ($b->{host}): " : "Default: "; $s .= scalar( localtime( $b->{blk} ) ) . ' : '; $s .= $b->{sec} - $time . " seconds"; &write_log($s); } } } &write_log('***** List blocks ended *****'); } sub list_blocks2 { if ( open( LB2, ">/tmp/guardian.lck" ) ) { foreach my $source ( sort { $blocks{$b}->{blk} <=> $blocks{$a}->{blk} } keys %blocks ) { my $b = $blocks{$source}; if ( $source && $b->{blk} ) { print LB2 "$source $b->{sec}\n"; } } print LB2 "OK\n"; close(LB2); rename "/tmp/guardian.lck", "/tmp/guardian.out"; } else { &write_log("failed to write to /tmp/guardian.lck"); } } sub check_log_name { foreach my $ah (@alertfiles) { my @s = stat( $ah->{f} ); if ( $s[1] != $ah->{inode} || $s[7] < $ah->{size} ) { close( $ah->{h} ); open( $ah->{h}, "<$ah->{f}" ); &write_log("Reopening $ah->{f}"); } $ah->{inode} = $s[1]; $ah->{size} = $s[7]; } if ($commandfile) { my @s = stat($commandfile); if ( $s[1] != $cmdinode || $s[7] < $cmdsize ) { close(CMD); open( CMD, "<$commandfile" ); &write_log("Reopening $commandfile"); } $cmdinode = $s[1]; $cmdsize = $s[7]; } } sub pull_command { return if !$pullinterval || !@pullcommand; return if $pullsec + $pullinterval > $time - 60; $pullsec = $time; print "p" if $debug && !$quiet; my @sources = (); my $ok = 0; my $pid = 0; if ($pulldumpfile) { $pulldumpfile = '' if ( !-f $pulldumpfile || !open( DUMP, '>' . $pulldumpfile ) ); } eval { local $SIG{ALRM} = sub { kill 'KILL', $pid if $pid; die "alarm\n"; }; alarm 5; if ( $pid = open( OUT, '-|', @pullcommand ) ) { while () { push @sources, $1 if /^$ipRegex/s; $ok = 1 if /OK/; print DUMP if $pulldumpfile; } close(OUT); $pid = 0; } alarm 0; }; close(DUMP) if $pulldumpfile; if ( $@ || !$ok ) { &write_log( "PullCommand error: " . $@ ); } elsif (@sources) { my $r = $rules[0]; foreach my $source (@sources) { foreach my $o ( @{ $r->{trak} } ) { next if !$o->{g}; my $kadd = 1; foreach my $l (@rules) { if ( $l->{k} && $l->{i} == $o->{i} && $l->{ip} && $l->{ip} eq $source ) { $l->{blk} = $time; $kadd = 0; last; } } next if !$kadd; my %t = %$o; $t{ip} = $source; delete $t{rule}; foreach my $l ( @{ $o->{rule} } ) { my %k = %$l; $k{r} =~ s/\$src\b/\Q$source\E/g; push @{ $t{rule} }, \%k; } push @rules, \%t; $nrules++; $t{blk} = $time; print "**** pull_comand: add trak $t{i} to $source.\n" if $debug && !$quiet; } } } } sub sig_handler_setup { $SIG{TERM} = \&clean_up_and_exit; $SIG{QUIT} = \&clean_up_and_exit; $SIG{INT} = \&clean_up_and_exit; $SIG{HUP} = \&flush_and_reload; $SIG{USR1} = \&list_blocks; } sub clean_up_and_exit { &write_log("received kill sig.. shutting down"); if ($Debug) { print "*** Rest command to run: $resetpath stop\n"; } else { system("$resetpath stop 2>/dev/null"); } %blocks = (); exit; } sub flush_and_reload { $reload = 1; } sub write_log { my $message = shift; return if !$message; if ($debug) { print $message, "\n"; } elsif ($logfile) { my $date = localtime(); open( LOG, ">>$logfile" ); print LOG $date, ' guardian: ', $message, "\n"; close(LOG); } else { openlog( 'guardian', '', 'user' ); syslog( 'info', $message ); closelog(); } } sub init_env { my $s = `ifconfig -a 2>/dev/null`; $s =~ s%$ipRegex% $ignoreIPs{$1}=1; $&%seg; $s = `netstat -rn 2>/dev/null`; $s =~ s%$ipRegex% $ignoreIPs{$1}=1; $&%seg; if ( -r '/etc/resolv.conf' ) { $s = `cat /etc/resolv.conf 2>/dev/null`; $s =~ s%$ipRegex% $ignoreIPs{$1}=1; $&%seg; } foreach ( split( /:/, $dir . ':' . $ENV{PATH} ) ) { if ( -x "$_/guardian_block.sh" && /(.+)/ ) { $blockpath = "$1/guardian_block.sh"; $unblockpath = "$1/guardian_unblock.sh"; $resetpath = "$1/guardian_reset.sh"; last; } } die "Can't find guardian_block.sh" if ( !$blockpath || !-x $blockpath ); die "Can't find guardian_unblock.sh" if ( !$unblockpath || !-x $unblockpath ); die "Can't find guardian_reset.sh" if ( !$resetpath || !-x $resetpath ); if ($debug) { print "IP#s to be ignored:\n"; foreach ( sort keys %ignoreIPs ) { print "\t$_\n"; } } } sub load_confs { $conf = $dir . '/guardian.conf' if !-s $conf; die "No proper config file $conf found." if !-s $conf; open( CONF, "<$conf" ) or die "Cannot read the config file $conf, $!\n"; my $RuleFile = ''; my $IgnoreFile = ''; while () { next if /^\s*(?:$|#)/; if (/Interface\s+(\S+)/) { $interface = $1; } elsif (/LogFile\s+(\S+)/) { $logfile = $1; } elsif (/AlertFiles\s+(.*\S)/) { $alertfile = $1; } elsif (/IgnoreFile\s+(\S+)/) { $IgnoreFile = $1; } elsif (/RuleFile\s+(\S+)/) { $RuleFile = $1; } elsif (/CommandFile\s+(\S+)/) { if ( -r $1 ) { $commandfile = $1; } elsif ( -r $dir . '/guardian.cmd' ) { $commandfile = $dir . '/guardian.cmd'; } if ($commandfile) { my @s = stat($commandfile); $cmdinode = $s[1]; $cmdsize = $s[7]; } } elsif (/TimeLimit\s+(\d+)/) { $timelimit = $1; } elsif (/MaxBlocks\s+(\d+)/) { $maxblocks = $1; } elsif (/PullInterval\s+(\d+)(\D?)/) { $pullinterval = $1; &toSecs( $pullinterval, $2 ); } elsif (/PullCommand\s+(.*\S)/) { @pullcommand = &parseSYS($1); $pullcommand = join ' ', @pullcommand; } elsif (/PullDumpFile\s+(.*\S)/) { $pulldumpfile = $1; } } close(CONF); $pullinterval = 300 if $pullinterval < 300; $pullinterval = 0 if !@pullcommand; die "No Interface given in $conf" if !$interface; foreach ( grep /\S/, split /\s+/, $alertfile ) { foreach my $f ( glob $_ ) { next if ( !-f $f || !-r $f ); my @s = stat($f); next if !$s[1]; if ( open( my $h, "<$f" ) ) { seek( $h, 0, 2 ); push @alertfiles, { f => $f, h => $h, inode => $s[1], size => $s[7], t => 0, }; } } } die "No valid AlertFiles given in $conf" if !@alertfiles; $alertfile = join ' ', map { $_->{f} } @alertfiles; if ($commandfile) { open( CMD, $commandfile ) or die "can't open command file: $commandfile: $!\n"; seek( CMD, 0, 2 ); } $logfile = $dir . '/guardian.log' if $logfile && !-w $logfile; die "Logfile $logfile is not writable or doesn't exist" if $logfile && !-w $logfile; if ( !$timelimit ) { $timelimit = 3600; print "Warning: No TimeLimit given in $conf. Set to $timelimit\n" if $debug; } if ( !$maxblocks ) { $maxblocks = 100; print "Warning: No MaxBlocks given in $conf. Set to $maxblocks\n" if $debug; } if ($debug) { print "Interface: $interface\n"; print "Config file: $conf\n"; print "AlertFiles files: $alertfile\n"; print "CommandFile file: $commandfile\n" if $commandfile; print "Log file: $logfile\n"; print "Default TimeLimit: $timelimit\n"; print "MaxBlocks: $maxblocks\n"; print "Block command path: $blockpath\n"; print "Unblock command path: $unblockpath\n"; print "Reset command path: $resetpath\n"; print "PullCommand: $pullcommand\n" if $pullcommand; print "PullInterval: $pullinterval\n" if $pullinterval; print "PullDumpFile: $pulldumpfile\n" if $pulldumpfile; } &load_ignores($IgnoreFile) if $IgnoreFile; $ignorehostDefaultRegex = '^(?:[^\.]+|(?:www|d?ns|smtp|mx|mail|proxy)\d?\..+)\.(?:com|edu|org)$'; if (@ignoreRegex) { @ignorehostRegex = @ignoreRegex; } &makeRegex( \@ignorehostRegex, $ignorehostDefaultRegex, 0, 0 ); print "Ignore host default regex:\n $ignorehostDefaultRegex\n" if $debug; if ( $opts{r} && -s $opts{r} ) { $RuleFile = $opts{r}; } &load_rules($RuleFile) if $RuleFile; } sub load_ignores { my $f = shift; $f = $dir . '/guardian.ignore' if !-s $f; die "No proper ignore file $f found." if !-s $f; open( IN, "<$f" ) or die "Cannot read the ignore file $f, $!\n"; print "Ignore File: $f\n" if $debug; while () { next if /^\s*(?:$|#)/; &makeRegex( \@ignoreRegex, $_, 0, 0 ); print "\t" . $_ if $debug; } close(IN); } sub makeRegex { my ( $ra, $s, $qt, $neg ) = @_; $s =~ s/(?:^\s+|\s+$)//sg; return if !$s || !$ra; my %h = (); $h{n} = 1 if $neg; if ( $s =~ /^!\s*(.+)/ ) { $s = $1; $h{n} = 1; } if ( $s =~ /^$ipRegex\/([\w\.]+)$/ ) { $h{ip} = unpack( 'N', inet_aton($1) ); my $sb = $3; if ( $sb =~ /^\d+$/ ) { my $t = 32 - $sb; $t = 0 if $t < 0; $h{sb} = 0xffffffff >> $t << $t; } elsif ( $sb =~ /^$ipRegex/ ) { $h{sb} = unpack( 'N', inet_aton($1) ); } else { $h{sb} = hex($sb); } $h{t} = 's'; } else { if ( $s =~ /^\.?(?:\w+\.){1,}\w*$/ ) { $s =~ s/\.+/\\./sg; $s =~ s/[\?\*]/.$&/sg; } $s =~ s/__IgnorehostDefault__/$ignorehostDefaultRegex/sg if $ignorehostDefaultRegex; eval { $s =~ /$s/isg; }; die "Bad regex: $s" if $@; $h{t} = 'r'; $h{r} = $qt ? qr/($s)/ : qr/$s/; } push @$ra, \%h; } sub matchRegex { my ( $ip, $ra, $m, $r, @m ) = @_; @match = () if $m; return 0 if !$ip || !$ra; foreach $r ( ref($ra) eq 'ARRAY' ? @$ra : $ra ) { if ( $r->{t} eq 's' ) { next if $ip !~ /^[\d\.]+$/; if ( ( unpack( 'N', inet_aton($ip) ) & $r->{sb} ) == ( $r->{ip} & $r->{sb} ) ) { if ( !$r->{n} ) { @match = ($ip) if $m; return 1; } } else { if ( $r->{n} ) { return 1; } } } else { if ( @m = $ip =~ /$r->{r}/ ) { if ( !$r->{n} ) { @match = @m if $m; return 1; } } else { if ( $r->{n} ) { return 1; } } } } return 0; } sub load_rules { my $f = shift; $f = $dir . '/guardian.rule' if !-s $f; die "No proper rule file $f found." if !-s $f; open( IN, "<$f" ) or die "Cannot read the rule file $f, $!\n"; print "Rule File: $f\n" if $debug; my $rule = undef; my $global = 0; my $globaltrak = 0; my $trak = 0; my $inperl = ''; my %perl = (); while () { $nrules++; $global = 1 if /^\s*### DO NOT REMOVE OR CHANGE THIS LINE ###/o; next if /^\s*(?:$|#)/; if (/^\s*def:\s+(\w+)\s+(.*\S)/i) { print "Warning: duplicated def '$1' on line $nrules\n" if $debug && $def{$1}; $def{$1} = $2; next; } if ( !$inperl && /^(perl|final):\s*$/ ) { $inperl = $1; } elsif ($inperl) { if (/^end;\s*$/) { if ( $perl{$inperl} && $perl{$inperl} =~ /\S/s ) { my $px = "\n#Perl $nrules\n"; if ($global) { if ($globaltrak) { foreach (@rules) { $_->{trak}->[-1]->{$inperl} .= $px . $perl{$inperl}; } } else { foreach (@rules) { $_->{$inperl} .= $px . $perl{$inperl}; } } } elsif ( $rule && $trak ) { $rule->{trak}->[-1]->{$inperl} .= $px . $perl{$inperl}; } elsif ( $rule && !$trak ) { $rule->{$inperl} .= $px . $perl{$inperl}; } } if ( $debug && !$rule && $perlinit ) { print "\t****Perl init code:\n", $perlinit, "\t****End\n"; } elsif ( $debug && $perl{$inperl} ) { print "\t****\u$inperl code:\n", $perl{$inperl}, "\t****End\n"; } $inperl = ''; } elsif ( !$rule ) { $perlinit .= $_; } else { $perl{$inperl} .= $_; } } next if !/^\s*(.+?)\s*\+\+\+\s*(.*?)\s*==>\s*(.*?)\s*$/; my $s = $1; my $ct = $2; my $at = $3; my $override = 0; my $ovs = ''; my $ovd = 0; my $ovneg = ''; %perl = (); $trak = 0; if ( $s =~ /^(Override:\s*!?\s*->\s*!?\s*)(.+)/ ) { $ovs = 'Global ' if $global; $ovs .= $1; $s = $2; $override = 1; $ovd = 1; } elsif ( $s =~ /^(Override:\s*!?\s*)(.+)/ ) { $ovs = ( $global && !$globaltrak ) ? 'Global ' : ' '; $ovs .= ' ' if ( !$global && $rule && $rule->{trak} ); $ovs .= $1; $s = $2; $override = 1; } elsif ( $s =~ /^(Track:\s*)(.+)/ ) { $ovs = $global ? 'Global ' : ' '; $ovs .= $1; $s = $2; $trak = 1; $globaltrak = 1 if $global; } if ( $ovs =~ /!/s ) { $ovneg = '! '; } my %h = ( rule => [], cti => [], i => $nrules, ); my $ct123 = ''; foreach ( split /\s+/, $ct ) { my $ct1 = 0; my $ct2 = ''; my $ct3 = 0; if (/^(\d+)([hp]*)\/(\d+)([smhdy]?)$/i) { $ct1 = $1; $ct2 = uc($2) if $2; $ct3 = $3; &toSecs( $ct3, $4 ); } elsif (/^(\d+)([hp]*)$/i) { $ct1 = $1; $ct2 = uc($2) if $2; $ct3 = 0; } if ($ct1) { push @{ $h{cti} }, { cnt => $ct1, cnp => $ct2, int => $ct3 }; $ct123 .= qq{ $ct1$ct2/$ct3}; } } my $at1 = ''; my $at2 = ''; my $msg = ''; if ( $at =~ /^(.*?)\s*(\d*)([smhdy]?)$/i ) { $at1 = $1; $at2 = $2; &toSecs( $at2, $3 ); $at1 =~ s/\$\{(\w+)\}/$def{$1}? $def{$1} : $&/seg; if ( $at1 =~ /^(ECHO|BELL)\s*(.*)\s*/s ) { my @a = ( $1, $2 ); if ( $a[1] =~ /^(['"])(.+?)\1\s*(.*)/s ) { $a[1] = $2; $a[2] = $msg = $3; } elsif ( $a[1] =~ /^(\S+)\s*(.*)/s ) { $a[1] = $1; $a[2] = $msg = $2; } if ($debug) { $h{sys} = \@a; $at1 = join ' ', @a; $msg = ''; } else { $at1 = 'LOG'; } } elsif ( $at1 =~ /^(DROP|LOG|NOOP)\s*(.*)\s*/s ) { $at1 = $1; $msg = $2; } elsif ( $at1 =~ /^\S+\s+\S+/ ) { my @a = &parseSYS($at1); $h{sys} = \@a; $at1 = join ' ', @a; } else { $at1 = '' if $at1 !~ /^(DROP|LOG|NOOP)$/; } $at1 = $trak ? 'LOG' : 'DROP' if !$at1; $at2 = $timelimit if !$at2; } $s =~ s%\s*;\s*(src|dst|sport|dport)\s*=\s*([^;]*)\s*% my $t=$1; my $s=$2; eval{ $s=~/$s/isg; }; die "Bad regex: $s" if $@; die "No quote found in regex: $t=$s" if $s !~ /\(.*?\)/; # ($1) $h{$t}=qr/$s/; ''%seg; $s =~ s%\s*;\s*(tag)\s*=\s*([^;]*)\s*% my $t=$1; my $s=$2; $s=$2 if $s=~/^(['"])(.+?)\1$/s; $h{$t}=$s; ''%seg; &makeRegex( $h{rule}, $s, 1, $ovneg ); if ( !$override ) { if ( $s =~ /^\^(.+)/ ) { $s = $1; &makeRegex( $h{rule}, '\s\[\*\*\]\s+(' . $s . ')', 0, $ovneg ); &makeRegex( $h{rule}, "$snortLogRegex(" . $s . ')', 0, $ovneg ) ; } else { &makeRegex( $h{rule}, '\s\[\*\*\]\s.*?(' . $s . ')', 0, $ovneg ); &makeRegex( $h{rule}, "$snortLogRegex.*?(" . $s . ')', 0, $ovneg ) ; } $h{over} = [] if !$trak; } else { $h{dest} = $ovd; } $h{act} = $at1; $h{sec} = $at2; $h{msg} = $msg; print "\t$ovs$s:$ct123 : $at1 $msg + $at2 :\n" if $debug; $h{act} = 'SYS' if $h{sys}; if ( !$override && !$trak ) { $rule = \%h; push @rules, $rule; } elsif ($global) { foreach (@rules) { if ($trak) { push @{ $_->{trak} }, \%h; $h{k} = $_->{i}; $h{g} = $globaltrak; } else { if ( $_->{trak} && @{ $_->{trak} } && $globaltrak ) { push @{ $_->{trak}->[-1]->{over} }, \%h; } else { push @{ $_->{over} }, \%h; } } } } elsif ($rule) { if ($trak) { push @{ $rule->{trak} }, \%h; $h{k} = $rule->{i}; } else { if ( $rule->{trak} && @{ $rule->{trak} } ) { push @{ $rule->{trak}->[-1]->{over} }, \%h; } else { push @{ $rule->{over} }, \%h; } } } } die "No proper rules found in $f" if !@rules; close(IN); %def = (); if ( !$globaltrak && $pullinterval ) { print "*** PullInterval $pullinterval set to 0, disabled. *** \n" if $debug; $pullinterval = 0; } } sub parseSYS { my ($s) = @_; my %m = (); my $m = 0; my $p = ''; $s = $2 if $s =~ /^(["'])(.+)\1$/; if ( $s =~ /^\s*\|\s*(.+)/s ) { $p = '|'; $s = $1; } if ( $s =~ /([\&;\`\|<>])/s ) { return ( $p, $s ) if $p; return ($s); } $s =~ s/((?new( [@_] ); $d->Terse(1); $d->Indent(1); if ($debug) { print "=+=+=+=====\n" . $d->Dump; } elsif ( open( TMP, ">/tmp/guardian2.out" ) ) { print TMP "=+=+=+=====\n" . $d->Dump; close(TMP); } } sub daemonize { if ( fork() ) { exit(0); } else { &write_log("Guardian2 process id $$"); chdir($dir); setpgrp( 0, 0 ); close(STDOUT); close(STDIN); close(STDERR); } } sub init { foreach ( ref( $_[0] ) ? values( %{ $_[0] } ) : @_ ) { if ( !defined($_) ) { $_ = ''; } } }