Bonjour à tous.
Je me propose dans ce tutoriel de vous expliquer, dans un premier temps, comment construire une trame TCP/IP et, dans un deuxième temps, comment la manipuler ; pour illustrer ce dernier point, je vous proposerais de développer un TCP listener (analyseur de réseau).
I. Introduction
Le protocole IP, pour Internet Protocol, est un protocole situé sur la couche réseau (n° 3) du modèle OSI. Il est chargé d'encapsuler sous forme de paquets le flux émis depuis la couche de transport (n° 4), et de les faire transiter ensuite vers la couche de liaison (n° 2).
Le protocole TCP, pour Transmission Control Protocol, est un protocole de transport de données bidirectionnel et contrôlé, d'où sa grade stabilité/fiabilité. Il appartient donc à la couche transport du modèle OSI, et est chargé de segmenter le flux provenant de la couche application (n° 7) en respectant le maximum transmission unit (MTU), c'est à dire la longueur maximum par segment.
Les paquets sont ensuite réunis sous forme de trames par la couche de liaison de données (n° 2) afin de transiter de manière cohérente jusqu'à la couche physique (n° 1). Chaque trame est identifiée par un ensemble de métadonnées lui assurant un transit et un contrôle précis, le système pouvant ainsi vérifier à tout moment l'intégrité de la trame en récupérant ces métadonnées. Ces dernières sont regroupées au sein d'un en-tête généré automatiquement, mais que l'on peut également construire de toute pièce afin de donner à la trame les informations que l'on désire.
II. Manipulation des en-têtes
A) Déclaration des en-têtes
La première étape consiste tout d'abord à déclarer les headers propres aux trames IP et aux trames TCP. Ces en-têtes consistent en l'utilisation d'un objet structure regroupant l'ensemble des informations relatives à la trame.
Header IP :
Header TCP :
Voilà les deux en-tête maintenant déclarés. Chaque paramètre est à partir de maintenant totalement modifiable.
B ) Manipulation des en-têtes
Une fois les headers déclarés, il nous faut pouvoir les manipuler. Pour cela, nous initions une trame et déclarons deux pointeurs de structure relatifs à cette trame.
Déclaration de la trame TCP/IP :
Déclaration des deux pointeurs :
Nous pouvons désormais manipuler les headers et leurs paramètres comme bon nous semble et donc récupérer leurs informations en se servant des pointeurs de structure.
III. Développement
Afin de manipuler les trames, nous utiliserons de nouveau les sockets ; donc si jamais vous n'avez pas encore vu ce tutoriel, je vous conseille fortement de le lire préalablement à l'étude de celui-ci.
Nous allons maintenant voir comment récupérer les informations des trames transitant sur notre réseau.
Pour ce faire, nous devons tout d'abord initialiser l'API WinSock2.
Activation de l'API WinSock2 :
Puis nous devons déclarer l'objet socket que nous utiliserons.
Déclaration du socket :
On notera ici l'utilisation du type RAW pour la socket, nous permettant ainsi de manipuler directement les couches réseaux.
Par la suite, nous devons récupérer l'adresse locale de la machine, c'est une étape un peu complexe au niveau du code utilisé, j'essaierai de détailler au maximum.
Déclaration de la structure nous permettant de récupérer l'adresse locale :
Déclaration du buffer stockant l'adresse locale :
Pour récupérer l'adresse locale, il nous faut d'abord récupérer le nom d'hôte standard de la machine, pour cela nous utilisons la fonction gethostname().
Récupération du nom d'hôte standard :
Maintenant que nous avons le nom d'hôte standard, nous pouvons récupérer directement l'adresse IPv4 locale grâce à la fonction gethostbyname().
Récupération de l'adresse locale :
Ensuite, nous devons initialiser la structure d'adressage (cf : tutoriel sur les sockets).
Déclaration d'une nouvelle structure d'adressage :
Nous remplissons directement les paramètres de la structures grâce à la fonction memcpy().
Copie des éléments structuraux :
Pour stocker l'adresse IP nous allons utiliser un pointeur un peu particulier, le pointeur FAR ; il permet de stocker 32 bits, soient 4 octets.
Déclaration du pointeur sur l'IP :
Ensuite nous stockons l'adresse locale dans le pointeur.
Stockage de l'adresse locale dans *IP :
Pour finir d'initialiser la structure d'adressage, il faut définir la famille d'adressage et copier l'adresse depuis *IP.
Déclaration de la famille d'adressage + copie de l'IP dans la structure :
Bien, maintenant que toutes les initialisations ont été effectuées, il nous faut lier notre socket au réseau, pour cela nous utilisons la fonction bind().
Synchronisation du socket :
Afin de récupérer les trames transitant sur le réseau, nous devons obligatoirement activer le mode PROMISCUOUS, c'est à dire le mode nous permettant d'écouter le réseau sans filtres de contrôles. Pour ce faire, nous utilisons la fonction WSAIoctl().
Activation du mode PROMISCUOUS :
Ce mode étant particulièrement contrôlé par Windows et nécessitant des autorisations spécifiques, il sera nécessaire de d'autoriser notre socket à récupérer ces informations, pour cela nous utilisons le paramètre SIO_RCVALL auquel on donne la valeur de conformité. _WSAIOW(IOC_VENDOR,1)
Initialisation des autorisations (à placer avant la fonction main) :
Nous devons ensuite déclarer deux pointeurs vers les headers précédemment déclarés.
Déclaration des pointeurs de manipulation des headers :
Maintenant que tout est initialisés, nous pouvons rentrer dans la boucle d'écoute du réseau.
Nous devons nous placer en réception de paquets, pour ce faire nous utilisons la fonction recv().
Réception des paquets :
Passons au traitement des données proprement dit. Nous allons récupérer chaque paramètre d'en-tête du paquet grâce aux pointeurs déclarés plus haut afin de les afficher.
Tout d'abord, il nous faut assigner chaque port TCP (destination, source) grâce à la fonction ntohs.
Déclaration des ports :
Assignation des ports d'écoute :
Il ne nous reste plus qu'à récupérer chaque information depuis l'en-tête.
Exemple de récupération de l'adresse source :
Exemple de récupération du checksum :
Vous pouvez maintenant faire de même avec les paramètres que vous souhaitez récupérer.
[spoiler]
[/spoiler]
Ne pas oublier de fermer la boucle, et d'ensuite libérer la stocket et désactiver l'API WinSock2.
Nettoyage :
Rappel des headers à inclure :
Rappel des constantes à définir avant le main :
Voilà, il nous vous reste plus qu'à assembler tous les bouts de code et vous obtiendrez un joli TCP listener.
Je me propose dans ce tutoriel de vous expliquer, dans un premier temps, comment construire une trame TCP/IP et, dans un deuxième temps, comment la manipuler ; pour illustrer ce dernier point, je vous proposerais de développer un TCP listener (analyseur de réseau).
I. Introduction
Le protocole IP, pour Internet Protocol, est un protocole situé sur la couche réseau (n° 3) du modèle OSI. Il est chargé d'encapsuler sous forme de paquets le flux émis depuis la couche de transport (n° 4), et de les faire transiter ensuite vers la couche de liaison (n° 2).
Le protocole TCP, pour Transmission Control Protocol, est un protocole de transport de données bidirectionnel et contrôlé, d'où sa grade stabilité/fiabilité. Il appartient donc à la couche transport du modèle OSI, et est chargé de segmenter le flux provenant de la couche application (n° 7) en respectant le maximum transmission unit (MTU), c'est à dire la longueur maximum par segment.
Les paquets sont ensuite réunis sous forme de trames par la couche de liaison de données (n° 2) afin de transiter de manière cohérente jusqu'à la couche physique (n° 1). Chaque trame est identifiée par un ensemble de métadonnées lui assurant un transit et un contrôle précis, le système pouvant ainsi vérifier à tout moment l'intégrité de la trame en récupérant ces métadonnées. Ces dernières sont regroupées au sein d'un en-tête généré automatiquement, mais que l'on peut également construire de toute pièce afin de donner à la trame les informations que l'on désire.
II. Manipulation des en-têtes
A) Déclaration des en-têtes
La première étape consiste tout d'abord à déclarer les headers propres aux trames IP et aux trames TCP. Ces en-têtes consistent en l'utilisation d'un objet structure regroupant l'ensemble des informations relatives à la trame.
Header IP :
Code:
typedef struct iphdr iphdr; struct iphdr { unsigned char IHL:4; unsigned char Version :4; // 4-bit IPv4 version unsigned char TypeOfService; // type du service standard unsigned short TotalLength; // taille totale de la trame unsigned short ID; // identifiant unique unsigned char FlagOffset :5; // déclarant de l'offset unsigned char MoreFragment :1; unsigned char DontFragment :1; unsigned char ReservedZero :1; unsigned char FragOffset; //offset du fragment unsigned char Ttl; // Time to live (temps de vie) unsigned char Protocol; // protocole d'échange (TCP,UDP etc) unsigned short Checksum; // IP checksum unsigned int Source; // adresse source unsigned int Destination; // adrsse de destination }IP_HDR;
Header TCP :
Code:
typedef struct tcphdr tcphdr; struct tcphdr { unsigned short PortSource; // port source unsigned short PortDest; // port de destination unsigned int seqnum; // numéro du segment unsigned int acknum; // numéro du flag ACK unsigned char unused:4, tcp_hl:4; unsigned char flags; // flags optionnels unsigned short window; unsigned short checksum; // séquence checksum unsigned short urgPointer; // indicateur d'urgence } TCP_HDR;
Voilà les deux en-tête maintenant déclarés. Chaque paramètre est à partir de maintenant totalement modifiable.
B ) Manipulation des en-têtes
Une fois les headers déclarés, il nous faut pouvoir les manipuler. Pour cela, nous initions une trame et déclarons deux pointeurs de structure relatifs à cette trame.
Déclaration de la trame TCP/IP :
Code:
char trame[2046];
Code:
iphdr *HeaderIP=(iphdr*)trame; tcphdr *HeaderTCP=(tcphdr*)(sizeof(iphdr)+trame);
Nous pouvons désormais manipuler les headers et leurs paramètres comme bon nous semble et donc récupérer leurs informations en se servant des pointeurs de structure.
III. Développement
Afin de manipuler les trames, nous utiliserons de nouveau les sockets ; donc si jamais vous n'avez pas encore vu ce tutoriel, je vous conseille fortement de le lire préalablement à l'étude de celui-ci.
Nous allons maintenant voir comment récupérer les informations des trames transitant sur notre réseau.
Pour ce faire, nous devons tout d'abord initialiser l'API WinSock2.
Activation de l'API WinSock2 :
Code:
WSADATA WSADatas; if(WSAStartup(MAKEWORD(2,2), &WSADatas) != 0) { printf("WSA failed to initialize -> WSAStartup() : %d\n\n", WSAGetLastError()); return 1; }
Puis nous devons déclarer l'objet socket que nous utiliserons.
Déclaration du socket :
Code:
SOCKET iSocket; if((iSocket = socket(AF_INET, SOCK_RAW, IPPROTO_IP)) == INVALID_SOCKET) { // printf("Socket failed to initialize -> socket() : %d\n\n", WSAGetLastError()); return 1; }
Par la suite, nous devons récupérer l'adresse locale de la machine, c'est une étape un peu complexe au niveau du code utilisé, j'essaierai de détailler au maximum.
Déclaration de la structure nous permettant de récupérer l'adresse locale :
Code:
struct hostent *get_addr;
Code:
char addr_buffer[64];
Pour récupérer l'adresse locale, il nous faut d'abord récupérer le nom d'hôte standard de la machine, pour cela nous utilisons la fonction gethostname().
Récupération du nom d'hôte standard :
Code:
gethostname(addr_buffer, sizeof(addr_buffer)); // le nom est copié dans le buffer
Maintenant que nous avons le nom d'hôte standard, nous pouvons récupérer directement l'adresse IPv4 locale grâce à la fonction gethostbyname().
Récupération de l'adresse locale :
Code:
get_addr=gethostbyname(addr_buffer);
Ensuite, nous devons initialiser la structure d'adressage (cf : tutoriel sur les sockets).
Déclaration d'une nouvelle structure d'adressage :
Code:
SOCKADDR_IN iSocketAddr;
Copie des éléments structuraux :
Code:
memcpy(&iSocketAddr.sin_addr.s_addr, get_addr->h_addr, get_addr->h_length);
Déclaration du pointeur sur l'IP :
Code:
char FAR *IP;
Stockage de l'adresse locale dans *IP :
Code:
IP = inet_ntoa(iSocketAddr.sin_addr);
Déclaration de la famille d'adressage + copie de l'IP dans la structure :
Code:
iSocketAddr.sin_family = AF_INET; iSocketAddr.sin_addr.s_addr = inet_addr(IP);
Bien, maintenant que toutes les initialisations ont été effectuées, il nous faut lier notre socket au réseau, pour cela nous utilisons la fonction bind().
Synchronisation du socket :
Code:
if(bind(iSocket, (SOCKADDR*)&iSocketAddr, sizeof(iSocketAddr)) == SOCKET_ERROR) { printf("Unable to listening network -> bind() : %d\n\n", WSAGetLastError()); closesocket(iSocket); return 1; }
Afin de récupérer les trames transitant sur le réseau, nous devons obligatoirement activer le mode PROMISCUOUS, c'est à dire le mode nous permettant d'écouter le réseau sans filtres de contrôles. Pour ce faire, nous utilisons la fonction WSAIoctl().
Activation du mode PROMISCUOUS :
Code:
unsigned int inDatas; // pointe vers les données en entrée DWORD dwBytesRet; // pointe vers le nombre de bytes en sortie WSAIoctl(sock,SIO_RCVALL,&inDatas,sizeof(inDatas),NULL,0,&dwBytesRet,NULL,NULL);
Initialisation des autorisations (à placer avant la fonction main) :
Code:
#define SIO_RCVALL _WSAIOW(IOC_VENDOR,1)
Nous devons ensuite déclarer deux pointeurs vers les headers précédemment déclarés.
Déclaration des pointeurs de manipulation des headers :
Code:
char trame[2048]; iphdr *HeaderIP=(iphdr*)trame; tcphdr *HeaderTCP=(tcphdr*)(sizeof(iphdr)+trame);
Maintenant que tout est initialisés, nous pouvons rentrer dans la boucle d'écoute du réseau.
Code:
for(;;) {
Nous devons nous placer en réception de paquets, pour ce faire nous utilisons la fonction recv().
Réception des paquets :
Code:
recv(iSocket, trame, sizeof(trame), 0);
Passons au traitement des données proprement dit. Nous allons récupérer chaque paramètre d'en-tête du paquet grâce aux pointeurs déclarés plus haut afin de les afficher.
Tout d'abord, il nous faut assigner chaque port TCP (destination, source) grâce à la fonction ntohs.
Déclaration des ports :
Code:
unsigned short portDest, portSrc;
Code:
portSrc = ntohs(HeaderTCP->PortSource); portDest = ntohs(HeaderTCP->PortDest);
Il ne nous reste plus qu'à récupérer chaque information depuis l'en-tête.
Exemple de récupération de l'adresse source :
Code:
char ip[64]; sprintf(ip,"%s:%d",inet_ntoa(*(struct in_addr *)&HeaderIP->Source), portSrc);
Code:
printf("Checksum : %d -> 0x%x", HeaderIP->Checksum, HeaderIP->Checksum);
Vous pouvez maintenant faire de même avec les paramètres que vous souhaitez récupérer.
[spoiler]
Code:
sprintf(ip,"%s:%d",inet_ntoa(*(struct in_addr *)&HeaderIP->Source), portSrc); printf("\n-> IP Source : %s",ip); sprintf(ip,"%s:%d",inet_ntoa(*(struct in_addr *)&HeaderIP->Destination), portDest); printf("\n-> IP Destination : %s",ip); printf("\n-> Version : %d -> 0x%x", HeaderIP->Version, HeaderIP->Version); printf("\n-> Checksum : %d -> 0x%x", HeaderIP->Checksum, HeaderIP->Checksum); printf("\n-> Protocole : %d -> 0x%x", HeaderIP->Protocol, HeaderIP->Protocol);
Ne pas oublier de fermer la boucle, et d'ensuite libérer la stocket et désactiver l'API WinSock2.
Nettoyage :
Code:
} // fin boucle for closesocket(iSocket); WSACleanup(); return EXIT_SUCCESS; // fin fonction main() }
Rappel des headers à inclure :
Code:
#include <winsock2.h> #include <windows.h> #include <stdio.h>
Code:
#define SIO_RCVALL _WSAIOW(IOC_VENDOR,1) #define RCVALL_ON 1 #define RCVALL_OFF 0
Voilà, il nous vous reste plus qu'à assembler tous les bouts de code et vous obtiendrez un joli TCP listener.