Calculer les statistiques sur les fabricants de puces Wi-Fi avec du XML généré par Kismet

Présentation de Kismet

Kismet est un est un sniffeur Wi-Fi. Il permet d'enregistrer en temps réel des paquets Wi-Fi dans différents formats (dont le texte brute, pcap, et XML). C'est un logiciel libre et open-source.

Sous au moins Debian, Ubuntu et Trisquel GNU/Linux, il peut être installé avec APT via le paquet kismet, par exemple via la commande apt install kismet.

Puisque c'est un logiciel libre, vous pouvez bien entendu récupérer les sources. Vous pouvez le faire dans un terminal avec git clone https://www.kismetwireless.net/git/kismet.git. Vous pourrez ensuite compiler le code C++.

Identification du fabricant / constructeur

La méthode la plus simple pour identifier un fabricant / constructeur est d'utiliser l'adresse MAC des paquets Wi-Fi. En effet, l'adresse MAC est un identifiant supposé unique d'une interface matérielle. Cela peut poser un problème de vie privée si on n'a pas confiance dans son réseau local, ou plus simplement à chaque fois que l'on envoie des probe requests. Heureusement, il est possible de changer logiciellement d'adresse MAC. Cela peut être fait de manière automatique, par exemple avec macchanger.

Les 3 premiers octets (d'une adresse MAC) correspondent à l'OUI (Organizationally Unique Identifier). Cette OUI permet de connaitre le constructeur de la carte. Cette information peut être utilisée à mauvais escient, par exemple pour exploiter des failles d'un pilote répandu (que l'on connait). On peut aussi s'en servir pour savoir quel type de population fréquente un lieu (en fonction du prix du matériel, de l'aspect symbolique de la marque, etc.).

À partir de Android 6.0, l'adresse MAC est aléatoire. Il en est de même à partir de Apple iOS 8.

Exemple de script

#!/usr/bin/env python2


# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided this notice is
# preserved.  This file is offered as-is, without any warranty.
# Names of contributors must not be used to endorse or promote products
# derived from this file without specific prior written permission.


from sys       import argv, stdout, stderr, exit
from os.path   import isfile, getsize as file_size
import libxml2
from json import dumps as json_dumps

try:
    from macpy import Mac
except:
    try:
        from Mac import Mac
    except:
        stderr.write("git clone https://github.com/hustcc/mac.py && todo\n")
        stderr.write("or\n")
        stderr.write("pip install mac.py\n")


mac_addresses = dict()
manufacturers = dict()


def mac_addresses_add(a_mac_address):
    if a_mac_address is None:
        return False
    
    if a_mac_address in mac_addresses:
        mac_addresses[a_mac_address] += 1
    else:
        mac_addresses[a_mac_address]  = 1
    return True

def fill_mac_addresses_with_xml_channel_node(node):
    if node is None or len(node.content) == 0:
        return False
    mac_addresses_add(node.content)
    return True

def fill_mac_addresses_with_xml_channel_nodes(nodes):
    nb = 0
    for node in nodes:
        if fill_mac_addresses_with_xml_channel_node(node):
            nb += 1
    return nb

def find_xml_mac_address_nodes_with_xpath_context(a_xpath_context,
                                                  network = True,
                                                  client  = True):
    XPATH_STR_NETWORK = "//wireless-network/BSSID"
    XPATH_STR_CLIENT  = "//wireless-client/client-mac"
    
    xpath_str = None
    if network and client:
        xpath_str = XPATH_STR_CLIENT +" | "+ XPATH_STR_NETWORK
    elif not network and client:
        xpath_str = XPATH_STR_CLIENT
    elif network and not client:
        xpath_str = XPATH_STR_NETWORK
    else:
        return 0
    
    nodes = a_xpath_context.xpathEval(xpath_str)
    return nodes

def fill_mac_addresses_with_xpath_context(a_xpath_context,
                                          network = True, client = True):
    nodes = find_xml_mac_address_nodes_with_xpath_context(a_xpath_context,
                                                          network, client)
    return fill_mac_addresses_with_xml_channel_nodes(nodes)

def fill_mac_addresses_with_xml_document(a_document,
                                         network = True, client = True):
    a_xpath_context = a_document.xpathNewContext()
    if a_xpath_context is None:
        return -1
    nb = fill_mac_addresses_with_xpath_context(a_xpath_context, network, client)
    a_xpath_context.xpathFreeContext()
    return nb

def fill_mac_addresses_with_xml_document_path(a_path,
                                              network = True, client = True):
    a_document = libxml2.parseFile(a_path)
    if a_document is None:
        return -1
    nb = fill_mac_addresses_with_xml_document(a_document, network, client)
    a_document.freeDoc()
    return nb

def fill_mac_addresses_with_xml_document_path_to_check(a_path,
                                                       network = True,
                                                       client  = True):
    if len(a_path) == '':
        stderr.write("Give a not empty XML file path as an argument\n")
        exit(1)
    if not isfile(a_path):
        stderr.write(a_path +" is not a file\n")
        exit(1)
    if file_size(a_path) == 0:
        stderr.write(a_path +" is empty\n")
        exit(1)
    return fill_mac_addresses_with_xml_document_path(a_path, network, client)


def manufacturers_add(a_manufacturer):
    if a_manufacturer is None:
        return False
    
    if a_manufacturer in manufacturers:
        manufacturers[a_manufacturer] += 1
    else:
        manufacturers[a_manufacturer]  = 1
    return True

def fill_manufacturers_with_mac_addresses():
    nb = 0
    a_mac_parser = Mac()
    
    for a_mac_address in mac_addresses:
        a_manufacturer = a_mac_parser.search(a_mac_address)
        if a_manufacturer is None:
            continue
        a_manufacturer_company = None
        
        if "company" in a_manufacturer:
            a_manufacturer_company = a_manufacturer["company"]
        elif "com" in a_manufacturer:
            a_manufacturer_company = a_manufacturer["com"]
        
        if(a_manufacturer_company is not None and
           a_manufacturer_company != "" and
           manufacturers_add(a_manufacturer_company)):
            nb += 1
    
    return nb


def print_mac_addresses_as_json(a_writer = stdout):
    if len(manufacturers) > 0:
        a_writer.write("[\n")
        is_first = True
        
        for a_mac_address in mac_addresses:
            if is_first:
                is_first = False
            else:
                a_writer.write(",")
                a_writer.write("\n")
            
            a_writer.write("  {\n")
            a_writer.write("     %s: %s,\n" %
                           ('"mac"',
                            json_dumps(a_mac_address)))
            a_writer.write("     %s: %s\n"  %
                           ('"count"',
                            '"'+ str(mac_addresses[a_mac_address]) +'"'))
            a_writer.write("  }")
        
        a_writer.write("\n]\n")

def print_mac_addresses(output_format):
    if output_format is None:
        return False
    output_format = output_format.strip().lower()
    
    if output_format == "json":
        print_mac_addresses_as_json()
        return True

def print_manufacturers_as_json(a_writer = stdout):
    if len(manufacturers) > 0:
        a_writer.write("[\n")
        is_first = True
        
        for a_manufacturer in manufacturers:
            if is_first:
                is_first = False
            else:
                a_writer.write(",")
                a_writer.write("\n")
            
            a_writer.write("  {\n")
            a_writer.write("     %s: %s,\n" %
                           ('"name"',
                            json_dumps(a_manufacturer)))
            a_writer.write("     %s: %s\n"  %
                           ('"count"',
                            '"'+ str(manufacturers[a_manufacturer]) +'"'))
            a_writer.write("  }")
            
        a_writer.write("\n]\n")

def print_manufacturers(output_format):
    if output_format is None:
        return False
    output_format = output_format.strip().lower()
    
    if output_format == "json":
        print_manufacturers_as_json()
        return True
    
    stderr.write(output_format +" is not a managed format\n")
    return False

def print_help(a_writer = stdout):
    a_writer.write("Give a XML file path as an argument\n")

def main():
    if len(argv) < 2:
        print_help(stderr)
        exit(1)
    
    network = True
    client = True
    output_format = "json"
    what_to_print = "manufacturers"
    
    for arg in argv[1:]:
        if arg == "--help":
            print_help(stdout)
            exit(0)
        elif arg == "--client":
            client = True
        elif arg == "--no-client":
            client = False
        elif arg == "--network":
            network = True
        elif arg == "--no-network":
            network = False
        elif arg.startswith("--output-format="):
            potential_output_format =  arg[len("--output-format="):]
            if len(potential_output_format) == 0:
                stderr.write("Put a format just after (without space or tab)"+
                             " --output-format=\n")
                exit(1)
            potential_output_format = potential_output_format.strip().lower()
            if not potential_output_format in ("json"):
                stderr.write(potential_output_format +" is not a managed format\n")
                exit(1)
            output_format = potential_output_format
        elif arg == "--print-manufacturers":
            what_to_print = "manufacturers"
        elif arg in ("--print-mac", "--print-mac-addr", "--print-mac-addresses"):
            what_to_print = "mac"
        else:
            fill_mac_addresses_with_xml_document_path_to_check(arg,
                                                               network, client)
            fill_manufacturers_with_mac_addresses()
    
    try:
        if what_to_print == "mac":
            print_mac_addresses(output_format)
        elif what_to_print == "manufacturers":
            print_manufacturers(output_format)
        else:
            stderr.write(what_to_print +" is not something that can be printed.\n")
            exit(1)
    except IOError as e:
        if not "Broken pipe" in str(e):
            stderr.write(str(e) +"\n")

if __name__ == '__main__':
    main()