Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Windows PE Malware Static Analyzer 🦠🔍

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).


🔀 Le pipeline en deux phases

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_delta donne 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).

⚙️ Ce que fait l'outil

Phase 1 — Analyse statique

  1. 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_stats indiquant si des plafonds ont été atteints (truncated) — la liste d'IOC n'est jamais silencieusement incomplète.
  2. 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.
  3. 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ête IMAGE_FILE_DLL est 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 (PsSetCreateProcessNotifyRoutine et 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/InternalName du VERSIONINFO, chemin PDB) — que renommer le fichier sur disque ne modifie pas.
    • IOC nt_device_paths : chemins \Device\.../\DosDevices\... côté noyau, forme \\.\Nom côté usermode — c'est ainsi qu'un loader dialogue avec le driver vulnérable qu'il vient de charger.

Phase 2 — Reverse engineering (Rizin + Ghidra)

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 pdg plus 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, et aaa lui-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.

En option

  • Règle YARA (--yara) depuis les IOCs/chaînes extraites — à affiner manuellement. Validées lors de la calibration avec le moteur yara officiel (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 via ps aux.
  • Mode batch multi-fichiers/dossier avec parallélisation (--recursive, --workers).

🛠️ Prérequis & Installation

Inchangé.

Étape 1 : Python et Git

sudo apt update
sudo apt install python3 python3-venv git

Étape 2 : Radare2 (depuis les sources)

git clone https://github.com/radareorg/radare2
radare2/sys/install.sh

Étape 3 : Décompilateur Ghidra (via r2pm)

r2pm -U
r2pm -ci r2ghidra

Étape 4 : Environnement virtuel Python

cd /chemin/vers/le/dossier/du/projet
python3 -m venv env
source env/bin/activate

Étape 5 : Dépendances Python

pip install -r requirements.txt

🚀 Utilisation

Règle d'or inchangée : activer le venv avant chaque lancement (source env/bin/activate).

Cas simple — statique, puis on te demande

python3 win_malware_analyzer.py la_meuh.exe

Statique seulement (triage rapide)

python3 win_malware_analyzer.py la_meuh.exe --reverse skip

Tout enchaîner sans question ni console (CI, scripts)

python3 win_malware_analyzer.py la_meuh.exe --reverse auto --no-interactive

Dossier entier (mode batch, parallélisé)

python3 win_malware_analyzer.py /chemin/vers/dossier_samples --recursive

Options

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)

Codes retour

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)

Comportement non interactif

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.


🔒 Notes de sécurité

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_NONBLOCK puis contrôle S_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ête IMAGE_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.net ne se fait plus écarter à tort, et tempuri.org.attacker.net n'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é.

📊 Calibration

Lot v8.4 — 26 échantillons réels (MalwareBazaar)

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 .sys mais structurellement des DLL GUI ordinaires (aucun import kernel-only) restent correctement classés executable — 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_CAPABILITIES ajoutées) et une DLL déguisée en .exe — dispatcheur DllMain visible 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 yara officiel 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.

Lot v8.2 — 25 binaires réels (EXE/DLL, historique)

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.

⚠️ Limites connues

  • Format : PE uniquement (.exe, .dll, .ocx, .cpl, .sys). Un MSI, un MSIX ou une archive ressort avec score: null et level: 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.

📄 Licence et usage

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.

Releases

Packages

Contributors

Languages