En guise d'introduction aux prochains billets sur le buffer-overflow, et pour bien comprendre comment fonctionne la pile, voici ce premier billet qui devrait clarifier les choses.

La pile

Il s'agit d'une zone de mémoire utilisée pour mémoriser des données temporairement:

  • lorsque l'on appelle un sous-programme, l'adresse à laquelle il faudra revenir pour continuer le déroulement du programme
  • les variables locales à ce sous-programme
Ces stockages sont temporaires car lorsque l'on sort du sous-programme ces données sont dépilées et donc perdues définitivement. C'est la raison pour laquelle il ne faut jamais retourner l'adresse d'une variable locale: elle est inutilisable hors du sous-programme où elle est définie.

Sur les architectures Intel c'est le registre ESP qui sert à accéder au sommet de la pile. Plus on a de variables locales et/ou d'appels imbriqués à des fonctions (par exemple pour de la récursivité), plus la pile grossit. Son sommet progresse vers les adresses décroissantes (l'adresse pointée par le registre ESP est de plus en plus basse).

Initialement la pile contient dans l'ordre (en commençant par les adresses hautes):

  • 4 octets nulls
  • le nom du programme
  • les variables d'environnement
  • les arguments du main
  • d'autres données
  • l'adresse de retour lorsque l'on sort du main (et qu'on retourne à la libc)
  • les variables locales au main
  • ...
En assembleur, les instructions pushl et popl permettent respectivement d'empiler et dépiler une donnée de 4 octets. Lorsqu'on empile, le registre esp est décrémenté de 4 et la donnée est écrite à cette nouvelle adresse. Lorsqu'on dépile c'est l'inverse qui se produit. L'instruction call sert à appeler un sous-programme : l'adresse de l'instruction qui succède au call est empilée (pour qu'on puisse y revenir plus tard) et le programme saute à l'adresse passée en paramètre au call. Inversement, l'instruction ret saute à l'adresse qu'elle dépile.

Comment connaître l'adresse à laquelle se trouve la pile ?

Première chose à savoir : lorsqu'une fonction retourne une valeur qui tient dans 32 bits (un int par exemple) gcc utilise le registre eax pour transmettre cette valeur à l'appelant. Par exemple, dans la fonction suivante (et sans optimisation particulière du compilateur, la valeur 1234 va être placée dans le registre eax, qui sera utilisée (ou pas) dans le code appelant:
int f() {
  return 1234;
}
Donc une solution pour connaître l'adresse du sommet de la pile peut s'écrire:
unsigned long* getesp() {
  __asm__("movl %esp, %eax");
}
Dans ce code, on appelle de l'assembleur en ligne (pour beaucoup plus d'infos voir http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html). Le code met juste la valeur actuelle du registre esp (le sommet de la pile) dans le registre eax (la valeur de retour de la fonction getesp). Donc on peut utiliser la fonction ainsi:
  printf("Ma pile est à l'adresse %x\n", getesp());

Jouons avec la pile

Que faut-il écrire comme code dans la fonction "function()" pour qu'à l'exécution le programme affiche que x vaut 12 (au lieu de 1) ?
#include <stdio.h>
unsigned long* getesp() {
  __asm__("movl %esp, %eax");
}

void function() {
  unsigned long* stack = getesp();
  // QUE MANQUE-T-IL ICI ???
}

void main() {
  int x;
  x = 12;
  function();
  x = 1;
  printf("maintenant x == %d\n",x);
}

Créons l'exécutable:
$ gcc stack_trick.c -o stack_trick

Si on l'exécute, l'affichage indiquera que x vaut 1...

Regardons le code machine produit par GCC

Pour cela utilisons la commande objdump ( -D pour désassembler, et le grep pour ne lister que les 7 lignes qui suivent celle contenant "<function>:" ) :
$ objdump -d stack_trick | grep \<function\>\: -A 7
08048237 <function>:
 8048237:    55                       push   %ebp
 8048238:    89 e5                    mov    %esp,%ebp
 804823a:    83 ec 10                 sub    $0x10,%esp
 804823d:    e8 ee ff ff ff           call   8048230 <getesp>
 8048242:    89 45 fc                 mov    %eax,-0x4(%ebp)
 8048245:    c9                       leave 
 8048246:    c3                       ret

Sur chaque ligne, les informations présentées par objdump sont ici en 3 colonnes :
  • l'adresse de chaque instruction. Cette adresse sera ensuite modifiée (on dit "relogée") lors du chargement du programme (et ne sera donc jamais celle affichée ici)
  • une ou plusieurs valeurs hexadécimales, ce sont les opcodes de l'instruction
  • l'instruction assembleur avec éventuellement une sorte de commentaire (pour le call, on a l'information que l'adresse à laquelle on saute est en fait la fonction getesp)
Le code que l'on va ajouter, va venir s'intercaler entre le mov et le leave.

Concernant les instructions qui modifient la pile, on repère:
  • 1 push (4 octets)
  • 1 sub de 16 octets (0x10)
  • 1 call (qui aura pour effet de descendre la pile de 4 octets supplémentaires)
  • à cela il faut ajouter encore 4 octets à cause du call vers function() depuis le main
Ce qui nous fait un total de 28 octets (ou 7 longs) entre le sommet de la pile renvoyé par getesp() et le sommet de la pile dans le main juste avant d'appeler function().

On sait donc maintenant que l'adresse à laquelle le programme ira quand on sortira de function() est stockée en "*(stack+7)". Nous devons donc modifier cette adresse pour qu'elle indique l'instruction C après le "x=1;".

Pour savoir de combien d'octets il faut augmenter l'adresse de retour de function(), regardons le code machine du main:
$ objdump -d stack_trick | grep \<main\>\: -A 16
080483db <main>:
 80483db:    55                       push   %ebp
 80483dc:    89 e5                    mov    %esp,%ebp
 80483de:    83 e4 f0                 and    $0xfffffff0,%esp
 80483e1:    83 ec 20                 sub    $0x20,%esp
 80483e4:    c7 44 24 1c 0c 00 00     movl   $0xc,0x1c(%esp)
 80483eb:    00
 80483ec:    e8 da ff ff ff           call   80483cb <function>
 80483f1:    c7 44 24 1c 01 00 00     movl   $0x1,0x1c(%esp)
 80483f8:    00
 80483f9:    b8 d0 84 04 08           mov    $0x80484d0,%eax
 80483fe:    8b 54 24 1c              mov    0x1c(%esp),%edx
 8048402:    89 54 24 04              mov    %edx,0x4(%esp)
 8048406:    89 04 24                 mov    %eax,(%esp)
 8048409:    e8 ee fe ff ff           call   80482fc <printf@plt>
 804840e:    c9                       leave 
 804840f:    c3                       ret

On y voit le call function() suivi immédiatement par un movl qui met la valeur 1 dans une variable locale qui se situe en 0x1c(%esp), il s'agit de la variable x. Cette instruction est traduite par les opcodes suivants:
c7 44 24 1c 01 00 00 00
qui occupent un total de 8 octets (attention objdump les affiche sur deux lignes, il y en a donc bien 8 et pas seulement 7).

Pour éviter d'exécuter cette instruction lorsque l'on reviendra du call, il faut donc que l'adresse de retour ne pointe pas sur la ligne du mov mais 8 octets plus loin. On peut donc maintenant compléter function():
void function() {
  unsigned long* stack = getesp();
  *(stack+7) += 8;
}

Et voilà !