Scapy, le couteau suisse Python pour le réseau – partie 2/2

2. Scan de ports

Il est assez facile, et très utile, d’écrire un programme effectuant un balayage de ports à l’aide de scapy : cela nous permet de trouver quels ports sont ouverts sur une machine distante. Il ne suffit pas d’envoyer n’importe quel paquet pour obtenir une réponse satisfaisante. Intéressons-nous à deux exemples détaillés.

2.1 Cas classique : scan TCP

Pour déterminer s’il est possible d’établir une connexion sur un port TCP donné, nous ne pouvons pas nous contenter d’envoyer un paquet forgé « au hasard » : nous risquerions de ne recevoir aucune réponse même si le port est ouvert. Essayons de contacter www.gnulinuxmag.com sans spécifier de drapeaux particuliers dans notre paquet TCP :

Nous devons donc simuler une connexion TCP en trois étapes. Notons que la troisième étape n’est pas strictement nécessaire : recevoir un SYN-ACK nous indiquera que le port scanné est ouvert. Grâce à ce que nous avons appris dans la première partie de cet article, nous pouvons écrire la fonction suivante :

Ce code est relativement simple : on forge un paquet TCP comportant le drapeau SYN, on l’envoie à l’hôte sur le port que l’on souhaite tester, et si l’on reçoit une réponse comportant les drapeaux SYN et ACK, on déclare le port ouvert.

Il est alors possible d’utiliser cette fonction dans l’interpréteur Python, et d’avoir sous la main un outil similaire à nmap, bien que beaucoup moins avancé :

Cette machine du réseau local fait sans doute tourner un serveur SSH, mais pas de serveur web (ou peut-être sur un port autre que le 80).

2.2 Plus compliqué : scan OpenVPN

Dans la partie précédente, nous avons pris l’exemple bien connu de la connexion TCP en 3 étapes. Comment aurait-il fallu procéder pour tester la présence d’un autre service, comme un serveur OpenVPN ?

L’association Aquilenet, fournisseur d’accès à Internet associatif en Gironde, met à disposition de ses adhérents un VPN (vpn.aquilenet.fr) qui écoute sur le port 1194 (le port classique pour OpenVPN). En environnement hostile, il peut pourtant être impossible de se connecter sur ce port, et il pourrait être utile de pouvoir tester rapidement si la connexion est possible sur l’un des autres ports sur lesquels écoute OpenVPN.

On ne peut malheureusement pas se contenter d’envoyer un paquet UDP quelconque, auquel le VPN ne répondrait pas :

Comme précédemment, nous devons donc forger un paquet valide et vérifier que le serveur OpenVPN y répond. Lançons donc la connexion dans un terminal, et regardons les paquets intéressants dans Wireshark (figure 2), grâce au filtre openvpn :

Fig. 2 : Premier paquet envoyé par notre client OpenVPN au serveur.


Nous pouvons alors écrire le code suivant :

La seule subtilité est la gestion du cas où, le port étant injoignable, le serveur nous renverrait un paquet ICMP dont le type serait destination unreachable. On peut noter que nous ne vérifions pas ici le type ; la seule présence de la couche ICMP nous indique que nous n’arrivons pas à joindre le port.

Certes, ce petit programme fonctionne, mais il n’est pas très élégant de recopier la suite d’octets comme nous l’avons fait. Nous aurions préféré écrire quelque chose comme :

Malheureusement, scapy ne définit pas de classe permettant de manipuler des paquets OpenVPN. Pourquoi ne pas l’écrire nous-mêmes dans la partie suivante ?

3. Définissez votre propre type de paquets

Scapy permet d’implémenter un type de paquet en écrivant une classe dérivée de la classe Packet. Voyons cela avec l’exemple du protocole OpenVPN, défini à l’adresse suivante : https://openvpn.net/index.php/open-source/documentation/security-overview.html.

3.1 Une liste de champs

Un paquet est juste une liste de champs. Il suffit donc de les lister, grâce à une syntaxe particulièrement claire :

Nous définissons ici deux champs, que nous avons vu dans la figure 2 :

  • le champ opcode, long de 5 bits, dont la valeur par défaut est 1 ;
  • le champ keyid, long de 3 bits, dont la valeur par défaut est 0.

Nous remarquons que le champ opcode est une énumération de toutes les valeurs possibles pour le champ. Il est d’ailleurs possible d’utiliser les deux syntaxes suivantes, strictement équivalentes, lors de la création d’un paquet :

Essayons maintenant de créer un paquet OpenVPN complet et de l’afficher :

En exécutant ce code, on peut voir 3 couches :

Nous aurions préféré voir une couche OpenVPN ainsi qu’une description lisible des champs définis dans notre classe. Il nous faut ici aider scapy en lui expliquant qu’il convient de décoder la couche qui vient après de l’UDP comme un paquet OpenVPN si le port utilisé est 1194 (le port par défaut d’OpenVPN) :

Si nous relançons le code précédent, nous obtenons désormais un résultat bien plus facile à lire :

On peut donc maintenant envoyer notre paquet et vérifier que nous obtenons bien une réponse, comme précédemment :

Nous n’avons pas implémenté ici tous les champs visibles dans la figure 2 (Session ID, Message Packet-ID, etc.), ce qui nous force à spécifier une bonne partie du paquet en y ajoutant une charge utile. C’est parce que ces champs dépendent en effet de l’opcode. Implémenter complètement le type de paquet OpenVPN est un exercice un peu fastidieux dont la réalisation complète ne présenterait qu’un intérêt limité dans le cadre de cet article. Montrons toutefois comment utiliser des champs « optionnels » dans nos paquets.

3.2 Champs optionnels

Lorsque le paquet OpenVPN est envoyé en utilisant TCP, le premier champ de la couche OpenVPN doit être la taille du paquet, encodée sur 16 bits. Ce champ ne doit pas être présent lorsqu’on utilise UDP. Scapy permet heureusement de déclarer des champs optionnels :

Nous indiquons ici que nous voulons insérer un ShortField uniquement si la fonction passée comme deuxième argument au constructeur de la classe ConditionalField retourne True pour ce paquet. On comprend aisément, en lisant le bout de code ci-dessus, que cette fonction lambda teste si la couche « du dessous » est TCP.  Il nous faut maintenant donner une valeur correcte à ce champ lors de la construction du paquet : la méthode post_build prend en paramètre le paquet et sa charge utile, et nous permet de modifier notre paquet :

Un petit tour dans la documentation du module struct de Python nous apprend que !H est la notation permettant d’obtenir un unsigned short en big endian, ce qui est exactement ce que nous voulons. Nous récupérons la longueur du paquet (moins la taille du champ length lui-même) et l’écrivons dans les deux premiers octets du paquet final.

Il ne nous resterait plus, pour complètement implémenter le format de paquet propre à OpenVPN, qu’à ajouter tous les champs qui dépendent de l’opcode, et à gérer leurs valeurs. Cela suggère bien évidemment de comprendre parfaitement le protocole OpenVPN. L’exercice est laissé au lecteur…

3.3 Sommes de contrôles

Donnons un autre exemple d’opération devant être effectuée dans la méthode post_build : le calcul des sommes de contrôles. Nous avons vu dans la première partie de cet article qu’il nous fallait utiliser la méthode show2, qui affiche un paquet après sa construction, pour pouvoir lire la valeur des sommes de contrôles de nos paquets. Voyons par exemple comment scapy gère le protocole IP :

On voit ici comment, très simplement, scapy calcule la somme de contrôle d’un paquet IP et l’insère au bon endroit. Il est souvent utile d’aller fouiller dans les sources de scapy afin de trouver ce genre de code qui peut être réutilisé dans l’implémentation d’autres types de paquets.

Conclusion

Cette présentation de scapy s’achève, et bien qu’elle ne montre pas toutes les possibilités offertes par l’outil, le lecteur devrait désormais avoir les notions de base lui permettant de commencer à l’utiliser, et devrait pouvoir approfondir par lui-même ses connaissances afin d’écrire le code réseau de ses rêves.

Il est de toute façon particulièrement intéressant de parcourir le code de scapy (comme nous l’avons fait à la fin de la dernière partie de cet article), ou de s’intéresser aux programmes écrits grâce à scapy, afin de découvrir tout le potentiel de ce cadriciel.

Cyril ROELANDT

Retrouvez cet article (et bien d’autres) dans GNU/Linux Magazine Hors-Série n°90, disponible sur la boutique et sur la plateforme de lecture en ligne Connect !

Laisser un commentaire