Les fonctions sont essentielles en assembleur et en reverse engineering. Pour analyser correctement un programme, il faut savoir reconnaître un appel de fonction, retrouver ses paramètres, comprendre sa stack frame et identifier sa valeur de retour.
1. La stack
La stack est utilisée notamment pour :
- les adresses de retour ;
- les registres sauvegardés ;
- certaines variables locales ;
- les paramètres supplémentaires ;
- les zones temporaires utilisées pendant les appels.
Sur x86-64, la stack grandit généralement vers les adresses plus basses.
Code: Select all
Adresse haute
+----------------------+
| anciennes données |
+----------------------+
| adresse de retour |
+----------------------+
| variables locales |
+----------------------+ <- RSP
Adresse basse
`push` place une valeur sur la stack.
Code: Select all
push rax
Code: Select all
RSP = RSP - 8
[RSP] = RAX
Code: Select all
pop rax
Code: Select all
RAX = [RSP]
RSP = RSP + 8
3. CALL — appeler une fonction
L'instruction :
Code: Select all
call my_function
- elle sauvegarde sur la stack l'adresse de l'instruction suivante ;
- elle transfère l'exécution vers la fonction appelée.
Code: Select all
push adresse_instruction_suivante
jmp my_function
4. L'adresse de retour
Supposons :
Code: Select all
401000: call test
401005: mov eax, 10
Code: Select all
401005
La fonction `test` peut ensuite s'exécuter, puis `ret` utilisera cette adresse pour revenir à l'instruction suivante.
5. RET — revenir à l'appelant
L'instruction :
Code: Select all
ret
Conceptuellement :
Code: Select all
RIP = [RSP]
RSP = RSP + 8
Code: Select all
call
ret
6. Création d'une stack frame
Une fonction peut réserver une zone de stack qui lui est propre.
Un prologue classique ressemble à :
Code: Select all
push rbp
mov rbp, rsp
sub rsp, 20h
`mov rbp, rsp` crée un repère stable pour la frame courante.
`sub rsp, 20h` réserve 32 octets supplémentaires.
7. RBP et RSP
RSP représente le sommet courant de la stack.
RBP peut servir de repère fixe pour retrouver les données de la fonction.
Exemple :
Code: Select all
mov eax, [rbp-4]
mov ecx, [rbp-8]
Code: Select all
RBP --------> repère fixe
|
| [RBP-4] variable locale
| [RBP-8] autre variable
|
RSP --------> sommet actuel
8. LEAVE
À la fin d'une fonction utilisant RBP, on peut rencontrer :
Code: Select all
leave
ret
Code: Select all
mov rsp, rbp
pop rbp
9. Convention d'appel Windows x64
Sous Windows x64, les quatre premiers paramètres entiers ou pointeurs sont généralement passés dans :
Code: Select all
RCX = paramètre 1
RDX = paramètre 2
R8 = paramètre 3
R9 = paramètre 4
La valeur de retour entière ou pointeur est généralement placée dans :
Code: Select all
RAX
10. Exemple avec VirtualAlloc
Pour :
Code: Select all
VirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect)
Code: Select all
RCX = lpAddress
RDX = dwSize
R8 = flAllocationType
R9 = flProtect
Code: Select all
RCX = 0
RDX = 1000h
R8 = 3000h
R9 = 4
call VirtualAlloc
Code: Select all
RAX = adresse retournée
11. Shadow Space sous Windows x64
La convention Microsoft x64 impose à l'appelant de réserver 32 octets de stack pour la fonction appelée.
Cette zone est appelée :
Code: Select all
shadow space
home space
On peut voir par exemple :
Code: Select all
sub rsp, 20h
call fonction
add rsp, 20h
12. Paramètres supplémentaires
Pour :
Code: Select all
foo(a, b, c, d, e, f)
Code: Select all
RCX = a
RDX = b
R8 = c
R9 = d
e et f -> stack
Pour retenir simplement :
Code: Select all
arguments 1 à 4 -> registres
arguments 5+ -> stack
La convention Windows x64 impose également des contraintes d'alignement.
Une valeur fondamentale à retenir est :
Code: Select all
16 octets
C'est une raison pour laquelle on peut rencontrer des tailles comme :
Code: Select all
sub rsp, 28h
sub rsp, 38h
- shadow space ;
- variables locales ;
- sauvegardes ;
- alignement.
Une fonction qui retourne un entier ou un pointeur utilise généralement :
Code: Select all
RAX
Code: Select all
mov eax, 5
ret
Code: Select all
return 5;
Code: Select all
xor eax, eax
ret
Code: Select all
return 0;
Code: Select all
XMM0
Sous Windows x64, certains registres peuvent être librement modifiés par une fonction appelée.
Parmi les volatiles :
Code: Select all
RAX
RCX
RDX
R8
R9
R10
R11
Parmi les non volatiles :
Code: Select all
RBX
RBP
RSI
RDI
R12
R13
R14
R15
Code: Select all
push rbx
...
pop rbx
Sous Linux x86-64 et d'autres systèmes utilisant System V, les paramètres entiers/pointeurs sont généralement passés dans :
Code: Select all
RDI = paramètre 1
RSI = paramètre 2
RDX = paramètre 3
RCX = paramètre 4
R8 = paramètre 5
R9 = paramètre 6
Code: Select all
Windows x64
1 RCX
2 RDX
3 R8
4 R9
System V
1 RDI
2 RSI
3 RDX
4 RCX
5 R8
6 R9
17. LEA et les adresses
L'instruction :
Code: Select all
lea rcx, [message]
À comparer avec :
Code: Select all
mov rcx, [message]
On peut retenir :
Code: Select all
LEA -> adresse
MOV avec [] -> contenu mémoire
18. Appels de fonctions externes
L'assembleur peut appeler des fonctions provenant de bibliothèques externes.
Exemple :
Code: Select all
call printf
Sous Windows x64 :
Code: Select all
lea rcx, [message]
call printf
19. GOT et adressage RIP-relative sous Linux
Avec les bibliothèques partagées sous Linux, on peut rencontrer la :
Code: Select all
GOT = Global Offset Table
En x86-64, l'adressage relatif à RIP est également fréquent :
Code: Select all
lea rax, [rip + offset]
En reverse, `[rip+...]` désigne donc souvent une donnée ou une adresse proche du code courant.
20. Fonctions variadiques
Certaines fonctions acceptent un nombre variable d'arguments.
Exemple :
Code: Select all
printf("%d %d", a, b);
Code: Select all
RCX = adresse de la chaîne de format
RDX = a
R8 = b
Pour le reverse, le point principal est qu'un appel à `printf` peut utiliser plusieurs registres et éventuellement la stack selon le nombre de paramètres.
21. Une stack frame plus complète
Une stack frame peut contenir plusieurs catégories de données :
Code: Select all
+---------------------------+
| paramètres supplémentaires|
+---------------------------+
| adresse de retour |
+---------------------------+
| ancien RBP |
+---------------------------+
| variables locales |
+---------------------------+
| registres sauvegardés |
+---------------------------+
| zones temporaires |
+---------------------------+
Il ne faut donc pas apprendre un unique schéma comme s'il était toujours identique.
22. Fonction simple sans stack frame
Code C :
Code: Select all
int add(int a, int b)
{
return a + b;
}
Code: Select all
mov eax, ecx
add eax, edx
ret
Code: Select all
ECX = a
EDX = b
EAX = a
EAX += b
return EAX
23. Exemple : fonction MAX
Code C :
Code: Select all
long max(long a, long b)
{
if (a > b)
return a;
return b;
}
Code: Select all
mov rax, rcx
cmp rcx, rdx
jg done
mov rax, rdx
done:
ret
- les paramètres dans RCX et RDX ;
- la comparaison ;
- la valeur de retour dans RAX.
Dans un debugger :
Step Into entre dans la fonction appelée.
Step Over exécute la fonction sans entrer dans son détail et s'arrête à l'instruction suivante.
Pour le reverse :
Code: Select all
Step Into -> fonction intéressante
Step Over -> fonction que l'on ne veut pas analyser maintenant
Supposons :
Code: Select all
00007FF6...1000 call test
00007FF6...1005 mov ebx, eax
Code: Select all
RSP
RCX
RDX
R8
R9
Code: Select all
00007FF6...1005
Après le `ret`, RIP revient à cette adresse.
26. Lire une stack frame
Exemple :
Code: Select all
push rbp
mov rbp, rsp
sub rsp, 30h
mov [rbp-8], rcx
mov [rbp-10h], rdx
Code: Select all
RBP = repère
[RBP-8] = copie du paramètre 1
[RBP-10] = copie du paramètre 2
27. Récursion
Une fonction récursive s'appelle elle-même.
Exemple :
Code: Select all
factorial(5)
-> factorial(4)
-> factorial(3)
-> factorial(2)
-> factorial(1)
Les retours se produisent ensuite dans l'ordre inverse.
28. Exemple : factorielle récursive
Code C :
Code: Select all
long long factorial(long long n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
Code: Select all
factorial:
cmp rcx, 1
jg recurse
mov eax, 1
ret
recurse:
push rcx
dec rcx
call factorial
pop rcx
imul rax, rcx
ret
29. Récursion et consommation de stack
Chaque appel récursif peut consommer de la stack pour :
- l'adresse de retour ;
- les variables locales ;
- les registres sauvegardés ;
- le shadow space ;
- les paramètres supplémentaires.
Code: Select all
stack overflow
- `call` sauvegarde l'adresse de retour et entre dans une fonction ;
- `ret` récupère cette adresse et revient à l'appelant ;
- `RSP` est le sommet courant de la stack ;
- `RBP` peut être un repère stable de stack frame ;
- `push` et `pop` modifient RSP ;
- Windows x64 utilise RCX, RDX, R8, R9 pour les quatre premiers paramètres entiers/pointeurs ;
- les paramètres supplémentaires sont sur la stack ;
- RAX contient généralement le retour entier/pointeur ;
- XMM0 est important pour les flottants ;
- Windows x64 réserve 32 octets de shadow space ;
- l'alignement de la stack est important ;
- `lea` charge une adresse ;
- une fonction simple peut ne pas avoir de stack frame ;
- les conventions Windows et System V ne passent pas les paramètres dans les mêmes registres ;
- chaque appel récursif ajoute un nouveau contexte sur la stack.
Exercice 1
Si :
Code: Select all
RSP = 1000h
Code: Select all
push rax
Exercice 2
Que fait `call test` avant de transférer l'exécution vers `test` ?
Exercice 3
Sous Windows x64, retrouve les registres de :
Code: Select all
foo(a, b, c, d)
Pour :
Code: Select all
foo(a, b, c, d, e)
Exercice 5
Réécris en pseudo-C :
Code: Select all
mov eax, ecx
add eax, edx
ret
Explique la différence entre :
Code: Select all
lea rcx, [message]
mov rcx, [message]
Que représente généralement une réservation de 32 octets avant un appel sous Windows x64 ?
Exercice 8
Analyse :
Code: Select all
push rbp
mov rbp, rsp
sub rsp, 20h
...
leave
ret
Exercice 9
Dans x64dbg, place un breakpoint juste avant :
Code: Select all
call VirtualAlloc
Code: Select all
RCX
RDX
R8
R9
Exercice 10
Observe RSP avant et après un `call` et retrouve l'adresse de retour au sommet de la stack.
Exercice 11
Explique pourquoi une fonction récursive consomme progressivement de la stack.
Exercice 12
Compare la convention d'appel Windows x64 et System V x64 et explique pourquoi cette différence est importante en reverse.
