Leçon 7.6 · Comportements malveillants· 45 min
Hooking et rootkits en mode utilisateur
Repérer les hooks inline, IAT et EAT en comparant mémoire et disque, distinguer produits de sécurité et rootkits, prouver la dissimulation par vue croisée.
Objectifs
- Expliquer pourquoi le hooking est à double usage et ce qu’un rootkit en mode utilisateur cache, et à quels observateurs
- Détecter les modifications inline, de l’IAT et de la table des exports en comparant un module en mémoire avec son fichier sur disque
- Lire un prologue de fonction modifié et classer un hook selon l’endroit où aboutit son saut, l’auteur de la signature de ce code et sa comparaison avec une référence saine
- Utiliser la détection par vue croisée, des API en direct face aux images mémoire jusqu’à l’analyse hors ligne du disque, pour prouver que quelque chose est caché
- Collecter la mémoire et le disque avant la remédiation et décrire un hook dans des termes qu’un autre analyste peut vérifier
Tracer les appels d’API et les appels système a mis
les hooks à votre service : API Monitor et Frida patchent des fonctions à
l’intérieur d’un processus pour que vous puissiez observer chaque appel. Cette
leçon retourne le même mécanisme. Quand un malware (logiciel malveillant)
patche des fonctions, le processus se met à mentir — au Gestionnaire des
tâches, à dir, à netstat et parfois à vos propres outils — et votre travail
consiste à le prouver.
L’angle est strictement défensif : à quoi ressemble un hook en mémoire, comment les scanners le trouvent, à qui il appartient et ce qu’il cachait. La méthode s’appuie sur Dumper la mémoire et extraire les charges utiles, qui a reproduit la disposition d’un PE en mémoire, et sur Imports, exports et IAT, qui a décrit les tables que visent les hooks.
Le hooking est à double usage
Un hook redirige un appel pour qu’un autre code s’exécute avant, à la place ou après. L’entrée du glossaire sur le hooking d’API en détaille le fonctionnement ; ce qui compte ici, c’est le nombre de programmes légitimes qui le pratiquent.
| Qui pose des hooks | Ce que l’on trouve généralement |
|---|---|
| EDR et antivirus | Des sauts au début de fonctions de ntdll menant vers une DLL signée de l’éditeur |
| Traceurs (Instrumentation binaire dynamique) | L’agent de Frida ou la DLL d’API Monitor, avec des trampolines en mémoire privée |
| Débogueurs | Points d’arrêt logiciels : un octet int3 (CC) sur une instruction |
| Les applications elles-mêmes | Des navigateurs qui hookent leurs propres modules ; le moteur de shims de Windows qui patche un ancien programme |
| Malwares | Des sauts vers du code non signé ou non adossé à un fichier depuis des API d’énumération, de réseau ou de télémétrie |
La FAQ de pe-sieve est sans détour sur ce point : l’outil détecte les hooks mais ne peut pas dire s’ils sont malveillants, et il sera bruyant sur un hôte équipé d’un antivirus. Un hook est une observation. La classification vient ensuite.
Ce que cache un rootkit en mode utilisateur
Un rootkit en mode utilisateur (ATT&CK T1014) charge du code dans les processus
qui observent — l’Explorateur, le Gestionnaire des tâches, cmd.exe,
PowerShell, les services — et filtre ce que renvoient leurs appels
d’énumération. Il ne modifie rien dans le noyau. Tout ce qu’il cache est
toujours là ; seules les réponses fournies aux processus hookés changent.
| Artefact caché | Énumération filtrée par le rootkit | Qui n’est pas dupé |
|---|---|---|
| Processus | NtQuerySystemInformation et les API d’instantané construites au-dessus | Les structures de processus du noyau dans une image mémoire ; les événements de démarrage de processus déjà présents dans le SIEM |
| Fichiers et dossiers | NtQueryDirectoryFile et ses appelants | La MFT lue depuis une image disque hors ligne |
| Clés et valeurs de registre | NtEnumerateKey, NtEnumerateValueKey | Les fichiers de ruche analysés hors ligne ; les ruches dans une image mémoire |
| Connexions réseau | GetExtendedTcpTable et les requêtes derrière netstat | Les objets réseau en mémoire ; les pare-feu, proxys et sondes réseau |
Trois conséquences en découlent. Les hooks sont propres à chaque processus : le rootkit doit être chargé dans chaque processus qu’il veut duper, souvent par un mécanisme d’injection ou de chargement automatique comme les AppInit DLLs ou une DLL SetWindowsHookEx. Un processus qu’il a manqué dit la vérité, si bien qu’un désaccord entre processus constitue une preuve. Et tout ce qui se trouve hors de l’hôte, ou sous le mode utilisateur, n’a jamais été dupé.
Toute dissimulation n’exige pas de hooks : la dissimulation de clés de registre par octet nul exploite une différence de nommage entre les API Win32 et les API natives, et les mêmes vérifications par vue croisée la repèrent. Les hooks sur les fonctions d’envoi d’un navigateur servent plutôt au vol (voir Vol d’identifiants et keylogging). Les rootkits en mode noyau sortent du cadre de cette leçon ; la future leçon Mode utilisateur, mode noyau et API native (module 5) explique cette frontière.
Où vivent les hooks
Chaque type de hook modifie une structure différente, et chacun dispose d’une référence saine connue à laquelle le comparer.
| Type de hook | Ce qui change en mémoire | Référence saine connue |
|---|---|---|
| Inline | Les premiers octets d’une fonction sont remplacés par un saut ; les instructions déplacées sont copiées dans un trampoline ailleurs | Les mêmes octets dans le fichier sur disque, après mappage et rebasage |
| IAT | Une entrée de l’IAT du module importateur contient une adresse autre que celle de l’export qu’elle nomme | L’adresse d’export de ce nom dans la DLL cible chargée |
| EAT | Une entrée de la table des adresses d’export d’une DLL pointe ailleurs, si bien que les appels ultérieurs à GetProcAddress obtiennent la mauvaise adresse | La table des exports du fichier sur disque |
| Hooks de messages Windows | Rien n’est patché ; une DLL est chargée dans les processus graphiques par SetWindowsHookEx | La liste des modules chargés dans chaque processus |
Les hooks inline dominent aussi bien dans les produits de sécurité que dans les malwares, car ils interceptent chaque appelant, quelle que soit la façon dont il a obtenu l’adresse ; les hooks d’IAT ratent le code qui résout les API à la main. La destruction de l’IAT est encore autre chose : un endommagement de la table, pas une redirection.
Comparer la mémoire avec le disque
Les scanners trouvent les hooks avec la méthode que vous avez utilisée dans le lab de dump mémoire, appliquée à l’envers : prendre le fichier du module sur disque, le disposer comme le ferait le loader, appliquer les mêmes relocations pour la base qu’a réellement le module, puis comparer. Ce qui devrait être identique et ne l’est pas relève d’une modification.
- Les sections de code sont comparées octet par octet. Après rebasage, les sections exécutables d’un module sain correspondent exactement au fichier.
- L’IAT ne peut pas être comparée au fichier, car c’est le loader qui la
remplit. On vérifie plutôt chaque entrée par rapport à l’export que son nom
désigne dans la DLL cible. Une entrée qui pointe ailleurs est un hook ; c’est
ce que fait le mode
/iatde pe-sieve. - La table des exports est comparée au fichier, et chaque RVA exportée doit tomber dans le code du module lui-même (ou être une chaîne de redirection, forwarder).
Excluez d’abord les différences attendues : pointeurs relocalisés, entrées de
l’IAT, données modifiables et champ ImageBase en mémoire. Puis méfiez-vous de
trois pièges. Un fichier mis à jour sur disque après le chargement du module
fait différer toutes les fonctions. Un fichier altéré concorde avec une copie
patchée : vérifiez donc sa signature ou comparez-le avec la même version
provenant d’un hôte sain. Enfin, le code peut être totalement absent du disque —
chargement réflectif ou
module stomping — ce qui relève de la
détection d’injection plutôt que de la détection de hooks ; la future leçon
Comprendre l’injection de processus traite ce sujet.
Lire un prologue modifié
Les fonctions compilées commencent de façon prévisible : ajustement de la pile,
sauvegarde de registres, ou test et branchement précoces. Sur x64, les stubs
d’appel système de ntdll commencent par mov r10, rcx suivi de
mov eax, <number>. Quand un désassembleur affiche autre chose au premier octet
d’une fonction, cherchez ces formes (les encodages sont détaillés dans
l’encodage des instructions x86) :
| Premiers octets | Instruction | Portée |
|---|---|---|
E9 xx xx xx xx | jmp rel32 | ±2 Go autour du site du patch, si bien que la cible est souvent un trampoline proche |
FF 25 00 00 00 00 + adresse de 8 octets | jmp qword ptr [rip] | N’importe où ; l’adresse de destination suit l’instruction |
48 B8 imm64, FF E0 | mov rax, imm64 ; jmp rax | N’importe où ; 12 octets écrasés |
CC | int3 | Un point d’arrêt de débogueur, ou du code qui s’attend à intercepter l’exception |
Deux indices confirment la modification même sans le fichier. Les octets qui
suivent le saut se décodent souvent en non-sens, parce que le saut a coupé une
instruction en deux : dans le lab ci-dessous, mov r9, rcx devient
mov ecx, ecx. Et la destination du saut se trouve généralement hors de la
fonction, souvent hors du module.
Astuce : Sur Windows 32 bits, de nombreuses fonctions système commencent par
mov edi, edi, précédé de cinq octets de remplissage — un point de hot-patch conçu pour être remplacé par un saut court. Un saut à cet endroit est normal pour les outils de correctif et les produits de sécurité, et tout aussi pratique pour un malware. Classez-le d’après sa destination, comme n’importe quel autre.
Légitime ou malveillant
Suivez le saut, puis répondez à ces questions dans l’ordre :
| Question | Oriente vers un produit de sécurité ou un outil bénin | Oriente vers un malware |
|---|---|---|
| Qu’est-ce qui adosse la destination ? | Un module sur disque, dans Program Files ou System32 | De la mémoire privée ou non adossée à un fichier ; un module dans %TEMP% ou dans un profil utilisateur ; un module dont le fichier a disparu |
| Qui l’a signé ? | Une signature valide d’un éditeur que vous avez déployé | Non signé, autosigné, ou une signature qui ne correspond pas au fichier |
| Quelles fonctions sont hookées ? | Les API de mémoire, de processus et de threads (allocation, changements de protection, threads distants) | Les API d’énumération (processus, répertoires, registre, connexions) ; les fonctions d’envoi et de chiffrement des navigateurs |
| Une référence saine concorde-t-elle ? | Les mêmes hooks, vers le même module, sur chaque hôte doté de ce produit | Des hooks sur un seul hôte, ou dans certains processus seulement |
La référence tranche la plupart des cas : scannez un hôte réputé sain doté des
mêmes logiciels et conservez le résultat. Le mode /iat 1 de pe-sieve applique
un filtre similaire en écartant les hooks d’IAT qui mènent vers des modules
système non modifiés, et sa FAQ cite comme inoffensifs les hooks de Firefox
menant vers son propre mozglue.dll.
Détection par vue croisée
On ne peut pas forcer un processus hooké à dire la vérité, mais on peut poser la même question à une vue que le rootkit ne contrôle pas. La détection par vue croisée (cross-view detection) compare une réponse de haut niveau à une réponse de plus bas niveau ; tout ce qui est présent en bas et absent en haut est caché.
| Artefact | Vue de haut niveau (filtrable) | Vue de plus bas niveau | Outils |
|---|---|---|---|
| Processus | Gestionnaire des tâches, tasklist, instantané de la console EDR pris depuis un processus hooké | Image mémoire : liste chaînée des processus, scan des pools, tables de threads et de handles | Volatility windows.pslist, windows.psscan, windows.malware.psxview |
| Modules chargés | Liste des modules issue du PEB (ToolHelp, la plupart des outils) | Mappages mémoire (VAD) dans le même processus | windows.malware.ldrmodules |
| Fichiers | Explorateur, dir, scripts de collecte en direct | Image disque hors ligne : la MFT et les index de répertoires | Montage en lecture seule sur un hôte d’analyse ; analyseurs de MFT |
| Registre | regedit, reg query, Autoruns sur l’hôte en fonctionnement | Fichiers de ruche issus de l’image disque ; ruches en mémoire | Analyseurs de ruches hors ligne ; Volatility windows.registry.* |
| Connexions | netstat, Moniteur de ressources | Objets réseau en mémoire ; trafic observé hors de l’hôte | windows.netscan, journaux de pare-feu et de proxy, sondes réseau |
Prenez la vue de bas niveau depuis un autre point d’observation — une image mémoire ou disque analysée sur une autre machine — car un second outil exécuté sur l’hôte infecté peut lui aussi être hooké. Attendez-vous aussi à des désaccords bénins : des processus se terminent entre deux collectes, et le scan des pools retrouve des processus terminés dont les structures subsistent. Une différence est une piste, pas un verdict.
Outils
| Outil | Portée | Ce qu’il signale en matière de hooks et de modifications |
|---|---|---|
| pe-sieve | Un processus | Hooks inline et patches par défaut, dans un fichier .tag par module (RVA;function->destination[details];size) ; hooks d’IAT avec /iat ; dumps du module patché |
| hollows_hunter | De nombreux processus | Le moteur de pe-sieve sur tout le système ; le scan des hooks doit être activé avec /hooks |
| Moneta | Régions mémoire | Régions d’image dont les pages de code ont été modifiées, ainsi que la mémoire privée exécutable et les threads non adossés à un fichier (-m ioc -p <pid>) |
| Volatility 3 | Une image mémoire | windows.malware.unhooked_system_calls compare les stubs de ntdll d’un processus à l’autre ; windows.etwpatch vérifie la présence de patches ret et jmp dans les fonctions ETW ; les plugins de vue croisée du tableau ci-dessus |
| Analyse de disque hors ligne | Une image disque | Fichiers, ruches et entrées de persistance, sans interroger le système infecté |
Volatility 3 a déplacé ses plugins de détection de malwares sous
windows.malware.* ; les noms courts utilisés dans la leçon sur le dump mémoire
sont des alias dépréciés dans la version 2.28. PE-bear charge un fichier .tag
de pe-sieve placé à côté du module dumpé et marque chaque offset patché.
Collecter avant de remédier
Les hooks n’existent qu’en mémoire. Redémarrer, tuer le processus ou laisser un antivirus nettoyer l’hôte détruit la seule preuve, et les réponses obtenues en direct sur cet hôte peuvent être filtrées. Collectez par ordre de volatilité :
- Une image mémoire complète de l’hôte, avant que quoi que ce soit d’autre ne s’y exécute.
- La sortie des scanners pour les processus suspects (dumps et fichiers
.tagde pe-sieve ou de hollows_hunter) — ils lisent la mémoire et modifient peu de choses. - Une image disque ou une collecte ciblée hors ligne du système de fichiers et des ruches.
- Les empreintes de chaque artefact, avec l’outil, la version et l’heure.
Notez lesquelles de vos commandes en direct ont été exécutées sur l’hôte
infecté. Si netstat n’a rien montré et que la mémoire révèle une connexion, cet
écart constitue en soi une constatation.
Que consigner dans le rapport
Une constatation de hook doit permettre à un autre analyste de la reproduire à partir de vos artefacts :
| Champ | Exemple |
|---|---|
| Emplacement | Hôte, nom du processus, PID, chemin de l’image |
| Cible | Module, fonction, RVA, type de hook (inline, IAT, EAT) |
| Preuve | Octets sur disque et en mémoire ; désassemblage du prologue patché |
| Destination | Adresse, type de région (image, privée, mappée), chemin du fichier sous-jacent, état de la signature |
| Classification | Produit de sécurité, bénin ou malveillant — avec la justification (référence saine, signataire, ensemble de fonctions) |
| Effet | Ce que la vue croisée a montré comme caché : quel processus, fichier, clé ou connexion |
| Artefacts et indicateurs | Image mémoire, dumps des scanners, fichiers .tag, image disque avec SHA-256 ; chemin et empreinte du module de hooking et de ce qui le charge |
Mettez en avant le mécanisme de chargement : la remédiation doit le supprimer, sinon le rootkit revient à l’ouverture de session suivante (voir Mécanismes de persistance).
Lab : trouver un prologue patché en comparant la mémoire avec le disque
Ce lab reproduit ce que fait pe-sieve, sur une DLL inoffensive et une image
mémoire simulée ; vous n’avez besoin d’un hôte Windows qu’à la dernière étape.
Il utilise un environnement virtuel avec pefile et capstone, ainsi que
mingw-w64. Les sorties proviennent de GCC 15.2.0 (mingw-w64), Python 3.14,
pefile 2024.8.26 et capstone 5.0.9 sous macOS.
-
Compilez une DLL qui exporte deux petites fonctions :
c /* hooklab.c: harmless DLL with two exported functions, used as a hook-scanning target */ #include <windows.h> /* Adler-32-style checksum over a buffer */ __declspec(dllexport) unsigned lab_checksum(const unsigned char *p, unsigned n) { unsigned a = 1, b = 0; for (unsigned i = 0; i < n; i++) { a = (a + p[i]) % 65521; b = (b + a) % 65521; } return (b << 16) | a; } /* count the ASCII letters in a NUL-terminated string */ __declspec(dllexport) int lab_count_letters(const char *s) { int n = 0; for (; *s; s++) if ((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z')) n++; return n; } BOOL WINAPI DllMain(HINSTANCE h, DWORD reason, LPVOID r) { (void)h; (void)reason; (void)r; return TRUE; }bash python3 -m venv venv && ./venv/bin/pip install pefile capstone x86_64-w64-mingw32-gcc -O1 -shared -s -o hooklab.dll hooklab.c shasum -a 256 hooklab.dlltext caad00014b67513d3f5d3418c01f3a9af1d895877999690b64189ad65bcb71d8 hooklab.dllVotre empreinte sera différente : l’éditeur de liens écrit un horodatage dans l’en-tête PE et dans le répertoire des exports, si bien que même une recompilation sur la même machine la modifie. Le code, lui, ne change pas, et les exports sont
lab_checksumà la RVA0x1360etlab_count_lettersà0x13cd. -
Enregistrez la simulation du loader sous
mapimage.py. C’est la fonctionmap_imagedu lab de dump mémoire, sans le décodage de la configuration : en-têtes et sections copiés à leurs RVA, relocations DIR64 appliquées pour une nouvelle base, et cette base écrite dans l’en-tête.python # mapimage.py: lay a PE out as the loader would (shared by both scripts) import struct import pefile def align(x, a): return (x + a - 1) // a * a def map_image(data, new_base): """Copy headers and sections to their RVAs, then rebase to new_base.""" pe = pefile.PE(data=data) oh = pe.OPTIONAL_HEADER mem = bytearray(oh.SizeOfImage) mem[:oh.SizeOfHeaders] = data[:oh.SizeOfHeaders] for s in pe.sections: n = min(s.SizeOfRawData, align(s.Misc_VirtualSize, oh.SectionAlignment)) mem[s.VirtualAddress:s.VirtualAddress + n] = data[s.PointerToRawData:s.PointerToRawData + n] delta = new_base - oh.ImageBase for block in getattr(pe, "DIRECTORY_ENTRY_BASERELOC", []): for e in block.entries: if e.type == pefile.RELOCATION_TYPE["IMAGE_REL_BASED_DIR64"]: v, = struct.unpack_from("<Q", mem, e.rva) struct.pack_into("<Q", mem, e.rva, v + delta) struct.pack_into("<Q", mem, oh.get_field_absolute_offset("ImageBase"), new_base) return mem -
Produisez les preuves : une image mémoire saine, et une copie dans laquelle le début de
lab_checksuma été écrasé par unjmprelatif vers un espace inutilisé à la fin de.text, où trois octets (xor eax, eax ; ret) tiennent lieu de gestionnaire. C’est ce qu’un scanner lirait dans un processus modifié, obtenu ici en éditant des octets dans un fichier, sans rien hooker.python # mkimage.py: produce a clean "memory image" and a tampered copy of hooklab.dll import struct import pefile from mapimage import map_image BASE = 0x7FFB2C410000 disk = open("hooklab.dll", "rb").read() pe = pefile.PE(data=disk) exports = {e.name.decode(): e.address for e in pe.DIRECTORY_ENTRY_EXPORT.symbols} clean = map_image(disk, BASE) open("hooklab.mem.bin", "wb").write(clean) # Simulate what a scanner finds in a tampered process: a 5-byte relative jmp # at the start of lab_checksum, to a stub in unused space at the end of .text. tampered = bytearray(clean) src = exports["lab_checksum"] dst = 0x2800 # .text ends at 0x23b0; page slack tampered[dst:dst + 3] = bytes([0x31, 0xC0, 0xC3]) # xor eax, eax ; ret tampered[src:src + 5] = b"\xE9" + struct.pack("<i", dst - (src + 5)) open("hooklab.tampered.bin", "wb").write(tampered) print(f"base {BASE:#x}, SizeOfImage {len(clean):#x}") print(f"lab_checksum RVA {src:#x} -> jmp to RVA {dst:#x}")text base 0x7ffb2c410000, SizeOfImage 0xc000 lab_checksum RVA 0x1360 -> jmp to RVA 0x2800cmp -l hooklab.mem.bin hooklab.tampered.binconfirme huit octets modifiés : cinq à partir de l’octet 4961 et trois à partir de l’octet 10241.cmpcompte à partir de 1 en décimal ; il s’agit donc des RVA0x1360et0x2800. -
Écrivez le détecteur sous
hookscan.py. Il lit la base dans l’en-tête en mémoire, mappe et rebase le fichier pour qu’il corresponde, compare chaque section exécutable sur toute sa taille mappée, regroupe les différences en plages, nomme chaque plage par export et offset, et désassemble les deux versions. Si le code modifié commence par unjmpou uncall, il en suit la cible. Enfin, il vérifie chaque export et la table des adresses d’export.python # hookscan.py: compare a module's in-memory image with its file on disk import struct, sys import pefile from capstone import Cs, CS_ARCH_X86, CS_MODE_64 from mapimage import map_image, align EXEC = 0x20000000 # IMAGE_SCN_MEM_EXECUTE disk = open(sys.argv[1], "rb").read() mem = open(sys.argv[2], "rb").read() pe = pefile.PE(data=disk) oh = pe.OPTIONAL_HEADER base, = struct.unpack_from("<Q", mem, oh.get_field_absolute_offset("ImageBase")) ref = map_image(disk, base) # the file, laid out and rebased like memory md = Cs(CS_ARCH_X86, CS_MODE_64) exports = sorted((e.address, e.name.decode()) for e in pe.DIRECTORY_ENTRY_EXPORT.symbols) def sname(s): return s.Name.rstrip(b"\0").decode() def section_of(rva): for s in pe.sections: if s.VirtualAddress <= rva < s.VirtualAddress + align(s.Misc_VirtualSize, oh.SectionAlignment): return s return None def describe(rva): """Name an RVA the way a report would: export+offset, or section and slack.""" s = section_of(rva) if s is None: return "outside the image" if rva >= s.VirtualAddress + s.Misc_VirtualSize: return f"{sname(s)}+{rva - s.VirtualAddress:#x} (past VirtualSize {s.Misc_VirtualSize:#x}: slack)" owner = [(a, n) for a, n in exports if a <= rva and section_of(a) is s] if owner: a, n = owner[-1] return f"{n}+{rva - a:#x}" return f"{sname(s)}+{rva - s.VirtualAddress:#x}" def disasm(buf, rva, count=3): return [(i.address, i.bytes.hex(" "), f"{i.mnemonic} {i.op_str}".strip()) for i in md.disasm(bytes(buf[rva:rva + 16]), base + rva, count)] print(f"module base {base:#x}, reference mapped from {sys.argv[1]}") diff_rvas = [] for s in pe.sections: if not s.Characteristics & EXEC: continue lo, hi = s.VirtualAddress, s.VirtualAddress + align(s.Misc_VirtualSize, oh.SectionAlignment) d = [r for r in range(lo, hi) if mem[r] != ref[r]] print(f"section {sname(s)} [{lo:#x}-{hi:#x}): {len(d)} bytes differ") diff_rvas += d runs = [] # group adjacent differences for r in diff_rvas: if runs and r - runs[-1][1] <= 8: runs[-1][1] = r else: runs.append([r, r]) for lo, hi in runs: print(f"\n[modified] RVA {lo:#x}, {hi - lo + 1} bytes, at {describe(lo)}") print(f" on disk : {ref[lo:hi + 1].hex(' ')}") print(f" in memory: {mem[lo:hi + 1].hex(' ')}") print(" disk code:") for a, b, t in disasm(ref, lo): print(f" {a:#x} {b:<22} {t}") print(" mem code :") for a, b, t in disasm(mem, lo): print(f" {a:#x} {b:<22} {t}") first = next(md.disasm(bytes(mem[lo:lo + 16]), base + lo, 1), None) if first and first.mnemonic in ("jmp", "call") and first.op_str.startswith("0x"): target = int(first.op_str, 16) trva = target - base where = describe(trva) if 0 <= trva < len(mem) else "outside the image" print(f" => {first.mnemonic} to {target:#x} (RVA {trva:#x}): {where}") print("\nexports:") edir = pe.DIRECTORY_ENTRY_EXPORT.struct for a, n in exports: touched = any(a <= lo < a + 16 for lo, _ in runs) print(f" {n:<18} RVA {a:#x} {'MODIFIED' if touched else 'clean'}") eat_disk = ref[edir.AddressOfFunctions:edir.AddressOfFunctions + 4 * edir.NumberOfFunctions] eat_mem = mem[edir.AddressOfFunctions:edir.AddressOfFunctions + 4 * edir.NumberOfFunctions] print(f"export address table: {'identical' if eat_disk == eat_mem else 'DIFFERS'} " f"({edir.NumberOfFunctions} entries)") -
Scannez d’abord l’image saine. Un détecteur qui signale un module non modifié est inutile ; ceci est donc le témoin :
bash ./venv/bin/python hookscan.py hooklab.dll hooklab.mem.bintext module base 0x7ffb2c410000, reference mapped from hooklab.dll section .text [0x1000-0x3000): 0 bytes differ exports: lab_checksum RVA 0x1360 clean lab_count_letters RVA 0x13cd clean export address table: identical (2 entries) -
Scannez l’image modifiée :
bash ./venv/bin/python hookscan.py hooklab.dll hooklab.tampered.bintext module base 0x7ffb2c410000, reference mapped from hooklab.dll section .text [0x1000-0x3000): 8 bytes differ [modified] RVA 0x1360, 5 bytes, at lab_checksum+0x0 on disk : 85 d2 74 62 49 in memory: e9 9b 14 00 00 disk code: 0x7ffb2c411360 85 d2 test edx, edx 0x7ffb2c411362 74 62 je 0x7ffb2c4113c6 0x7ffb2c411364 49 89 c9 mov r9, rcx mem code : 0x7ffb2c411360 e9 9b 14 00 00 jmp 0x7ffb2c412800 0x7ffb2c411365 89 c9 mov ecx, ecx 0x7ffb2c411367 89 d2 mov edx, edx => jmp to 0x7ffb2c412800 (RVA 0x2800): .text+0x1800 (past VirtualSize 0x13b0: slack) [modified] RVA 0x2800, 3 bytes, at .text+0x1800 (past VirtualSize 0x13b0: slack) on disk : 00 00 00 in memory: 31 c0 c3 disk code: 0x7ffb2c412800 00 00 add byte ptr [rax], al 0x7ffb2c412802 00 00 add byte ptr [rax], al 0x7ffb2c412804 00 00 add byte ptr [rax], al mem code : 0x7ffb2c412800 31 c0 xor eax, eax 0x7ffb2c412802 c3 ret 0x7ffb2c412803 00 00 add byte ptr [rax], al exports: lab_checksum RVA 0x1360 MODIFIED lab_count_letters RVA 0x13cd clean export address table: identical (2 entries)Lisez-la comme un rapport. Le patch se trouve à l’offset
0x0delab_checksum: un hook de prologue. Les cinq octets coupentmov r9, rcxen deux, si bien que ses deux derniers octets se décodent désormais enmov ecx, ecx: l’indice de l’instruction tronquée. Le déplacement0x149best relatif à l’instruction suivante (0x1365 + 0x149b = 0x2800). La destination se trouve dans l’image mais au-delà de laVirtualSizede.text, dans l’espace libre de fin de page qu’aucune fonction compilée n’occupe, et son code fait renvoyer 0 à la fonction quelle que soit l’entrée : un filtre qui répond « rien ».lab_count_letterset la table des exports sont sains. Un vrai gestionnaire se trouve généralement hors du module, là où s’applique le tableau de classification. -
Constatez pourquoi le rebasage compte. Mappez plutôt le fichier à sa base préférée et comparez chaque section, pas seulement le code, avec l’image saine :
python # norebase.py: what a naive whole-image comparison sees without rebasing import pefile from mapimage import map_image disk = open("hooklab.dll", "rb").read() mem = open("hooklab.mem.bin", "rb").read() pe = pefile.PE(data=disk) naive = map_image(disk, pe.OPTIONAL_HEADER.ImageBase) # preferred base, no rebase for s in pe.sections: lo, hi = s.VirtualAddress, s.VirtualAddress + s.Misc_VirtualSize d = sum(1 for r in range(lo, hi) if naive[r] != mem[r]) if d: print(f"{s.Name.rstrip(b'\0').decode():7} {d:3} bytes differ") print("headers", sum(1 for r in range(0x400) if naive[r] != mem[r]), "bytes differ")text .data 8 bytes differ .rdata 88 bytes differ headers 4 bytes differLes pointeurs relocalisés et le champ
ImageBasediffèrent, et une comparaison naïve de l’image entière les signalerait tous..textn’en contient aucun : le code x64 adresse les données de façon relative à RIP. Le code d’une DLL 32 bits est rempli d’adresses absolues ; y omettre le rebasage noierait donc aussi la comparaison du code. -
Dans votre VM Windows, rencontrez des hooks que vous n’avez pas écrits :
- Ouvrez le Bloc-notes dans x64dbg, placez un point d’arrêt logiciel sur
kernelbase.CreateFileWet, sans reprendre l’exécution, lancezpe-sieve64.exe /pid <pid>depuis une seconde invite. Cherchez un patch danskernelbase.dllet lisez son fichier.tag: leint3d’un débogueur est une modification au sens de cette définition. Placez ensuite un point d’arrêt matériel à la place et comparez. - Lancez
hollows_hunter64.exe /hooksavec l’antivirus de la VM activé. Pour chaque module hooké, ouvrez le fichier.tag, suivez la destination et remplissez le tableau de classification ci-dessus. - Suspendez la VM, puis exécutez
windows.pslist,windows.psscanetwindows.malware.psxviewsur son fichier mémoire. Expliquez chaque processus qui n’apparaît pas dans toutes les vues.
- Ouvrez le Bloc-notes dans x64dbg, placez un point d’arrêt logiciel sur
Questions : Pourquoi le détecteur compare-t-il .text sur toute sa taille
mappée plutôt que sur sa VirtualSize, et qu’aurait-il manqué sinon ? Si le
saut de l’image modifiée avait mené vers une DLL signée dans Program Files,
que vérifieriez-vous avant de la déclarer bénigne ? Un rootkit en mode
utilisateur cache un processus au Gestionnaire des tâches : quelles sont les deux
sources de vue croisée ci-dessus qui le montreraient encore, et pourquoi ? Pourquoi une empreinte de hooklab.tampered.bin est-elle inutile
comme indicateur, et que fourniriez-vous au SOC à la place ?
À retenir
- Le hooking est à double usage : les EDR, les débogueurs, les traceurs et les applications posent des hooks ; un hook détecté est donc une observation qui demande une classification.
- Un rootkit en mode utilisateur filtre les résultats d’énumération dans les processus où il est chargé ; ce qu’il cache existe toujours dans le noyau, sur le disque et sur le réseau.
- Détectez les modifications en comparant la mémoire avec un mappage rebasé du fichier : le code octet par octet, les entrées de l’IAT par rapport aux exports qu’elles nomment, les tables d’exports par rapport au fichier. Un saut au premier octet d’une fonction suivi d’instructions tronquées est un hook inline.
- Classez un hook selon l’endroit où il aboutit, l’auteur de la signature de ce code, les fonctions qu’il couvre et la présence ou non du même hook sur une référence saine.
- Prouvez la dissimulation par une détection par vue croisée menée depuis un autre point d’observation : images mémoire, analyse de disque hors ligne et sources réseau.
- Collectez la mémoire, les dumps des scanners et le disque avant la remédiation, et consignez le hook, sa destination, sa classification, son effet et son mécanisme de chargement.