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.
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.
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
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.
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; }
Un mail que j'ai reçu récement ...
Merci à Morten
A recetly mail
thanks 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; }