Print
Parent Category: Security
Hits: 6312

I will show you how you can protect your small/large network against hacker attacks using Snort, OpenBGPD, Remote Triggered Black Hole Filtering method configured on the border router  and a perl script named Snort2BGP.

 

The next working example was tested on a snort OpenBSD machine.

Here is a tutorial for Snort install on OpenBSD host.

After Snort is installed you need to configure OpenBGPD daemon.My configuration looks like this:


# $OpenBSD: bgpd.conf,v 1.8 2007/03/29 13:37:35 claudio Exp $
# sample bgpd configuration file
# see bgpd.conf(5)

# global configuration
AS 65121
router-id 172.16.0.3
fib-update no

# neighbors and peers

neighbor 172.16.0.1 {
remote-as 65121
descr R
#local-address 172.16.0.3
#passive
holdtime 180
holdtime min 3
announce self
softreconfig out yes
}

# filter out prefixes longer than 24 or shorter than 8 bits

deny from any
allow to any set community 65121:66

As you can see the daemon is configured only to anounce prefixes with community 66. We will filter to black hole all prefixes anouced by OpenBSD machine matching the 66 community.

 

The configuration on the border router may look like this:

 

router bgp 65121
bgp log-neighbor-changes
bgp graceful-restart restart-time 120
bgp graceful-restart stalepath-time 360
bgp graceful-restart
neighbor 172.16.0.3 remote-as 65121
neighbor 172.16.0.3 description To: Atlas IDS
!
address-family ipv4
neighbor 172.16.0.3 activate
neighbor 172.16.0.3 send-community
neighbor 172.16.0.3 soft-reconfiguration inbound
neighbor 172.16.0.3 route-map from-IDS in
no auto-summary
no synchronization
exit-address-family

ip route 192.168.191.2 255.255.255.255 Null0 name IDS

ip bgp-community new-format
ip community-list 66 permit 65121:66

route-map from-IDS permit 10

match community 66
set ip next-hop 192.168.191.2

 

As you already know if you've used snort before, all alerts are logged on /var/log/snort/alert.

The script snort2bgp watches the /var/log/snort/alert file and when an alert raise it run "bgpctl network add badhost/32".

The badhost/32 prefix is announced to the border router wich route this prefix to null0.This way all your hosts on this network are protected against badhost.

Here is the snort2bgp perl script:


#!/usr/bin/perl -T

use strict;
use warnings;
#use diagnostics;
use Fcntl qw(:seek);
use Getopt::Std;
use Sys::Syslog qw(:standard :macros setlogsock);

my $name      = 'snort2bgp';
my $version   = '4.3';
my $bgpctl     = '/usr/sbin/bgpctl';

# <default>
#my $alertfile = '/var/snort/log/alert';
my $alertfile = '/var/log/snort/alert';
my $pidfile   = "/var/run/$name.pid";
my $amnesty   = 3600;
# </default>

# %bad_hosts keys will be both amnesty ticks and hosts.  Their namespaces
# don't overlap so this doesn't matter.
# For each key being an amnesty tick, the corresponding value is a hash
# containing the host(s) being blocked.
# For each key being a host, the corresponding value is its amnesty tick,
# so it can be doubled for recidivists.
my %bad_hosts;

my $nblocked  = 0;      # Number of currently blocked hosts*.
my $tick      = 1;
my $ALERTFILE;
my $alertsize;
my $re_subnet = qr/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}/o;
my $re_ip     = qr/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/o;
my $re_port   = qr/\d{1,5}/o;
# bgpctl(8) wrappers.
my $bgpctl_block;
my $bgpctl_unblock;
my $bgpctl_resume;
# White list.
my $wl;                 # Contains either a hash ref or Net::Patricia object.
my $wlfile;
my $wlmtime   = 0;
my $wlradix;            # Whether $wl is a hash ref or Net::Patricia object.
my $wlnew;              # Subroutine to create a new empty whitelist.
my $wladd;              # Subroutine to add a host to the whitelist.
my $wlget;              # Subroutine to search a host in the whitelist.


sub log {
my ($level, $msg) = @_;
# Compat with older Sys::Syslog modules.
syslog($level, "%s", $msg);
}


sub fatal {
&log(@_);
exit 1;
}


sub dumpinfo {
&log(LOG_DEBUG, "On tick $tick, $nblocked blocked hosts:");
foreach my $k (keys %bad_hosts) {
next unless $k =~ m/^$re_ip$/;
&log(LOG_DEBUG, "  $k (unblocked at $bad_hosts{$k})");
}
}


sub byebye {
my ($sig) = @_;

&log(LOG_NOTICE, "Received SIG$sig. Exiting with $nblocked blocked hosts.");
unlink $pidfile;
exit 0;
}


sub usage {
my ($basename) = ($0 =~ m#.*([^/]*)$#);
print STDERR <<EOF
Usage: $basename [-ht] [-f alertfile] [-p pidfile] [-s amnesty] [-w whitelist]
Options:
-h  show this help message
-f  change Snort's alert file
-p  change pidfile
-s  change amnesty time
-w  set whitelist file
Defaults:
alertfile: /var/log/snort/alert
pidfile: /var/run/$name.pid
amnesty: 3600
no whitelist
EOF
}


# register_host($host)
#   Register or extend amnesty.  Return 1 if the host has been registered, 0 if
#   its amnesty has been extended.
sub register_host {
my ($host, $startup) = @_;
my $ret;
# Values for a newly blocked host.
my $duration = $amnesty;            # Blocking duration.
my $exptick = $tick + $duration;    # Blocking expiration tick.

# Host was already blocked.
if (exists $bad_hosts{$host}) {
$ret = 0;

# Double block duration.
my $oldexptick = $bad_hosts{$host};
$duration = $bad_hosts{$oldexptick}->{$host} * 2;
$exptick = $tick + $duration;

# Cleanup old expiration tick.
delete $bad_hosts{$oldexptick}->{$host};
(%{$bad_hosts{$oldexptick}} == 0) and delete $bad_hosts{$oldexptick};

&log(LOG_INFO, "Host $host is already blocked; ".
"extending amnesty to $duration ticks");
} else {
$ret = 1;

if (defined $startup) {
&log(LOG_INFO, "Recovering previously blocked host $host");
} else {
&log(LOG_INFO, "Blocking host $host for $duration ticks");
}
}

$bad_hosts{$host} = $exptick;

$bad_hosts{$exptick} = {} unless exists $bad_hosts{$exptick};
$bad_hosts{$exptick}->{$host} = $duration;

return $ret;
}


# mysytem($cmd, $msg)
#   Execute command ($cmd) check the return value.
#   $msg is issued in case of failure.
#   Return 1 on success, 0 on failure.
sub mysystem {
my ($cmd, $msg) = @_;
my $status;

$status = system ("$cmd >/dev/null 2>&1");

if ($status == -1) {
&log(LOG_WARNING, "$msg, system() failed: $!.".
" (command: $cmd)");
return 0;
}

$status = $status >> 8;
if ($status != 0) {
&log(LOG_WARNING, "$msg, command returned: $status.".
" (command: $cmd)");
return 0;
}

return 1;
}


# table_block($host)
#   Block host using table.
sub table_block {
my ($host) = @_;

return &mysystem("$bgpctl network add $host/32", "Can't block $host");
}


# table_unblock($host)
#   Unblock host using table.
sub table_unblock {
my ($host) = @_;

return &mysystem("$bgpctl network delete $host/32", "Can't unblock $host");
}


# table_resume()
#   Retrieve previously blocked host using table.
#   Return the number of retrieved hosts.
sub table_resume {
my $ANCHORS;
my $nhosts;

defined (open $ANCHORS, "$bgpctl network show | grep -v flags |   sed 's/^*     //' 2>/dev/null |") or
&fatal(LOG_ERR, "Can't retrieve previously blocked host: $!");
$nhosts = 0;
while (my $anchor = <$ANCHORS>) {
chomp $anchor;
$anchor =~ s/^\s*//;
®ister_host($anchor, "startup");
$nhosts++;
}
close $ANCHORS;
&log(LOG_NOTICE, "$nhosts hosts reloaded from table '$name'");

return $nhosts;
}


# Whitelist helper functions with Net::Patricia.
sub radix_wlnew { return new Net::Patricia; }
sub radix_wladd { my $h = shift; $wl->add_string($h, 1); };
sub radix_wlget { my $h = shift; return $wl->match_string($h); };

# Whitelist helper functions without Net::Patricia.
sub hash_wlnew { return {}; };
sub hash_wladd { my $h = shift; $wl->{$h} = 1; };
sub hash_wlget { my $h = shift; return $wl->{$h}; };


# initialize()
sub initalize {
my %opts;
my $PIDFILE;
my $startinfo = '';

setlogsock('unix');
openlog($name, 'pid,ndelay', LOG_DAEMON);

$SIG{INT} = \&byebye;
$SIG{TERM} = \&byebye;
$SIG{USR1} = \&dumpinfo;

#
# Parse command-line.
getopts('f:hp:s:tw:', \%opts);

if (exists $opts{h}) {
&usage();
exit 0;
}
exists $opts{f} and $alertfile = $opts{f};
exists $opts{p} and $pidfile = $opts{p};
if (exists $opts{s}) {
$opts{s} =~ m/^\d+$/ or &fatal(LOG_ERR, "$opts{s}: Not a number");
$amnesty = $opts{s};
}
exists $opts{w} and $wlfile = $opts{w};

$startinfo .= ' using table';
$bgpctl_block = \&table_block;
$bgpctl_unblock = \&table_unblock;
$bgpctl_resume = \&table_resume;

#
# Check and sanitize environment.
delete @ENV{qw(PATH IFS CDPATH ENV)};
-f $alertfile or &fatal(LOG_ERR, "$alertfile: No such file");
-r $alertfile or &fatal(LOG_ERR, "$alertfile: Not readable");
if (defined $wlfile) {
-f $wlfile or &fatal(LOG_ERR, "$wlfile: No such file");
-r $wlfile or &fatal(LOG_ERR, "$wlfile: Not readable");
}
-x $bgpctl or &fatal(LOG_ERR, "$bgpctl: Not executable");

#
# Try to load module from Dave Plonka to handle RADIX tree.
eval 'use Net::Patricia';
if ($@ eq '') {
$startinfo .= ' with Net::Patricia';
$wlradix = 1;
$wlnew = \&radix_wlnew;
$wladd = \&radix_wladd;
$wlget = \&radix_wlget;
} else {
$wlradix = 0;
$wlnew = \&hash_wlnew;
$wladd = \&hash_wladd;
$wlget = \&hash_wlget;
}
$wl = $wlnew->();

#
# Handle pidfile.
if (-f $pidfile) {
defined (open $PIDFILE, '<', $pidfile) or
&fatal(LOG_ERR, "Can't read pidile '$pidfile': $!");
my $pid = <$PIDFILE>;
close $PIDFILE;
chomp $pid;

# Untaint $pid.
$pid =~ m/^(\d+)$/;
$pid = $1;

kill (0, $pid) == 0 or
&fatal(LOG_ERR, "$name seems to be running as PID $pid");

unlink $pidfile;
}

defined (open $PIDFILE, '>', $pidfile) or
&fatal(LOG_ERR, "Can't write pidfile '$pidfile': $!");
print $PIDFILE "$$\n";
close $PIDFILE;

#
# Issue startup message.
&log(LOG_NOTICE, "Starting$startinfo...");

#
# And finally, check for previously blocked hosts.
$nblocked = $bgpctl_resume->();
&update_title();
}


sub update_title {
$0 = "$name $version :: blocking $nblocked hosts";
}


# try_load_whitelist()
#  Flush and load whilelist.
sub try_load_whitelist {
my $WL;
my $radix_needed = 0;
my @stat;
my $nentries = 0;

@stat = stat $wlfile;
if (@stat == 0) {
&log(LOG_ERR, "Cannot stat whitelist '$wlfile': $!");
return;
}
$stat[9] == $wlmtime and return;

if ($stat[9] < $wlmtime) {
&log(LOG_WARNING, "Whitelist mtime went backward by ".
($wlmtime - $stat[9])." seconds, reloading it anyway)")
}

if (not defined open $WL, '<', $wlfile) {
&log(LOG_ERR, "Cannot open whitelist '$wlfile': $!");
return;
}

$wlmtime = $stat[9];
$wl = $wlnew->();
&log(LOG_NOTICE, "Loading whitelist from '$wlfile'");

while (my $line = <$WL>) {
chomp $line;

if ($wlradix) {
unless ($line =~ /^$re_subnet$/ or $line =~ /^$re_ip$/) {
&log(LOG_WARNING, "Skipping invalid whitelist entry $line");
next;
}

} else {
if ($line =~ /^$re_subnet$/) {
$radix_needed++;
&log(LOG_WARNING, "Skipping unsupported CIDR whitelist entry $line");
next;
}
unless ($line =~ /^$re_ip$/) {
&log(LOG_WARNING, "Skipping invalid whitelist entry $line");
next;
}
}

&log(LOG_INFO, "Adding $line to whitelist");
$nentries++;
$wladd->($line);
}

&log(LOG_NOTICE, "Loaded $nentries entries from whitelist");
if ($radix_needed) {
&log(LOG_WARNING, "Can't handle subnet in whitelist without ".
"Net::Patricia; $radix_needed entries have been skipped");
}
close $WL;
}


# open_alertfile()
#   (Re-)open the global $alertfile as $ALERTFILE.  Retry forever if needed.
#   Reset the global $alertsize variable.
sub open_alertfile {

if (defined $ALERTFILE) {
close $ALERTFILE;
undef $ALERTFILE
}

while (1) {
defined (open $ALERTFILE, '<', $alertfile) and last;
print STDERR "Can't read alertfile '$alertfile': $!\n";
sleep 10;
}
$alertsize = 0;
}


# check_for_attack($line)
#   Check the line is an attack are return the offending host if any.
sub check_for_attack {
if ($_[0] =~ /($re_ip)\:$re_port -> $re_ip\:$re_port/o) {
return $1;
}
return 0;
}


# check_for_portscan($line)
#   Check the line is a portscan are return the offending host if any.
sub check_for_portscan {
if ($_[0] =~ /PORTSCAN DETECTED from ($re_ip)/io) {
return $1;
}
return 0;
}


# block($host)
#   Block host.
sub block {
my $host = $_[0];

if (defined $wlget->($host)) {
&log(LOG_INFO, "Skipping whitelisted host $host");
return;
}

# Check if host is already blocked.
if (®ister_host($host) == 0) {
goto KILL_STATES;
}
$nblocked++;

if ($bgpctl_block->($host) == 0) {
$nblocked--;
goto KILL_STATES;
}
&update_title();

KILL_STATES:
system("$bgpctl reload >/dev/null 2>&1") == 0 or
&log(LOG_WARNING, "Can't clear soft out bgp for $host: $!");
}


# unblock()
#   Unblock all host for which we have reached the expiration tick.
sub unblock {
my $hosts;

return if not exists $bad_hosts{$tick};

$hosts = $bad_hosts{$tick};
delete $bad_hosts{$tick};

foreach my $h (keys %$hosts) {
&log(LOG_INFO, "Unblocking host $h");

$bgpctl_unblock->($h);

delete $bad_hosts{$h};
$nblocked--;
}
&update_title();
}


&initalize();
&open_alertfile();
seek $ALERTFILE, 0, SEEK_END;           # Won't fail.
$alertsize = tell $ALERTFILE;           # Idem.

while (1) {
# unblock old hosts
&unblock();

defined $wlfile and &try_load_whitelist();

while (my $line = <$ALERTFILE>) {
chomp $line;
my $blocked;

$blocked = &check_for_attack($line);
if ($blocked) {
&block($blocked);
next;
}
$blocked = &check_for_portscan($line);
if ($blocked) {
&block($blocked);
next;
}

# Junk line.
}

my @stat = stat $alertfile;
if (@stat != 0 && $stat[7] < $alertsize) {
# File has shrinked, probably because of rotation; reopen and process
# it immediately.
&open_alertfile();
next;
}

seek $ALERTFILE, 0, SEEK_CUR;       # Reset EOF.  Won't fail.
$tick++;
sleep 1;
}

It has many features like: white list file, variable amnesty time, etc.

 

Note:

We use Hosting and VPS Hosting, from: www.star-host.org

We like and trust them.

Good prices, high security.