La pile en assembleur x86
Ce tutoriel est susceptible de contenir quelques erreurs, so n'hésitez pas à me les remonter que je les corrige
Ce tutoriel a pour but de présenter quelques aspects fréquemment utilisés de la pile sous un processeur x86 (je ne parlerai que du mode protégé 32bits ici). Je présenterai d'abord quelques généralités que tout le monde connaît (ou presque), avant d'étudier un peu plus en profondeur l'utilisation de la pile lors des appels de fonctions. Ensuite, pour les courageux (et suite aux réclamations/protestations sur IRC), j'ai écrit un petit pavé vite fait sur les conventions d'appel.
I- Introduction / Généralités
La pile est une zone de la mémoire, qui est beaucoup utilisée en assembleur x86. Comprendre son fonctionnement est ainsi primordial pour développer en assembleur, ou encore pour le reverse-engineering. Ce tutoriel ne parlera que du mode protégé en 32bit, utilisé par la plupart des systèmes d'exploitation (le mode réel est très peu utilisé, et je n'ai pas encore étudié l'assembleur x86_64).
La pile est délimitée par deux registres: EBP (pour "Base Pointer"), et ESP (pour "Stack Pointer"), qui contiennent les adresses mémoire de la pile. EBP permettra en général d'accéder à des données stockées sur la pile (les paramètres passés à une fonction par exemple, ou encore les variables locales; ce qu'on étudiera plus en détail dans la suite de ce tuto).
Pour placer des éléments, on peut utiliser l'instruction "push". Cette instruction placera la donnée à l'adresse pointée par esp, avant de décrémenter esp de 4 octets (la plupart des OS modernes ont une pile qui grandit des adresses hautes, ie "grandes", vers les adresses basses, ie "petites"). Ceci est valable quelque soit la taille des données pushées sur la pile (par exemple un "push 42h" stockera en réalité 00000042h sur la pile). L'instruction "pop" elle, place la donnée pointée par esp dans le registre spécifié (ou à l'adresse spécifiée), avant d'incrémenter de 4 octets ESP.
II- La pile et les paramètres de fonctions
Comme je l'ai dit précédemment, on peut passer des paramètres aux fonctions par la pile. Cependant, on ne peut pas accéder directement aux arguments directement avec un "mov eax,[esp]". En effet, le processeur "push" sur la pile l'adresse de retour de la fonction, c'est-à-dire
l'adresse des instructions à exécuter lorsqu'on sera sorti de la fonction (à noter que l'exploitation de buffer overflows consiste juste à écraser cette adresse de retour pour rediriger l'exécution vers un code injecté par l'attaquant). Ainsi, il faut utiliser "mov eax,[esp+4]" pour accéder au premier argument de la fonction (rappelez-vous, on décrémente la pile chaque fois qu'on push quelque chose).
Cependant, si on a besoin de push/pop des éléments à l'intérieur de notre fonction, il nous faudra à chaque fois recalculer les offsets pour accéder aux arguments, ce qui n'est guère pratique. La solution pour pallier ce problème est d'envelopper notre fonction de la sorte:
Ici on place la sauvegarde de ebp sur la pile, puis on place la valeur de esp dans ebp. On pourra ainsi accéder à notre premier argument via [ebp+8] (et oui, on a encore push un truc sur la pile :') ). L'instruction "leave" permet de restaurer le registre esp à la valeur du registre ebp, puis de restaurer ebp (ce qui revient à un "mov esp, ebp - pop ebp". Bref, notre pile ressemble à ça après le prologue de fonction:
Ainsi, durant l'exécution de la fonction, on pourra accéder aux arguments relativement à ebp, qui n'est pas censé bouger pendant l'exécution de la fonction (contrairement à esp). On passera ainsi les arguments à notre fonction comme ceci:
Pour ceux qui aiment les termes techniques, on a mis en place une "stack frame" pour notre fonction.
III- Variables locales aux fonctions
Notre fonction a désormais un "stack frame" en place, ce qui va nous permettre de définir assez facilement des variables locales (visibles uniquement pour la fonction). Il nous suffit de soustraire à esp la taille dont on a besoin pour stocker les arguments, avec ce code:
Notre pile aura alors cette allure:
Et on accèdera aux variables par [ebp-x], et les données que l'on aura besoin de push seront au-dessus des variables locales.
Lorsqu'on sortira de la fonction, les variables resteront présentes sur le "cadavre" de la pile, mais pas directement accessibles, et attendant d'être réécrites (quelle triste fin :') ).
IV- Bonus track: les conventions d'appel
Cette partie (bonus \o/) se concentre sur les différentes manières dont une fonction peut être appelée. En effet, une fonction peut "choisir" de prendre ses paramètres via les registres ou la pile, ou encore de nettoyer la pile, c'est-à-dire d'enlever tous les arguments passés à la fonction lors de la pile. Je vais donc énumérer les conventions les plus courantes:
J'espère que ce tuto vous aura aidé à mieux comprendre les spécificités de la pile sous l'asm x86
Ce tutoriel a pour but de présenter quelques aspects fréquemment utilisés de la pile sous un processeur x86 (je ne parlerai que du mode protégé 32bits ici). Je présenterai d'abord quelques généralités que tout le monde connaît (ou presque), avant d'étudier un peu plus en profondeur l'utilisation de la pile lors des appels de fonctions. Ensuite, pour les courageux (et suite aux réclamations/protestations sur IRC), j'ai écrit un petit pavé vite fait sur les conventions d'appel.
I- Introduction / Généralités
La pile est une zone de la mémoire, qui est beaucoup utilisée en assembleur x86. Comprendre son fonctionnement est ainsi primordial pour développer en assembleur, ou encore pour le reverse-engineering. Ce tutoriel ne parlera que du mode protégé en 32bit, utilisé par la plupart des systèmes d'exploitation (le mode réel est très peu utilisé, et je n'ai pas encore étudié l'assembleur x86_64).
La pile est délimitée par deux registres: EBP (pour "Base Pointer"), et ESP (pour "Stack Pointer"), qui contiennent les adresses mémoire de la pile. EBP permettra en général d'accéder à des données stockées sur la pile (les paramètres passés à une fonction par exemple, ou encore les variables locales; ce qu'on étudiera plus en détail dans la suite de ce tuto).
Pour placer des éléments, on peut utiliser l'instruction "push". Cette instruction placera la donnée à l'adresse pointée par esp, avant de décrémenter esp de 4 octets (la plupart des OS modernes ont une pile qui grandit des adresses hautes, ie "grandes", vers les adresses basses, ie "petites"). Ceci est valable quelque soit la taille des données pushées sur la pile (par exemple un "push 42h" stockera en réalité 00000042h sur la pile). L'instruction "pop" elle, place la donnée pointée par esp dans le registre spécifié (ou à l'adresse spécifiée), avant d'incrémenter de 4 octets ESP.
II- La pile et les paramètres de fonctions
Comme je l'ai dit précédemment, on peut passer des paramètres aux fonctions par la pile. Cependant, on ne peut pas accéder directement aux arguments directement avec un "mov eax,[esp]". En effet, le processeur "push" sur la pile l'adresse de retour de la fonction, c'est-à-dire
l'adresse des instructions à exécuter lorsqu'on sera sorti de la fonction (à noter que l'exploitation de buffer overflows consiste juste à écraser cette adresse de retour pour rediriger l'exécution vers un code injecté par l'attaquant). Ainsi, il faut utiliser "mov eax,[esp+4]" pour accéder au premier argument de la fonction (rappelez-vous, on décrémente la pile chaque fois qu'on push quelque chose).
Cependant, si on a besoin de push/pop des éléments à l'intérieur de notre fonction, il nous faudra à chaque fois recalculer les offsets pour accéder aux arguments, ce qui n'est guère pratique. La solution pour pallier ce problème est d'envelopper notre fonction de la sorte:
Code ASM :
mafonction:
push ebp
mov ebp, esp
... corps de la fonction ...
leave
ret
Ici on place la sauvegarde de ebp sur la pile, puis on place la valeur de esp dans ebp. On pourra ainsi accéder à notre premier argument via [ebp+8] (et oui, on a encore push un truc sur la pile :') ). L'instruction "leave" permet de restaurer le registre esp à la valeur du registre ebp, puis de restaurer ebp (ce qui revient à un "mov esp, ebp - pop ebp". Bref, notre pile ressemble à ça après le prologue de fonction:
Code :
| pile |
+--------------------+ <--- ebp = esp
| sauvegarde de EBP |
+--------------------+ <--- ebp+4
| addresse de retour |
+--------------------+ <--- ebp+8
| argument 1 |
+--------------------+ <--- ebp+0ch
| ... |
+--------------------+ <--- ebp+4+4*n
| argument n |
+--------------------+
Code :
push argumentn
....
push argument1
call mafonction
III- Variables locales aux fonctions
Notre fonction a désormais un "stack frame" en place, ce qui va nous permettre de définir assez facilement des variables locales (visibles uniquement pour la fonction). Il nous suffit de soustraire à esp la taille dont on a besoin pour stocker les arguments, avec ce code:
Code ASM :
mafonction:
push ebp
mov ebp, esp
sub esp, 0x40 ; on réserve 64 octets pour les variables locales
... corps de la fonction ...
leave
ret
Notre pile aura alors cette allure:
Code :
| pile |
+--------------------+ <--- esp
| |
| variables locales |
| ... |
| |
+--------------------+ <--- ebp
| sauvegarde de EBP |
+--------------------+ <--- ebp+4
| addresse de retour |
+--------------------+ <--- ebp+8
| argument 1 |
+--------------------+ <--- ebp+0ch
| ... |
+--------------------+ <--- ebp+4+4*n
| argument n |
+--------------------+
Lorsqu'on sortira de la fonction, les variables resteront présentes sur le "cadavre" de la pile, mais pas directement accessibles, et attendant d'être réécrites (quelle triste fin :') ).
IV- Bonus track: les conventions d'appel
Cette partie (bonus \o/) se concentre sur les différentes manières dont une fonction peut être appelée. En effet, une fonction peut "choisir" de prendre ses paramètres via les registres ou la pile, ou encore de nettoyer la pile, c'est-à-dire d'enlever tous les arguments passés à la fonction lors de la pile. Je vais donc énumérer les conventions les plus courantes:
- stdcall: c'est la convention d'appel par défaut des APIs win32 (exceptés celles du runtime C, msvcrt.dll): les arguments sont passés sur la pile, et la fonction nettoie la pile avant de revenir à la routine appelante.
- cdecl: c'est la convention par défaut utilisée par la stdlib: les arguments sont eux aussi passés par la pile, mais c'est à la fonction appelante de nettoyer la pile après l'appel de la fonction (en incrémentant esp du nombre d'octets pushed sur la pile). Cette convention est utilisée dans la stdlib C
- fastcall/pascal: ces conventions récupèrent les arguments d'abord via les registres, puis récupèrent les arguments supplémentaires (si besoin) sur la pile. La pile est nettoyée par la fonction appelée. On retrouve cette convention d'appel dans les programmes écrits en Pascal (ie Delphi, etc), ou encore lors de l'appel d'un syscall Linux.
J'espère que ce tuto vous aura aidé à mieux comprendre les spécificités de la pile sous l'asm x86
spin
Contributeur Messages : 325 Sujets : 15 Points: 38 Inscription : Nov 2011 |
RE: La pile en assembleur x86
Salut, je trouve le tuto assez clair mais j'ai quelques remarques.
Dans la section I : que veut dire « la pile est délimitée par ESP et EBP » ? Que doivent contenir au juste ces deux registres ? Et on peut se demander aussi pourquoi on alloue 4 octets ; c'est systématique ou ça dépend de la taille de ce qu'on veut passer en argument, ou encore autre chose ? Les lecteurs pourraient se le demander. Dans la section II, dans le code d'exemple, il faut dire à quoi correspond l'instruction 'leave' au cas où on ne la connaîtrait pas, je pense. Dans la section III, tu nous dis comment faire des variables locales dans une fonction. Maisle lecteur peut se demander ce qu'il advient de cette zone pour les variables locales quand on quitte la fonction. Et pour les conventions d'appel avec le C, rien de mieux que de présenter directement l'output des compilos :þ Bon tutoriel sinon |
supersnail
Éleveur d'ornithorynques Messages : 1,613 Sujets : 72 Points: 466 Inscription : Jan 2012 |
RE: La pile en assembleur x86
J'ai modifié le tutoriel en tenant compte des remarques (sauf la partie IV, je laisse le lecteur intéressé se renseigner par lui-même, cette partie servant étant un peu "à côté" du sujet )
Mon blog
Code : push esp ; dec eax ; inc ebp ; and [edi+0x41],al ; dec ebp ; inc ebp "VIM est merveilleux" © supersnail |
phopho
Newbie Messages : 10 Sujets : 1 Points: 0 Inscription : Nov 2012 |
RE: La pile en assembleur x86
Tuto très bien expliqué pour moi.
Merci supersnail |
ark
Psyckomodo! Messages : 1,033 Sujets : 48 Points: 317 Inscription : Sep 2011 |
RE: La pile en assembleur x86
Pour ceux que ça intéresse, je me suis pencher sur l’état de la stack au moment de l'appel de la fonction main() telle que "préparée" par gcc, par contre c'est en 64bits. (je sais pas si c'est différent en 32...)
Code : Highest address J'en suis pas sur a 100%, alors dites moi si je me suis trompé quelque part :) |
supersnail
Éleveur d'ornithorynques Messages : 1,613 Sujets : 72 Points: 466 Inscription : Jan 2012 |
RE: La pile en assembleur x86
Comme je l'ai dit, en 64bits c'est sûrement différent, j'ai aucune compétence là-dedans...
Bref on peut rencontrer ce genre de trucs en 64bits, mais pas en 32 imo
Mon blog
Code : push esp ; dec eax ; inc ebp ; and [edi+0x41],al ; dec ebp ; inc ebp "VIM est merveilleux" © supersnail |
gruik
gouteur de savon Messages : 757 Sujets : 44 Points: 482 Inscription : Oct 2012 |
RE: La pile en assembleur x86
c'est la meme chose en 32 et en 64 pour le coup, juste le nom des registres qui change
|