Écrit par bonziski — https://github.com/Yannis-Zribi
SOMMAIRE
0 fondamentaux des dll
1 qu'est-ce que le dll hooking
2 inline hooking
3 iat hooking
4 la structure du fichier pe
5 détecter les inline hooks
6 retirer les inline hooks
7 détecter les iat hooks
8 retirer les iat hooks
──[ 0. Fondamentaux des DLL ]──
Une Dynamic Link Library (DLL) est un binaire partagé qui expose des fonctions
appelables à l'exécution par n'importe quel processus. L'équivalent Linux, c'est
un .so.
Ce que les DLLs apportent :
- Des exes plus petits — le code vit dans la DLL, pas dans chaque binaire
- Moins de RAM — une DLL chargée une fois, partagée entre tous les appelants
- Maintenance plus simple — patche la DLL, tout le monde en profite
N'importe qui peut créer et signer sa propre DLL. Windows utilise la signature
pour vérifier l'intégrité et empêcher la modif (les DLL non signées ou
trafiquées ne se chargent pas si DSE est activé).
──[ 1. Qu'est-ce que le DLL Hooking ]──
Le DLL hooking, c'est comme ça que les EDR interceptent les appels d'API Win32.
Quand une fonction hookée est appelée, l'exécution est détournée par le code de
l'EDR avant (ou à la place de) la vraie implémentation.
L'EDR corrèle l'appel avec le contexte du processus, repère les patterns
suspects, log l'événement. Deux exemples classiques :
notepad.exe écrit dans un fichier — normal, ignoré
notepad.exe lance cmd.exe — suspect, flaggé
Deux techniques principales dans la nature.
──[ 2. Inline Hooking ]──
L'EDR patche la fonction direct en mémoire. Les premiers octets sont écrasés par
un JMP vers le trampoline de l'EDR.
Sur disque la DLL est intacte (et signée — le kernel ne chargerait pas une DLL
signée modifiée). Le hook n'existe qu'en mémoire.
NtReadVirtualMemory — prologue propre (sur disque) :
4C 8B D1 MOV R10, RCX
B8 3F 00 00 00 MOV EAX, 0x3F
...
NtReadVirtualMemory — hookée (en mémoire) :
E9 XX XX XX XX JMP <fonction EDR>
Chaque appel à NtReadVirtualMemory saute inconditionnellement dans l'EDR.
──[ 3. IAT Hooking ]──
L'Import Address Table (IAT) est une table de pointeurs de fonctions propre à
chaque module. Quand ton exe appelle MessageBoxA, il lit le pointeur dans son
IAT et saute là-bas. L'IAT est remplie par le loader au démarrage.
L'IAT hooking remplace un pointeur de cette table par l'adresse d'une fonction
de l'EDR. Seul le module dont l'IAT est patchée est affecté ; les autres
processus non (contrairement à l'inline hooking, qui patche la DLL partagée en
mémoire).
Avant le hook :
IAT[MessageBoxA] → user32!MessageBoxA (0x7FF812345678)
Après le hook IAT :
IAT[MessageBoxA] → edr.dll!HookMsgBox (0x7FF900001234)
──[ 4. La Structure du Fichier PE ]──
Détection et unhooking demandent tous les deux le format PE (Portable
Executable), utilisé pour les .exe, .dll, .sys et compagnie.
DOS Header — historique ; contient e_lfanew (offset des NT Headers)
DOS Stub — affiche "This program cannot be run in DOS mode" sur les vieux
OS
NT Headers — signature + FileHeader + OptionalHeader
Section Headers — tableau décrivant chaque section (.text, .rdata, .idata…)
Localiser l'Import Directory :
NTHeaders → OptionalHeader → DataDirectory[1] → VirtualAddress
La section .idata contient l'Import Descriptor Table : une entrée par DLL
importée, avec deux tableaux parallèles — l'ILT (noms avant résolution) et l'IAT
(adresses après résolution).
──[ 5. Détecter les Inline Hooks ]──
Compare les N premiers octets d'une fonction entre sa copie en mémoire et sa
copie sur disque. Différence = patché.
// Lire N octets depuis la mémoire
HMODULE h = GetModuleHandleA("ntdll.dll");
FARPROC f = GetProcAddress(h, "NtReadVirtualMemory");
memcpy(memBuf, (BYTE*)f, N);
// Lire N octets depuis le fichier sur disque
// (localiser la RVA de la fonction via l'export directory,
// convertir en file offset, puis ReadFile)
ReadFile(hFile, diskBuf, N, &read, NULL);
// Comparer
for (SIZE_T i = 0; i < N; i++) {
if (memBuf[i] != diskBuf[i]) { /* HOOKÉE */ }
}
La copie disque fait foi — une DLL signée ne peut pas être modifiée sans casser
sa signature.
Détection automatisée avec pe-sieve (https://github.com/hasherezade/pe-sieve) :
pe-sieve64.exe /pid <PID>
Extrait de sortie :
Hooked: 1
...
8cea0;MessageBoxA->7ff8ed4a0178;5
Le fichier .tag donne le nom de la fonction et la cible du JMP. Ouvre la DLL
dumpée dans CFF Explorer, va à la RVA, décode les octets pour confirmer le JMP.
──[ 6. Retirer les Inline Hooks ]──
Une fois un hook repéré, remets les octets d'origine depuis la version disque :
// Comparer, et si différent, restaurer depuis la copie disque
if (memcmp(memBuf, diskBuf, N) != 0) {
WriteProcessMemory(
GetCurrentProcess(),
(LPVOID)f,
diskBuf,
N,
NULL
);
}
WriteProcessMemory gère les permissions de page en interne sur les Windows
modernes. Si ça échoue en ACCESS_DENIED, utilise VirtualProtect pour rendre la
page writable, écris, puis remets la protection d'origine.
──[ 7. Détecter les IAT Hooks ]──
Parcours l'IAT de chaque module chargé, compare l'adresse de fonction
enregistrée avec celle attendue (résolue depuis la DLL canonique), signale les
écarts.
1. Snapshot de tous les modules chargés (CreateToolhelp32Snapshot).
2. Pour chaque module, parser DOS → NT → Optional → Import Descriptor.
3. Pour chaque DLL importée dans le descripteur :
a. Parcourir l'ILT pour récupérer les noms de fonctions.
b. Lire l'entrée IAT correspondante (adresse runtime).
c. Appeler GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS)
pour savoir quelle DLL possède cette adresse.
d. Si la DLL propriétaire diffère de la DLL d'import attendue (et
que ce n'est pas un forwarder légitime), hook IAT.
HMODULE provider = NULL;
GetModuleHandleExA(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR)cur, &provider);
char provName[MAX_PATH];
GetModuleFileNameA(provider, provName, MAX_PATH);
if (_stricmp(BaseName(provName), expectedDllName) != 0)
/* HOOKÉE */
Whiteliste les forwarders légitimes (kernelbase.dll, DLL virtuelles api-ms-*,
etc.) pour éviter les faux positifs.
Détection automatisée :
pe-sieve64.exe /pid <PID> /iat 3
Sortie :
IAT Hooks: 1
20150;[user32.MessageBoxA #657]->7ff91330114f[...dllhookiat...]
DLL propriétaire "dllhookiat" — pas user32.dll. Hook IAT confirmé.
──[ 8. Retirer les IAT Hooks ]──
Remets l'entrée IAT à la vraie adresse de la fonction :
// Résoudre la vraie adresse
HMODULE hReal = GetModuleHandleA("user32.dll");
FARPROC expected = GetProcAddress(hReal, "MessageBoxA");
// L'adresse de l'entrée IAT (pointeur vers le pointeur de fonction)
void *iatEntry = &ftTable[i].u1.Function;
DWORD old;
// Rendre temporairement la page IAT inscriptible
VirtualProtect(iatEntry, sizeof(uintptr_t), PAGE_READWRITE, &old);
// Patcher
ftTable[i].u1.Function = (uintptr_t)expected;
// Restaurer la protection
VirtualProtect(iatEntry, sizeof(uintptr_t), old, &old);
// Vider le cache d'instructions
FlushInstructionCache(GetCurrentProcess(), iatEntry, sizeof(uintptr_t));
Après ça, les appels à MessageBoxA depuis ce module vont direct dans user32.dll.
L'EDR n'est plus dans le chemin pour cette entrée IAT.