Après une introduction sur l'utilité et le fonctionnement de la pile, introduction dans laquelle nous avons joué à modifier le déroulement normal d'un programme C, nous allons voir d'un peu plus près le problème du buffer overflow et ce que l'on entend par un exploit de buffer overflow.

Au programme dans ce post : le principe général et l'étude du programme "vulnérable" qui sera la cible de notre attaque dans les prochains posts.

Principe

Le principe d'un buffer overflow est le suivant :
  • un programme copie des données dans un buffer local sans s'assurer que la place allouée est suffisante pour les accueillir (notre programme d'exemple fera un simple strcpy au lieu d'un strncpy, mais il aurait pu faire un fscanf(in,"%s",buf); ou autre)
  • dans la plupart des cas, déborder de la zone mémoire va provoquer une erreur de segmentation, dans d'autres cas cela va corrompre des données
  • nous verrons par la suite qu'en fournissant les données adéquates au programme, on peut réussir à exécuter le code machine que l'on veut, et ainsi lancer un programme quelconque (un shell par exemple) par l'intermédiaire du programme défaillant (au lieu de bêtement le faire planter)

Le gros problème de sécurité se pose lorsque le programme vulnérable possède le droit "setuid root" et qu'un utilisateur de base exploite le buffer overflow pour obtenir un shell root. Une autre exploitation de cette vulnérabilité consiste à faire exécuter non pas un shell mais du code qui va ouvrir une connexion sur un port de la machine et ensuite exécuter des commandes envoyées depuis une machine distante ("remote shell").

Notre but dans cette série de billets est de réussir à ouvrir un shell root en passant des données corrompues à un programme vulnérable qui s'exécute en mode setuid root.

Le programme vulnérable

Pour fixer les idées et se donner un exemple le plus simple possible, nous allons utiliser le programme suivant qui fait l'erreur de ne pas vérifier que la chaîne passée en 1er argument du programme ne fait pas plus que 7 caractères (+ l'octet nul de fin de chaîne).

#include <stdio.h>
#include <string.h>

int main(int argc, char** argv) {
    char name[8];
    if (argc == 2) {
        strcpy(name,argv[1]);
        printf("hello %s !\n",name);
    }
    puts("bye bye");
    return 0;
}

Faire planter le programme

Compilons ce programme avec les informations de débug (qui serviront plus bas) :
        $ gcc -g vulnerable.c -o vulnerable

Lorsque l'on lance ce programme avec en premier argument une chaîne de caractères trop longue, on obtient une erreur de segmentation:

        $ ./vulnerable Rodger
        hello Rodger !
        bye bye
        $ ./vulnerable ABCDEFGHIJKLMNOPQRSTUVWX
        hello ABCDEFGHIJKLMNOPQRSTUVWX !
        Segmentation fault
Le programme fait cette erreur car le strcpy a copié plus de caractères que le tableau name ne pouvait en accueillir et les caractères en trop sont allés écraser l'adresse de retour de la fonction main(). Le main s'exécute donc parfaitement, en faisant correctement le printf, mais lorsque l'on doit revenir à l'appelant (la libc) l'adresse à laquelle on aurait dû retourner a été corrompue et l'on saute à une adresse invalide. Un exploit de buffer overflow consiste à passer au programme vulnérable une chaîne de caractères spécialement écrite pour que l'on ne saute pas n'importe où dans la mémoire mais au contraire à l'adresse où se trouve un peu de code machine qui, par exemple, lancera un shell.

Comprendre ce plantage

Lancer "ddd" (d'où le flag "-g" de la compilation précédente).
        $ ddd ./vulnerable
Dans le menu "Source", cocher "Display Machine Code". On obtient ainsi la correspondance entre le code C et le code assembleur. Dans cette zone contenant le code assembleur, poser un point d'arrêt sur l'instruction "ret". Exécuter le programme avec en lui passant la chaîne de caractères "ABCDE...WX" en argument : dans le menu "Program" cliquer sur "Run..." et saisir la chaîne complète.
Le programme s'exécute jusqu'au point d'arrêt. Dans la sous-fenêtre gdb, taper la commande suivante :
        (gdb) x $esp
On obtient l'affichage suivant :
        0xbffff44c:        0x58575655
qui nous indique que la pile (le registre esp) pointe à l'adresse 0xbffff44c, et que l'on trouve à cette adresse les 4 octets 0x58, 0x57, 0x56 et 0x55 qui sont respectivement les codes ASCII des caractères 'X', 'W', 'V' et 'U' (man ascii pour le vérifier).
Exécuter l'instruction "ret" en cliquant sur "NEXTI" (ou en tapant "nexti" dans la sous-fenêtre gdb), l'erreur de segmentation se produit car on a essayé de sauter à l'adresse 0x58575655 qui n'est pas accessible. En tapant la commande suivante (dans la sous-fenêtre gdb) :
        (gdb) x $eip
on en a la confirmation : eip est le registre qui contient l'adresse de l'instruction à exécuter (EIP = Extended Instruction Pointer).

En passant la bonne chaîne de caractères en 1er argument du programme vulnérable, il est donc possible de faire sauter l'exécution à (presque) n'importe quelle adresse, si notre code "malicieux" se trouve à cette adresse c'est gagné... C'est ce que nous verrons dans les prochains posts...