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 :
| Type | Objectif principal | Exemples | Comportement typique du stub |
|---|---|---|---|
| Packer (compresseur) | Des fichiers plus petits | UPX, MPRESS, ASPack | Décompresse les sections, reconstruit les imports, saute au point d’entrée original |
| Crypter | Échapper à la détection par signature | Crypters grand public vendus avec des malwares | Déchiffre une charge utile embarquée, souvent dans un nouveau processus ou dans de la mémoire fraîchement allouée |
| Protecteur | Résister à la rétro-ingénierie et à la falsification | Themida, VMProtect, Enigma | Anti-débogage, obfuscation des imports, virtualisation du code |
Tous trois partagent un même schéma d’exécution :
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ᵢ :
H = − Σ pᵢ · log₂(pᵢ) summed over the 256 possible byte valuesLe 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 :
| Indicateur | Pourquoi il suggère un packing |
|---|---|
| Une section de taille brute 0 mais de grande taille virtuelle | Espace réservé à du code qui sera écrit à l’exécution (UPX0 d’UPX) |
| Section inscriptible + exécutable | Le stub doit écrire le code qu’il exécutera ensuite |
Point d’entrée dans la dernière section ou hors de .text | L’exécution commence dans le stub, pas dans le programme |
Table des imports minuscule avec LoadLibrary, GetProcAddress, VirtualProtect / VirtualAlloc | Les 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 lisibles | Les chaînes sont dans le blob compressé |
| Overlay ou ressource à forte entropie | Charge 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
.rsrcremplie 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.
-
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 -
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)}") -
Lancez-le sur le programme simple :
bash python3 entropy.py hello.exe 4096text 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.24Le code se situe dans la plage attendue de 5 à 6,5 ; rien ne dépasse 7,2.
-
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.31Relevez tous les indicateurs structurels d’un coup :
UPX0n’a pas de données brutes,UPX1est dense, les noms de sections constituent une signature, et la table des imports (lancezcapimports.pyde la leçon précédente) se réduit àLoadLibraryA,GetProcAddress,VirtualProtectetExitProcess, plus un import par DLL du runtime C. Dans la vue par fenêtre glissante, la première fenêtre est étiquetéeheadersmais obtient un score élevé : avec une fenêtre de 4 096 octets, elle couvre déjà l’essentiel deUPX1. Relancez avec1024pour voir la frontière nettement. -
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.97L’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.
-
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.