Calcul de statistiques sur les canaux Wi-Fi avec du XML généré par Kismet

Une carte Wi-Fi peut utiliser différentes fréquences. Il existe des canaux Wi-Fi qui sont des fréquences autorisées (selon la juridiction) et des numéros simples associés.

On peut chercher à savoir quel est le canal le plus utilisé. Cette information peut être utilisée pour intercepter le maximum de paquets Wi-Fi en sniffant le canal le plus utilisé. Mais on peut aussi s'en servir pour se prémunir de ce genre d'attaques "intelligentes" en faisant utiliser le canal le moins susceptible d'être intercepté à un point d'accès Wi-Fi que l'on contrôle.

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. ArchLinux propose aussi un paquet se nommant 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++.

Langage Python et manipulation du XML

Python a plusieurs bibliothèques pour manipuler du XML. Il y a par exemple libxml2 qui se base sur une bibliothèque écrite en langage C, ce qui lui permet d'être rapide.

Identifier le canal Wi-Fi utilisé avec le XML généré par Kismet

Chaque balise wireless-client ou wireless-network peut contenir une balise channel. La valeur de la balise channel est le numéro du canal Wi-Fi.

La recherche des balises peut se faire en parcourant via SAX ou en créant puis parcourant un arbre DOM. Pour se faciliter la tâche, XPath peut être utilisé.

Pour obtenir le canal de chaque paquet, l'expression XPath 1.0 suivante peut être utilisée : //channel. Pour obtenir le canal de chaque paquet des routeurs Wi-Fi, l'expression XPath 1.0 suivante peut être utilisée : //wireless-network/channel. Pour obtenir le canal de chaque paquet des clients Wi-Fi, l'expression XPath 1.0 suivante peut être utilisée : //wireless-client/channel.

Exemple de script

Script de base

#!/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


channels = dict()


def channels_add(a_channel):
    a_channel_uint = int(a_channel)
    
    if a_channel_uint in channels:
        channels[a_channel_uint] += 1
    else:
        channels[a_channel_uint]  = 1

def fill_channels_with_xml_channel_node(node):
    channels_add(node.content)

def fill_channels_with_xml_channel_nodes(nodes):
    nb = 0
    for node in nodes:
        fill_channels_with_xml_channel_node(node)
        nb += 1
    return nb

def find_xml_channel_nodes_with_xpath_context(a_xpath_context):
    nodes = a_xpath_context.xpathEval("//channel")
    return nodes

def fill_channels_with_xpath_context(a_xpath_context):
    nodes = find_xml_channel_nodes_with_xpath_context(a_xpath_context)
    return fill_channels_with_xml_channel_nodes(nodes)

def fill_channels_with_xml_document(a_document):
    a_xpath_context = a_document.xpathNewContext()
    nb = fill_channels_with_xpath_context(a_xpath_context)
    a_xpath_context.xpathFreeContext()
    return nb

def fill_channels_with_xml_document_path(a_path):
    a_document = libxml2.parseFile(a_path)
    nb = fill_channels_with_xml_document(a_document)
    a_document.freeDoc()
    return nb


def get_channels_sum():
    return sum(channels.values())

def print_channels(a_writer = stdout):
    the_sum = get_channels_sum()
    the_sum_less_0 = the_sum
    if 0 in channels:
        the_sum_less_0 -= channels[0]
    
    for channel in channels:
        value = channels[channel]
        percentage        = float(value) / float(the_sum)
        percentage_less_0 = float(value) / float(the_sum_less_0)
        a_writer.write("%2u -> %6u (%0.3f)" %
                       (channel, value, percentage))
        if channel == 0:
            a_writer.write(" (0 == unknow)")
        else:
            a_writer.write(" (%0.3f)" % percentage_less_0)
        a_writer.write("\n")


def main():
    if len(argv) < 2:
        exit(1)
    
    for arg in argv[1:]:
        fill_channels_with_xml_document_path(arg)
    
    print_channels()

if __name__ == '__main__':
    main()

Script paramètrable avec gestion des erreurs

#!/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


channels = dict()


def channels_add(a_channel):
    if a_channel is None:
        return False
    
    a_channel_uint = -1
    try:
        a_channel_uint = int(a_channel)
    except ValueError:
        return False
    if a_channel_uint < 0:
        return False
    
    if a_channel_uint in channels:
        channels[a_channel_uint] += 1
    else:
        channels[a_channel_uint]  = 1
    return True

def fill_channels_with_xml_channel_node(node):
    if node is None or len(node.content) == 0:
        return False
    return channels_add(node.content)

def fill_channels_with_xml_channel_nodes(nodes):
    nb = 0
    for node in nodes:
        if fill_channels_with_xml_channel_node(node):
            nb += 1
    return nb

def find_xml_channel_nodes_with_xpath_context(a_xpath_context,
                                              network = True, client = True):
    xpath_str = None
    if network and client:
        xpath_str = "//channel"
    elif not network and client:
        xpath_str = "//wireless-client/channel"
    elif network and not client:
        xpath_str = "//wireless-network/channel"
    else:
        return 0
    
    nodes = a_xpath_context.xpathEval(xpath_str)
    return nodes

def fill_channels_with_xpath_context(a_xpath_context,
                                     network = True, client = True):
    nodes = find_xml_channel_nodes_with_xpath_context(a_xpath_context,
                                                      network, client)
    return fill_channels_with_xml_channel_nodes(nodes)

def fill_channels_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_channels_with_xpath_context(a_xpath_context, network, client)
    a_xpath_context.xpathFreeContext()
    return nb

def fill_channels_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_channels_with_xml_document(a_document, network, client)
    a_document.freeDoc()
    return nb

def fill_channels_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_channels_with_xml_document_path(a_path, network, client)


def get_channels_sum():
    return sum(channels.values())

def print_channels(a_writer = stdout):
    the_sum = get_channels_sum()
    the_sum_less_0 = the_sum
    if 0 in channels:
        the_sum_less_0 -= channels[0]
    
    for channel in channels:
        value = channels[channel]
        percentage        = float(value) / float(the_sum)
        percentage_less_0 = float(value) / float(the_sum_less_0)
        a_writer.write("%2u -> %6u (%0.3f)" %
                       (channel, value, percentage))
        if channel == 0:
            a_writer.write(" (0 == unknow)")
        else:
            a_writer.write(" (%0.3f)" % percentage_less_0)
        a_writer.write("\n")

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
    
    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
        else:
            fill_channels_with_xml_document_path_to_check(arg, network, client)
    
    print_channels()

if __name__ == '__main__':
    main()