Tutoriel presentant les facons de programmer un sniffer (extrait de minithins.net) (analyseur de traffic reseaux) : - soit en RAW socket - soit en utilisant libpcap (ou winPcap sous windows)
Programmer un sniff de deux facons.
(extrait de minithins.net)
Somaire:
0) Introduction
1) Sniffer via libpcap
2) Sniffer avec les sockets en RAW
3) Exploiter les résultats
0) Introduction
Un sniffeur est un programme lancé permettant de capter les datagrammes qui
passent sur un réseau, et de les rendre lisible pour l'être humain. On peut
ainsi savoir ce qu'il se passe sur le réseau, et ainsi détecter les sources
de problèmes.
Je connais à ce jour, deux facons différentes de programmer un sniffeur. La
premiere est d'utiliser les sockets en RAW, la seconde d'utiliser libpcap
(Packet Capture library), et tous deux sous Linux ou *BSD. Meme si le captage
des datagrammes est simple, il va faloir ensuite interpreter ce qu'il se passe
sur le réseau, et pour cela nous allons avoir besoin de connaitre (avoir des
notions générales) le tcp/i, et comment il marche.
Le matériel pour établir ce tutoriel se compose de 2 machines, l'une sous
FreeBSD, l'autre sous Linux (redhat 6.2).
1) Sniffer via libpcap
(note: je découvre sans doute en même temps que vous libpcap :-)
Libpcap, comme je l'ai déjà dis, est une librairie pour la capture de paquets.
Cela signifie qu'a l'appel des fonctions qui la compose, le programme va lire
sans arret ce qui passe sur un des périphériques réseaux, et nous donner ce
qu'il lit.
Libpcap est téléchargeable à www.tcpdump.org (tcpdump est aussi un très bon
sniffeur, une fois que l'on sait s'en servir). Pour l'installer, lisez le
INSTALL ou le README avec lequel libpcap est fournit. (RTFM !#@!)
Une fois cela fait, une seule commande est à faire:
virginie:/usr/www/htdocs/mags$ man pcap
(et oui, vous aurez l'aide.)
En lisant la doc, donc, j'ai remarqué (on peut dire trouvé) les deux fonctions
nécessaire pour la réalisation de notre programme:
pcap_t *pcap_open_live(char *device, int snaplen,
int promisc, int to_ms, char *ebuf)
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
La première, pcap_open_live, permet:
'pcap_open_live() is used to obtain a packet capture descriptor to look
at packet on the network'.
Elle est utilisée pour obtenir un descripteur de capture de paquets pour
surveiller les datagrammes sur le réseau. Ca va renvoyer, une fois les
paramètres bien choisis, une variable, de type 'pcap_t', qui va permettre
d'agir sur ce genre d'interface virtuel qui joue sur la capture.
Les paramètres sont : 'char *device', chaine de caractères devant comporter
le nom de l'interface réseau, 'int snaplen', entier qui doit spécifier le
nombre maximum d'octets à capturer, 'int promisc', qui doit soit contenir 0,
soit un autre nombre (faux, vrai), pour dire si l'interface doit se situer
en mode promiscious (pour ecouter tout les paquets, meme ceux qui ne sont pas
déstinés à nous), 'int to_ms', le timeout en lecture, 'char *ebuf', le buffer
de sortie d'erreur. (utilisé quand pcap_open_live ne marche pas, ce qui lui
fait retourner une erreur).
Pour pcap_next:
Une fois le descripteur ouvert, on va recevoir des paquets dans les buffers.
pcap_next permet de les lire, un par un, a chaque appel de cette fonction, en
renvoyant l'adresse du prochain paquet.
Qu'est ce que ca donne en programmant ?
# -- test 1 -- #
#include
// pour les entrées sorties (printf ...)
#include
// pour utiliser libpcap
int main(void)
{
int i;
// on va en avoir besoin pour notre boucle :)
pcap_t *desc = NULL;
// desc = notre descripteur
struct pcap_pkthdr usefull;
// usefull est une structure pcap_pkthdr (header de paquet), type de pcap
// dans notre programme, on doit la définir pour utiliser pcap_next, mais
// on l'utilise pas.
u_char *packet;
// pointeur sur nos paquets captés
if ((desc = pcap_open_live("xl0", 1500, 0, 1000, NULL))==NULL)
{
perror("pcap_open_live()");
exit(1);
}
// on ouvre le device xl0 (BSD: 3com vortex/boomerang), on lit 1500 octets
// par paquets, on ne met pas le périphérique en mode prosmisc, avec un
// timeout de 1000 secondes, et sans buffer d'erreur.
// Si pcap_open_live se passe mal, alors le if s'executera, et on aura le
// droit à l'erreur, et le programme quitera (perror, exit)
printf("A partir de maintenant, je surveille le traffic sur xl0
");
// Si tout se passe bien
while (1)
// boucle sans fin
// on attend de recevoir pleins de paquets, et donc on doit les recevoir
// pendant cette boucle. Il lira le réseau sans arret.
{
packet = (u_char *) pcap_next(desc, &usefull);
// on lit le prochain paquet. Si il n'y a pas de paquets en attente, alors
// la fonction renvoit NULL (et au bout d'une seconde, 1000 ms (notre timeout)
// sinon, elle renvoit l'adresse du paquet recu, et on le traite après:
if (packet != NULL)
{
// si le paquet n'est pas NULL
for (i=0; i virgo.toone.org: icmp: echo request
11:58:11.532662 virgo.toone.org > virginie.toone.org: icmp: echo reply
^C
4 packets received by filter
0 packets dropped by kernel
virginie:~$
J'avoue, l'hexa c'est pas très lisible.
Décomposons nos datagrammes un par un:
le premier:
ff ff ff ff ff ff 00 10 5a df 2d 92 08 06 00 01 08 00 06 04 00 01 00 10 5a df
déjà, il faut savoir que ce qu'on lit est directement ce que l'on recoit de la
carte, et donc c'est ce qui se passe à la couche liaison. Rappel de ce que l'on
recoit à cette couche:
+------------+------------+----+----/ /----+--------+
| Adr Dest. | Adr Source |type| Données | CRC |
+------------+------------+----+----/ /----+--------+
6 octets 6 octets 2 46-1500 4 octets
(RFC 894)
On a:
adresse MAC de destination: ff ff ff ff ff ff
adresse MAC source: 00 10 5a df 2d 92
type: 08 06
on consulte la doc sur le tcp/ip, on voit que cela est de l'arp (type 08 06),
un broadcast de la machine avec comme adresse MAC 00:10:5a:df:2d:92
Je vérifie ma config:
virginie:~$ ifconfig xl0
xl0: flags=8943 mtu 1500
inet 192.168.0.1 netmask 0xffffff00 broadcast 192.168.0.255
ether 00:10:5a:df:2d:92
media: autoselect (10baseT/UTP) status: active
supported media: autoselect 100baseTX
100baseTX 10baseT/UTP 10baseT/UTP 100baseTX
virginie:~$
C'est mon adresse MAC qui a envoyé ca.
Au même moment, tcpdump nous a dit:
11:58:11.532055 arp who-has virgo.toone.org tell virginie.toone.org
une requete ARP de virginie.toone.org (ma machine) demandant à tout le monde
qui est virgo.toone.org (via le broadcast).
C'est bon, les données entre tcpdump et notre sniffeur marche, on l'a vérifié :)
On verra plus loin, comment construire un sniffeur permettant de décoder toutes
les trames.
Une autre fonction que l'on peut utiliser existant dans libpcap, est:
char *pcap_lookupdev(char *errbuf)
Elle permet de récupérer l'interface réseau par défaut.
Si on veut l'utiliser dans notre programme, on n'a qu'à remplacer "xl0" dans
if ((desc = pcap_open_live("xl0", 1500, 0, 1000, NULL))==NULL)
par pcap_lookupdev(NULL), ce qui nous donne:
(..)
if ((desc = pcap_open_live(pcap_lookupdev(NULL), 1500, 0, 1000, NULL))==NULL)
(..)
Une autre fonction de libpcap, que j'ai découvert apres avoir sortis la
1ère release de cette doc, c'est pcap_stats. Elle permet de sortir des stats
(et oui).
Pour l'utiliser:
//int pcap_stats(pcap_t *p, struct pcap_stat *ps)
//
//struct pcap_stat {
// u_int ps_recv; /* number of packets received */
// u_int ps_drop; /* number of packets dropped */
// u_int ps_ifdrop; /* drops by interface XXX not yet supported */
//};
Cette fonction marche en deux temps. On doit l'appeler après initialisation du
descripteur de pcap (avec pcap_open_live), pour initialiser le 'log', et une
autre fois quand on veut récuperer les résultats.
ex:
// ... Début du programme ...
struct pcap_stat *ps;
// Déclaration du pointeur
// ... suite ...
ps = (struct pcap_stat*)malloc(sizeof(struct pcap_stat));
// Allocation en mémoire
// ... suite ...
desc = pcap_open_live(device, 1518, 1, 1000, NULL);
pcap_stats(desc,ps);
// Initialisation (après pcap_openlive)
// ... suite du programme ...
pcap_stats(desc,ps);
printf("nombre de paquets total : %6i",ps->ps_recv);
printf("nombre de paquets droppés : %6i",ps->drop);
// ... fin
Voila pour libpcap. Elle incorpore plus de fonctions, que j'introduierai peut
etre plus tard, lors d'un autre cours, sans doute porté que là dessus.
2) Sniffer avec les sockets en RAW
Sur certains systèmes, on ne trouve pas libpcap. Ca arrive, même si celui ci
est très portable. On est donc obligé d'utiliser autre chose, de facon plus
violente (enfin, je trouve), et cela est les RAW sockets.
Qu'est ce qu'une socket ?
Une socket est un point de terminaison d'une communication. Quand on va lire
et envoyer des données dans un réseau, on le fera souvent avec, et ce, même
sans le savoir. C'est un point essentiel dans la communication des systèmes
Unix.
Qu'est ce qu'une RAW socket ?
Une RAW socket permet de lire et de construire des paquets en brut, construits
à la main. Elles permettent l'acces aux protocoles internes du réseau et des
interfaces. Le type RAW est uniquement utilisable par le super utilisateur
(un utilisateur normal ne pourra pas faire n'importe quoi :)
Comment utiliser ces RAW socket ?
on utilise la primitive socket()
virginie:/usr/www/htdocs/mags$ man socket
(...)
SYNOPSIS
#include
#include
int
socket(int domain, int type, int protocol)
(...)
Le domaine, c'est un peu 'ou' on va utiliser la socket. Nous, on va lire
dans l'internet (néanmoins votre réseau local qui marche comme), et donc
utiliser:
(Adresse families, /usr/include/sys/socket.h)
#define AF_INET 2 /* internetwork: UDP, TCP, etc. */
Le type, c'est le type de socket, nous RAW: SOCK_RAW
Le protocole, on va prendre un protocole dans /etc/protocols, comme par
exemple ICMP (1)
Une fois la socket ouverte et créée, on va devoir la lire :)
Pour cela, on peut (et on va) utiliser recvfrom(), qui ressemble à ca:
ssize_t
recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from,
socklen_t *fromlen)
D'apparence ca fait peur, je sais.
'int s' est la socket, 'void *buf' est le buffer, 'size_t len' sa taille,
'int flags' sont les flags optionnels (nous = 0), 'struct sockaddr *from'
est une structure associée à ce que l'on recoit, 'socklen_t *fromlen' est
la taille de la précédente structure.
Après construction du programme:
# -- sniffeur en raw -- #
#include
#include
#include
#include
// les fichiers d'entete (pour socket(), recvfrom(), les types différents
int main(void)
{
int i;
int sock, bytes_recieved, fromlen;
char buffer[65535];
struct sockaddr_in from;
// variables
sock = socket(AF_INET,SOCK_RAW,0);
// ouverture de la socket elle méme
while (1)
{
bytes_recieved = recvfrom(sock, buffer, sizeof buffer, 0,
(struct sockaddr *)&from, &fromlen);
// reception des données
if (bytes_recieved > 0)
{
for (i=0; i virgo.toone.org: icmp: echo request
13:50:29.021512 virgo.toone.org > virginie.toone.org: icmp: echo reply
Et oui, c'est tout ce que l'on recoit. On a pas les données à la couche liaison,
non plus les requètes ARP, et on a que la réponse du ping.
(On en recoit qu'un tiers).
Je préfère libpcap :)
3) Exploiter les résultats.
L'objectif de ce chapitre est de passer de quelque chose de la forme:
virginie:/usr/home/cns$ ./test
A partir de maintenant, je surveille le traffic sur xl0
ff ff ff ff ff ff 00 10 5a df 2d 92 08 06 00 01 08 00 06 04 00 01 00 10 5a df
00 10 5a df 2d 92 00 20 af f3 24 29 08 06 00 01 08 00 06 04 00 02 00 20 af f3
00 20 af f3 24 29 00 10 5a df 2d 92 08 00 45 00 00 54 7d ad 00 00 ff 01 bc a7
00 10 5a df 2d 92 00 20 af f3 24 29 08 00 45 00 00 54 b7 05 00 00 20 01 62 50
^C
virginie:/usr/home/cns$
à quelque chose de plus lisible pour l'être humain.
On va donc faire pas mal de formatage et compagnie pour cela.
De plus, on repart à partir de la libpcap, car on a beaucoup plus d'informations
et donc c'est plus utile.
On repart du code:
#include
#include
int main(void)
{
int i;
pcap_t *desc = NULL;
struct pcap_pkthdr usefull;
u_char *packet;
if ((desc = pcap_open_live(pcap_lookupdev(NULL), 1500, 0, 1000, NULL))==NULL)
{
perror("pcap_open_live()");
exit(1);
}
printf("A partir de maintenant, je surveille le traffic sur %s
",
pcap_lookupdev(NULL));
while (1)
{
packet = (u_char *) pcap_next(desc, &usefull);
if (packet != NULL)
{
for (i=0; iether_shost[i]);
if (i!=5) printf(":");
}
printf(" -> ");
for (i=0;iether_dhost[i]);
if (i!=5) printf(":");
}
printf(" type: %.4x
",paquet->ether_type);
}
Une fois la fonction et la structure incorporées au début du programme, on peut
l'executer pour voir (on appelle la fonction dans la boucle principale du
programme, et de cette facon:
print_ether_header((struct ether_header*)packet);
(on appelle la fonction, en disant qu'il faut convertir sous le format de cette
structure.)
On compile, et execute:
virginie:/usr/www/htdocs/mags/progs$ gcc -lpcap -o test test.c ; ./test
A partir de maintenant, je surveille le traffic sur xl0
00:10:5a:df:2d:92 -> 00:20:af:f3:24:29 type: 0008
00:20:af:f3:24:29 -> 00:10:5a:df:2d:92 type: 0008
^C
virginie:/usr/www/htdocs/mags/progs$
(en lancant un ping, en parallèle)
Effectivement, on obtient quelquechose de plus lisible.
Bon, les adresses ARP c'est bien, mais un peu dur à lire, surtout quand on a
un réseau de 1000 machines (ou plus, bien sur :-)
J'ai un peu cherché, mais à part faire une requète rarp, ou autre, il n'y a pas
de moyen direct pour convertir une adresse arp en adresse IP, voir le nom de la
machine.
Souvenez vous, si vous avez lu la doc que j'ai écris sur le tcp/ip, je disais
qu'en principe, soit le type de trame est 08 (donc paquet IP), alors les
adresses IP sont récupérable via l'entete IP, soit c'est une requète ARP/RARP,
dans quels cas il est aussi possible de récupérer l'adresse IP (les adresses IP
en fait, car on a source et destination.)
On doit remarquer aussi que le type donnée 0008 est 'incorrect', car inversé.
Il faut inverser les 2 octects pour avoir la donnée correcte, qui est 0800.
On va alors ne pas se compliquer la vie, et rechercher les IPs en lisant
le paquet de données.
Pour ne pas avoir des écrans de résultats à ralonge à chaque paquets
(80 colonnes ca fait peu :-), on va utiliser des variables globales qu'on
initialise en passant des paramètres au programme, afin de gérer la sortie
visuelle.
on modifie déjà la clause main, qui passe de
int
main(void)
à
int
main(int argn, char **argv)
On va pouvoir récupérer les arguments, et cela en créant une fonction
void
initvars(int argn, char **argv)
qui va lire chaque arguments un par un, et gérer nos variables.
On retouche le programme, et on a:
# -- Sniffeur avec pcap, version alpha 0.02 -- #
#include
#include
/***************************************************************************/
// Variables
int affiche_ethernet_header;
/***************************************************************************/
// Déclaration des structures:
struct ether_header {
u_char ether_dhost[6]; //destination host (adresse de destination)
u_char ether_shost[6]; //source host (adresse source)
u_short ether_type; //type de trame
};
/***************************************************************************/
void initvars(int argn, char **argv)
{
int arg;
// on met tout au paramètres par défaut
affiche_ethernet_header = 0;
while ((arg = getopt(argn, argv, "aE")) != EOF)
// c'est BEAUCOUP plus simple quand on connait getopt :-)
{
switch(arg)
{
case 'E': affiche_ethernet_header = 1; break;
default:
printf("
E: en tete Ethernet
");
break;
}
}
}
/***************************************************************************/
void print_ether_header(struct ether_header *paquet)
{
int i;
for (i=0;iether_shost[i]);
if (i!=5) printf(":");
}
printf(" -> ");
for (i=0;iether_dhost[i]);
if (i!=5) printf(":");
}
printf(" ");
printf("Type de trame: %.4x
",paquet->ether_type);
}
int
main(int argn, char **argv)
{
int i;
pcap_t *desc = NULL;
struct pcap_pkthdr usefull;
u_char *packet;
/*
Initialisation des variables globales
*/
initvars(argn, argv);
/*
Pour l'affichage: si à 1, alors on affiche, sinon on n'affiche pas.
*/
if ((desc = pcap_open_live(pcap_lookupdev(NULL), 1500, 0, 1000, NULL))==NULL)
{
perror("pcap_open_live()");
exit(1);
}
printf("A partir de maintenant, je surveille le traffic sur %s
",
pcap_lookupdev(NULL));
while (1)
{
packet = (u_char *) pcap_next(desc, &usefull);
if (packet != NULL)
{
if (affiche_ethernet_header)
print_ether_header((struct ether_header*)packet);
}
}
}
# -- FIN 'Sniffeur avec pcap, version alpha 0.02' -- #
Pour le faire marcher, faites:
virginie:/usr/www/htdocs/mags/progs$ ./test -E
A partir de maintenant, je surveille le traffic sur xl0
00:10:5a:df:2d:92 -> 00:20:af:f3:24:29 Type de trame: 0008
00:20:af:f3:24:29 -> 00:10:5a:df:2d:92 Type de trame: 0008
(-E pour faire afficher l'entète Ethernet)
Passons à l'entète IP
(je passe le formatage complet de l'entète ethernet, c'est trop trivial)
Rappelons le format du header IP:
0 7 15 16 31
+--------+--------+----------------+--------------------------------+
| version|longueur| type de service| longueur totale sur 16 bits |
| 4 bits | 4 bits | (TOS) 8 bits | (en octets) |
+--------+--------+----------------+------+-------------------------+
| identification |3 bits| 13 bits fragment offset |
| |flags | |
+-----------------+----------------+------+-------------------------+
| durée de vie | protocole | somme de contrôle d'en tête |
| (TTL) 8 bits | 8 bits | (16 bits) |
+-----------------+----------------+--------------------------------+
| adresse IP source sur 32 bits |
| |
+-------------------------------------------------------------------+
| adresse IP destination sur 32 bits |
| |
+-------------------------------------------------------------------+
| |
/ options (s'il y en a) /
/ données /
| |
+-------------------------------------------------------------------+
Et hop, la structure associée à ca:
struct ip {
#if BYTE_ORDER == LITTLE_ENDIAN // l'ordre des bits dépendant des systèmes
u_char ip_hl:4, // header length (taille)
u_char ip_v:4; // version
#endif
#if BYTE_ORDER == BIG_ENDIAN
u_char ip_v:4, // version
u_char ip_hl:4; // header length
#endif
u_char ip_tos; // Type de service
short ip_len; // taille totale
u_short ip_id; // identification
short ip_off; // champ de décalage du fragment
#define IP_DF 0x4000 // drapeau de non fragmentation
#define IP_MF 0x2000 // drapeau de fragmentation
#define IP_OFFMASK 0x1fff // masque pour la fragmentation
u_char ip_ttl; // time to live
u_char ip_p; // protocole
u_short ip_sum; // somme de controle
struct in_addr ip_src; // adresse source
struct in_addr ip_dest; // adresse destination
};
(Note, on définit aussi in_addr, ca ne mange pas de pain:
struct in_addr {
u_long s_addr;
};
)
Voila ce que l'on doit traiter si le type de trame est 0x0800
Pour récupérer la structure ip quand on a un paquet, il suffit de déclarer un
pointeur vers cette structure:
struct ip *paquetip;
Et de l'assigner dès qu'on a le paquet:
paquetip=(struct ip*)(packet+sizeof(struct ether_header));
Et voila, il ne reste plus qu'à appeler
printf("%x
",paquetip->ip_p); //pour avoir le protocole
printf("%l
",paquetip->ip_src.s_addr); //pour avoir l'adresse IP
On pourra en faire de même avec les protocoles UDP, TCP ...
Mais jusqu'à la, si vous savez programmer en C, si vous avez la doc sur
le TCP/IP (une fois complète), et un peu de jujotte, vous pourrez faire un
sniffeur plus puissant que tcpdump lui même.
J'espère vous avoir appris quelquechose.
|