Objectif du chapitre
Ce chapitre montre comment partir d’une fonction inconnue ou peu documentée et reconstruire progressivement :
- son rôle ;
- ses paramètres ;
- ses structures internes ;
- ses callbacks ;
- ses boucles ;
- sa logique générale ;
- un pseudo-code C/C++ compréhensible.
L’idée importante est qu’en reverse engineering on ne cherche pas à lire de l’assembleur pour lire de l’assembleur. On cherche à transformer des instructions machine en concepts connus : pointeurs, structures, boucles, tableaux, arbres, callbacks et appels d’API.
1. Repérer une API intéressante
Une API non documentée peut être repérée dans les exports d’une DLL, dans Ghidra, dans x64dbg, avec dumpbin ou en observant les appels effectués par un programme.
Code: Select all
dumpbin /exports ntdll.dll
Code: Select all
RtlInitializeGenericTable
RtlInsertElementGenericTable
RtlLookupElementGenericTable
RtlDeleteElementGenericTable
2. Commencer par les paramètres
En x64 Windows, les quatre premiers paramètres entiers/pointeurs sont généralement passés dans RCX, RDX, R8 et R9.
Si RCX est utilisé comme base d’accès mémoire :
Code: Select all
mov eax, [rcx+14h]
Pseudo-code :
Code: Select all
eax = *(int*)((char*)rcx + 0x14);
Code: Select all
mov rax, rcx
Code: Select all
mov rax, [rcx]
Code: Select all
mov eax, [rcx+10h]
Code: Select all
eax = *(uint32_t*)((char*)rcx + 0x10);
4. LEA : calculer une adresse sans lire la mémoire
Code: Select all
lea rax, [rcx+20h]
Equivalent C :
Code: Select all
rax = (char*)rcx + 0x20;
Code: Select all
mov rax, [rcx+20h] ; lit la valeur en mémoire
lea rax, [rcx+20h] ; calcule seulement l'adresse
5. Déduire une structure à partir des offsets
Supposons :
Code: Select all
mov rax, [rcx]
mov rbx, [rcx+8]
mov edx, [rcx+10h]
mov eax, [rcx+14h]
Code: Select all
struct UNKNOWN
{
void* field0; // +0x00
void* field1; // +0x08
uint32_t field2; // +0x10
uint32_t field3; // +0x14
};
6. Identifier un compteur
Code: Select all
mov eax, [rcx+14h]
inc eax
mov [rcx+14h], eax
Code: Select all
table->Count++;
Code: Select all
mov eax, [rcx+14h]
ret
7. Identifier un pointeur
Code: Select all
mov rax, [rcx+8]
test rax, rax
je empty
Code: Select all
if (table->Root == nullptr)
{
...
}
8. Reconnaître une condition
Code: Select all
cmp eax, ebx
je equal
jg greater
jl lower
Code: Select all
if (eax == ebx) { ... }
else if (eax > ebx) { ... }
else { ... }
Code: Select all
loop_start:
test rcx, rcx
je done
mov eax, [rcx]
mov rcx, [rcx+8]
jmp loop_start
done:
Pseudo-code :
Code: Select all
while (node != nullptr)
{
int value = node->value;
node = node->next;
}
Code: Select all
mov eax, [rcx+r8*4]
inc r8
cmp r8, rdx
jb loop_start
Code: Select all
for (size_t i = 0; i < count; i++)
{
int value = array[i];
}
11. Reconnaître un callback
Code: Select all
mov rax, [rcx+20h]
call rax
Pseudo-code :
Code: Select all
table->CompareCallback(...);
Code: Select all
call qword ptr [rcx+20h]
Code: Select all
mov rax, [rcx+20h]
mov rcx, rbx
mov rdx, rsi
call rax
Code: Select all
typedef int (*COMPARE_CALLBACK)(void* a, void* b);
13. Cas pratique : Generic Table
Le chapitre analyse plusieurs fonctions de la Generic Table de NTDLL. Le but n’est pas seulement de comprendre ces fonctions, mais d’apprendre une méthode réutilisable sur n’importe quelle API inconnue.
14. RtlInitializeGenericTable
Une fonction d’initialisation révèle souvent directement la disposition interne d’une structure.
Exemple simplifié :
Code: Select all
mov [rcx+00h], rdx
mov [rcx+08h], r8
mov [rcx+10h], r9
mov dword ptr [rcx+18h], 0
Code: Select all
struct TABLE
{
void* CompareRoutine;
void* AllocateRoutine;
void* FreeRoutine;
uint32_t Count;
};
Code: Select all
mov eax, [rcx+18h]
ret
16. Recherche dans une structure
Code: Select all
mov rbx, [rcx+8]
search:
test rbx, rbx
je not_found
mov eax, [rbx+10h]
cmp edx, eax
je found
jl go_left
mov rbx, [rbx+8]
jmp search
go_left:
mov rbx, [rbx]
jmp search
Code: Select all
NODE* node = table->Root;
while (node)
{
if (value == node->value) return node;
if (value < node->value) node = node->left;
else node = node->right;
}
17. Reconstruire la structure NODE
À partir des accès [rbx], [rbx+8] et [rbx+10h] :
Code: Select all
struct NODE
{
NODE* left;
NODE* right;
int value;
};
18. Comprendre les arbres binaires
Code: Select all
50
/ \
20 80
/ \ / \
10 30 60 90
19. Splay Tree
Un splay tree peut réorganiser ses nœuds après un accès pour rapprocher l’élément utilisé de la racine.
Avant :
Code: Select all
100
/
50
/
20
Code: Select all
20
\
50
\
100
Une recherche repose souvent sur un pointeur vers le nœud courant, une comparaison et un déplacement vers le fils gauche ou droit.
Code: Select all
loop:
test rbx, rbx
je not_found
mov rcx, searchValue
mov rdx, [rbx+DataOffset]
call compareCallback
test eax, eax
je found
jl left
right:
mov rbx, [rbx+RightOffset]
jmp loop
left:
mov rbx, [rbx+LeftOffset]
jmp loop
Une insertion doit généralement rechercher si l’élément existe, trouver le futur parent, allouer un nœud, copier les données, relier le nœud, incrémenter le compteur et éventuellement réorganiser l’arbre.
Pseudo-code simplifié :
Code: Select all
NODE* Insert(TABLE* table, void* data)
{
NODE* parent = FindInsertionPoint(table, data);
NODE* node = table->AllocateRoutine(sizeof(NODE));
node->left = nullptr;
node->right = nullptr;
memcpy(node->data, data, table->ElementSize);
if (parent == nullptr) table->Root = node;
else if (table->CompareRoutine(data, parent->data) < 0) parent->left = node;
else parent->right = node;
table->Count++;
return node;
}
Code: Select all
copy_loop:
mov eax, [rsi]
mov [rdi], eax
add rsi, 4
add rdi, 4
sub rcx, 4
jne copy_loop
Code: Select all
memcpy(destination, source, size);
23. RtlDeleteElementGenericTable
Une suppression implique généralement de trouver le nœud, réparer les liens, éventuellement réorganiser l’arbre, appeler le callback de libération et décrémenter le compteur.
24. Déduire le type d’un champ
Code: Select all
mov eax, [rcx+10h] ; lecture 32 bits
mov rax, [rcx+10h] ; lecture 64 bits
movzx eax, byte ptr [rcx+10h] ; 1 octet
movzx eax, word ptr [rcx+10h] ; 2 octets
25. Workflow x64dbg
1. Charger le programme.
2. Placer un breakpoint sur l’API intéressante.
3. Lancer le programme.
4. Observer RCX/RDX/R8/R9.
5. Suivre les pointeurs dans le dump mémoire.
6. Avancer instruction par instruction.
7. Noter chaque offset.
8. Noter les appels indirects.
9. Donner un rôle aux registres.
10. Réécrire progressivement en pseudo-code.
Exemple de notes :
Code: Select all
RCX = table
RDX = element
[RCX+08] = root
[RCX+18] = count
[RCX+20] = compareCallback
Dans Ghidra : ouvrir la fonction, regarder le désassemblage et le décompilateur, renommer les variables, créer une structure, appliquer la structure au pointeur et renommer les fonctions selon leur comportement.
Avant :
Code: Select all
FUN_140001230(param_1, param_2)
Code: Select all
FindNode(table, key)
Si on observe :
Code: Select all
[RCX+00]
[RCX+08]
[RCX+10]
[RCX+18]
[RCX+20]
28. Reconstruire du C à partir de l’ASM
Code: Select all
mov eax, [rcx+18h]
test eax, eax
je empty
mov rax, [rcx+8]
ret
empty:
xor eax, eax
ret
Code: Select all
void* GetRoot(TABLE* table)
{
if (table->Count == 0) return nullptr;
return table->Root;
}
Étape 1 : identifier les paramètres.
Étape 2 : repérer les pointeurs.
Étape 3 : noter tous les accès [registre+offset].
Étape 4 : identifier les callbacks avec CALL registre ou CALL [mémoire].
Étape 5 : reconstruire les conditions avec CMP, TEST et Jcc.
Étape 6 : reconstruire les boucles.
Étape 7 : identifier tableaux, listes, arbres ou structures.
Étape 8 : écrire du pseudo-code C.
Étape 9 : confirmer avec d’autres fonctions utilisant la même structure.
30. Exercice pratique complet
Code: Select all
#include <iostream>
struct Node
{
int value;
Node* left;
Node* right;
};
Node* Find(Node* node, int value)
{
while (node)
{
if (node->value == value)
return node;
if (value < node->value)
node = node->left;
else
node = node->right;
}
return nullptr;
}
int main()
{
Node a{50, nullptr, nullptr};
Node b{20, nullptr, nullptr};
Node c{80, nullptr, nullptr};
a.left = &b;
a.right = &c;
Node* result = Find(&a, 80);
if (result)
std::cout << result->value << std::endl;
}
Structure attendue en x64 :
Code: Select all
struct Node
{
int value; // +0x00
padding // +0x04
Node* left; // +0x08
Node* right; // +0x10
};
Face à une fonction inconnue, le but est de transformer progressivement :
Code: Select all
mov eax, [rcx+14h]
Code: Select all
table->Count
Code: Select all
call qword ptr [rcx+20h]
Code: Select all
table->CompareCallback(...)
Code: Select all
cmp eax, ebx
jl left
jg right
Le reverse engineering consiste surtout à reconnaître des motifs, construire des hypothèses, puis les confirmer avec d’autres fonctions et avec l’exécution réelle du programme.
