Windows Fundamentals pour le reverse engineering — architecture, mémoire, API, PE, syscalls et x64dbg

Salut ce forum est dédié à comprendre les entrailles de l'ordinateur au plus bas niveau bienvenue dans le monde du reverse et de l'assembleur

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Windows Fundamentals pour le reverse engineering — architecture, mémoire, API, PE, syscalls et x64dbg

Post by Hydraxx »

Windows Fundamentals pour le reverse engineering — architecture, mémoire, API, PE, syscalls et x64dbg

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
Quand tu vois une API comme :

Code: Select all

VirtualAlloc
CreateFileW
CreateProcessW
CreateMutexW
ReadFile
WriteFile
tu dois progressivement savoir :
  • 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.
Ce cours mélange donc théorie minimale et pratique maximale.

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
User Mode

Le code user-mode comprend notamment :
  • les applications ;
  • les DLL ;
  • les runtimes ;
  • les bibliothèques ;
  • les processus utilisateur.
Un programme classique C++ s'exécute ici.

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.
Le kernel possède plus de privilèges.

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
En reverse, cette architecture te permet de comprendre où tu te trouves.

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.
Donc en reverse, beaucoup d'indices viennent du système.

Exemple :

Code: Select all

CreateFileW
ReadFile
VirtualAlloc
WriteFile
Avant même de lire l'assembleur, tu peux soupçonner :

Code: Select all

ouvrir un fichier
↓
lire des données
↓
allouer un buffer
↓
écrire quelque chose
C'est pour cela que connaître Windows accélère énormément le reverse.

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
Cette adresse ne correspond pas nécessairement directement à une adresse physique RAM.

Windows et le processeur effectuent la traduction.

Pourquoi c'est utile

Deux processus peuvent utiliser :

Code: Select all

0x0000000140000000
sans partager la même mémoire physique.

Chaque processus possède son propre espace virtuel.

En reverse

Quand x64dbg affiche :

Code: Select all

RAX = 000001F4A1230000
tu dois te demander :
  • cette adresse est-elle valide ?
  • dans quelle région mémoire se trouve-t-elle ?
  • stack ?
  • heap ?
  • module ?
  • mapped file ?
  • allocation privée ?
4. Pages mémoire

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
Windows gère la mémoire par régions et par pages.

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
En reverse, les protections donnent des indices.

Exemple :

Code: Select all

RX
signifie souvent :

Code: Select all

code exécutable
Alors que :

Code: Select all

RW
signifie souvent :

Code: Select all

données modifiables
5. Paging et page faults

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
Le système examine alors la situation.

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.
En reverse

Un crash du type :

Code: Select all

Access Violation
peut être lié à :
  • pointeur nul ;
  • pointeur invalide ;
  • page non accessible ;
  • écriture dans une page en lecture seule ;
  • exécution dans une zone non exécutable.
6. Working Set

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
C'est une notion utile pour comprendre :
  • les page faults ;
  • le paging ;
  • la consommation mémoire ;
  • certaines observations avec les outils système.
7. Mémoire user et mémoire kernel

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
créent de la mémoire dans l'espace utilisateur.

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.
8. Kernel Address Space

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
9. Section Objects

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.
C'est un mécanisme fondamental de Windows.

Il est utilisé pour :
  • memory-mapped files ;
  • partage mémoire ;
  • chargement d'images exécutables ;
  • DLL ;
  • mappings partagés.
Exemple user-mode

Code: Select all

CreateFileMappingW
MapViewOfFile
Ces API permettent de créer puis mapper une section.

Vision

Code: Select all

fichier
↓
section
↓
mapping
↓
adresse virtuelle dans le processus
10. Exemple pratique : File Mapping

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
);
En reverse, tu peux placer des breakpoints sur :

Code: Select all

CreateFileMappingW
MapViewOfFile
Puis regarder les arguments.

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
Chaque VAD décrit une région mémoire.

Il peut indiquer par exemple :
  • adresse de début ;
  • adresse de fin ;
  • type de mapping ;
  • protections ;
  • caractéristiques de la région.
En reverse user-mode avec x64dbg, tu ne manipules pas directement les VAD la plupart du temps.

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
Donc :

Code: Select all

VAD = structure interne
Memory Map = vue pratique du résultat
12. Types d'allocations user-mode

Dans un processus, tu rencontres principalement :
  • allocations privées ;
  • heaps ;
  • stacks ;
  • images exécutables ;
  • mapped files.
Private allocation

Exemple :

Code: Select all

VirtualAlloc
Heap

Exemples :

Code: Select all

HeapAlloc
malloc
new
Stack

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
);
Sous Windows x64 :

Code: Select all

RCX = lpAddress
RDX = dwSize
R8  = flAllocationType
R9  = flProtect
Exemple :

Code: Select all

VirtualAlloc(
    nullptr,
    0x1000,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_READWRITE
);
Assembleur possible :

Code: Select all

xor ecx, ecx
mov edx, 1000h
mov r8d, 3000h
mov r9d, 4
call VirtualAlloc
Après l'appel :

Code: Select all

RAX = adresse de la zone
Réflexe reverse

Si tu vois :

Code: Select all

R8D = 3000h
R9D = 4
pense :

Code: Select all

MEM_RESERVE | MEM_COMMIT
PAGE_READWRITE
14. Reserve et Commit

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
`MEM_RESERVE | MEM_COMMIT` est très courant.

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
);
En reverse, un appel à `VirtualProtect` est intéressant.

Pourquoi ?

Parce qu'il peut indiquer :
  • changement de protection ;
  • préparation de code exécutable ;
  • patching ;
  • JIT ;
  • décompression ;
  • chargement dynamique.
À regarder

Sous Windows x64 :

Code: Select all

RCX = adresse
RDX = taille
R8  = nouvelle protection
R9  = pointeur vers ancienne protection
16. VirtualQuery

`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)
);
Tu peux récupérer :
  • BaseAddress ;
  • AllocationBase ;
  • RegionSize ;
  • State ;
  • Protect ;
  • Type.
C'est utile pour comprendre si une adresse appartient à :

Code: Select all

MEM_PRIVATE
MEM_MAPPED
MEM_IMAGE
17. ReadProcessMemory et WriteProcessMemory

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
);
Sous Windows x64, les quatre premiers arguments sont dans :

Code: Select all

RCX
RDX
R8
R9
Les suivants sont sur la stack.

En reverse

Place un breakpoint sur :

Code: Select all

ReadProcessMemory
WriteProcessMemory
Puis regarde :

Code: Select all

RCX = handle processus
RDX = adresse distante
R8  = buffer local
R9  = taille
Cela donne immédiatement énormément d'informations.

18. Objects et Handles

Windows repose beaucoup sur des objets kernel.

Exemples :
  • process ;
  • thread ;
  • event ;
  • mutex ;
  • semaphore ;
  • file ;
  • section.
Une application ne manipule généralement pas directement le pointeur kernel de l'objet.

Elle utilise un :

Code: Select all

HANDLE
Exemple :

Code: Select all

HANDLE hFile = CreateFileW(...);
`hFile` est une valeur permettant au processus de référencer un objet.

Important

Un handle n'est pas :

Code: Select all

l'adresse de l'objet
C'est un identifiant géré via la handle table du processus.

19. Handle Table

Chaque processus possède une table de handles.

Conceptuellement :

Code: Select all

HANDLE
↓
entrée dans la handle table
↓
objet kernel
En reverse, si tu vois :

Code: Select all

RCX = 00000000000000A4
call CloseHandle
`0xA4` n'est probablement pas une adresse.

C'est probablement un handle.

Réflexe

Un petit entier comme :

Code: Select all

0x84
0xA4
0x11C
passé à une API d'objet Windows peut être un handle.

20. Named Objects

Certains objets kernel peuvent être nommés.

Exemple :

Code: Select all

CreateMutexW(
    nullptr,
    FALSE,
    L"MyMutex"
);
Deux processus utilisant le même nom peuvent obtenir des handles vers le même objet.

C'est utile pour :
  • synchronisation inter-processus ;
  • IPC ;
  • détection d'instance unique ;
  • partage de ressources.
En reverse

Cherche les chaînes comme :

Code: Select all

Global\MyMutex
Local\MyEvent
MyMapping
Elles donnent souvent des indices très forts.

21. Namespace des objets

Windows organise certains objets dans des namespaces.

Code: Select all

BaseNamedObjects
Devices
GLOBAL??
Pour le reverse user-mode, retiens surtout :

Code: Select all

nom d'objet → excellent indice
Exemple :

Code: Select all

Global\SessionLock
peut indiquer un mécanisme de synchronisation global.

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.
Un processus n'est pas l'unité d'exécution réelle.

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.
23. Threads

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.
Le scheduler décide quel thread utilise le CPU.

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.
24. Context Switching

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
Pour toi, retiens surtout :

Code: Select all

un thread peut être interrompu puis reprendre plus tard
Le contexte CPU est donc essentiel.

25. Synchronisation

Windows fournit plusieurs objets de synchronisation.

Event

État signalé / non signalé.

Exemple :

Code: Select all

CreateEventW
SetEvent
ResetEvent
WaitForSingleObject
Mutex

Un seul propriétaire à la fois.

Code: Select all

CreateMutexW
ReleaseMutex
Semaphore

Compteur de ressources.

Code: Select all

CreateSemaphoreW
ReleaseSemaphore
Critical Section

Synchronisation optimisée user-mode.

Code: Select all

InitializeCriticalSection
EnterCriticalSection
LeaveCriticalSection
26. Exemple pratique : reconnaître une attente

C++ :

Code: Select all

WaitForSingleObject(hEvent, INFINITE);
En reverse :

Code: Select all

mov rcx, rbx
mov edx, 0FFFFFFFFh
call WaitForSingleObject
Tu peux reconstruire :

Code: Select all

handle = RBX
timeout = INFINITE
Si tu sais que `RBX` contient un event, tu comprends :

Code: Select all

le thread attend cet event indéfiniment
27. Séquence de création d'un processus


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
Cette séquence est extrêmement utile en reverse.

Pourquoi ?

Parce qu'au démarrage d'un processus, tu peux tomber dans du code de :

Code: Select all

ntdll
loader
runtime
CRT
avant d'arriver au vrai code utilisateur.

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 participe à l'initialisation du loader.

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
Exemples d'API :

Code: Select all

CreateFileW
VirtualAlloc
CreateProcessW
CreateThread
CreateMutexW
RegOpenKeyExW
En reverse, les imports Win32 sont extrêmement précieux.

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
Le point important pour le reverse :

Code: Select all

nom API Win32
↓
wrapper / implémentation user-mode
↓
ntdll
↓
Native API
↓
syscall
Tu n'as pas besoin de suivre toutes les couches à chaque fois.

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
On y retrouve des fonctions comme :

Code: Select all

NtCreateFile
NtAllocateVirtualMemory
NtProtectVirtualMemory
NtQueryInformationProcess
NtOpenProcess
NtReadVirtualMemory
NtWriteVirtualMemory
La Win32 API peut appeler ces fonctions en interne.

Exemple

Conceptuellement :

Code: Select all

VirtualAlloc
↓
NtAllocateVirtualMemory
↓
syscall
↓
Memory Manager
32. Pourquoi la Native API est importante en reverse

Un programme peut appeler directement :

Code: Select all

NtOpenProcess
NtReadVirtualMemory
NtCreateSection
sans passer par les wrappers Win32 classiques.

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*
dans `ntdll`.

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
Schéma :

Code: Select all

application
↓
ntdll!NtXXX
↓
syscall
↓
kernel
↓
service kernel
↓
retour
Le stub de syscall contient généralement un numéro de service.

Exemple conceptuel :

Code: Select all

mov r10, rcx
mov eax, SERVICE_NUMBER
syscall
ret
Important

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
Ce motif indique très fortement :

Code: Select all

stub de Native API
La valeur dans `EAX` correspond au numéro de service pour cette version.

Pour savoir quelle API tu regardes, utilise :
  • le nom du symbole ;
  • l'import ;
  • l'appelant ;
  • le contexte.
35. Format PE

Les exécutables Windows utilisent généralement le format :

Code: Select all

PE = Portable Executable
Il est utilisé par :
  • EXE ;
  • DLL ;
  • drivers.
Comprendre le PE est très important en reverse.

Il permet de retrouver :
  • sections ;
  • imports ;
  • exports ;
  • entry point ;
  • image base ;
  • resources ;
  • relocations ;
  • TLS ;
  • debug info.
36. Image Base

Un module est prévu pour être chargé à une adresse de base.

Exemple :

Code: Select all

ImageBase = 0000000140000000
Les adresses internes sont souvent exprimées relativement à cette base.

37. RVA

RVA signifie :

Code: Select all

Relative Virtual Address
C'est une adresse relative au début de l'image.

Exemple :

Code: Select all

ImageBase = 0x140000000
RVA       = 0x1234
Adresse virtuelle :

Code: Select all

0x140000000 + 0x1234
= 0x140001234
C'est une notion essentielle dans le PE.

38. Sections PE

Un PE contient différentes sections.

Exemples courants :

Code: Select all

.text
.rdata
.data
.pdata
.rsrc
.reloc
.text

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
Les offsets dans le fichier ne correspondent pas toujours directement aux adresses en mémoire.

C'est une source classique de confusion.

Pour le reverse pratique, retiens :

Code: Select all

offset fichier != RVA != adresse virtuelle
Il faut savoir convertir.

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
Avec ASLR, cette notion est très importante.

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
Runtime linking

Le programme charge une DLL dynamiquement avec :

Code: Select all

LoadLibrary
GetProcAddress
En reverse, le runtime linking est particulièrement intéressant parce que l'API peut ne pas apparaître directement dans l'import table.

42. Exemple pratique : LoadLibrary/GetProcAddress

C++ :

Code: Select all

HMODULE h = LoadLibraryW(L"user32.dll");

auto pMessageBox =
    GetProcAddress(h, "MessageBoxW");
En reverse :

Code: Select all

lea rcx, [rip+User32String]
call LoadLibraryW

mov rcx, rax
lea rdx, [rip+MessageBoxString]
call GetProcAddress
Après `GetProcAddress` :

Code: Select all

RAX = adresse de la fonction
Puis tu peux voir :

Code: Select all

call rax
ou :

Code: Select all

call qword ptr [rbx]
43. Import Table

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
En reverse, elle permet de comprendre rapidement les capacités du programme.

44. IAT

IAT signifie :

Code: Select all

Import Address Table
Le loader remplit la IAT avec les adresses réelles des fonctions importées.

Un appel peut ressembler à :

Code: Select all

call qword ptr [rip+VirtualAlloc_IAT]
ou dans un désassembleur :

Code: Select all

call qword ptr [VirtualAlloc]
Réflexe reverse

Quand tu vois :

Code: Select all

call [adresse]
et que cette adresse appartient à la IAT, il s'agit probablement d'une API importée.

45. Export Table

Une DLL peut exposer des fonctions via sa table d'exports.

Exemple :

Code: Select all

MyLibrary.dll
    FunctionA
    FunctionB
Le loader ou `GetProcAddress` peut retrouver ces fonctions par :
  • nom ;
  • ordinal.
46. PE Directories

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.
TLS

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
Donc si quelque chose se produit très tôt, vérifie :
  • TLS callbacks ;
  • initialisateurs CRT ;
  • loader ;
  • constructeurs globaux.
48. I/O System

Le système d'I/O Windows relie :

Code: Select all

application
↓
API
↓
I/O Manager
↓
driver
↓
périphérique / filesystem / réseau
Il peut y avoir plusieurs drivers dans une stack.

Exemple :

Code: Select all

application
↓
filesystem
↓
filter driver
↓
storage driver
Pour le reverse user-mode, le plus important est de savoir que les API I/O finissent par être traitées par le kernel et les drivers.

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
);
Sous Windows x64, les quatre premiers arguments sont :

Code: Select all

RCX = lpFileName
RDX = dwDesiredAccess
R8  = dwShareMode
R9  = lpSecurityAttributes
Les autres arguments sont sur la stack.

Dans x64dbg

Breakpoint :

Code: Select all

bp CreateFileW
Quand le breakpoint se déclenche :
  • regarde RCX ;
  • suis RCX dans le dump ;
  • lis le chemin ;
  • regarde RDX ;
  • regarde R8 ;
  • regarde l'appelant.
Tu obtiens très vite le fichier ciblé et le mode d'ouverture.

50. Win32 Subsystem


Des composants comme :

Code: Select all

USER32
GDI32
win32k
participent au fonctionnement de l'interface graphique.

Pour le reverse user-mode classique :

Code: Select all

USER32.dll
GDI32.dll
sont souvent les composants les plus visibles.

Exemples :

Code: Select all

CreateWindowExW
GetMessageW
DispatchMessageW
MessageBoxW
51. Object Management USER/GDI

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
ne représentent pas forcément exactement le même mécanisme interne.

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
Il permet de gérer :
  • access violations ;
  • division par zéro ;
  • exceptions explicites ;
  • autres fautes processeur.
Un programme peut décider de :
  • gérer l'exception ;
  • continuer ;
  • propager ;
  • terminer.
53. Exceptions hardware et software

Hardware exception

Exemple :

Code: Select all

accès mémoire invalide
division par zéro
instruction invalide
Software exception

Générée explicitement par le programme ou runtime.

Exemple :

Code: Select all

RaiseException
En reverse, un comportement étrange peut être basé sur des exceptions.

54. TEB / TIB

Chaque thread possède des structures associées.

Le TEB :

Code: Select all

Thread Environment Block
contient des informations propres au thread.

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]
et :

Code: Select all

RCX = 0000000000000000
L'instruction tente donc de lire :

Code: Select all

[0]
Résultat probable :

Code: Select all

Access Violation
En x64dbg, tu regardes :
  • l'instruction fautive ;
  • le registre base ;
  • l'adresse mémoire ;
  • l'appelant ;
  • la stack.
Tu peux ainsi déterminer l'origine du crash.

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
Traduction :

Code: Select all

VirtualAlloc(
    NULL,
    0x1000,
    MEM_RESERVE | MEM_COMMIT,
    PAGE_READWRITE
);
CreateFileW

Code: Select all

lea rcx, [rip+Path]
mov edx, 80000000h
mov r8d, 1
xor r9d, r9d
call CreateFileW
Tu peux déjà lire :

Code: Select all

path = chaîne pointée par RCX
GENERIC_READ
FILE_SHARE_READ
lpSecurityAttributes = NULL
WaitForSingleObject

Code: Select all

mov rcx, rbx
mov edx, 0FFFFFFFFh
call WaitForSingleObject
Traduction :

Code: Select all

WaitForSingleObject(
    RBX,
    INFINITE
);
57. Comment utiliser les API comme points d'entrée

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
Puis :
  • 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.
Même principe avec :

Code: Select all

CreateFileW
RegOpenKeyExW
CreateProcessW
OpenProcess
ReadProcessMemory
WriteProcessMemory
CreateThread
CreateMutexW
58. Imports : première chose à regarder

Quand tu ouvres un binaire inconnu :

Étape 1

Regarde les imports.

Exemple :

Code: Select all

VirtualAlloc
WriteProcessMemory
CreateRemoteThread
OpenProcess
Même sans lire le code, tu sais déjà que le programme manipule :
  • un autre processus ;
  • de la mémoire distante ;
  • un thread distant.
Tu sais donc quelles zones analyser en priorité.

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"
Puis suis leurs références.

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.
Si tu vois une nouvelle zone :

Code: Select all

RWX
créée juste après `VirtualAlloc`, cela peut être intéressant.

Si tu vois :

Code: Select all

MEM_IMAGE
c'est probablement un module.

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
Analyse :

Code: Select all

RCX = NULL
RDX = 0x2000
R8  = 0x3000
R9  = 0x4
Donc :

Code: Select all

VirtualAlloc(
    NULL,
    0x2000,
    MEM_RESERVE | MEM_COMMIT,
    PAGE_READWRITE
)
Puis :

Code: Select all

mov rbx, rax
signifie :

Code: Select all

RBX conserve le pointeur retourné
62. Mini reverse 2 : WriteProcessMemory

Tu vois :

Code: Select all

mov rcx, rsi
mov rdx, rbx
lea r8, [rsp+40h]
mov r9d, 20h
call WriteProcessMemory
Interprétation :

Code: Select all

RCX = hProcess
RDX = adresse distante
R8  = buffer local
R9  = 0x20 octets
Donc le programme écrit :

Code: Select all

32 octets
dans un autre processus.

Tu peux ensuite :
  • inspecter le buffer local ;
  • inspecter l'adresse distante ;
  • remonter pour savoir d'où vient le handle.
63. Mini reverse 3 : CreateProcessW

Quand tu places un breakpoint sur :

Code: Select all

CreateProcessW
regarde notamment :

Code: Select all

RCX = lpApplicationName
RDX = lpCommandLine
Tu peux suivre ces pointeurs dans le dump.

Tu peux ainsi retrouver :
  • le programme lancé ;
  • la ligne de commande ;
  • les paramètres.
Pour les arguments suivants, regarde la stack.

64. Mini reverse 4 : appel via IAT

Tu vois :

Code: Select all

call qword ptr [rip+1234h]
Tu suis :

Code: Select all

rip+1234h
et tu trouves :

Code: Select all

KERNEL32!CreateFileW
Tu sais alors que l'appel indirect est en réalité :

Code: Select all

CreateFileW
Tu regardes donc les registres juste avant le `call`.

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
Tu peux reconstruire :

Code: Select all

HMODULE h = LoadLibraryW(DllName);
void* f = GetProcAddress(h, FuncName);
Puis si plus loin :

Code: Select all

call rbx
le programme appelle dynamiquement cette fonction.

66. Mini reverse 6 : condition sur un handle

C++ :

Code: Select all

HANDLE h = CreateFileW(...);

if (h == INVALID_HANDLE_VALUE)
{
    fail();
}
Assembleur possible :

Code: Select all

call CreateFileW
cmp rax, -1
je failed
Réflexe :

Code: Select all

RAX = retour API
-1 = INVALID_HANDLE_VALUE
Tu peux reconstruire la condition.

67. Mini reverse 7 : synchronisation

Tu vois :

Code: Select all

lea r8, [rip+MutexName]
xor edx, edx
xor ecx, ecx
call CreateMutexW
Tu suis `R8`.

Tu trouves :

Code: Select all

Global\MyAppMutex
Tu peux en déduire :

Code: Select all

le programme crée un mutex nommé
Puis si :

Code: Select all

GetLastError
est appelé et comparé à :

Code: Select all

ERROR_ALREADY_EXISTS
le programme vérifie probablement s'il existe déjà une instance.

68. Travail pratique dans x64dbg

Ta méthode peut être :

1. Ouvrir le programme

Regarder :
  • modules ;
  • imports ;
  • strings ;
  • memory map.
2. Choisir une API cible

Exemple :

Code: Select all

VirtualAlloc
CreateFileW
CreateProcessW
3. Poser un breakpoint

Quand l'API est appelée :
  • regarder RCX ;
  • RDX ;
  • R8 ;
  • R9 ;
  • stack pour les arguments suivants.
4. Regarder RAX après retour

Pour beaucoup d'API :

Code: Select all

RAX = valeur retournée
5. Remonter dans l'appelant

Le plus intéressant n'est pas toujours l'API.

C'est souvent :

Code: Select all

pourquoi elle est appelée
6. Renommer mentalement les variables

Exemple :

Code: Select all

RBX = allocatedBuffer
RSI = hProcess
RDI = fileHandle
Même sans symboles, tu reconstruis progressivement la logique.

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
Si ton objectif est la logique du programme, utilise souvent `Step Over`.

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
sont essentiels.

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;
}
Ce que tu ferais dans x64dbg

1. Breakpoint :

Code: Select all

CreateFileW
Tu regardes :

Code: Select all

RCX
et tu retrouves :

Code: Select all

data.bin
2. Après le retour :

Code: Select all

RAX = hFile
3. Breakpoint :

Code: Select all

VirtualAlloc
Tu regardes :

Code: Select all

RCX = NULL
RDX = 1000h
R8  = 3000h
R9  = 4
4. Après le retour :

Code: Select all

RAX = buffer
Tu suis RAX dans le dump.

5. Breakpoint :

Code: Select all

ReadFile
Arguments :

Code: Select all

RCX = hFile
RDX = buffer
R8  = 0x100
R9  = &read
6. Après le retour, tu regardes la zone `buffer`.

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
pense :

Code: Select all

nouvelle région mémoire
↓
regarder taille / flags / protection
↓
RAX = pointeur
Quand tu vois :

Code: Select all

CreateFileW
pense :

Code: Select all

RCX = chemin
↓
RAX = handle
Quand tu vois :

Code: Select all

ReadProcessMemory
WriteProcessMemory
pense :

Code: Select all

handle processus
adresse distante
buffer local
taille
Quand tu vois :

Code: Select all

call [IAT]
pense :

Code: Select all

fonction importée
Quand tu vois :

Code: Select all

LoadLibrary + GetProcAddress
pense :

Code: Select all

résolution dynamique
Quand tu vois :

Code: Select all

NtXXX
pense :

Code: Select all

Native API
↓
potentiellement syscall
Quand tu vois :

Code: Select all

syscall
pense :

Code: Select all

transition user → kernel
Quand tu vois :

Code: Select all

.text
.rdata
.data
pense :

Code: Select all

code
constantes / strings
globals
Quand tu vois :

Code: Select all

Access Violation
pense :

Code: Select all

adresse invalide / protection / pointeur
Conclusion

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.
Le bon réflexe n'est pas :

Code: Select all

"je vais lire tout le binaire"
mais plutôt :

Code: Select all

"je vais chercher les points d'entrée utiles"
Ces points d'entrée sont souvent :

Code: Select all

imports
strings
API
memory map
IAT
DLL
breakpoints
Ensuite tu remontes vers la logique.

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"

Who is online

Users browsing this forum: No registered users and 0 guests