Recherche de SSID grâce à des Probe Request avec Scapy

Brève présentation de Scapy

Scapy est un programme qui permet de manipuler des paquets réseaux (enregistrer, charger, sniffer, filtrer, analyser, etc.). C'est un logiciel libre et open-source. Il est écrit en langage Python. Parmi ces nombreuses fonctionnalités, il y a par exemple : la gestion des paquets relatifs au Wi-Fi (sous forme déjà structurée), le sniffage, et la gestion des fichiers pcap.

Installation de Scapy

Installation de Scapy depuis un gestionnaire de paquets

Au moins Debian et Trisquel le proposent. Il existe en 2017 en 2 versions : une pour Python 2 et une pour Python 3. python-scapy désigne le paquet pour Python 2 et vous devriez deviner pour python3-scapy. L'installation nécessite les droits root/SuperUtilisateur, acquérable avec la commande su de manière permanente (déconseillé) ou avec sudo command de manière temporaire (conseillé). apt-get install packet, aptitude install packet, ou apt install packet installera le paquet et ses dépendances (dont la bonne version de Python si elle n'est pas déjà installée).

Installation depuis les sources

En 2017, le code source est sur GitHub. Les sources peut être récupérées avec git clone https://github.com/secdev/scapy. Avec les droits root/SuperUtilisateur, l'installation se fait avec la commande python setup.py install (il vous faut donc Python préalablement).

Récupération de paquets Wi-Fi

Avant de chercher des SSID et des clients qui essayent de se connecter, il faut récupérer des paquets à analyser. Par défaut, il est courant que les interfaces sans-fil n'écoutent que les communications qui les concernent (sous GNU/Linux, c'est le mode managed pour iwconfig). Pour tout écouter (si votre carte Wi-Fi ainsi que son pilote le permettent), il faut passer en mode monitor. Sous GNU/Linux, il faut exécuter avec les droits SuperUtilisateur iwconfig wlan0 mode monitor (en remplaçant wlan0 par votre interface Wi-Fi, dont la liste vous est donné par iwconfig). S'il y a une erreur tel que Error for wireless request "Set Mode" (8B06) : SET failed on device wlan0 ; Device or resource busy., c'est que votre interface est allumée, alors qu'il faut qu'elle soit éteinte pour changer de mode, ifconfig wlan0 down l'éteint et ifconfig wlan0 up l'allume (encore une fois remplacé wlan0 par votre interface Wi-Fi).

Une fois passé en mode monitor, il faut écouter votre interface Wi-Fi. Vous pouvez enregistrer les paquets que vous captez (au format pcap) pour les analyser ultérieurement (c'est ce que je vous conseille dans un premier temps) ou les analyser à la volé puis les jeter avec un script (ce qui est plus efficace mais nécessite d'avoir un script qui marche). Vous pouvez par exemple récupérer des paquets avec WireShark (un logiciel libre connu avec une interface graphique). Il est également possible de le faire avec Scapy, tout d'abord via la fonction sniff (qui retourne des paquets) qui a un arguement count pour limiter le nombre de paquets et timeout pour s'arrêter après un temps donné, puis avec la fonction wrpcap. Par exemple, avec les droits root/SuperUtilisateur, wrpcap("packets.pcap", sniff(count=1000)) (via Scapy, que ce soit en ligne de commande dans un shell scapy ou dans un script) récupère 1000 paquets et les enregistre dans un fichier "packets.pcap".

Filtrage des Probe Request

Nous sommes dans le cas du Wi-Fi, donc nous nous appuyons sur l'ensemble de normes IEEE 802.11. Dans notre cas, nous cherchons des paquets contenant une Probe Request. Dans celles-ci nous cherchons le SSID, qui se trouve dans un paquet contenu dans la Probe Request, de type "IEEE 802.11 wireless LAN management frame" en tant que "Tagged parameter" pour WireShark et de type "802.11 Information Element" pour Scapy. Un filtrage avec Scapy pour un paquet peut donc se faire avec Dot11 in packet and Dot11ProbeResp in packet[Dot11] and Dot11Elt in packet[Dot11][Dot11ProbeResp]. Mais on peut faire plus court : Dot11ProbeResp in packet and Dot11Elt in packet. Il est à noter que l'on aurait aussi pu récupérer les beacons : (Dot11ProbeResp in packet or Dot11Beacon in packet) and Dot11Elt in packet. Pour information, une Probe Request sert au scan actif, tandis qu'un beacon sert au scan passif.

Le filtrage se fait via une fonction qui renvoie vrai ou faux. On peut utiliser une fonction standard (définie avec def) ou une lambda fonction. À partir d'une liste de paquets, il y a la méthode filter, par exemple packets = packets.filter(lambda p: Dot11ProbeResp in p and Dot11Elt in p). Plutôt que de charger en mémoire tous les paquets, puis d'appliquer un filtrage, on peut filter au fur et à mesure qu'on lit les paquets. La fonction sniff permet de lire un fichier pcap (avec son argument offline) et d'appliquer un filtre (avec son argument lfilter). On peut par exemple l'utiliser ainsi : packets = sniff(offline="packets.pcap", lfilter=lambda p: Dot11ProbeResp in p and Dot11Elt in p).

Filtrer tous les paquets, puis faire un traitement est simple et permet de bien décomposer les étapes. Mais ça suppose de pouvoir enregistrer tous les paquets, et c'est un gachi de mémoire si on sait à priori ce que l'on veut en faire puisque l'on n'exploite généralement pas 100% des données d'un paquet. L'argument prn de sniff permet d'appliquer une fonction à chaque paquet (qui ne doit pas nécessairement retourner de valeur) et l'argument store peut être mis à False pour ne pas conserver les paquets en RAM.

Extraire le SSID d'une Probe Request

Dans un "802.11 Information Element", il y a un identifiant de type ("ID" pour Scapy et "Tag number" pour WireShark) (sous forme de naturel, c'est-à-dire d'entier positif) et une valeur ("info" pour Scapy). L'identifiant de type pour un SSID est la valeur 0. L'"info" contient le nom du SSID.

Exemple de scripts

Script de base

#!/usr/bin/env python2

from scapy.all import *
from sys       import argv
from os.path   import isfile


if len(argv) < 2 or argv[1] == "":
    print("Please provide a file as an argument")
    exit(1)

if not isfile(argv[1]):
    print(argv[1] +" is not a file")
    exit(1)


ssid_searched = set()

def find_ssid_searched(packet):
    if Dot11ProbeResp in packet and Dot11Elt in packet[Dot11ProbeResp]:
        packet = packet[Dot11ProbeResp]
        packet = packet[Dot11Elt]
        if packet.ID == 0: # SSID
            ssid_searched.add(packet.info)

sniff(offline = argv[1],
      prn = find_ssid_searched,
      store = False)

for a_ssid in ssid_searched:
    print(a_ssid)

Script avec adresse MAC

#!/usr/bin/env python2


from scapy.all import *
from sys       import argv
from os.path   import isfile


ssid_searched = dict()

def find_ssid_searched(packet):
    if(Dot11          in packet and
       Dot11ProbeResp in packet[Dot11] and
       Dot11Elt       in packet[Dot11][Dot11ProbeResp]):
        packet = packet[Dot11]
        addr_src = packet.addr1
        
        packet = packet[Dot11ProbeResp]
        packet = packet[Dot11Elt]
        if packet.ID == 0: # SSID
            if not packet.info in ssid_searched.keys():
                ssid_searched[packet.info] = set()
            ssid_searched[packet.info].add(addr_src)

def print_ssid_searched():
    for a_ssid in ssid_searched:
        print(a_ssid)
        ssid_searched[a_ssid] = sorted(ssid_searched[a_ssid])
        for addr in ssid_searched[a_ssid]:
            print("- "+ str(addr))


if len(argv) > 1 and argv[1] != "" and isfile(argv[1]):
    sniff(offline = argv[1],
          prn = find_ssid_searched,
          store = False)
    print_ssid_searched()
else:
    sniff(prn = find_ssid_searched,
          store = False)
    nb = len(ssid_searched)
    if nb == 0:
        print("\rNo SSID found!")
        print("Do you have enabled monitor mode?")
    else:
        print("\r"+ str(nb) +" found")
        print_ssid_searched()

Script avec adresses MAC du point d'accès et des clients

#!/usr/bin/env python2


from scapy.all import *
from sys       import argv
from os.path   import isfile


ssid_searched = dict()

def find_ssid_searched(packet):
    if(Dot11          in packet and
       Dot11ProbeResp in packet[Dot11] and
       Dot11Elt       in packet[Dot11][Dot11ProbeResp]):
        packet = packet[Dot11]
        addr_src = packet.addr1
        addr_dst = packet.addr2
        
        packet = packet[Dot11ProbeResp]
        packet = packet[Dot11Elt]
        if packet.ID == 0: # SSID
            if not packet.info in ssid_searched.keys():
                ssid_searched[packet.info] = dict()
            if not addr_dst in ssid_searched[packet.info].keys():
                sid_searched[packet.info][addr_dst] = set()
            ssid_searched[packet.info][addr_dst].add(addr_src)

def print_iterable_as_list(an_iterable, prefix=""):
    prefix = str(prefix) + "- "
    for value in an_iterable:
        print(prefix + str(value))

def print_ssid_searched():
    for a_ssid in ssid_searched:
        print("- "+ str(a_ssid) +
              " ("+ str(len(ssid_searched[a_ssid])) +" MAC)")
        for addr_ssid in ssid_searched[a_ssid]:
            print("  - "+ str(addr_ssid) +
                  " ("+ str(len(ssid_searched[a_ssid][addr_ssid])) +")")
            ssid_searched[a_ssid][addr_ssid] = sorted(ssid_searched[a_ssid][addr_ssid])
            print_iterable_as_list(ssid_searched[a_ssid][addr_ssid], "    ")

def get_client_hardware_addresses():
    addresses = set()
    for a_ssid in ssid_searched:
        for addr_ssid in ssid_searched[a_ssid]:
            for address in ssid_searched[a_ssid][addr_ssid]:
                addresses.add(address)
    return addresses

def print_client_hardware_addresses():
    addresses = get_client_hardware_addresses()
    print(str(len(addresses))+ " client address"+
          ("" if len(addresses) == 0 else "es")
          +" found")
    
    addresses = sorted(addresses)
    for address in addresses:
        print("- "+ str(address))
        for a_ssid in ssid_searched:
            for addr_ssid in ssid_searched[a_ssid]:
                if address in ssid_searched[a_ssid][addr_ssid]:
                    print("  - "+ str(a_ssid) +
                          " "+ str(ssid_searched[a_ssid].keys()))

def sum_up():
    if len(ssid_searched) > 0:
        print_ssid_searched()
        print("")
        print_client_hardware_addresses()

if len(argv) > 1 and argv[1] != "" and isfile(argv[1]):
    sniff(offline = argv[1],
          prn = find_ssid_searched,
          store = False)
    sum_up()
else:
    sniff(prn = find_ssid_searched,
          store = False)
    nb = len(ssid_searched)
    if nb == 0:
        print("\rNo SSID found!")
        print("Do you have enabled monitor mode?")
    else:
        print("\r"+ str(nb) +" SSID found")
        sum_up()

Script avec adresses MAC et génération de CSV

#!/usr/bin/env python2


from scapy.all import *
from sys       import argv
from os.path   import isfile


ssid_searched = dict()


def filter_probe(packet):
    return (
        Dot11          in packet and
        Dot11ProbeResp in packet[Dot11] and
        Dot11Elt       in packet[Dot11][Dot11ProbeResp]
        )

def process(packet):
    packet = packet[Dot11]
    addr_src = packet.addr1
    addr_dst = packet.addr2
    
    packet = packet[Dot11ProbeResp]
    packet = packet[Dot11Elt]
    if packet.ID == 0: # SSID
        if not packet.info in ssid_searched.keys():
            ssid_searched[packet.info] = dict()
        if not addr_dst in ssid_searched[packet.info].keys():
            ssid_searched[packet.info][addr_dst] = set()
        ssid_searched[packet.info][addr_dst].add(addr_src)

def find_ssid_searched(packet):
    if filter_probe(packet):
        process(packet)


def print_iterable_as_list(an_iterable, prefix=""):
    prefix = str(prefix) + "- "
    for value in an_iterable:
        print(prefix + str(value))

def print_ssid_searched():
    for a_ssid in ssid_searched:
        print("- "+ str(a_ssid) +
              " ("+ str(len(ssid_searched[a_ssid])) +" MAC)")
        for addr_ssid in ssid_searched[a_ssid]:
            print("  - "+ str(addr_ssid) +
                  " ("+ str(len(ssid_searched[a_ssid][addr_ssid])) +")")
            ssid_searched[a_ssid][addr_ssid] = sorted(ssid_searched[a_ssid][addr_ssid])
            print_iterable_as_list(ssid_searched[a_ssid][addr_ssid], "    ")

def get_client_hardware_addresses():
    addresses = set()
    for a_ssid in ssid_searched:
        for addr_ssid in ssid_searched[a_ssid]:
            for address in ssid_searched[a_ssid][addr_ssid]:
                addresses.add(address)
    return addresses

def print_client_hardware_addresses():
    addresses = get_client_hardware_addresses()
    print(str(len(addresses))+ " client address"+
          ("" if len(addresses) == 0 else "es")
          +" found")
    
    addresses = sorted(addresses)
    for address in addresses:
        print("- "+ str(address))
        for a_ssid in ssid_searched:
            for addr_ssid in ssid_searched[a_ssid]:
                if address in ssid_searched[a_ssid][addr_ssid]:
                    print("  - "+ str(a_ssid) +
                          " "+ str(ssid_searched[a_ssid].keys()))

def sum_up_markdown():
    if len(ssid_searched) > 0:
        print_ssid_searched()
        print("")
        print_client_hardware_addresses()

def sum_up_csv(header=True):
    if header:
        print("SSID;SSID_MAC;client_MAC")
    
    for a_ssid in ssid_searched:
        for addr_ssid in ssid_searched[a_ssid]:
            for addr_client in ssid_searched[a_ssid][addr_ssid]:
                print(a_ssid +";"+ addr_ssid +";"+ addr_client)

def sum_up(a_format="text"):
    a_format = a_format.strip().lower()
    if a_format == "tex" or a_format == "text" or a_format == "md" or a_format == "markdown":
        sum_up_markdown()
    elif a_format == "csv":
        sum_up_csv()
    else:
        print(a_format +" is not a managed format!")


pcap_file_path = None
output_format = "text"

for arg in argv[1:]:
    if arg != "" and isfile(arg):
        pcap_file_path = arg
    else:
        arg_lowered = arg.lower()
        if arg_lowered == "--text" or arg_lowered == "--md" or arg_lowered == "--markdown":
            output_format = "text"
        elif arg_lowered == "--csv":
            output_format = "csv"
        else:
            print("Argument not managed")


if pcap_file_path is not None:
    sniff(offline = pcap_file_path,
          prn = find_ssid_searched,
          store = False)
    sum_up(output_format)
else:
    sniff(prn = find_ssid_searched,
          store = False)
    nb = len(ssid_searched)
    if nb == 0:
        print("\rNo SSID found!")
        print("Do you have enabled monitor mode?")
    else:
        print("\r"+ str(nb) +" SSID found")
        sum_up(output_format)

Sur le même sujet