Menu

Un scanner de ports simple avec Sockets en Python

Port Scanner

Introduction

Dans ce nouveau tutoriel, nous allons voir comment créer un scanner de ports à l’aide du langage Python. C’est un projet relativement simple, organisé en deux volets. Nous commencerons dans un premier temps par construire un scanner de port en ligne de commande puis nous créerons une version graphique de l’outil.

Attention: Sachez que l’outil décrit dans ce tutoriel est à usage exclusivement pédagogique et qu’il est formellement interdit de scanner les ports d’un hôte sans sa permission explicite. Pour tester le scanner, faites-le au niveau de votre propre réseau. Vous êtes entièrement responsables de l’usage que vous faites de ce scanner de ports.

Cet article représente la première partie de ce tutoriel. Dans ce premier volet, nous allons voir comment créer un scanner de ports en ligne de commande avec Python. Dans l’article suivant, nous verrons comment utiliser le module Tkinter pour apporter à notre scanner de ports une interface graphique intuitive et facile d’utilisation puis nous produirons un exécutable de l’application via le module PyInstaller.

Le code source final de ce projet est accessible sur GitHub.

Rappels

Avant d’aller plus loin, rappelons quelques notions de base pour mieux faciliter la compréhension du code que nous allons rédiger. Voyons donc comment deux machines sont capables de communiquer entre elles.

Dans ce tutoriel, on va écrire un programme qui va tenter de se connecter à une machine via son adresse IP sur un ou plusieurs ports, selon une architecture client-serveur.

Généralement, un serveur peut héberger un ou plusieurs services / protocoles (HTTP, FTP,SSH, etc.). Lorsqu’un client veut accéder à un service, il doit envoyer une requête au serveur. Cependant l’adresse IP de ce dernier n’est pas suffisante pour spécifier le service voulu. C’est pour cela qu’on associe à chaque service un identifiant unique qu’on appelle port ou numéro de port.

Une machine possède 65535 ports, organisés en trois plages de ports distinctes :

  • [ 0 – 1023] : les ports de cette plage sont fixes et connus de tous. Ils correspondent aux ports standards (“Well-Known Ports”, en anglais) associés à des services spécifiques.
  • [1024 – 49151] : cet intervalle fait référence aux ports que l’on peut associer à des services et enregistrer à l’Internet Assigned Numbers Authority (IANA).
  • [49152 – 65535] : ces ports correspondent aux ports dynamiques. Ils peuvent être utilisés et réattribués par les services et les applications selon le besoin.

Lorsque un service s’exécute sur une machine, le port qui lui est associé est ouvert. On dit aussi qu’il est en écoute sur le réseau. Un client peut alors se connecter à ce port pour requêter ledit service.

Une adresse IP associée à un numéro de port constitue une socket. Les sockets (“connecteurs”, en français) constituent les extrémités d’un canal de communication et de transfert de données entre deux machines ou deux programmes.

Qu’est-ce qu’un scanner de ports ?

Avant de rentrer dans le vif du sujet, voyons ce qu’est exactement un scanner de ports.

Un scanner de ports sert à lister les ports ouverts d’un hôte distant (une machine cible). Il permet de se connecter à un hôte via son adresse IP sur un port spécifique.

Le scan de ports est utilisé dans les tests de pénétration. Il aide à déterminer quel service ou application est hébergé(e) sur une machine, selon que le port correspondant est ouvert ou fermé. Le service ou l’application peut ne pas être à jour et présenter des vulnérabilités. Celles-ci peuvent être exploitées par des personnes malveillantes pour accéder au système.

En scannant les ports de nos équipements, on peut voir si des applications inutiles sont en exécution. On peut alors décider de les retirer, afin qu’elle ne constituent pas un moyen d’intrusion et protéger ainsi nos machines.

Scanner de ports basique

Nous allons commencer par créer un petit scanner de base qui va s’exécuter sur notre machine. A partir de ce programme, nous tenterons d’établir des connexions locales ou distantes sur différents ports pour découvrir ceux qui sont ouverts. Nous avons vu que pour établir une connexion et transférer de l’information, les machines et les programmes utilisent des sockets.

Programmation de sockets en Python

Pour programmer des sockets en Python, il nous faut importer le module socket comprenant les différentes méthodes qui faciliteront la création et la manipulation de sockets.

Dans notre code, on utilise la méthode socket() pour créer une socket côté client. Cette méthode prend en paramètres la famille d’adresse et le type de socket, qui fait référence au protocole réseau utilisé par les services visés par notre scan :

  • Le premier paramètre AF_INET correspond à l’adressage IPv4 ;
  • Le second paramètre SOCK_STREAM fait référence au protocole TCP.

Pour se connecter à une autre socket, on utilise la méthode connect_ex() de notre objet socket, qui prend en argument l’adresse IP et le numéro de port lié au service cible d’une machine donnée. Cette méthode renvoie une indication d’erreur errno si la connexion échoue (on considère que le port est fermé). Dans le cas contraire, elle renvoie la valeur 0 (le port est ouvert).

Scanner de base avec les sockets

Le code ci-après correspond à un scanner basique qui permet de scanner le port d’une cible donnée (“target”, en anglais). Dans ce premier exemple, la cible de notre scan sera notre propre machine. On va alors utiliser pour cela l’hôte local (“localhost“, en anglais). Pour rappel, localhost est aussi un nom de domaine et a une adresse IP qui lui est associée. Lorsqu’on utilise le protocole IPv4, l’adresse IP de notre hôte local est 127.0.0.1.

import socket
target = '127.0.0.1'
port = 80
def scan_port(target, port):
print(f'Scan Target > {target}')
# Create a socket object
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# Test connection
test = s.connect_ex((target, port))
if test == 0:
print(f'Port {port} is [open]')
if __name__ == '__main__':
scan_port(target, port)

Dans ce programme, on commence donc par importer le module socket. On définit ensuite comme cible l’adresse IP de l’hôte local 127.0.0.1 et le port 80 correspondant au service HTTP.

Dans la fonction de base scan_port(), on crée l’objet socket et on utilise la méthode qui lui est associée connect_ex() pour se connecter à la cible. On ajoute une condition if test == 0, qui permet d’afficher un message print(f'Port {port} is [open]'), indiquant que le port est ouvert lorsque la connexion est établie.

On fait ensuite appel à la fonction scan_port() pour démarrer le scan au lancement du script.

Si on n’a pas de serveur web en marche sur notre machine, aucun message ne s’affichera bien évidemment. Pour ma part, je lance mon serveur web de développement Django, de l’application ToDoApp par exemple. Je modifie ensuite, dans le code, le numéro de port à la valeur 8000 (port du serveur Django). Je pourrais alors voir que le scanner a réussi à voir que le port 8000 est bel et bien ouvert.

Scan Target > 127.0.0.1
Port 8000 is [open]

Scanner une liste de ports

Modifions maintenant notre script pour scanner plusieurs ports. On va par la même occasion restructurer notre code en ajoutant quelques fonctions. On fait aussi appel au module datetime pour calculer le temps d’exécution du scan.

import socket
from datetime import datetime
def get_target():
hostname = input("Enter your target hostname (or IP address) : ")
target = socket.gethostbyname(hostname)
print(f'Scan Target > {target}')
return target
def get_port_list():
print(f'Ports Range > [1 – 1024]')
return range(1, 1024)
def scan_port(target, port):
# Create a socket object
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# Test connection
test = s.connect_ex((target, port))
if test == 0:
print(f'Port {port} is [open]')
def port_scanner():
try:
target = get_target()
port_list = get_port_list()
start_time = datetime.now()
for port in port_list:
scan_port(target, port)
except:
print("Something went wrong !")
else:
end_time = datetime.now()
print("Scanning completed in", end_time – start_time)
if __name__ == '__main__':
port_scanner()

Dans notre script, on importe datetime et on définit les fonctions suivantes :

  • get_target() : cette fonction donne la main à l’utilisateur pour qu’il saisisse sa cible. Si l’utilisateur saisit un nom d’hôte, la fonction gethostbyname() retourne l’adresse IPv4 associée à celui-ci et s’il entre une adresse IP, celle-ci est retournée inchangée.
  • get_port_list() : avec cette fonction on se contentera pour l’instant de retourner la liste des ports connus allant de 1 à 1023. On enrichira cette fonction dans le second volet du tuto, où on définira la liste de ports via une interface graphique.
  • scan_port() : cette fonction qu’on a définit plus tôt reste telle quelle.
  • port_scanner() : Dans cette fonction, on fait appel aux fonctions get_target() et get_port_list() pour récupérer les valeurs qu’elles retournent dans les variables target et port_list. On met notre scanner basique scan_port() dans une boucle qui parcourt port_list. Les variables start_time et end_time sont utilisées pour récupérer les temps de début et de fin datetime.now() de l’opération de balayage des ports. Elles nous permettent de calculer et d’afficher le temps que prend une opération de scan.

La dernière ligne du script correspond à l’appel de la fonction port_scanner().

On peut tester le scanner en saisissant le nom de l’hôte local localhost par exemple. On constate cependant que le balayage de ports a pris un temps non négligeable.

Enter your target hostname (or IP address) : localhost
Scan Target  > 127.0.0.1
Ports Range  > [1 - 1024]
Port 135 is [open]
Port 445 is [open]
Port 902 is [open]
Port 912 is [open]
Scanning completed in 0:34:32.289149

Faire appel au Multithreading

Pour améliorer le temps de réponse, on va faire appel au multithreading sous Python, qui permet d’exécuter des programmes simultanément. Pour cela, on utilise le module threading.

On va donc modifier la fonction port_scanner() et créer des threads (“fils d’exécutions”, en français) pour exécuter des scans simultanés sur nos ports.

import socket
from datetime import datetime
import threading
def get_target():
hostname = input("Enter your target hostname (or IP address) : ")
target = socket.gethostbyname(hostname)
print(f'Scan Target > {target}')
return target
def get_port_list():
print(f'Ports Range > [1 – 1024]')
return range(1, 1024)
def scan_port(target, port):
# Create a socket object
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# Test connection
test = s.connect_ex((target, port))
if test == 0:
print(f'Port {port} is [open]')
def port_scanner():
try:
target = get_target()
port_list = get_port_list()
thread_list = list()
start_time = datetime.now()
for port in port_list:
scan = threading.Thread(target=scan_port, args=(target, port))
thread_list.append(scan)
scan.daemon = True
scan.start()
for scan in thread_list:
scan.join()
except:
print("Something went wrong !")
else:
end_time = datetime.now()
print("Scanning completed in", end_time – start_time)
if __name__ == '__main__':
port_scanner()

Dans le programme, on importe d’abord le module threading. Dans la fonction port_scanner(), on initialise la liste thread_list, puis on crée nos threads dans la boucle qui parcourt la liste des ports.

Pour créer l’objet thread, qu’on nommera scan, on instancie la classe Thread qui prend comme paramètres : une cible correspondant à la fonction qui doit être exécutée dans le thread, à savoir scan_port(), ainsi que les arguments qui doivent être passés à celle-ci (target, port).

On ajoute les threads à la liste thread_list. On définit aussi le paramètre daemon des threads à True pour qu’ils n’empêchent pas le programme principal de se terminer correctement. Une fois nos threads déclarés, on les lance avec la méthode start().

Après avoir démarré les threads, on fait appel à la méthode join() pour que le programme principal se mette en pause et attende que l’exécution de tous les threads se termine.

On peut lancer le programme pour voir la différence avec le code précédent en terme de performance.

Enter your target hostname (or IP address) : localhost
Scan Target  > 127.0.0.1
Ports Range  > [1 - 1024]
Port 135 is [open]
Port 445 is [open]
Port 902 is [open]
Port 912 is [open]
Scanning completed in 0:00:02.596342

On peut voir qu’on est passé d’une opération qui avait pris plus de 30 minutes à un scan de moins de 3 secondes !

Conclusion

Nous nous arrêtons là pour cette première partie du tutoriel dédié à la construction d’un scanner de ports. Dans cet article, nous avons couvert quelques concepts clés, telles que les sockets, dont le concours est à la base de la mise en communication des machines sur les réseaux. Nous avons aussi vu comment rendre un programme plus performant, à l’aide d’une technique de parallélisation des processus qu’on appelle threading. Dans l’article qui va suivre, nous allons créer une interface graphique pour le scanner que nous venons de construire.