<index> / <windows-internals> / dll-hooking
[ en | fr ]
┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
DLL Hooking & Unhooking
~ bonziski
É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.
Vue d'ensemble du DLL hooking — l'EDR intercepte chaque adresse de fonction
──[ 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.
flowchart LR
    C["Appelant"] --> F["Fonction (hookée)"]
    F -->|"JMP 0x..."| E["Code EDR"]
    E -->|"prologue original + JMP retour"| R["Reste de la fonction"]
Inline hooking — flux d'exécution redirigé vers 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 — assembleur propre (sur disque)
NtReadVirtualMemory — hookée (en mémoire) :
    E9 XX XX XX XX  JMP <fonction EDR>
NtReadVirtualMemory — hookée (JMP comme première instruction)
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)
IAT hooking — l'EDR remplace le pointeur de fonction dans la table de l'appelant
──[ 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.
Fichier PE — types de binaires Windows (.exe, .dll, .sys)
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…)
Disposition PE — vue d'ensemble des sections et headers
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
Sortie pe-sieve — fonction hookée détectée
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.
Dossier de dump pe-sieve — fichiers .tag et DLL dumpées
CFF Explorer — RVA montrant les octets patchés
Désassembleur en ligne — JMP décodé vers l'adresse de l'EDR
──[ 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 */
Structure de l'IAT — emplacements des pointeurs de fonctions dans le module
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...]
Fichiers de dump IAT pe-sieve
Hook IAT dans CFF Explorer — adresse pointant vers la DLL de l'EDR
Comparaison d'adresses — pointeurs de fonctions hookés vs normaux
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.
sequenceDiagram
    participant P as Processus
    participant I as IAT
    participant E as Hook EDR
    participant R as user32.dll réelle

    Note over I,E: Avant l'unhook
    P->>I: appel MessageBoxA
    I->>E: l'EDR intercepte
    E->>R: transmet (ou bloque)

    Note over I,R: Après l'unhook
    P->>I: appel MessageBoxA
    I->>R: appel direct — EDR contourné
MessageBoxW — hookée et censurée par l'EDR
MessageBoxA — hookée et censurée par l'EDR