Created
August 20, 2012 00:27
-
-
Save deekayen/3398840 to your computer and use it in GitHub Desktop.
Distributed denial of service amplification monitor by Jeff Taylor (Shdwdrgn). Written for hosts of OpenNIC DNS servers.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/perl | |
$| = 1; | |
# ddos.pl 2012.06.29 | |
# by Jeff Taylor (Shdwdrgn) | |
#--- REQUIRED PERL LIBRARIES ---# | |
# Net::Pcap (debian: libnet-pcap-perl) | |
# IPTables::ChainMgr (debian: libiptables-chainmgr-perl) | |
#--- EDIT THESE VALUES AS NEEDED ---# | |
# BLOCKFILE: where to store IP and port information | |
use constant BLOCKFILE => "/root/ddos.dns"; | |
# NETDEV: device your DNS service listens on | |
use constant NETDEV => "eth0"; | |
# NETMASK: add a mask for your server's real or local IP | |
use constant NETMASK => [ "127.0.0.0/8", "10.0.0.0/8" ]; | |
# DEBUG: 0=off, 1=show debug info | |
use constant DEBUG => 0; | |
# THRESH: maximum queries on a single port | |
use constant THRESH => 10; | |
# MAXPERSEC: maximum number of queries per second | |
use constant MAXPERSEC => 25; | |
# MAXCOUNT: limits the queries/sec that we observe | |
use constant MAXCOUNT => 64; | |
# MAXMULTI: Limits how high the multiplier can go | |
use constant MAXMULTI => 8; | |
# MAXBLOCKS: If blocking this number of ports or more | |
# for an IP, start blocking all DNS queries from IP | |
use constant MAXBLOCKS => 4; | |
# BASETIME: used to calculate how long an IP is blocked | |
use constant BASETIME => 225/128; # Approx. 2 seconds | |
# CHAIN: Define whic iptables chain to add rules to | |
use constant CHAIN => "INPUT"; | |
# Adjust the path to iptables for your system | |
my %opts = ( | |
'iptables' => '/sbin/iptables', | |
'iptout' => '/tmp/iptables.out', | |
'ipterr' => '/tmp/iptables.err', | |
'debug' => 0, | |
'verbose' => 0 | |
); | |
#--- DO NOT EDIT PAST THIS POINT ---# | |
use POSIX qw(strftime); | |
use Switch; | |
use IPTables::ChainMgr; | |
#use Net::Pcap; | |
#use NetPacket::Ethernet; | |
#use NetPacket::IP; | |
#use NetPacket::TCP; | |
#use strict; | |
my $blocked; | |
my @data; | |
my $dom; | |
my @tmp; | |
my $IP; | |
my $port; | |
my $key; | |
my $line; | |
my $hms; | |
my %pkt; | |
my $tmr = time(); | |
my $h; my $m; my $s; | |
my $expire; | |
my $max; | |
my $portcount; | |
my $netrules; | |
my $lastReset; | |
my $ipt_obj = new IPTables::ChainMgr(%opts) | |
or die "[*] Could not acquire IPTables::ChainMgr object"; | |
#---------------------- Error Handlers ----------------------# | |
$SIG{__DIE__} = 'INT_handler'; | |
$SIG{'INT'} = 'INT_handler'; | |
$SIG{'TERM'} = 'INT_handler'; | |
sub INT_handler { | |
if (DEBUG) { print "\nEnding script...\n"; } | |
close(DUMP); | |
foreach $IP (keys %pkt) { | |
foreach $port (keys %{$pkt{$IP}}) { | |
if ($pkt{$IP}{$port}{block} == 1) { | |
if (DEBUG) { print "* Unloading $IP:$port\n"; } | |
iptUpdate($IP, $port, "D"); | |
} | |
} | |
} | |
exit(0); | |
} | |
#--------------------- Begin main loop ---------------------# | |
$netrules=""; | |
foreach $IP (@{+NETMASK}) { | |
if ($netrules) { $netrules .= " and "; } | |
$netrules .= "not src net $IP"; | |
} | |
$netrules = "tcpdump -pnt -i ".NETDEV." $netrules and not udp src port 53 and udp dst port 53 and '(ip[2:2] != 0)' 2>/dev/null |"; | |
open DUMP, "$netrules" or die "Invalid tcpdump defined\n"; | |
while (<DUMP>) { | |
chomp ($line = <DUMP>); | |
# Reload array data every hour | |
$h = strftime("%H", localtime); | |
if ($lastReset != $h) { | |
$lastReset = $h; | |
getBlockfile(%pkt); | |
} | |
# Values for {block} | |
# 0 - Port traffic detected, but no action taken | |
# 1 - IP:port is currently blocked | |
# 2 - IP:port block expired, now monitoring for repeat offense | |
# Record each packet in array | |
@data = split(/ /, $line); | |
@tmp = split(/\./, $data[1]); | |
$IP = "$tmp[0].$tmp[1].$tmp[2].$tmp[3]"; | |
$port = $tmp[4]; | |
#$url = ($data[5] eq "[1au]") ? $data[7] : $data[6]; | |
#@bits = reverse (split(/\./, $url)); | |
if ($IP) { | |
# Add new IP to array | |
if (! $pkt{$IP}) { | |
$pkt{$IP}{0}{block} = 0; | |
$pkt{$IP}{0}{count} = 0; | |
$pkt{$IP}{0}{multi} = 0; | |
} | |
$pkt{$IP}{0}{count}++; | |
# New port number seen for this IP | |
if (! $pkt{$IP}{$port}) { | |
if (($pkt{$IP}{$port}{block} > 0) && (DEBUG)) { print "WARNING! $IP:$port adding existing block\n"; } | |
$pkt{$IP}{$port}{block} = 0; | |
$pkt{$IP}{$port}{count} = 0; | |
$pkt{$IP}{$port}{multi} = 0; | |
# $pkt{$IP}{$port}{start} = $tmr; | |
} | |
# Do unless port is currently blocked | |
if ($pkt{$IP}{$port}{block} != 1) { | |
$pkt{$IP}{$port}{count}++; | |
#$pkt{$IP}{$port}{"$bits[1].$bits[0]"}++; | |
if ($pkt{$IP}{$port}{count} > MAXCOUNT) { $pkt{$IP}{$port}{count}=MAXCOUNT; } | |
} | |
# If IP:port exceeds limits while being monitor, prepare to block again | |
if (($pkt{$IP}{$port}{block} == 2) && ($pkt{$IP}{$port}{count} >= THRESH)) { $pkt{$IP}{$port}{block} = 0; } | |
} | |
# If one second has passed since the last check, examine collected data | |
if ($tmr != time()) { | |
$tmr = time(); | |
$h = strftime("%H", localtime); | |
$m = strftime("%M", localtime); | |
$s = strftime("%S", localtime); | |
$hms = "$h:$m:$s"; | |
if (DEBUG) { print "$hms Count: ", scalar(keys %pkt), " \r"; } | |
foreach $IP (keys %pkt) { | |
$pkt{$IP}{0}{portblock} = 0; | |
foreach $port (reverse sort keys %{$pkt{$IP}}) { | |
if ($port == 0) { | |
if (($pkt{$IP}{0}{block} == 2) && ($pkt{$IP}{0}{portblock} >= MAXBLOCKS)) { $pkt{$IP}{0}{block} = 0; } | |
} else { | |
# Count the number of ports blocked for this IP | |
if ($pkt{$IP}{$port}{block} == 1) { $pkt{$IP}{0}{portblock}++; } | |
} | |
# Add new block | |
if ($pkt{$IP}{$port}{block} == 0) { | |
$max = ""; | |
if (($port > 0) && ($pkt{$IP}{$port}{count} >= THRESH)) { $max = "port"; } | |
if (($port == 0) && ($pkt{$IP}{0}{count} >= MAXPERSEC)) { $max = "maxpersec"; } | |
if (($port == 0) && ($pkt{$IP}{0}{portblock} >= MAXBLOCKS)) { $max = "maxblock"; } | |
if ($max) { | |
$pkt{$IP}{$port}{block} = 1; | |
$pkt{$IP}{$port}{start} = $tmr; | |
$pkt{$IP}{$port}{timer} = $pkt{$IP}{$port}{count}; | |
if (DEBUG) { print "$hms [$pkt{$IP}{$port}{multi}] ",$max,"Blocking $IP:$port - $pkt{$IP}{$port}{count} hits\n"; } | |
iptUpdate($IP, $port, "I"); | |
updateBlockfile($IP, $port, %pkt); | |
} | |
# Remove expired block | |
} else { | |
$expire = $pkt{$IP}{$port}{start} + ((($pkt{$IP}{$port}{block} + 1) ** $pkt{$IP}{$port}{multi}) * $pkt{$IP}{$port}{timer} * BASETIME); | |
if ($tmr > $expire) { | |
$blocked = $tmr - $pkt{$IP}{$port}{start}; | |
if ($pkt{$IP}{$port}{block} == 2) { | |
if (DEBUG) { print "$hms Deleting $IP:$port after $blocked seconds\n"; } | |
$pkt{$IP}{$port}{block} = 0; | |
updateBlockfile($IP, $port, %pkt); | |
delete ($pkt{$IP}{$port}); | |
} else { | |
if (DEBUG) { print "$hms Unblocking $IP:$port after $blocked seconds\n"; } | |
iptUpdate($IP, $port, "D"); | |
$pkt{$IP}{$port}{block} = 2; | |
$pkt{$IP}{$port}{start} = $tmr; | |
$pkt{$IP}{$port}{multi}++; | |
if ($pkt{$IP}{$port}{multi}>MAXMULTI) { $pkt{$IP}{$port}{multi}=MAXMULTI; } | |
updateBlockfile($IP, $port, %pkt); | |
$pkt{$IP}{$port}{count} = 0; | |
} | |
} | |
} | |
# If record still exists, make updates | |
if ($pkt{$IP}{$port}) { | |
# Unblocked IPs should decrease their count at a normal rate | |
if ($pkt{$IP}{$port}{block} == 2) { | |
$pkt{$IP}{$port}{count}--; | |
if ($pkt{$IP}{$port}{count} < 1) { $pkt{$IP}{$port}{count} = 0; } | |
} | |
# Remove array element when no longer needed | |
if (($port > 0) && ($pkt{$IP}{$port}{block} == 0)) { | |
$pkt{$IP}{$port}{count}--; | |
if ($pkt{$IP}{$port}{count} <= 0) { | |
if (($pkt{$IP}{$port}{timer} > 0) && (DEBUG)) { print "WARNING! Removed $IP:$port from array [$pkt{$IP}{$port}{block}]\n"; } | |
delete ($pkt{$IP}{$port}); | |
} | |
} | |
} #if exists | |
} #foreach PORT | |
# Clean up unused IPs | |
$portcount = scalar(keys %{$pkt{$IP}}); | |
if (($portcount <= 1) && ($pkt{$IP}{0}{block} == 0)) { | |
if (($pkt{$IP}{0}{portblock} > 0) && (DEBUG)) { print "WARNING! [$pkt{$IP}{0}{portblock}] Cleanup $IP:port\n"; } | |
delete ($pkt{$IP}); | |
} else { | |
$pkt{$IP}{0}{count} = 0; | |
} | |
} #foreach IP | |
} | |
} | |
print "Error: tcpdump not found or bad NETMASK defined\n"; | |
#----------------------- Subroutines ------------------------# | |
sub getBlockfile { | |
my $pkt = shift; | |
if (-e BLOCKFILE) { | |
# Read blockfile into memory | |
open FH, BLOCKFILE; my @LINES = <FH>; close(FH); | |
foreach $line (@LINES) { | |
@data = split(/[ \t]+/, $line); | |
if (($data[0]) && ($data[0] ne "#IP:Port")) { | |
@tmp = split(/:/, $data[0]); | |
$IP = $tmp[0]; $IP =~ s/^\#//; | |
$port = $tmp[1]; | |
$pkt{$IP}{$port}{multi} = $data[1]; | |
if ($pkt{$IP}{$port}{multi} eq "") { $pkt{$IP}{$port}{multi} = 0; } | |
$pkt{$IP}{$port}{count} = $data[2]; | |
if ($pkt{$IP}{$port}{count} eq "") { $pkt{$IP}{$port}{count} = THRESH; } | |
if ($pkt{$IP}{$port}{count} == 0) { $pkt{$IP}{$port}{count} = THRESH; } | |
if (! $pkt{$IP}{$port}{timer}) { $pkt{$IP}{$port}{timer} = $pkt{$IP}{$port}{count}; } | |
if (! $pkt{$IP}{$port}{start}) { $pkt{$IP}{$port}{start} = $data[3]; } | |
$pkt{$IP}{$port}{block} = 1; | |
if (substr($data[0], 0, 1) eq "#") { | |
$pkt{$IP}{$port}{block} = 2; | |
$pkt{$IP}{$port}{count} = 0; | |
} else { | |
iptUpdate($IP, $port, "I"); | |
} | |
if (DEBUG) { print "Loading ($pkt{$IP}{$port}{block}) $IP:$port\n"; } | |
} | |
} | |
} else { | |
# Create new blockfile | |
open FH, ">", BLOCKFILE; | |
print FH "#IP:Port #Multi #Count #Start #Expires\n"; | |
close(FH); | |
} | |
} | |
sub updateBlockfile { # IP, port, %pkt | |
my $IP = shift; | |
my $port = shift; | |
my $pkt = shift; | |
my $key = "$IP:$port"; | |
my $found = 0; | |
my $expire = int($pkt{$IP}{$port}{start} + ((($pkt{$IP}{$port}{block} * 2) ** $pkt{$IP}{$port}{multi}) * $pkt{$IP}{$port}{timer} * BASETIME)); | |
open FH, BLOCKFILE; | |
my @LINES = <FH>; | |
close(FH); | |
open FH, ">", BLOCKFILE; | |
foreach $line (@LINES) { | |
if ($pkt{$IP}{$port}{multi} eq "") { $pkt{$IP}{$port}{multi} = 0; } | |
switch ($pkt{$IP}{$port}{block}) { | |
case 1 { | |
if (($line =~ /^$key/) || ($line =~ /^\#$key/)) { | |
print FH setLen("$IP:$port"), " $pkt{$IP}{$port}{multi} $pkt{$IP}{$port}{count} $pkt{$IP}{$port}{start} $expire\n"; | |
$found++; | |
} else { print FH $line; } | |
} | |
case 2 { | |
if (($line =~ /^$key/) || ($line =~ /^\#$key/)) { | |
print FH setLen("#$IP:$port"), " $pkt{$IP}{$port}{multi} $pkt{$IP}{$port}{count} $pkt{$IP}{$port}{start} $expire\n"; | |
$found++; | |
} else { print FH $line; } | |
} | |
else { | |
print FH $line unless (($line =~ /^$key/) || ($line =~ /^\#$key/)); | |
} | |
} | |
} | |
if (($pkt{$IP}{$port}{block} == 1) && (!$found)) { | |
print FH setLen("$IP:$port"), " $pkt{$IP}{$port}{multi} $pkt{$IP}{$port}{count} $pkt{$IP}{$port}{start} $expire\n"; | |
} | |
close(FH); | |
} | |
sub setLen { | |
my $str = $_[0]; | |
if (length $str < 16) { $str .= " "; } | |
return $str; | |
} | |
sub iptUpdate { # IP, port, I/D | |
my $ip = $_[0]; | |
if (substr($ip, 0, 1) eq "#") { $ip = substr($ip, 1); } | |
my $port = $_[1]; | |
my $act = $_[2]; | |
if ($ip && $act) { | |
if ($act eq "D") { | |
if ($port > 0) { | |
$ipt_obj->delete_ip_rule($ip, '0.0.0.0/0', 'filter', CHAIN, 'DROP', {'protocol' => 'udp', 's_port' => $port}); | |
} else { | |
$ipt_obj->delete_ip_rule($ip, '0.0.0.0/0', 'filter', CHAIN, 'DROP', {'protocol' => 'udp', 'd_port' => 53}); | |
} | |
} | |
if ($act eq "I") { | |
if ($port > 0) { | |
$ipt_obj->add_ip_rule($ip, '0.0.0.0/0', 1, 'filter', CHAIN, 'DROP', {'protocol' => 'udp', 's_port' => $port}); | |
} else { | |
$ipt_obj->add_ip_rule($ip, '0.0.0.0/0', 1, 'filter', CHAIN, 'DROP', {'protocol' => 'udp', 'd_port' => 53}); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment