Forger un paquet ICMP

November 26, 2010 in C++, InfoSec

Si vous voulez savoir ce que je fais le vendredi soir ou si tout simplement vous vous intéressez à l’Internet, voici comment forcer un paquet ICMP de type 8 ou en terme moins pédant comment forger un ping.

Prenons une analogie postale pour expliquer le concept. J’utilise une enveloppe pour envoyer un message par le biais de la poste. Cette enveloppe à un timbre, une adresse et une adresse de retour. Forger un paquet revient à construire cette enveloppe et à remplir les champs d’adresses à la main. Je vais plus précisément forger un paquet ICMP de type ping. ICMP est un protocole qui se situe juste au dessus de la couche IP. Il ne repose pas ainsi sur UDP ou TCP.

Je me suis aidé de l’excellent tutorial de mixter. Ce dernier forge un paquet TCP sur Unix. Je vous propose de faire un paquet ICMP sur Windows (pour changer !).

Tout d’abord précisons les types de données. Nous utiliserons la définition des sockets BSD.

/*
* type.h
* Internet Protocol Stack
*/

struct ipheader {
 unsigned char ip_hl:4, ip_v:4; /* this means that each member is 4 bits */
 unsigned char ip_tos;
 unsigned short int ip_len;
 unsigned short int ip_id;
 unsigned short int ip_off;
 unsigned char ip_ttl;
 unsigned char ip_p;
 unsigned short int ip_sum;
 unsigned int ip_src;
 unsigned int ip_dst;
}; /* total ip header length: 20 bytes (=160 bits) */
typedef struct ipheader ipheader_t;

struct icmpheader {
 unsigned char icmp_type;
 unsigned char icmp_code;
 unsigned short int icmp_cksum;
 /* The following data structures are ICMP type specific */
 unsigned short int icmp_id;
 unsigned short int icmp_seq;
}; /* total icmp header length: 8 bytes (=64 bits) */
typedef struct icmpheader icmpheader_t;

Je vous sers tout cela d’un coup car cela n’a pas réellement d’importance. Nous forgeons un paquet à partir de la couche IP. Donc nous avons besoin de définir les champs de la couche IP puis de la couche ICMP. Si vous n’avez aucune idée à quoi cela peut ressembler, regardez ce schéma Wikipedia. Vous y avez la description de l’en-tête IP. Tout ce que la structure de données représente, ce sont les champs de cet en-tête. Et nous faisons de même avec l’en-tête ICMP.

Rentrons ensuite dans le vif du sujet. Définissons les en-têtes de notre programme :

/* platform dependent */
#ifdef _WIN32

#include <winsock2.h>
#include <ws2tcpip.h>

#endif

/* common headers */
#include "type.h"
#include "stdio.h"
#include "utils.h"

#define SOURCE "192.168.2.10"
#define TARGET "192.168.2.1"

#define ICMP_PING 8

A noter que le fichier utils.h est le fichier qui contient les en-têtes IP et ICMP (en d’autre termes, vous devez créer ce fichier et mettre les structures dedans). Le reste c’est du très classique avec les en-têtes Windows pour les sockets et la bibliothèque standard. Vous noterez que les adresses sont codées en dur. Pour débugger j’utilise mon ordinateur et mon routeur (la boucle locale sur Windows 7 n’est pas capturable avec Wireshark, enfin). Le ICMP_PING désigne le type de paquet ICMP, ici un ping (ou 8 en langage machine).

Plaçons nous ensuite dans une fonction main. On déclare nos variables :

/* init data type */
    int c_socket = 0;
    icmpheader_t *h_icmp = NULL;
    ipheader_t *h_ip = NULL;
    struct sockaddr_in sin_in;
    struct sockaddr_in sin_out;
    char *packet = NULL;
    int len_packet = 0;
    char buffer[4096];
    int sizelen;

    int ret = 0;

    #ifdef _WIN32
    /* Init stack */
    WSADATA wsaData;

    WSAStartup(MAKEWORD(2,2), &wsaData);
    #endif

    /* init socket */
    c_socket = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);

    {   /* force kernel to not add header before packet */
        int one = 1;
        const int *val = &one;
        if (setsockopt(c_socket, IPPROTO_IP, IP_HDRINCL, val, sizeof (one)) < 0)
            printf ("Warning: Cannot set HDRINCL!n");
    }

On initialise donc nos buffers et nos pointeurs. On s’en servira plus tard. Le seul point à retenir est la création d’un socket en mode RAW. On s’assure ensuite que le kernel n’ajoutera pas accidentellement des headers de son jus ce qui ferait doublon avec les nôtres (et forcement ça marche moins bien après). Ce bout de code est mixter approved, donc pas de commentaire haineux s’il vous plait (des accolades au milieu du code, toussa toussa) !

/* init packet */
        len_packet = sizeof(ipheader_t) + sizeof(icmpheader_t); /* set packet size */
        packet = malloc(len_packet);
        h_ip  = (ipheader_t*) packet; /* put ip header at the beginning */
        h_icmp = (icmpheader_t*)(packet + sizeof(ipheader_t));
                            /* put icmp header after ip header */

        memset(packet, 0, len_packet); /* clear */

On calcul la taille du paquet qui ne contiendra que des en-têtes (on pourrait rajouter du payload facilement mais ce n’est pas le sujet). On fait ensuite pointer nos en-têtes sur les parties du paquet qui leur conviennent. Comme ICMP est encapsulé dans IP, il est normal que l’en-tête IP se situe avant celui d’ICMP mais je ne vous apprend rien. Il est très important de mettre à 0 tous les champs du paquets. Et oui ami de Java, C ne fait pas ça pour vous.

/* init ip header */
        h_ip->ip_hl = 5;
        h_ip->ip_v = 4;
        h_ip->ip_tos = 0;
        h_ip->ip_len = len_packet;
        h_ip->ip_id = htonl(54321);
        h_ip->ip_off = 0;
        h_ip->ip_ttl = 255;
        h_ip->ip_p = IPPROTO_ICMP;
        h_ip->ip_sum = 0; /* 0 means filled by the kernel before transmission */
        h_ip->ip_src = inet_addr(SOURCE);
        h_ip->ip_dst = inet_addr(TARGET);

        /* init icmp header */
        h_icmp->icmp_type = ICMP_PING;
        h_icmp->icmp_code = 0;
        h_icmp->icmp_id = 0;
        h_icmp->icmp_seq = 0;
        h_icmp->icmp_id = htons(0x01);
        h_icmp->icmp_seq = htons(0x0a);

        h_icmp->icmp_cksum = 0; /* serious stuff */
        h_icmp->icmp_cksum = csum((unsigned short*)h_icmp, sizeof(icmpheader_t));

Nous initialisons les champs des en-têtes. La seule chose importante à retenir à mon sens est le cheksum ICMP. Ce dernier est calculé de la même façon que celui d’IP. Cependant le kernel calcul tout seul le checksum pour le paquet IP, ICMP n’a pas le droit à cette faveur. Nous utilisons donc un bout de code issu de la RFC 1705. Cette RFC date de 1988 et comportait une erreur. A la vue d’une erreur j’étais excité à l’idée de faire un rapport de bug mais il semble que le bug ait été rapporté en 2004. Donc bref voici la fonction de checksum.

unsigned short      /* this function generates header checksums RFC 1705*/
csum (unsigned short *addr, int count)
{
    /* Compute Internet Checksum for "count" bytes
    *         beginning at location "addr".
    */
    long sum = 0;

    while( count > 1 )
    {
        /*  This is the inner loop */
        sum += * (unsigned short *) addr++;
        count -= 2;
    }

    /*  Add left-over byte, if any */
    if( count > 0 )
        sum += * (unsigned char *) addr;

    /*  Fold 32-bit sum to 16 bits */
    while (sum>>16)
        sum = (sum & 0xffff) + (sum >> 16);

    return ~sum;
}

Il y a des manières plus malignes de le faire (mixter encore lui !).

Maintenant nous sommes prêt à envoyer ce maudit paquet et à recevoir le pong en retour.

        sin_out.sin_family = AF_INET;
        sin_out.sin_addr.s_addr = inet_addr(TARGET);

        /* send icmp echo */
        ret = sendto(c_socket, packet, h_ip->ip_len, 0, (struct sockaddr*)&sin_out, sizeof(struct sockaddr));
        printf("ret : %dn", ret);
        if(ret != h_ip->ip_len)
            printf("ERROR sendto");

        sizelen = sizeof(struct sockaddr);
        /* receive echo reply */
        ret = recvfrom(c_socket, buffer, len_packet, 0, (struct sockaddr*)&sin_in, &sizelen);

        if(ret != len_packet)
            printf("ERROR rcvfrom");

Rien d’extraordinaire non plus ici. Vous noterez le stockage inutile de l’adresse de l’émetteur du pong.

Et enfin un peu de code pour nettoyer tout ça.

    /* clean the mess */

    #ifdef _WIN32
    closesocket(c_socket);
    WSACleanup();
    #endif

    if(h_icmp)
        free(h_icmp);
    if(h_ip)
        free(h_ip);
    if(packet)
        free(packet);

    return 0;

Voilà une implémentation basique mais néanmoins complète d’un ping. A noter qu’il faut être root ou administrateur pour faire marcher le bousin. Vous pouvez maintenant imaginer forger à peut près n’importe quoi et même inventer votre propre protocole de couche 3 ou 4.

Edit : Je mélange allègrement Anglais et Français dans cet article, je m’excuse mais c’est une déformation professionnelle :)