Skip to content

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écutionPourquoi le fichier ne le montre pasCe qu’un dump vous apporte
La charge utile dépaquetée ou déchiffréeLe 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 clairAdresses 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 contenuSecondes é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.

OutilCommentRemarques
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âchesDétails → clic droit sur le processus → Créer un fichier de vidageDump complet écrit dans votre dossier %LOCALAPPDATA%\Temp ; rien à installer
System InformerClic 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.

  1. 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.
  2. Quelle est sa protection ? RX ou RWX sur 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. RWX est le signal le plus bruyant ; un loader soigneux écrit en RW, puis bascule en RX avec VirtualProtect. Voir la protection mémoire.
  3. Par quoi commence-t-elle ? MZ à l’offset 0 avec une signature PE\0\0 valide à 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 de MZ ne 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 :

SignalFaible seul parce queFort quand il est combiné avec
Private + exécutableLes environnements JIT (.NET, navigateurs, moteurs de scripts) le font légitimementLe processus n’est pas un hôte JIT
MZ dans une région PrivateCertains logiciels conservent des copies de PE comme donnéesLa 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 symbolesL’adresse tombe dans une région Private RX/RWX
Une région Image dont les octets diffèrent du fichier sur disqueLe hot-patching et les relocations provoquent de petites différencesDe 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 ImageBase de 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. .bss ou 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

ApprocheCe qui changeFichier obtenuUtilisé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éeMême taille que le dump ; octets inchangésPE-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équenceCompact, proche d’un fichier normalpe-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 dossier process_<pid> avec un rapport JSON. /dmode choisit la disposition du dump — virtuelle, démappée ou réalignée — et /imp lui 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 :

bash
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 hexdump

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

ChampExemple
SHA-256 du dumpcalculé immédiatement après l’enregistrement
Sourcenom du processus, PID, PID parent, chemin de l’image
Régionadresse de base, taille, type (Private/Image/Mapped), protection
Momentheure réelle et déclencheur (« après la première requête DNS », « sur le point d’arrêt sur VirtualProtect »)
Outil et modeprocdump -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 .bin ou .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.

  1. 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
  2. Enregistrez le script sous mapdump.py. map_image joue le rôle du loader : il copie les en-têtes, place chaque section à sa RVA dans un tampon de SizeOfImage octets, 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 fait main — décoder la configuration. Les deux fonctions unmap_ 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)
  3. Exécutez-le :

    bash
    ./venv/bin/python mapdump.py labcfg.exe

    D’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 fait 0xb000 octets — exactement SizeOfImage — 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 en add byte ptr [rax], al, la « .data » en 0x2000 est en réalité le milieu de .text (e8 … est un call), et pefile ne trouve aucun import. La configuration décodée est bien dans ce fichier — à l’offset 0x3000 — mais rien de ce qui se fie à l’en-tête ne la trouvera.

  4. 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 de 0x200 octets pour son contenu en mémoire.

  5. 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 differ

    Le code est identique à l’octet près. .data diffère par les 23 octets de configuration décodés plus les pointeurs relocalisés ; .rdata ne diffère que par les pointeurs relocalisés. Notre loader simulé n’a pas lié les imports, donc .idata est 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.

  6. Dans votre VM Windows, refaites la démarche en conditions réelles. Copiez-y labcfg.exe, mais comme il se termine immédiatement, ajoutez un getchar(); avant return 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 -ma et 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 cherchez mode=lab dans 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é et scan_report.json. Un processus non modifié ne devrait rien signaler de suspect ni produire de dump. Quand pe-sieve signale un module, /dmode choisit la disposition de ce qu’il écrit — comparez un dump unmapped et un dump realigned d’un échantillon ultérieur avec vos fichiers fix2 et fix1.
    • Hachez chaque fichier produit et remplissez pour chacun le tableau de documentation ci-dessus.

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 MZ et 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, dlllist et malfind de 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.