Dans ce chapitre, nous allons voir comment la mémoire d’un programme est organisée et comment le processeur accède aux données. L’objectif est surtout de comprendre ce que l’on observe lorsqu’on analyse un programme en assembleur ou dans un débogueur.
1. Introduction à la mémoire
La mémoire peut être vue comme une immense suite d’octets. Chaque octet possède une adresse permettant au processeur de le retrouver.
Cependant, un programme moderne ne travaille généralement pas directement avec les adresses physiques de la RAM. Il utilise des adresses virtuelles.
Chaque processus possède ainsi son propre espace d’adressage virtuel.
Cela permet notamment :
- d’isoler les processus ;
- d’empêcher normalement un programme d’accéder directement à la mémoire d’un autre ;
- de simplifier la gestion de la mémoire ;
- de donner à chaque processus sa propre vision de la mémoire.
2. Memory Mapping
Le processeur doit traduire les adresses virtuelles utilisées par un programme vers des emplacements de mémoire physique.
La mémoire virtuelle est découpée en pages. Sous Windows x86/x64, une taille de page très courante est :
Code: Select all
4096 octets = 4 Ko
Schématiquement :
Code: Select all
Adresse virtuelle
|
v
Tables de pages
|
v
Adresse physique
|
v
RAM
Par exemple, avec des pages de 4096 octets :
Code: Select all
4096 = 2^12
Le système peut également définir des protections différentes pour les pages : lecture, écriture, exécution, etc.
Lorsqu’un programme tente d’accéder à une page qui n’est pas actuellement accessible de la manière demandée, le processeur déclenche une faute de page. Le système d’exploitation doit alors déterminer la raison de cette faute et la traiter si cela est possible.
Une faute de page n’implique donc pas automatiquement un crash. Certaines font partie du fonctionnement normal de la mémoire virtuelle. En revanche, un accès réellement invalide peut finir par provoquer une exception telle qu’une violation d’accès.
3. Modèle mémoire d’un processus Windows
L’espace mémoire d’un processus contient plusieurs régions ayant des rôles différents.
Une représentation simplifiée est :
Code: Select all
Adresses hautes
+--------------------+
| Heap |
+--------------------+
| DLLs |
+--------------------+
| Stack |
+--------------------+
| .data / .bss |
+--------------------+
| .text |
+--------------------+
Adresses basses
Section .text
La section `.text` contient principalement le code machine du programme.
Exemple :
Code: Select all
section .text
global main
main:
xor eax, eax
ret
Section .data
La section `.data` contient les données initialisées.
Exemple :
Code: Select all
section .data
a dd 4
b dd 4.4
c times 10 dd 0
d dw 1, 2
e db 0xFF
f db "hello world", 0
Section .bss
La section `.bss` est utilisée pour réserver de l’espace pour des données non initialisées.
Exemple :
Code: Select all
section .bss
g resd 1
h resd 10
i resb 100
4. Stack et Heap
La stack
La stack, ou pile, est notamment utilisée lors des appels de fonctions.
Elle peut contenir :
- des variables locales ;
- des adresses de retour ;
- des registres sauvegardés ;
- des données temporaires ;
- certains paramètres selon l’architecture et la convention d’appel.
Code: Select all
push rbp
mov rbp, rsp
sub rsp, 16
xor eax, eax
leave
ret
Code: Select all
sub rsp, 16
Le heap
Le heap sert principalement aux allocations dynamiques.
En C/C++ :
Code: Select all
int* buffer = new int[100];
delete[] buffer;
Code: Select all
HeapAlloc
HeapFree
VirtualAlloc
VirtualFree
Différence générale
Code: Select all
STACK
- fortement liée aux appels de fonctions
- variables locales
- gestion principalement automatique
- durée de vie souvent liée à la portée/appel
HEAP
- allocation dynamique
- taille choisie pendant l'exécution
- durée de vie contrôlée par le programme
- nécessite une gestion correcte des allocations
En assembleur, plusieurs directives permettent de définir la taille des données.
On rencontre notamment :
Code: Select all
db ; byte
dw ; word
dd ; double word
dq ; quad word
Code: Select all
db = 1 octet
dw = 2 octets
dd = 4 octets
dq = 8 octets
Code: Select all
value8 db 10
value16 dw 1000
value32 dd 123456
value64 dq 123456789
Code: Select all
resb
resw
resd
resq
Code: Select all
buffer resb 100
array resd 10
La directive `times` permet de répéter une définition :
Code: Select all
values times 10 dd 0
6. Exemple pratique en assembleur
Prenons un programme contenant plusieurs variables :
Code: Select all
segment .data
a dd 4
b dd 4.4
c times 10 dd 0
d dw 1, 2
e db 0xFF
f db "hello world", 0
segment .bss
g resd 1
h resd 10
i resb 100
segment .text
global main
main:
push rbp
mov rbp, rsp
sub rsp, 16
xor eax, eax
leave
ret
Un listing d’assembleur permet de faire le lien entre :
- l’adresse ou l’offset ;
- les octets générés ;
- l’instruction ou la donnée correspondante.
Code: Select all
push rbp
Code: Select all
55
7. Représentation des données en mémoire
Lorsqu’on observe la mémoire dans un débogueur, on ne voit pas directement :
Code: Select all
"variable = 4"
Par exemple, une valeur 32 bits peut apparaître sous la forme :
Code: Select all
04 00 00 00
Par exemple :
Code: Select all
Valeur : 0x12345678
Mémoire :
78 56 34 12
Code: Select all
"hello"
Code: Select all
68 65 6C 6C 6F 00
8. Observer la mémoire avec un débogueur
Le chapitre utilise ebe pour examiner le programme.
Le principe est identique avec d’autres débogueurs modernes comme x64dbg, WinDbg ou GDB.
Une méthode classique consiste à :
- charger le programme ;
- placer un breakpoint ;
- lancer l’exécution ;
- attendre que le breakpoint soit atteint ;
- ouvrir une vue mémoire ;
- examiner l’adresse d’une variable.
On peut alors observer :
- les registres ;
- la stack ;
- le code désassemblé ;
- les adresses ;
- les données en mémoire.
Supposons que le programme possède :
Code: Select all
value dd 4
À cette adresse, on peut retrouver les quatre octets correspondant à la variable.
Pour la valeur 4 :
Code: Select all
04 00 00 00
Code: Select all
0A 00 00 00
Code: Select all
10
Cette technique permet de comprendre une idée essentielle du reverse engineering :
les variables, structures, chaînes et instructions que l’on manipule dans le code finissent toutes par avoir une représentation concrète en mémoire.
10. Breakpoints et analyse dynamique
Un breakpoint permet d’arrêter le programme à un endroit précis.
Par exemple :
Code: Select all
mov eax, [variable]
Après l’exécution de l’instruction, on peut vérifier le contenu de `eax`.
Cette approche permet de suivre le trajet d’une donnée :
Code: Select all
Mémoire
|
v
Instruction
|
v
Registre
|
v
Calcul
|
v
Nouvelle valeur
11. Ce qu’il faut retenir
Les points essentiels de ce chapitre sont :
- un processus utilise principalement des adresses virtuelles ;
- le système et le matériel traduisent ces adresses vers la mémoire physique ;
- la mémoire virtuelle est organisée en pages ;
- un processus possède plusieurs régions mémoire ayant des rôles différents ;
- `.text` contient principalement le code ;
- `.data` contient des données initialisées ;
- `.bss` réserve des données non initialisées ;
- la stack est fortement liée aux appels de fonctions et aux variables locales ;
- le heap permet les allocations dynamiques ;
- les données sont finalement représentées par des octets ;
- x86/x64 utilise le little endian pour les valeurs multi-octets ;
- un débogueur permet d’observer les registres et la mémoire pendant l’exécution ;
- les breakpoints permettent d’arrêter le programme pour suivre précisément les données.
12. Exercices
Exercice 1
Quelle quantité de mémoire est réservée ici ?
Code: Select all
buffer resd 20
Quelle valeur représente cette suite d’octets en little endian ?
Code: Select all
78 56 34 12
Dans quelle section placerais-tu chacune des données suivantes ?
Code: Select all
message db "Hello", 0
counter dd 10
buffer resb 256
Place un breakpoint avant une instruction qui charge une variable en registre. Observe la variable en mémoire, exécute une instruction puis vérifie le registre correspondant.
Exercice 5
Crée plusieurs variables avec :
Code: Select all
db
dw
dd
dq
Exercice 6
Définis une valeur :
Code: Select all
number dd 0x12345678
