Programmation Winsock avancée avec les systèmes NT


  1. Introduction
  2. Une des raisons importantes pour laquelle j'ai décidé d'écrire cet article est le manque de documentation en Français à ce sujet sur Internet, j'espère que ce présent article participera à la disparition de ce problème. Dans celui-ci nous étudierons l'utilisation de l'API Winsock (Winsock 1.1 en ce qui concerne les sockets streams et plus particulièrement Winsock 2 pour les raw sockets). La connaissance de cette API permettra de créer des applications TCP basiques (client, serveur) jusqu'à des applications plus complexes ayant comme possibilitées la modification de l'en tête des datagrammes réseaux. Ce qui permettra de créer des réalisations un peu plus attrayantes tel qu'un forgeur de paquets, un scanneur de ports avancé, ou encore même un sniffer ! Et tout ceci sera accompli sans aucune des librairies distribuées sur internet tel que winpcap, ou encore la librairie de chez Komodia ! Et oui, malheureusement, une multitude de personnes pensent qu'il n'est pas possible de travailler à bas niveau avec les protocoles IP, TCP et autres sans librairies externes ou encore sans écrire un driver (NDIS) avec le DDK (Driver Development Kits) de chez Microsoft, ce qui est totalement faux et c'est bien ce que je vais démontrer par ce présent article. Je ne remettrai pas en cause l'écriture d'un driver NDIS qui, malgrès sa complexité, offre beaucoup plus de possibilitées. Mais là n'est pas notre objectif.

  3. Winsock 2 et architecture
  4. Nous avons pu observer que la version 1.1 de Winsock (winsock.dll ou encore wssock32.dll) présente dans les précédents systèmes d'exploitation de chez Microsoft était assez 'limitée', il n'était en aucun cas possible de modifier les datagrammes réseau en utilisant celle-ci, ce qui réduisait considérablement les possibilités. Mais ce n'est pas du tout la même situation en ce qui concerne Winsock 2, celui ci fournit beaucoup plus de flexibilité, présent sur la nouvelle génération des systèmes d'exploitation de Microsoft il va permettre contrairement à son prédécesseur de fournir une multitude de nouveaux services comme la modification de l'en-tête des datagrammes réseau ou encore des fonctions de debuggage et de tracing qui faciliteront la recherche des problèmes réseaux et bien plus encore. Il va réaliser toutes ces tâches grâce à une nouvelle architecture qui contient comme toujours une dll (ws2_32.dll) placée sous les dll de l'API Winsock 1.1 pour assurer une compatibilité avec les programmes utilisant Winsock dans sa verion 1.1, mais plus encore, et c'est bien ici qu'intervient la puissance de Winsock 2, celui ci va directement placer une couche à plus bas niveau (Winsock 2 SPI), celle ci permettra d'intervenir directement sur les couches réseau. Je pense vous éclaircir les idées grâce à ce petit schéma.

         +---------------+ +-----------------++------------------+    
         |   Winsock 2   | | 16-bit Winsock  || 32-bit Winsock   |
         |  application  | | 1.1 application || 1.1 application  |
         +---------------+ +-----------------++------------------+
                 |                 |                  |
                 |         +-----------------++------------------+ <--- Winsock 1.1 API
                 |         |   Winsock.dll   ||  wssock32.dll    |
                 |         |    (16-bit)     ||   (32-bit)       |
                 |         +-----------------++------------------+
                 |                                    |
         +-------------------------------------------------------+ <--- Winsock 2.0 API
         |                  ws2-32.dll (32-bit)                  |
         +-------------------------------------------------------+
                                    |
         +-------------------------------------------------------+ <--- Winsock 2.0 SPI
         |+-----------++--------------++------------++----------+|     /
         ||  TCP/IP   || TCP/IP-based || additional ||layered sp||    /
         || transport ||   namespace  ||   service  |+----------+|   /
         ||  service  || service pro- ||  providers |+----------+| <- 
         ||  provider || vider (dns)  || (spx, etc.)|| any tsp  ||
         |+-----------++--------------++------------++----------+|
         +-------------------------------------------------------+
    

    Note : Winsock 2 n'est pas disponible sur les systèmes NT 3,51 ou les systèmes précédents celui-ci.
    Remarque : Vous pouvez télécharger la version 2 de Winsock pour Windows 95 à cette adresse : ftp://ftp.microsoft.com/bussys/Winsock/Winsock2/

  5. Les Sockets Streams
  6. Nous allons commencer par cette petite introduction sur les sockets streams pour que vous puissiez prendre conscience des différentes fonctions que peut fournir la librairie Winsock. Seulement après cette parenthèse nous nous attaquerons à la programmation des raw sockets grâce à Winsock 2 sous Windows 2000 ou encore Windows XP.

    1. Qu'est ce qu'un socket ?
    2. Un socket est une sorte de point de communication, il est défini par une adresse réseau (IP) et un numéro de port qui est associé à une application sur une machine. Celui-ci va permettre une communication entre deux processus locaux ou distants.

    3. Programmation d'un serveur TCP
    4. Dans cette sous partie nous allons étudier la création d'un petit serveur qui ne fera que réacheminer à son destinataire le message que celui-ci aura précédemment envoyé (écho). Nous utiliserons Winsock 1.1 pour cette réalisation, car la version 2 ne serait pas utile en ce qui concerne cette application, ceci permettra une meilleur compatibilité avec les systèmes ne supportant pas Winsock 2. Etudions la structure de fonctionnement général d'un serveur :

                                     +---------------------------+
                                     | initialisation de Winsock |
                                     +---------------------------+
                                                  |
                                                  |
                 +-----------------------------------------------------------------+
                 | Création et attachement du socket d'écoute ( socket(), bind() ) |
                 +-----------------------------------------------------------------+
                                                  |
                                                  |
                                 +-----------------------------------+
                                 | Ouverture du service ( listen() ) |
                                 +-----------------------------------+
                                                  |
                                                  |
                           +------------------------------------------------+
                     +---->| Attente des demandes de connexion ( accept() ) |
                     |     +------------------------------------------------+
                     |                            |
                     |                            |
                     |      Client =>   | Demande de connexion |
                     |                            |
                     |                            |
                     |        +----------------------------------------+
                     |        | Traitement de la demande de connection |
                     |        +----------------------------------------+
                     |                            |
                     |                            |
                     |     | Système de multithreading si le server gère |
                     |     |        plusieurs clients en même temps      |
                     |                            |
                     |                            |
                     +----------------------------+
      

    5. Les fichiers nécessaires
    6. Pour pouvoir utiliser Winsock 1.1, il vous faudra plusieurs fichiers que je citerai ci-dessous :

      - winsock.h
      - wsock32.lib

      Ces fichiers sont tous deux présents dans Visual C++ 6 ("vc98/include/" pour winsock.h ou encore "vc98/lib" pour wsock32.lib). Si encore une fois vous travaillez sous Visual C++ 6 vous devrez inclure la librairie wsock32.lib dans votre projet, pour ceci : projects => settings => link et dans cet onglet vous verrez "objets/library modules", vous devez ajouter dans cette case wsock32.lib. Si vous ne travaillez pas avec cette IDE, une autre solution est de charger les fonctions de la librairie dynamiquement, mais ce n'est pas le sujet de cet article. (Fonctions : "LoadLibrary", "GetProcAddress")

    7. Initialisation de l'API Winsock 1.1
    8. Afin de pouvoir utiliser Winsock, il faut l'initialiser, pour ceci une fonction existe : WSAStartup();

      Prototype : int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

      Ce prototype nous indique que nous devons passer un mot en premier argument et l'adresse d'une structure de type WSADATA en second argument.

      Exemple :

      int InitWinsock(void)
      {
       	WORD wVersionRequested; 
       	WSADATA WSAData; 			       /* structure WSADATA définie dans winsock.h */
       	int err;
      	  	
       	wVersionRequested = MAKEWORD(1,1); 	       /* 1.1 version voulut de Winsock */  	
       	err = WSAStartup(wVersionRequested, &WSAData); /* appel de notre fonction */	  	
        					               /* controle d'erreur */
        	if(err == 0)	fprintf(stdout, "[*] Winsock a ete initialise avec succes.\n");
      	else {
      		fprintf(stderr, "Erreur lors de l'initialisation de Winsock, code d'erreur : %d\n", GetLastError());
      		return(-1);
      	}
      	
      	return(0);
      }
      

      Maintenant nous pouvons utiliser les fonctions de la librairie Winsock 1.1 sans problème particulier.

    9. Création du socket
    10. Maintenant que l'API Winsock est initialisée nous allons pouvoir créer notre socket, pour réaliser ceci nous devons remplir plusieurs tâches :

      - Déclarer notre socket
      - Déclarer une structure sockaddr_in définie encore une fois dans winsock.h
      - Remplir cette structure avec les options voulues
      - Créer notre socket grâce à la fonction socket();
      - Associer notre adresse locale au socket créé grâce à la fonction bind();

      Prototypes : SOCKET socket(int af, int type, int protocol);
      	     int bind (socket s, const struct sockaddr FAR * addr, int namelen);
      +------------------------------------------------+
      | SOCKET socket(int af, int type, int protocol); |
      +------------------------------------------------+
      

      Cette fonction va permettre de créer notre socket pour ensuite travailler sur celui-ci. Le premier paramètre définit la famille du socket, vous trouverez d'ailleurs les différentes familles existantes dans winsock.h, nous utiliserons AF_INET car cette famille représente les communications Internetwork: UDP, TCP, etc. Le second paramètre définit le type du socket, toujours une définition de tous les types existants dans winsock.h, nous nous contenterons du type SOCK_STREAM car c'est bien ce que nous recherchons, un socket stream. Enfin le troisième type représente le protocole voulu, nous utiliserons IPPROTO_IP.

      +---------------------------------------------------------------------+
      | int bind(SOCKET s, const struct sockaddr FAR * addr, int namelen);  |
      +---------------------------------------------------------------------+
      

      Cette fonction va associer notre adresse locale au socket passé en argument. Le premier paramètre n'a normalement plus de secrets pour vous, c'est un socket. Le second paramètre est l'adresse d'une structure de type sockaddr, celle que l'on a remplie juste avant l'utilisation de la fonction socket() d'ailleurs. Et, pour finir, le dernier paramètre est la taille de cette même structure donc nous nous contenterons d'un simple sizeof(struct sockaddr_in);.

      Exemple :

      SOCKET CreateSocket(void)
      {
        	SOCKET sock;					 /* déclaration de notre socket */
        	struct sockaddr_in sin;				 /* déclaration de la structure sockaddr_in */
      	 
        	memset(&sin, 0x0, sizeof(struct sockaddr_in));
       	sin.sin_addr.s_addr	= htonl(inaddr_any);  	 /* définis l'adresse du server */
        	sin.sin_family	        = AF_INET;		 /* famille du socket */
        	sin.sin_port		= htons(1337);           /* port sur lequel va etre assigner le server */
      	 
        	sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); /* appel de la fonction socket */
        	
        	if(sock != INVALID_SOCKET)	fprintf(stdout, "[*] Socket cree avec succes.\n");
        	else {
       		fprintf(stderr, "Erreur lors de la creation du socket, code d'erreur : %d\n", WSAGetLastError());
       		return(-1);
        	}
        	          	
         	if(bind(sock, (sockaddr *)&sin, sizeof(struct sockaddr_in)) != SOCKET_ERROR) /* associe l'adresse local au socket */
      	  	fprintf(stdout, "[*] Adresse local associee au socket avec succes.\n");
      	else {
      	   	fprintf(stderr, "Erreur lors de l'association de l'adresse local au socket, code d'erreur : %d\n", WSAGetLastError());
      	   	return(-1);
      	}
      	
      	return(sock); /* retourne le socket */
      }
      

      Si vous êtes assez observateur vous avez pu remarquer que j'utilise une fonction nommée htons(); pour passer le port sur lequel va écouter le serveur à ma structure "sin". Pourquoi est-ce que j'ai utilisé cette fonction me direz vous? En fait, c'est un problème auquel sont confrontés tous les ordinateurs communiquants entre eux sur le réseau : L'ordre des octets. Pour stocker en mémoire une valeur tenant sur 2 octets, les processeurs le font différemment selon leur architecture. Certains placent en première position l'octet de poids faible pour mettre en seconde position l'octet de poids fort , cette organisation est nommée "little endian". D'autres processeurs font autrement et rangent d'abord l'octet de poids fort, suivi de celui de poids faible, ceux-ci sont qualifiés de "big endian".

      Stockage en mémoire de la valeur : 0x1425
                
                   big endian                    	 little endian
                +--------------+            		+-------------+
                | 0x14, | 0x25 +            		| 0x25 | 0x14 |
                +--------------+            		+-------------+
      

      La librairie Winsock met à notre disposition quatre fonctions permettant de transformer un entier long ou court depuis l'ordre des octets de l'hôte vers celui du réseau, et inversement. Toutes ces routines sont encore une fois déclarées dans winsock.h :

      Prototypes : u_long  htonl(u_long  hostlong);
      	     u_short htons(u_short hostshort);
      	     u_long  ntohl(u_long  netlong);
      	     u_short ntohs(u_short netshort);
      

      Je ne vais pas les décrire une par une, mais je vous conseille d'aller jeter un oeil dans ce que tout bon programmeur Windows se doit de posséder : "Win 32 Programmer's Reference".
      Toutes ces fonctions y sont très bien détaillées.

    11. Ouverture du service et attente d'un client
    12. Notre socket est créé, maintenant nous allons le mettre en écoute et attendre la connexion d'un client. Pour réaliser ceci nous allons utiliser la fonction listen(), cette étape effectuée nous attendrons la connexion d'un client sur notre server. Une fois cette condition remplie, nous nous servirons de la fonction accept() qui permettra de gérer la demande du client.
      Donc si nous récapitulons dans un ordre logique cela donne :

      - Passer le socket en mode écoute ( listen() )
      - Attendre une connexion cliente et la gérer ( accept() )

      prototypes : int listen(SOCKET s, int backlog);
                   socket accept(SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen);
      			
      
      +------------------------------------+
      | int listen(SOCKET s, int backlog); |
      +------------------------------------+
      

      En ce qui concerne le second argument, je pense ne pas avoir besoin de détailler, c'est ce que l'on appelle le backlog, cela correspond en fait au nombre maximum de connexions qui seront ecoutées en même temps par le serveur. Passons à l'étude de la fonction accept();.

      +------------------------------------------------------------------------+
      | socket accept(SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen); |
      +------------------------------------------------------------------------+
      

      Cette fonction va nous permettre de traiter la ou les connexions clientes. Pour étudier celles-ci je passerai directement au second paramètre qui est en fait l'adresse d'une structure de type sockaddr dans laquelle la fonction se chargera de stocker les informations sur le client en cas de connexion, le dernier paramètre est la taille de cette structure que nous définirons par une simple sizeof(struct sockaddr);.

      Maintenant passons au code.

      Exemple :

      int Server(SOCKET server_socket)
      {
        	SOCKET client_socket;
        	struct sockaddr_in adresse;
      	int adresse_size = 0;
      	
      	memset(&adresse, 0, sizeof(struct sockaddr_in));
        	
        	/* passe en mode écoute le serveur */
        	if(listen(server_socket, 0) == 0)	fprintf(stdout, "[*] Serveur passe en mode ecoute avec succes.\n");
        	else {
        		fprintf(stderr, "Erreur lors du passage en mode ecoute du serveur, code d'erreur : %d\n", WSAGetLastError());
        		return(-1);
        	}	
        	
        	while(1) {
      		adresse_size = sizeof(adresse);
      		/* accept les connexions clientes */
      		client_socket = accept(server_socket, (struct sockaddr *)&adresse, &adresse_size); 
      		if(client_socket != INVALID_SOCKET) fprintf(stdout, "[*] Client accepte avec succes\n");
      	 	else {
      	  		fprintf(stderr, "erreur lors de l'acceptation du client, code d'erreur : %d\n", WSAGetLastError());
      	 		return(-1);
      		}   		
      	}
      	   	
      	return(0);
      }
      

    13. Gestion de la connexion cliente
    14. Maintenant que notre client est connecté et que le serveur l'a accepté, il va falloir réaliser une action pour rendre ce serveur un peu plus attrayant aux yeux de notre cher client. Pour ceci, le but est d'attendre que le client nous envoit des données pour qu'ensuite le serveur réachemine ces mêmes données à ce même client et ceci sera réalisé à chaque données envoyées par le client (écho).
      Comme précédemment nous allons récapituler dans un ordre précis toutes ces actions :

      	  
      	   +--- Attendre que le client envois des données ( recv() )
      	   |    | -> Données reçues
      	   |   - Renvoyer les mêmes données à ce client ( send() )
      	   |    |
      	   +----+
      	   
      prototypes : int recv(SOCKET s, char FAR * buf, int len, int flags);	
      	     int send(SOCKET s, const char FAR * buf, int len, int flags);
      	  	      
      +---------------------------------------------------------+
      | int recv(SOCKET s, char FAR * buf, int len, int flags); |
      +---------------------------------------------------------+
      

      Cette fonction va permettre de recevoir des données par l'intermédiaire du socket passé en argument. Ceci est très utile quand on veut communiquer avec le client, souvent même indispensable. Maintenant passons en revue les arguments, le premier argument est le socket sur lequel nous voulons lire les données reçues, le second est un buffer de type char * dans lequel seront stockées les données reçues, le troisième argument est la longueur du buffer précédemment utilisé, et pour finir le quatrième argument est un peu obsolète, nous le laisserons de coté pour lui attribuer la valeur 0.

      +---------------------------------------------------------------+
      | int send(SOCKET s, const char FAR * buf, int len, int flags); |
      +---------------------------------------------------------------+
      

      La fonction send() est indispensable, celle-ci va nous permettre d'envoyer des données soit en tant que client au serveur ou inversement en tant que serveur vers le client. Les arguments sont les mêmes que pour la fonction recv(), sauf pour le buffer, celui-ci, au lieu de se voir attribuer les données reçues devra contenir ce que vous voulez envoyer.

      Note : Ces deux fonctions renvoient, en cas de succès, le nombre d'octets envoyés pour la fonction send() ou reçus pour la fonction recv().

      Maintenant que nous avons pris conscience de l'utilité de ces fonctions, passons à l'application ; le code.

      exemple :
      
      
      void traite_connexion(SOCKET socket_client)
      {
        	int ret;
        	char *buffer = NULL;
      	  	
        	buffer = (char *)malloc(1024 * sizeof(char)); 			 /* allocation de mémoire pour le buffer qui va
      	  							           recevoir les données */
        	if(buffer == NULL) {
          		fprintf(stderr, "Erreur d'allocation memoire.\n");
        		exit(-1);
        	}
        		  	
        	while(1) {
      		ret = recv(socket_client, buffer, 1024, 0);		/* reception et stockage des données dans buffer */
        		if(ret != SOCKET_ERROR)	fprintf(stdout, "[*] Donnees recu.\n");
        		else {
        			fprintf(stderr, "Erreur lors de la reception des donnees, code d'erreur : %d\n", WSAGetLastError());
      			break;
      		}
      
      		buffer[ret] = '\0';					/* ajout du caractère de fin pour le strlen() qui va suivre */
      	  		  		
        		ret = send(socket_client, buffer, strlen(buffer), 0);	/* envois du contenu de buffer au client */
        		if(ret != SOCKET_ERROR)	fprintf(stdout, "[*] Donnees envoyer.\n");
        		else {
        			fprintf(stderr, "Erreur lors de l'envoi des donnees, code d'erreur : %d\n", WSAGetLastError());
      			break;
      		}
      	}	
      	
      	closesocket(socket_client);					/* ferme le socket gerant la connexion cliente */
        	free(buffer);	  						/* libère la mémoire allouée pour notre buffer */
      }
      

      Voila c'est terminé en ce qui concerne ce serveur "écho", j'espère que cela a pu vous éclaircir les idées sur le concept de serveur et encore mieux sur sa programmation grâce à la bibliothéque Winsock.
      Vous pourrez trouver le code source complet de ce serveur ici : echoserver.c

    15. Programmation d'un client TCP
    16. Après vous avoir montré comment programmer un serveur grâce à Winsock, je vais ici vous apprendre à créer un client TCP vraiment basique qui permettra de dialoguer avec le précédent serveur. Etudions la structure d'un client de ce type.

                                     +---------------------------+
                                     | Initialisation de Winsock |
                                     +---------------------------+
                                                  |
                                                  |
                                 +---------------------------------+
                                 | Création du socket ( socket() ) |
                                 +---------------------------------+
                                                  |
                                                  |
                                     +--------------------------+
                                     | Connexion  ( connect() ) |
                                     +--------------------------+
                                                  |
                                                  |
                     +--------------------------------------------------------------+
                     | Gestion de diverses manières de la connexion ( recv(), send() ) |
                     +--------------------------------------------------------------+
      

      Pour réaliser ce modèle vous devrez prendre connaissance d'une seule nouvelle fonction, car vous connaissez déjà la plupart de celles nécessaires au client grâce au travail effectué avec le serveur. Cette fonction est la fonction connect(); . Etudions donc celle-ci.

      Prototype : int connect(SOCKET s, const struct sockaddr FAR * name, int namelen);

      +-----------------------------------------------------------------------+
      | int connect(SOCKET s, const struct sockaddr FAR * name, int namelen); |
      +-----------------------------------------------------------------------+
      

      Cette fonction va nous permettre d'établir un lien avec le serveur, une connexion plus précisément. Ce qui, par la suite, nous permettra de dialoguer avec l'application serveur. Elle prend comme premier paramètre le socket avec lequel vous voulez établir la connexion, le second paramètre est l'adresse d'une structure de type sockaddr, et enfin le dernier paramètre représente la taille de cette structure.

      Exemple :

      	SOCKET sock;
      	struct sockaddr_in sin;
      
      
      	sock = socket(AF_INET, SOCK_STREAM, 0);
      	
      	memset(&sin, 0x0, sizeof(struct sockaddr_in));
      	sin.sin_addr.s_addr	= inet_addr("127.0.0.1");
      	sin.sin_family		= AF_INET;
      	sin.sin_port		= htons(1337);
      		  
      	connect(sock, (sockaddr *)&sin, sizeof(struct sockaddr_in)); 
      	 
      

      Je n'incluerais pas le code du client dans cette section, cela ne ferait que l'alourdir... Il est disponible ici : TCPclient.c

    17. Conclusion

    Cette introduction aux sockets streams est terminée, maintenant passons à une notion plus intéressante : Les raw sockets.

  7. Les raw sockets
    1. Introduction
    2. Nous allons ici aborder un sujet un peu plus complexe et plus intérressant que les sockets streams, je parle bien sûr des raw sockets. Par l'intermédiaire de ceux-ci nous apprendrons comment forger nos propres paquets que cela soit TCP, ICMP où autres. Nous analyserons de même la manière de récupérer le flux transitant sur nos interfaces réseau, toujours uniquement grâce à Winsock 2. Donc, logiquement, après avoir lu cette section vous serez capable d'écrire votre propre sniffer ou encore votre forgeur de paquets personnel, ce qui peut être très interressant.

    3. Qu'est ce qu'un raw socket ?
    4. Un raw socket est un socket avec un pouvoir 'supérieur', il permet d'écrire ou de lire des paquets sur une interface réseau, avec les paramètres souhaités. c'est à dire qu'il aura la possibilité de modifier l'en-tête d'un paquet pour ensuite l'envoyer avec les options de ce même en-tête modifié (adresse IP source, flag TCP, etc).

    5. L'accès aux raw sockets
    6. Pour utiliser les raw sockets, nous allons devoir remplir certaines conditions, tout d'abord, nous devrons avoir en notre possession les droits du super utilisateur de la machine concernée. Si ce n'est pas le cas, nous allons déclencher une erreur de type WSAEACCESS lors de la tentative de création de notre raw socket.
      Nous pouvons remarquer que cette restriction peut être contournée selon plusieurs méthodes :

      - Avec Windows XP Home Edition chaque programme s'executant obtient par defaut les privilèges du super utilisateur, ce qui réduit donc à néant la sécurité d'accès au niveau des raw sockets.
      - Sur Windows NT4, il est possible de neutraliser ce contrôle en modifiant une certaine clef de la base des registres, que je citerai ci-dessous.

      HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Afd\Parameters\DisableRawSecurity
      Nous devons donc modifier cette valeur DWORD pour la fixer à 1 et donc désactiver le système de contrôle. Pour valider cette modification, un redémarrage sera nécessaire.

      Avec Windows 2000, à ma connaissance aucune technique "simple" ne permet de neutraliser ce contrôle d'accès.

      Maintenant nous allons étudier les différents moyens possibles pour renforcer cette sécurité de contrôle d'accès aux raw sockets et donc éviter qu'un utilisateur mal intentionné accède à ce type de socket et fasse des dégâts.
      Donc notre but est de bloquer l'accès à ces raw sockets, pour ceci plusieurs logiciels existent, j'ai pu en remarquer 2 développés par Gibson Research qui sont libres d'emploi. L'un permet de vérifier où en est votre système au niveau du contrôle d'accès aux raw sockets et l'autre permet de bloquer à TOUS les utilisateurs présents sur la machine l'utilisation de ce type de socket grâce à l'installation d'un nouveau driver système (KDM).
      Tout d'abord étudions le premier, il se nomme SocketToMe, il va donc nous permettre d'étudier les droits de l'utilisateur lançant ce programme concernant l'accès aux raw socket. Vous pourrez le trouver ici
      Etudions maintenant le second, celui ci permet de bloquer à TOUS les utilisateurs présents sur le système l'accès aux raw sockets. Ce second utilitaire se nomme, "SocketLock", il est disponible ici.

    7. Les différents protocoles
    8. Pour pouvoir jouer avec l'en-tête des datagrammes il va déjà falloir connaitre à quoi celui-ci correspond. Je vais essayer ici de vous décrire les options des protocoles de base comme le protocole IP, TCP, ou encore ICMP. Pour forger votre propre en-tête, vous allez tout simplement devoir remplir les structures représentant l'en-tête des datagrammes voulus.

      1. Le protocole IP (Internet Protocol)
      2. Ce protocole joue un rôle majeur sur Internet. Celui-ci assure sans connexion un service non fiable de délivrance de paquets IP. Ce service est dit non fiable car il n'assure pas la délivrance de chaque paquet à l'hôte de destination, ces paquets peuvent très bien être perdus, retardés, dupliqués, altérés, ou encore mis dans le désordre. Et aucune option dans le paquet n'indique ce genre de problèmes. En plus concis, nous pouvons dire que ce protocole gère le routage sur Internet.

        Voici à quoi ressemble ce type de datagramme :

        <----------------------------------- 32 bits ----------------------------------->
        version
        longueur d'en-tête
        type de service
        longueur totale
        identification
        drapeau
        décalage fragment
        durée de vie
        protocole
        somme de contrôle en-tête (checksum)
        adresse IP source
        adresse IP destination
        données

        Etudions ensemble chacune des options présentes dans ce datagramme.

        version : ce champ définie la version du protocole IP voulu, actuellement la version la plus courante est la 4 (IPv4), elle est codée sur 4 bits. Si ce champ n'est pas initialisé avec une valeur correcte, le paquet est tout simplement rejeté.
        longueur d'en-tête : il s'agit du nombre de mots de 32 bits sur lesquels sont répartis l'en-tête, toujours codé sur 4 bits.
        type de service (tos: type of service) : ce champ indique la façon dont les packets doivent être traités. Il est codé sur 8 bits.
        longueur totale : il s'agit de la taille totale en octets du datagramme, et comme ce champ est de 2 octets on en déduit que la taille complète d'un datagramme ne peut dépasser 65535 octets. Utilisée avec la longueur de l'en-tête elle permet de déterminer où; commencent exactement les données transportées.
        identification, drapeaux (flags) et déplacement de fragment : ces trois champs interviennent dans le processus de fragmentation des datagrammes IP, nous ne rentrerons pas dans de profonds détails à ce sujet, pour ceci allez plutôt vous renseigner auprès du rfc. (IP - internet protocol, RFC 791)
        durée de vie (ttl : time to live) : ce champ indique le nombre maximum de routeurs que peut traverser notre paquet. Cela permet d'éviter qu'un datagramme "tourne" indéfiniment sur internet. Il permet aussi la réalisation d'un outil comme traceroute.
        protocole : ce champ permet de savoir quel protocole de plus haut niveau a servi à créé ce datagramme. Les valeurs codées l'étant sur 8 bits sont : 1 pour ICMP, 2 pour IGMP, 6 pour TCP et 17 pour UDP. Ainsi, la station destinatrice qui reçoit un datagramme IP pourra diriger les données qu'il contient vers la couche adéquate.
        somme de contrôle en-tête : ce champ contient une valeur codée sur 16 bits qui permet de contrôler l'intégrité de l'en-tête afin de déterminer si celui-ci n'a pas été altéré pendant la transmission. La somme de contrôle est le complément à un de tous les mots de 16 bits de l'en-tête (champ somme de contrôle exclu). Celle-ci est en fait telle que lorsque l'on fait la somme des champs de l'en-tête (somme de contrôle inclue), on obtient un nombre avec tous les bits positionnés à 1.
        adresse IP source : ce champ représente l'adresse IP de la machine émettrice, il permet au destinataire de répondre (codée sur 32 bits).
        adresse IP destination : adresse IP du destinataire (toujours codée sur 32 bits).

        Maintenant observons la structure associée à ce type de datagramme :

        typedef struct iphdr { 
            unsigned char  verlen;   /* version du protocol IP + la longeur de l'en tète */
            unsigned char  tos;	     /* type de service */
            unsigned short tot_len;  /* longueur totale du datagramme */
            unsigned short id;	     /* identification */
            unsigned short offset;   /* décalage */
            unsigned char  ttl;	     /* durée de vie du paquet */
            unsigned char  protocol; /* protocole */
            unsigned short checksum; /* somme de contrôle */
            unsigned int   saddr;    /* adresse IP source */ 
            unsigned int   daddr;    /* adresse IP destinataire */
        } IP_HDR;

      3. Le protocole TCP (Transmission Control Protocol)
      4. Ce protocole assure la liaison entre les applications et la couche inférieure du modèle (IP), il est trés utilisé sur l'internet pour sa fiabilité au niveau de la vérification de l'état des paquets, ou encore grâce à sa sécurité et ses possibilités de multiplexage/démultiplexage.

        Observons comment est constituée une trame TCP :

        0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
        port source
        port destination
        numéro de séquence
        accusé de réception
        décalage
        données
        réservée
        urg
        ack
        psh
        rst
        syn
        fin
        fenêtre
        somme de contrôle (checksum)
        pointeur de données urgentes
        options
        remplissage (padding)
        données

        Etudions ensemble les champs constituant ce datagramme :

        port source : ce champ est codé sur 16 bits, il correspond au port relatif à l'application en cours sur la machine source.
        port destination : toujours codé sur 16 bits, il correspond au port relatif à l'application en cours sur la machine de destination.
        numéro de séquence (isn) : champ codé sur 32bits, il correspond au numéro du premier octet de données par rapport au début de la transmission (sauf si syn est marqué). Si syn est marqué, le numéro de séquence est le numéro de séquence initial (isn) et le premier octet à pour numéro isn+1.
        accusé de réception : champ codé sur 32 bits. si ack est marqué ce champ contient le numéro de séquence du prochain octet que le récepteur s'attend à recevoir. Une fois la connexion établie, ce champ est toujours renseigné.
        décalage données (data offset) : taille de l'en-tête TCP en nombre de mots de 32 bits. Il indique où commencent les données. L'en-tête TCP, dans tous les cas, a une taille correspondant à un nombre entier de mots de 32 bits.
        réservé : codé sur 6 bits. Réservé pour usage futur. Doivent nécessairement être à 0.
        bits de contrôle : toujours codé sur 6 bits, ces "flags" indique le "type" du paquet.
        • urg: pointeur de données urgentes significatif
        • ack: accusé de réception significatif
        • psh: fonction push
        • rst: réinitialisation de la connexion
        • syn: synchronisation des numéros de séquence
        • fin: fin de transmission
        fenêtre
        : nombre d'octets à partir de la position marquée dans l'accusé de réception que le récepteur est capable de recevoir. Codé sur 16 bits.
        somme de contrôle (checksum) : ce champ contient une valeur codée sur 16 bits qui permet de contrôler l'intégrité de l'en-tête afin de déterminer si celui-ci n'a pas été altéré pendant la transmission. La somme de contrôle est le complément à un de tous les mots de 16 bits de l'en-tête (champ somme de contrôle exclu). Celle-ci est en fait telle que lorsque l'on fait la somme des champs de l'en-tête (somme de contrôle inclue), on obtient un nombre avec tous les bits positionnés à 1.
        pointeur de données urgentes : communique la position d'une donnée urgente en donnant son décalage par rapport au numéro de séquence. Le pointeur doit pointer sur l'octet suivant la donnée urgente. Ce champ n'est interprété que lorsque urg est marqué. ce champ est également codé sur 16 bits.
        options : les champs d'option peuvent occuper un espace de taille variable à la fin de l'en-tête TCP. ils formeront toujours un multiple de 8 bits. Toutes les options sont prises en compte par le checksum. Un paramètre d'option commence toujours sur un nouvel octet. (voir rfc 793 pour plus de renseignement)
        remplissage (padding) : les octets de bourrage terminent l'en-tête TCP :

          • de sorte que le nombre d'octets de celle-ci soit toujours multiple de 4 (32 bits)
          • de sorte que l'offset de données marqué dans l'en-tête corresponde bien au début des données applicatives.
        options : la taille maximale de segment de ce champ est de 16 bits. Si cette option est présente, elle communique à l'émetteur la taille maximale des segments qu'il pourra envoyer. Ce champ doit être envoyé dans la requête de connexion initiale (avec syn marqué). Si cette option est absente, le segment pourra être pris de n'importe quelle taille.

        Maintenant nous pouvons étudier la structure associée à ce type de trame :

        
        typedef struct tcphdr
        {
        	unsigned short sport;      /* port source */
        	unsigned short dport;      /* port de destination */
        	unsigned int   seqnum;     /* numéro de séquence */
        	unsigned int   acknum;     /* accusé de réception */
        	unsigned char  dataoffset; /* décalage des données (data offset) */ 
        	unsigned char  flags;	   /* flags */
        	unsigned short windows;    /* fenêtre */
        	unsigned short checksum;   /* checksum */
        	unsigned short urgpointer; /* pointeur de données urgentes */
        } TCP_HDR;

      5. Le protocole ICMP (Internet Control Message Protocol)

      ICMP est le protocole utilisé pour gérer les informations contrôlant le trafic IP, qui permet notamment aux routeurs d'envoyer des messages de contrôle ou d'erreur vers d'autres ordinateurs ou routeurs connectés. Ces messages ICMP sont transportés sur le réseau sous forme de datagramme, ainsi les messages d'erreurs peuvent eux mêmes être sujets d'erreurs. Toutefois en cas d'erreur sur un datagramme transportant un message ICMP, aucun message d'erreur n'est délivré pour éviter un effet "boule de neige" en cas d'incident sur le réseau.

      Voici à quoi ressemble ce genre de trame :

      en-tête

      message ICMP

      type
      (8 bits)
      code
      (8 bits)
      checksum
      (16 bits)
      message
      (taille variable)

      Pour ce qui est de la description des différents champs constituant ce type de datagramme je préfère vous envoyer lire le RFC 792, qui est plus précis à ce sujet, il y a trop de types, et de codes pour que je les décrive ici. Observons plutôt la structure qui est associée à ce datagramme :

      
      typedef struct icmphdr
      {
      	unsigned char  type;       /* type ICMP */
      	unsigned char  code;       /* code ICMP */
      	unsigned short checksum;   /* checksum  */
      	unsigned short id;         /* id, utilisé lors de la reconnaissance entre programmes des paquets ICMP */
      	unsigned short sequence;   /* séquence  */
      	unsigned long  timestamp;  /* timestamp */
      } ICMP_HDR;

    9. Forger ses propres paquets
      1. Paquets ICMP
      2. Maintenant que nous savons à quoi ressemble la structure d'un paquet ICMP ou encore celle du protocole IP, nous pouvons nous permettre de forger notre propre paquet grâce à un programme en C. Je vais donc ici vous montrer un simple exemple qui permettra d'envoyer une requête ICMP echo request à une machine présente sur le réseau.
        Tout d'abord nous devons initialiser l'API Winsock dans sa version 2 contrairement à précedemment. Pour ce faire, rien de complexe, nous avons juste à remplacer les paramètres passés à la macro MAKEWORD.

        Ce qui donnera : MAKEWORD(2, 2)

        Ensuite nous devons comme d'habitude créer notre socket mais cette fois ci nous n'allons pas juste nous contenter d'un socket stream mais plutôt d'un raw socket. Pour créer celui-ci nous allons appeler une fonction de l'API Winsock 2, cette fonction se nomme WSASocket().
        Etudions de suite ensemble son prototype :

        prototype : SOCKET WSASocket(int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags);

        +--------------------------------------------------------------------------------------------------------------+
        | SOCKET WSASocket(int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags); |
        +--------------------------------------------------------------------------------------------------------------+
        

        Comme vous pouvez le constater cette fonction nous renvoie un descripteur sur le socket créé, le premier paramètre correspond à la famille du socket, le second correspond au type du socket, le troisième permet de connaître le protocole utilisé pour ainsi savoir à quelle famille d'adresse celui-ci correspond, le quatrième définit une structure de type LPWSAPROTOCOL_INFO permettant de spécifier les caractéristiques du socket créé, le sixième paramètre est réservé à Windows, nous ne nous en n'occupons pas, quant au dernier paramètre il indique les attributs de notre socket. En cas d'échec cette fonction renvoie une erreur du type INVALID_SOCKET, si vous voulez obtenir précisément le code de sortie vous pouvez utiliser la fonction WSAGetLastError().

        Maintenant passons à un exemple :

        SOCKET socket;	
        socket = WSASocket(AF_INET, SOCK_RAW, IPPROTO_RAW, NULL, 0,0);
        if (socket == INVALID_SOCKET)
        {
        	fprintf(stderr, "erreur lors de la création du raw socket : %d\n", WSAGetLastError());
        	WSACleanup();
        	exit(-1);
        }
        
        Comme vous pouvez observer ici nous passons en second paramètre SOCK_RAW ce qui permet de créer un socket de type raw, ensuite nous passons au paramètre suivant la valeur IPPROTO_RAW, celle ci permet de spécifier que nous utilisons le protocole IP mais toujours en mode raw.

        Après avoir initialisé Winsock, créé notre raw socket, il nous reste encore une chose à faire, spécifier les bonnes options pour notre socket, pour ceci une fonction existe, toujours appartenant à l'API Winsock, cette fonction se nomme : setsockopt();
        étudions la de suite avant de l'utiliser.

        Prototype : int setsockopt(socket s, int level, int optname, const char *optval, int optlen);
        +-----------------------------------------------------------------------------------+
        | int setsockopt(SOCKET s, int level, int optname, const char FAR * optval, int optlen); |
        +-----------------------------------------------------------------------------------+
        
        Nous passons directement au deuxième argument qui représente le niveau sur lequel vont être définies les options (SOL_SOCKET, IPPROTO_TCP, et IPPROTO_IP), les paramètres optval et optlen sont utilisés pour déterminer les options pour setsockopt, optlen doit contenir la taille du buffer pointé par optval, si aucune option n'est fournie ou renvoyée, optval peut être NULL. En cas de succès cette fonction renvoie 0, dans le cas contraire celle-ci renvoie SOCKET_ERROR.
        Maintenant servons nous de cette fonction pour définir les bonnes options à notre socket :
        int optval = 1;
          
        if (setsockopt(socket, IPPROTO_IP, 2, (char *)&optval, sizeof(optval)) == SOCKET_ERROR)
        {
            fprintf(stderr, "erreur lors de l'appel à setsockopt : %d\n", WSAGetLastError());
            WSACleanup();
            exit(-1);
        }
        
        
        A partir d'ici, notre socket est prêt à être utilisé, nous allons donc devoir nous occuper de la création de notre paquet, pour ceci nous allons commencer par remplir notre structure IP avec les options voulues pour ensuite faire de même avec notre structure ICMP. Donc commençons avec notre en tête IP :
        unsigned short packet_size, ip_version, ip_len;	/* taille de notre paquet, IP version, longueur */
        struct iphdr *ip; /* notre structure IP */
        ....
        /* taille de notre paquet */
        packet_size = sizeof(struct iphdr) + sizeof(struct icmphdr); 
        /* on alloue un espace mémoire pour notre structure IP */
        ip = (struct iphdr *)malloc(sizeof(struct iphdr));
        /* On l'initialise à 0 */
        memset(ip, 0x0, sizeof(struct iphdr)); 
        
        /* longueur de l'en tête IP */
        ip_len = sizeof(struct iphdr) / sizeof(unsigned long);
        /* IP version */
        ip_version = 4;
        /* on remplie la structure IP */
        ip->verlen   = (ip_version << 4) | ip_len;
        ip->tos      = 0;
        ip->tot_len  = htons (sizeof (struct iphdr) + sizeof (struct icmphdr));
        ip->id       = 1;
        ip->offset   = 0;
        ip->ttl      = 255;
        ip->protocol = IPPROTO_ICMP;
        ip->saddr    = inet_addr(argv[1]); /* adresse IP source */
        ip->daddr    = inet_addr(argv[2]); /* adresse IP de destination */
        ip->checksum = 0; /* on initialise le champ checksum de notre structure IP à 0 avant l'appel de la fonction calculant le checksum */
        ip->checksum = in_cksum((unsigned short *)ip, sizeof(struct iphdr)); /* calcul du checksum avec la fonction */
        								     /* la fonction in_cksum se trouve dans les codes cités dans cet article */
        
        Maintenant que notre structure IP est correctement initialisée, passons à la structure ICMP.
        struct icmphdr *icmp;
        /* on alloue un espace mémoire pour notre structure ICMP */
        icmp = (struct icmphdr *)malloc(sizeof(struct icmphdr));
        /* On l'initialise à 0 */
        memset(icmp, 0x0, sizeof(struct icmphdr));
        
        /* on la remplie */
        icmp->type     = 8;  /* type ICMP echo request */
        icmp->code     = 0;
        icmp->id       = (ushort)GetCurrentProcessId(); 
        icmp->seq      = (ushort)GetCurrentProcessId(); 
        icmp->checksum = 0;
        icmp->checksum = in_cksum((unsigned short *)icmp, sizeof(struct icmphdr));
        
        Voila, nos deux structures sont remplies avec les options voulues, maintenant nous allons créer une image de notre paquet dans un buffer, un tableau de char plus précisement, ce tableau aura comme taille la taille de notre paquet, c'est à dire, la taille de notre structure IP, plus la taille de notre structure ICMP. Voyons plus précisemment ce que cela donne niveau code.
        char *ptr = NULL, packet[32]; /* notre paquet, 32 = sizeof(struct icmphdr) + sizeof(struct iphdr) */
        
        ZeroMemory(packet, sizeof(packet));
        ptr = packet;
        memcpy(ptr, ip, sizeof(struct iphdr));
        ptr += sizeof(struct iphdr);
        memcpy(ptr, icmp, sizeof(struct icmphdr));
        ptr += sizeof(struct icmphdr);
        
        Enfin, nous voila avec notre paquet prêt à être envoyé à son dstinataire, mais pour ceci, il nous reste encore une chose à découvrir, une nouvelle fonction plus précisemment, cette fonction se nomme sendto(). Etudions comment celle-ci se présente.

        Prototype : int sendto(socket s, const char FAR * buf, int len, int flags, const struct sockaddr FAR * to, int tolen);
        +------------------------------------------------------------------------------------------------------------+
        | int sendto(SOCKET s, const char FAR * buf, int len, int flags, const struct sockaddr FAR * to, int tolen); |
        +------------------------------------------------------------------------------------------------------------+
        
        Cette fonction va nous permettre d'envoyer un paquet à une adresse spécifiée par la structure to de type sockaddr. Elle prend comme premier paramètre le socket émetteur, comme second un pointeur sur les données à envoyer, le troisième paramètre doit contenir la taille des données émises. Le quatrième paramètre n'est pas très important, passons directement au cinquième qui correspond à un pointeur sur une structure de type sockaddr contenant l'adresse de destination, quand au dernier paramètre il spécifie simplement la taille de l'adresse pointée par la structure to.

        observons cette exemple :
        struct sockaddr_in sin; /* notre structure sockaddr_in qui contiendra l'adresse de destination */
        
        /* on remplie notre structure sockaddr_in */ 
        sin.sin_family           = AF_INET;
        sin.sin_addr.s_un.s_addr = inet_addr("127.0.0.1"); // Adresse de destination
        
        /* hop, nous envoyons notre paquet à destination */
        if(sendto(socket, packet, sizeof(packet), 0x0, (struct sockaddr *)&sin, sizeof(struct sockaddr_in)) == SOCKET_ERROR)	    
        {
        	fprintf(stderr, "Erreur lors de l'appel de la fonction sendto. Code erreur : %d\n", WSAGetLastError());	
        	free(ip);
        	free(icmp);
        	WSACleanup();
        }
        
        Nous avons terminé en ce qui concerne le forging de paquet ICMP, ceci, bien sûr, n'est qu'un exemple d'application, vous pouvez ensuite réaliser avec ceci un ICMP paquet forgeur ( icmprpk.c ) ou un même un smurfeur (mais c'est mal :)). Vous pouvez trouver le code complet démontrant toute cette section ici : icmp.c

      3. Paquets TCP
      4. Après avoir joué avec le protocole ICMP, attaquons un peu le TCP, en forgeant nos propres datagrammes TCP nous allons pouvoir réaliser de multiples choses, comme envoyer une multitude de paquets TCP avec le flag SYN activé à une machine distante, ce qui va créer un effet de surcharge au niveau des connexions de l'hote distant, cette attaque se nomme le "syn flooding", nous allons aussi pouvoir spoofer des connexions TCP avec un peu plus d'expérience (problème de connaissance du numéro de séquence, sur certain OS :)).
        Maintenant passons à la pratique. Nous connaissons toutes les fonctions nécessaires pour envoyer notre propre paquet TCP, la seule chose innovante ici va être de devoir remplir une structure de type TCP_HDR pour ensuite calculer le checksum de notre paquet grâce à un pseudo en-tête le permettant. Voyons comment réaliser cela.
        Je ne vais pas répéter les détails sur l'initialisation de Winsock, etc... Vous avez dejà vu ça dans la précédente section, passons directement aux choses intérressantes. Donc ici, au lieu de déclarer une structure de type ICMP_HDR nous allons la remplacer par une de type TCP_HDR :

        struct tcphdr *tcp;  /* notre structure TCP */
        /* maintenant passons à la structure TCP */
        /* on alloue de la mémoire pour celle-ci */
        tcp = (struct tcphdr *)malloc(sizeof(struct tcphdr));
        memset(tcp, 0x0, sizeof(struct tcphdr));
        
        /* on la remplit */
        tcp->sport      = htons(1500);	/* port source */
        tcp->dport      = htons(80) ;	/* port de destination */
        tcp->seqnum     = htonl(1337);	/* numéro de séquence */
        tcp->acknum     = htonl(1337);  /* ack number */
        tcp->dataoffset = (5) << 4;     /* décalage */
        tcp->flags      = 0x02;         /* 0x02 = flag syn */
        tcp->window     = htons(1337);  /* fenètre */
        tcp->checksum   = 0;		/* checksum */
        tcp->urgpointer = 0;		/* options */
        
        Maintenant que notre structure TCP est remplie avec les options voulues nous allons devoir calculer le checksum pour notre paquet. pour cela nous allons devoir utiliser une pseudo structure, voyons de suite à quoi ressemble celle-ci :
        struct pseudohdr
        {
        	unsigned long saddr;
        	unsigned long daddr;
        	char useless;
        	unsigned char protocol;
        	unsigned short length;
        	struct tcphdr TCP;
        };
        
        Nous connaissons maintenant l'allure de notre pseudo structure, alors nous pouvons la remplir pour ensuite calculer notre checksum, et c'est ce que nous allons faire de suite.
        struct pseudohdr *psdheader; /* pseudo header */
        
        /* allocation mémoire pour notre pseudo header */
        psdheader = (struct pseudohdr *)malloc(sizeof(struct pseudohdr));
        memset(psdheader, 0x0, sizeof(struct pseudohdr));
        
        /* on remplit notre structure */
        psdheader->saddr    = inet_addr(argv[1]); /* adresse source */
        psdheader->daddr    = inet_addr(argv[3]); /* adresse de destination */
        psdheader->useless  = 0;		  /* null */
        psdheader->protocol = IPPROTO_TCP;	  /* protocol */
        psdheader->length   = htons(sizeof(struct tcphdr)); /* longueur de notre structure TCP_HDR */
        psdheader->tcp      = *tcp;		  /* structure TCP_HDR */
        
        /* Maintenant nous calculons le checksum pour notre paquet */
        tcp->checksum = in_cksum((unsigned short *)psdheader, sizeof(struct pseudohdr)); 
        
        Voila tout est prêt, nous pouvons envoyer notre paquet après avoir copié le contenu des structures dans un buffer de type char, c'est d'ailleurs ce que nous avons fait dans la section précédente pour notre paquet ICMP. Vous trouverez donc ici la source du programme illustrant ce chapitre. Ce programme permet d'envoyer des paquets TCP avec le flag SYN activé. Il a pour possibilité de modifier l'adresse source de ce même paquet.

      5. Paquets UDP

      Forger ses propres paquets UDP est relativement simple, il suffit de remplir la structure définissant l'en-tête UDP au niveau réseau ce qui n'est pas très complexe, donc après lecture de cette partie vous serez en mesure d'envoyer des datagrammes UDP de tous type, et même en modifiant votre IP source, ce qui, avec le protocole UDP peut être très pratique, celui-ci ne requiert aucune authentification au niveau des connexions, vous pourrez donc exploiter ceci.
      Etudions cette mystérieuse structure :

      typedef struct udphdr
      {
      	unsigned short srcport; 
      	unsigned short dstport; 
      	unsigned short length; 
      	unsigned short checksum; 
      } UDP_HDR;
      
      unsigned short srcport : ce premier membre définit le port source.
      unsigned short desport : ce second membre définit encore une fois le port mais cette fois ci celui de destination.
      unsigned short length : ce membre va définir la longueur de notre en-tête UDP.
      unsigned short checksum : ce dernier membre représente le checksum de notre paquet, sachez qu'il n'est pas obligatoire
      de le calculer, vous pouvez simplement l'initialiser à 0.

      Voyons cet exemple d'initialisation pour notre structure de type UDP_HDR :
      /* notre structure udphdr */
      struct udphdr *udp;
      		
      /* initialisation mémoire */
      udp = (struct udphdr *)malloc(sizeof(struct udphdr)); 
      memset(udp, 0x0, sizeof(struct udphdr));
      
      /* remplissage */
      udp->srcport  = htons(1500);  /* port source */
      udp->dstport  = htons(80);    /* port destination */
      udp->length   = htons (sizeof(struct udphdr)); /* longueur */
      udp>checksum = 0;            /* checksum */
      
      Maintenant, vous en savez assez avec ceci et les exemples précédents pour envoyer vos propres datagrammes UDP.
      Vous trouverez un exemple de code illustrant cette partie ici .

    10. Programmer un sniffer grâce aux raw sockets

    1. Introduction
    2. Ici nous allons étudier comment programmer un sniffer uniquement grâce aux fonctions que fournit la librarie Winsock 2. Celle-ci va nous permettre de récupérer grâce à un raw socket toutes les trames traversant notre réseau. Après les connaissances acquises précédemment cela ne va pas être très compliqué, nous prendrons uniquement conscience de deux nouvelles fonctions. Je voudrai quand même ajouter une petite précision sur le sniffer que nous allons programmer, malheureusement nous ne pourrons récupérer toutes les informations touchant au protocole Ethernet, comme l'adresse mac par exemple, le niveau d'implémentation de l'API Winsock 2 n'est pas assez bas pour récupérer ce type d'information.

    3. Qu'est ce qu'un sniffer ?
    4. Un sniffer est un programme nous permettant de capturer les trames traversant un réseau pour ensuite les afficher dans un format compréhensible par l'être humain. Par ailleurs, il permet aux administrateurs de détecter des anomalies au niveau de leurs installations réseau, ce qui est donc très utile, et de plus, il va aussi habiliter les personnes mal intentionnées à récupérer des mots de passe ou toutes sortes d'informations interessantes passant sur un réseau.

    5. Pratique
    6. Passons maintenant à la partie la plus interessante, la programmation. Je vais donc vous résumer les étapes que nous allons devoir franchir pour arriver à nos fins.

      - Initialiser Winsock dans sa version 2 ou supérieure.
      - Créer notre raw socket, qui nous servira à récupérer les trames réseau.
      - Récupérer notre adresse IP locale pour ensuite l'assigner au socket grâce à la fonction bind()
      - Placer notre interface réseau en mode promiscuous grâce à la fonction WSAIoctl(), ce qui va permettre de récupérer toutes les trames transitant sur le réseau même celles qui ne nous sont pas destinées.
      - Lancer une boucle qui sera destinée à recevoir tous les paquets grâce à la fonction recvfrom() .
      - Traiter les paquets reçus.

      Maintenant nous allons étudier les seules choses que vous ne connaissez pas, c'est à dire les fonctions WSAIoctl et recvfrom().
      Donc procédons à cette petite étude :

      Prototype : int WSAIoctl(SOCKET s, DWORD dwIoControlCode, LPVOID lpvInBuffer, DWORD cbInBuffer, LPVOID lpvOutBuffer, DWORD cbOutBuffer, 
      			 LPDWORD lpcbBytesReturned, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
      int recvfrom(SOCKET s, char FAR *buf, int len, int flags, struct SOCK_ADDR *from, int FAR *fromlen);

      +-------------------------------------------------------------------------------------------------------------------------------------+
      | int WSAIoctl(SOCKET s, DWORD dwIoControlCode, LPVOID lpvInBuffer, DWORD cbInBuffer, LPVOID lpvOutBuffer, DWORD cbOutBuffer,         |
      |              LPDWORD lpcbBytesReturned, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);      |
      +-------------------------------------------------------------------------------------------------------------------------------------+
      
      Cette fonction va nous être très utile, elle va permettre de passer notre interface réseau en mode promiscuous, ce qui aura pour but de pouvoir récupérer tous les paquets transitant sur le réseau même ceux qui ne nous sont pas destinés. Je préfère vous donner un exemple d'utilisation de cette fonction par la suite, vous pourrez vous renseigner sur le type et l'utilité de tous ses arguments sur msdn.microsoft.com ou encore dans le fichier d'aide "Win32 Programmer's Reference".
      +------------------------------------------------------------------------------------------------------+
      | int recvfrom(SOCKET s, char FAR *buf, int len, int flags, struct SOCK_ADDR *from, int FAR *fromlen); |
      +------------------------------------------------------------------------------------------------------+
      
      Cette fonction va permettre de recevoir les datagrammes passant sur le réseau tout en stockant leur adresse source. Elle prend comme premier argument un socket qui correspond, pour nous, à notre raw socket, le second argument est un buffer dans lequel seront stockés les paquets reçus, le troisième argument correspond à sa taille, le paramètre flags n'est pas important pour nous, nous l'initialiserons à 0, la structure de type sock_addr correspondant au quatrième argument va servir à stocker les informations sur l'émetteur du paquet reçu, et enfin le dernier argument correspond à la taille "véritable" du buffer reçu. Si aucune erreur ne se produit, cette fonction renvoie le nombre de bytes reçus, dans le cas contraire celle-ci renvoie une erreur de type SOCKET_ERROR.

      Je ne vais pas détailler toutes les opérations suivantes, comme par exemple traiter les paquets reçus, ce n'est que de la programmation et il n'y a aucune difficulté, mais je vais quand même vous donnez un exemple commenté de sniffer codé en C grâce à l'API Winsock 2.

      RSniffer : http://barbus.homeunix.org/redkod/products/win/rsniffer.c


  8. Références
  9. Win32 Programmer's Reference.
    Windows* Sockets 2 Application Programming Interface.
    Winsock direct: the value of system area networks by Jim Pinkerton.
    Microsoft Developer Network : http://msdn.microsoft.com/
    Gibson Research Corporation : http://grc.com/
    IP - Internet Protocol - RFC 791
    ICMP - Internet Control Message Protocol - RFC 792
    TCP - Transmission Control Protocol - RFC 793

  10. Remerciements
Je voudrai tout d'abord remercier Madchat pour avoir participé à l'évolution du développement sous Windows en créant un répertoire dédié à ce sujet sur leur site. Ensuite je tiens aussi à passer un remerciement à toutes les personnes soutenant RedKod que cela soit sur IRC ou ailleurs. Et bien sûr un grand greetz pour tous les membres actifs de RedKod (NostroBO, ChiRon).



R-e-D red@secureroot.com http://barbus.homeunix.org/r-e-d/