Détecter les exécutables packés : UPX, entropie et autres
Signes statiques d'un exécutable Windows packé ou protégé — noms de sections, entropie, sections W+X, point d'entrée, imports, overlay — et leurs pièges.
En bref. Un packer compresse ou chiffre le vrai programme et ajoute un petit stub qui le restaure en mémoire. En statique, c'est le stub que l'on voit : peu d'imports, des noms de sections étranges, un gros bloc à forte entropie, parfois une section inscriptible et exécutable et un point d'entrée hors de .text. Aucun signe n'est concluant à lui seul ; ensemble, ils sont fiables. La solution consiste à décompresser — avec l'outil du packer lui-même lorsqu'il existe, ou dynamiquement dans une sandbox — puis à analyser à nouveau.
Les signes, un par un
Noms de sections. Les packers nomment généralement leurs sections : UPX utilise UPX0 (vide sur disque, volumineuse en mémoire) et UPX1 (les données compressées plus le stub) ; MPRESS utilise .MPRESS1/.MPRESS2 ; VMProtect .vmp0/.vmp1 ; Themida .themida. Ces noms sont triviaux à modifier et ne sont donc que des indices — mais les échantillons réels s'en donnent rarement la peine.
Marqueurs. UPX écrit la chaîne magique UPX! et une chaîne de version dans les en-têtes. C'est sur elles que s'appuie upx -d pour décompresser, et c'est aussi pourquoi les altérer fait échouer upx -d (UPX project).
Entropie. L'entropie de Shannon, en bits par octet, mesure à quel point les octets semblent aléatoires. Le code x86/x64 compilé se situe généralement autour de 5 à 6,8 ; les données compressées ou chiffrées approchent 8. Le PE Parser signale les sections à 7,2 ou plus (en ignorant .rsrc, où images et archives sont normales).
Permissions. Un stub doit écrire le code décompressé quelque part, puis l'exécuter. Une section marquée à la fois inscriptible et exécutable est rare dans la sortie d'un compilateur et courante chez les packers.
Point d'entrée. Les compilateurs placent le point d'entrée dans la section de code. Un stub réside souvent dans sa propre section, fréquemment la dernière.
Imports. Un programme packé importe ce dont le stub a besoin — typiquement LoadLibraryA, GetProcAddress, VirtualProtect, VirtualAlloc, ExitProcess — et résout le reste à l'exécution. Moins de dix imports dont GetProcAddress constituent un signal fort. Cela rend aussi l'imphash inutile pour le regroupement : tous les fichiers packés avec le même outil le partagent.
Taille brute vs taille virtuelle. Une section de 0 octet sur disque mais de grande taille virtuelle (UPX0) est un espace que le stub remplit à l'exécution.
Overlay. Certains packers, installeurs et droppers ajoutent leur charge utile après la dernière section au lieu de l'y inclure.
L'échantillon, lu en statique
Le m64.exe synthétique de l'échantillon de PE Parser présente la plupart de ces signes à la fois, sans danger (son seul code est un ret) : une section nommée UPX1 remplie d'octets aléatoires (entropie 7,96) et marquée RWX, un callback TLS, un overlay de 4 Ko de bruit, et des imports correspondant à des groupes de comportements connus. L'outil signale « UPX » à partir du seul nom de section — il n'y a pas de marqueur UPX! — et indique sur quels éléments repose chaque détection.
Les faux positifs à prévoir
- Les installeurs (NSIS, Inno Setup, archives auto-extractibles) contiennent de gros overlays ou ressources compressés.
- Les logiciels commerciaux protégés et les jeux utilisent VMProtect, Themida ou Denuvo de façon légitime.
- Les binaires Go et Rust sont volumineux et liés statiquement ; ils ne sont pas packés mais présentent des agencements de sections inhabituels.
- Les assemblies .NET n'importent que
mscoree.dll!_CorExeMain; le vrai code est de l'IL stocké dans les métadonnées. - Fichiers signés : une signature valide d'un éditeur connu abaisse la priorité, mais les malwares signés existent.
Et ensuite
- UPX : déposez le fichier dans le PE Parser, qui le décompresse statiquement — même quand l'en-tête
UPX!a été effacé ou les sections renommées pour faire échouerupx -d— et reconstruit les imports d'origine, si bien que l'imphash correspond au build d'origine. Pour les autres packers qui ont un outil de décompression, lancez-le sur une copie et analysez le résultat. - Sinon, décompressez dynamiquement dans une sandbox isolée et faites un dump de la mémoire du processus.
- Consignez aussi les empreintes de l'échantillon packé : les autres victimes en auront la forme packée.
La détection dans PE Parser repose sur un petit ensemble d'heuristiques documentées, écrites pour l'outil — noms de sections, marqueurs, chaînes de runtime, Rich header — et non sur une base de signatures comme celle de Detect It Easy. Considérez-la comme une piste, et lisez le guide du format PE pour savoir où se trouve chaque signe dans le fichier.
FAQ
Comment savoir si un EXE est packé sans l'exécuter ?
Recherchez des sections exécutables à forte entropie, des sections à la fois inscriptibles et exécutables, un point d'entrée hors de .text ou dans la dernière section, très peu d'imports avec LoadLibrary et GetProcAddress, des noms de sections de packer comme UPX0 et UPX1, et des marqueurs de packer comme la chaîne UPX!.
À partir de quelle entropie un fichier est-il packé ?
Une entropie proche de 8 bits par octet signale des données compressées ou chiffrées. Le code compilé se situe généralement entre 5 et 6,8 environ ; au-delà d'environ 7,2 dans une section exécutable, le packing est fortement probable, mais les images, les archives et les certificats ont eux aussi une entropie élevée.