A simple useful script for simple intrusion detection (FreeBSD + ipfw)

Jul 09, 2020 18:15

Давно сделал и использую этот скрипт. Решил поделиться с "сообществом".
Еще не знаю, где такое правильно публиковать, поэтому просто оставлю это здесь.

I have made the script a long ago and used it for a long time.
Now I have decided to share the script with the community.
I did not learn yet, what is the right place for such publications.
So I simple leave it here for now.



Here I present a script which works a like simple intrusion detections system.
The script uses the IPFW firewall and blocks connections and/or datagrams from hosts wich expose extremal traffic statistics. The script initialy was created to block password guessing attacks on ssh, ftp and similar services. However later I have used it successfully to resist to DNS DDOS attack.

#!/usr/bin/perl
$PROTO = 'tcp';
$SETUP = 'setup';
$MIN_MEAS_TIME = 120; #sec.
$MIN_MEAN_INT = 10; #sec
$MAX_MEAN_INT = 60;
$MAX_PEAK_COUNT = 10;
$PEAK_TIME = 0;
$CLEARING_PERIOD = 180; #sec
$ANL_RULE_NO = 4000;
$MIN_RULE_NO = 4001;
$MAX_RULE_NO = 4498;
$RULES_FILE = '/var/tmp/detect_rules';

if (-f $ARGV[0])
{
do $ARGV[0];
}

if (0)
{
print "PROTO=$PROTO, MIN_MEAS_TIME=$MIN_MEAS_TIME, MIN_MEAN_INT=$MIN_MEAN_INT, ".
"MAX_MEAN_INT=$MAX_MEAN_INT, MAX_PEAK_FREQ=$MAX_PEAK_FREQ, PEAK_TIME=$PEAK_TIME, ".
"CLEARING_PERIOD=$CLEARING_PERIOD, ".
"ANL_RULE_NO=$ANL_RULE_NO, MIN_RULE_NO=$MIN_RULE_NO, MAX_RULE_NO=$MAX_RULE_NO, ".
"RULES_FILE=$RULES_FILE\n";
exit 0;
}

sub print_time()
{
my $strnow = localtime();
print "$strnow : ";
}

#sub save_pid
#{
# open(PID, ">$PID_FILE");
# print PID "$$\n";
# close(PID);
#}

sub install_rule
{
my ($rule, $host, $port) = @_;
my $command = "/sbin/ipfw add $rule deny $PROTO from $host to any $port $SETUP";
print_time();
print "$command\n";
system($command);
}

sub uninstall_rule
{
my $rule = $_[0];
my $command = "/sbin/ipfw delete $rule";
print_time();
print "$command\n";
system($command);
}

@numbers = ();
for ($i = $MIN_RULE_NO; $i <= $MAX_RULE_NO; $i++)
{
$numbers[$i] = 0;
}
sub get_number
{
my $i;
for ($i = $MIN_RULE_NO; $i <= $MAX_RULE_NO; $i++)
{
if (!$numbers[$i])
{
$numbers[$i] = 1;
return $i;
}
}
return $MAX_RULE_NO;
}
sub save_numbers
{
open(NUMBERS, ">$RULES_FILE");
my $i;
for ($i = $MIN_RULE_NO; $i <= $MAX_RULE_NO; $i++)
{
if ($numbers[$i])
{
print NUMBERS "$i\n";
}
}
close(NUMBERS);
}
sub delete_old_rules
{
open(NUMBERS, "<$RULES_FILE");
while($str = )
{
chomp $str;
uninstall_rule $str;
}
close(NUMBERS);
save_numbers();
}

%table = ();

sub analyze
{
my $key = $_[0];
my $stime = $table{$key}{stime};
my $time = time() - $stime;
my $count = $table{$key}{count};
#print "$key $count $time ($stime)\n";
if (!$count) { return; }
my $meanint = $time / $count;
if ($time > 0 && $time <= $PEAK_TIME)
{
if ($count > $MAX_PEAK_COUNT)
{
if (!$table{$key}{rule})
{
$table{$key}{rule} = get_number();
install_rule($table{$key}{rule}, $table{$key}{host}, $table{$key}{port});
save_numbers();
}
}
}
elsif ($time > $MIN_MEAS_TIME)
{
if ($meanint < $MIN_MEAN_INT)
{
if (!$table{$key}{rule})
{
$table{$key}{rule} = get_number();
install_rule($table{$key}{rule}, $table{$key}{host}, $table{$key}{port});
save_numbers();
}
}
elsif ($meanint > $MAX_MEAN_INT)
{
if ($table{$key}{rule})
{
uninstall_rule($table{$key}{rule});
$numbers[$table{$key}{rule}] = 0;
save_numbers();
}
delete $table{$key};
}
}
}

sub clear
{
for my $key (keys %table)
{
analyze($key);
}
}

sub count
{
my $host = $_[0];
my $port = $_[1];
my $str = $_[2];
#print "$host $port from $str\n";
my $key = $host.'-'.$port;
if (exists $table{$key})
{
$table{$key}{count}++;
analyze($key);
}
else
{
$table{$key} = { host => $host, port => $port, stime => time(), count => 1, rule => 0 };
#print "+++",$key,"+++",%{$table{$key}}, "+++\n";
}
}

$| = 1;

print_time();
print "$0 started\n";
delete_old_rules();
#save_pid();

$ctime = time();

while ($str = )
{
#print "$str";
#exit 0;
if ($str =~ /ipfw: ${ANL_RULE_NO} [A-z, ]+([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}):[0-9]* [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:([0-9]*)/)
{
print "$str";
count($1, $2, $str);
}
if (time() - $ctime > $CLEARING_PERIOD)
{
clear();
$ctime = time();
}
}

The corresponding IPFW configuration:

...
# make a hole for password guess blocking
idsports="21,22,23,53,110,143,3389"
$fwcmd add 4000 count log tcp from any to any $idsports setup in via $oif
$fwcmd add 4499 count tcp from any to any $idsports setup in via $oif
...

Here rule numbers 4000 and 4499 (-1) corresponds with script variables
$ANL_RULE_NO = 4000;
$MIN_RULE_NO = 4001;
$MAX_RULE_NO = 4498; # note
and 4001 - 4498 is a place for rules generated by the script.
The $fwcmd and $oif shell variables contain the path to ipfw command and the name of the WAN network interface respectively. These variables should be defined somewhere above, this is a standard practice for IPFW configuration scripts.

The corresponding syslog.conf configuration:

...
security.* /var/log/security
security.* |exec /path/to/the/script >>/path/to/script's/log/file 2>&1
...

Here the 1st line was in the syslog.conf file from the beginning (from the installation), and the second line is added especialy to activate the script.

Note that the syslog facility which is accepting IPFW log messages (the "security" in the example) differs from version to version of FreeBSD and in your system in could be the other. For instance, on some older system
I have to provide the same with different lines:

...
!ipfw
*.* /var/log/ipfw.log
*.* |exec /path/to/the/script >>/path/to/script's/log/file 2>&1
...

(As in previous example, the 1st line was here from the beginning and the second was added especialy to activate the script.)

The sample paramater block to resist DNS DDOS attack:

$PROTO = 'udp';
$SETUP = '';
$MIN_MEAS_TIME = 120; #sec.
$MIN_MEAN_INT = 5; #sec
$MAX_MEAN_INT = 30;
$MAX_PEAK_COUNT = 30;
$PEAK_TIME = 5;
$ANL_RULE_NO = 4501;
$MIN_RULE_NO = 4502;
$MAX_RULE_NO = 4999;
$RULES_FILE = '/var/tmp/detect_udp_rules';

I keep this parameter block in a separate file and the parameters replace the original ones when the script is called with the path to the file as command line parameter.

Corresponding syslog.conf line is:
...
security.* |exec /path/to/the/script /path/to/the/parameter/file >>/path/to/script's/log/file 2>&1
...
(Or similar on the base of the "!ipfw" trick, see above.)

Corresponding IPFW configuration is:
...
idsuports="53"
$fwcmd add 4501 count log udp from any to any $idsuports in via $oif
$fwcmd add 5000 count udp from any to any $idsuports in via $oif
...
However I found that is useful also to slow down the speed of incoming DNS datagrams by means of IPFW pipe facility.

That's all. Hope, this can help anybody.

#attack, #sub, #dns_ddos, #exit, #freebsd, #password_guess, #print, #ids, #save_pid, #sec, #ipfw

Previous post Next post
Up