Version Française
Security : Linux : Jamd
Latest version : v1.0.1 (05-May-2010)
I - Overview :
Jamd is a small perl daemon to tarpit port scanners, spammers, script-kiddies and various DoS attacks (slowloris).
Tarpit method is already well-known with tools like LaBrea and iptables tarpit module as well. Jamd uses the same concept but makes it simpler :
- portability : it works on any Linux distribution, FreeBSD and probably others *BSD and even possibly on Mac.
- plug'n'play : it can be ran and stopped easily, there's nothing to compile and/or patch, nothing to setup.
II - Parameters :
options :
--stop : stop the daemon.
--help : display this menu.
--debug : run in debug mode (verbose, no daemon).
--test : simulation (same as debug but doesn't send packets).
--nopromisc : do not use promiscuous mode.
--interface : network interface.
--stats : display stats
TCP parameters :
--source <IP> : source IP (the victim).
--sport <src_port> : source port (single port '--sport 25', multiports
'--sport 25,80,81' or port range '--sport 25-81').
--destination <IP> : destination IP (your server).
--dport <dest_port> : destination port (single port '--dport 25', multiports
'--dport 25,80,81' or port range '--dport 25-81').
To run jamd, you need at least to include your network interface and an IP or port (source or destination) as command line parameters.
'--dport' and '--sport' parameters can be a single port, multiports or port range.
III - Perl modules needed :
The following Perl modules are needed to run jamd :
IV - Using jamd :
When using jamd, you must ensure that your server will not reply to any request your are willing to tarpit. This usually implies adding a rule to the server firewall that will drop those packets. Under Linux, iptables can be used with the '-j DROP' action (see below).
Here is a short and non-exhaustive list of jamd usages.
you want to tarpit NetBios port scans (from port 137 to 139) on your server (IP is 1.2.3.4, eth0 interface) :
# iptables -I INPUT -i eth0 -d 1.2.3.4 -p tcp --dport 137:139 -j DROP
2) run jamd (port range):
# jamd --interface eth0 --destination 1.2.3.4 --dport 137-139
you have an IP (1.2.3.4 / eth0) and you aren't using it. Bind it and tarpit all its ports :
# iptables -I INPUT -i eth0 -d 1.2.3.4 -j DROP
2) run jamd :
# jamd --interface eth0 --destination 1.2.3.4
your server (1.2.3.4 / eth0) does not need neither FTP (21) nor POP3 (110) access ? Tarpit them both :
# iptables -I INPUT -i eth0 -d 1.2.3.4 -p tcp -m multiport --dports 21,110
2) run jamd (multiports) :
# jamd --interface eth0 --destination 1.2.3.4 --dport 21,110
your server (1.2.3.4 / eth0) is facing a slowloris DoS attack (2000 connections) on port 80, coming from IP 2.2.2.2. Tarpit the attacker :
# iptables -I INPUT -i eth0 -s 2.2.2.2 -p tcp --dport 80 -j DROP
2) run jamd :
# jamd --interface eth0 --destination 1.2.3.4 --dport 80 --source 2.2.2.2
3) restart Apache to cool it down and close all those open sockets.
Within few second and without even noticing the change, the attacker will become the victim. Those 2000 sockets will remain open on his server only, not yours. The results is more or less equivalent to a reverse "netkill" DoS attack.
you have an IP (1.2.3.4, eth0), no SMTP sever listening on port 25 and own a domain name (domain.com). Enjoy yourself : tarpit spammers !
1) create a subdomain (ex : jamd.domain.com), point it to your IP and add a MX record :
jamd.domain.com. IN MX 10 jamd.domain.com.
2) insert / hide tons of fake email addresses inside your HTML pages of your website (ie: hello@jamd.domain.com, eatme@jamd.domain.com, spamsucks@jamd.domain.com etc).
3) block any access to the port 25 of that IP :
# iptables -I INPUT -i eth0 -d 1.2.3.4 -p tcp --dport 25 -j DROP
4) run jamd :
# jamd --interface eth0 --destination 1.2.3.4 --dport 25
Spammers will be pleased to collect all those email addresses and to get jammed on your port 25.
V - Source :
use strict;
use Socket;
use Net::RawIP;
use Net::Pcap;
use NetPacket::Ethernet qw(:strip);
use NetPacket::IP;
use NetPacket::TCP;
use POSIX qw(setsid);
use Getopt::Long;
# signal handlers :
$SIG{INT} = \&stop;
$SIG{QUIT} = \&stop;
$SIG{TERM} = \&stop;
my $appname = 'jamd';
my $version = '1.01';
my $logfile = '/var/log/jamd.log';
my $pidfile = '/var/run/jamd.pid';
my $copyright = '(c) 2010 Jerome Bruandet - http://spamcleaner.org/';
if ( $> ) {
print "$appname $version - $copyright\n\n";
print "\t[ERROR] : you must be root\n\n";
exit 1;
}
my ( $pid, $res, $date, $error );
my ( $err, $addr, $mask, $filter_t, $pcap, $test, $filter );
my ( $debug, $stop, $help, $interface, $nopromisc,
$source, $sport, $destination, $dport );
my $promisc = 1;
GetOptions (
'debug' => \$debug,
'stop' => \&stop,
'test' => \$test,
'help' => \&help,
'nopromisc' => \$nopromisc,
'interface=s' => \$interface,
'source=s' => \$source,
'sport=s' => \$sport,
'destination=s' => \$destination,
'dport=s' => \$dport,
'stats' => \&show_stats,
);
my @tmp_filter;
$promisc = 0 if ( $nopromisc );
if ( $sport =~ /-/ ) {
push @tmp_filter, 'tcp src portrange ' . $sport . ' ';
} elsif ( $sport =~ /,/ ) {
my @tmp = split( /,/, $sport );
$sport = '';
foreach ( @tmp ) {
goto sp if (! $sport );
$sport.= 'and ';
sp: $sport.= "tcp src port $_ ";
}
push @tmp_filter, $sport;
} elsif ( $sport =~ /^\d+$/ ) {
push @tmp_filter, 'tcp src port ' . $sport . ' ';
} else {
$sport = '';
}
if ( $dport =~ /-/ ) {
push @tmp_filter, 'tcp dst portrange ' . $dport . ' ';
} elsif ( $dport =~ /,/ ) {
my @tmp = split( /,/, $dport );
$dport = '';
foreach ( @tmp ) {
goto dp if (! $dport );
$dport.= 'and ';
dp: $dport.= "tcp dst port $_ ";
}
push @tmp_filter, $dport;
} elsif ( $dport =~ /^\d+$/ ) {
push @tmp_filter, 'tcp dst port ' . $dport . ' ';
} else {
$dport = '';
}
if ( $source ) {
if ( $source !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ ) {
print "$appname $version : $copyright\n\n";
print "\t[ERROR] : source IP ($source) is not correct\n\n";
exit 1;
} else {
push @tmp_filter, 'src host ' . $source . ' ';
}
}
if ( $destination ) {
if ( $destination !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ ) {
print "$appname $version : $copyright\n\n";
print "\t[ERROR] : destination IP ($destination) is not correct\n\n";
exit 1;
} else {
push @tmp_filter, 'dst host ' . $destination . ' ';
}
}
if ( ( ! $source ) && ( ! $sport ) && ( ! $destination ) && ( ! $dport ) ) {
print "$appname $version : $copyright\n\n";
print "\t[ERROR] : you must select at least a port (or an IP)\n";
print "\tand the network interface\n\nrun jamd --help\n\n";
exit 1;
}
if ( ! $interface ) {
print "$appname $version : $copyright\n\n";
print "\t[ERROR] : interface is missing\n\n";
exit 1;
}
`ifconfig | grep '^$interface' > /dev/null`;
if ( $? >> 8) {
print "$appname $version : $copyright\n\n";
print "\t[ERROR] : cannot find interface ($interface)\n\n";
exit 1;
}
$debug = 1 if $test;
foreach ( @tmp_filter ) {
goto filter if (! $filter);
$filter.= 'and ';
filter:
$filter.= $_;
}
chop $filter if ( $filter =~ /\s$/ );
&run;
exit;
sub run {
my $res = &is_running;
if (! $res ) {
print "$appname $version : $copyright\n\n";
print "\t[ERROR] : daemon is already running (PID : $pid)\n\n";
exit 1;
}
if ( ! $debug ) {
print "$appname $version : $copyright\n\n" .
"\t- starting daemon on interface [$interface]\n" .
"\t- filter : $filter\n" .
"\t- DON'T FORGET to drop those packets with your firewall !\n\n";
}
&daemonize if ( ! $debug );
open PID, ">$pidfile";
print PID $$;
close PID;
open LOG, ">>$logfile";
select((select(LOG), $|=1)[0]);
$date = `date '+%d/%b/%Y %T'`;
chomp $date;
my $temp;
if ( $test ) { $temp = ' in ** test mode **' }
elsif ( $debug ) { $temp = ' in ** debug mode **' }
print LOG "$date [$appname $version] : starting ".
"daemon (PID : $$)$temp\n";
print "$appname $version : $copyright\n\n";
print "\t[OK] : starting daemon (PID : $$)$temp\n\n";
print "- setting up packet sniffer :\n" .
"\tinterface : $interface\n" if $debug;
print LOG "$date [$appname $version] : interface : $interface\n";
if ( Net::Pcap::lookupnet( $interface, \$addr, \$mask, \$err) == -1 ) {
print LOG "$date [$appname $version] : ERROR ".
"Net::Pcap::lookupnet failed ($err)\n";
print "- ERROR : check $logfile\n" if ( $debug );
$error = 1;
&stop;
}
print "\tpromiscuous : $promisc\n" if $debug;
print LOG "$date [$appname $version] : promiscuous mode : $promisc\n";
my $pcap = Net::Pcap::open_live( $interface, 96, $promisc, 0, \$err);
if (! $pcap ) {
print LOG "$date [$appname $version] : ERROR ".
"Net::Pcap::open_live failed ($err)\n";
print "- ERROR : check $logfile\n" if ( $debug );
$error = 1;
&stop;
}
print "\tfilter : $filter\n" if $debug;
print LOG "$date [$appname $version] : filter : '$filter'\n";
if ( Net::Pcap::compile( $pcap, \$filter_t, $filter, 0, $mask ) == -1 ) {
print LOG "$date [$appname $version] : ERROR ".
"Net::Pcap::compile failed\n";
print "- ERROR : check $logfile\n" if ( $debug );
$error = 1;
&stop;
}
Net::Pcap::setfilter( $pcap, $filter_t);
print "- listening...\n" if $debug;
print LOG "$date [$appname $version] : listening....\n";
Net::Pcap::loop($pcap, -1, \&process_packet, 0);
}
sub process_packet {
my( $user_data, $header, $packet ) = @_;
if ( $packet =~ /^.{14}\x08\x00/ ) {
print "- linux cooked detected, removing 2 bytes\n" if $debug;
$packet = substr( $packet, 2);
}
my $ether_data = NetPacket::Ethernet::strip($packet);
my $ip = NetPacket::IP->decode($ether_data);
my $tcp = NetPacket::TCP->decode($ip->{'data'});
if ( $tcp->{flags} == SYN ) {
print "- SYN from [$ip->{src_ip}:$tcp->{src_port}]" .
" to [$ip->{dest_ip}:$tcp->{dest_port}]" .
" => sending SYN/ACK\n" if $debug;
$date = `date '+%d/%b/%Y %T'`;
chomp $date;
print LOG "$date [$appname $version] : connection from " .
"$ip->{src_ip}:$tcp->{src_port} to ".
"$ip->{dest_ip}:$tcp->{dest_port}\n";
return if $test;
my $seq = int( rand( 5000000 ) );
my $packet = Net::RawIP->new( {
ip => {
frag_off => 0, tos => 0,
saddr => $ip->{dest_ip},
daddr => $ip->{src_ip}
},
tcp =>{
dest => $tcp->{src_port},
source => $tcp->{dest_port},
seq => $seq,
ack => 1,
syn => 1,
ack_seq => $tcp->{seqnum}+1,
window => 0
}
} );
$packet->send;
} elsif ( $tcp->{flags} == ( SYN | ACK ) ) {
print "- SYN/ACK from [$ip->{src_ip}:$tcp->{src_port}]" .
" to [$ip->{dest_ip}:$tcp->{dest_port}]" .
" => sending RST\n" if $debug;
return if $test;
my $packet = Net::RawIP->new( {
ip => {
frag_off => 0,
tos => 0,
saddr => $ip->{dest_ip},
daddr => $ip->{src_ip}
},
tcp =>{
dest => $tcp->{src_port},
source => $tcp->{dest_port},
seq => $tcp->{acknum},
rst => 1
}
} );
$packet->send;
} elsif (( $tcp->{flags} == ACK ) || ( $tcp->{flags} == (PSH | ACK) )) {
print "- ACK from [$ip->{src_ip}:$tcp->{src_port}]" .
" to [$ip->{dest_ip}:$tcp->{dest_port}]" .
" => jamming with 0-byte window !\n" if $debug;
return $test;
my $packet = Net::RawIP->new( {
ip => {
frag_off => 0,
tos => 0,
saddr => $ip->{dest_ip},
daddr => $ip->{src_ip}
},
tcp => {
dest => $tcp->{src_port},
source => $tcp->{dest_port},
seq => $tcp->{acknum},
ack => 1,
syn => 0,
ack_seq => $tcp->{seqnum}+1,
window => 0
}
} );
$packet->send;
} else {
print "- ignoring TCP flag #" . $tcp->{flags} .
" from [$ip->{src_ip}:$tcp->{src_port}]" .
" to [$ip->{dest_ip}:$tcp->{dest_port}]\n" if $debug;
}
}
sub stop {
my $res = &is_running;
print "$appname $version : $copyright\n\n";
if ( $res ) {
print "\t[ERROR] : daemon is not running\n\n";
exit 1;
}
if ( kill 9, $pid ) {
unlink $pidfile;
$date = `date '+%d/%b/%Y %T'`;
chomp $date;
open LOG, ">>$logfile";
print LOG "$date [$appname $version] : stopping daemon\n";
close LOG;
print "\t[OK] : stopping daemon\n\n";
exit;
# unlikely... :
} else {
print "\t[ERROR] : cannot stop the daemon\n\n";
}
}
sub daemonize {
chdir '/' or die "- [ERROR] : cannot chdir to [/] : $!\n";
umask 0;
open STDIN, '/dev/null' or
die "- [ERROR] : cannot read [/dev/null] : $!\n";
open STDOUT, '>/dev/null' or
die "- [ERROR] : cannot write to [/dev/null] : $!\n";
open STDERR, '>/dev/null'
or die "- [ERROR] : cannot write to [/dev/null] : $!\n";
defined(my $pid = fork) or die "- [ERROR] : cannot fork : $!\n";
exit if $pid;
setsid or die "- [ERROR] : cannot start a session : $!\n";
}
sub is_running{
if ( -e $pidfile ){
open IN, "<$pidfile";
while (<IN>){
chomp;
$pid = $_;
last;
}
close IN;
`ps p $pid >/dev/null`;
$res = ( $? >> 8 );
return $res;
}
return 1;
}
sub help {
print "\n$appname $version - $copyright
options :
--stop : stop the daemon.
--help : display this menu.
--debug : run in debug mode (verbose, no daemon).
--test : simulation (same as debug but doesn't send packets).
--nopromisc : do not use promiscuous mode.
--interface : network interface.
--stats : display stats
TCP parameters :
--source <IP> : source IP (the victim).
--sport <src_port> : source port (single port '--sport 25', multiports
'--sport 25,80,81' or port range '--sport 25-81').
--destination <IP> : destination IP (your server).
--dport <dest_port> : destination port (single port '--dport 25', multiports
'--dport 25,80,81' or port range '--dport 25-81').
";
exit;
}
sub show_stats {
print "\n$appname $version - $copyright\n\n";
if (! -e $logfile ) {
print "\t[ERROR] : you do not have any log yet !\n\n";
exit 1;
}
my ( $start_date, %jammed );
my $today = 0; my $tot = 0;
$date = `date '+%d/%b/%Y'`;
chomp $date;
open IN, "<$logfile";
while ( <IN> ) {
if ( (/^(\d{2}\/.+?\/\d{4})\s+/) && ( ! $start_date ) ){
$start_date = $1;
} else {
if (/^(\d{2}\/.+?\/\d{4})\s+.+?connection from ([\d.]+):/ ) {
if ( $1 eq $date ) {
$today++;
}
$jammed{$2}++;
$tot++;
}
}
}
close IN;
if ( ! $tot ) {
print "- no jammed IPs yet!\n\n";
exit;
}
my $logsize = (stat( $logfile ))[7];
print "- logfile size : $logsize bytes\n";
print "- total jammed connections since $start_date : $tot\n";
print "- total jammed connections today $date : $today\n";
print "- top 15 script-kiddies :\n\t IPs connections\n";
my $count;
foreach my $jamip ( sort { $jammed{$b} <=> $jammed{$a} } keys %jammed ) {
$count++;
print "\t$jamip";
for ( length( $jamip ) .. 15 ) { print " " };
print " $jammed{$jamip}\n";
last if $count == 15;
}
exit;
}
VI - Download :
jamd.tgz - v1.0.1
Others articles : Linux, Security, antispam, spam, firewall, DDoS, attack, network, protection, iptables