Jouer avec la pile (en C/ASM sous linux)
Par Rodger le mercredi, avril 7 2010, 22:12 - Lien permanent
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
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
- ...
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)
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
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à !