AccueilFAQStatistiquesDiversContact

Securité : Linux : Jamd




Dernière version : v1.0.1 (05-Mai-2010)



I - Présentation :

Jamd est un petit daemon qui permet de tarpiter/engluer les scanners de port, script-kiddies, spammers et dans certains cas, certaines attaques DoS (slowloris).

La méthode du tarpit est connue depuis longtemps grâce notamment à l'utilitaire LaBrea mais aussi au module tarpit d'iptables. Jamd reprend le même concept mais en simplifiant son utilisation :
- portabilité : il fonctionne sous toute distribution Linux, FreeBSD et probablement autres *BSD et peut-être même sous Mac.
- simplicité : on peut le lancer et l'arrêter quand on veut, il n'y a rien à compiler et/ou patcher ni même à configurer.



II - Paramètres :


   options :
     --stop              : arrête daemon.
     --help              : affiche ce menu.
     --debug             : tourne en mode debug (ne lance pas le daemon).
     --test              : simulation (comme debug mais ne répond pas aux paquets).
     --nopromisc         : ne met pas l'interface en mode promiscuous.
     --interface         : interface réseau.
     --stats             : affiche les stats.

   TCP parameters :
     --source <IP>       : IP du client (la victime).
     --sport <src_port>  : port du client (single port '--sport 25', multiports
                           '--sport 25,80,81' ou port range '--sport 25-81').
     --destination <IP>  : IP de destination (votre serveur).
     --dport <dest_port> : port de destination (single port '--dport 25', multiports
                           '--dport 25,80,81' ou port range '--dport 25-81').

Le lancement nécessite comme paramètres au moins l'interface ainsi qu'un port ou une IP (source ou destination).

Les paramètres '--dport' et '--sport' acceptent un seul port (single port), une combinaison (multiports) ou éventail (port range) de ports.


III - Module Perl nécessaires :

Les modules suivants sont nécessaire pour lancer jamd :

  • Net::RawIP : utilisé pour créer des paquets TCP.

  • Net::Pcap : utilisé pour capturer les paquets TCP.

  • NetPacket::Ethernet : utilisé pour décoder les paquets TCP interceptés.

  • Getopt::Long : utilisé pour récupérer les paramètres de ligne de commande.



IV - Utilisation :

Dans tous les cas de figure, vous devez vous assurer que votre serveur ne réponde pas aux requêtes que vous souhaitez tarpiter. Cela implique généralement l'ajout d'une règle au firewall pour ignorer ces paquets. Avec iptables sous linux, utilisez l'action '-j DROP'.

Voici une liste non exhaustive d'exemples d'utilisation de jamd.

  • vous souhaitez tarpiter tous les scans des ports Netbios (ports 137 à 139) effectués sur l'IP 1.2.3.4, interface eth0 de votre serveur :
    • 1) bloquez ces ports avec iptables (sous Linux) :

      # iptables -I INPUT -i eth0 -d 1.2.3.4 -p tcp --dport 137:139 -j DROP
      

      2) lancez jamd (port range):

      # jamd --interface eth0 --destination 1.2.3.4 --dport 137-139
      

  • vous disposez d'une IP supplémentaire (1.2.3.4 / eth0) qui ne vous sert à rien. Vous pouvez donc tarpiter tous les ports de celle-ci :
    • 1) bloquez tout accès à cette IP :

      # iptables -I INPUT -i eth0 -d 1.2.3.4 -j DROP
      

      2) lancez jamd :

      # jamd --interface eth0 --destination 1.2.3.4
      

  • votre serveur (1.2.3.4 / eth0) ne dispose pas d'accès FTP (21) ni d'accès POP3 (110). Tarpitez les connexions à ces 2 ports :
    • 1) bloquez tout accès à ces 2 ports :

      # iptables -I INPUT -i eth0 -d 1.2.3.4 -p tcp -m multiport --dports 21,110
      

      2) lancez jamd (multiports) :

      # jamd --interface eth0 --destination 1.2.3.4 --dport 21,110
      

  • votre serveur 1.2.3.4 / eth0 est victime d'une attaque slowloris (2000 connexions) sur le port 80, provenant de l'IP 2.2.2.2. Tarpitez votre attaquant :
    • 1) bloquez l'accès de l'attaquant à votre port 80 :

      # iptables -I INPUT -i eth0 -s 2.2.2.2 -p tcp --dport 80 -j DROP
      

      2) lancez jamd :

      # jamd --interface eth0 --destination 1.2.3.4 --dport 80 --source 2.2.2.2
      

      3) relancez Apache pour qu'il se remette de ses émotions et ferme toutes ces sockets.

      En quelques secondes, sans le savoir ni même s'en apercevoir, l'attaquant va se transformer en victime. Les 2000 sockets établies seront maintenues sur son serveur uniquement, pas sur le votre. Le résultat est à peu près équivalent à une attaque DoS "netkill" inversée.

  • vous disposez d'une adresse IP 1.2.3.4 sur laquelle aucun serveur SMTP n'écoute et d'un nom de domaine 'domaine.com'. Faites-vous plaisir : tarpitez les spammers !
    • 1) créez un sous domaine (ex : jamd.domaine.com), faitez-le pointer sur cette IP et ajoutez-lui un champ MX :

      jamd.domaine.com.       IN   MX 10  jamd.domaine.com.
      
      2) insérez/cachez des dizaines d'adresses email dans le code HTML de votre site (ex: hello@jamd.domaine.com, eatme@jamd.domaine.com, spamsucks@jamd.domaine.com etc).

      3) bloquez l'accès à votre port 25 :

      # iptables -I INPUT -i eth0 -d 1.2.3.4 -p tcp --dport 25 -j DROP
      

      4) lancez jamd :

      # jamd --interface eth0 --destination 1.2.3.4 --dport 25
      

      Les spammers se feront un plaisir de voler ces adresses email et de venir s'engluer sur votre 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-Mai-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;
    # gestion des paramètres :
    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 );
    
    # récupère les paramètres port/port range/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 = '';
    }
    
    # paramètres des 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;
    
    # préparation du filtre :
    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";
       }
    
       # lance le daemon :
       &daemonize if ( ! $debug );
    
       # sauvegarde son PID :
       open PID, ">$pidfile";
       print PID $$;
       close PID;
    
       # fichier log :
       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";
    
       # on capture 96 octets maxi pour chaque paquet TCP :
       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 infinie :
       Net::Pcap::loop($pcap, -1, \&process_packet, 0);
    
    }
    
    ######################################################################
    
    sub process_packet {
    
       my( $user_data, $header, $packet ) = @_;
    
       # on vérifie s'il s'agit d'un paquet SOCK_RAW
       # ou bien d'un paquet SOCK_DGRAM (linux cooked) :
       if ( $packet =~ /^.{14}\x08\x00/ ) {
          print "- linux cooked detected, removing 2 bytes\n" if $debug;
          # suppression des 2 premiers octets si SOCK_DGRAM :
          $packet = substr( $packet, 2);
       }
    
       # decode le paquet :
       my $ether_data = NetPacket::Ethernet::strip($packet);
       my $ip = NetPacket::IP->decode($ether_data);
       my $tcp = NetPacket::TCP->decode($ip->{'data'});
    
       # vérifie ses 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";
          # on ne fait rien de plus si on est en mode 'test' :
          return if $test;
          # on repond au SYN avec un SYN/ACK pour 'établir' la connexion :
          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;
          # on n'envoie pas de SYN, on n'a donc pas de raison
          # de recevoir un SYN/ACK => on reset :
          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;
          # notre victime pense que la connexion est établie,
          # on continue et on le ralenti en envoyant des ACKs,
          # avec une fenêtre initialisée à 0 :
          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 {
          # ce paquet ne nous interesse pas, on l'ignore :
          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;
       }
    
       # on n'a pas beaucoup de choix : on le tue violemment sinon
       # la loop de Net::Pcap pourrait ne pas être interrompue :
       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 - Téléchargement :

        jamd.tgz - v1.0.1