samedi 26 mars 2011

Fonctionnement de la pile

La pile est un élément crucial dans un programme qui peut être l'objet de beaucoup d'attaques. Il est important de bien comprendre son fonctionnement si on veut être capable d'en exploiter ses vulnérabilités.
Comme je l'ai expliqué dans la Segmentation de la mémoire d’un programme, lors de l'appel d'une fonction une zone de la pile est réservée appelée stack frame. Il permet de stocker tout les éléments nécessaires au bon fonctionnement de la fonction comme ses variables locales et ses arguments, et tout le nécessaire pour remettre la pile et le programme dans son état d'origine lorsque la fonction se sera terminée.

Le nom du segment (stack) lui vient de sa façon de fonctionner, il se comporte comme une pile avec un fonctionnement LIFO (Last In First Out) ou FILO (ce qui revient au même). A chaque fois qu'une fonction est appelée, un stack frame est empilé dans la pile, et à chaque fois qu'une fonction se termine, un stack frame est dépilé.


Voilà l'état de la pile dans le programme de mon précédent post juste avant le retour de la fonction g() appelée dans f().

La pile étant en réalité juste une zone mémoire, le programme a besoin de savoir où se trouve le sommet de la pile. Pour cela un registre du processeur existe et s'appelle ESP qui pointe toujours sur le sommet (paradoxalement le sommet de pile est l'adresse la plus basse de la pile). De la même façon un registre nommé EBP pointe sur le début du stack frame courant (et a donc une adresse plus haute).

Exécution d'une fonction

L'exécution d'une fonction se fait en plusieurs étapes :
  • La préparation des arguments de la fonction
  • L'appel de la fonction
  • Prologue de la fonction qui va permettre de réserver l'espace nécessaire aux variables locales
  • L'exécution de la fonction
  • Le retour de la fonction et la libération du stack frame
Chaque étape va être décrite précisément sur le programme suivant et grâce à gdb muni d'un .gdbinit bien configuré (merci au blog de StalkR pour me l'avoir fait découvrir).
#include <stdio.h>
#include <stdlib.h>
 
void f(int x, int y) {
 int local1 = 1;
 char local2[] = "buffer";
 return;
}
 
int main(int argc, char **argv) {
 f(1,2);
 
 return EXIT_SUCCESS;
}
# gcc stack_frame.c -w -O0 -ggdb -std=c99 -static -D_FORTIFY_SOURCE=0 -fno-pie -Wno-format -Wno-format-security -fno-stack-protector -z norelro -z execstack -o stack_frame
Il est important de bien compiler le programme avec les mêmes options, sinon gcc met certaines protections sur la pile qui vont rendre les choses plus compliquées à comprendre, comme l'inversion ou l'ajout de certaines zones mémoires. Cela s'appelle le SSP ou Stack Smashing Protection, que j'essaierai de développer dans un autre article.

Préparation des arguments de la fonction

On commence par désassembler le programme :
time0ut# gdb -q stack_frame
Reading symbols from time0ut/stack_frame...done.
gdb$ dis main
Dump of assembler code for function main:
   0x080482c0 <+0>: push   ebp
   0x080482c1 <+1>: mov    ebp,esp
   0x080482c3 <+3>: sub    esp,0x8
   0x080482c6 <+6>: mov    DWORD PTR [esp+0x4],0x2
   0x080482ce <+14>: mov    DWORD PTR [esp],0x1
   0x080482d5 <+21>: call   0x80482a0 
   0x080482da <+26>: mov    eax,0x0
   0x080482df <+31>: leave  
   0x080482e0 <+32>: ret 
End of assembler dump.1
gdb$ dis f
Dump of assembler code for function f:
   0x080482a0 <+0>: push   ebp
   0x080482a1 <+1>: mov    ebp,esp
   0x080482a3 <+3>: sub    esp,0x10
   0x080482a6 <+6>: mov    DWORD PTR [ebp-0x4],0x1
   0x080482ad <+13>: mov    DWORD PTR [ebp-0xb],0x66667562
   0x080482b4 <+20>: mov    WORD PTR [ebp-0x7],0x7265
   0x080482ba <+26>: mov    BYTE PTR [ebp-0x5],0x0
   0x080482be <+30>: leave  
   0x080482bf <+31>: ret
End of assembler dump.
gdb$ b *0x080482d5
Breakpoint 1 at 0x080482d5: file stack_frame.c, line 11.
gdb$ r
Breakpoint 1, 0x080482d5 in main (argc=0x1, argv=0xbffff324) at stack_frame.c:11
11  f(1,2);
gdb$ x/2xw $esp
0xbffff280: 0x00000001  0x00000002


Les deux commandes spécifiées en gras permettent de mettre les arguments de la fonction sur la pile. L'argument 2 est mis en premier puis l'argument 1. Lors de l'appel d'une fonction les arguments sont donc mis dans l'ordre inverse.
La commande gdb x/2xw $esp permet d'afficher 2 mots (2*4 octets) en hexadécimal à l'adresse pointée par ESP qui a pour valeur 0xbffff280. On affiche donc 2 éléments de la pile qui sont les arguments 1 et 2.

L'état de la pile avant l'appel de la fonction (c'est à dire avant l'exécution de call 0x80482a0) est donc comme suit :

Appel de la fonction

Une fois les arguments de la fonction empilés, l'appel peut se faire via la commande call, qui se fait à l'adresse 0x080482d5. La commande call a deux objectifs :
  • Sauvegarder dans la pile l'adresse de l'instruction qui suit le call ce qui permettra de reprendre où on en est à la fin de l'exécution de la fonction. Cela se fera grâce au registre EIP qui pointe toujours sur la prochaine instruction à exécuter.
  • Sauter dans le code (segment text) de la fonction en modifiant le registre EIP pour que celui ci pointe sur la première instruction de la fonction et dont l'adresse est passée en paramètre à call.

On vérifie tout cela avec gdb :
gdb$ print/x $eip
$1 = 0x80482d5  # EIP pointe bien pour le moment sur l'instruction call
=> 0x80482d5 
: call 0x80482a0 0x80482da
: mov eax,0x0 0x80482df
: leave
gdb$ stepi # On exécute le call et donc on rentre dans la fonction gdb$ print/x $eip $2 = 0x80482a0 # Après le call EIP pointe sur la première instruction de f => 0x80482a0 : push ebp 0x80482a1 : mov ebp,esp 0x80482a3 : sub esp,0x10 gdb$ x/3xw $esp 0xbffff27c: 0x080482da 0x00000001 0x00000002 # Le sommet de pile a bougé et on a maintenant l'adresse de la prochaine instruction a exécuter après f

On peut remarquer que la prochaine instruction à exécuter après la fonction (0x080482da) se trouve 5 octets plus haut que l'instruction du call. C'est toujours le cas, car la taille de l'instruction call est de 5 octets.

Après le call, la tête de notre pile est donc la suivante :

Prologue de la fonction

Le prologue de la fonction va permettre de réserver l'espace nécessaire pour stocker les variables locales. Il est constitué de 3 instructions :
  • push ebp
  • Cette instruction va permettre de sauvegarder le registre EBP qui pointe encore sur le début du stack frame de la fonction appelante. Cela permettra de le restaurer après la fin de la fonction courante.
  • mov ebp,esp
  • Cette instruction permet de dire au programme que le début du stack frame commence ici. Heureusement on vient de sauvegarder EBP, donc on a pas perdu sa valeur.
  • sub esp,0x10
  • Cette instruction permet de réserver l'espace nécessaire aux variables locales de la fonction. Dans notre cas, on réserve 0x10 soit 16 octets. En réalité nous n'aurions besoin de réserver que 12 octets (local1 fait 4 octets et local2 7 octets, mais comme on fonctionne par mot de 4 octets, cela fait 12). Cependant le compilateur gcc alloue un supplément de mémoire. Il faut noter que comme la pile grandit vers les adresses basses, la réservation d'espace se fait via une soustraction et non une addition.
On vérifie tout cela avec gdb:
gdb$ print/x $ebp
$3 = 0xbffff288
gdb$ stepi # Exécution de push ebp
gdb$ x/4xw $esp
0xbffff278: 0xbffff288 0x080482da x00000001 0x00000002 # Le sommet de pile a bougé et on a maintenant l'adresse de EBP
gdb$ stepi # Exécution de mov ebp,esp
gdb$ x/4xw $ebp
0xbffff278: 0xbffff288 0x080482da x00000001 0x00000002 # EBP et ESP pointent sur la même zone mémoire
gdb$ stepi # Exécution de sub esp,0x10
gdb$ print/d $ebp-$esp
$4 = 16 # On vient de réserver 16 octets
gdb$ stepi # On passe toutes les inialisations des variables locales
gdb$ stepi
gdb$ stepi
=> 0x80482be : leave  
   0x80482bf : ret    
   0x80482c0 
: push ebp
gdb$ x/8xw $esp 0xbffff268: 0x080cd0ac 0x66756200 0x00726566 0x00000001 0xbffff278: 0xbffff288 0x080482da 0x00000001 0x00000002

La valeur 0x080cd0ac sur le sommet de pile est une valeur ajoutée par gcc. Elle ne nous intéresse pas. Les valeurs 0x66756200 et 0x00726566 sont en réalité la variable locale appelée local2 dans f() : 0x66 (f) 0x75 (u) 0x62 (b) 0x72 (r) 0x65 (e) 0x66 (f). La valeur suivante est la variable local1.


gdb$ x/12b 
0xbffff26c: 0x00 0x62 0x75 0x66 0x66 0x65 0x72 0x00
0xbffff274: 0x01 0x00 0x00 0x00


Comme on peut le voir ici, et plus particulièrement à l'adresse 0xbffff274, les octets sont stockés en mémoire dans le sens inverse, c'est à dire que les octets de poids faible sont stockées en premier. Ceci est dû au fait je tourne sur une architecture x86 qui est en little endian.

Voilà ce que donne la mémoire si on précise l'ordre des octets :

Si la variable local2 commence à l'adresse 0xbffff26d et non à l'adresse 0xbffff26c c'est car le compilateur connaît exactement la taille du buffer qui fait 7 octets (taille de la chaine "buffer" + \0). L'octet 0x00 de l'adresse 0xbffff26c, est juste du padding.

Retour de la fonction et libération du stack frame

Le retour d'une fonction se fait par les instructions leave puis ret.
  • Instruction leave
  • L'instruction leave a pour objectif de supprimer le stack frame, c'est à dire de rétablir les registres ESP et EBP à leur valeur avant le call. Elle est équivalente aux deux commandes suivantes : 
    • mob esp,ebp qui aurait pour objectif de remettre ESP à la même valeur que EBP (c'est à dire pointant sur la zone mémoire contenant l'ancienne valeur de EBP).
    • pop ebp qui aurait pour objectif de remettre l'ancienne valeur de EBP dans EBP, et du coup de décrémenter ESP de 4 octets pour qu'il pointe sur l'instruction suivant le call
    Bien entendu le leave se fait en une seule instruction, du coup l'état intermédiaire présenté dans la première figure n'existe pas réellement.
  • Instruction ret
  • L'instruction ret a pour objectif de remettre le registre EIP à la valeur qui pointe sur l'instruction après le call. Suite au leave, ESP pointe sur une zone mémoire contenant l'adresse de l'instruction suivant le call. Elle est donc équivalent à l'instruction pop eip.

Même si les variables locales sont toujours présentes dans la pile, elles sont considérées comme de l'espace vide. La pile se retrouve donc dans l'état qui était le sien avant le call. La fonction appelante (ici main) a en charge de nettoyer les paramètres.

Cet article permet d'expliquer précisément le fonctionnement de la pile. Il peut y avoir certaines différences en fonction de l'architecture sur laquelle tourne le programme, du compilateur ou même des options de compilation. Cependant l'idée principale reste la même. La compréhension de cet article sera nécessaire pour la compréhension de quelques uns de mes prochains articles.

mercredi 23 mars 2011

Segmentation de la mémoire d'un programme

Quand on s'essaye à l'exploitation de binaire, il est impératif de connaître parfaitement comment fonctionne un programme et comment sa mémoire est segmentée.
La mémoire d'un programme est divisée en plusieurs segments :
  • text
  • data
  • bss
  • heap (ou tas en français)
  • stack (ou pile en français)
Chaque segment contient des données bien précises.

Le segment text contient les instructions du programme avec toute sa logique. Ce segment est en lecture seule car le code n'est pas modifié lors de l'exécution. Cela permet d'avoir plusieurs processus qui partagent cette même zone. Quand plusieurs utilisateurs exécutent la commande ls en même temps par exemple, le code de la commande contenu dans le segment text n'est pas dupliqué, il est partagé par tous les processus. Cela ne pose aucun problème car cette zone ne peut être modifiée.

Les segments bss et data permettent de stocker les variables globales et statiques du programme. Les variables initialisées sont stockées dans le segment data alors que les variables non initialisées sont situées dans le segment bss. Contrairement au segment text, les segments bss et data ne sont pas en lecture seule (le programme peut modifier ses variables globales et statiques au cours de son exécution), par contre leur taille est fixe puisque connue dès la compilation du programme.

Le segment heap est une zone qui va permettre l'allocation de mémoire de façon dynamique, via les fonctions bien connues de type malloc. Ce segment a donc une taille variable car les zones mémoires vont pouvoir être allouées/désallouées dynamiquement en fonction des besoins du programme.

Le segment stack ou pile a pour objectif de stocker les variables locales des fonctions ainsi que le contexte de ces dernières (arguments...). A chaque fois qu'une fonction est appelée par un programme, une zone mémoire lui est allouée dans la pile on appelle ça un stack frame. Chaque appel de fonction produit un nouveau stack frame propre à cet appel, ce qui permet par exemple d'avoir des contextes complètement différents pour la même fonction et donc des comportements différents. Quand la fonction se termine, le stack frame est détruit. La pile est aussi de taille variable, car il n'est pas possible de savoir quelles fonctions et combien de fois elles seront appelées. Au fur est à mesure des appels de fonction la pile va grandir et diminuer. Contrairement au segment heap, la pile grandit vers les adresses basses : Plus les stack frames se rajoutent et plus leurs adresses sont basses.

Le programme suivant va permettre de mettre en évidence les différentes explications que je viens de donner (il est largement commenté) :
#include <stdio.h>
#include <stdlib.h>
 
// Variables globales non initialisées qui vont dans le segment BSS
int global_a;
static static_a;
 
// Variables globales initialisées qui vont dans le segment DATA
int global_b = 1;
static int static_b = 2;
 
void g(void);
 
void f(void) {
 // Variable locale de f qui va dans le segment STACK
 int local_f_a= 1;
 
 // Variable statique initialisée, donc va dans le segment DATA
 // La valeur est commune lors de tous les appels de fonction
 static int static_f_c = 3;
 
 printf("Addresse Variable local_f_a : %08x\n",&local_f_a);
 printf("Addresse Variable static_f_c : %08x\n",&static_f_c);
 
 g();
 
 return;
}
 
void g(void) {
 // Variable locale de g qui va dans le segment STACK
 int *local_g_a = NULL;
 
 local_g_a = (int*)malloc(sizeof(int));
 
 printf("Addresse Variable local_g_a : %08x\n",&local_g_a);
 printf("Addresse pointee par local_g_a : %08x\n",local_g_a);
 
 free(local_g_a);
 
 return;
}
 
int main(int argc, char **argv) {
 // Variable locale de main qui va dans le segment STACK
 // main est une fonction comme une autre
 int local_main_a = 10;
 
 printf("Addresse Variable local_main_a : %08x\n",&local_main_a);
 printf("Addresse Variable global_a : %08x\n",&global_a);
 printf("Addresse Variable static_a : %08x\n",&static_a);
 printf("Addresse Variable global_b : %08x\n",&global_b);
 printf("Addresse Variable static_b : %08x\n",&static_b);
 
 f();
 g();
 
 return EXIT_SUCCESS;
}

L'exécution du programme donne :

# ./memory_segment
Addresse Variable local_main_a : bff082dc
Addresse Variable global_a : 0804a034
Addresse Variable static_a : 0804a030
Addresse Variable global_b : 0804a01c
Addresse Variable static_b : 0804a020
Addresse Variable local_f_a : bff082ac
Addresse Variable static_f_c : 0804a024
Addresse Variable local_g_a : bff0827c
Addresse pointee par local_g_a : 09e15008
Addresse Variable local_g_a : bff082ac
Addresse pointee par local_g_a : 09e15008
Les variables dont l'adresse est la plus basse sont les variables global_b, static_b et static_f_c. Normal, comme le montre le schéma plus haut, ce sont des variables globales et statiques initialisées, donc leur place est dans le segment data, la zone mémoire la plus basse après le code du programme. La variable static_f_c même si elle se trouve dans une fonction est d'abord une variable statique. Toutes les fonctions f() appelées utilisent la même zone mémoire pour cette variable. Une modification dans un appel, se verra donc dans l'appel suivant.

Ensuite viennent les variables global_a et static_a des variables globales non initialisées, donc dans le segment bss.

La variables locales ont une adresse beaucoup plus grande, commençant par 0xbff. Si on regarde l'ordre d'appel des fonctions, main() est créée avant f(). Du coup comme dit précédemment les variables de main() ont une adresse plus haute car la pile grandit vers les adresses basses. f() appelant g(), les variables de f() ont une adresse plus haute que celles de la fonction g() appelée dans f(). Par contre, lors de l'appel de g() dans main(), on voit que local_g_a a exactement la même adresse qu'avait local_f_a. Cela ne pose aucun problème, la fonction f() est totalement terminée, du coup son stack frame a été détruit, et le stack frame de g() s'est créée au même endroit.

La variable local_g_a est une variable locale à g() donc située dans la pile. Par contre malloc alloue une zone mémoire qui elle pointe dans tas (ou heap). La zone mémoire qui stocke local_g_a est dans la pile, et l'adresse contenue dans cette zone mémoire pointe vers le tas.