Un ami, le genre qui a toujours « un bon plan », m'envoie un lien un soir avec la phrase qui va avec : « vas-y les yeux fermés, tout est safe là-dedans. » Le site proposait des logiciels créatifs à des prix dérisoires. Pas d'abonnement, pas de licence, pas de mise en garde. Juste un lien et une promesse.
J'aurais pu ignorer. À la place, j'ai téléchargé le premier logiciel qui m'est venu à l'esprit, Sapphire, un plugin After Effects à plusieurs centaines d'euros, le genre de ligne budgétaire qui se négocie avec soi-même pendant six mois. Pas pour l'utiliser. Pour l'analyser.
Ce qui suit, c'est ce que j'ai trouvé. Et surtout les deux endroits où je me suis planté en route, parce qu'ils sont plus instructifs que le verdict.
Pourquoi les logiciels crackés sont une cible privilégiée
Ce n'est pas une intuition, c'est mesuré. Le centre de threat intelligence d'AhnLab relève que les cracks, keygens et téléchargements « gratuits » sont restés en juin 2025 le principal mode de diffusion des infostealers, avec un empoisonnement SEO systématique pour faire remonter les faux portails devant les sources légitimes. Même constat côté Recorded Future dans son rapport 2025 sur l'économie des infostealers : chaque machine compromise livre en moyenne 87 identifiants.
Le profil visé est cohérent. Quelqu'un d'assez à l'aise techniquement pour installer un plugin, mais sans réflexe de sécurité particulier sur ce qu'il exécute. Parmis les mécaniques qui reviennent, il y en a une particulièrement efficace : l'installation demande souvent de désactiver l'antivirus « pour que ça fonctionne ». Le malware réclame lui-même les conditions de son exécution, et on les lui accorde.
Avant de toucher à quoi que ce soit
La première règle quand on reçoit un fichier douteux, c'est de ne rien exécuter avant d'avoir une idée de ce qu'on a en face. Dans mon cas, l'ISO faisait 5,5 Go. Première vérification réflexe : le hash.
Si la source publie un SHA256 ou un MD5, on compare :
sha256sum fichier.iso
Ici, aucun hash officiel. Ce n'est pas une surprise, on n'est pas sur le site de l'éditeur. Mais c'est déjà une information : impossible de vérifier l'intégrité sans référence.
Étape suivante, regarder ce que l'ISO contient sans le monter. isoinfo liste le système de fichiers d'une image ISO sans rien exécuter. Le premier appel liste les fichiers, le second sort les métadonnées de l'image (label, éditeur, date de création) :
isoinfo -l -i fichier.iso
isoinfo -d -i fichier.iso
Résultat : 4 fichiers .exe et 4 fichiers .nfo. Les .nfo sont les fichiers texte de présentation typiques des groupes de crack, c'est le profil d'un release warez classique. Rien d'inattendu pour l'instant, mais rien de rassurant non plus.
ClamAV, et le piège du faux négatif
ClamAV est l'antivirus open source de référence sous Linux. Ce qu'on fait d'instinct, c'est lancer un scan sur l'ISO directement :
clamscan --recursive --scan-archive=yes fichier.iso
Résultat :
Infected files: 0
Data scanned: 0.00 MB
Data read: 5638.92 MB (ratio 0.00:1)
« 0 infecté ». ClamAV m'a répondu ça avec l'aplomb de quelqu'un qui n'a pas ouvert le dossier. Parce que la ligne qui compte, c'est Data scanned: 0.00 MB : il a lu 5,6 Go et scanné zéro octet. Il ne sait pas extraire un système de fichiers ISO tout seul. Le résultat est techniquement vrai et pratiquement inutile.
La bonne approche, monter l'ISO en lecture seule d'abord, puis scanner le point de montage :
mkdir /tmp/iso_mount
sudo mount -o loop,ro fichier.iso /tmp/iso_mount
sudo clamscan --recursive /tmp/iso_mount/
Le flag ro est important : rien ne s'exécute, on accède uniquement au système de fichiers en lecture. Le scan reconnaît cette fois les 8 fichiers. Et Data scanned reste désespérément à 0.00 MB.
Sur le coup j'ai mis ça sur le dos d'un problème de permissions du loop mount. C'était faux. La vraie raison est bien plus intéressante, mais il me manquait encore une information pour la comprendre. J'y reviens plus bas.
strings, ou comment lire sans exécuter
strings extrait toutes les chaînes de caractères lisibles dans un binaire. C'est une des premières choses à faire sur un exécutable suspect : sans lancer une seule instruction, on peut souvent lire l'intention du code.
strings /tmp/iso_mount/fichier.exe | grep -iE "(http|cmd|hkey|powershell|createprocess)"
Ce que j'ai obtenu m'a arrêté net. Des centaines de variations de la chaîne cmd :
CmD
cMd
CMdPV
WobxCmd
cMDU2
CMD/f
cmDbrw&!
Et quelques occurrences du même genre sur HTTP et sur les clés de registre Windows : HttPS, HtTP, xhttP, hkEy, HKey, hkeyG2.
L'interprétation vient immédiatement. En environnement Windows, cMd, CMD et cmd exécutent tous cmd.exe, la casse n'a aucune importance pour l'interpréteur. Écrire cMdPV au lieu de cmd ne change rien à l'exécution mais rend la chaîne illisible pour un scanner à signatures. MITRE ATT&CK classe ça sous T1027.010, Command Obfuscation , et ça collerait parfaitement.
J'ai noté ça comme un indice fort et je suis passé à la suite. C'était une erreur, mais pour la voir il fallait d'abord regarder la taille des fichiers.
Anatomie d'un PE et le binary padding
Les exécutables Windows suivent un format standardisé appelé PE (Portable Executable). Ce format décrit la structure du fichier : ses sections de code, de données, ses ressources embarquées, et la liste des fonctions qu'il importe depuis d'autres DLLs. On peut lire tout ça sans exécuter quoi que ce soit.
Avant ça, un détail qui méritait attention : chaque .exe pesait 1,4 Go. Pour un plugin d'effets visuels, c'est le poids d'une installation complète de jeu vidéo pour un truc censé manipuler des pixels. pefile , un module Python sans dépendances, permet d'inspecter la structure réelle du fichier. On compare la fin de la dernière section déclarée avec la taille réelle sur disque :
import pefile, os
pe = pefile.PE("fichier.exe")
last = pe.sections[-1]
real_size = last.PointerToRawData + last.SizeOfRawData
file_size = os.path.getsize("fichier.exe")
print(f"Taille fichier : {file_size / 1024**2:.1f} MB")
print(f"Taille PE réelle : {real_size / 1024**2:.1f} MB")
print(f"Padding : {(file_size - real_size) / 1024**2:.1f} MB")
Résultat :
Taille fichier : 1411.5 MB
Taille PE réelle : 0.9 MB
Padding : 1410.6 MB
Le binaire réel fait moins d'1 Mo. Les 1 410 Mo restants sont du binary padding, des données ajoutées artificiellement après la fin du PE valide. La technique est documentée sous T1027.001 , et MITRE en donne exactement le mobile : augmenter la taille d'un fichier réduit l'efficacité des outils qui ne sont pas conçus pour scanner de gros fichiers, et diminue la probabilité qu'il soit collecté pour analyse.
La contrainte structurante est du côté des plateformes. VirusTotal plafonne les soumissions à 650 Mo, et sa documentation explique pourquoi elle ne veut pas de gros fichiers :
Au-delà de cette taille, les fichiers sont généralement des conteneurs (archives, images ISO). Mieux vaut soumettre les fichiers internes : certains moteurs ont des problèmes de performance sur les gros fichiers, d'autres ne savent pas inspecter certains types, et sur un gros paquet on perd le contexte de quel fichier interne déclenche la détection.
Documentation VirusTotal, Should I upload files larger than 650MBs? , traduit et condensé
Le calcul de l'attaquant est donc trivial : 1,4 Go, et le fichier ne franchit plus la porte d'aucun analyseur automatisé. Il existe des outils dédiés au dégonflage, mais un head -c sur la taille PE calculée plus haut suffit :
head -c 943718 fichier.exe > fichier_tronque.exe
Je ne connaissais pas vraiment cette technique avant de faire mes recherche sur "comment vérifier la taille réelle d'un exe". Je suis un peu tombé sur le cul en découvrant que l'entièreté de mon plugin n'était qu'un fiefé mensonge. C'est honteux et confus, que je jura presque trop tard, que l'on ne m'y reprendrais plus !
Ce chiffre de 1 410 Mo ne règle pas seulement la question de VirusTotal. Il invalide rétroactivement les deux étapes précédentes.
Retour sur ClamAV
Le Data scanned: 0.00 MB après montage n'était pas une histoire de permissions. Ce sont les limites de taille par défaut de ClamAV. clamscan applique un --max-filesize de 25 Mo et un --max-scansize de 100 Mo, précisément comme garde-fou contre les archive bombs. Un .exe de 1,4 Go n'est pas scanné : il est ignoré, silencieusement, et ressort comme un fichier propre.
L'option qui rend ça visible existe, et c'est celle qu'il faudras prendre le réflexe d'ajouter :
clamscan --alert-exceeds-max=yes --max-filesize=2G --max-scansize=4G /tmp/iso_mount/
Le binary padding ne sert donc pas qu'à passer sous le radar des plateformes en ligne. Il neutralise aussi l'antivirus local avec ses réglages d'usine. Deux protections différentes désactivées par la même astuce, et aucune des deux ne prévient qu'elle a renoncé.
Retour sur strings
Le second faux positif est plus embarrassant, parce que c'est moi qui l'ai produit. Ce strings | grep tournait sur le fichier complet. Donc sur 1,4 Go de remplissage, et sur seulement 0,9 Mo de binaire réel. Autrement dit, 99,9 % de ce que strings a lu ne venait pas du programme.
La question devient donc : sur un tel volume de données arbitraires, combien de correspondances attend-on par pur hasard ? C'est mesurable, et ça ne demande aucun échantillon de malware. On génère la même quantité de bruit, et on lui applique le grep de tout à l'heure :
head -c 1479065600 /dev/urandom > bruit.bin
strings bruit.bin | grep -icE "cmd"
Sur 1 410,5 Mio de données aléatoires, sans le moindre code exécutable dedans :
| Motif | Correspondances dans du bruit pur |
|---|---|
cmd |
404 |
http |
4 |
hkey |
1 |
powershell |
0 |
createprocess |
0 |
404 pour cmd. « Des centaines de variations », exactement ce que j'avais observé, produites par des données qui ne contiennent rien.
Le calcul le confirme. Un motif de trois caractères insensible à la casse a une probabilité de (2/256)³ par position, soit environ 423 occurrences attendues sur ce volume. La mesure tombe à 404. Un motif de quatre caractères comme http est 128 fois plus rare, d'où l'écart entre 404 et 4.
Mais le diagnostic qui tranche vraiment, c'est la distribution de casse. Sur le bruit :
59 cmd 58 CMd 52 CmD 51 CMD
47 Cmd 46 cmD 46 cMd 45 cMD
Les huit casings possibles, équiprobables. C'est la signature du hasard. Un obfuscateur ne produit pas ça : il génère des variantes selon sa propre logique, et la distribution est forcément déséquilibrée, avec des formes surreprésentées et d'autres absentes. C'est donc le test à faire sur le binaire tronqué :
strings fichier_tronque.exe | grep -ioE "cmd" | sort | uniq -c | sort -rn
Une réserve, et il faut la poser clairement. Tout ceci suppose que le padding est à haute entropie. S'il s'agissait d'octets nuls ou d'un motif répété, strings n'en tirerait rien, le bruit de fond serait nul, et les correspondances viendraient bien du binaire. La commande qui tranche tient en une ligne :
tail -c +943718 fichier.exe | head -c 10000000 | gzip -c | wc -c
Dix mégaoctets qui ressortent à dix mégaoctets, c'est de l'aléatoire. Quelques kilooctets, c'est du remplissage structuré.
Je suis parvenu à cette idée de commande un peu tard, je n'avais plus le fichier (j'ai commencé la rédaction de cet article il y a un peu trop longtemps). C'est une faute de méthode et autant la dire, parce que la première chose à faire avec un échantillon, avant même de l'ouvrir, c'est de le conserver et d'en noter le SHA256. Une analyse qu'on ne peut plus rejouer n'est pas une analyse, c'est un souvenir.
Cela dit, trois indications convergent.
Le volume. Le binaire réel fait 0,9 Mo. Au taux mesuré sur du bruit, 0,29 correspondance par Mio, il devrait en produire 0,26 par hasard. Pour que "des centaines" viennent de lui, il faudrait qu'un obfuscateur ait semé plusieurs centaines de littéraux cmd en casse mixte dans moins d'un mégaoctet de code. Ce n'est pas ainsi que fonctionne l'obfuscation de commandes, qui porte sur la ligne construite à l'exécution, pas sur des chaînes dispersées dans les données.
La casse. Sur les sept variantes que j'avais relevées, on trouve sept casings distincts (CmD, cMd, CMd, Cmd, cMD, CMD, cmD) sur les huit possibles, chacun une seule fois. C'est la distribution plate du hasard. J'ai choisi ces exemples pour l'illustration, le biais de sélection existe donc, mais un obfuscateur aurait plutôt tendance à répéter ses formes.
Le voisinage. CMdPV, cMDU2, cmDbrw&! sont structurellement identiques à ce que crache le bruit : CMDPW, cmDq, cMd}=. Des caractères adjacents sans aucune signification, soit le contraire de ce qu'on attend d'une chaîne posée intentionnellement.
Rien de tout ça ne remplace la commande gzip. Mais l'hypothèse du bruit explique l'ensemble des observations, là où celle de l'obfuscation exige une densité invraisemblable. Je la classe comme écartée sans pouvoir la fermer, et c'est une nuance que je préfère laisser visible plutôt que de trancher pour faire propre.
Ce que je retiens de cette étape : un indicateur mesuré sur un volume de données qu'on ne maîtrise pas ne vaut rien tant qu'on n'a pas établi son bruit de fond. J'avais un signal. Je n'avais pas d'échelle pour le lire.
La table des imports, ou le programme qui se raconte lui-même
Une fois le fichier tronqué à sa taille réelle, on peut enfin travailler sur le vrai binaire. Et la partie la plus révélatrice d'un PE, c'est sa table des imports : la liste des fonctions qu'il appelle depuis les DLLs système. C'est, en quelque sorte, la liste de tout ce que le programme peut faire.
pe = pefile.PE("fichier_tronque.exe")
for lib in pe.DIRECTORY_ENTRY_IMPORT:
print(lib.dll.decode())
for imp in lib.imports:
if imp.name:
print(f" {imp.name.decode()}")
Ce que ça a donné, section par section.
Élévation de privilèges, depuis advapi32.dll :
AdjustTokenPrivileges
LookupPrivilegeValueW
OpenProcessToken
GetTokenInformation
Le binaire cherche à obtenir des droits administrateur. Ce n'est pas une fonctionnalité d'un plugin d'effets visuels.
Injection de code en mémoire, depuis kernel32.dll :
VirtualAlloc
VirtualProtect
SuspendThread
ResumeThread
VirtualQueryEx
Ce combo est la signature classique d'une injection de processus : allouer de la mémoire dans un autre processus, y écrire du code, le rendre exécutable, puis le lancer. SuspendThread et ResumeThread servent à figer le processus cible pendant l'injection.
Chargement dynamique pour cacher ses capacités :
LoadLibraryA
LoadLibraryW
GetProcAddress
Plutôt que de déclarer explicitement ses imports, ce qui les rendrait visibles dans la table, le binaire résout ses propres dépendances à l'exécution. Les scanners statiques ne voient que ce qui est déclaré, le reste reste caché jusqu'au lancement. MITRE référence ce comportement sous T1027.007, Dynamic API Resolution .
Payload embarqué dans les ressources :
FindResourceW
LoadResource
LockResource
SizeofResource
La section .rsrc du PE fait 71 Ko. Ces quatre fonctions servent à extraire et charger du contenu embarqué dans les ressources du binaire. C'est le mécanisme typique d'un dropper : le vrai payload est là, en attente d'être extrait et exécuté.
Reconnaissance réseau avec NetWkstaGetInfo, depuis netapi32.dll. Cette fonction renvoie le nom de la machine, le domaine Windows et la version du système. De la reconnaissance locale, utilisée pour identifier l'environnement avant de déclencher le payload, ou pour envoyer ces informations à un serveur distant.
Modification du registre avec RegOpenKeyExW et RegQueryValueExW. Persistance probable au démarrage via une clé Run.
Un point d'honnêteté sur cette section. Une table d'imports décrit un potentiel, pas un comportement. VirtualAlloc est appelé par des milliers de programmes parfaitement sains. Ce qui fait signal ici, c'est la combinaison, et le fait qu'elle n'ait aucun rapport avec la fonction annoncée du logiciel. Contrairement aux strings, ce faisceau ne peut pas se produire par accident : chaque nom est déclaré explicitement par le compilateur, il n'y a pas de bruit de fond possible.
Ce que la sandbox avait montré
L'analyse statique donne une image précise de ce qu'un binaire peut faire. Ce qu'elle ne dit pas, c'est ce qu'il fait réellement à l'exécution. Pour ça il existe des sandboxes en ligne : des environnements Windows isolés qui lancent le fichier et observent son comportement, connexions réseau ouvertes, processus créés, clés de registre modifiées, fichiers touchés.
J'avais soumis le binaire tronqué sur Hybrid Analysis . Et je dois écrire cette section au passé, parce que le rapport a disparu avec le reste : ni lien, ni hash, ni capture. Ce qui suit est un souvenir, pas une source, et je préfère le dire ici plutôt que de le laisser passer pour établi.
De ce que j'en ai retenu, le binaire ne se contentait pas de s'installer. Il prenait des captures d'écran, agrégeait des données locales avant de les exfiltrer, et se supprimait après exécution. Il embarquait aussi des mécanismes de détection d'environnement d'analyse, machine virtuelle, sandbox, débogueur, avec adaptation du comportement en conséquence. Le rapport listait plusieurs dizaines de techniques ATT&CK, de la reconnaissance système à l'exfiltration programmée.
Tout ça est cohérent avec la table des imports, et c'est bien le problème : c'est précisément quand un souvenir confirme ce qu'on attendait qu'il faut s'en méfier. Je n'ai aucun moyen de savoir si je me rappelle du rapport ou de la conclusion que j'en avais tirée. Cette section ne porte donc rien dans le verdict. Elle raconte une étape, elle ne prouve rien.
Ce qui reste vrai indépendamment, c'est la limite structurelle de l'exercice. Un rapport de sandbox décrit ce que le binaire a fait dans cet environnement-là. Un malware qui embarque de la détection de sandbox est par définition un malware dont le rapport de sandbox est incomplet. Même avec le lien sous la main, la certitude n'aurait pas été totale.
Le verdict
| Technique | Indicateur observé | Fiabilité |
|---|---|---|
| Bypass des plateformes en ligne | 1 410 Mo de padding artificiel | Certaine |
| Contournement de l'antivirus local | Dépassement de max-filesize / max-scansize |
Certaine |
| Élévation de privilèges | AdjustTokenPrivileges |
Import déclaré |
| Injection de processus | VirtualAlloc + SuspendThread |
Import déclaré |
| Résolution dynamique | LoadLibrary + GetProcAddress |
Import déclaré |
| Payload embarqué | .rsrc 71 Ko + FindResource |
Import déclaré |
| Reconnaissance locale | NetWkstaGetInfo |
Import déclaré |
| Persistance | Registre Windows | Import déclaré |
| Exfiltration et anti-analyse | Rapport de sandbox | De mémoire, non vérifiable |
| Obfuscation de commandes | Variations de casse dans strings |
Écartée, très probablement du bruit |
Une fois retirées les lignes qui ne tiennent plus, il reste une table d'imports. Et elle suffit. Un binaire qui demande des privilèges administrateur, alloue de la mémoire dans un processus tiers, résout ses dépendances à l'exécution, embarque 71 Ko de ressources qu'il sait extraire, identifie la machine et touche au registre, ce n'est pas un plugin d'effets visuels avec une signature abîmée. C'est un dropper, un exécutable dont le rôle est d'installer autre chose.
Que cet autre chose ait été un infostealer, je le crois, et le souvenir du rapport de sandbox va dans ce sens. Mais je ne peux plus le démontrer, et un article qui vient de consacrer une section entière à la différence entre un signal et du bruit ne va pas terminer en s'asseyant sur un souvenir.
Tout ça, hash, isoinfo, ClamAV correctement paramétré, strings, pefile, tient en moins d'une heure sur n'importe quelle machine Linux, avec des outils gratuits et documentés, sans exécuter une seule instruction du binaire. La seule étape qui a demandé une machine tierce, c'est la sandbox.
L'ami qui m'avait dit que le site était « 100 % safe », je ne lui en veux pas. Il y allait probablement les yeux fermés depuis des mois sans le moindre soucis visible, et c'est exactement ce que ce type de malware est conçu pour produire : rien. Pas de ralentissement, pas de popup, pas de fichier chiffré. Juste des identifiants qui partent, et une machine qui répond à quelqu'un d'autre.
Ce qui me reste de cette analyse, ce n'est pas le verdict, il était prévisible. C'est que rien de ce que j'ai lancé ne m'a averti de ses propres angles morts. ClamAV a dit « propre » parce qu'il avait renoncé à lire. strings a dit « obfuscation » parce que je lui avais donné un gigaoctet de hasard à digérer. Et moi j'ai dit « infostealer » en m'appuyant sur un rapport que je n'ai pas archivé. Trois affirmations confiantes, trois fois personne pour signaler ce qui manquait.
Un outil qui ne dit rien ne dit pas que tout va bien. Il dit qu'il n'a pas regardé, et c'est à vous de savoir lequel des deux.