Objectif du cours
Ce chapitre sert à comprendre Windows du point de vue du reverse engineer.
Le but n'est pas d'apprendre Windows comme un administrateur système, mais de savoir reconnaître ce que fait un programme quand on l'observe dans x64dbg, Ghidra, WinDbg ou un désassembleur.
L'idée centrale est :
Code: Select all
programme Win32
↓
DLL user-mode
↓
Native API / ntdll
↓
syscall
↓
kernel
↓
drivers / objets / mémoire / I/O
Code: Select all
VirtualAlloc
CreateFileW
CreateProcessW
CreateMutexW
ReadFile
WriteFile
- quels arguments regarder ;
- où la donnée se trouve ;
- ce que Windows fait derrière ;
- comment la retrouver dans le debugger ;
- comment remonter de l'API vers la logique du programme.
1. Architecture générale de Windows NT
Windows moderne repose sur l'architecture Windows NT.
Le principe fondamental est la séparation entre :
Code: Select all
User Mode
et
Kernel Mode
Le code user-mode comprend notamment :
- les applications ;
- les DLL ;
- les runtimes ;
- les bibliothèques ;
- les processus utilisateur.
Il n'a pas accès librement aux structures internes du kernel.
Kernel Mode
Le kernel-mode contient notamment :
- le noyau ;
- l'exécutif Windows ;
- les drivers ;
- le gestionnaire mémoire ;
- le gestionnaire d'objets ;
- le scheduler ;
- l'I/O manager.
Une erreur kernel peut affecter tout le système.
Une erreur user-mode affecte généralement seulement le processus concerné.
Vision simplifiée
Code: Select all
Application
|
v
Kernel32 / KernelBase / User32 / Advapi32 ...
|
v
ntdll.dll
|
v
syscall
|
v
ntoskrnl.exe
|
+--> Memory Manager
+--> Object Manager
+--> I/O Manager
+--> Process Manager
+--> Scheduler
+--> Drivers
2. Pourquoi l'OS est important en reverse
Un programme interagit constamment avec Windows.
Même un programme très simple doit :
- être chargé en mémoire ;
- créer un processus ;
- avoir au moins un thread ;
- utiliser une stack ;
- charger des DLL ;
- appeler des API ;
- accéder à des fichiers ou à la mémoire.
Exemple :
Code: Select all
CreateFileW
ReadFile
VirtualAlloc
WriteFile
Code: Select all
ouvrir un fichier
↓
lire des données
↓
allouer un buffer
↓
écrire quelque chose
3. Mémoire virtuelle
Chaque processus possède son propre espace d'adresses virtuelles.
Le programme manipule des adresses virtuelles.
Exemple :
Code: Select all
000001F4A0000000
Windows et le processeur effectuent la traduction.
Pourquoi c'est utile
Deux processus peuvent utiliser :
Code: Select all
0x0000000140000000
Chaque processus possède son propre espace virtuel.
En reverse
Quand x64dbg affiche :
Code: Select all
RAX = 000001F4A1230000
- cette adresse est-elle valide ?
- dans quelle région mémoire se trouve-t-elle ?
- stack ?
- heap ?
- module ?
- mapped file ?
- allocation privée ?
La mémoire virtuelle est organisée en pages.
Sur Windows x64, une page classique fait généralement :
Code: Select all
0x1000 octets = 4096 octets
Chaque page peut avoir différents attributs.
Exemples :
Code: Select all
PAGE_READONLY
PAGE_READWRITE
PAGE_EXECUTE
PAGE_EXECUTE_READ
PAGE_EXECUTE_READWRITE
PAGE_NOACCESS
Exemple :
Code: Select all
RX
Code: Select all
code exécutable
Code: Select all
RW
Code: Select all
données modifiables
Une page virtuelle n'est pas forcément présente immédiatement en RAM.
Quand le processeur accède à une page qui n'est pas actuellement disponible, il peut générer un :
Code: Select all
page fault
Ce page fault n'est pas forcément une erreur.
Windows peut :
- charger la page depuis le disque ;
- mapper une page ;
- résoudre une allocation ;
- faire du copy-on-write ;
- détecter un véritable accès invalide.
Un crash du type :
Code: Select all
Access Violation
- pointeur nul ;
- pointeur invalide ;
- page non accessible ;
- écriture dans une page en lecture seule ;
- exécution dans une zone non exécutable.
Le working set représente grossièrement les pages d'un processus actuellement présentes en mémoire physique.
Toutes les pages virtuelles d'un processus ne sont pas forcément résidentes.
Pour le reverse pratique, retiens surtout :
Code: Select all
espace virtuel != mémoire actuellement en RAM
- les page faults ;
- le paging ;
- la consommation mémoire ;
- certaines observations avec les outils système.
Windows sépare fortement les espaces accessibles au code utilisateur et au kernel.
Une application user-mode ne peut pas librement lire ou écrire la mémoire kernel.
Cette séparation protège le système.
En pratique
Dans un programme user-mode :
Code: Select all
VirtualAlloc
HeapAlloc
malloc
new
Dans un driver kernel, les mécanismes sont différents.
Pour ton reverse user-mode, le plus important est de savoir reconnaître :
- stack ;
- heap ;
- images de modules ;
- mapped files ;
- allocations privées.
Le kernel possède ses propres régions mémoire.
Pour le reverse user-mode, ce n'est pas la partie la plus importante.
Mais pour le kernel reverse, drivers et Windows Internals, elle devient essentielle.
Paged Pool
Mémoire kernel pouvant être paginée.
Nonpaged Pool
Mémoire kernel qui doit rester résidente.
Typiquement utilisée pour des données accessibles à des IRQL où le paging n'est pas possible.
Pour toi :
Code: Select all
user-mode reverse → notion secondaire
kernel reverse → notion importante
Un section object représente une région mémoire qui peut être mappée dans un ou plusieurs espaces d'adressage.
Il peut être :
- adossé au pagefile ;
- adossé à un fichier.
Il est utilisé pour :
- memory-mapped files ;
- partage mémoire ;
- chargement d'images exécutables ;
- DLL ;
- mappings partagés.
Code: Select all
CreateFileMappingW
MapViewOfFile
Vision
Code: Select all
fichier
↓
section
↓
mapping
↓
adresse virtuelle dans le processus
C++ :
Code: Select all
HANDLE hMap = CreateFileMappingW(
INVALID_HANDLE_VALUE,
nullptr,
PAGE_READWRITE,
0,
4096,
L"MyMapping"
);
void* p = MapViewOfFile(
hMap,
FILE_MAP_ALL_ACCESS,
0,
0,
4096
);
Code: Select all
CreateFileMappingW
MapViewOfFile
Pour `CreateFileMappingW`, le nom du mapping peut être très intéressant.
Pour `MapViewOfFile`, le retour dans `RAX` correspond à l'adresse mappée.
Tu peux ensuite suivre `RAX` dans le dump.
11. VAD Trees
Windows utilise des structures internes pour décrire les régions virtuelles d'un processus.
Code: Select all
VAD = Virtual Address Descriptor
Il peut indiquer par exemple :
- adresse de début ;
- adresse de fin ;
- type de mapping ;
- protections ;
- caractéristiques de la région.
Mais conceptuellement, ils expliquent pourquoi Windows connaît les différentes régions mémoire.
Réflexe pratique
Dans x64dbg, la Memory Map te montre justement une vue pratique des régions.
Tu peux voir :
Code: Select all
module
stack
heap
private
mapped
Code: Select all
VAD = structure interne
Memory Map = vue pratique du résultat
Dans un processus, tu rencontres principalement :
- allocations privées ;
- heaps ;
- stacks ;
- images exécutables ;
- mapped files.
Exemple :
Code: Select all
VirtualAlloc
Exemples :
Code: Select all
HeapAlloc
malloc
new
Créée pour les threads.
Image
EXE ou DLL chargée.
Mapped file
Fichier ou section mappée.
En reverse, savoir le type de région aide à comprendre ce que représente une adresse.
13. VirtualAlloc
`VirtualAlloc` est l'une des API mémoire les plus importantes.
Prototype simplifié :
Code: Select all
LPVOID VirtualAlloc(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
Code: Select all
RCX = lpAddress
RDX = dwSize
R8 = flAllocationType
R9 = flProtect
Code: Select all
VirtualAlloc(
nullptr,
0x1000,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
Code: Select all
xor ecx, ecx
mov edx, 1000h
mov r8d, 3000h
mov r9d, 4
call VirtualAlloc
Code: Select all
RAX = adresse de la zone
Si tu vois :
Code: Select all
R8D = 3000h
R9D = 4
Code: Select all
MEM_RESERVE | MEM_COMMIT
PAGE_READWRITE
Deux notions importantes :
Reserve
Réserve une plage d'adresses virtuelles.
Commit
Engage des ressources pour permettre l'utilisation réelle des pages.
Conceptuellement :
Code: Select all
reserve = je réserve l'espace d'adresses
commit = je rends les pages utilisables
15. VirtualProtect
`VirtualProtect` change la protection d'une région mémoire.
Exemple :
Code: Select all
DWORD oldProtect;
VirtualProtect(
p,
0x1000,
PAGE_EXECUTE_READWRITE,
&oldProtect
);
Pourquoi ?
Parce qu'il peut indiquer :
- changement de protection ;
- préparation de code exécutable ;
- patching ;
- JIT ;
- décompression ;
- chargement dynamique.
Sous Windows x64 :
Code: Select all
RCX = adresse
RDX = taille
R8 = nouvelle protection
R9 = pointeur vers ancienne protection
`VirtualQuery` permet d'obtenir des informations sur une région mémoire.
Exemple :
Code: Select all
MEMORY_BASIC_INFORMATION mbi;
VirtualQuery(
address,
&mbi,
sizeof(mbi)
);
- BaseAddress ;
- AllocationBase ;
- RegionSize ;
- State ;
- Protect ;
- Type.
Code: Select all
MEM_PRIVATE
MEM_MAPPED
MEM_IMAGE
Ces API servent à lire et écrire dans l'espace mémoire d'un autre processus.
Exemple :
Code: Select all
ReadProcessMemory(
hProcess,
address,
buffer,
size,
&bytesRead
);
Code: Select all
RCX
RDX
R8
R9
En reverse
Place un breakpoint sur :
Code: Select all
ReadProcessMemory
WriteProcessMemory
Code: Select all
RCX = handle processus
RDX = adresse distante
R8 = buffer local
R9 = taille
18. Objects et Handles
Windows repose beaucoup sur des objets kernel.
Exemples :
- process ;
- thread ;
- event ;
- mutex ;
- semaphore ;
- file ;
- section.
Elle utilise un :
Code: Select all
HANDLE
Code: Select all
HANDLE hFile = CreateFileW(...);
Important
Un handle n'est pas :
Code: Select all
l'adresse de l'objet
19. Handle Table
Chaque processus possède une table de handles.
Conceptuellement :
Code: Select all
HANDLE
↓
entrée dans la handle table
↓
objet kernel
Code: Select all
RCX = 00000000000000A4
call CloseHandle
C'est probablement un handle.
Réflexe
Un petit entier comme :
Code: Select all
0x84
0xA4
0x11C
20. Named Objects
Certains objets kernel peuvent être nommés.
Exemple :
Code: Select all
CreateMutexW(
nullptr,
FALSE,
L"MyMutex"
);
C'est utile pour :
- synchronisation inter-processus ;
- IPC ;
- détection d'instance unique ;
- partage de ressources.
Cherche les chaînes comme :
Code: Select all
Global\MyMutex
Local\MyEvent
MyMapping
21. Namespace des objets
Windows organise certains objets dans des namespaces.
Code: Select all
BaseNamedObjects
Devices
GLOBAL??
Code: Select all
nom d'objet → excellent indice
Code: Select all
Global\SessionLock
22. Processus
Un processus est essentiellement :
- un espace d'adresses virtuelles ;
- des ressources ;
- des handles ;
- des modules ;
- un contexte de sécurité ;
- un ou plusieurs threads.
Ce sont les threads qui exécutent le code.
En reverse
Quand tu ouvres un programme dans x64dbg, tu observes un processus.
Tu peux alors voir :
- ses modules ;
- ses threads ;
- sa memory map ;
- ses handles ;
- ses imports ;
- son code.
Un thread est l'unité d'exécution.
Il possède notamment :
- un contexte CPU ;
- des registres ;
- une stack user ;
- une stack kernel ;
- un état d'exécution.
En reverse
Un programme multithread peut rendre l'analyse plus complexe.
Tu dois parfois identifier :
- quel thread exécute la fonction ;
- quel thread attend ;
- quel thread crée une ressource ;
- quel thread traite l'I/O.
Lors d'un changement de thread, Windows sauvegarde l'état du thread actuel et restaure celui du prochain.
Conceptuellement :
Code: Select all
sauvegarde registres thread A
↓
choix thread B
↓
restauration registres thread B
↓
thread B continue
Code: Select all
un thread peut être interrompu puis reprendre plus tard
25. Synchronisation
Windows fournit plusieurs objets de synchronisation.
Event
État signalé / non signalé.
Exemple :
Code: Select all
CreateEventW
SetEvent
ResetEvent
WaitForSingleObject
Un seul propriétaire à la fois.
Code: Select all
CreateMutexW
ReleaseMutex
Compteur de ressources.
Code: Select all
CreateSemaphoreW
ReleaseSemaphore
Synchronisation optimisée user-mode.
Code: Select all
InitializeCriticalSection
EnterCriticalSection
LeaveCriticalSection
C++ :
Code: Select all
WaitForSingleObject(hEvent, INFINITE);
Code: Select all
mov rcx, rbx
mov edx, 0FFFFFFFFh
call WaitForSingleObject
Code: Select all
handle = RBX
timeout = INFINITE
Code: Select all
le thread attend cet event indéfiniment
Version simplifiée :
Code: Select all
CreateProcess
↓
création de l'objet processus
↓
création de l'espace d'adresses
↓
mapping de l'exécutable
↓
mapping de ntdll.dll
↓
création du premier thread
↓
création de la stack
↓
initialisation loader
↓
chargement DLL
↓
entry point
Pourquoi ?
Parce qu'au démarrage d'un processus, tu peux tomber dans du code de :
Code: Select all
ntdll
loader
runtime
CRT
28. LdrInitializeThunk
Lors du démarrage d'un processus, Windows passe par des routines internes de `ntdll`.
Une fonction importante est :
Code: Select all
LdrInitializeThunk
Elle prépare notamment le chargement des dépendances et l'environnement d'exécution.
En reverse
Si x64dbg s'arrête dans `ntdll` au démarrage, ce n'est pas encore forcément ton code.
Tu dois souvent continuer jusqu'au point d'entrée du module principal.
29. Win32 API
La Win32 API est l'interface classique utilisée par les applications Windows.
Exemples de DLL :
Code: Select all
Kernel32.dll
KernelBase.dll
User32.dll
Gdi32.dll
Advapi32.dll
Code: Select all
CreateFileW
VirtualAlloc
CreateProcessW
CreateThread
CreateMutexW
RegOpenKeyExW
30. Kernel32, KernelBase et autres DLL
Historiquement, beaucoup d'API étaient vues dans `Kernel32.dll`.
Sur les Windows modernes, de nombreuses fonctions sont implémentées ou redirigées via :
Code: Select all
KernelBase.dll
Code: Select all
nom API Win32
↓
wrapper / implémentation user-mode
↓
ntdll
↓
Native API
↓
syscall
Mais il faut savoir qu'elles existent.
31. Native API
La Native API est une couche plus proche du kernel.
Elle est notamment exposée via :
Code: Select all
ntdll.dll
Code: Select all
NtCreateFile
NtAllocateVirtualMemory
NtProtectVirtualMemory
NtQueryInformationProcess
NtOpenProcess
NtReadVirtualMemory
NtWriteVirtualMemory
Exemple
Conceptuellement :
Code: Select all
VirtualAlloc
↓
NtAllocateVirtualMemory
↓
syscall
↓
Memory Manager
Un programme peut appeler directement :
Code: Select all
NtOpenProcess
NtReadVirtualMemory
NtCreateSection
Donc si tu ne regardes que les imports Win32, tu peux manquer une partie de la logique.
Réflexe
Cherche aussi :
Code: Select all
Nt*
Zw*
Rtl*
Ldr*
33. System Call Mechanism
Pour exécuter une opération kernel, le programme doit passer de user-mode à kernel-mode.
Sur Windows x64 moderne, cela passe typiquement par :
Code: Select all
syscall
Code: Select all
application
↓
ntdll!NtXXX
↓
syscall
↓
kernel
↓
service kernel
↓
retour
Exemple conceptuel :
Code: Select all
mov r10, rcx
mov eax, SERVICE_NUMBER
syscall
ret
Le numéro exact de syscall peut varier selon la version de Windows.
Ne le mémorise pas comme une constante universelle.
34. Exemple pratique : reconnaître un syscall
Dans `ntdll`, tu peux tomber sur quelque chose proche de :
Code: Select all
mov r10, rcx
mov eax, 18h
syscall
ret
Code: Select all
stub de Native API
Pour savoir quelle API tu regardes, utilise :
- le nom du symbole ;
- l'import ;
- l'appelant ;
- le contexte.
Les exécutables Windows utilisent généralement le format :
Code: Select all
PE = Portable Executable
- EXE ;
- DLL ;
- drivers.
Il permet de retrouver :
- sections ;
- imports ;
- exports ;
- entry point ;
- image base ;
- resources ;
- relocations ;
- TLS ;
- debug info.
Un module est prévu pour être chargé à une adresse de base.
Exemple :
Code: Select all
ImageBase = 0000000140000000
37. RVA
RVA signifie :
Code: Select all
Relative Virtual Address
Exemple :
Code: Select all
ImageBase = 0x140000000
RVA = 0x1234
Code: Select all
0x140000000 + 0x1234
= 0x140001234
38. Sections PE
Un PE contient différentes sections.
Exemples courants :
Code: Select all
.text
.rdata
.data
.pdata
.rsrc
.reloc
Code exécutable.
.rdata
Constantes, données en lecture seule, chaînes, tables.
.data
Données globales modifiables.
.rsrc
Resources.
.reloc
Informations de relocation.
En reverse, la section te donne immédiatement une idée de ce que tu regardes.
39. Section Alignment
Le PE distingue notamment :
Code: Select all
FileAlignment
SectionAlignment
C'est une source classique de confusion.
Pour le reverse pratique, retiens :
Code: Select all
offset fichier != RVA != adresse virtuelle
40. Relocations
Un module n'est pas toujours chargé à son adresse préférée.
Dans ce cas, le loader peut devoir corriger certaines adresses.
C'est le rôle des relocations.
Conceptuellement :
Code: Select all
adresse attendue
↓
module chargé ailleurs
↓
delta
↓
correction des adresses concernées
41. DLL et linking
Il existe plusieurs formes de linking.
Static linking
Le code de bibliothèque est intégré dans l'exécutable.
Dynamic linking
Le programme dépend d'une DLL.
Exemple :
Code: Select all
Kernel32.dll
User32.dll
ntdll.dll
Le programme charge une DLL dynamiquement avec :
Code: Select all
LoadLibrary
GetProcAddress
42. Exemple pratique : LoadLibrary/GetProcAddress
C++ :
Code: Select all
HMODULE h = LoadLibraryW(L"user32.dll");
auto pMessageBox =
GetProcAddress(h, "MessageBoxW");
Code: Select all
lea rcx, [rip+User32String]
call LoadLibraryW
mov rcx, rax
lea rdx, [rip+MessageBoxString]
call GetProcAddress
Code: Select all
RAX = adresse de la fonction
Code: Select all
call rax
Code: Select all
call qword ptr [rbx]
La table d'import décrit les fonctions externes dont le module dépend.
Exemple :
Code: Select all
KERNEL32.dll
VirtualAlloc
CreateFileW
CloseHandle
USER32.dll
MessageBoxW
44. IAT
IAT signifie :
Code: Select all
Import Address Table
Un appel peut ressembler à :
Code: Select all
call qword ptr [rip+VirtualAlloc_IAT]
Code: Select all
call qword ptr [VirtualAlloc]
Quand tu vois :
Code: Select all
call [adresse]
45. Export Table
Une DLL peut exposer des fonctions via sa table d'exports.
Exemple :
Code: Select all
MyLibrary.dll
FunctionA
FunctionB
- nom ;
- ordinal.
Le PE contient plusieurs data directories.
Les plus utiles en reverse :
- Export Directory ;
- Import Directory ;
- Resource Directory ;
- Base Relocation Directory ;
- Debug Directory ;
- TLS Directory ;
- Load Configuration ;
- Bound Import ;
- Delay Import.
Peut contenir des callbacks exécutés avant l'entry point.
Très important en reverse.
Delay Import
Fonctions résolues seulement lorsqu'elles sont réellement utilisées.
47. TLS Callbacks
Un programme peut exécuter du code avant `main` ou l'entry point via des TLS callbacks.
En reverse, c'est important car :
Code: Select all
le comportement intéressant peut commencer avant main
- TLS callbacks ;
- initialisateurs CRT ;
- loader ;
- constructeurs globaux.
Le système d'I/O Windows relie :
Code: Select all
application
↓
API
↓
I/O Manager
↓
driver
↓
périphérique / filesystem / réseau
Exemple :
Code: Select all
application
↓
filesystem
↓
filter driver
↓
storage driver
49. Exemple pratique : CreateFileW
Code :
Code: Select all
HANDLE h = CreateFileW(
L"C:\\test\\data.bin",
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
0,
nullptr
);
Code: Select all
RCX = lpFileName
RDX = dwDesiredAccess
R8 = dwShareMode
R9 = lpSecurityAttributes
Dans x64dbg
Breakpoint :
Code: Select all
bp CreateFileW
- regarde RCX ;
- suis RCX dans le dump ;
- lis le chemin ;
- regarde RDX ;
- regarde R8 ;
- regarde l'appelant.
50. Win32 Subsystem
Des composants comme :
Code: Select all
USER32
GDI32
win32k
Pour le reverse user-mode classique :
Code: Select all
USER32.dll
GDI32.dll
Exemples :
Code: Select all
CreateWindowExW
GetMessageW
DispatchMessageW
MessageBoxW
Les handles USER/GDI ne se comportent pas exactement comme les handles kernel classiques.
Pour le reverse, retiens simplement :
Code: Select all
HANDLE kernel
HWND / HDC / HFONT / HBITMAP
Mais ce sont tous des identifiants utiles pour suivre les objets utilisés par le programme.
52. Structured Exception Handling
Windows fournit un mécanisme de gestion des exceptions :
Code: Select all
SEH = Structured Exception Handling
- access violations ;
- division par zéro ;
- exceptions explicites ;
- autres fautes processeur.
- gérer l'exception ;
- continuer ;
- propager ;
- terminer.
Hardware exception
Exemple :
Code: Select all
accès mémoire invalide
division par zéro
instruction invalide
Générée explicitement par le programme ou runtime.
Exemple :
Code: Select all
RaiseException
54. TEB / TIB
Chaque thread possède des structures associées.
Le TEB :
Code: Select all
Thread Environment Block
Sur les anciens mécanismes x86, le TIB/TEB était notamment lié à la chaîne SEH.
Sur x64 moderne, le mécanisme d'exception est différent et repose beaucoup sur les tables d'unwind.
Important
Ne mélange pas :
Code: Select all
SEH x86 classique basé sur chaîne
et
SEH x64 basé sur metadata / unwind
Pour ton reverse x64 moderne, garde surtout le concept général.
55. Exemple pratique : Access Violation
Tu vois :
Code: Select all
mov eax, [rcx]
Code: Select all
RCX = 0000000000000000
Code: Select all
[0]
Code: Select all
Access Violation
- l'instruction fautive ;
- le registre base ;
- l'adresse mémoire ;
- l'appelant ;
- la stack.
56. Traduction Win32 → assembleur
Le plus important pour toi est de reconnaître les API dans le code machine.
VirtualAlloc
Code: Select all
xor ecx, ecx
mov edx, 1000h
mov r8d, 3000h
mov r9d, 4
call VirtualAlloc
Code: Select all
VirtualAlloc(
NULL,
0x1000,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
);
Code: Select all
lea rcx, [rip+Path]
mov edx, 80000000h
mov r8d, 1
xor r9d, r9d
call CreateFileW
Code: Select all
path = chaîne pointée par RCX
GENERIC_READ
FILE_SHARE_READ
lpSecurityAttributes = NULL
Code: Select all
mov rcx, rbx
mov edx, 0FFFFFFFFh
call WaitForSingleObject
Code: Select all
WaitForSingleObject(
RBX,
INFINITE
);
C'est l'une des meilleures techniques de reverse Windows.
Supposons que tu veux comprendre les allocations.
Tu places un breakpoint sur :
Code: Select all
VirtualAlloc
- tu exécutes ;
- le breakpoint se déclenche ;
- tu regardes les arguments ;
- tu notes l'adresse de retour ;
- tu remontes dans l'appelant ;
- tu suis la zone mémoire.
Code: Select all
CreateFileW
RegOpenKeyExW
CreateProcessW
OpenProcess
ReadProcessMemory
WriteProcessMemory
CreateThread
CreateMutexW
Quand tu ouvres un binaire inconnu :
Étape 1
Regarde les imports.
Exemple :
Code: Select all
VirtualAlloc
WriteProcessMemory
CreateRemoteThread
OpenProcess
- un autre processus ;
- de la mémoire distante ;
- un thread distant.
59. Strings : deuxième point d'entrée
Cherche les chaînes.
Exemples :
Code: Select all
"C:\\config.ini"
"Global\\MyMutex"
"Access denied"
"127.0.0.1"
"kernel32.dll"
C'est souvent plus rapide que de partir de l'entry point.
60. Memory Map : troisième point d'entrée
Dans x64dbg, la Memory Map permet de voir :
- modules ;
- stacks ;
- heaps ;
- allocations privées ;
- mappings ;
- protections.
Code: Select all
RWX
Si tu vois :
Code: Select all
MEM_IMAGE
61. Mini reverse 1 : VirtualAlloc
Tu observes :
Code: Select all
00007FF700001000 xor ecx, ecx
00007FF700001002 mov edx, 2000h
00007FF700001007 mov r8d, 3000h
00007FF70000100D mov r9d, 4
00007FF700001013 call VirtualAlloc
00007FF700001018 mov rbx, rax
Code: Select all
RCX = NULL
RDX = 0x2000
R8 = 0x3000
R9 = 0x4
Code: Select all
VirtualAlloc(
NULL,
0x2000,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
)
Code: Select all
mov rbx, rax
Code: Select all
RBX conserve le pointeur retourné
Tu vois :
Code: Select all
mov rcx, rsi
mov rdx, rbx
lea r8, [rsp+40h]
mov r9d, 20h
call WriteProcessMemory
Code: Select all
RCX = hProcess
RDX = adresse distante
R8 = buffer local
R9 = 0x20 octets
Code: Select all
32 octets
Tu peux ensuite :
- inspecter le buffer local ;
- inspecter l'adresse distante ;
- remonter pour savoir d'où vient le handle.
Quand tu places un breakpoint sur :
Code: Select all
CreateProcessW
Code: Select all
RCX = lpApplicationName
RDX = lpCommandLine
Tu peux ainsi retrouver :
- le programme lancé ;
- la ligne de commande ;
- les paramètres.
64. Mini reverse 4 : appel via IAT
Tu vois :
Code: Select all
call qword ptr [rip+1234h]
Code: Select all
rip+1234h
Code: Select all
KERNEL32!CreateFileW
Code: Select all
CreateFileW
65. Mini reverse 5 : LoadLibrary + GetProcAddress
Tu vois :
Code: Select all
lea rcx, [rip+DllName]
call LoadLibraryW
mov rcx, rax
lea rdx, [rip+FuncName]
call GetProcAddress
mov rbx, rax
Code: Select all
HMODULE h = LoadLibraryW(DllName);
void* f = GetProcAddress(h, FuncName);
Code: Select all
call rbx
66. Mini reverse 6 : condition sur un handle
C++ :
Code: Select all
HANDLE h = CreateFileW(...);
if (h == INVALID_HANDLE_VALUE)
{
fail();
}
Code: Select all
call CreateFileW
cmp rax, -1
je failed
Code: Select all
RAX = retour API
-1 = INVALID_HANDLE_VALUE
67. Mini reverse 7 : synchronisation
Tu vois :
Code: Select all
lea r8, [rip+MutexName]
xor edx, edx
xor ecx, ecx
call CreateMutexW
Tu trouves :
Code: Select all
Global\MyAppMutex
Code: Select all
le programme crée un mutex nommé
Code: Select all
GetLastError
Code: Select all
ERROR_ALREADY_EXISTS
68. Travail pratique dans x64dbg
Ta méthode peut être :
1. Ouvrir le programme
Regarder :
- modules ;
- imports ;
- strings ;
- memory map.
Exemple :
Code: Select all
VirtualAlloc
CreateFileW
CreateProcessW
Quand l'API est appelée :
- regarder RCX ;
- RDX ;
- R8 ;
- R9 ;
- stack pour les arguments suivants.
Pour beaucoup d'API :
Code: Select all
RAX = valeur retournée
Le plus intéressant n'est pas toujours l'API.
C'est souvent :
Code: Select all
pourquoi elle est appelée
Exemple :
Code: Select all
RBX = allocatedBuffer
RSI = hProcess
RDI = fileHandle
69. Ce qu'il ne faut pas faire
Erreur 1 : suivre toutes les DLL système
Inutile.
Tu peux vite te perdre dans :
Code: Select all
KernelBase
ntdll
ucrtbase
vcruntime
Erreur 2 : lire tout depuis l'entry point
Souvent inefficace.
Imports + strings + breakpoints API sont souvent bien plus rapides.
Erreur 3 : ignorer la Memory Map
La mémoire raconte beaucoup de choses.
Erreur 4 : oublier la convention d'appel
Sous Windows x64 :
Code: Select all
RCX
RDX
R8
R9
Erreur 5 : confondre handle et pointeur
Un handle peut être un petit entier.
Un pointeur est généralement une adresse virtuelle.
70. Défi pratique complet
Code C++ :
Code: Select all
#include <Windows.h>
int main()
{
HANDLE hFile = CreateFileW(
L"data.bin",
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
0,
nullptr
);
if (hFile == INVALID_HANDLE_VALUE)
return 0;
void* buffer = VirtualAlloc(
nullptr,
0x1000,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
);
if (!buffer)
{
CloseHandle(hFile);
return 0;
}
DWORD read = 0;
ReadFile(
hFile,
buffer,
0x100,
&read,
nullptr
);
CloseHandle(hFile);
return 0;
}
1. Breakpoint :
Code: Select all
CreateFileW
Code: Select all
RCX
Code: Select all
data.bin
Code: Select all
RAX = hFile
Code: Select all
VirtualAlloc
Code: Select all
RCX = NULL
RDX = 1000h
R8 = 3000h
R9 = 4
Code: Select all
RAX = buffer
5. Breakpoint :
Code: Select all
ReadFile
Code: Select all
RCX = hFile
RDX = buffer
R8 = 0x100
R9 = &read
Tu peux voir les données lues.
Tu viens de reconstruire le comportement sans avoir besoin des symboles.
71. Résumé mental à retenir
Quand tu vois :
Code: Select all
VirtualAlloc
Code: Select all
nouvelle région mémoire
↓
regarder taille / flags / protection
↓
RAX = pointeur
Code: Select all
CreateFileW
Code: Select all
RCX = chemin
↓
RAX = handle
Code: Select all
ReadProcessMemory
WriteProcessMemory
Code: Select all
handle processus
adresse distante
buffer local
taille
Code: Select all
call [IAT]
Code: Select all
fonction importée
Code: Select all
LoadLibrary + GetProcAddress
Code: Select all
résolution dynamique
Code: Select all
NtXXX
Code: Select all
Native API
↓
potentiellement syscall
Code: Select all
syscall
Code: Select all
transition user → kernel
Code: Select all
.text
.rdata
.data
Code: Select all
code
constantes / strings
globals
Code: Select all
Access Violation
Code: Select all
adresse invalide / protection / pointeur
Comprendre Windows est l'un des plus gros accélérateurs en reverse engineering.
Tu n'as pas besoin de connaître chaque structure interne du noyau pour reverser un programme user-mode.
En revanche, tu dois être capable de reconnaître :
- la mémoire virtuelle ;
- les allocations ;
- les handles ;
- les processus ;
- les threads ;
- les API Win32 ;
- la Native API ;
- les syscalls ;
- le format PE ;
- les DLL ;
- les imports / exports ;
- les sections ;
- les exceptions.
Code: Select all
"je vais lire tout le binaire"
Code: Select all
"je vais chercher les points d'entrée utiles"
Code: Select all
imports
strings
API
memory map
IAT
DLL
breakpoints
C'est exactement ainsi qu'on passe de :
Code: Select all
"je vois des API Windows"
Code: Select all
"je comprends ce que fait le programme"
