HomeFAQStatisticsVariousContact

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 Mac.
- plug'n'play : it can be ran and stopped easily, there's nothing to compile and/or patch.



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) :
    • 1) block those 3 ports with iptables (Linux) :

      # 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 :
    • 1) block any access to that IP :

      # 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 :
    • 1) block any access to those 2 ports :

      # 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 :
    • 1) block his access to port 80 :

      # 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 :

    #!/usr/bin/perl
    ######################################################################
    # jamd :
    #
    # tarpit daemon to jam ports scanners, script-kiddies,
    # spammers and some DoS attacks (eg: slowloris)
    #
    # (c) Jerome Bruandet - http://spamcleaner.org/
    #
    # version 1.0.1 - 05-May-2010
    #
    # doc : http://spamcleaner.org/en/misc/jamd.html
    #
    ######################################################################
    # This program is free software: you can redistribute it and/or modify
    # it under the terms of the GNU General Public License as published by
    # the Free Software Foundation, either version 3 of the License, or
    # (at your option) any later version.
    #
    # This program is distributed in the hope that it will be useful,
    # but WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    # GNU General Public License for more details.
    ######################################################################
    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;
    # command line parameters :
    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 );
    
    # check for port/port range and multiport :
    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 = '';
    }
    
    # check IPs :
    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;
    }
    
    # interface :
    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;
    
    # setup our filter :
    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";
       }
    
       # become a daemon :
       &daemonize if ( ! $debug );
    
       # save PID :
       open PID, ">$pidfile";
       print PID $$;
       close PID;
    
       # log file :
       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";
    
       # we only capture the fisrt 96 bytes of each packet :
       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";
       # loop forever :
       Net::Pcap::loop($pcap, -1, \&process_packet, 0);
    
    }
    
    ######################################################################
    
    sub process_packet {
    
       my( $user_data, $header, $packet ) = @_;
    
       # check whether it is a SOCK_RAW packet
       # or a SOCK_DGRAM packet (linux cooked) :
       if ( $packet =~ /^.{14}\x08\x00/ ) {
          print "- linux cooked detected, removing 2 bytes\n" if $debug;
          # remove 2 first bytes :
          $packet = substr( $packet, 2);
       }
    
       # decode packet :
       my $ether_data = NetPacket::Ethernet::strip($packet);
       my $ip = NetPacket::IP->decode($ether_data);
       my $tcp = NetPacket::TCP->decode($ip->{'data'});
    
       # check flags :
       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";
          # don't do anything if we are running in test mode :
          return if $test;
          # reply to SYN with a SYN/ACK :
          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;
          # we don't send SYN, therefore we have no reason
          # to receive any SYN/ACK => reset it :
          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;
          # our victim thinks the connection is established,
          # let's slow it down with zero-byte window ACKs :
          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 {
          # don't know what this packet is, let's ignore it :
          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;
       }
    
       # we don't have much choice : kill it the hard way otherwise
       # we may no be able to break the Net::Pcap::loop :
       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;
    
    }
    ######################################################################
    # EOF
    
    




    VI - Download :

        jamd.tgz - v1.0.1