Objectif du cours
Ce cours a un objectif très concret :
Code: Select all
prendre du code C/C++
↓
voir comment il apparaît en assembleur
↓
apprendre à le reconnaître dans x64dbg
↓
reconstruire la logique du programme
Le but est d’apprendre à reconnaître des motifs.
Quand tu vois :
Code: Select all
mov eax, [rcx+8]
Code: Select all
"on lit probablement un champ dans une structure ou un objet"
Code: Select all
cmp eax, 10
jge ...
Code: Select all
"il y a probablement une condition ou une limite de boucle"
Code: Select all
sub rsp, 40h
Code: Select all
"la fonction réserve de la place sur la stack"
1. La vision haut niveau et la vision bas niveau
Un développeur écrit :
Code: Select all
int x = 10;
int y = 20;
int z = x + y;
Il manipule seulement :
- des registres ;
- des adresses mémoire ;
- des valeurs ;
- des instructions.
Code: Select all
mov dword ptr [rsp+20h], 10
mov dword ptr [rsp+24h], 20
mov eax, dword ptr [rsp+20h]
add eax, dword ptr [rsp+24h]
mov dword ptr [rsp+28h], eax
Code: Select all
[rsp+20h] = x
[rsp+24h] = y
[rsp+28h] = z
Si tu vois une instruction comme :
Code: Select all
mov eax, dword ptr [rsp+20h]
- regarder la valeur de RSP ;
- calculer mentalement RSP + 0x20 ;
- suivre cette adresse dans la vue dump ;
- observer la valeur ;
- voir quelles autres instructions utilisent la même adresse.
Que représente probablement ceci ?
Code: Select all
mov eax, [rsp+20h]
add eax, 5
mov [rsp+20h], eax
Code: Select all
une variable locale est lue,
augmentée de 5,
puis réécrite
Code: Select all
x += 5;
Une variable C++ peut être stockée à différents endroits.
Cas 1 : variable dans un registre
Code :
Code: Select all
int x = 10;
return x;
Code: Select all
mov eax, 10
ret
La valeur reste directement dans `EAX`.
Cas 2 : variable sur la stack
Code :
Code: Select all
int x = 10;
int y = x + 2;
Code: Select all
sub rsp, 30h
mov dword ptr [rsp+20h], 10
mov eax, dword ptr [rsp+20h]
add eax, 2
mov dword ptr [rsp+24h], eax
add rsp, 30h
ret
Code: Select all
RSP
│
├── +00h espace réservé
├── +08h
├── +10h
├── +18h
├── +20h x = 10
├── +24h y = 12
│
Code :
Code: Select all
int g_Count = 5;
Code: Select all
mov eax, dword ptr [g_Count]
Code: Select all
mov eax, dword ptr [rip+1234h]
À reconnaître en reverse
- `[rsp+offset]` → souvent variable locale ;
- `[rbp-offset]` → souvent variable locale dans certaines fonctions ;
- `[rip+offset]` → souvent global, constante ou donnée de section ;
- registre seul → valeur temporaire ou variable optimisée.
Prenons :
Code: Select all
struct Player
{
int hp;
int mana;
};
Code: Select all
Player
+0x00 → hp
+0x04 → mana
Code: Select all
Player p;
p.hp = 100;
p.mana = 50;
Code: Select all
mov dword ptr [rcx], 100
mov dword ptr [rcx+4], 50
Tu peux lire :
Code: Select all
[rcx] → champ offset 0
[rcx+4] → champ offset 4
Code: Select all
struct Session
{
int id;
int state;
void* context;
};
Code: Select all
+0x00 → id
+0x04 → state
+0x08 → context
Code: Select all
mov eax, [rcx]
cmp dword ptr [rcx+4], 1
jne inactive
mov rax, [rcx+8]
Code: Select all
session->id
session->state
session->context
Si plusieurs instructions utilisent :
Code: Select all
[rcx]
[rcx+4]
[rcx+8]
[rcx+10h]
Code: Select all
structure
objet
tableau de champs
contexte de fonction
Code: Select all
mov eax, [rdx+8]
add eax, 10
mov [rdx+8], eax
Code: Select all
un champ situé à l’offset +8
est lu,
augmenté de 10,
puis réécrit
Code: Select all
obj->field += 10;
En reverse, beaucoup d’erreurs viennent de la confusion entre :
Code: Select all
adresse
et
valeur contenue à cette adresse
Code: Select all
int x = 10;
int* p = &x;
Code: Select all
x est à l’adresse 0000000000100000
Code: Select all
p = 0000000000100000
*p = 10
Code: Select all
mov rax, p
mov ecx, [rax]
Code: Select all
rax = adresse
[rax] = contenu de l’adresse
Si `RAX` vaut :
Code: Select all
000001F4A1200000
Cela peut être une adresse.
Tu dois vérifier :
- si cette adresse appartient à une zone mémoire valide ;
- ce qu’elle contient ;
- si elle pointe vers une chaîne ;
- si elle pointe vers une structure ;
- si d’autres instructions utilisent `[rax+offset]`.
Code C++ :
Code: Select all
int values[4] = {10, 20, 30, 40};
En mémoire :
Code: Select all
base+0x00 → 10
base+0x04 → 20
base+0x08 → 30
base+0x0C → 40
Code: Select all
values[i]
Code: Select all
mov eax, [rcx+rdx*4]
Code: Select all
RCX = adresse du tableau
RDX = index
*4 = taille d’un int
Reconnaître la taille des éléments
Code: Select all
[base + index*1] → éléments de 1 octet
[base + index*2] → éléments de 2 octets
[base + index*4] → éléments de 4 octets
[base + index*8] → éléments de 8 octets
Code: Select all
mov eax, [rcx+r8*4]
Code: Select all
lecture d’un tableau d’éléments de 4 octets
à l’index R8
Prenons :
Code: Select all
int Add(int a, int b)
{
return a + b;
}
Code: Select all
RCX
RDX
R8
R9
Code: Select all
int Add(int a, int b)
Code: Select all
RCX = a
RDX = b
Code: Select all
mov eax, ecx
add eax, edx
ret
Code: Select all
EAX / RAX
C++ :
Code: Select all
int result = Add(10, 20);
Code: Select all
mov ecx, 10
mov edx, 20
call Add
mov [rsp+20h], eax
Code: Select all
argument 1 = 10
argument 2 = 20
appel
résultat dans eax
stockage dans variable locale
Code: Select all
mov ecx, 100
mov edx, 5
call sub_140001000
Code: Select all
elle reçoit probablement au moins 2 arguments
arg1 = 100
arg2 = 5
Une fonction est généralement appelée avec :
Code: Select all
call adresse
Puis le code de la fonction s’exécute.
À la fin :
Code: Select all
ret
Schéma :
Code: Select all
main
|
| call Function
v
Function
|
| ret
v
main continue ici
Deux commandes sont importantes :
- Step Into : entrer dans la fonction appelée ;
- Step Over : exécuter l’appel sans entrer dedans.
Code: Select all
Step Into
Code: Select all
Step Over
Code :
Code: Select all
if (x == 10)
{
success();
}
Code: Select all
cmp eax, 10
jne skip
call success
skip:
Code: Select all
cmp eax, 10
Puis :
Code: Select all
jne skip
Donc l’appel n’est exécuté que si :
Code: Select all
eax == 10
C++ :
Code: Select all
if (x > 100)
{
High();
}
else
{
Low();
}
Code: Select all
cmp eax, 100
jle low
call High
jmp end_if
low:
call Low
end_if:
9. `test` : très fréquent en reverse
`test` réalise un ET logique uniquement pour modifier les flags.
Très souvent :
Code: Select all
test rax, rax
je failed
Code: Select all
if (rax == 0)
goto failed;
- un pointeur nul ;
- un handle nul ;
- un résultat égal à zéro ;
- un booléen.
Code: Select all
void* p = malloc(100);
if (!p)
return;
Code: Select all
call malloc
test rax, rax
je allocation_failed
Quand tu vois :
Code: Select all
test reg, reg
Code: Select all
"on teste probablement si cette valeur vaut zéro"
Code :
Code: Select all
for (int i = 0; i < 5; i++)
{
DoWork();
}
Code: Select all
xor ebx, ebx
loop_start:
cmp ebx, 5
jge loop_end
call DoWork
inc ebx
jmp loop_start
loop_end:
Code: Select all
un saut vers une adresse située plus haut
Code: Select all
0000000140001020 cmp eax, 5
0000000140001023 jge 140001035
...
0000000140001032 jmp 140001020
Tu peux donc soupçonner une boucle.
Boucle sur une chaîne
C :
Code: Select all
while (*p != '\0')
{
p++;
}
Code: Select all
loop_start:
mov al, [rcx]
test al, al
je done
inc rcx
jmp loop_start
done:
Code: Select all
test al, al
Le zéro final correspond au terminateur :
Code: Select all
'\0'
11. La stack
La stack est essentielle en reverse.
Elle sert notamment à stocker :
- variables locales ;
- adresses de retour ;
- registres sauvegardés ;
- arguments supplémentaires ;
- données temporaires.
Code: Select all
int Function()
{
int a = 10;
int b = 20;
return a + b;
}
Code: Select all
sub rsp, 30h
mov dword ptr [rsp+20h], 10
mov dword ptr [rsp+24h], 20
mov eax, [rsp+20h]
add eax, [rsp+24h]
add rsp, 30h
ret
Code: Select all
sub rsp, 30h
Le :
Code: Select all
add rsp, 30h
Attention
`sub rsp` ne fait pas un `VirtualAlloc`.
La stack existe déjà.
On déplace simplement `RSP` afin de réserver une partie de cette stack pour la fonction.
Schéma
Code: Select all
adresse haute
|
| anciennes données
|
| adresse de retour
|
| variable b
| variable a
RSP ->| zone actuelle de la fonction
|
adresse basse
12. `push` et `pop`
`push` place une valeur sur la pile.
Exemple :
Code: Select all
push rbx
Code: Select all
rsp = rsp - 8
[rsp] = rbx
Code: Select all
pop rbx
Code: Select all
rbx = [rsp]
rsp = rsp + 8
Code: Select all
push rbx
...
pop rbx
ret
Réflexe
Ne pense pas :
Code: Select all
push = allocation mémoire générale
Code: Select all
push = stocker temporairement une valeur sur la stack
Le heap sert aux allocations dynamiques.
Exemple :
Code: Select all
int* p = new int(100);
Code: Select all
int* p = (int*)malloc(sizeof(int));
*p = 100;
Exemple conceptuel :
Code: Select all
call malloc
test rax, rax
je failed
mov dword ptr [rax], 100
Code: Select all
RAX = pointeur vers la nouvelle allocation
[RAX] = contenu
Code: Select all
HeapAlloc
VirtualAlloc
LocalAlloc
CoTaskMemAlloc
operator new
C++ :
Code: Select all
void* p = VirtualAlloc(
nullptr,
0x1000,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
Code: Select all
RCX = nullptr
RDX = 0x1000
R8 = 0x3000
R9 = 0x04
Code: Select all
call VirtualAlloc
Code: Select all
RAX = adresse retournée
14. Exemple pratique : reconnaître une allocation
Tu vois dans x64dbg :
Code: Select all
xor ecx, ecx
mov edx, 1000h
mov r8d, 3000h
mov r9d, 4
call qword ptr [VirtualAlloc]
Code: Select all
VirtualAlloc(
NULL,
0x1000,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
)
Code: Select all
0x1000 = taille
0x3000 = MEM_COMMIT | MEM_RESERVE
0x4 = PAGE_READWRITE
Code: Select all
mov rbx, rax
Code: Select all
RBX garde probablement le pointeur retourné
Prenons :
Code: Select all
class Player
{
public:
int hp;
void Damage(int amount)
{
hp -= amount;
}
};
Code: Select all
player.Damage(20);
Code: Select all
RCX = this
Code: Select all
RDX = 20
Code: Select all
mov eax, [rcx]
sub eax, edx
mov [rcx], eax
ret
Code: Select all
this->hp -= amount;
Si une fonction reçoit un pointeur dans `RCX` et fait énormément de :
Code: Select all
[rcx+offset]
Code: Select all
this
En C++, le polymorphisme peut faire apparaître des appels indirects.
Exemple :
Code: Select all
obj->Run();
Code: Select all
mov rax, [rcx]
call qword ptr [rax+10h]
Code: Select all
[rcx] = pointeur vers la vtable
[rax+10h] = entrée de méthode virtuelle
call [...] = appel indirect
17. Listes chaînées
Code :
Code: Select all
struct Node
{
int value;
Node* next;
};
Code: Select all
while (node)
{
std::cout << node->value;
node = node->next;
}
Code: Select all
loop:
test rcx, rcx
je done
mov eax, [rcx]
; utilisation de value
mov rcx, [rcx+8]
jmp loop
done:
Code: Select all
[rcx] = value
[rcx+8] = next
C’est typiquement une structure chaînée.
18. Sections mémoire d’un programme
Un exécutable contient plusieurs catégories de données.
En simplifiant :
Code: Select all
.text → code exécutable
.rdata → données en lecture seule
.data → données globales initialisées
.bss → données globales non initialisées
Pour toi, au début, retiens surtout :
Code: Select all
.text = code
.rdata = chaînes / constantes
.data = globals modifiables
Si tu cherches :
Code: Select all
"Access denied"
Une référence peut t’amener à :
Code: Select all
cmp eax, 1
je success
lea rcx, [rip+AccessDenied]
call Print
19. Imports : excellent point d’entrée
Les imports donnent beaucoup d’informations.
Exemples :
Code: Select all
CreateFileW
ReadFile
WriteFile
VirtualAlloc
HeapAlloc
RegOpenKeyExW
socket
connect
CreateProcessW
OpenProcessToken
Exemple :
Code: Select all
CreateFileW
ReadFile
CryptDecrypt
WriteFile
Code: Select all
ouvrir un fichier
↓
lire des données
↓
traiter/déchiffrer
↓
écrire un résultat
Dans x64dbg :
- ouvre les imports ;
- trouve une API intéressante ;
- place un breakpoint dessus ;
- lance le programme ;
- quand le breakpoint se déclenche, regarde les registres arguments ;
- remonte dans l’appelant.
20. Exemple pratique complet : structure + fonction + condition
Code source :
Code: Select all
struct Session
{
int id;
bool active;
};
int CheckSession(Session* s)
{
if (!s->active)
return 0;
return s->id + 5;
}
Code: Select all
CheckSession:
cmp byte ptr [rcx+4], 0
je inactive
mov eax, dword ptr [rcx]
add eax, 5
ret
inactive:
xor eax, eax
ret
Étape 1
Code: Select all
cmp byte ptr [rcx+4], 0
Cela ressemble à :
Code: Select all
bool
char
petit champ de structure
Code: Select all
je inactive
On peut suspecter :
Code: Select all
if (!something)
Code: Select all
mov eax, dword ptr [rcx]
Probablement un autre champ.
Étape 3
Code: Select all
add eax, 5
Étape 4
Code: Select all
ret
Étape 5
Branche inactive :
Code: Select all
xor eax, eax
ret
Donc :
Code: Select all
return 0;
Code: Select all
if (*(byte*)(rcx + 4) == 0)
return 0;
return *(int*)(rcx + 0) + 5;
Code: Select all
if (!s->active)
return 0;
return s->id + 5;
21. Exemple pratique complet : boucle sur tableau
Code :
Code: Select all
int Sum(int* values, int count)
{
int total = 0;
for (int i = 0; i < count; i++)
total += values[i];
return total;
}
Code: Select all
xor eax, eax
xor r8d, r8d
loop_start:
cmp r8d, edx
jge done
add eax, dword ptr [rcx+r8*4]
inc r8d
jmp loop_start
done:
ret
Code: Select all
RCX = values
EDX = count
R8D = i
EAX = total
Code: Select all
[rcx+r8*4]
Code: Select all
values[i]
- un tableau ;
- un index ;
- une taille de 4 octets ;
- une boucle ;
- un accumulateur.
Code :
Code: Select all
int Length(const char* s)
{
int len = 0;
while (*s)
{
len++;
s++;
}
return len;
}
Code: Select all
xor eax, eax
loop_start:
cmp byte ptr [rcx], 0
je done
inc eax
inc rcx
jmp loop_start
done:
ret
Code: Select all
RCX = pointeur sur la chaîne
EAX = longueur
Code: Select all
cmp byte ptr [rcx], 0
Si zéro :
Code: Select all
fin de chaîne
Code: Select all
inc rcx
Donc tu reconnais une boucle sur une chaîne C.
23. Exemple pratique complet : appel d’API Windows
Code :
Code: Select all
HANDLE h = CreateFileW(
path,
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
0,
nullptr
);
Code: Select all
CreateFileW
Code: Select all
RCX = lpFileName
RDX = dwDesiredAccess
R8 = dwShareMode
R9 = lpSecurityAttributes
Tu peux suivre `RCX` dans le dump.
Si tu vois :
Code: Select all
C:\test\data.bin
Cette technique est souvent bien plus rapide que de lire tout le programme depuis le début.
24. Comment travailler dans x64dbg
Pour chaque morceau intéressant :
1. Repère l’entrée
Utilise :
- imports ;
- chaînes ;
- API ;
- breakpoints ;
- références.
Regarde :
Code: Select all
RAX
RBX
RCX
RDX
R8
R9
RSP
RBP
RIP
Si un registre ressemble à une adresse valide :
Code: Select all
clic droit
→ suivre dans le dump
Si tu vois :
Code: Select all
[rcx+8]
[rcx+10h]
[rcx+18h]
Code: Select all
structure probable
Cherche :
Code: Select all
cmp
test
je
jne
jg
jl
jge
jle
Cherche les sauts vers une adresse antérieure.
7. Repère les appels
Regarde les arguments juste avant :
Code: Select all
call
Même très approximatif.
Exemple :
Code: Select all
if (field == 0)
return 0;
return other_field + 5;
25. Erreurs classiques du débutant
Erreur 1 : vouloir comprendre chaque instruction
Inutile.
Cherche d’abord les motifs importants.
Erreur 2 : confondre adresse et valeur
Toujours vérifier si un registre pointe vers une zone mémoire.
Erreur 3 : croire que le pseudo-code est exact
Un décompilateur reconstruit une approximation.
Toujours vérifier l’assembleur quand quelque chose paraît étrange.
Erreur 4 : ignorer les appels d’API
Les API sont souvent les meilleurs points de repère.
Erreur 5 : oublier les optimisations
Un code Release peut être très différent du C++ original.
Exemple :
Code: Select all
int x = 5;
return x * 2;
Code: Select all
mov eax, 10
ret
26. Méthode de traduction mentale
Quand tu vois :
Code: Select all
mov eax, [rcx+8]
Code: Select all
field = object->field;
Code: Select all
mov [rcx+8], eax
Code: Select all
object->field = value;
Code: Select all
test rax, rax
je ...
Code: Select all
if (!ptr)
Code: Select all
cmp eax, 10
jne ...
Code: Select all
if (x != 10)
Code: Select all
call Function
Code: Select all
RCX
RDX
R8
R9
Code: Select all
[rcx+r8*4]
Code: Select all
tableau[index]
Code: Select all
sub rsp, ...
Code: Select all
stack frame / variables locales
Exercice 1
Code: Select all
mov eax, [rcx+4]
sub eax, 1
mov [rcx+4], eax
Que fait probablement ce code ?
Réponse :
Code: Select all
un champ à l’offset +4 est décrémenté
Code: Select all
obj->field--;
Code: Select all
test rcx, rcx
je failed
Code: Select all
if (rcx == nullptr)
goto failed;
Code: Select all
cmp eax, edx
jge done
inc eax
jmp loop
Code: Select all
probablement une boucle avec une condition de type :
i < limit
Code: Select all
mov eax, [rcx+r8*4]
Code: Select all
lecture probable d’un int dans un tableau
Code: Select all
mov rcx, rbx
mov edx, 20
call sub_140001000
Code: Select all
fonction appelée avec :
arg1 = RBX
arg2 = 20
Voici un code C++ :
Code: Select all
struct Player
{
int hp;
int armor;
};
int Hit(Player* p, int damage)
{
int realDamage = damage - p->armor;
if (realDamage < 0)
realDamage = 0;
p->hp -= realDamage;
if (p->hp < 0)
p->hp = 0;
return p->hp;
}
Code: Select all
mov eax, edx
sub eax, dword ptr [rcx+4]
test eax, eax
jge damage_ok
xor eax, eax
damage_ok:
sub dword ptr [rcx], eax
cmp dword ptr [rcx], 0
jge hp_ok
mov dword ptr [rcx], 0
hp_ok:
mov eax, dword ptr [rcx]
ret
Code: Select all
[rcx] = hp
[rcx+4] = armor
edx = damage
eax = realDamage / résultat
Code: Select all
damage - armor
↓
si négatif → 0
↓
hp -= damage réel
↓
si hp négatif → 0
↓
return hp
29. Ce qu’il faut retenir
Le cœur du chapitre tient dans cette idée :
Code: Select all
le reverse engineering consiste à reconstruire
les concepts haut niveau
à partir de leur représentation bas niveau
- `[rsp+offset]` → variable locale probable ;
- `[registre+offset]` → structure ou objet probable ;
- `[base+index*taille]` → tableau probable ;
- `test reg, reg` → test de zéro probable ;
- `cmp + jcc` → condition probable ;
- saut vers le haut → boucle probable ;
- `call` → regarder les arguments ;
- `eax/rax` après appel → valeur de retour probable ;
- `sub rsp` → réservation de stack ;
- `VirtualAlloc`, `HeapAlloc`, `malloc`, `new` → allocation dynamique.
Il faut arriver à regarder :
Code: Select all
cmp byte ptr [rcx+4], 0
je failed
mov eax, [rcx]
add eax, 5
ret
Code: Select all
"on teste un champ booléen dans une structure,
puis on retourne un autre champ + 5"
Tu fais réellement du reverse engineering.
