Linux Embedded

Le blog des technologies libres et embarquées

Introduction à Haka : Utilisation de Haka (1)

Depuis un an déjà Open Wide, Arkoon et Télécom ParisTech travaillent sur un outil d'analyse de paquets réseau appelé Haka. Ce framework permet d'analyser facilement le trafic issu d'une capture réseau ou lu à partir d'une interface et de les modifier grâce à des règles écrites en Lua.

Haka vient d'atteindre la version 0.2.2 et mérite que l'on y jette un coup d’œil. Ce premier article détaillera comment utiliser Haka pour analyser un fichier de capture réseau ou une interface, le prochain article s'intéressera à la modification d'un flux et un troisième article regardera comment écrire un dissecteur protocolaire.

Installation de Haka

L'installation de Haka n'est pas très compliqué. La liste des dépendances est un peu longue mais elle est proprement détaillée sur le site web. Une fois les dépendances installées et les sources téléchargées, la procédure de compilation est classique pour un projet basé sur cmake. Notez que le site web dispose également de paquetages debian précompilés.

Notez l'utilisation d'une cible "make localinstall" qui permet d'installer haka dans le répertoire de build et fournit un script d'environnement pour modifier les chemins en conséquence. Une fois cette commande terminée, un sous-répertoire out/ est créé dans votre répertoire de build. Le script env.sh doit être sourcé. Les outils haka seront ensuite dans votre environnement et prêt à être utilisés.

Une fois haka installé, nous pouvons passer au vif du sujet.

Analyse de fichier PCAP.

Les fichiers .pcap sont des fichiers contentant des captures de traffic réseau. Le format pcap est un format courant dans le monde du réseau et la plupart des outils savent lire et écrire de tels fichiers. L'outil hakapcap permet de lire un fichier pcap et d'appliquer des règles lua dessus.

Pour cette article nous utiliserons un fichier pcap fourni par le projet haka (ici) dont nous ne vous dirons rien. Le but de cet article sera d'en analyser le contenu.

Notre premier script se contentera d'imprimer un message à chaque paquet dans le fichier pcap. Editez un fichier linuxembedded.lua et mettez le contenu suivant

local ipv4 = require("protocol/ipv4")
haka.log("linuxembedded.fr","Hello World")
haka.rule{
   hook = ipv4.events.receive_packet,
   eval = function()
     haka.log("linuxembedded.fr","Packet received")
   end
}

Pour analyser notre fichier pcap, utilisez la commande suivante

$ hakapcap linuxembedded.lua darpa-stat.pcap

Le programme affichera un grand nombre de traces, une pour chaque paquet contenu. Cela ne nous aide pas beaucoup, mais haka est en place. Regardons le script.

La première ligne est une commande standard lua de chargement de module. Le module protocol/ipv4 contient des fonctions pour accéder aux paquets IP. Le chargement du module renvoie une structure que nous sauvegardons dans la variable ipv4.

La seconde ligne permet d'ajouter une trace. Nous les mettons dans une catégorie linuxembedded.fr pour aider à la lisibilité.

Le bloc de code suivant décrit une règle Haka. Il s'agit d'un tableau clé/valeur ayant deux clés.

  • Une clé hook pointe sur une variable de type event contenu dans le module ipv4. Il s'agit de l'événement qui doit déclencher la règle. Ici, la réception d'un paquet.
  • Une clé eval qui pointe sur une fonction à appeler à chaque fois que l'événement a lieu. Ici nous ne faisons qu'émettre une trace.

Notre règle est très simple, lisible et facile à mettre en place. La grande force de Haka réside dans la facilité d'écriture des règles. Un événement, quelques lignes de programmation fonctionnel et le tour est joué.

Exploration de l'API Haka.

La règle interactive

Notre première règle ne nous a pas appris grand chose sur notre fichier pcap. Il contient des paquets IP. Il serait intéressant d'explorer le contenu de ces paquets... de façon interactive. Le fichier lua ci-dessous arrêtera Haka à chaque paquet pour nous permettre de l'analyser

local ipv4 = require("protocol/ipv4")
haka.log("linuxembedded.fr","Stoping at each IPV4 packet")
haka.rule{
 hook = ipv4.events.receive_packet,
 eval = haka.interactive_rule("ipv4")
}

La fonction haka.interactive_rule() renvoie une fonction qui arrète l'exécution et affiche un prompt (ipv4). Relançons notre script. La console devrait afficher le contenu suivant

interactive rule:
inputs = table {
 1 : userdata ipv4 {
 checksum : 11893
 dst : userdata addr 209.67.29.11
 flags : userdata ipv4_flags
 frag_offset : 0
 hdr_len : 20
 id : 15919
 len : 44
 name : "ipv4"
 payload : userdata vbuffer
 proto : 6
 raw : userdata packet
 src : userdata addr 172.16.116.201
 tos : 0
 ttl : 63
 version : 4
 }
}

Hit ^D to end the interactive session
ipv4>

La console interactive Haka est un outil particulièrement pratique pour explorer l'API. Si vous entrez simplement le nom d'une variable, la console affichera son contenu

ipv4> inputs[1]
 #1 userdata ipv4 {
 checksum : 11893
 dst : userdata addr 209.67.29.11
 flags : userdata ipv4_flags
 frag_offset : 0
 hdr_len : 20
 id : 15919
 len : 44
 name : "ipv4"
 payload : userdata vbuffer
 proto : 6
 raw : userdata packet
 src : userdata addr 172.16.116.201
 tos : 0
 ttl : 63
 version : 4
 }

La ligne de commande peut également compléter grace à la touche tab

ipv4> inputs[1].<tab>
inputs[1].can_continue inputs[1].hdr_len inputs[1].receive 
inputs[1].checksum inputs[1].id inputs[1].send 
inputs[1].compute_checksum inputs[1].inject inputs[1].src 
inputs[1].continue inputs[1].len inputs[1].tos 
inputs[1].drop inputs[1].name inputs[1].ttl 
inputs[1].dst inputs[1].payload inputs[1].verify_checksum 
inputs[1].error inputs[1].proto inputs[1].version 
inputs[1].flags inputs[1].raw 
inputs[1].frag_offset inputs[1].reassemble

La variable inputs est un tableau contenant le paramètres passés à la fonction de callback. Ici nous n'avons qu'un seul paramètre (inputs[1]) qui contient le paquet IPV4. L'ensemble des champs du paquets sont facilement accessibles comme sous-champs de l'objet. Notons en particulier le champ payload qui permet d'accéder au contenu du paquet sous forme de tableau et le champs raw qui permet d'accéder à l'ensemble du paquet (entête IPV4 compris) également sous forme de tableau.

Le dissecteur TCP

Tout ceci est donc très simple mais la couche IPV4 est quand même très basse et ne nous fournit pas grand chose. Nous pouvons voire la source et la destination des paquets, mais pas le port... En effet le port ne fait pas partie du protocol IP (qui communique de machine à machine) mais des protocoles TCP et UDP (qui sont port à port). Modifions notre script pour analyser les paquets TCP.

local tcp = require("protocol/tcp")
haka.log("linuxembedded.fr","Stoping at each TCP packet")
haka.rule{
 hook = tcp.events.receive_packet,
 eval = haka.interactive_rule("tcp")
}

Les changements sont mineurs. Nous utilisons simplement les règles d'un autre protocole. La sortie de la console ressemble à l'exemple ci-dessous

interactive rule:
inputs = table {
 1 : userdata tcp {
 ack_seq : 0
 checksum : 59003
 dstport : 80
 flags : userdata tcp_flags
 hdr_len : 24
 ip : userdata ipv4
 name : "tcp"
 payload : userdata vbuffer
 res : 0
 seq : 183275097
 srcport : 1773
 urgent_pointer : 0
 window_size : 512
 }
}

Hit ^D to end the interactive session
tcp>

Cette fois notre paquet est un paquet TCP. Il contient les champs du protocole TCP (ports source et destination) mais pas les adresses (qui sont restés dans le paquet IP). Le paquet ip est accessible grâce au champs ip. La logique est la même que précédemment: Le paquet ne contient que les entête de son niveau et pointe sur une structure parente pour le reste.

Notons que le dstport est le port 80. Notre paquet est un paquet HTTP. Il s'agit du premier paquet de l'établissement d'une connexion TCP, mais notre pcap contient un grand nombre de paquets, nous n'allons pas les regarder un à un. Haka est là pour automatiser ce genre de tâches...

haka.log("linuxembedded.fr","Checking known TCP and UDP ports")
local service_file = io.open("/etc/services")
local tcp = require("protocol/tcp")

services = {}

for line in service_file:lines() do
 name,port,protocol = string.match(line,"(%S+)%s+(%d+)/(%w+)")
 if name then
   if not services[protocol] then
    services[protocol] = {}
   end
   services[protocol][tonumber(port)] = name
 end
end
service_file:close()

function find_service(port,protocol)
 if not services[protocol] then return nil end
 return services[protocol][port]
end

haka.rule{
 hook = tcp.events.receive_packet,
 eval = function(packet)
   local dstsrv = find_service(packet.dstport,"tcp")
   if dstsrv then 
     haka.log("linuxembedded.fr","Packet to known TCP port : "..dstsrv)
   else
     haka.log("linuxembedded.fr","Packet to unknown TCP port : "..packet.dstport)
   end
 end
}

Le script ci-dessus utilise le fichier /etc/services (un fichier standard sous linux) pour créer un tableau permettant d'associer un numéro de port TCP à un nom de service connu. La première partie est du pure lua et ne sera pas détaillée ici.

Une fois le tableau construit, une simple trace nous permet de facilement faire l'inventaire du contenu du pcap... Il y a beaucoup d'HTTP et beaucoup de ports inconnu.

Le dissecteur tcp_connection

Haka peut rassembler les paquets venant d'une même connexion pour nous. Le dissecteur tcp_connection permets de suivre les paquets regroupés par connexion. Tout d'abord une version simple permet de voir les paquets (dont les paquets protocolaires)

local tcp_connection = require("protocol/tcp_connection")
haka.log("linuxembedded.fr","Stoping at each TCP connection")
haka.rule{
 hook = tcp_connection.events.receive_packet,
 eval = haka.interactive_rule("tcp connection")
}

Cette règle très simple nous permets de nous arrêter sur chaque paquet, mais la console nous affiche maintenant la sortie suivante :

interactive rule:
inputs = table {
 1 : class tcp_connection {
 connection : userdata cnx
 dstip : userdata addr 209.67.29.11
 dstport : 80
 input : "up"
 output : "down"
 srcip : userdata addr 172.16.116.201
 srcport : 1773
 state : class StateMachineInstance
 stream : table
 }
 2 : userdata tcp {
 ack_seq : 0
 checksum : 59003
 dstport : 80
 flags : userdata tcp_flags
 hdr_len : 24
 ip : userdata ipv4
 name : "tcp"
 payload : userdata vbuffer
 res : 0
 seq : 183275097
 srcport : 1773
 urgent_pointer : 0
 window_size : 512
 }
 3 : "up"
}

Hit ^D to end the interactive session
tcp connection>

Le premier champs de input est un objet connexion. Ce même objet nous sera passé pour chaque paquet lié à la connexion. Il permet de stocker des informations relatives à la connexion. Le deuxième champs est un paquet TCP tels que nous l'avons vu dans l'exemple précédent. Le troisième champs nous indique si le paquet était montant (dans le sens de l'ouverture de la connexion) ou descendant.

Nous avons donc partiellement résolu le problème du suivi TCP. Les paquets sont proprement rattachés à leur connexion et nous pouvons suivre les échanges (SYN, SYN-ACK, ACK) d'établissement de connexion. Si nous voulons suivre le déroulement de la connexion cette approche serait parfaite... Mais nous voulons analyser le contenu et pour cela il faut rassembler les paquets en stream.

Haka permet de travailler sur la connexion vue sous forme de stream. Un seul événement sera créé pour la connexion et cet événement recevra un itérateur qui permet de parcourir les données comme un tableau.

local tcp_connection = require("protocol/tcp_connection")
haka.log("linuxembedded.fr","TCP connection as stream")
haka.rule{
 hook = tcp_connection.events.receive_data,
 options = {streamed = true },
 eval = function (flow, iterator, direction)
   local result = ""
   for data in iterator:foreach_available() do
     local result = result..data:asstring()
   end
   haka.log("linuxembedded.fr "..direction,result)
 end
}

Nous avons ici une simple boucle qui nous permet de suivre nos flux et l'affichage corresponds bien aux échanges TCP complets. C'est beaucoup plus simple et lisible que les paquets TCP brut. On ne voit plus les paquets protocolaires et les paquets sont rassemblés de façon transparente.

Plus intéressant encore, ce code fonctionnerait également si nous étions en mode serveur (plutôt que de lire nos paquets depuis un fichier) l’événement s'exécuterait au fur et à mesure de l'arrivée des paquets, l'impression a lieu lorsque foreach_available renvoie false c'est à dire à la fermeture de la connexion TCP, mais la boucle elle même traite les paquets au fur et à mesure de leur arrivée. La fonction foreach_available bloque tant que la connexion est ouverte en attendant de nouveaux paquets.

Le dissecteur HTTP

Le dissecteur TCP peut nous permettre de facilement analyser un flux et un peu de travail à coup d'expressions régulières nous permet de facilement trouver des champs intéressants... mais haka sait déjà parser les entêtes HTTP.

haka.log("linuxembedded.fr","Exploring HTTP")
local http = require('protocol/http')
http.install_tcp_rule(80)


haka.rule{
 hook = http.events.request,
 eval = haka.interactive_rule("http request")
}
haka.rule{
 hook = http.events.response,
 eval = haka.interactive_rule("http response")
}
haka.rule{
 hook = http.events.response_data,
 eval = haka.interactive_rule("http response data")
}

Les trois règles ci-dessus nous permettent d'explorer les trois événements les plus importants du protocole HTTP : requête, réponse, données attachées à une réponse (il y a également un événement request_data mais celui-ci est plus rare). Ce script permets d'explorer facilement les possibilités du dissecteur HTTP. Notons que, contrairement aux autres dissecteurs, il est nécessaire d'attacher explicitement le dissecteur HTTP au port 80. Le protocole contenu dans un paquet TCP n'est pas défini par le protocole TCP lui-même (le numéro de port n'est qu'une convention) et il est courant de faire passer des requêtes HTTP sur d'autre ports. Cette différence d'API nous donne donc plus de souplesse pour analyser le contenu des connexions HTTP

Une rapide exploration du contenu du fichier pcap nous indique qu'un grand nombre d'images GIF sont transmises. Celles-ci commencent par la chaîne de caractère GIF. Sauvegardons les au passage...

haka.log("linuxembedded.fr","Exporting GIF")
local http = require('protocol/http')
http.install_tcp_rule(80)

index = 0

haka.rule{
 hook = http.events.response_data,
 options = {streamed = true },
 eval = function (flow, iterator, direction)
   if iterator:available() < 3 then return end
   if iterator:sub(3):asstring() == "GIF" then
     local gif_file = io.open("gif_"..tostring(index)..".gif","w+")
     index = index+1
     gif_file:write("GIF")
     for data in iterator:foreach_available() do
       gif_file:write(data:asstring())
     end
     gif_file:close()
   end
 end
}

Et voila. Votre répertoire est maintenant plein d'images Gif correspondant à la page téléchargée. Amusant.

Utilisation d'expressions régulières

Le modèle stream de Haka est assez facile à utiliser pour filtrer les données dans une boucle for standard sans bloquer le trafic réseau mais haka fournit également sa propre bibliothèque d'expression régulières (basée sur pcre) pour traiter ces flots de données. Repérer des balises intéressantes devient trivial. L'exemple ci-dessous permet d'extraire facilement les commentaires du flux HTML

haka.log("linuxembedded.fr","Détection des commentaires HTML")
local http = require('protocol/http')
http.install_tcp_rule(80)
local rem = require("regexp/pcre")

local re = rem.re:compile("<!--.*-->")

haka.rule {
 hook = http.events.response_data,
 options = { streamed = true, },
   eval = function (flow, iter)
   repeat
     local ret = re:match(iter,true)
     if ret then
       haka.log("linuxembedded.fr",ret:asstring())
     end
   until not iter:wait()
 end
}

Les expressions régulières HAKA gèrent intelligemment la notion de stream. la fonction re:match cherche une expression régulière dans le stream, quitte à attendre l'arrivée de nouvelles données et renvoie la première ooccurrence trouvée. La boucle repeat...until permet de reprendre l'analyse du flux jusqu'à la fin de celui-ci.

Conclusion

Ce premier article sur HAKA nous a permis de découvrir l'intérêt de ce petit outil : Il est très facile de se mettre sur n'importe quel couche réseau et d'analyser contextuellement ce  qui s'y passe. Le code est trivial a écrire et fait ce qu'on attends de lui

Haka est un grep appliqué à la carte réseau. Il permet de repérer facilement ce que l'on cherche sur un flux réseau sans se soucier de gérer l'assemblage des paquets en flux ou le suivi des connexions.

Dans les prochains articles de cette série nous explorerons les possibilités de modifications des paquets et flux réseau et, dans un troisième article, comment implémenter l'analyse de nouveaux protocoles réseaux sous forme de grammaire Haka.

 

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.