Skip to content

Leçon 3.4 · Triage statique· 35 min

Détecter les packers et l’entropie

Reconnaissez les exécutables packés ou chiffrés par l’entropie, les anomalies de sections, les imports et les signatures, en évitant les faux positifs.

Objectifs

  • Expliquer ce que font les packers, les crypters et les protecteurs, et pourquoi ils mettent en échec l’analyse statique
  • Calculer l’entropie de Shannon et interpréter les valeurs sur le fichier entier, par section et par fenêtre glissante
  • Combiner l’entropie avec des indicateurs structurels pour conclure avec assurance sur le packing
  • Reconnaître les sources légitimes de forte entropie qui ne relèvent pas du packing

Tout ce que vous avez vu jusqu’ici dans ce module — chaînes, imports, capacités — suppose que le code intéressant se trouve dans le fichier, là où vous pouvez le lire. Un échantillon packé rompt cette hypothèse. Son vrai code est compressé ou chiffré, et un petit stub le restaure en mémoire à l’exécution. Le triage statique d’un fichier packé décrit surtout le packer, pas le malware (logiciel malveillant).

L’une des premières questions de tout triage est donc : est-ce packé ? Cette leçon vous apprend à y répondre avec des preuves, et à éviter les fausses alertes qui piègent les débutants.

Packers, crypters et protecteurs

Les termes se recoupent, mais ils visent des objectifs différents :

TypeObjectif principalExemplesComportement typique du stub
Packer (compresseur)Des fichiers plus petitsUPX, MPRESS, ASPackDécompresse les sections, reconstruit les imports, saute au point d’entrée original
CrypterÉchapper à la détection par signatureCrypters grand public vendus avec des malwaresDéchiffre une charge utile embarquée, souvent dans un nouveau processus ou dans de la mémoire fraîchement allouée
ProtecteurRésister à la rétro-ingénierie et à la falsificationThemida, VMProtect, EnigmaAnti-débogage, obfuscation des imports, virtualisation du code

Tous trois partagent un même schéma d’exécution :

text
  on disk                              in memory at runtime
  ┌───────────────┐                    ┌────────────────────┐
  │ headers       │                    │ headers            │
  │ stub code     │ ── stub runs ───►  │ stub code          │
  │ packed blob   │    decompresses /  │ original code  ◄───┼── jump to OEP
  │ (high entropy)│    decrypts,       │ original data      │
  └───────────────┘    fixes imports   │ rebuilt IAT        │
                                       └────────────────────┘

Le stub se termine en transférant le contrôle au point d’entrée original (OEP) du code dépaqueté. Trouver ce moment et dumper la mémoire est l’objet du futur module Unpacking ; cette leçon ne porte que sur la détection. Les pages de techniques packing UPX et crypter d’exécution décrivent les deux familles plus en détail.

L’entropie en une formule

L’entropie de Shannon mesure à quel point un bloc d’octets est imprévisible. Pour un tampon où la valeur d’octet i apparaît avec une probabilité pᵢ :

text
H = − Σ pᵢ · log₂(pᵢ)        summed over the 256 possible byte values

Le résultat s’exprime en bits par octet, de 0,0 à 8,0 :

  • 0,0 — tous les octets sont identiques (un bloc de zéros).
  • ≈ 3–5 — texte, tables, données structurées.
  • ≈ 5–6,5 — code x86/x64 compilé typique.
  • ≈ 7,2 et au-delà — données compressées ou chiffrées ; des octets réellement aléatoires approchent 8,0.

La compression et le chiffrement suppriment tous deux la redondance, ce qui explique qu’ils poussent l’entropie vers le maximum. Du code ordinaire ne peut pas y parvenir : opcodes, registres et motifs d’instructions courants se répètent trop.

Astuce : L’entropie dépend du nombre d’octets mesurés. Une fenêtre de 1 Kio de données parfaitement aléatoires n’atteint qu’environ 7,8, car 1 024 échantillons ne peuvent pas remplir uniformément 256 compartiments. Comparez des valeurs mesurées avec la même taille de fenêtre.

Trois façons de mesurer

  • Fichier entier — un seul chiffre ; rapide mais grossier. Une grande section à faible entropie peut masquer un petit blob chiffré.
  • Par section — la vue de triage la plus utile : quelle partie du fichier est dense ?
  • Fenêtre glissante — l’entropie de chaque bloc consécutif (par exemple de 1 à 4 Kio), tracée sous forme de graphe. La vue d’entropie de Detect It Easy et le script du lab ci-dessous font tous deux cela. Elle révèle les blobs à l’intérieur d’une section et les données de l’overlay.

Les indicateurs au-delà de l’entropie

L’entropie seule ne constitue jamais un verdict. Combinez-la avec les indicateurs structurels vus dans Sections PE et Lire les capacités à partir des imports :

IndicateurPourquoi il suggère un packing
Une section de taille brute 0 mais de grande taille virtuelleEspace réservé à du code qui sera écrit à l’exécution (UPX0 d’UPX)
Section inscriptible + exécutableLe stub doit écrire le code qu’il exécutera ensuite
Point d’entrée dans la dernière section ou hors de .textL’exécution commence dans le stub, pas dans le programme
Table des imports minuscule avec LoadLibrary, GetProcAddress, VirtualProtect / VirtualAllocLes imports sont reconstruits à l’exécution (voir résolution dynamique des imports)
Noms de sections inhabituels (UPX0, .aspack, .themida, chaînes aléatoires)Structure générée par un packer
Très peu de chaînes lisiblesLes chaînes sont dans le blob compressé
Overlay ou ressource à forte entropieCharge utile stockée hors des sections (voir Ressources et overlays)

Trois de ces indicateurs ou plus réunis, avec une section dense, permettent de conclure avec assurance.

Identification par signature

Des outils comme Detect It Easy (DiE) comparent des motifs d’octets connus au point d’entrée et des règles structurelles pour nommer le packer, le compilateur et l’éditeur de liens. C’est rapide et souvent parfaitement juste pour les packers courants — mais les signatures peuvent être usurpées (un échantillon peut contenir de faux noms de sections UPX) et les crypters sur mesure n’ont aucune signature. Traitez une étiquette DiE comme une piste à confirmer, et comprenez « aucun packer détecté » comme « aucun packer connu détecté ». capa signale de même lorsqu’un fichier semble packé, et déclenche des règles comme « packed with UPX ».

Les faux positifs

Une forte entropie est courante dans des fichiers parfaitement légitimes :

  • Ressources compressées. Les icônes PNG et JPEG, les ZIP embarqués et les polices sont déjà compressés. Une section .rsrc remplie d’images obtient un score élevé.
  • Installeurs et archives auto-extractibles. Les fichiers NSIS, Inno Setup et 7-Zip SFX transportent une archive compressée dans l’overlay — par conception.
  • Protecteurs légitimes. Les logiciels commerciaux, les jeux et les outils sous licence utilisent souvent des protecteurs ; les applications .NET peuvent être obfusquées.
  • Certificats et tables cryptographiques embarqués — petits mais denses.

Le critère discriminant est l’endroit où se trouve l’entropie. Des données denses dans une ressource que le programme charge comme image, c’est normal. Des données denses dans la section qui contient le point d’entrée, ou un blob déchiffré dans de la mémoire exécutable, ne le sont pas.

Lab : mesurer, puis juger

Il vous faut mingw-w64, UPX et Python 3 avec pefile. Vous allez comparer trois binaires inoffensifs : un programme simple, le même programme packé avec UPX, et un programme qui embarque 32 Kio d’octets aléatoires sous forme de ressource.

  1. Compilez les trois variantes :

    bash
    printf '#include <stdio.h>\nint main(void){puts("hello, analyst");return 0;}\n' > hello.c
    x86_64-w64-mingw32-gcc -s -o hello.exe hello.c
    upx -o hello_upx.exe hello.exe
    
    head -c 32768 /dev/urandom > blob.bin
    echo '101 RCDATA "blob.bin"' > blob.rc
    x86_64-w64-mingw32-windres blob.rc -O coff -o blob.res.o
    x86_64-w64-mingw32-gcc -s -o hello_blob.exe hello.c blob.res.o
  2. Enregistrez ce script sous le nom entropy.py :

    python
    # entropy.py — per-section and sliding-window entropy for a PE file
    import math, sys
    from collections import Counter
    import pefile
    
    def entropy(buf: bytes) -> float:
        """Shannon entropy in bits per byte, 0.0 to 8.0."""
        n = len(buf)
        if n == 0:
            return 0.0
        return max(0.0, -sum(c / n * math.log2(c / n) for c in Counter(buf).values()))
    
    def region(pe, off: int) -> str:
        """Name the part of the file that contains file offset `off`."""
        for s in pe.sections:
            if s.PointerToRawData <= off < s.PointerToRawData + s.SizeOfRawData:
                return s.Name.rstrip(b"\x00").decode(errors="replace")
        end = max(s.PointerToRawData + s.SizeOfRawData for s in pe.sections)
        return "overlay" if off >= end else "headers"
    
    path = sys.argv[1]
    window = int(sys.argv[2]) if len(sys.argv) > 2 else 1024
    data = open(path, "rb").read()
    pe = pefile.PE(data=data)
    
    print(f"{path}: {len(data)} bytes, whole-file entropy {entropy(data):.2f}\n")
    print(f"{'section':8} {'raw size':>9} {'entropy':>8}")
    for s in pe.sections:
        name = s.Name.rstrip(b"\x00").decode(errors="replace")
        print(f"{name:8} {s.SizeOfRawData:>9} {entropy(s.get_data()):>8.2f}")
    
    print(f"\nwindow = {window} bytes; one '#' = 0.25 bits/byte; '|' marks 7.2")
    for off in range(0, len(data), window):
        h = entropy(data[off:off + window])
        bar = list(("#" * round(h * 4)).ljust(32))
        bar[29] = "|"
        print(f"{off:#08x} {region(pe, off):8} {h:4.2f} {''.join(bar)}")
  3. Lancez-le sur le programme simple :

    bash
    python3 entropy.py hello.exe 4096
    text
    hello.exe: 16384 bytes, whole-file entropy 4.70
    
    section   raw size  entropy
    .text         7168     5.69
    .data          512     0.57
    .rdata        2560     4.02
    .pdata        1024     2.30
    .xdata         512     3.31
    .bss             0     0.00
    .idata        2560     3.52
    .tls           512     0.00
    .reloc         512     1.24

    Le code se situe dans la plage attendue de 5 à 6,5 ; rien ne dépasse 7,2.

  4. Lancez-le sur le build UPX :

    text
    hello_upx.exe: 8704 bytes, whole-file entropy 7.00
    
    section   raw size  entropy
    UPX0             0     0.00
    UPX1          7168     7.48
    UPX2          1024     3.31

    Relevez tous les indicateurs structurels d’un coup : UPX0 n’a pas de données brutes, UPX1 est dense, les noms de sections constituent une signature, et la table des imports (lancez capimports.py de la leçon précédente) se réduit à LoadLibraryA, GetProcAddress, VirtualProtect et ExitProcess, plus un import par DLL du runtime C. Dans la vue par fenêtre glissante, la première fenêtre est étiquetée headers mais obtient un score élevé : avec une fenêtre de 4 096 octets, elle couvre déjà l’essentiel de UPX1. Relancez avec 1024 pour voir la frontière nettement.

  5. Lancez-le sur le programme avec la ressource aléatoire :

    text
    hello_blob.exe: 49664 bytes, whole-file entropy 7.27
    ...
    .text         7168     5.69
    .rsrc        33280     7.97

    L’entropie du fichier entier est plus élevée que celle du build UPX, et pourtant ce programme n’est pas packé du tout : le code est intact, les imports sont normaux, et les octets denses se trouvent dans une ressource que le programme n’exécute jamais.

  6. Facultatif : ouvrez les trois fichiers dans Detect It Easy (graphe d’entropie et recherche de signatures) et dans PE-bear, et comparez avec votre script.

Questions : Quel chiffre unique vous aurait induit en erreur à l’étape 5, et quels indicateurs ont corrigé cette impression ? Si un échantillon avait une section .text dense, une table des imports d’apparence normale et aucune signature de packer connue, que vérifieriez-vous ensuite ? Pourquoi une fenêtre plus petite abaisse-t-elle l’entropie maximale observable ?

À retenir

  • Les échantillons packés cachent leur vrai code ; le triage statique d’un fichier packé décrit surtout le stub, alors détectez le packing tôt.
  • L’entropie va de 0 à 8 bits par octet ; le code se situe autour de 5 à 6,5, les données compressées ou chiffrées au-dessus d’environ 7,2.
  • Mesurez par section et avec une fenêtre glissante, pas seulement sur le fichier entier.
  • Un verdict exige plusieurs indicateurs : section dense plus anomalies de taille brute, sections inscriptibles et exécutables, point d’entrée dans une section inhabituelle, table des imports minuscule ou signature de packer.
  • Les ressources compressées, les installeurs et les protecteurs légitimes produisent aussi une forte entropie — jugez selon l’endroit où se trouvent les données denses et selon qu’elles sont exécutées ou non.