Outil d'analyse statique automatisé, portable, conçu pour les environnements de production (SOC / Blue Team / DFIR). Ce script Python dissèque des binaires Windows sans jamais les exécuter, puis propose une phase de reverse engineering assistée avec console interactive.
Trois familles de PE sont traitées avec leurs spécificités : exécutables (.exe), bibliothèques (.dll, .ocx, .cpl, .ax — imports et exports, sideloading) y compris les assemblys .NET, et pilotes noyau (.sys — capacités WDM/KMDF, recoupement BYOVD contre les pilotes vulnérables connus).
Zéro nouvelle dépendance : uniquement lief et r2pipe (+ rizin/radare2 sur le système).
C'est le changement structurant de la v7 : l'analyse statique et le reverse sont désormais deux étapes distinctes, avec deux rapports JSON séparés.
┌─────────────────────┐
binaire.exe ───▶ │ PHASE 1 : STATIQUE │ ──▶ report_<nom>_<hash8>_static.json
└─────────────────────┘ (+ .yar si --yara)
│
[?] Lancer le reverse ? ──── non ──▶ arrêt propre (code 0)
│ oui
┌─────────────────────┐
│ PHASE 2 : REVERSE │
│ auto → interactif │ ──▶ report_<nom>_<hash8>_reverse.json
└─────────────────────┘ (+ .yar généré en session)
Points importants :
- Le rapport statique est écrit sur disque AVANT le reverse. Si rizin plante, si tu fais Ctrl+C, ou si tu réponds non, le travail d'analyse statique est déjà sauvegardé.
- Les deux JSON sont distinguables par leur suffixe
_static/_reverse, en plus des 8 premiers caractères du SHA256 qui évitent les collisions entre échantillons de même nom. - Le rapport reverse référence le rapport statique (
linked_static_report) et recalcule le score de suspicion en intégrant les signaux du reverse (suspicion_deltadonne le score avant/après). - En cas de Ctrl+C pendant une analyse rizin longue, le rapport reverse partiel est quand même écrit (
"interrupted": true, code retour 130).
-
Métadonnées & IOCs
- MD5, SHA256, entropie globale (détection de packing).
- Chaînes ASCII / UTF-16, avec décodage automatique du Base64 et brute-force XOR mono-octet (fenêtre bornée).
- IOCs : IPv4, IPv6, URLs, domaines (filtrés des faux positifs type
kernel32.dll), emails, wallets crypto (BTC/ETH), clés de registre, chemins Windows suspects, commandes PowerShell/CMD. - Bloc
extraction_statsindiquant si des plafonds ont été atteints (truncated) — la liste d'IOC n'est jamais silencieusement incomplète.
-
Parsing structurel (LIEF)
- En-tête PE : architecture, point d'entrée, ImpHash, empreinte du Rich Header.
- Protections compilées (ASLR, DEP/NX, CFG, SEH).
- Imports classiques et différés (delay-load), croisés avec la table de capacités comportementales.
- Callbacks TLS (anti-sandbox), overlay, ressources (version info, manifest, empreinte d'icône).
- Checksum PE recalculé et comparé au champ déclaré — algorithme réimplémenté depuis la doc publique, à valider sur un échantillon réel avant usage opérationnel (voir avertissement dans le code).
- Vérification Authenticode best-effort (dépend de la version de LIEF).
- Classification exe / dll / driver, plus détection des assemblys .NET.
- Pour un binaire dont la table d'imports est muette (.NET, imports détruits, driver à résolution dynamique), les capacités sont retrouvées dans les chaînes et pondérées à la baisse.
- Capacités .NET managées (
DOTNET_MANAGED_CAPABILITIES) : un malware .NET n'a besoin d'aucun P/Invoke pour exfiltrer par HTTP, persister au démarrage ou chiffrer des fichiers — tout passe par la BCL, dont les noms de types/méthodes vivent en clair dans les métadonnées. - Identité du signataire extraite du certificat (sujet, émetteur, validité) — un certificat forgé ou volé est un signal majeur.
- Analyse des exports (DLL/EXE) : noms, exports sans nom (ordinaux), exports transférés, empreinte pour regrouper les variantes. Un exécutable qui expose des exports est lui-même anormal (motif de DLL déguisée en
.exe— trouvé sur un échantillon réel du lot de test) et compte à charge. - Surface d'exécution d'une DLL : les exports que les hôtes Windows appellent —
DllRegisterServer(regsvr32),ServiceMain(svchost),CPlApplet(control.exe), conventions rundll32,ReflectiveLoader. - Détection de DLL proxy : quand la majorité des exports sont transférés vers une autre bibliothèque, c'est la signature du DLL sideloading.
- Détection de packer par signatures de sections et faisceau d'indices structurels.
- Score de suspicion heuristique (0-100) — aide au tri, ne remplace pas l'analyse manuelle.
-
Pilotes noyau (
.sys)- Classification fiable par import d'une bibliothèque uniquement chargeable en mode noyau (
ntoskrnl.exe,hal.dll,fltmgr.sys...) — bien plus robuste que l'extension sur disque ou le seul sous-système, tous deux imitables. Le drapeau d'en-têteIMAGE_FILE_DLLest exigé en corroboration : un simple import forgé ne suffit pas à se faire (mal-)classer driver. - Capacités noyau dédiées : tamper de notify routines (
PsSetCreateProcessNotifyRoutineet son retrait — signature d'un « EDR killer » noyau), callbacks de protection de handle (ObRegisterCallbacks), accès mémoire noyau arbitraire (MmMapIoSpace,ZwMapViewOfSection...). - Recoupement BYOVD : le nom du binaire est comparé à une liste de pilotes signés mais vulnérables, réellement abusés en attaque (recoupée avec le projet LOLDrivers). Vérifié en priorité dans les métadonnées internes (
OriginalFilename/InternalNamedu VERSIONINFO, chemin PDB) — que renommer le fichier sur disque ne modifie pas. - IOC
nt_device_paths: chemins\Device\.../\DosDevices\...côté noyau, forme\\.\Nomcôté usermode — c'est ainsi qu'un loader dialogue avec le driver vulnérable qu'il vient de charger.
- Classification fiable par import d'une bibliothèque uniquement chargeable en mode noyau (
Passe automatique :
- Xrefs vers les API sensibles (y compris via l'IAT).
- Décompilation du point d'entrée et des fonctions référençant le plus d'API sensibles distinctes, avec le classement exposé dans
priority_functions(selection_reason: "api_xref"). - Filet de sécurité par complexité : sur un échantillon packé ou obfusqué — la majorité du lot de test réel —, la table d'imports est quasi vide et le signal API ne désigne presque aucune fonction ; l'analyste ne recevait alors que le point d'entrée, presque toujours le seul stub de dépaquetage. À budget restant, les fonctions les plus volumineuses (blocs basiques) complètent la sélection (
selection_reason: "complexity_fallback"), plafonnées en taille pour ne pas choisir une fonction condamnée à l'échec (voir ci-dessous). - CFG léger (blocs + arêtes) pour ces fonctions.
- Décompilation bornée dans le temps, indépendamment du moteur : sur un vrai binaire Go du lot de test, une fonction de 550+ blocs basiques (inlining massif du runtime Go) faisait tourner
pdgplus de 90 s sans jamais aboutir.r2ghidra.timeout— le mécanisme natif prévu pour ce rôle — s'est révélé peu fiable en conteneur, rendant carrément toute décompilation muette une fois positionné. Un garde-fou Python indépendant tue le processus rizin sous-jacent si une commande dépasse son délai (pdg,agfj, etaaalui-même).
Console interactive — la session rizin reste ouverte, donc l'analyse aaa n'est pas rejouée : c'est là que se fait le gain de temps réel sur un gros échantillon.
| Commande | Effet |
|---|---|
l [filtre] [n] |
Lister les fonctions (tri par taille) |
top |
Rappeler le classement des fonctions issu de la passe auto |
exp [filtre] |
Lister les exports, surface d'exécution en tête |
d <fonc|0xADDR> |
Décompiler et ajouter au rapport |
s <fonc|0xADDR> [n] |
Désassembler n instructions (défaut 80) |
g <fonc|0xADDR> |
Extraire le CFG et l'ajouter au rapport |
x <API> |
Références croisées vers une API |
str [n] |
Chaînes des sections de données |
str all [n] |
Chaînes de tout le fichier (bruyant : en-têtes, code) |
pick <texte> |
Marquer une chaîne comme signal pour la règle YARA |
picks |
Afficher les chaînes marquées |
note <texte> |
Note d'analyste horodatée |
y |
Générer une règle YARA (chaînes marquées en priorité) |
sum |
Résumé de la session |
? / q |
Aide / quitter et écrire le JSON |
Tout ce que tu fais dans la console est journalisé dans interactive_session : commandes, horodatages UTC, décompilations, CFG, xrefs, notes. Traçabilité d'analyse gratuite.
- Règle YARA (
--yara) depuis les IOCs/chaînes extraites — à affiner manuellement. Validées lors de la calibration avec le moteuryaraofficiel (yara regle.yar echantillon) contre leur échantillon source : outil non requis pour l'analyse, utile pour qui veut reproduire cette vérification. - Lookup VirusTotal par hash, désactivé par défaut (choix OPSEC). Préfère la variable d'environnement
VT_API_KEY: une clé passée en argument est lisible par tout utilisateur de la machine viaps aux. - Mode batch multi-fichiers/dossier avec parallélisation (
--recursive,--workers).
Inchangé.
sudo apt update
sudo apt install python3 python3-venv git
git clone https://github.com/radareorg/radare2
radare2/sys/install.sh
r2pm -U
r2pm -ci r2ghidra
cd /chemin/vers/le/dossier/du/projet
python3 -m venv env
source env/bin/activate
pip install -r requirements.txt
Règle d'or inchangée : activer le venv avant chaque lancement (source env/bin/activate).
python3 win_malware_analyzer.py la_meuh.exe
python3 win_malware_analyzer.py la_meuh.exe --reverse skip
python3 win_malware_analyzer.py la_meuh.exe --reverse auto --no-interactive
python3 win_malware_analyzer.py /chemin/vers/dossier_samples --recursive
| Option | Effet |
|---|---|
-o, --output-dir DIR |
Dossier de sortie des rapports (défaut : dossier courant) |
-r, --recursive |
Parcourt les sous-dossiers en mode batch |
--reverse ask|auto|skip |
ask (défaut) : demande après la phase statique — auto : enchaîne — skip : statique seulement |
--skip-reverse |
Alias historique de --reverse skip |
--no-interactive |
Reverse automatique sans ouvrir la console |
--max-functions N |
Fonctions décompilées en plus de l'entry point (défaut 15) |
--no-cfg |
Désactive l'extraction du CFG léger |
--yara |
Génère une règle .yar à côté du rapport statique |
--vt-key CLE |
Lookup VirusTotal (défaut : VT_API_KEY) |
--timeout S |
Timeout rizin / réseau (défaut 30s) |
--workers N |
Process parallèles en mode batch |
-v / -vv |
Verbosité (INFO / DEBUG) |
| Code | Signification |
|---|---|
0 |
Succès |
1 |
Cible introuvable / aucun fichier à analyser |
2 |
Échec sur au moins un fichier |
130 |
Reverse interrompu au clavier (le rapport partiel est écrit) |
L'invite ne s'affiche jamais si stdin n'est pas un TTY (cron, pipe, CI) ni en mode batch : ask dégrade alors en auto, et aucun worker ne touche à stdin. Tes usages scriptés existants ne changent pas.
Le mode batch est déterminé par la nature de la cible (un dossier) et non par le nombre de fichiers trouvés : un dossier ne contenant qu'un seul échantillon reste traité en lot.
L'outil manipule des binaires hostiles par nature ; plusieurs garde-fous sont en place :
- Liens symboliques refusés. La cible n'est pas suivie si c'est un lien — résous-le avant de le passer en argument. Le refus est appliqué en amont, donc il vaut aussi pour LIEF et rizin, qui ouvrent le fichier par son chemin.
- FIFO, devices, sockets et répertoires refusés (ouverture en
O_NONBLOCKpuis contrôleS_ISREG). - Injection de commande rizin. Les noms de symboles proviennent du binaire analysé, donc de l'attaquant. Les fonctions connues sont ciblées par adresse numérique, jamais par leur nom ; les cibles saisies à la main sont filtrées par une liste blanche stricte qui rejette les métacaractères rizin (
@seek temporaire,$variables,;, backticks, pipes...). Conséquence assumée : un symbole réellement décoré (MSVC, stdcall_foo@8) n'est pas atteignable par son nom — utilise son adresse (d 0x401500). - TOCTOU. Si le fichier change entre la phase statique et la phase reverse, le reverse est annulé : les hashes du rapport ne seraient plus fiables.
- Écriture atomique des rapports (fichier temporaire +
os.replace) : pas de JSON à moitié écrit en cas de Ctrl+C, de disque plein, ou de deux workers batch tombant sur des échantillons dupliqués. - Bornes anti-DoS : plafonds sur le nombre de chaînes analysées, d'IOC par catégorie, d'artefacts de session, et budget temps sur l'extraction IOC. Quand un plafond est atteint, c'est écrit noir sur blanc dans
extraction_stats.truncated. - Classification driver non-usurpable par un signal isolé. Un import forgé vers
ntoskrnl.exe(jamais réellement résolu) ne suffit plus à faire passer un binaire ordinaire pour un pilote noyau et désactiver ainsi les règles de score qui s'appliquent aux exécutables/DLL — le drapeau d'en-têteIMAGE_FILE_DLL, qu'un vrai driver porte toujours, est exigé en corroboration. - Filtres de domaines par frontière, pas par sous-chaîne. Les exclusions « boilerplate » (infrastructure PKI, espaces de noms XML .NET, dépôts de paquets Go/Rust/Node) vérifient une frontière de label plutôt qu'un simple
in: un C2 nomméevil-w3.org-c2.netne se fait plus écarter à tort, ettempuri.org.attacker.netn'est plus traité comme du bruit inoffensif. - Analyse tournée dans un environnement isolé (recommandé) : bien que l'outil n'exécute jamais l'échantillon, il en fait parser les octets par LIEF et rizin — deux analyseurs eux-mêmes exposés à des entrées hostiles. Le lot de test réel de ce projet est systématiquement analysé dans un conteneur Docker jetable, réseau désactivé.
Lot délibérément hétérogène et difficile : 6 vrais pilotes noyau (.sys), un binaire Go, plusieurs binaires signés par un certificat valide (technique d'abus de signature), une DLL Lazarus Group, et le reste en EXE/DLL classiques (droppers, loaders, agents). Analysé dans un conteneur Docker jetable, réseau désactivé, mot de passe d'archive infected.
| Score | Nombre | |
|---|---|---|
minimal (< 15) |
1 / 26 | voir « ce que le score n'est pas » ci-dessous |
faible (15-34) |
7 / 26 | dont un échantillon entièrement packé, zéro import résolu |
moyen (35-59) |
12 / 26 | |
élevé (≥ 60) |
6 / 26 | dont les 4 pilotes aux capacités noyau les plus riches |
- Les 6 pilotes du lot sont tous signés et correctement classés
driver(scores 17 à 100/100 selon les capacités noyau exposées). Deux fichiers portant l'extension.sysmais structurellement des DLL GUI ordinaires (aucun import kernel-only) restent correctement classésexecutable— non-régression vérifiée après le durcissement anti-usurpation de la classification. - Deux faux négatifs confirmés et corrigés en cours de boucle : un malware .NET géré sans le moindre P/Invoke (9/100 → capacités
DOTNET_MANAGED_CAPABILITIESajoutées) et une DLL déguisée en.exe— dispatcheurDllMainvisible en reverse, deux exports non standard (7/100 → 17/100 après ajout de la règle « exécutable exposant des exports »). - Règles YARA auto-générées validées par le moteur
yaraofficiel contre leur échantillon source : 26/26, à trois reprises au fil des corrections. - Le seul échantillon
minimal(6/100) est une application .NET dont les chaînes ne révèlent aucune capacité offensive au sens propre (nom, ressources et flux de contrôle d'un utilitaire ordinaire) : conforme à la limite documentée plus bas — une charge dont le comportement est entièrement résolu ailleurs (second stage distant, par exemple) échappe par construction à l'analyse statique.
22 malveillants (rançongiciels, droppers, loaders, RAT, charges Go signées, agents packés, DLL d'injection, patcheurs AMSI .NET) et 3 sains, dont un installeur Microsoft signé et un utilitaire détecté à tort par une vingtaine d'antivirus.
| Résultat | |
|---|---|
| Malveillants « élevé » | 3 / 21 |
| Malveillants « moyen » ou plus | 18 / 21 |
| Binaires sains | 0, 9 et 29 / 100 |
Un seul malveillant note en dessous du binaire sain le plus haut. Les faux négatifs restants sont un auto-extractible 7-Zip, un packer Delphi et une application Electron de 37 Mo dont la seule capacité visible est BCryptEncrypt — ce dernier reste hors de portée d'une analyse purement statique.
Ce que le score n'est pas. Une heuristique d'aide au tri, pas un verdict. Un score élevé demande une analyse manuelle ; un score faible ne blanchit rien. Chaque point est justifié par une ligne dans suspicion.reasons, précisément pour que l'analyste puisse contester le raisonnement.
- Format : PE uniquement (
.exe,.dll,.ocx,.cpl,.sys). Un MSI, un MSIX ou une archive ressort avecscore: nulletlevel: non_applicable— l'outil ne prétend pas les analyser. - Le score dépend de ce que le binaire expose. Une charge dont les capacités sont entièrement résolues à l'exécution (ou livrées par un second stage distant) ne peut pas être caractérisée statiquement ; l'outil signale alors le packing plutôt que le comportement, voire ne signale rien du tout si le binaire n'a lui-même rien de visible.
- Les données lues dans le binaire sont contrôlées par l'attaquant — noms de sections, d'exports, sujet du certificat, table d'imports déclarée. Les heuristiques qui s'y appuient sont recoupées (émetteur du certificat, cibles de redirection, adresses plutôt que noms de symboles, drapeau d'en-tête en corroboration d'un import pour la classification driver), mais ce recoupement a ses limites.
- La calibration reflète son lot. 25 puis 26 binaires est une base sérieuse, pas une garantie. Les familles encore absentes — infostealer Rust, rootkit userland, conteneur MSIX — n'ont pas été éprouvées.
- La liste de pilotes vulnérables connus (BYOVD) est une liste curée, pas la base LOLDrivers complète : une correspondance de nom est un indice fort, son absence ne blanchit pas un driver inconnu de la liste.
- La signature Authenticode est vérifiée par LIEF, sans confrontation au magasin de certificats du système : une chaîne auto-signée peut être cohérente en interne. C'est pourquoi l'émetteur est confronté à une liste d'autorités reconnues.
Outil d'analyse statique : aucun échantillon n'est exécuté à aucun moment. Les rapports sont écrits en 0600 — ils contiennent du code extrait de binaires malveillants. Le lookup VirusTotal est désactivé par défaut : l'activer transmet l'empreinte de l'échantillon à un tiers.