Leçon 6.5 · Analyse dynamique· 45 min
Dumper la mémoire et extraire les charges utiles
Capturez la mémoire d’un processus ou du système, trouvez la région de la vraie charge utile et refaites d’un PE mappé en mémoire un fichier exploitable.
Objectifs
- Expliquer pourquoi les charges utiles déchiffrées, la configuration d’exécution et le code injecté n’existent souvent qu’en mémoire
- Choisir entre un dump complet de processus, le dump d’une seule région et le dump d’un module, et produire chacun avec les outils standard
- Repérer les régions suspectes d’après le type de mémoire, la protection, les en-têtes MZ et les adresses de départ des threads
- Reconvertir un PE enregistré dans sa disposition mémoire en disposition fichier et reconstruire ses imports
- Hacher, documenter et stocker les dumps comme les échantillons malveillants actifs qu’ils sont
Les leçons précédentes de ce module ont observé un échantillon de l’extérieur : quels processus il a lancés, quels fichiers et quelles clés il a touchés, quelles API il a appelées. Cela vous dit ce que l’échantillon a fait, pas ce qu’il est une fois en cours d’exécution — et pour un binaire packé ou chiffré par un crypter, c’est justement la forme en cours d’exécution que vous vouliez analyser.
Le dump mémoire comble ce fossé : laissez l’échantillon se déchiffrer lui-même, puis copiez le résultat avant qu’il ne disparaisse. Cette leçon couvre ce qu’il faut dumper, comment trouver la région qui compte, et le problème de format qui piège presque tout le monde : un PE enregistré depuis la mémoire n’est pas disposé comme un fichier PE.
Pourquoi dumper la mémoire
Détecter les packers et l’entropie se terminait sur un avertissement : le triage statique d’un fichier packé décrit surtout le stub. Trois types de preuves sont couramment disponibles en mémoire et nulle part ailleurs.
| Ce qui n’existe qu’à l’exécution | Pourquoi le fichier ne le montre pas | Ce qu’un dump vous apporte |
|---|---|---|
| La charge utile dépaquetée ou déchiffrée | Le fichier contient un blob compressé ou chiffré plus un stub (packer, crypter à l’exécution) | Du vrai code pour le désassembleur, de vraies chaînes, de vrais imports |
| La configuration en clair | Adresses de C2, identifiants de campagne et clés sont stockés encodés et décodés juste avant usage (chiffrement de chaînes par XOR) | Des IOC (indicateurs de compromission) à transmettre au SOC sans avoir d’abord rétro-conçu le décodeur |
| Du code que le fichier n’a jamais contenu | Secondes étapes téléchargées à l’exécution, shellcode, charges utiles injectées dans un autre processus (process hollowing, injection de DLL réflective) | La seule copie de cette étape que vous obtiendrez jamais |
La mémoire n’est pas non plus un témoin parfait : un échantillon peut déchiffrer une chaîne, l’utiliser puis l’effacer, ou garder sa charge utile chiffrée pendant qu’il dort. Trop tôt, vous attrapez le stub ; trop tard, le processus s’est terminé. Dumpez plus d’une fois — après la première activité réseau, après une étape marquante dans votre journal de surveillance comportementale, ou sur un point d’arrêt que vous avez choisi dans le débogueur.
Que dumper
Il existe trois granularités, et chacune convient à une question différente.
Un processus entier
Un dump complet (MiniDumpWithFullMemory) contient toutes les pages
engagées ainsi que la liste des modules, les threads et les handles : volumineux,
mais complet, et vous pouvez l’ouvrir plus tard dans WinDbg ou en extraire des
régions. Un minidump contient les piles des threads, les listes de modules et
peu d’autres choses — généralement pas les allocations privées où vit le code
dépaqueté. Pour le travail sur les malwares (logiciels malveillants), optez par
défaut pour le dump complet.
| Outil | Comment | Remarques |
|---|---|---|
| ProcDump (Sysinternals) | procdump -accepteula -ma <pid> out.dmp | -ma donne un dump complet ; sans cette option, vous obtenez un minidump. Il peut aussi attendre un déclencheur, comme la fin du processus (-t) |
| Gestionnaire des tâches | Détails → clic droit sur le processus → Créer un fichier de vidage | Dump complet écrit dans votre dossier %LOCALAPPDATA%\Temp ; rien à installer |
| System Informer | Clic droit sur le processus → Create dump file… | Même principe, avec le choix du type de dump |
Astuce : Dumpez quand le processus est au repos dans un état connu, pas en pleine écriture. Si vous êtes dans x64dbg, mettre d’abord le processus en pause vous donne un instantané cohérent ; s’il tourne librement, prenez deux dumps à quelques secondes d’intervalle et comparez-les.
Une seule région
Quand vous savez quelle allocation vous voulez, l’onglet Memory de System
Informer → clic droit → Save… écrit exactement cette région ; dans x64dbg,
utilisez Memory Map → clic droit → Dump Memory to File. Le résultat est petit
et prêt pour strings, YARA ou un désassembleur, mais ce sont des octets bruts
sans contexte : notez donc à côté son adresse de base et sa protection.
Un module chargé
Quand la charge utile est un PE — un exécutable dépaqueté, une DLL mappée manuellement — vous voulez le récupérer sous forme de fichier PE fonctionnel. Enregistrer sa région vous donne les octets ; la section sur la disposition, plus bas, explique pourquoi ils ne seront pas analysables tant que vous ne les aurez pas corrigés.
Trouver la région intéressante
Processus, threads et DLL se terminait sur la vérification croisée qui guide cette leçon : cherchez de la mémoire exécutable qu’aucun module n’explique. Dans l’onglet Memory de System Informer, posez-vous trois questions.
- De quel type est-elle ? Image (sections mappées par le loader), Mapped
(vues de fichiers, sections partagées) ou Private (
VirtualAlloc, tas, piles). Le code légitime vit dans des régions Image. - Quelle est sa protection ?
RXouRWXsur une région Private signifie que quelqu’un a alloué de la mémoire puis l’a rendue exécutable — le geste caractéristique des unpackers, des injecteurs et des loaders de shellcode.RWXest le signal le plus bruyant ; un loader soigneux écrit enRW, puis bascule enRXavecVirtualProtect. Voir la protection mémoire. - Par quoi commence-t-elle ?
MZà l’offset 0 avec une signaturePE\0\0valide àe_lfanew, c’est un PE que le loader n’a jamais chargé. Certains loaders effacent les en-têtes après le mappage ; l’absence deMZne prouve donc rien.
Vérifiez ensuite les threads : une adresse de départ, ou une trame de pile, à l’intérieur d’une région Private exécutable la désigne comme du code vivant plutôt que comme un tampon résiduel. Combinez les signaux :
| Signal | Faible seul parce que | Fort quand il est combiné avec |
|---|---|---|
| Private + exécutable | Les environnements JIT (.NET, navigateurs, moteurs de scripts) le font légitimement | Le processus n’est pas un hôte JIT |
MZ dans une région Private | Certains logiciels conservent des copies de PE comme données | La région est exécutable, ou un thread y démarre |
| Départ de thread non adossé | Les callbacks du pool de threads se résolvent parfois bizarrement sans symboles | L’adresse tombe dans une région Private RX/RWX |
| Une région Image dont les octets diffèrent du fichier sur disque | Le hot-patching et les relocations provoquent de petites différences | De grandes différences dans .text — l’image a été évidée ou écrasée |
La dernière ligne compte parce que le hollowing et l’écrasement de module placent du code étranger dans une région étiquetée Image, adossée à un fichier innocent. Seule la comparaison des octets en mémoire avec le fichier les attrape — ce que les scanners ci-dessous automatisent.
Le problème de disposition
Sections PE et disposition en mémoire a montré le loader
copiant chaque section de PointerToRawData vers VirtualAddress. Enregistrez
cette mémoire et vous obtenez la disposition mémoire — sections à leurs RVA,
espaces alignés sur la page, un fichier de SizeOfImage octets — alors que la
table des sections qu’il contient porte toujours des offsets fichier. Tout outil
qui se fie à l’en-tête lit les mauvais octets : des zéros au point d’entrée, des
imports vides, des sections dans PE-bear dont les octets ne correspondent pas.
En plus de la disposition, un dump diffère du fichier d’origine par son contenu :
- Les relocations ont été appliquées. Si l’ASLR a chargé l’image ailleurs
qu’à sa base préférée, chaque pointeur absolu listé dans la table des
relocations a changé, et le champ
ImageBasede l’en-tête en mémoire contient la base réelle. - L’IAT est remplie. Chaque entrée de l’IAT contient désormais l’adresse d’exécution d’une API dans une DLL, plutôt qu’un pointeur vers un nom. Un packer qui a résolu les imports lui-même peut n’avoir laissé aucun répertoire d’imports.
- Les variables globales contiennent l’état d’exécution — y compris, fort utilement, les chaînes décodées.
- Les sections non initialisées contiennent des données.
.bssou la section vide d’un packer (UPX0) contiennent désormais des octets, qui ont besoin de place dans le fichier.
Deux façons de corriger la disposition
| Approche | Ce qui change | Fichier obtenu | Utilisée par |
|---|---|---|---|
| Brut = virtuel (pe-sieve l’appelle realigned) | Réécrire chaque en-tête de section pour que PointerToRawData = VirtualAddress et SizeOfRawData = la taille virtuelle alignée | Même taille que le dump ; octets inchangés | PE-bear (modifier les en-têtes de section), pe-sieve |
| Démapper vers la disposition fichier (pe-sieve : unmapped) | Recopier chaque section à des offsets consécutifs alignés sur FileAlignment et réécrire les en-têtes en conséquence | Compact, proche d’un fichier normal | pe-sieve, la plupart des unpackers |
La première ne déplace aucun octet ; elle est donc plus sûre quand vous vous méfiez de la table des sections. La seconde paraît normale à tous les outils mais ne conserve que ce que décrivent les en-têtes.
Les imports sont une réparation à part
Corriger la disposition rend le fichier analysable ; cela ne restaure pas une
table des imports détruite. En reconstruire une implique de trouver l’IAT en
mémoire, de résoudre chaque adresse en module!function et d’écrire un nouveau
répertoire d’imports. Scylla (autonome ou en tant que plugin x64dbg) le fait
sur un processus vivant : définissez l’OEP, IAT
Autosearch, Get Imports, Dump, puis Fix Dump ; PE-bear est l’outil pour
inspecter et retoucher le résultat à la main. Choisir le bon moment et le bon OEP
est le sujet du module ultérieur sur l’unpacking manuel — ici, retenez que « le
dump est analysable » et « le dump a des imports » sont deux jalons distincts.
Les scanners qui le font pour vous
Trois outils gratuits automatisent les vérifications ci-dessus.
- pe-sieve analyse un processus (
pe-sieve64.exe /pid <pid>). Il compare chaque module chargé à son fichier sur disque, cherche des images PE et du shellcode en mémoire privée (/shellc), détecte les hooks et le code patché, et dumpe tout ce qu’il signale dans un dossierprocess_<pid>avec un rapport JSON./dmodechoisit la disposition du dump — virtuelle, démappée ou réalignée — et/implui demande de reconstruire les imports. - hollows_hunter fait tourner le moteur de pe-sieve sur tous les processus (ou ceux qui correspondent à un nom), ce qui est le moyen le plus rapide de demander « y a-t-il quoi que ce soit d’injecté sur cette VM ? » après la détonation.
- Moneta adopte le point de vue de la carte mémoire : il énumère les régions et signale les anomalies comme la mémoire privée exécutable, les régions d’image modifiées et les threads démarrant dans de la mémoire non adossée.
pe-sieve raisonne en fichiers PE et produit des dumps chargeables ; Moneta raisonne en régions. Tous deux signaleront quand même la mémoire JIT d’un navigateur — le jugement reste le vôtre.
Mémoire de tout le système
Quand un échantillon s’injecte dans plusieurs processus, ou qu’on vous remet une
image issue d’un incident, capturez la machine entière. Dans un labo, prenez un
snapshot de la VM suspendue (VMware écrit un fichier .vmem que vous pouvez
analyser directement) ; sur des hôtes réels, les équipes de réponse utilisent des
outils comme WinPmem ou DumpIt. Volatility 3 lit ces images :
vol -f mem.raw windows.pslist # processes, PIDs, parents, times
vol -f mem.raw windows.dlllist --pid 4312 # modules the loader knows about
vol -f mem.raw windows.malfind --pid 4312 # private executable regions, with hexdumpmalfind applique l’heuristique que vous avez utilisée à la main — mémoire
privée exécutable, avec un aperçu du désassemblage — et en partage les faux
positifs. L’analyse forensique de la mémoire est une discipline à part entière ;
considérez ceci comme une piste.
Documenter les dumps
Un dump n’est une preuve que si vous pouvez dire d’où il vient. Notez, pour chaque fichier :
| Champ | Exemple |
|---|---|
| SHA-256 du dump | calculé immédiatement après l’enregistrement |
| Source | nom du processus, PID, PID parent, chemin de l’image |
| Région | adresse de base, taille, type (Private/Image/Mapped), protection |
| Moment | heure réelle et déclencheur (« après la première requête DNS », « sur le point d’arrêt sur VirtualProtect ») |
| Outil et mode | procdump -ma, Save de System Informer, pe-sieve /dmode 3 |
| Post-traitement | « démappé par script, imports reconstruits avec Scylla » — et le SHA-256 de chaque fichier dérivé |
Hachez le dump brut et chaque version corrigée. Aucune ne correspondra aux autres ni à l’échantillon d’origine — disposition, relocations, IAT et données d’exécution modifient toutes des octets — si bien que le hachage d’un dump est un identifiant de traçabilité, pas un IOC de détection. Pour la détection, utilisez des règles YARA sur du code stable ou des chaînes décodées (voir Écrire vos premières règles YARA) ainsi que les valeurs de configuration extraites.
Attention : Le dump d’une charge utile dépaquetée est du code malveillant actif, souvent plus dangereux que l’original puisque la couche protectrice a disparu. Traitez-le exactement comme un échantillon : gardez-le dans le labo, stockez-le dans une archive protégée par mot de passe (par convention, le mot de passe
infected), donnez-lui une extension non exécutable comme.binou.dmp, et ne le copiez jamais sur votre hôte. Les dumps complets de processus peuvent aussi contenir des identifiants, des jetons et des données personnelles provenant de la VM — une raison de plus de garder les VM de labo exemptes de vrais comptes, comme mis en place dans Construire un laboratoire d’analyse sûr.
Lab : simuler un loader, puis réparer le dump
Le problème de disposition relève de la pure arithmétique ; vous pouvez donc le
reproduire en Python avant de toucher à une VM : compilez un programme inoffensif
qui décode une chaîne de « configuration » à l’exécution, mappez-le comme le fait
le loader, enregistrez cette image comme le ferait un outil de dump, et
réparez-la de deux façons. Il vous faut mingw-w64 et un environnement virtuel
avec pefile et capstone.
-
Préparez l’environnement et compilez l’échantillon :
c // labcfg.c — harmless: decodes a "config" string at runtime and prints it #include <stdio.h> static unsigned char cfg[] = { /* "mode=lab;host=127.0.0.1" ^ 0x5A */ 0x37,0x35,0x3e,0x3f,0x67,0x36,0x3b,0x38,0x61,0x32,0x35,0x29,0x2e, 0x67,0x6b,0x68,0x6d,0x74,0x6a,0x74,0x6a,0x74,0x6b,0x00 }; int main(void) { for (unsigned i = 0; cfg[i]; i++) cfg[i] ^= 0x5A; /* cleartext exists only in memory */ printf("config: %s\n", (char *)cfg); return 0; }bash python3 -m venv venv && ./venv/bin/pip install pefile capstone x86_64-w64-mingw32-gcc -O1 -s -o labcfg.exe labcfg.c shasum -a 256 labcfg.exe -
Enregistrez le script sous
mapdump.py.map_imagejoue le rôle du loader : il copie les en-têtes, place chaque section à sa RVA dans un tampon deSizeOfImageoctets, applique les relocations de base 64 bits pour une base de type ASLR, écrit cette base dans l’en-tête, puis fait ce que faitmain— décoder la configuration. Les deux fonctionsunmap_sont les réparations du tableau ci-dessus.python # mapdump.py — simulate a loader, save a "memory dump", then realign it import hashlib, struct, sys import pefile from capstone import Cs, CS_ARCH_X86, CS_MODE_64 def align(x, a): return (x + a - 1) // a * a def sha(buf): return hashlib.sha256(buf).hexdigest()[:16] def name(s): return s.Name.rstrip(b"\x00").decode(errors="replace") def map_image(data, new_base): """Lay the file out by RVA, as the Windows loader does, and rebase it.""" 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)) raw = data[s.PointerToRawData:s.PointerToRawData + n] mem[s.VirtualAddress:s.VirtualAddress + len(raw)] = raw # rest stays 0 # apply base relocations (DIR64) for the new base, like ASLR does 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) # the loader writes the real base into the in-memory header struct.pack_into("<Q", mem, oh.get_field_absolute_offset("ImageBase"), new_base) # simulate the program running: main() XOR-decodes its config in .data i = mem.find(bytes([0x37, 0x35, 0x3e, 0x3f, 0x67])) while mem[i]: mem[i] ^= 0x5A i += 1 return bytes(mem) def unmap_raw_equals_virtual(mem): """Fix 1: keep memory layout, rewrite headers so raw == virtual.""" pe = pefile.PE(data=mem) for s in pe.sections: s.PointerToRawData = s.VirtualAddress s.SizeOfRawData = align(s.Misc_VirtualSize, pe.OPTIONAL_HEADER.SectionAlignment) return pe.write() def unmap_to_file_layout(mem): """Fix 2: copy each section back to a FileAlignment offset.""" pe = pefile.PE(data=mem) fa = pe.OPTIONAL_HEADER.FileAlignment out = bytearray(mem[:pe.OPTIONAL_HEADER.SizeOfHeaders]) for s in pe.sections: body = mem[s.VirtualAddress:s.VirtualAddress + s.Misc_VirtualSize] s.PointerToRawData = len(out) if body else 0 s.SizeOfRawData = align(len(body), fa) out += body.ljust(s.SizeOfRawData, b"\x00") hdr = pe.write()[:pe.OPTIONAL_HEADER.SizeOfHeaders] return hdr + bytes(out[len(hdr):]) def report(label, buf): pe = pefile.PE(data=buf) oh = pe.OPTIONAL_HEADER print(f"== {label}: {len(buf)} bytes, sha256 {sha(buf)}…, ImageBase {oh.ImageBase:#x}") print(f" {'section':8} {'VA':>7} {'VSize':>7} {'RawPtr':>7} {'RawSize':>7}") for s in pe.sections[:4]: print(f" {name(s):8} {s.VirtualAddress:#7x} {s.Misc_VirtualSize:#7x} " f"{s.PointerToRawData:#7x} {s.SizeOfRawData:#7x}") ep = oh.AddressOfEntryPoint off = pe.get_offset_from_rva(ep) code = buf[off:off + 12] ins = next(Cs(CS_ARCH_X86, CS_MODE_64).disasm(code, oh.ImageBase + ep), None) print(f" entry RVA {ep:#x} -> file offset {off:#x}: {code[:8].hex(' ')}" f" => {ins.mnemonic + ' ' + ins.op_str if ins else '(not code)'}") data = next(s for s in pe.sections if name(s) == ".data") blob = buf[data.PointerToRawData:data.PointerToRawData + 0x40] if b"mode" in blob: txt = blob[blob.find(b"mode"):].split(b"\0")[0].decode() else: txt = "no cleartext; first bytes " + blob[:8].hex(" ") print(f" .data via PointerToRawData {data.PointerToRawData:#x}: {txt}") imps = [e.dll.decode() for e in getattr(pe, "DIRECTORY_ENTRY_IMPORT", [])] print(f" imports parsed: {len(imps)} DLLs {imps[:2]}") print() def section_diff(a, b): pa, pb = pefile.PE(data=a), pefile.PE(data=b) print("per-section comparison, original vs fix 2 (first VirtualSize bytes):") for sa, sb in zip(pa.sections, pb.sections): n = sa.Misc_VirtualSize x = a[sa.PointerToRawData:sa.PointerToRawData + n] if sa.SizeOfRawData else bytes(n) y = b[sb.PointerToRawData:sb.PointerToRawData + n] d = sum(1 for i, j in zip(x, y) if i != j) print(f" {name(sa):8} {sha(x)} {sha(y)} {d:4} bytes differ") orig = open(sys.argv[1], "rb").read() mem = map_image(orig, new_base=0x7FF6A1B20000) open("labcfg.mem.bin", "wb").write(mem) report("original file", orig) report("memory image (as dumped)", mem) fixed1 = unmap_raw_equals_virtual(mem) open("labcfg.fix1.exe", "wb").write(fixed1) report("fix 1: raw = virtual", fixed1) fixed2 = unmap_to_file_layout(mem) open("labcfg.fix2.exe", "wb").write(fixed2) report("fix 2: unmapped to file layout", fixed2) section_diff(orig, fixed2) -
Exécutez-le :
bash ./venv/bin/python mapdump.py labcfg.exeD’abord, le fichier d’origine et l’image mémoire brute. Seules les quatre premières sections sont affichées ; les hachages sont tronqués à 16 chiffres hexadécimaux :
text == original file: 16384 bytes, sha256 12710c38b3a06375…, ImageBase 0x140000000 section VA VSize RawPtr RawSize .text 0x1000 0x1a90 0x400 0x1c00 .data 0x3000 0xc0 0x2000 0x200 .rdata 0x4000 0x9e8 0x2200 0xa00 .pdata 0x5000 0x228 0x2c00 0x400 entry RVA 0x1440 -> file offset 0x840: 48 83 ec 28 48 8b 05 d5 => sub rsp, 0x28 .data via PointerToRawData 0x2000: no cleartext; first bytes 37 35 3e 3f 67 36 3b 38 imports parsed: 9 DLLs ['KERNEL32.dll', 'api-ms-win-crt-environment-l1-1-0.dll'] == memory image (as dumped): 45056 bytes, sha256 4fef6962a593fae3…, ImageBase 0x7ff6a1b20000 section VA VSize RawPtr RawSize .text 0x1000 0x1a90 0x400 0x1c00 .data 0x3000 0xc0 0x2000 0x200 .rdata 0x4000 0x9e8 0x2200 0xa00 .pdata 0x5000 0x228 0x2c00 0x400 entry RVA 0x1440 -> file offset 0x840: 00 00 00 00 00 00 00 00 => add byte ptr [rax], al .data via PointerToRawData 0x2000: no cleartext; first bytes e8 c3 09 00 00 eb aa 66 imports parsed: 0 DLLs []La configuration du fichier est encore encodée (
37 35 3e 3f…). L’image mémoire fait0xb000octets — exactementSizeOfImage— et sa table des sections est inchangée, si bien que chaque recherche tombe à côté : le « point d’entrée » est du remplissage d’en-tête que capstone décode consciencieusement enadd byte ptr [rax], al, la «.data» en0x2000est en réalité le milieu de.text(e8 …est uncall), et pefile ne trouve aucun import. La configuration décodée est bien dans ce fichier — à l’offset0x3000— mais rien de ce qui se fie à l’en-tête ne la trouvera. -
Passons aux deux réparations :
text == fix 1: raw = virtual: 45056 bytes, sha256 d8081e62a23a5915…, ImageBase 0x7ff6a1b20000 section VA VSize RawPtr RawSize .text 0x1000 0x1a90 0x1000 0x2000 .data 0x3000 0xc0 0x3000 0x1000 .rdata 0x4000 0x9e8 0x4000 0x1000 .pdata 0x5000 0x228 0x5000 0x1000 entry RVA 0x1440 -> file offset 0x1440: 48 83 ec 28 48 8b 05 d5 => sub rsp, 0x28 .data via PointerToRawData 0x3000: mode=lab;host=127.0.0.1 imports parsed: 9 DLLs ['KERNEL32.dll', 'api-ms-win-crt-environment-l1-1-0.dll'] == fix 2: unmapped to file layout: 16896 bytes, sha256 ed3989ff97eb4461…, ImageBase 0x7ff6a1b20000 section VA VSize RawPtr RawSize .text 0x1000 0x1a90 0x400 0x1c00 .data 0x3000 0xc0 0x2000 0x200 .rdata 0x4000 0x9e8 0x2200 0xa00 .pdata 0x5000 0x228 0x2c00 0x400 entry RVA 0x1440 -> file offset 0x840: 48 83 ec 28 48 8b 05 d5 => sub rsp, 0x28 .data via PointerToRawData 0x2000: mode=lab;host=127.0.0.1 imports parsed: 9 DLLs ['KERNEL32.dll', 'api-ms-win-crt-environment-l1-1-0.dll']Les deux sont analysables et montrent toutes deux la configuration décodée. La correction 1 n’a déplacé aucun octet : RVA et offset fichier sont désormais égaux (
0x1440 → 0x1440). La correction 2 est un fichier compact, plus grand de 512 octets que l’original parce que.bss, vide sur disque, reçoit maintenant un emplacement brut de0x200octets pour son contenu en mémoire. -
Comparez le contenu, pas seulement la disposition :
text per-section comparison, original vs fix 2 (first VirtualSize bytes): .text 526bd1659438b078 526bd1659438b078 0 bytes differ .data ca4fbacadbb6d96f 706bc7a61588e747 43 bytes differ .rdata 0df4dd7330cf2676 2b837950fb2de061 136 bytes differ .pdata a33ab3e879dff4f7 a33ab3e879dff4f7 0 bytes differ ... .idata 238a2d1f456486e9 238a2d1f456486e9 0 bytes differ .reloc 9ca899cf3c7ac52c 9ca899cf3c7ac52c 0 bytes differLe code est identique à l’octet près.
.datadiffère par les 23 octets de configuration décodés plus les pointeurs relocalisés ;.rdatane diffère que par les pointeurs relocalisés. Notre loader simulé n’a pas lié les imports, donc.idataest inchangée ici — sous Windows, les entrées de l’IAT qu’elle contient recevraient de vraies adresses d’API et différeraient aussi. Quatre fichiers, quatre hachages, un seul programme. -
Dans votre VM Windows, refaites la démarche en conditions réelles. Copiez-y
labcfg.exe, mais comme il se termine immédiatement, ajoutez ungetchar();avantreturn 0;et recompilez pour qu’il reste actif. Ensuite :- Exécutez-le, notez le PID et prenez un dump complet avec
procdump -accepteula -ma <pid> labcfg_full.dmp. Prenez un minidump sans-maet comparez les tailles. - Dans System Informer, ouvrez le processus → Memory. Trouvez les régions
appartenant à
labcfg.exe(type Image) et celle en RW à la base de l’image +0x3000: c’est.data. Clic droit → Save…, puis cherchezmode=labdans le fichier enregistré — la configuration décodée, qu’un examen statique du fichier ne pouvait pas vous montrer. - Exécutez
pe-sieve64.exe /pid <pid>et lisez le résumé etscan_report.json. Un processus non modifié ne devrait rien signaler de suspect ni produire de dump. Quand pe-sieve signale un module,/dmodechoisit la disposition de ce qu’il écrit — comparez un dump unmapped et un dump realigned d’un échantillon ultérieur avec vos fichiersfix2etfix1. - Hachez chaque fichier produit et remplissez pour chacun le tableau de documentation ci-dessus.
- Exécutez-le, notez le PID et prenez un dump complet avec
Questions : Pourquoi le point d’entrée de l’image mémoire brute s’est-il décodé en une instruction valide, et qu’est-ce que cela vous apprend sur la confiance à accorder à la sortie d’un désassembleur ? Laquelle des deux réparations choisiriez-vous si vous soupçonniez que la table des sections a été altérée, et pourquoi ? Si le répertoire d’imports d’un échantillon avait été effacé après le chargement, lesquelles des vérifications du lab échoueraient même après correction de la disposition ? Pourquoi le SHA-256 du dump est-il inutile comme indicateur de détection, et que donneriez-vous plutôt au SOC ?
À retenir
- Dumpez la mémoire pour récupérer ce qui ne touche jamais le disque : charges utiles dépaquetées, configuration décodée et étapes injectées ou téléchargées. Le moment du dump compte autant que la manière.
- Prenez des dumps complets de processus (
procdump -ma, Gestionnaire des tâches) pour l’exhaustivité, des enregistrements de régions (System Informer, x64dbg) pour la précision, et des dumps de modules quand vous avez besoin d’un PE fonctionnel. - Trouvez la région qui compte par le type, la protection, les en-têtes
MZet les adresses de départ des threads ; la mémoire privée exécutable et les threads non adossés sont les signaux essentiels, et les hôtes JIT le principal faux positif. - Un PE enregistré depuis la mémoire est en disposition mémoire alors que ses
en-têtes décrivent une disposition fichier ; corrigez-le en posant brut =
virtuel ou en le démappant sur
FileAlignment, puis reconstruisez séparément les imports avec Scylla et vérifiez dans PE-bear. - pe-sieve, hollows_hunter et Moneta automatisent ces vérifications ;
pslist,dlllistetmalfindde Volatility 3 les appliquent aux images de tout un système. - Hachez et documentez chaque dump et chaque fichier dérivé, et traitez les dumps comme des échantillons actifs — ils sont souvent plus dangereux que le fichier de départ.