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.