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.
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 :
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 :
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 *
- import inspect
- from scapy.all import *
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)])))
- print ('\n'.join(sorted ([m[1].__module__ + '.' + m[0] for m in inspect.getmembers(scapy.all, inspect.isclass)])))
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)])))
- 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)])))
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 *
- from scapy.all import *
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)
- 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)
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()
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
IP et ICMP
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()
show2() a permis de le constater. Notez l'existence d'une fonction show()
show() qui affiche le paquet avant que Scapy ne le complète, notamment en calculant les checksums.
Enfin, un appel à la fonction
sr1()
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()
sr1() pour ne récupérer que la première réponse ;
sr()
sr() pour récupérer toutes les réponses ;
srloop()
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)
- 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)
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()
srp1(), srp()
srp(), et srploop()
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 peu
1. 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
- import sys
- import socket
- from scapy.layers.inet import IP
- from scapy.layers.inet import ICMP
- import scapy.sendrecv
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])
- hostname = sys.argv[1]
- hops = int(sys.argv[2])
- timeout = int(sys.argv[3])
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)
- host = socket.gethostbyname(hostname)
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()
format() et non plus les possibilités de formatage de print()
print() :
print('Traceroute jusqu\'à {:s} ({:s}) en {:d} hops maximum, délai de réponse de {:d} ms...'.format(hostname, host, hops, timeout))
- print('Traceroute jusqu\'à {:s} ({:s}) en {:d} hops maximum, délai de réponse de {:d} ms...'.format(hostname, host, hops, timeout))
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)
- packet = IP(dst=host, ttl=i)/ICMP(type='echo-request')
- packets = scapy.sendrecv.sr1(packet, verbose=False, timeout=timeout)
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))
- try:
- hostname = socket.gethostbyaddr(packets[0].src)[0]
- except:
- hostname = 'Hôte non trouvé !'
- print('[{:02d}] {:s} ({:s})'.format(i, hostname, packets[0].src))
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
- if packets [0].src == host:
- break
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))
- print('[{:02d}] Hors délai !'.format(i))
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é.')
- 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é.')
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.
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 :
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 :
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.