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

Here I present a script which works like a simple intrusion detections system.
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.

If this Forum is not a right place for such publications, I hope, the Community can suggest the right place.

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.
Code:
#!/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 = <numbers>)
   {
     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 = <stdin>)
{
  #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:

Code:
# 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
Code:
]
$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:

Code:
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 especially 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:

Code:
!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 parameter block to resist DNS DDOS attack:
Code:
$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:
Code:
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:
Code:
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.
 
Last edited:
The script initialy was created to block password guessing attacks on ssh, ftp and similar services.

You can use blacklistd in FreeBSD base. Our OpenSSH can use blacklistd(8) service out of the box. In three command:

Code:
sysrc blacklistd_enable=YES
touch /etc/ipfw-blacklist.rc
sysrc sshd_flags="-oUseBlacklist=yes"

service sshd restart
service blacklistd restart

then watch your ipfw table port(22) ( or other port for related services ).

Also pf/ipfilter are supported by blacklistd helper
 
Probably one of the moderators did some cleanup for you. Use it as an opportunity for learning!

Can you do me a favor? Explain, in 4 or 5 sentences, how your system works? Not the details of the script (you have those above), but what statistics are collected, how they are acted on, and when you do what.
 
There's similar program called fail2ban which can analyze the log files of many different services based on different patterns. Instead of using a range of rule numbers in IPFW you can use a IPFW table.
 
You can use blacklistd in FreeBSD base. Our OpenSSH can use blacklistd(8) service out of the box. In three command:

Code:
sysrc blacklistd_enable=YES
touch /etc/ipfw-blacklist.rc
sysrc sshd_flags="-oUseBlacklist=yes"

service sshd restart
service blacklistd restart

then watch your ipfw table port(22) ( or other port for related services ).

Also pf/ipfilter are supported by blacklistd helper
A (minor?) improvement: sysrc sshd_flags+="-oUseBlacklist=yes". So if $sshd_flags are set in rc.conf,(5) they will be conserved. I do such in my rc.conf regularly with $kld_load="${kld_load} addition", because I have different sections for Base/Security/Network/Services/GUI settings, and a comment for many of them. After 2 years, you forget why a specific knob was set. sysrc(8) supports "+=", unfortunately sh(1)-syntax in rc.conf does not.

Dmitry E. Gouriev : consider to link this thread (or your post) in Useful Scripts (do not duplicate, just link)
 
Can you do me a favor? Explain, in 4 or 5 sentences, how your system works?
The script analyses the messages from IPFW log, to be strict, from the special logging IPFW rule. Changing the rule, you control the set of packets the script will react on. For each pair of (source IP address, destination port number) the script count a number of packets arrived and a time period of visible activity. The number of packets and the time are the data to decide to block or not to block the packet source host. The script evalutes two additional values: a mean interval between packets and a number of packets in a short peak period of time. If either of the values exceed defined limits, the script adds a rule to IPFW to block annoying host. If after time the traffic parameters returns to normal values (say, the host stops to tramsmit at all), the script deletes corresponding rule from IPFW. This is the main idea. Of course, there are lots of details, which remain details.
 
Updated version of the script above.
The problem solved in this version is that syslogd periodicaly restart child processes, including this script. And at that times the script forgets its database and allow blocked hosts to transmit again. The updated version if free of this trouble.
And now the script is posted as attachment so there are no formatting troubles.
 

Attachments

Updated version of the script above.
The problem solved in this version is that syslogd periodicaly restart child processes, including this script. And at that times the script forgets its database and allow blocked hosts to transmit again. The updated version if free of this trouble.
And now the script is posted as attachment so there are no formatting troubles.
What exactly does this script? I see that it adds a block firewall rule for TCP, but how do you decide what traffic to block?
 
Back
Top