Les registres sont de très petites zones de stockage directement accessibles par le processeur. Ils sont essentiels en assembleur : presque toutes les opérations importantes passent par eux.
En reverse engineering, comprendre les registres permet de suivre les paramètres d'une fonction, les valeurs de retour, les pointeurs, les calculs et l'évolution du programme instruction par instruction.
1. Pourquoi utiliser des registres ?
La RAM permet de stocker énormément de données, mais son accès est beaucoup plus lent qu'un accès aux registres du CPU.
On peut retenir cette hiérarchie simplifiée :
Code: Select all
Registres CPU
|
v
Caches CPU
|
v
RAM
Le processeur les utilise donc en permanence pour conserver temporairement les valeurs sur lesquelles il travaille.
Par exemple :
Code: Select all
mov rax, 10
add rax, 5
Après l'addition :
Code: Select all
RAX = 15
L'architecture x86-64 possède notamment 16 registres généraux 64 bits :
Code: Select all
RAX
RBX
RCX
RDX
RSI
RDI
RBP
RSP
R8
R9
R10
R11
R12
R13
R14
R15
Code: Select all
AX -> accumulateur
BX -> registre de base
CX -> compteur
DX -> registre de données
SI -> source index
DI -> destination index
BP -> base pointer
SP -> stack pointer
3. Les sous-registres
Un même registre peut être accédé avec différentes tailles.
Prenons `RAX` :
Code: Select all
RAX = 64 bits
EAX = 32 bits
AX = 16 bits
AL = 8 bits faibles
AH = 8 bits suivants
Code: Select all
RAX
+---------------------------------------------------------------+
| 64 bits |
+--------------------------------+------------------------------+
| EAX |
+---------------+---------------+
| AX |
+-------+-------+
| AH | AL |
+-------+-------+
Code: Select all
RBX -> EBX -> BX -> BL/BH
RCX -> ECX -> CX -> CL/CH
RDX -> EDX -> DX -> DL/DH
Code: Select all
RSI -> ESI -> SI -> SIL
RDI -> EDI -> DI -> DIL
RBP -> EBP -> BP -> BPL
RSP -> ESP -> SP -> SPL
R8 -> R8D -> R8W -> R8B
R9 -> R9D -> R9W -> R9B
...
R15 -> R15D -> R15W -> R15B
Une particularité extrêmement importante en x86-64 est qu'une écriture dans un registre 32 bits met automatiquement à zéro les 32 bits supérieurs du registre 64 bits correspondant.
Exemple :
Code: Select all
RAX = 0xFFFFFFFFFFFFFFFF
mov eax, 1
Code: Select all
RAX = 0x0000000000000001
En revanche, écrire uniquement dans `AX` ou `AL` ne remet pas automatiquement tout le reste de `RAX` à zéro.
5. RIP — Instruction Pointer
RIP est le registre contenant l'adresse de l'instruction en cours d'exécution / de la prochaine instruction selon le contexte d'observation.
Dans un debugger, RIP permet donc de savoir où se trouve actuellement l'exécution.
Exemple conceptuel :
Code: Select all
RIP -> 00007FF6...1000
00007FF6...1000 mov eax, 5
00007FF6...1005 add eax, 2
00007FF6...1008 ret
6. RSP et RBP
RSP est le stack pointer.
Il pointe vers le sommet courant de la pile.
Il est particulièrement important pour :
- les appels de fonctions ;
- les adresses de retour ;
- les sauvegardes de registres ;
- les variables locales placées sur la stack ;
- certains paramètres.
Code: Select all
sub rsp, 20h
Puis :
Code: Select all
add rsp, 20h
RBP peut servir de base stable pour une stack frame.
Exemple classique :
Code: Select all
push rbp
mov rbp, rsp
7. RFLAGS
Le registre RFLAGS contient plusieurs indicateurs décrivant notamment le résultat de certaines opérations.
Parmi les flags importants :
Code: Select all
ZF = Zero Flag
CF = Carry Flag
SF = Sign Flag
OF = Overflow Flag
Code: Select all
cmp eax, 10
je equal
Si :
Code: Select all
EAX == 10
C'est fondamental en reverse engineering, car beaucoup de conditions apparaissent sous la forme :
Code: Select all
cmp ...
je ...
jne ...
jg ...
jl ...
Un debugger affiche généralement une fenêtre contenant l'état des registres.
On peut y retrouver :
Code: Select all
RAX
RBX
RCX
RDX
RSI
RDI
RSP
RBP
R8-R15
RIP
RFLAGS
Par exemple :
Code: Select all
mov rax, 100
add rax, 20
Code: Select all
RAX = valeur quelconque
Code: Select all
RAX = 100
Code: Select all
RAX = 120
Code: Select all
instruction
registres
stack
mémoire
`mov` est l'une des instructions les plus fréquentes en assembleur.
Son principe général est :
Code: Select all
mov destination, source
Exemple :
Code: Select all
mov rax, 100
Code: Select all
RAX <- 100
10. Constante vers registre
Une valeur directement présente dans l'instruction est appelée une valeur immédiate.
Exemple :
Code: Select all
mov rax, 100
Autre exemple :
Code: Select all
mov eax, 10
Code: Select all
EAX = 10
Code: Select all
RAX = 000000000000000Ah
Une opération extrêmement importante consiste à charger une valeur depuis la mémoire.
Exemple :
Code: Select all
mov rax, [a]
C'est une notion essentielle.
Compare :
Code: Select all
adresse de a
Code: Select all
[a]
Le second désigne la valeur stockée à cet emplacement.
En C/C++, on peut rapprocher cela de :
Code: Select all
int* p = &a;
adresse : p
valeur : *p
Exemple :
Code: Select all
a dq 175
mov rax, [a]
Code: Select all
RAX = 175
L'opération inverse consiste à prendre une valeur contenue dans un registre et à l'écrire en mémoire.
Exemple :
Code: Select all
mov [a], rax
Code: Select all
RAX = 175
Conceptuellement :
Code: Select all
RAX
|
| valeur
v
mémoire [a]
13. Registre vers registre
On peut également copier une valeur d'un registre vers un autre.
Exemple :
Code: Select all
mov rax, rbx
Code: Select all
RBX = 42
Code: Select all
RAX = 42
RBX = 42
C'est une copie, pas un déplacement destructif malgré le nom `mov`.
14. Suivre une suite de MOV
Prenons :
Code: Select all
mov rax, 100
mov rbx, rax
mov [value], rbx
mov rcx, [value]
Code: Select all
1. RAX = 100
2. RBX = RAX
RBX = 100
3. [value] = RBX
mémoire[value] = 100
4. RCX = [value]
RCX = 100
15. Attention à la taille des données
Le processeur doit savoir quelle quantité de mémoire est manipulée.
Exemples :
Code: Select all
mov al, [value] ; 8 bits
mov ax, [value] ; 16 bits
mov eax, [value] ; 32 bits
mov rax, [value] ; 64 bits
Code: Select all
byte
word
dword
qword
Code: Select all
byte = 8 bits
word = 16 bits
dword = 32 bits
qword = 64 bits
16. Adresse ou contenu ?
C'est probablement l'une des distinctions les plus importantes à maîtriser.
Supposons :
Code: Select all
RAX = 000001F000001000h
Mais cette valeur peut également être interprétée comme une adresse.
Si le code fait :
Code: Select all
mov rbx, [rax]
On peut retenir :
Code: Select all
RAX -> valeur contenue dans RAX
[RAX] -> mémoire pointée par RAX
17. Registres et fonctions Windows x64
En reverse Windows x64, certains registres apparaissent constamment à cause de la convention d'appel Microsoft x64.
Pour les arguments entiers ou pointeurs, les quatre premiers sont généralement passés dans :
Code: Select all
RCX = premier argument
RDX = deuxième argument
R8 = troisième argument
R9 = quatrième argument
La valeur de retour entière ou pointeur est généralement placée dans :
Code: Select all
RAX
Code: Select all
VirtualAlloc(
lpAddress,
dwSize,
flAllocationType,
flProtect
)
Code: Select all
RCX -> lpAddress
RDX -> dwSize
R8 -> flAllocationType
R9 -> flProtect
Code: Select all
RAX -> adresse retournée
18. Exemple de reverse
Supposons que le debugger affiche :
Code: Select all
mov edx, 1000h
mov r8d, 3000h
mov r9d, 4
call VirtualAlloc
Code: Select all
RDX = 0x1000
R8 = 0x3000
R9 = 0x4
Par exemple, selon le contexte :
Code: Select all
0x3000 = MEM_COMMIT | MEM_RESERVE
0x4 = PAGE_READWRITE
Code: Select all
RAX
C'est précisément pour ce genre d'analyse que la maîtrise des registres est beaucoup plus importante en reverse user mode que de connaître par cœur tous les détails des tables de pages.
19. Points essentiels à retenir
Pour être à l'aise en reverse x86-64, retiens surtout :
- les registres sont des zones de stockage extrêmement rapides du CPU ;
- RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP et R8-R15 sont les principaux registres généraux ;
- RAX est 64 bits, EAX 32 bits, AX 16 bits et AL 8 bits ;
- écrire dans EAX remet à zéro les 32 bits supérieurs de RAX ;
- RIP permet de suivre l'exécution ;
- RSP pointe vers le sommet de la stack ;
- RFLAGS contient notamment ZF, CF, SF et OF ;
- `mov destination, source` copie une valeur ;
- les crochets indiquent généralement un accès au contenu mémoire ;
- `mov rax, [adresse]` lit la mémoire ;
- `mov [adresse], rax` écrit en mémoire ;
- `mov rax, rbx` copie RBX dans RAX sans modifier RBX ;
- la taille 8/16/32/64 bits de l'opération est importante ;
- sous Windows x64, RCX, RDX, R8 et R9 sont essentiels pour suivre les quatre premiers arguments entiers/pointeurs ;
- RAX contient généralement la valeur de retour entière ou pointeur.
Exercice 1
Après :
Code: Select all
mov rax, 10
mov rbx, rax
mov rcx, 20
Code: Select all
RAX
RBX
RCX
On a :
Code: Select all
RAX = FFFFFFFFFFFFFFFFh
Code: Select all
mov eax, 5
Exercice 3
Explique la différence entre :
Code: Select all
mov rax, rbx
mov rax, [rbx]
On a :
Code: Select all
value dq 50
mov rax, [value]
mov rbx, rax
add rbx, 10
mov [value], rbx
Code: Select all
RAX = ?
RBX = ?
[value] = ?
Tu observes avant un appel Windows :
Code: Select all
RCX = 0
RDX = 1000h
R8 = 3000h
R9 = 4
call VirtualAlloc
Exercice 6
Dans x64dbg, place un breakpoint sur une fonction simple puis avance instruction par instruction.
À chaque instruction, note :
Code: Select all
RIP
RAX
RCX
RDX
RSP
RFLAGS
Exercice 7
Explique avec tes propres mots la différence entre :
Code: Select all
une valeur
une adresse
le contenu d'une adresse
