Un traceroute roots en Python avec Scapy

Scapy est un package de Python bien connu dans le monde du hacking. Il permet d'interagir à bas niveau avec une interface réseau, c'est-à-dire de déterminer les octets qu'elle émet et de récupérer ceux qu'elle reçoit.
Scapy peut donc notamment être utilisé pour forger des trames et/ou paquets sur un réseau. Cela peut permettre de se livrer à des activités que la morale réprouve, comme l'ARP cache poisoning et autres joyeusetés. A l'inverse, comme nous le recommanderons, cela peut permettre de s'adonner à des activités tout à fait respectables, comme le diagnostic.
Scapy, pour sniffer, spoofer et autres
Dans tous les cas, se mettre à Scapy permet de se former au fonctionnement de réseaux en commençant par là où il faut commencer chaque fois qu'on prétend réellement apprendre quelque chose dans quel que domaine, c'est-à-dire à la base. C'est cette finalité pédagogique que poursuit cet article, à l'attention de tous ceux qui n'ont jamais mis le nez dans le réseau. Il présente comment réaliser une version simplifiée d'un outil qui figure inévitablement dans la boite à outils de tout hacker, à savoir traceroute.

Se mettre à l'écoute avec WireShark

Avant de rentrer dans le code, il est nécessaire d'installer quelques outils pour constituer un environnement de travail. Au-delà d'un EDI tel que l'excellent PyCharm, il s'agit de se doter de Scapy, de WireShark et de Npcap si vous travaillez sur Windows – ce qui sera le cas ici.
Pour ce qui concerne Scapy, la chose est rendue d'une simplicité déconcertante par l'excellent pip. Depuis une ligne de commandes, exécutez la commande suivante, et c'est réglé :
pip install scapy
Les deux autres outils vont vous permette de visualiser parallèlement ce qui sort et rentre sur l'interface que vous aurez choisie pour envoyer des données sur le réseau avec Scapy.
WireShark est sniffer que sa qualité a rendu incontournable. Lors de son exécution, l'installateur vérifie la présence d'un pilote d'interface permettant l'accès aux données circulant via cette dernière. Sur Windows, un tel pilote n'est pas disponible par défaut. Pour cette raison, s'il n'en détecte pas une version déjà installée, WireShark propose d'installer Npcap, un pilote qui permet la chose.
Après avoir installé WireShark, il convient de vérifier que tout fonctionne bien en tentant de sniffer les messages ICMP échangés lors d'un ping.
Démarrez WireShark. Ce dernier affiche une liste des interfaces. Si vous cliquez une fois sur l'une d'entre elles, vous pouvez alors saisir un filtre dans le champ figurant au-dessus de la liste. En l'espèce, sélectionnez votre interface réseau par défaut, et saisissez icmp, filtre prédéfini pour les messages ICMP uniquement. Double-cliquez ensuite sur cette connexion pour accéder à la capture :
WireShark, le sniffer par excellence
Notez qu'il est possible de capturer sur plusieurs interfaces simultanément en les sélectionnant de manière multiple (Maj + clique). Pour accéder au sniffing sans réinitialiser la sélection, cliquez alors sur l'icône Start capturing packets.
Ouvrez une ligne de commandes (cmd.exe), et exécutez un ping quelconque, comme par exemple :
ping -n 1 www.stashofcode.fr
Dans WireShark, vous devez voir s'afficher la liste des messages ICMP échangés :
Une capture de WireShark, bien caviardée!
C'est tout de suite l'occasion de formuler une mise en garde. Parce qu'elle affiche le détail des paquets, une fenêtre de WireShark contient énormément d'informations que vous pourriez juger sensibles – en premier lieu, l'adresse MAC de votre interface par défaut et votre adresse IP. En conséquence, ne faites pas circuler de capture de cette fenêtre sans avoir pris le temps de caviarder les informations en question.
En fait, évitez de faire circuler des telles captures tout court, car caviarder prend un certain temps – voyez la capture réalisée pour illustrer ce bête de ping –, et dans la précipitation, vous risquez d'oublier de masquer une de ces informations, tout particulièrement dans le dump où il n'est pas intuitif de les repérer – même si WireShark vous aide en affichant à quoi correspond un octet quand vous le pointez avec la souris.

Premiers pas avec Scapy

La documentation de Scapy présente l'intérêt d'être assez didactique. Toutefois, son propos n'est que d'introduire au sujet. Par ailleurs, il ne semble pas exister de guide de référence dressant la liste exhaustive de tout ce qui peut être nécessaire pour coder, notamment les classes et les fonctions.
Pour pallier le problème, la documentation explique qu'il est possible de générer des diagrammes UML. Cela prend du temps et ne présente aucun intérêt, comme UML assez généralement – on est entre codeurs, pas entre pisseurs de « docs ». Deux lignes de code constituent une solution plus efficace. Après importation des packages requis... :
import inspect
from scapy.all import *
...cette ligne permet de générer la liste des classes... :
print ('\n'.join(sorted ([m[1].__module__ + '.' + m[0] for m in inspect.getmembers(scapy.all, inspect.isclass)])))
...et celle-ci permet de générer la liste des fonctions :
print ('\n'.join(sorted ([m[0] + ' ' + str(inspect.signature(m[1])) + ' [' + inspect.getfile(m[1])[len('C:\\Python36\\lib\\site-packages\\scapy\\'):] + ']' for m in inspect.getmembers(scapy.all, inspect.isfunction)])))
C'est un jeu très limité de classes, constantes et fonctions de Scapy qui seront utilisées ici. Pour la précision du propos, les importations seront détaillées. Dans la réalité, il est possible de s'en passer en procédant à l'importation générale déjà utilisée :
from scapy.all import *
Envoyons un premier paquet ? Par exemple, un message ICMP Echo, comme vous pourriez en envoyer pour réaliser ping permettant de vérifier que le serveur hébergeant votre site Web préféré www.stashofcode.fr est bien en ligne. Vous fournirez ce nom en argument, ce qui vous permettra de chatouillez un autre serveur si cela vous chante.
Le code est le suivant :
import sys
import socket
from scapy.layers.inet import IP
from scapy.layers.inet import ICMP
import scapy.sendrecv

host = socket.gethostbyname(sys.argv[1])
packet = IP(dst=host)/ICMP(type='echo-request')
packet.show2()
scapy.sendrecv.sr1(packet)
A l'exécution, Python doit afficher le détail de ce que vient d'être envoyé, à savoir un paquet IP encapsulant un message ICMP Echo. Par ailleurs, WireShark doit avoir tracé les échanges.
Après un appel à socket.gethostbyname() destiné à résoudre le nom en adresse IP, les premières fonctionnalités de Scapy sont utilisées, et elles permettent de vite comprendre combien le package peut faciliter la vie pour créer des paquets.
En effet, pour créer le paquet souhaité, il a suffi d'une ligne invoquant successivement les constructeurs des classes IP et ICMP, et combinant les résultats via un opérateur surchargé, la barre de fraction. Lors des appels aux constructeurs, seuls les paramètres jugés utiles dans le contexte ont été précisés. Scapy s'est occupé du reste.
Le résultat produit par l'appel à la méthode show2() a permis de le constater. Notez l'existence d'une fonction show() qui affiche le paquet avant que Scapy ne le complète, notamment en calculant les checksums.
Enfin, un appel à la fonction sr1() a permis de commander l'envoi du paquet en tant que paquet de couche 3 sur l'interface par défaut, et de récupérer la réponse. Plusieurs fonctions auraient pu être utilisées :
  • sr1() pour ne récupérer que la première réponse ;
  • sr() pour récupérer toutes les réponses ;
  • srloop() pour afficher toutes les réponses.
Pour rappel, dans le modèle OSI auquel il est fait référence, cette couche est Réseau ; c'est celle du protocole IP. Notez qu'un grand intérêt de Scapy est de permettre de taper plus bas, au niveau de la couche 2, celle du Data Link. Ainsi, il est possible de composer une trame Ethernet :
from scapy.layers.inet import IP
from scapy.layers.inet import ICMP
from scapy.layers.inet import Ether

packet = Ether()/IP(dst=host)/ICMP(type='echo-request')
scapy.sendrecv.srp(packet)
A ce niveau, les fonctions présentées à l'instant ont toutes des homologues qui permettent de spécifier l'interface via laquelle le paquet doit être envoyé : srp1(), srp(), et srploop().
Votre petit colis ayant été bien ficelé, comme le dirait Benoît Poelvoorde, il pouvait être plongé dans la rivière des données. Comme Scapy, WireShark a dû permettre de constater qu'il remontait effectivement à la surface, en offrant toutefois plus de moyens pour l'autopsier.

De l'importance de traceroute

Si vous voulez faire l'intéressant en matière de sécurité informatique, procurez-vous n'importe quel manuel de guerre, comme par exemple L'Art de la guerre de Sun Tzu ou De la guerre de Clausewitz, et recyclez le propos en l'adaptant un peu1. En effet, la guerre dans le monde cyber, c'est comme la guerre dans le monde réel – pour caricaturer une formule du casque à pointe, disons que la première est la continuation de la seconde par d'autres moyens.
Par exemple, dans A Technique for Network Topology Deception, les auteurs font référence à un autre en ces termes : "Whaley describes deception in two categories: hiding the real, or dissimulation; and showing the false, or simulation". Dans le brick and mortar, un militaire y verrait un truisme. Dans le dans le cyberspace, des chercheurs y voient une découverte. Comme le disaitl'aurait dit Mongénéral : "Des chercheurs qui cherchent, on en trouve. Des chercheurs qui trouvent, on en cherche". Enfin, bref...
Donc depuis que l'Homme existe, pour organiser sa défense comme son attaque, la première chose à faire est de savoir où l'on met les pieds – "La géographie, ça sert d'abord à faire la guerre", comme l'a écrit Yves Lacoste. En matière de sécurité informatique, cela revient notamment à cartographier le réseau. Pour ce faire, traceroute est indéniablement d'une grande utilité.
L'outil consiste à exploiter une particularité d'IP, à savoir le Time to Live (TTL). Pour décrire le concept, rien de mieux que de se référer à la source, à savoir la Request for Comments (RFC) 791. Librement traduit, cela donne :
Le TTL donne une indication de la durée de vie maximale d'un datagramme. Il est renseigné par l'expéditeur du datagramme, et réduit à chaque étape de la route où ce datagramme est traité. Si le TTL atteint zéro avant que le datagramme parvienne à sa destination, le datagramme est détruit. On peut voir le TTL comme un délai avant autodestruction.
L'intérêt du TTL est qu'il permet d'éviter de congestionner le réseau y faisant circuler ad vitam aeternam des paquets qui ne peuvent être acheminés.
La bonne idée des auteurs de traceroute, c'est d'exploiter le fait que lorsqu'il constate que le TTL d'un paquet qui lui parvient expire, un hôte sur la route – un routeur – avertit l'expéditeur que son paquet est détruit. Pour ce faire, le routeur lui retourne un message ICMP Time Exceed.
Dès lors, il est possible de concevoir le scénario suivant pour cartographier la route qu'un paquet emprunte pour atteindre un destinataire :
  • envoyer un message ICMP Echo encapsulé dans un paquet IP de TTL valant N (initialisé à 1) ;
  • si le destinataire répond par un message ICMP Echo Reply, s'arrêter là ;
  • sinon, noter l'IP de l'intermédiaire qui répond par le message ICMP Time Exceed, incrémenter N, et reprendre le processus.

Votre traceroute avec Scapy

Le principe de traceroute est donc des plus simples. L'implémentation en Python l'est tout autant, comme vous allez rapidement pouvoir le constater. Tout d'abord, quelques imports nécessaires – comme déjà expliqué, les imports sont détaillés pour vous permettre de bien visualiser ce qui est utilisé :
import sys
import socket
from scapy.layers.inet import IP
from scapy.layers.inet import ICMP
import scapy.sendrecv
Tout d'abord, vous devez lire les arguments transmis en ligne de commande. Pour que ce traceroute soit un minimum ergonomique, disons qu'il prendra trois arguments dans l'ordre : le nom de l'hôte qu'il s'agit de rejoindre, le nombre maximum d'hôtes dont il s'agit de tester la présence sur la route (incluant l'hôte à rejoindre), et le délai maximum pour attendre une réponse d'un hôte.
hostname = sys.argv[1]
hops = int(sys.argv[2])
timeout = int(sys.argv[3])
A partir de son nom, récupérez l'adresse IP de l'hôte :
host = socket.gethostbyname(hostname)
De là, affichez quelques informations pour faire comprendre à l'utilisateur comment sa commande est comprise. C'est l'occasion de faire remarquer qu'il convient d'utiliser format() et non plus les possibilités de formatage de print() :
print('Traceroute jusqu\'à {:s} ({:s}) en {:d} hops maximum, délai de réponse de {:d} ms...'.format(hostname, host, hops, timeout))
La boucle principale consiste à envoyer un message ICMP Echo dont le TTL du paquet IP va progressivement être incrémenté, jusqu'à ce que l'hôte qu'il s'agit de rejoindre finisse par répondre. A chaque étape, vous devez afficher des informations sur l'hôte qui a répondu – si un hôte a répondu – afin que l'utilisateur puisse visualiser la route.
Tout d'abord, vous devez donc forger le paquet et de l'envoyer en demandant à récupérer la première réponse :
packet = IP(dst=host, ttl=i)/ICMP(type='echo-request')
packets = scapy.sendrecv.sr1(packet, verbose=False, timeout=timeout)
Vous devez ensuite vérifier si vous avez reçu une réponse, ce qui ne sera pas le cas si le délai pour répondre a été dépassé. Si une réponse a été reçue, vous pouvez essayer de récupérer le nom de l'hôte qui en est à l'origine pour l'afficher, en plus de son adresse IP. Cela requiert de gérer une exception :
try:
	hostname = socket.gethostbyaddr(packets[0].src)[0]
except:
	hostname = 'Hôte non trouvé !'
print('[{:02d}] {:s} ({:s})'.format(i, hostname, packets[0].src))
De plus, s'il y a eu réponse, vous devez tester une condition de fin prématurée, à savoir si l'hôte qui a répondu est celui que vous tentiez de rejoindre :
if packets [0].src == host:
	break
Enfin, si vous n'avez pas reçu de réponse, vous devez le signaler à l'utilisateur :
print('[{:02d}] Hors délai !'.format(i))
A final, cela donne :
for i in range(1, hops + 1):
	packet = IP(dst=host, ttl=i)/ICMP(type='echo-request')
	packets = scapy.sendrecv.sr1(packet, verbose=False, timeout=timeout)
	if packets:
		try:
			hostname = socket.gethostbyaddr(packets[0].src)[0]
		except:
			hostname = 'Hôte non trouvé !'
		print('[{:02d}] {:s} ({:s})'.format(i, hostname, packets[0].src))
		if packets [0].src == host:
			break
	else:
		print('[{:02d}] Hors délai !'.format(i))
print('Traceroute terminé.')
C'est tout, et ce n'est donc vraiment pas sorcier. Cliquez ici pour récupérer le code et vous amuser avec.

L'intoxication non alimentaire

Pour reprendre le fil du propos stratégique entamé plus tôt, depuis que l'Homme existe, il cherche à tromper l'ennemi en lui faisant croire que ce qui est n'est pas, ou n'est pas ce que c'est : il prive ou il intoxique, c'est selon. En matière de complexité, privation et intoxication se valent bien. En effet, dès lors qu'il s'agit de s'attaquer à autre chose qu'un électeur de Donald Trump, il est tout aussi délicat de conserver un secret que de faire avaler une couleuvre.
Comme vous ne manquerez pas de le constater, et comme cela a été soigneusement prévu dans le code qui a été présenté, certaines étapes de la route que vous chercherez à tracer ne pourront être identifiées :
[01] Hôte non trouvé ! (***.***.***.***)
[02] Hôte non trouvé ! (10.70.0.1)
[03] 195-132-10-209.rev.numericable.fr (195.132.10.209)
[04] Hors délai !
[05] ip-174.net-80-236-0.static.numericable.fr (80.236.0.174)
[06] Hors délai !
[07] cpe-et003679.cust.jaguar-network.net (31.7.252.135)
[08] Hors délai !
[09] mutu.phpnet.org (195.144.11.40)
Traceroute terminé.
Que se passe-t-il ? Le message ICMP Echo est bien envoyé encapsulé dans un paquet IP au TTL donné, mais il n'entraîne aucune réponse, quel que soit le timeout. C'est qu'à cette étape, l'hôte chez qui le TTL du paquet expire est configuré pour ne pas répondre. C'est un cas de privation.
Toutefois, cela n'empêche pas de savoir qu'il y a une étape, ce qui peut constituer une information assez intéressante pour conduire une attaque, tout particulièrement une attaque qui vise à couper un hôte de son environnement non plus en le saturant de requêtes, comme c'est le cas dans une attaque de type Denial of Service (DoS), mais en saturant les routes par lesquelles il est relié à l'extérieur, par du trafic apparemment légitime qui plus est. Ce type d'attaque est décrit dans The Coremelt Attack.
Pour s'en défendre, certains entreprennent de battre en brèche la possibilité de cartographier un réseau avec traceroute en mobilisant des systèmes d'intoxication – deception, disent les engliches. Ici, il ne s'agit donc plus de faire en sorte que l'assaillant ne se retrouve qu'avec du gruyère, dont il ne resterait éventuellement que les trous – le faire zazou. Il s'agit de renvoyer des informations qui conduiront cet assaillant à se faire une image erronée de la topologie du réseau – bref, l'empapaouter.
Ainsi, NetHide: Secure and Practical Network Topology Obfuscation, les auteurs décrivent un système qui repose sur la possibilité de configurer un routeur pour modifier le TTL d'un paquet entrant.
L'exemple qu'ils donnent implique la topologie réelle représenté sur la figure suivante. Dans cette typologie, un paquet dont le TTL vaut 2 envoyé à destination de E rentre par A et succombe en C :
Expiration du TTL sans empapaoutage
Si A est configuré pour modifier le paquet en incrémentant son TTL de 1, le paquet succombe en D. Par conséquent, l'assaillant croit que sur la route qui le relie à E, A est relié directement à D, alors qu'en réalité, A est relié à D par l'intermédiaire de C :
Expiration du TTL avec empapaoutage
Le système élaboré sur cette possibilité ne se résume pas à cela. A la lecture du papier, il apparaît d'autant plus complexe que les auteurs cherchent à permettre l'utilisation de traceroute à des fins de diagnostic tout en trompant sur la topologie réelle du réseau.
Toutefois, dans le cadre de cet article, il ne s'agissait pas d'aller au-delà. Retenez donc simplement qu'en plus de ne pas toujours retourner d'information, le traceroute que vous venez de coder en Python peut donc ne pas retourner une information fiable. Mais que cela ne vous empêche pas de vous amuser avec Scapy !
1 Packagez ensuite en fonction de votre interlocuteur, présentant la chose comme une élaboration lors de réunions de travail, comme une vulgarisation lors de cocktails mondains.