Étudiants
- Youssef EL KADI
- Mathéo GALUBA
TPs réalisés
- [[#tp2---utilisations-de-sockets-de-type-raw|TP2 - Utilisations de sockets de type RAW]]
- TP5 - Implémentation d’un sniffer réseau
TP2 - Utilisations de sockets de type RAW
1 - Préambule
Exercice 1
Le fichier /usr/include/netinet/ip.h contient une structure nommée struct iphdr. Étudiez cette structure et comparez-la au format de la trame IP que vous trouverez sur Internet.
struct iphdr
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ihl:4;
unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
unsigned int version:4;
unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
uint8_t tos;
uint16_t tot_len;
uint16_t id;
uint16_t frag_off;
uint8_t ttl;
uint8_t protocol;
uint16_t check;
uint32_t saddr;
uint32_t daddr;
/*The options start here. */
};
Source: https://www.malekal.com/quelle-est-la-structure-paquet-tcp-ip/
| Champ | Taille (bits) | Description |
|---|---|---|
| version | 4 | Numéro de version du protocole IP |
| ihl (Internet Header Length) | 4 | Longueur de l’en-tête en mots de 32 bits (1 mot = 4 octets) |
| tos (Type of Service) | 8 | Ancien champ de “Type de service”, remplacé par le champ DSCP (Differentiated Services Code Point) |
| tot_len (Total Length) | 16 | Taille totale du paquet IP (en-tête + données) en octets |
| id (Identification) | 16 | Identifiant du paquet |
| frag_off (Fragment Offset + Flags) | 16 | Contient à la fois les drapeaux de fragmentation et le décalage du fragment |
| ttl (Time To Live) | 8 | Durée de vie du paquet (en nombre de sauts) |
| protocol | 8 | Protocole de la couche supérieure (ICMP, TCP, UDP, etc.) |
| check (Header Checksum) | 16 | Somme de contrôle de l’en-tête IP |
| saddr (Source Address) | 32 | Adresse IP source (IPv4) |
| daddr (Destination Address) | 32 | Adresse IP de destination |
| options (non inclus ici) | variable | Champs optionnels (sécurité, routage, timestamp, etc.) |
Exercice 2
Le fichier /usr/include/netinet/ip_icmp.h contient une structure nommée struct icmphdr. Étudiez cette structure et comparez-la au format de la trame ICMP que vous trouverez sur Internet.
struct icmphdr
{
uint8_t type; /* message type */
uint8_t code; /* type sub-code */
uint16_t checksum;
union
{
struct
{
uint16_t id;
uint16_t sequence;
} echo; /* echo datagram */
uint32_t gateway; /* gateway address */
struct
{
uint16_t __glibc_reserved;
uint16_t mtu;
} frag; /* path mtu discovery */
} un;
};
Source: https://tecadmin.net/icmp-internet-control-message-protocol/
| Champ | Taille (bits) | Description |
|---|---|---|
| type | 8 | Type du message ICMP |
| code | 8 | Sous-type (ou “code”) |
| checksum | 16 | Somme de contrôle ICMP |
| un (union) | variable | Contient différents sous-champs selon le type ICMP |
Exercice 3
Lisez la documentation des fonctions htons et ntohs.
The htons() function converts the unsigned short integer hostshort from host byte order to network byte order.
The ntohs() function converts the unsigned short integer netshort from network byte order to host byte order.
Ces deux fonctions servent à convertir l’ordre des octets entre “host byte order” (spécifique à chaque machine suivant les détails d’implémentation) et “network byte order” (indépendant de la machine).
Source: https://www.man7.org/linux/man-pages/man3/htons.3.html Source: https://www.ibm.com/docs/en/zvm/7.2.0?topic=domains-network-byte-order-host-byte-order
2 - Création des fonctions de manipulation
2.1 - Affichage d’un tableau d’octets
Exercice 4
Écrivez une fonction void display(char * buffer, int length) qui affiche les length octets du tableau buffer, en affichant les octets en hexadécimal, deux par deux, et seize par lignes.
// Exercice 4: Affichage d'un tableau d'octets
void display(char *buffer, int length) {
printf("Contenu du buffer (%d octets):\n", length);
for (int i = 0; i < length; i++) {
printf("%02x", (unsigned char)buffer[i]);
// Ajouter un espace tous les 2 octets (sauf à la fin)
if (i % 2 == 1 && i < length - 1) {
printf(" ");
}
// Retour à la ligne tous les 16 octets
if ((i + 1) % 16 == 0) {
printf("\n");
}
}
if (length % 16 != 0) {
printf("\n");
}
printf("\n");
}2.2 - Entête IP
Exercice 5
Écrivez une fonction qui remplit une structure struct iphdr.
// Exercice 5: Remplir une structure iphdr
void fill_iphdr(struct iphdr *ip, uint32_t saddr, uint32_t daddr,
uint16_t tot_len, uint8_t protocol) {
ip->version = 4; // IPv4
ip->ihl = 5; // Header length = 5 * 4 = 20 octets
ip->tos = 0; // Type of Service
ip->tot_len = htons(tot_len); // Longueur totale (en network byte order)
ip->id = htons(0x1234); // Identifiant du paquet
ip->frag_off = 0; // Pas de fragmentation
ip->ttl = 100; // Time To Live
ip->protocol = protocol; // Protocole (ICMP = 1)
ip->check = 0; // Sera calculé plus tard
ip->saddr = saddr; // Adresse source
ip->daddr = daddr; // Adresse destination
}2.3 - Entête ICMP
Exercice 6
Écrivez une fonction qui remplit une structure struct icmphdr.
// Exercice 6: Remplir une structure icmphdr
void fill_icmphdr(struct icmphdr *icmp, uint8_t type, uint8_t code, uint16_t id,
uint16_t sequence) {
icmp->type = type; // Type du message ICMP
icmp->code = code; // Code (sous-type)
icmp->checksum = 0; // Sera calculé plus tard
icmp->un.echo.id = htons(id); // Identifiant echo
icmp->un.echo.sequence = htons(sequence); // Numéro de séquence
}2.4 - Sommes de contrôle
Exercice 7
Implémentez la fonction short checksum(char * addr, int count) qui calcule la somme de contrôle selon l’algorithme.
// Exercice 7: Calcul de la somme de contrôle
// Source: https://www.rfc-editor.org/rfc/rfc1071
short checksum(char *addr, int count) {
/* Compute Internet Checksum for "count" bytes
* beginning at location "addr".
*/
register long sum = 0;
while (count > 1) {
/* This is the inner loop */
sum += *(unsigned short *)addr;
addr += 2;
count -= 2;
}
/* Add left-over byte, if any */
if (count > 0) {
sum += *(unsigned char *)addr * 256;
}
/* Fold 32-bit sum to 16 bits */
while (sum >> 16) {
sum = (sum & 0xffff) + (sum >> 16);
}
short checksum = ~sum;
return checksum;
}Exercice 8
Après avoir rempli les structures struct iphdr et struct icmphdr dans un tableau d’octets, mettez à jour la somme de contrôle d’ICMP. Cette somme de contrôle porte sur l’ensemble du paquet ICMP (incluant ses éventuelles données).
// Exercice 8: Mettre à jour la somme de contrôle ICMP
void update_icmp_checksum(char *icmp, int length) {
struct icmphdr *icmp_hdr = (struct icmphdr *)icmp;
icmp_hdr->checksum = 0;
icmp_hdr->checksum = checksum(icmp, length);
}Exercice 9
Après avoir mis à jour la somme de contrôle d’ICMP, mettez a jour la somme de contrôle IP. Cette somme de contrôle porte sur l’ensemble du paquet IP (incluant le paquet ICMP et ses éventuelles données).
// Exercice 9: Mettre à jour la somme de contrôle IP
void update_ip_checksum(char *ip, int length) {
struct iphdr *ip_hdr = (struct iphdr *)ip;
ip_hdr->check = 0;
ip_hdr->check = checksum(ip, length);
}2.5 - Vérification
Exercice 10
En choisissant les mêmes paramètres que ceux indiqués au-dessus, vérifiez que votre programme affiche bien les octets suivants, avant le calcul des sommes de contrôle :
4500 001c 1234 0000 6401 0000 c0a8 0001
c0a8 0002 0800 0000 5678 0001
Sortie de notre programme :
=== Exercice 10: Avant calcul des sommes de contrôle ===
Contenu du buffer (28 octets):
4500 001c 1234 0000 6401 0000 c0a8 0001
c0a8 0002 0800 0000 5678 0001
Exercice 11
En choisissant les mêmes paramètres que ceux indiqués au-dessus, vérifiez que votre programme affiche bien les octets suivants, après le calcul des sommes de contrôle :
4500 001c 1234 0000 6401 c65c c0a8 0001
c0a8 0002 0800 a186 5678 0001
Sortie de notre programme :
=== Exercice 11: Après calcul des sommes de contrôle ===
Contenu du buffer (28 octets):
4500 001c 1234 0000 6401 c359 c0a8 0001
c0a8 0002 0800 a186 5678 0001
On notera toutefois que les sommes de contrôle diffèrent légèrement, mais nous avons bien validé la nôtre et l’erreur se trouve dans la consigne (aide utilisé : https://en.wikipedia.org/wiki/Internet_checksum).
3 - Envoi du paquet ECHO_REQUEST
Exercice 12
Créez une socket pour la famille PF_INET, de type SOCK_RAW et concernant le protocole IPPROTO_RAW. Vous penserez à tester le retour de la fonction et à utiliser perror en cas d’erreur.
int sock_send = socket(PF_INET, SOCK_RAW, IPPROTO_RAW);
if (sock_send < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
printf("Socket RAW créée avec succès (fd: %d)\n", sock_send);Exercice 13
Envoyez le paquet sur la socket créée. Vous penserez à tester le retour de la fonction et à utiliser perror en cas d’erreur.
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = 0;
dest_addr.sin_addr.s_addr = daddr;
// dest_addr.sin_zero est déjà initialisé à 0 par memset
if (sendto(sock_send, buffer, tot_len, 0, (struct sockaddr *)&dest_addr,
sizeof(struct sockaddr_in)) < 0) {
perror("sendto");
exit(EXIT_FAILURE);
}
printf("Paquet envoyé avec succès à %s\n", inet_ntoa(dest_addr.sin_addr));4 - Réception du paquet ECHO_REPLY
Pour cette partie, nous avons décidé de créer un deuxième exécutable.
Nous avons donc sender pour envoyer le paquet et receiver qui écoute une réponse de manière bloquante.
Exercice 14
Créez une autre socket de type SOCK_RAW mais de protocole IPPROTO_ICMP à présent.
int sock_rec = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sock_rec < 0) {
perror("socket");
exit(EXIT_FAILURE);
}Exercice 15
Utilisez la fonction recvfrom pour recevoir la réponse éventuelle de votre ping.
struct sockaddr_in from;
socklen_t fromlen = sizeof(from);
ssize_t rec_len = recvfrom(sock_rec, buffer, sizeof(buffer), 0,
(struct sockaddr *)&from, &fromlen);
if (rec_len < 0) {
perror("recvfrom");
exit(EXIT_FAILURE);
}Améliorations
Récupération automatique de l’adresse IP source (machine locale)
Nous avons implémenté une fonction get_local_ip() qui retourne l’IPv4 locale afin d’éviter son entrée manuelle par l’utilisateur ou le code en dure de cette IP dans le code.
uint32_t get_local_ip() {
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket (local IP)");
exit(EXIT_FAILURE);
}
struct sockaddr_in temp_addr;
memset(&temp_addr, 0, sizeof(temp_addr));
temp_addr.sin_family = AF_INET;
temp_addr.sin_port = htons(80);
inet_pton(AF_INET, "8.8.8.8", &temp_addr.sin_addr);
if (connect(sock, (struct sockaddr *)&temp_addr, sizeof(temp_addr)) < 0) {
perror("connect (local IP)");
close(sock);
exit(EXIT_FAILURE);
}
struct sockaddr_in local_addr;
socklen_t addr_len = sizeof(local_addr);
if (getsockname(sock, (struct sockaddr *)&local_addr, &addr_len) < 0) {
perror("getsockname");
close(sock);
exit(EXIT_FAILURE);
}
close(sock);
return local_addr.sin_addr.s_addr;
}Passage d’un paramètre ‘destination’
Notre exécutable prend en paramètre l’IPv4 de destination à la manière de la commande ping sous GNU+Linux.
Calcul du temps de réponse
Nous avons ajouté un calcul du temps entre l’envoi et la réception et nous affichons le résultat à l’utilisateur en milliseconde.
struct timeval start, end;
gettimeofday(&start, NULL);
// [...] Envoi
printf("PING à %s\n", inet_ntoa(dest_addr.sin_addr));
// [...] Reception
gettimeofday(&end, NULL);
long sec = end.tv_sec - start.tv_sec;
long usec = end.tv_usec - start.tv_usec;
double elapsed_ms = (sec * 1000.0) + (usec / 1000.0);
printf("PONG de %s en %.3f ms (%zd octets)\n", inet_ntoa(from.sin_addr),
elapsed_ms, rec_len);Fermeture des sockets
Bien que ça ne soit pas préciser dans le sujet, nous appelons la fonction close() sur toutes les sockets une fois leur utilisation terminées.
uint32_t get_local_ip() {
// [...]
close(sock);
return local_addr.sin_addr.s_addr;
}
int main(int argc, char *argv[]) {
// [...]
close(sock_send);
close(sock_recv);
return EXIT_SUCCESS;
}TP5 - Implémentation d’un sniffer réseau
Exercice 1
Installez tcpcdump sur votre machine.
Exercice 2
Exécutez tcpdump -i eth0 ip et chargez la page web de Google. Que se passe-t’il ? Pensez à remplacer, si besoin, eth0 par l’interface amenant à votre connexion Internet.
sudo tcpdump -i wlan0 ipNous utilisons wlan0 car notre connexion Internet passe par le WiFi.
Lorsqu’on charge la page Google (https://www.google.com), tcpdump capture et affiche tous les paquets IP échangés. On observe :
Requêtes DNS
Résolution du nom de domaine www.google.com
13:08:53.458229 IP archlinux-matheo.59416 > _gateway.domain: 32098+ A? www.google.com. (32)
13:08:53.458242 IP archlinux-matheo.59416 > _gateway.domain: 59232+ AAAA? www.google.com. (32)
13:08:53.492594 IP _gateway.domain > archlinux-matheo.59416: 32098 1/0/0 A 216.58.205.196 (48)
13:08:53.493683 IP _gateway.domain > archlinux-matheo.59416: 59232 1/0/0 AAAA 2a00:1450:4006:807::2004 (60)
- Double requête DNS : Type A (IPv4) et AAAA (IPv6)
- Réponses obtenues :
- IPv4 :
216.58.205.196 - IPv6 :
2a00:1450:4006:807::2004
- IPv4 :
Résolution du nom de domaine www.gstatic.com
13:08:53.672292 IP archlinux-matheo.43146 > _gateway.domain: 56214+ A? www.gstatic.com. (33)
13:08:53.681512 IP _gateway.domain > archlinux-matheo.43146: 56214 1/0/0 A 216.58.205.195 (49)
Connection TCP
13:08:55.426416 IP 194.214.237.10.36723 > archlinux-matheo.snapenetio: Flags [S], seq 2335130437, win 64240
13:08:55.426445 IP archlinux-matheo.snapenetio > 194.214.237.10.36723: Flags [S.], seq 3316363639, ack 2335130438, win 65160
13:08:55.462400 IP 194.214.237.10.36723 > archlinux-matheo.snapenetio: Flags [.], ack 1, win 502
Étapes du handshake TCP :
[S]: SYN - Demande de connexion[S.]: SYN-ACK - Acceptation de la connexion[.]: ACK - Confirmation
Échange de données TLS/HTTPS
13:08:55.465585 IP 194.214.237.10.36723 > archlinux-matheo.snapenetio: Flags [P.], seq 1:224, ack 1, win 502, length 223
13:08:55.466156 IP archlinux-matheo.snapenetio > 194.214.237.10.36723: Flags [P.], seq 1:1034, ack 224, win 64, length 1033
13:08:56.093808 IP archlinux-matheo.42356 > 82.221.107.34.bc.googleusercontent.com.http: Flags [.], ack 3134761312, win 63 13:08:56.105422 IP 82.221.107.34.bc.googleusercontent.com.http > archlinux-matheo.42356: Flags [.], ack 1, win 1050
Exercice 3
Implémentez un sniffer affichant un message à chaque paquet reçu.
// #include [...]
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <interface>\n", argv[0]);
return EXIT_FAILURE;
}
const char *ifname = argv[1];
// Création d'une socket RAW pour capturer tous les paquets
int sock_rec = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock_rec == -1) {
perror("socket(AF_PACKET)");
return EXIT_FAILURE;
}
// Configuration de l'interface réseau
struct sockaddr_ll sll;
memset(&sll, 0, sizeof(sll));
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
if (ioctl(sock_rec, SIOCGIFINDEX, &ifr) == -1) {
perror("ioctl(SIOCGIFINDEX)");
close(sock_rec);
return EXIT_FAILURE;
}
sll.sll_family = AF_PACKET;
sll.sll_ifindex = ifr.ifr_ifindex;
sll.sll_protocol = htons(ETH_P_ALL);
// Liaison de la socket à l'interface
if (bind(sock_rec, (struct sockaddr *)&sll, sizeof(sll)) == -1) {
perror("bind");
close(sock_rec);
return EXIT_FAILURE;
}
printf("Sniffer démarré sur l'interface %s...\n\n", ifname);
// Buffer pour stocker les paquets reçus
unsigned char buffer[65536];
int packet_count = 0;
// Boucle de réception
while (1) {
struct sockaddr_ll from;
socklen_t fromlen = sizeof(from);
ssize_t rec_len = recvfrom(sock_rec, buffer, sizeof(buffer), 0,
(struct sockaddr *)&from, &fromlen);
if (rec_len < 0) {
perror("recvfrom");
close(sock_rec);
return EXIT_FAILURE;
}
packet_count++;
printf("Paquet #%d reçu : %zd octets\n", packet_count, rec_len);
}
close(sock_rec);
return EXIT_SUCCESS;
}1 Messages IP
Exercice 4
Ajoutez l’affichage d’informations de base sur les messages IP : temps de réception, source, destination, taille, type de protocole.
// Récupération du timestamp
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
char timestr[64];
struct tm *tm_info = localtime(&ts.tv_sec);
strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M:%S", tm_info);
// Extraction et affichage des informations IP
struct iphdr *ip = (struct iphdr *)(buffer + ip_off);
char src_ip[INET_ADDRSTRLEN], dst_ip[INET_ADDRSTRLEN];
struct in_addr saddr, daddr;
saddr.s_addr = ip->saddr;
daddr.s_addr = ip->daddr;
inet_ntop(AF_INET, &saddr, src_ip, sizeof(src_ip));
inet_ntop(AF_INET, &daddr, dst_ip, sizeof(dst_ip));
printf(
"[%s.%03ld] - IP: %s -> %s\n protocol=%u\n size=%zd octets",
timestr, ts.tv_nsec / 1000000L, src_ip, dst_ip, ip->protocol, rec_len);- Utilisation de
clock_gettime(CLOCK_REALTIME, &ts)pour obtenir l’horodatage - Conversion des adresses IP depuis le format réseau (
ip->saddr,ip->daddr) vers des chaînes lisibles par les humains avecinet_ntop() - Affichage du protocole (
ip->protocol) et de la taille totale du paquet
2 Messages ICMP
Exercice 5
Ajoutez la prise en compte des messages ICMP.
Pour la suite du TP, nous avons isolé la gestion et l’affichage de chaque protocole dans des procédures. Ainsi, dans la fonction main, nous dispatchons la gestion du paquet IP selon le protocole comme suit :
switch (ip->protocol) {
// [...] Autres protocoles
case IPPROTO_ICMP:
dump_icmp(rec_len, buffer, transport_off);
break;
}Exemple du résultat de dump_icmp sur la sortie standard lors de l’exécution de la commande ping 192.168.1.254 -c 1 :
[2025-11-11 09:57:52.138] - 192.168.1.130 -> 192.168.1.254 98 octets
ICMP: type=8, code=0
[2025-11-11 09:57:52.142] - 192.168.1.254 -> 192.168.1.130 98 octets
ICMP: type=0, code=0
Exercice 6
Ajoutez la prise en charge des types de messages ICMP suivants : echo request, echo reply, time exceeded, port unreachable.
Ajout d’un switch dans la procédure dump_icmp, les type de message sont des macros qui proviennent de #include <netinet/ip_icmp.h> (voir [[#tp2---utilisations-de-sockets-de-type-raw|TP2 - Utilisations de sockets de type RAW]])
void dump_icmp(ssize_t rec_len, unsigned char *buffer, size_t transport_off) {
if (rec_len < (ssize_t)(transport_off + sizeof(struct icmphdr))) {
printf("entête icmp malformé\n");
return;
}
struct icmphdr *icmp = (struct icmphdr *)(buffer + transport_off);
printf("\t\tICMP: type=%u, code=%u\n", icmp->type, icmp->code);
switch (icmp->type) {
case ICMP_ECHOREPLY:
printf("\t\tEcho Reply\n");
break;
case ICMP_DEST_UNREACH:
printf("\t\tDestination Unreachable\n");
break;
case ICMP_ECHO:
printf("\t\tEcho Request\n");
break;
case ICMP_TIME_EXCEEDED:
printf("\t\tTime Exceeded\n");
break;
default:
break;
}
}Exercice 7
Vérifiez que votre sniffer fonctionne en exécutant ping et traceroute sur une machine distante. Analysez les résultats.
Sortie standard après l’exécution de ping 192.168.1.254 -c 1 :
[2025-11-11 10:07:30.554] - 192.168.1.130 -> 192.168.1.254 98 octets
ICMP: type=8, code=0
Echo Request
[2025-11-11 10:07:30.557] - 192.168.1.254 -> 192.168.1.130 98 octets
ICMP: type=0, code=0
Echo Reply
Sortie standard après l’exécution de :
tracepath 8.8.8.8
1?: [LOCALHOST] pmtu 1500
1: _gateway 3.597ms
1: _gateway 2.170ms
2: no reply
3: 212-195-252-172.abo.bbox.fr 27.791ms
4: no reply
5: 212.194.170.0 13.802ms asymm 7
6: no reply
[2025-11-11 10:12:44.605] - 192.168.1.254 -> 192.168.1.130 590 octets
ICMP: type=11, code=0
Time Exceeded
[2025-11-11 10:12:44.608] - 192.168.1.254 -> 192.168.1.130 590 octets
ICMP: type=11, code=0
Time Exceeded
[2025-11-11 10:12:47.639] - 212.195.252.172 -> 192.168.1.130 70 octets
ICMP: type=11, code=0
Time Exceeded
[2025-11-11 10:12:48.016] - 192.168.1.130 -> 192.168.1.254 179 octets
ICMP: type=3, code=3
Destination Unreachable
[2025-11-11 10:12:48.369] - 192.168.1.130 -> 192.168.1.254 123 octets
ICMP: type=3, code=3
Destination Unreachable
[2025-11-11 10:12:48.384] - 192.168.1.130 -> 192.168.1.254 135 octets
ICMP: type=3, code=3
Destination Unreachable
[2025-11-11 10:12:50.668] - 212.194.170.0 -> 192.168.1.130 70 octets
ICMP: type=11, code=0
Time Exceeded
[2025-11-11 10:13:01.359] - 192.168.1.130 -> 192.168.1.254 133 octets
ICMP: type=3, code=3
Destination Unreachable
3 Messages UDP
Exercice 8
Ajoutez la prise en compte des messages UDP. Vous afficherez le numéro de port source et le numéro de port destination.
Ajout du cas protocole UDP à notre switch dans la fonction main :
case IPPROTO_UDP:
dump_udp(rec_len, buffer, transport_off);
break;Exemple de message sur la sortie standard :
[2025-11-11 10:16:07.869] - 192.168.1.254 -> 192.168.1.130 95 octets
UDP: ports 53 --> 37165
Ce paquet utilise le port 53 donc probablement un paquet DNS…
Exercice 9
Exécutez à nouveau traceroute sur une machine distante et analysez le résultat.
4 Messages TCP
Exercice 10
Ajoutez la prise en compte des messages TCP. Vous afficherez le numéro de port source et le numéro de port destination.
Ajout du cas protocole TCP à notre switch et de la procédure dump_tcp :
switch (ip->protocol) {
case IPPROTO_TCP:
dump_tcp(rec_len, buffer, transport_off);
break;
case IPPROTO_UDP:
dump_udp(rec_len, buffer, transport_off);
break;
case IPPROTO_ICMP:
dump_icmp(rec_len, buffer, transport_off);
break;
}void dump_tcp(ssize_t rec_len, unsigned char *buffer, size_t transport_off) {
if (rec_len < (ssize_t)(transport_off + sizeof(struct tcphdr))) {
printf("entête TCP malformé \n");
return;
}
struct tcphdr *tcp = (struct tcphdr *)(buffer + transport_off);
unsigned short sport = ntohs(tcp->source);
unsigned short dport = ntohs(tcp->dest);
size_t payload_off = transport_off + tcp->doff * 4;
ssize_t payload_len = rec_len - payload_off;
printf("\tTCP: ports %u -> %u \n", sport, dport);
char *payload = (char *)(buffer + payload_off);
// [...]
}Exemple de messages sur la sortie standard lors d’une communication TCP :
[2025-11-11 10:39:54.448] - 194.214.237.10 -> 192.168.1.130 843 octets
TCP: ports 35324 -> 22000
[2025-11-11 10:39:54.449] - 194.214.237.10 -> 192.168.1.130 66 octets
TCP: ports 35324 -> 22000
[2025-11-11 10:39:54.966] - 140.82.121.5 -> 192.168.1.130 105 octets
TCP: ports 443 -> 43266
[2025-11-11 10:39:54.966] - 192.168.1.130 -> 140.82.121.5 66 octets
TCP: ports 43266 -> 443
[2025-11-11 10:39:54.966] - 192.168.1.130 -> 140.82.121.5 105 octets
TCP: ports 43266 -> 443
[2025-11-11 10:39:54.966] - 192.168.1.130 -> 140.82.121.5 90 octets
TCP: ports 43266 -> 443
[2025-11-11 10:39:54.981] - 140.82.121.5 -> 192.168.1.130 66 octets
TCP: ports 443 -> 43266
Exercice 11
Ajoutez la prise en compte des messages HTTP. Votre sniffer affichera la page demandée dès qu’il aura recueilli suffisamment d’information.
Dans la procédure dump_tcp nous ajoutons un ensemble de conditions basé sur les numéros de port afin d’identifier les protocoles utilisés. Dans un premier temps, HTTP et HTTPS :
char *payload = (char *)(buffer + payload_off);
if (sport == 443 || sport == 80) {
dump_http(payload, payload_len);
}La procédure dump_http :
void dump_http(char *payload, ssize_t payload_len) {
if (strstr(payload, "GET ") || strstr(payload, "POST ") ||
strstr(payload, "HTTP/1.1") || strstr(payload, "HTTP/1.0")) {
fwrite(payload, 1, (payload_len < 512 ? payload_len : 512), stdout);
printf("\n");
}
}On se limite au 512 premiers octets de la payload.
Exemple en exécutant la commande curl http://httpforever.com :
- Résolution DNS (les résultats montrés ici prennent en compte les changement effectué dans l’Exercice 14)
[2025-11-11 12:21:28.765] - 192.168.1.130 -> 192.168.1.254 75 octets
UDP: ports 56139 -> 53
DNS Query: httpforever.com DNS Answer: Type A, Class IN
[2025-11-11 12:21:28.775] - 192.168.1.254 -> 192.168.1.130 91 octets
UDP: ports 53 -> 56139
DNS Query: httpforever.com DNS Answer: Type A, Class IN
IP Address: 146.190.62.39
[2025-11-11 12:21:28.775] - 192.168.1.130 -> 192.168.1.254 75 octets
UDP: ports 35499 -> 53
DNS Query: httpforever.com DNS Answer: Type AAAA, Class IN
[2025-11-11 12:21:28.788] - 192.168.1.254 -> 192.168.1.130 103 octets
UDP: ports 53 -> 35499
DNS Query: httpforever.com DNS Answer: Type AAAA, Class IN
IPv6 Address: 2604:a880:4:1d0::1f1:2000
On constate que l’on a pu résoudre deux adresse IP. Pour simplifier, nous allons forcer l’IPv4 en la passant directement à la commande curl (sinon par défaut, curl va poursuivre avec l’IPv6).
- Requête HTTP
[2025-11-11 12:15:27.324] - 192.168.1.130 -> 146.190.62.39 143 octets
TCP: ports 44534 -> 80
GET / HTTP/1.1
Host: 146.190.62.39
User-Agent: curl/8.17.0
Accept: */*
- Réponse HTTP
[2025-11-11 12:15:27.532] - 146.190.62.39 -> 192.168.1.130 2962 octets
TCP: ports 80 -> 44534
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 11 Nov 2025 11:20:27 GMT
Content-Type: text/html
Content-Length: 5124
Last-Modified: Wed, 22 Mar 2023 14:54:48 GMT
Connection: keep-alive
ETag: "641b16b8-1404"
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
Feature-Policy: accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'
Content-Security-Policy: default-src 'self'; script-src cdnjs.cloudflare.com 'self' 'report-sha256'; style-src cdnjs.cloudflare.com 'self' fonts.googleapis.com 'unsafe-inline'; font-src fonts.googleapis.com fonts.gstatic.com cdnjs.cloudflare.com; frame-ancestors 'none'; report-uri https://scotthelme.report-uri.com/r/d/csp/enforce
Accept-Ranges: bytes
<!DOCTYPE HTML>
<html>
<head>
<title>HTTP Forever</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="description" content="A site that will always be
Exercice 12
Ajoutez la prise en compte des messages SMTP. Votre sniffer affichera l’adresse email source et les adresses email destinations dès qu’il aura recueilli suffisamment d’information.
De la même manière, voici la procédure dump_smtp qui est appelé selon la condition suivante :
if (sport == 25 || dport == 25 || sport == 587 || dport == 587) {
dump_smtp(payload, payload_len);
}void dump_smtp(char *payload, ssize_t payload_len) {
if (strstr(payload, "MAIL FROM:") || strstr(payload, "RCPT TO:")) {
char *from = strstr(payload, "MAIL FROM:");
if (from) {
printf(" Source email: ");
while (*from && *from != '<')
from++;
if (*from == '<')
from++;
while (*from && *from != '>')
putchar(*from++);
printf("\n");
}
char *to = strstr(payload, "RCPT TO:");
if (to) {
printf(" Destination email: ");
while (*to && *to != '<')
to++;
if (*to == '<')
to++;
while (*to && *to != '>')
putchar(*to++);
printf("\n");
}
}
}Exercice 13
Ajoutez la prise en compte des messages POP3. Votre sniffer affichera le nom d’utilisateur source dès qu’il aura recueilli suffisamment d’information.
if (sport == 110 || dport == 110) {
dump_pop3(payload, payload_len);
}void dump_pop3(char *payload, ssize_t payload_len) {
if (strstr(payload, "USER ")) {
return;
}
char *u = strstr(payload, "USER ");
u += 5;
printf(" POP3: username=");
while (*u && *u != '\r' && *u != '\n')
putchar(*u++);
printf("\n");
}Bien sur, pour tester ces deux protocoles, nous avons besoin de l’accès à un serveur mail donc nous ne sommes pas en mesure de fournir un exemple de ces deux derniers messages.
Exercice 14
Ajoutez la prise en compte des messages DNS. Votre sniffer affichera le nom compris dans la requête faite et l’adresse IP comprise dans la réponse, dès qu’il aura recueilli suffisamment d’information. 2
On notera que les paquets DNS peuvent utiliser les deux protocoles de transport (TPC ou UDP). On retrouve donc l’appel de dump_dns à deux endroits : dump_tcp et dump_udp.
if (sport == 53 || dport == 53) {
dump_dns(payload, payload_len);
}void dump_dns(char *payload, ssize_t payload_len) {
if (payload_len < 12)
return;
char *qname = payload + 12;
printf(" DNS Query: ");
char *p = qname;
while (*p) {
int len = *p++;
for (int i = 0; i < len; i++)
putchar(*p++);
if (*p)
putchar('.');
}
// Print the DNS answer
printf(" DNS Answer: ");
p += 1; // skip null byte
if (p + 4 > payload + payload_len)
return;
unsigned short qtype = ntohs(*(unsigned short *)p);
p += 2;
unsigned short qclass = ntohs(*(unsigned short *)p);
p += 2;
if (qtype == 1) {
printf("Type A, ");
} else if (qtype == 28) {
printf("Type AAAA, ");
} else {
printf("Type %u, ", qtype);
}
if (qclass == 1) {
printf("Class IN\n");
} else {
printf("Class %u\n", qclass);
}
char *ans = p;
ans += 12; // skip TTL and data length
if (ans + 4 > payload + payload_len)
return;
if (qtype == 1) {
struct in_addr addr;
memcpy(&addr, ans, 4);
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, ip_str, sizeof(ip_str));
printf(" IP Address: %s\n", ip_str);
} else if (qtype == 28) {
if (ans + 16 > payload + payload_len)
return;
struct in6_addr addr6;
memcpy(&addr6, ans, 16);
char ip6_str[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &addr6, ip6_str, sizeof(ip6_str));
printf(" IPv6 Address: %s\n", ip6_str);
}
printf("\n");
}Exemple d’affichage d’un paquet DNS :
[2025-11-11 10:54:05.830] - 192.168.1.254 -> 192.168.1.130 103 octets
UDP: ports 53 -> 56530
DNS Query: mail.google.com DNS Answer: Type AAAA, Class IN
IPv6 Address: 2a00:1450:4006:811::2005
Améliorations
Support des IPv6 dans l’affichage des paquets DNS
Nous avons ajouté le support des IPv6, car la plupart des noms de domaines courant pointent vers des IPv6. Pour ce faire, nous avons traité les IP selon le type d’enregistrement DNS : A (qtype == 1) pour les IPv4 et AAAA (qtype == 28) pour les IPv6. Voir le code présenté dans l’Exercice 14.