Introduction aux débordements de buffer
par Stéphane Aubert (06/10/2000)
--[ Introduction
Voici quelques notes qui introduisent et essayent d'expliquer le problème
des débordements de buffer (tampon en français).
Pour comprendre les débordements de buffer il faut avoir quelques bases sur
le fonctionnement d'un microprocesseur, de l'assembleur, des systèmes
d'exploitations, des compilateurs et la programmation.
Pour comprendre vraiment les débordements de buffer il faut en faire.
Tout ce qui est écrit dans ce document est testé sur Linux (Slackware 7).
Certaines choses sont testées sur *BSD, et j'essaierai autant que possible
de faire un parallèle avec Solaris (euh Sparc) et même Windows NT (non non
pas sur Sparc ;-)
--[ Rappels
Lorsqu'une commande Unix est exécutée depuis le shell la fonction C
utilisée est exceve.
Vérification :
% strace ./sp
execve("./sp", ["./sp"], [/* 42 vars */]) = 0
brk(0) = 0x8049548
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40013000
open("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY) = 3
fstat(3, {st_mode=0, st_size=0, ...}) = 0
mmap(0, 19178, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40014000
close(3) = 0
open("/lib/libc.so.6", O_RDONLY) = 3
fstat(3, {st_mode=0, st_size=0, ...}) = 0
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3"..., 4096) = 4096
...
De même avec des paramètres :
% strace ./sp AA BB CC
execve("./sp", ["./sp", "AA", "BB", "CC"], [/* 42 vars */]) = 0
Ces paramètres ainsi que tout l'environnement (résultat de la commande env)
sont accessibles au programme lancé car ils sont stockés dans sa pile.
: :
+----------------+
| tableau env |
+----------------+
| tableau argv |
+----------------+
| argc |
+----------------+
| vars d'enviro. |
+----------------+
| arguments |
+----------------+ <-- base de la pile
Il est possible de s'en persuader avec gdb :
% gdb ./sp
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
...
(gdb) b main
Breakpoint 1 at 0x80483cf: file sp.c, line 7.
par error:
Cannot justify.
...
Breakpoint 1, main (argc=2, argv=0xbffff654) at sp.c:7
7 printf( "SP : 0x%lx\n", get_esp('a','b') );
(gdb) x/3000bx 0xbfffffff-3000
...
0xbffff7b6: 0x77 0x2f 0x2e 0x2f 0x73 0x70 0x00 0x41
0xbffff7be: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff7c6: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff7ce: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff7d6: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff7de: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff7e6: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
On retrouve bien les 'A' (0x41) dans la pile ! (C'est clair non ? ;-) reste
encore à connaître l'adresse de la pile et même savoir ce qu'est une pile,
comment elle fonctionne et comment les processeurs et les compilateurs s'en
servent (voir chapitre rappels sur la pile).
Explication de la commande x/3000bx 0xbfffffff-3000 de gdb :
x : afficher un bout de la mémoire
3000b : afficher 3000 octets (on peut aussi souhaiter des word : 3000w)
x : afficher le résultat en hexa
0xbfffffff-3000 : adresse de début de dump
Pour obtenir les 32 octets (affiché en ascii) à partir de l'adresse
0xbffff7b6 il faut lancer :
(gdb) x/32bc 0xbffff7b6
0xbffff7b6: 119 'w' 47 '/' 46 '.' 47 '/' 115 's' 112 'p' 0 '\000'65 'A'
0xbffff7be: 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A'
0xbffff7c6: 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A'
0xbffff7ce: 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A'
Ou 16 mots (word) affichés en hexa :
(gdb) x/16wx 0xbffff7b6
0xbffff7b6: 0x2f2e2f77 0x41007073 0x41414141 0x41414141
0xbffff7c6: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff7d6: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff7e6: 0x41414141 0x41414141 0x41414141 0x41414141
--[ Rappels sur la pile
Si pour vous une pile est le petit cylindre qui fait marcher plus longtemps
les petits lapins roses, vous allez devoir vous accrocher ;-)
Sinon vous vous souvenez certainement qu'une pile est une structure mémoire
(aisément modélisable en lamda-calcul) qui possède une base et un sommet.
En assembleur le sommet est stocké dans un registre, ESP pour intel ou SP
pour Sparc. On ne peut ajouter et supprimer des informations qu'au sommet
d'une pile.
Sur Unix (sur pc), un programme est exécuté dans un certain environnement.
La gestion paginée de la mémoire fait que le code à exécuter est chargé en
mémoire à une adresse virtuelle qui est toujours la même (souvent) sur un
même système.
Avant l'exécution du programme, pour que tout fonctionne, le système
réserve de la mémoire. Ce programme a besoin de sa propre pile et d'espace
pour allouer de la mémoire (avec par exemple la fonction malloc). Ainsi, on
se retrouve systématiquement dans la configuration suivante :
+----------------+ Mémoire haute exemple : 0xbfffffff
| Pile (Stack) | |
| |<--SP |Sens de remplissage de la pile
| | V
+----------------+
| Données |
| (dont le tas : |
| heap) |
| |
+----------------+
| |
| |
| |
|Code |<--IP (instruction pointer)
+----------------+ Mémoire basse
IP (instruction pointer, eip chez Intel) contient l'adresse de la prochaine
instruction à exécuter.
Je répète : IP contient l'adresse de la prochaine instruction à exécuter.
Je répète : SP contient l'adresse du prochain octet libre dans la pile.
...
Je répète : IP contient l'adresse de la prochaine instruction à exécuter.
Je répète : SP contient l'adresse du prochain octet libre dans la pile.
--[ Rôle et fonctionnement de la pile
La pile allouée à un programme permet principalement l'appel de fonctions en
permettant de sauvegarder des variables (registres) qui pourront ainsi être
modifiées dans la fonction et restaurées à la sortie de la fonction mais
elle sert aussi à passer les paramètres aux fonctions et à allouer les
variables locales. Ces variables locales sont ainsi "protégées" même lors
de fonctions récursives.
Les principales fonctions assembleur pour utiliser la pile sont (chez
Intel) push et pop (respectivement pushl et popl) pour empiler et dépiler
un entier long dans la pile. Il faut aussi bien comprendre que le sommet de
pile est stocké dans un registre (ex: %esp) et qu'en modifiant ce registre
on modifie l'état de la pile.
Ainsi, l'instruction : "subl $1024, %esp" permet d'allouer 1024 octets dans
la pile en augmentant la taille de la pile de 1024 octets. Pour augmenter
la taille de la pile il faut bien diminuer la valeur du registre SP car la
pile est tête-bêche (la tête en bas).
Si on regarde le code asm généré par GCC lors de l'appel de fonctions
et l'allocation de variables locales avec "gcc simple.c -S" il est
relativement simple de comprendre la gestion de la pile.
% cat simple.c
int Func( char a ) {
char tmp[256], b;
b = a;
return 2;
}
int main( void ) {
int x;
x = Func( 0x41 );
return 0x33;
}
Il est aussi possible de voir le code avec la fonction disassemble de gdb :
(gdb) disassemble Func
Dump of assembler code for function Func:
0x8048380 <Func>: push %ebp
0x8048381 <Func+1>: mov %esp,%ebp
0x8048383 <Func+3>: sub $0x108,%esp
0x8048389 <Func+9>: mov 0x8(%ebp),%eax
0x804838c <Func+12>: mov %al,0xffffffff(%ebp)
0x804838f <Func+15>: mov 0xffffffff(%ebp),%al
0x8048392 <Func+18>: mov %al,0xfffffefb(%ebp)
0x8048398 <Func+24>: mov $0x2,%eax
0x804839d <Func+29>: jmp 0x80483a0 <Func+32>
0x804839f <Func+31>: nop
0x80483a0 <Func+32>: leave
0x80483a1 <Func+33>: ret
(gdb) disassemble main
Dump of assembler code for function main:
0x80483a4 <main>: push %ebp
0x80483a5 <main+1>: mov %esp,%ebp
0x80483a7 <main+3>: sub $0x4,%esp
0x80483aa <main+6>: push $0x41
0x80483ac <main+8>: call 0x8048380 <Func>
0x80483b1 <main+13>: add $0x4,%esp
0x80483b4 <main+16>: mov %eax,%eax
0x80483b6 <main+18>: mov %eax,0xfffffffc(%ebp)
0x80483b9 <main+21>: mov $0x33,%eax
0x80483be <main+26>: jmp 0x80483c0 <main+28>
0x80483c0 <main+28>: leave
0x80483c1 <main+29>: ret
Remarque : la fonction main est une fonction comme les autres :) avec les
mêmes problèmes de débordements de buffer !
Pour l'instant les lignes importantes sont :
main : push %ebp permet de sauvegarder %ebp car la ligne suivante écrase
%ebp (Base Pointer)
main+3 : sub $0x4,%esp réserve 4 octets (var locale) dans la pile
main+6 : push $0x41 empile le paramètre de la fonction Func
main+8 : call va dérouter le processeur à l'adresse de Func en ayant
préalablement empiler l'adresse de la prochaine instruction à exécuter
après la fonction, ici : 0x80483b1
Donc à chaque appel de fonction la pile va contenir :
: :<-SP dans la fonction
+----------------+
| var. locale C |
+----------------+
| var. locale B |
+----------------+
| var. locale A |
+----------------+
| base pointer |
+----------------+
| @ de retour |
+----------------+
| paramètre X |
+----------------+
| paramètre Y |
+----------------+
| paramètre Z |
+----------------+
|/ / / / / /|
| / / / / / |
+----------------+ <-- base de la pile = mémoire haute
Avec la fonction Func :
void Func( int X, int Y, int Z ) {
int A; int B; int C;
...
Pour la suite il faudra connaître l'adresse du sommet de la pile même
approximativement. Quand on exécute un programme localement il est simple
même en C d'obtenir le Stack Pointer :
% cat sp.c
long get_esp() {
__asm__("movl %esp,%eax\n");
}
int main( void ) {
printf( "SP : 0x%lx\n", get_esp() );
}
% gcc sp.c -g -o sp
% ./sp
SP : 0xbffff6f0
Attention : c'est sur un Linux !
Remarque : tout le monde a bien compris comment fonctionne la fonction
get_esp. Pas la peine de rappeler que la variable renvoyée par une fonction
est stockée dans le registre AX (heuu %eax c'est plus du 8086 depuis
longtemps ;-)). Donc il suffit de copier SP dans AX et de retourner au
programme principal.
Si on exécute ce programme sur d'autre systèmes on obtient des valeurs
différentes.
Linux mais root : 0xbffff780
root autre env. : 0xbffff7a0
Linux 2.0.36 : 0xbffff9e0
Sur un autre 2.0.36 : 0xbffffbf0
FreeBSD 5.0-CURRENT : 0xbfbffa28
OpenBSD 2.6 : 0xdfbfdce8
OpenBSD 2.7 : 0xdfbfd7a8
NetBSD 1.4Y : 0xbfbfd684
NT4.0 SP4 : 0x240ff98
NT4.0 SP6a : 0x240ff98
Solaris 2.6 (sparc) : 0xeffffcd0
Remarque :
Sous Solaris Sparc il faut changer la fonction get_esp par :
unsigned long get_esp() {
__asm__("or %sp, %sp, %i0");
}
--[ Description du problème de débordement de buffer
Il y a problème lorsqu'il y a débordement. Il y a débordement lorsque le
code est mal écrit.
Ce qui permet d'écrire : Il y a problème lorsque le code est mal écrit !
Le problème se situe plus précisément dans la pile lors, pour un exemple
simple, de l'affectation d'une variable locale. Cette variable locale,
comme nous l'avons vu, est allouée dans la pile.
Pour l'exemple (en langage C ;-)) suivant :
void Func( int X ) {
char tmp[16];
...
}
Lors de l'exécution du programme la place de 16 caractères (consécutifs)
sera réservée dans la pile pour la variable nommée tmp.
Si dans cette même fonction une suite d'instructions vient remplir cette
variable avec plus de 16 caractères (octets), l'emplacement réservé à la
variable tmp va être écrasé mais aussi les octets suivants dans la pile.
: :<-SP dans la fonction
+----------------+
| |<- tmp
| 256 octets |
| | |
+----------------+ |
| base pointer | | Sens de remplissage de la pile
+----------------+ |
| @ de retour | V
+----------------+
| paramètre X |
+----------------+
|/ / / / / /|
| / / / / / |
+----------------+ <-- base de la pile = mémoire haute
Les octets tout de suite écrasés sont le base pointer puis l'adresse de
retour qui sera dépilée lorsque la fonction se terminera. A cet instant, le
programme va exécuter l'instruction qui se trouve à l'adresse de retour.
Qui était je le rappelle l'adresse de l'instruction suivant l'appel de
cette fonction (la prochaine instruction à exécuter).
Si l'adresse de retour est écrasée, par exemple, par des caractères 'A'
l'adresse de la prochaine instruction à exécuter est 0x41414141 (non
valide), on peut dire qu'aucune instruction n'est présente à cette adresse,
donc le programme arrête de fonctionner et plante. Le résultat obtenu est
alors nommé : déni de service.
Si l'adresse de retour (pour faire simple, c'est pas tout à fait comme ça
mais bon ...) est l'adresse de tmp et qu'à cet endroit on trouve des
instructions que le processeur est en mesure d'exécuter alors à la sortie
de la fonction le programme exécute des instructions non prévues par les
programmeurs mais fournies par un utilisateur.
Si cet utilisateur est à l'autre bout d'Internet, il peut alors faire
exécuter du code à distance sur le serveur et c'est ce qui s'appelle
exploiter un débordement de buffer à distance. Les octets qui sont utilisés
pour écraser la pile (ceux qui contiennent des instructions) sont appelés
le shellcode (car ces premiers shellcode permettaient de lancer /bin/sh et
d'avoir un shell interactif à distance).
Dans les 2 cas nous disons que le programme est vulnérable.
Pour se protéger des débordements de buffer il faut absolument faire
attention aux codes sources (faut il encore pouvoir les lire ;-) ).
Plus particulièrement, il faut faire attention à ce genre de programmation :
#include
#include
void main( int argc, char *argv[], char *envp[] ) {
char buf[256];
strcpy( buf, getenv("TERM") );
...
}
La variable d'environnement TERM peut contenir plus de 256 octets ...
void BonneProgrammation ( void ) {
char reponse[16];
printf( "Votre réponse [oui/non]:\n" );
gets( reponse );
printf( "Vous avez dit : %s\n", reponse );
}
int main ( void ) {
BonneProgrammation();
return 0;
}
Compilation : gcc t.c -o t
A l'exécution ça donne :
Premier essai :
% ./t
Votre réponse [oui/non]:
non
Vous avez dit : non
Deuxième essai :
% ./t
Votre réponse [oui/non]:
c'est ça que l'on apprend à l'école ???????
Vous avez dit : c'est ça que l'on apprend à l'école ???????
segmentation fault ./t
Oops ;-)))
Pour trouver des débordements de buffer il n'est pas nécessaire d'avoir les
codes sources, ce n'est pas le cas si l'on souhaite les corriger.
--[ Exercice ;-)
Je l'adore, il est tiré de "Smashing The Stack For Fun And Profit".
Que fait ce programme ? Pourquoi ? Comment fonctionne-t-il ?
/*-----------------------------------*/
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;
ret = buffer1 + 12;
(*ret) += 8;
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
/*-----------------------------------*/
--[ Quelques liens
. Smashing The Stack For Fun And Profit
by Aleph One
ftp://phrack.infonexus.com/pub/phrack/phrack49.tar.gz
. How to write Buffer Overflows
by Mudge
http://www.l0pht.com/advisories/bufero.html
. Writing buffer overflow exploits - a tutorial for beginners
by Mixter
http://mixter.void.ru/exploit.txt
. http://www.bufferoverflow.org/textos.htm
--[ Fin ]------------------------------------------------------------------
Les paragraphes suivants ne sont pas détaillés ici (ils ne font plus partie
de l'introduction) ;-)) :
--[ Écriture du shellcode
. manuellement
. avec hellkit
--[ Écriture de l'exploit
. le programme qui permet d'exploiter un débordement
--[ Autres infos sur les débordements de buffer
. débordement de petits buffer (qui ne permettent pas d'insérer beaucoup
de code)
. écriture de buffer overflow sans utiliser une longue suite de NOP
. débordement de buffer dans le tas (heap buffer overflow)
. débordement de buffer sous windows (et oui ;-))
--[ Comment se protéger des débordements de buffer
. audit de code
. rendre la pile non exécutable (ex sous Solaris)
. attention aux sauts par trampoline
. modifier aléatoirement le SP en début de code
(ex: appel dans une fonction recursive de la fonction main originale)
. voir stackguard and co.
--[ etc.