Annonce

Réduire
Aucune annonce.

Trame TCP/IP : construire et manipuler les trames.

Réduire
X
 
  • Filtre
  • Heure
  • Afficher
Tout nettoyer
nouveaux messages

  • Tutoriel Trame TCP/IP : construire et manipuler les trames.

    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 :
    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];
    Déclaration des deux pointeurs :
    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;
        }
    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 :
    Code:
    struct hostent *get_addr;
    Déclaration du buffer stockant l'adresse locale :
    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;
    Nous remplissons directement les paramètres de la structures grâce à la fonction memcpy().

    Copie des éléments structuraux :
    Code:
    memcpy(&iSocketAddr.sin_addr.s_addr, get_addr->h_addr, get_addr->h_length);
    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 :
    Code:
    char FAR *IP;
    Ensuite nous stockons l'adresse locale dans le pointeur.

    Stockage de l'adresse locale dans *IP :
    Code:
    IP = inet_ntoa(iSocketAddr.sin_addr);
    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 :
    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);
    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) :
    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;
    Assignation des ports d'écoute :
    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);
    Exemple de récupération du checksum :
    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);
    [/spoiler]



    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>
    Rappel des constantes à définir avant le main :
    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.




    Dernière modification par MadHatter, 03 janvier 2013, 16h28.
    Ex-membre Hackademiciens.
Chargement...
X