Repérage de machines (Ecrit en 2003)

Je vais décrire ici un script perl très interressant pour l'administration réseaux et surtout pour la sécurité et la détection d'intrus. Ce script permet de repérer précisément une machine inconnue sur le réseau ou, d'un coté plus pratique, permet de retrouver une machine connue. Ce script ne peut fonctionner qu'avec des switchs administrables, deplus il utilise les adresses ARP pour faire le repérage, le script ne passe donc pas les routeur, et, par conséquant, est limité a l'analyse d'un seul nuage de diffusion.

Fonctionnement

Commencons par trouver l'emplacement d'une machine dont l'adresse ip est connue. Il faut pour cela procéder à une etape qui consiste à assigner chaque port de chaque switch à un emplacement physique dans vos locaux (n° de bureau). Un switch fonctionne de la manière suivante: c'est un element réseau qui travaille principalement sur la couche OSI 2 donc sur le transport. Il retient pour chacun de ses ports pendant un temps prédéterminé la liste des adresses MAC qui peuvent être contactées par celui-ci afin de pouvoir commuter les ports entre eux et transmettre l'information uniquement de l'emmeteur vers le recepteur (contrairement aux hubs qui transmettent l'information de l'emmeteur vers chaque recepteurs). Cette liste d'adresses accessibles par port est stockée dans chaque switch et est fournit par le switch via le protocole SNMP. Il faudra donc comment interoger un equipement via le protocole SNMP.

Il faut résoudre la problématique suivante: à partir d'une adresse ip ou dns, il faut trouver un emplacement physique. Il faut pour cela trouver l'adresse ip à partir de l'adresse dns ou accessoirement l'adresse dns a partir de l'adresse ip (trop d'informations, ce n'est pas superflu), ensuite trouver l'adresse MAC et finalement interroger le switch pour savoir sur lequel de ses ports est connecté l'adresse MAC.

Resolution dns

Celle ci se fait très facilement en perl. Il suffit d' écrire une fonction qui retourne l'adresse ip et l'adresse dns à partir de l'ip ou du dns.
Il faut d'abord détecter si le paramètre d'entrée est une ip ou un nom dns, pour cela, une simple expression régulière ferra l'affaire.
il faut ensuite faire les résolutions, les fonction gethostbyaddr et gethostbyname ferrons l'affaire. Voilà le code de la fonction:

sub getAdress {
  #recuperation du parametre
  my $dns = shift;

  #initialisation des varaibles:
  my ($name, $altnames, $addrtype, $len, @addrlist);
  my ($a, $b, $c, $d);
  my %ret;

  #detection du type d'entrée
  if($dns =~ /[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/){
  
    #separation de l'ip par octet
    my @bytes = split (/\./, $dns);

    #packaging de l'ip pour la fonction suivante
    my $packaddr = pack ('C4', @bytes);

    #resolution ip => dns
    if(!(($name, $altnames, $addrtype, $len, @addrlist) = gethostbyaddr($packaddr, 2))){
      #retourne des valeur par defaut en cas d'erreur
      $name = "?";
      $altnames = "";
      $addrtype = "";
      $len = "";
      $addrlist[0] = $packaddr;
    }
  } else {
    #resolution dns=> ip
    if(!(($name, $altnames, $addrtype, $len, @addrlist) = gethostbyname($dns))){
      #retourne des valeur par defaut en cas d'erreur
      $name = $dns;
      $altnames = "";
      $addrtype = "";
      $len = "";
      my @falseip = ("0","0","0","0");
      $addrlist[0] = pack ('C4', @falseip);
    }
  }

  ($a, $b, $c, $d) = unpack('C4', $addrlist[0]);  #mise en forme de l'ip
  $ret{'dns'} = $name;                            #preparation de la varible de retour
  $ret{'ip'} = "$a.$b.$c.$d";
  $ret{'unpack'} = @addrlist;
  return %ret;                                    #retour
}

la fonction retourne 0.0.0.0 si l'adresse ip n'as pas pu être resolue et «?» si le dns n'as pas pu etre résolu

Resolution IP vers MAC

Il faut maintenant s'attaquer à la résolution MAC, pour celle ci, je n'ai trouvé aucune fonction qui effectue la conversion. Voyons donc comment fonctionne le système. Lorsque l'on veut communiquer avec une adresse ip présente sur le nuage de diffusion, le kernel Linux emmet un paquet en diffusion qui demande quelle est l'adresse MAC corespondante à l'adresse ip demandée, la réponse est stockée dans une table et est conservée un laps de temps predefini. Cette table est gérée par le noyaux et est accessible via ce pseudo fichier: /proc/net/arp. Pour que le noyau résolve une adresse arp, il faut envoyer quelques paquets à l'adresse ip à tester et ensuite intéroger la table ARP du noyau. Pour envoyer une information quelconque via le réseau, j'utiliserai la classe IO::Socket::INET. Voilà le code de la fonction:

sub getMac {
  #Recupération de l'adresse ip en parametre
  my $ip = shift;
  #Initialisation des variables
  my $mac = "00:00:00:00:00:00";
  #Envoie d'une paquet quelconque afin que la rresolution arp s'effectue
  my $MySocket = new IO::Socket::INET->new(PeerPort=>1, Proto=>'udp', PeerAddr=>$ip);
  $MySocket->send("Hello");
  $MySocket->send("You");
  #Analyse de la table arp du noyaux
  open(PF, "</proc/net/arp");
  while(<PF>){
    s/\n//;
    #si l'on trouve l'adresse ip dans cette table on recupere son adresse mac
    $macp = "[0-9,A-Z,a-z]{2}";
    if(/^$ip\s/){
      /.*($macp:$macp:$macp:$macp:$macp:$macp).*/;
      $mac = $1 if(defined($1));
    }
        }
        close(PF);
        return $mac;
}

la fonction retourne 00:00:00:00:00:00 si l'adresse ip n'est pas dans la table arp du noyau.

Interogation des switchs

il ne reste plus qu'a interoger les switch sur l'emplacement de l'arp. Pour cela nous allons utiliser len package Net::SNMP. L'interogation des switchs comporte deux pieges: le premier est que certain port servent d'uplink entre deux switch, ces ports possedent toutes les adresses mac des switchs sur lesquels ils sont connectés (afin de pouvoir diriger le paquet ers le bon switchs). Il faudra donc definir ces port a l'avance et penser a ne pas tenir compte de leur informations. L'autre probleme est que les adresses mac stockées dans les tables arp des differents switchs sont stockées temporairement elles peuvent ne pas être disponibles si le switch n'as pas routé de paquets vers la machine concernée depuis quelque temps, afin de pallier ce problème, il suffit d'envoyer quelque donées a destionation de la machine, c'est pourquoi lors de la résoltuion mac j'envoie deux paquets pour que le switch memorise à coup sur l'adresse mac.
Maintenant un petit mot sur SNMP, c'est un protocole d'administration qui permet de recuperer des infos variés telle que le nom de la machine ou sa table de routage. Nous nous interressons uniquement a sa table des ports ethernet en fonction de la table de correspondance mac, cette table est fournie par cet OID: 1.3.6.1.2.1.17.4.3.1.2 la suite de l'oid est composée des 6 numeros de l'adresse mac au format decimal. Nous devrons donc ecrire une fonction pour decoder le fomat Mac récupéré en format decimal pointé. Cette opération sera decomposée en deux fonction: une fonction de decoupage et mise en forme de l'adresse mac et une fonction de conversion hexa vers decimal. Voilà la fonction de mise en forme:

sub DecodeMac {
  #recuperation du parametre d'entre
  my $arp = shift;
  #initialisation des variables
  my $return;

  #decoupage des paquets
  my @paquets = split(/:/, $arp);

  #mise en forme et conversion
  foreach(@paquets){
    $return .= ".".Hex2Dec($_);
  }
  return $return;
}

Cette fonction renvoie le format mac d'entre sous un format deciaml pointé.

voilà la fonction de decodage, celle ci est basé sur un simple tableau de corespondance haxa decimal

sub Hex2Dec {
  #recuperation du parametre
  my $car = shift;
  
  #initialisation de la table de conversion
  my %table = (
    "0"=>"0","1"=>"1","2"=>"2","3"=>"3","4"=>"4",
    "5"=>"5","6"=>"6","7"=>"7","8"=>"8","9"=>"9",
    
    "A"=>"10","B"=>"11","C"=>"12",
    "D"=>"13","E"=>"14","F"=>"15",
    
    "a"=>"10","b"=>"11","c"=>"12",
    "d"=>"13","e"=>"14","f"=>"15"
  );
  
  #separation des deux octets
  my @chars = split(//, $car);


  #conversion
  return $table{$chars[0]}*16 + $table{$chars[1]};
}

Il ne reste plus qu'a interroger les switchs pour recuperer le port. L'interogatrion en elle emme est tres simple, mais afin d'avoir un script souple, il faut faire queqlues variabls de configurations afin de pouvoir changer l'ip des switch ou meme rajouter un switch sans avoir à toucher le code.

#liste des adresse ip des differents switchs a questionner
our @switchs = (
  "192.168.1.9",
  "192.168.1.6",
  "192.168.1.8",
  "192.168.1.7"
);

#numeros des switchs
our %SNames = (
  "192.168.1.9" => "1",
  "192.168.1.6" => "2",
  "192.168.1.8" => "3",
  "192.168.1.7" => "4"
);

#listes des ports a ignorer separes par des virgules
our %NotScan = (
  "192.168.1.9"=> "12,13,36,37", 
  "192.168.1.6"=> "12,13,36,37", 
  "192.168.1.8" =>"12,13,36,37", 
  "192.168.1.7" =>"12,13,36,37"
);

sub findport {
  #recupere le parametre
  my $arp = shift;

  #initialise les varibales
  my %ret;
  my $erreur;
  $ret{'unit'} = "0";
  $ret{'port'} = "0";
  my $research = "1.3.6.1.2.1.17.4.3.1.2".DecodeMac($arp);
  
  #interoge tous les switchs
  foreach(@switchs){
    my $ip = $_;
    my ($session, $error) = Net::SNMP->session(
      -hostname   =>$_,
      -community  => 'public',
      -port       => 161
    );
    my $result = $session->get_request(
      -varbindlist => [$research]
    );
    if(defined($result)){
      my $port = $result->{$research};
      $erreur = 0;
      
      #si un port a ignorer est concerner, on change de switch
      foreach(split(/,/,$NotScan{$ip})){
        if($_ == $port){
          $erreur = 1;
        }
      }
      if($erreur==0){
        $ret{'unit'} = $SNames{$_};
        $ret{'port'} = $port;
      }
    }         
    $session->close;
  }
  return %ret;
}

Add-ons - des infos et codes supplémentaires ...


Un mail que j'ai reçu récement ...
Merci à Morten

A recetly mail
thanks Morten

I ones made at program like yours, that could find a switchport, given one IP address of a unit and the IP or DNS address of the "root" switch - I did my program in bash (linux) and then again in Kix32 (windows) - But I like yours perl implementation much better - and have now added a little functionality to your perl script - Given that SwitchNames and DNS is configured correctly, the new functionality automatically finds the correct switch, before returning values to the calling program... and You dont have to know how the switches are connected at all...

Thanx again

/Morten
sub FindPort {
  use Net::SNMP qw(oid_lex_sort oid_base_match SNMP_VERSION_1 DEBUG_ALL);

  my $CISCO=".1.3.6.1.2.1.17.4.3.1.2";
  my $DLINK=".1.3.6.1.2.1.17.7.1.2.2.1.2.1";

  my $switch      = $ARGV[0];  #ie. 10.0.0.2
  my $PCIP        = $ARGV[1];  #ie. 10.0.10.2
  my $community   = $ARGV[2];  #ie. public

  $mac=DecodeMac(getMac($PCIP));
   
  $dbPort      = "$CISCO";
  $PortIfIndex = ".1.3.6.1.2.1.17.1.4.1.2";
  $ifName      = ".1.3.6.1.2.1.31.1.1.1.1";
  $neighbor    = ".1.3.6.1.4.1.9.9.23.1.2.1.1.6";
   

  $stop=0;
  while (($switch) and ("$stop"<"1")){
    ($session, $error) = Net::SNMP->session(
            -hostname  => $switch,
            -community => $community,
            -port      => 161
            );
    $snmp1        = $dbPort  . $mac;
    $result = $session->get_request( -varbindlist => [$snmp1] );
    $map1 = $result->{$snmp1};
  
    $snmp2 = $PortIfIndex . "." . $map1;
    $result = $session->get_request( -varbindlist => [$snmp2] );
    $map2 = $result->{$snmp2};
  
    $snmp3 = $ifName . "." . $map2;
    $result = $session->get_request( -varbindlist => [$snmp3]  );
    $map3 = $result->{$snmp3};
  
    $odi;
    $StopHere = $neighbor . "." . $map2;
    @args=$StopHere;
##
## snmpwalk.....
##
    while (defined($session->get_next_request(@args))) {
      $oid = ($session->var_bind_names)[0];
      if (!oid_base_match($StopHere, $oid)) { last; }
      $map4 = $session->var_bind_list->{$oid};
      @args = (-varbindlist => [$oid]);
    }
##
## Simpel antagelse - naar svaret starter med "Fa0/" saa er vi enden
## Boer maaske laves om til, "Naar naeste enhed ikke er en switch"
##
    if ( !(split("Fa0\/", $map3) eq "1"))
    { $stop=1;};
  
  
    $switch=$map4;
    $session->close;
  }
  return $switch,$map3;
}