ex0ns

about
A Computer Science Student's blog, about programming, algorithms, hack and a lot more.

Introduction aux sockets : ARPs

05 Nov 2012

Dans cet article j'aborderai un vaste domaine dans la programmation : celui des sockets ! Un si vaste continent que moulte explorateurs y ont perdu la vie (ou du moins une vie). Mon but n'est pas de faire une explication détaillée sur le fonctionnement des sockets, des connexions, du modèle OSI, je ne pourrais de toute facon par rivaliser avec le fantastique guide de Beej (que je vous conseille fortement de lire par la même occasion).

Le point que j'aborderai précisement tout au long de cet article est loins d'être le plus évident lorsque l'on s'attaque au socket, c'est pour quoi je vous conseille de lire le guide avant d'entamer la lecture de cet article qui traite des Raw Sockets! Comme d'habitude, il est fort probable que des erreurs se soient glissées dans cet article. En effet, je ne fais que partager des projets qui m'ont fait découvrir un domaine, je n'ai donc que peut de recul façe au code, à la conception ou à la technique utilisée. Pour autant, j'espère que ces projets inspirent certains d'entre vous.

Le projet

Nous allons créer une "bibliothèque" (ou plutôt un ensemble de fonctions) permettant de gérer les ARPs, tout cela en C.

Un ARP ? C'est quoi ?

Acronyme de "Address Resolution Protocol" qui est le protocol permettant de faire le lien entre une adresse IP et une addresse physique (MAC) ce protocol n'est présent que sur les réseaux locaux et permet aux machines de communiquer entre elles. En effet, les ARPs permettent aux machines connectées au réseau de garder une correspondance entre les addresses IP et les addresses MAC, sans eux les machines ne pourraient pas communiquer (une machine a besoin de connaitre l'addresses MAC de son destinataire afin d'être sûr que ce soit bien lui qui reçoive le message).

Lorsque vous démarrez votre machine, le cache ARP est vide (dans une console, arp -a pour le visionner), ou ne contient qu'une entrée (celle du routeur). Imaginons que nous voulons effectuer un PING sur une autre machine (du réseau local), la machine ayant l'adresse IP 192.168.0.3, votre PC va d'abord regarder dans la table ARP s'il existe une adresse MAC correspondant à cette IP, dans le cas contraire, il va devoir lla récupérer, et c'est là que les ARP interviennent, votre machine va emettre une requête à toutes les machines du réseau local (broadcast) en demandant l'addresse MAC correspondante à l'IP qu'il veut contacter. A ce moment là, la machine 192.168.0.3 voit qu'on essaye de la contacter, et envoi un ARP en réponse à notre machine, contenant son adresse MAC, le PING peut maintenant être envoyé !

Regardez à nouveau la table arp (arp -a) une nouvelle entrée est apparue, associant l'addresse MAC retournée à l'adresse IP 192.168.0.3. Ainsi, à chaque fois que vous contactez un appareil connecté sur le réseau local, un paquet de type ARP est envoyé afin de determiner l'adresse MAC de la cible (imprimante, téléphone, ordinateur, ...).

Création des structures

Pour notre projet, nous aurons besoin de forger des paquets ARP pour cela, il nous faut un structure bien définie, que nous remplierons par la suite, et qui nous servirons de base, vous aurez deviné que la première structure que nous allons devoir créer et celle des paquets ARP, nous pouvons trouver leur composition sur wikipedia, il ne reste plus qu'a coder tout ca:

struct arpHdr {
    unsigned short int htype;          // Hardware type  
    unsigned short int ptype;          // Protocol type  
    unsigned char hlen;               // Hardware address length  
    unsigned char plen;               // Protocol address length  
    unsigned short int oper;           // ARP operation  
    unsigned char sha[6];          // Sender hardware address.  
    unsigned char spa[4];          // Sender protocol address.  
    unsigned char tha[6];          // Target hardware address.  
    unsigned char tpa[4];          // Target protocol address.  
} __attribute__ ((packed));        // Les variables de la structure se suivent dans la mémoire (pas "d'espace vide")

Jusque là, à part le __attribute__ ((packed)), rien de bien compliqué, si ce n'est qu'il faut faire attention à la taille de SHA, THA et SPA, TPA (respectivement 48 et 32 bits) La seconde structure que nous allons créer est une parti du paquet de type Ethernet (celui qui encapsule les données du protocol), il se compose de trois informations: * L'adresse physique de destination sur 6 octets * L'adresse physique de l'émetteur sur 6 octets * Le type/La taille sur 2 octets Ce qui nous donne:

struct ethHdr{
    u_int8_t tha[6];        // Target hardware address
    u_int8_t sha[6];        // Source hardware address
    u_int16_t type;         // Type/Length
} __attribute__ ((packed));

La troisième classe nous permettra de manipuler plus simplement les deux précédentes, elle représentera notre paquet final, celui qui sera envoyé:

struct arpFull{
    struct ethHdr eth;          // Ethernet packet information
    struct arpHdr arp;          // ARP packet information
} arp;

Maintenant que nous avons nos structures, nous pouvons nous attacher à la création des fonctions.

Les fonctions

La première fonction que nous allons créer et celle qui permettra de remplir la structure arphdr et ethhdr de facon à émettre une requête en broadcast (destiné à toutes les machines) sur le réseau, en anglais, cette requête se traduirai par le fameux "Who has 192.168.1.3 ? Tell 192.168.1.2" (dans cet exemple, notre machine se trouve à l'ip 192.168.1.2 et la machine dont nous voulons connaitre l'addresse MAC porte l'IP 192.168.1.3.

void fill_header_request(char *srcip, char *dstip){
    char broadcastAddr[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // FF:FF:FF:FF:FF:FF
    struct in_addr src, dst; // Needed to convert the IP with inet_addr

    memcpy(arp.arp.tha, broadcastAddr, 6); // Set the target hardware address in the ARP
    memcpy(arp.eth.tha, broadcastAddr, 6); // Same for the Ethernet protocol

    arp.eth.type = htons(ETH_P_ARP); // 0x0806 ARP protocol ID

        // now our Ethernet Packet is ready

    arp.arp.htype = htons(1); // 0x1 Ethernet Type
    arp.arp.ptype = htons(ETH_P_IP); // 0x0800 IP packets only
    arp.arp.hlen = 6;
    arp.arp.plen = 4;
    arp.arp.oper = htons(1); // 1 For request, 2 for reply (see previous Wiki page)

    src.s_addr = inet_addr(srcip);
    dst.s_addr = inet_addr(dstip);

    memcpy(&arp.arp.spa, (unsigned char *)&src.s_addr, 4); // Set the source IP address
    memcpy(&arp.arp.tpa, (unsigned char *)&dst.s_addr, 4); // Same for the target   

}

Je n'ai pas grand chose à ajouter sur ce code, il est relativement compréhensible, nous remplissons les structures des paquets ethernet et ARP, il faut juste penser à convertir l'adresse IP grâce à une structure in_addr et la fonction inet_addr. De même, écrivons la fonction que aura comme rôle de remplir ces structures de la même façon, mais pour effectuer une réponse ARP. Une réponse ARP est un paquet qui se traduit par la réponse à la question précédente, c'est à dire : "192.168.1.3 is at 00:01:02:03:04:05", le cache ARP des deux machines est alors mis à jour et elles peuvent communiquer sur le réseaux sans avoir besoin d'envoyer un ARP à chaque fois. Comme vous pouvez vous en douter, les deux fonctions vont avoir de très nombreuses ressemblances :

void fill_header_reply(char *srcip, char *dstip, char *dsthwd){
    struct in_addr src, dst; // Needed to convert the IP with inet_addr
    char hardwareAddr[6] = {0};
    int i;
    for(i=0; i<6 ;i++){
        char *next;
        hardwareAddr[i] = strtol(dsthwd, &next, 16); // Hex value of char between ":"
        dsthwd=++next; // The next one 
    }

    memcpy(arp.eth.tha, hardwareAddr, 6); // Set ther hardware address in Ethernet
    memcpy(arp.arp.tha, hardwareAddr, 6); // Same for ARP protocol

    arp.eth.type = htons(ETH_P_ARP); // 0x0806 ARP protocol ID

    arp.arp.htype = htons(1); // 0x1 Ethernet Type
    arp.arp.ptype = htons(ETH_P_IP); // 0x0800 IP packets only
    arp.arp.hlen = 6;
    arp.arp.plen = 4;
    arp.arp.oper = htons(2); // 1 For request, 2 for reply (see previous Wiki page)

    src.s_addr = inet_addr(srcip);
    dst.s_addr = inet_addr(dstip);

    memcpy(&arp.arp.spa, (unsigned char *)&src.s_addr, 4);
    memcpy(&arp.arp.tpa, (unsigned char *)&dst.s_addr, 4);

}

Ce qui change ici est la première boucle, qui peut paraitre étrange, mais elle convertie très simplement une addresse MAC de la forme 00:01:02:03:04:05 à des valeurs hexadécimales, dans un tableau. Nous n'avons plus qu'à remplir la structure, et changer OPER en 2 afin de préciser qu'il s'agit d'une réponse et non pas d'une requête. Coici maintenant le coeur de programme (problème ?), la création et l'envoi du paquet final (de la requête/réponse ARP), c'est ici que les sockets vont (enfin) intervenir !

int sendArp(char *iface){
    struct sockaddr_ll device;
    struct ifreq ifr;
    int sockfd;
    char buffer[1024];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    strncpy(ifr.ifr_name, iface, IFNAMSIZ); // Set the interface name (and fill with 0)
    ioctl(sockfd, SIOCGIFINDEX, &ifr);  
    device.sll_ifindex = ifr.ifr_ifindex; // iface index
    ioctl(sockfd, SIOCGIFHWADDR, &ifr);
    memcpy(device.sll_addr, ifr.ifr_hwaddr.sa_data, 6); // iface hardware address

    device.sll_family = AF_PACKET; // Always AF_PACKET
    device.sll_protocol = htons(ETH_P_IP); //  0x0800 IP packets only
    device.sll_hatype = 1; // 0x1 (Ethernet hardware address)
    device.sll_halen = 6;


    /* Now our device structure is ready, we need to fill the sender information in ARP and Ethernet header */

    memcpy(arp.arp.sha, ifr.ifr_hwaddr.sa_data, 6); // Set the sender Mac address in ARP 
    memcpy(arp.eth.sha, ifr.ifr_hwaddr.sa_data, 6); // Same for Ethernet

    close(sockfd); // We want to use it again

    sockfd = socket( PF_PACKET, SOCK_RAW, htons(ETH_P_ARP)); 
    /*
    This is the most interesting point, let's see how it works
        PF_PACKET : Low level packet interface 
        SOCK_RAW : Provides raw network protocol access.
        ETH_P_ARP : ARP protocol
        Now we have a raw socket and we can put everything we want in it !
        The kernel doesn't do anything with your packet, it's all yours     
    */
    memcpy(buffer, &arp, sizeof(struct arpFull)); // Copy our structure (ARP header and EThernet Header to the buffer);
    if(sendto(sockfd, buffer, sizeof(struct arpFull), 0, (struct sockaddr *)&device, (socklen_t) sizeof(struct sockaddr_ll)) == -1){
        printf("Error");
    }

}

Analysons un peu cette fonction, afin de découvrir le fonctionnement des raws sockets. Premièrement, afin de récuperer le numéro de l'interface ainsi que son addresse MAC, nous devons créer un "file descriptor", que nous passerons en argument à la fonction ioctl, il faut ensuite préciser quel type de requête on veut effectuer avec ioctl, SIOCGIFINDEX nous retourne l'index correspondant à l'interface portant le nom contenu dans la structure (dans ce cas, ifr.ifrname, qui nous avons défini une ligne avant), puis nous appellons à nouveau la fonction, mais avec une requete de type _SIOCGIFHWADDR, qui retourne de la même façon l'adresse MAC de l'interface. Il nous faut ensuite compléter notre structure sockaddr_ll que nous passerons en paramètre à la fonction sendto au moment de l'envoi. Le remplissage de cette structure n'a rien de très différent avec ce qu'on a fait avant (protocol, type, taille). La prochaine étape consiste à (enfin) finaliser nos en-tête ARP et Ethernet en y insérant notre adresse MAC récupérée à l'aide IOCTL, nos deux en-têtes sont maintenant complètes, il ne nous reste plus qu'a créer une socket et à les envoyer ! Pour créer la raw socket afin de garder un controle total sur cette dernière, les arguments sont les suivants (également detaillés dans le code) :

  • PF_PACKET : Permet d'utiliser des sockets de bas niveau (le kernel ne fait "plus rien")
  • SOCK_RAW : Fournit l'acces au protocol de type RAW
  • ETHPARP : Celui la ou le connait, c'est le protocol ARP

Nous avons donc notre raw socket, que nous controlons intégralement, il ne nous reste plus qu'a y attacher nos données (les structures remplies précedement), et à envoyer tout ca vers sur le réseau \o/. On copie donc le contenu de nos structures dans notre buffer et on envoie tout ça grâce à sendto, là encore, rien de très complexe (il suffit de lire la doc !).

Ah oui, j'allais oublier, pour ceux qui on eu le courage de me lire jusqu'ici (je vous l'accorde, c'est un acte de bravoure !), voici les #include et dont vous aurez besoin, ainsi qu'un petit main() :)

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <asm/types.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>   

int main(int argc, char *argv[]){
    fill_header_request(argv[1], argv[2]); // argv[1] -> source IP (your IP) argv[2] -> destination IP (your target)
    sendArp("wlan0"); // Change the device name
    return 0;
}

Ajout du 11/01/2013

La source complète du projet (utilisé dans les illustrations suivantes) : arp_forge. On m'a également conseillé de rajouter quelques illustrations, les voici: 192.168.1.51 -> L'adresse IP de mon PC 192.168.1.50 -> L'android que nous voulons ajouter à la liste ARP

Au début, nous avons quelque chose qui ressemble à ca (wireshark est vide, notre table arp incomplète)

Arp empty Arp empty

Après avoir lancé le programme comme ./a.out 192.168.1.51 192.168.1.51 (voir le main donné ci-dessus) qui aura comme conséquence de broadcaster afin d'avoir un retour sur l'addresse MAC de cette IP (obtenir l'adresse MAC à partir de l'adresse IP de l'appareil).

Wireshark dump

AzureWav est la source (mon PC) qui broadcast un "who has" sur tout le réseau local, puis Samsung (l'android) nous réponds en nous donnant son adresse IP, c'est exactement ce que nous voulions, du coté de la table arp, nous avons :

Wireshark dump

En espérant que ces illustrations éclaircissent un peu mes propos confus !