Leçon 10.2 · Au-delà de l'EXE· 45 min
Analyser un malware .NET
Reconnaître un assembly .NET, lire ses métadonnées et son IL, choisir le bon décompilateur et venir à bout des obfuscateurs et des étapes en mémoire.
Objectifs
- Reconnaître un assembly .NET à son en-tête CLR, à son import de mscoree et à la sortie de Detect It Easy
- Expliquer ce que contiennent les flux de métadonnées CLR et l’IL, et pourquoi le code managé se décompile en un quasi-code source
- Choisir entre dnSpyEx, ILSpy, de4dot et dnfile selon la tâche d’analyse
- Venir à bout de l’obfuscation des noms, du chiffrement des chaînes, de l’obfuscation du flot de contrôle et des étapes Assembly.Load en mémoire
- Localiser les valeurs de configuration d’un échantillon .NET et les consigner dans un rapport
Une large part des malwares (logiciels malveillants) grand public — chevaux de Troie d’accès à distance (RAT), info-stealers, loaders, crypters — est écrite en C# ou en VB.NET. Pour un analyste, c’est plutôt une bonne nouvelle. Un programme .NET n’est pas compilé en instructions x86 mais en un bytecode intermédiaire, et il transporte une description détaillée de ses propres types et méthodes. Avec le bon outil, vous lisez quelque chose de très proche du code source d’origine, souvent en quelques minutes.
La mauvaise nouvelle, c’est que les auteurs de malwares le savent aussi. Presque tous les échantillons .NET rencontrés dans la nature sont passés par un obfuscateur, et beaucoup ne sont qu’une mince première étape qui déchiffre et charge la vraie charge utile en mémoire. Cette leçon montre comment reconnaître du code managé, ce que contient réellement le fichier, quels outils utiliser et comment franchir les obstacles qui se dressent entre vous et la configuration.
Reconnaître un assembly .NET
Un exécutable .NET reste un fichier PE, donc tout ce qui a été vu dans En-têtes PE s’applique. Trois signes vous indiquent qu’il s’agit de code managé :
| Signe | Où regarder | Ce que vous voyez |
|---|---|---|
| En-tête CLR Runtime | Répertoire de données 14 | Une RVA non nulle et une taille de 72 octets (IMAGE_COR20_HEADER) |
| Import de mscoree | Table des imports | Un seul import : _CorExeMain (EXE) ou _CorDllMain (DLL) depuis mscoree.dll |
| Verdict de l’outil | Detect It Easy | « Library: .NET » avec une version du framework, souvent aussi le nom d’un obfuscateur |
Cet import est celui contre lequel mettait en garde Ressources, overlays et
autres cachettes : le point
d’entrée natif est un stub qui saute vers
_CorExeMain, lequel passe la main au Common Language Runtime (CLR). Sur les
Windows modernes, le loader reconnaît l’en-tête CLR et n’exécute même pas le
stub. Dans un cas comme dans l’autre, le désassembler ne vous apprend rien. Un
échantillon dont la table des imports se résume à une seule fonction de
mscoree.dll n’est pas « étrangement minimal » comme le serait un fichier natif
packé — il est simplement managé. Considérez que le triage fondé sur les
imports habituel ne s’applique pas, et changez
d’outils.
Deux variantes méritent d’être signalées. Les bundles monofichiers produits par le .NET moderne sont un exécutable hôte natif auquel sont ajoutés des assemblies managés ; Detect It Easy les signale, et vous devez extraire les assemblies intégrés avant de décompiler (ILSpy sait ouvrir ces bundles). Les builds Native AOT sont compilés jusqu’au code machine et ne contiennent pas d’IL : ils se présentent et s’analysent comme n’importe quel binaire natif, et un décompilateur .NET ne peut rien pour vous.
Astuce : Le verdict .NET de Detect It Easy nomme généralement aussi l’obfuscateur ou le protecteur (ConfuserEx, .NET Reactor, SmartAssembly, Eazfuscator et bien d’autres). Notez ce nom en premier : il décide de votre heure suivante.
Ce que contient le fichier : métadonnées et IL
L’en-tête CLR pointe vers deux choses : les métadonnées et les corps de
méthodes, qui sont du bytecode Common Intermediate Language (CIL ou IL). La
racine des métadonnées (signature BSJB) est suivie d’une poignée de flux :
| Flux | Contenu | Intérêt pour l’analyste |
|---|---|---|
#~ | Tables de métadonnées : TypeDef, MethodDef, Field, MemberRef, TypeRef, ManifestResource, Constant… | La structure complète du programme |
#Strings | Identifiants UTF-8 : noms de types, de méthodes, de champs et d’espaces de noms | Des noms lisibles (sauf s’ils ont été renommés) |
#US | « User strings » : chaque littéral chargé par ldstr, en UTF-16 | URL, chemins, clés, configuration encodée |
#GUID | GUID, dont l’identifiant de version du module (MVID) | Regroupement (clustering) |
#Blob | Signatures, valeurs constantes, arguments d’attributs personnalisés | Constantes et attributs tels que le GUID TypeLib |
Les métadonnées existent parce que le runtime en a besoin. Le CLR vérifie les
types, résout les appels par leur nom d’un assembly à l’autre, prend en charge
la réflexion et compile à la volée (JIT, just-in-time) chaque méthode lors de
sa première utilisation. Chaque classe, signature de méthode, champ et cible
d’appel reste donc dans le fichier. Les appels au framework passent par la table
MemberRef sous la forme System.Convert::FromBase64String ou
System.Reflection.Assembly::Load — cette table joue le rôle que la table des
imports joue pour le code natif, avec bien plus de précision.
L’IL lui-même est une machine à pile : ldstr empile une chaîne, call dépile
des arguments et empile un résultat, stloc range une valeur dans un
emplacement local. Les compilateurs l’optimisent à peine, puisque le JIT s’en
charge plus tard. Ajouté à une information de types complète, c’est ce qui
permet aux décompilateurs de retrouver un C#
structurellement proche du source — l’inverse de la situation décrite dans Les
décompilateurs et leurs limites, où types et
signatures devaient être reconstruits à la main. Ce qui se perd est minime : les
noms des variables locales (stockés uniquement dans des fichiers PDB séparés) et
les commentaires.
Les références au sein de l’IL sont des jetons de métadonnées (metadata
tokens) : une valeur de 32 bits dont l’octet de poids fort désigne la table et
dont les 24 bits de poids faible désignent la ligne. 0x06000004 est la ligne 4
de MethodDef (table 0x06) ; 0x70000001 est l’offset 1 dans le tas #US.
Vous verrez ces jetons dans dnSpyEx, dans les options de de4dot et dans vos
notes.
Les outils et quand utiliser chacun
| Outil | Type | À utiliser pour |
|---|---|---|
| dnSpyEx | Décompilateur, débogueur, éditeur (Windows) | Établi principal : lire le C#, poser des points d’arrêt dans le code décompilé, inspecter les variables locales, dumper des modules, patcher des méthodes |
| ILSpy | Décompilateur (multiplateforme) | Lecture propre et export d’un assembly entier sous forme de projet C# ; second avis quand la sortie de dnSpyEx paraît étrange |
| de4dot | Désobfuscateur (ligne de commande) | Détecter et retirer les obfuscateurs connus : renommage, déchiffrement des chaînes, nettoyage du flot de contrôle |
| dnfile | Bibliothèque Python | Triage scripté et extracteurs de configuration : tables, #US, ressources, à grande échelle |
| monodis / ildasm | Désassembleurs IL | L’IL brut quand le décompilateur masque ou déforme quelque chose |
dnSpyEx est le fork communautaire maintenu du dnSpy d’origine, archivé en 2020. Son débogueur explique sa domination dans l’analyse de malwares : vous déboguez au niveau du C#, sans le code source, et pouvez vous arrêter dans n’importe quelle méthode que vous savez lire. Le moteur de décompilation d’ILSpy est d’ailleurs celui sur lequel s’appuie dnSpyEx, si bien que leurs sorties se ressemblent ; ILSpy a tendance à suivre en premier les nouvelles fonctionnalités du langage C#.
de4dot est archivé et n’est plus mis à jour, et il est en retard sur les versions actuelles des protecteurs commerciaux. Il reste la victoire la plus rapide lorsqu’il reconnaît l’obfuscateur, et des forks existent pour certains protecteurs. Exécutez-le sur une copie et conservez l’original pour le hachage et le rapport.
Astuce : N’exécutez jamais un échantillon « pour voir ce qu’il fait » dans dnSpyEx en dehors de votre laboratoire d’analyse. Le débogueur exécute le vrai code. Posez vos points d’arrêt avant d’appuyer sur Start.
Les obstacles et comment les analystes les franchissent
Obfuscation des noms
Les renommeurs remplacent chaque identifiant par des noms dénués de sens,
non imprimables ou délibérément trompeurs — des sosies Unicode, des noms qui ne
diffèrent que par des caractères invisibles, des dizaines de surcharges portant
le même nom. Le code s’exécute quand même, car le CLR résout par jeton et non par
nom lisible. Les appels au framework ne peuvent pas être renommés, si bien que
les entrées MemberRef telles que HttpWebRequest, RegistryKey::SetValue ou
Assembly::Load restent vos points d’ancrage. de4dot réécrit les noms en
substituts lisibles (Class3, method_7) ; à partir de là, vous renommez au fil
de votre compréhension, exactement comme en analyse native.
Chiffrement des chaînes
L’obfuscateur remplace chaque ldstr par un appel tel que
Class5.smethod_2(1842) qui déchiffre à l’exécution une chaîne tirée d’un blob
chiffré. Le tas #US ne contient alors plus rien d’utile — l’équivalent du
chiffrement de chaînes par XOR en code
natif. Trois façons de s’en sortir, par ordre de préférence :
- de4dot, s’il reconnaît l’obfuscateur, déchiffre et réinjecte les chaînes automatiquement.
- de4dot avec un déchiffreur désigné : donnez-lui le jeton de la méthode de
déchiffrement (
--strtyp delegate --strtok 0x06000123) et il appelle cette méthode pour chaque site d’appel. Cela exécute du code de l’échantillon : ne le faites donc que dans le laboratoire. - Débogueur : posez un point d’arrêt sur le
returndu déchiffreur dans dnSpyEx et lisez le résultat dans la fenêtre Locals, ou arrêtez-vous une fois que tout le bloc de paramètres a été déchiffré.
Obfuscation du flot de contrôle
Les corps de méthodes sont réécrits en un switch à l’intérieur d’une boucle
pilotée par une variable d’état, avec des prédicats
opaques et des branches parasites — de
l’aplatissement du flot de contrôle
appliqué à l’IL. Le décompilateur produit toujours du C# valide, mais une
méthode de 20 lignes en fait 200. de4dot défait les schémas courants. Quand il
n’y parvient pas, ne lisez pas la machine à états : déboguez à travers elle et
observez quels appels ont réellement lieu.
Corps de méthodes chiffrés
Certains protecteurs (le mode anti-tamper de ConfuserEx en est l’exemple classique) chiffrent l’IL de chaque méthode et le déchiffrent dans le constructeur statique du module, avant que quoi que ce soit d’autre ne s’exécute. Le décompilateur affiche des corps vides ou invalides. Laissez le constructeur s’exécuter sous le débogueur, puis dumpez le module depuis la mémoire : le fichier dumpé contient des corps déchiffrés qui se décompilent normalement.
Ressources contenant d’autres étapes
Les assemblies .NET transportent des ressources de manifeste, listées dans
la table ManifestResource et lues à l’exécution avec
GetManifestResourceStream. Certaines sont des conteneurs .resources qui
renferment des entrées nommées (images, chaînes, tableaux d’octets) ; d’autres
sont des blobs bruts. Les loaders y cachent l’étape suivante — parfois dans les
données de pixels d’un bitmap —, chiffrée, compressée ou les deux. Extrayez
chaque ressource, vérifiez sa taille et son entropie, et trouvez le code qui la
lit en recherchant le nom de la ressource ou les appels à
GetManifestResourceStream et ResourceManager.
Charges utiles chargées en mémoire
Le geste caractéristique des loaders .NET est Assembly.Load(byte[]) :
déchiffrer un tableau d’octets, le charger comme assembly sans toucher au
disque, trouver son point d’entrée par réflexion et l’appeler avec Invoke.
C’est le cousin managé de l’injection réflexive de
DLL, et l’étape suivante n’existe
jamais sous forme de fichier que vous pourriez hacher.
La parade consiste à intercepter les octets. Dans dnSpyEx, soit vous posez un
point d’arrêt sur Assembly.Load et enregistrez l’argument byte[] depuis la
fenêtre Locals (clic droit, Save), soit vous laissez l’appel se terminer et
ouvrez Debug → Windows → Modules : les assemblies en mémoire apparaissent
dans la liste et peuvent être enregistrés sur disque d’un clic droit.
L’assembly dumpé est alors un nouvel échantillon — triez-le depuis le début.
Certaines étapes s’enchaînent sur trois ou quatre niveaux ; recommencez jusqu’à
atteindre du code qui fait autre chose que charger du code.
Vérifications anti-analyse
Les malwares managés vérifient Debugger.IsAttached, appellent
IsDebuggerPresent via P/Invoke, cherchent des artefacts de sandbox (bac à
sable) ou dorment avant d’agir. Ces vérifications sont faciles à repérer dans le
code décompilé, et dnSpyEx vous permet de modifier la méthode (clic droit,
Edit Method (C#)), de la recompiler et d’enregistrer une copie patchée.
L’anti-débogage en général est traité dans la leçon Anti-débogage du module 9.
Astuce : Identifiez avant de combattre. Le nom du protecteur donné par Detect It Easy, un rapide
de4dot -d sample.exe(détection seule) et un coup d’œil aux premiers noms de classes vous disent s’il existe un nettoyeur connu. Les heures passées à rétro-ingénierer à la main un déchiffreur de chaînes que de4dot prend déjà en charge sont des heures perdues.
Où se trouve la configuration
Dans les échantillons .NET, la configuration — hôtes et ports de C2, nom de mutex, chemin d’installation, clés de chiffrement, identifiant de campagne ou de build — est généralement concentrée en un seul endroit. Cherchez dans cet ordre :
- Une classe de paramètres. De nombreuses familles conservent une classe
statique dont les champs sont affectés dans son constructeur statique
(
.cctor). Les valeurs sont souvent des chaînes Base64, parfois chiffrées en AES avec une clé qui est elle-même un champ de la même classe ; les RAT open source comme AsyncRAT suivent ce modèle. - Le tas
#USpour tout ce qui n’est pas chiffré — chaque littéral chargé par le code s’y trouve, encodé en UTF-16. - Les tableaux d’octets statiques : un tableau initialisé à partir de
données est compilé en un appel à
RuntimeHelpers.InitializeArrayavec un champ stocké dans le fichier lui-même (cherchez<PrivateImplementationDetails>). - Les ressources, en particulier les petites, à forte entropie ou portant un nom personnalisé.
- La mémoire, quand tout ce qui précède est chiffré : arrêtez-vous après la routine de déchiffrement et lisez les valeurs.
Une fois le schéma connu, écrivez un petit script dnfile qui extrait les valeurs de façon statique ; la leçon du module 8 sur l’extraction des configurations de malwares construit des extracteurs complets sur cette idée.
Que mettre dans le rapport
Ajoutez une section .NET à la structure de rapport de triage que vous utilisez déjà :
- Faits sur le runtime : .NET Framework ou .NET moderne, architecture cible, bundle monofichier, mode mixte ou Native AOT.
- Protection : nom de l’obfuscateur ou du protecteur et la façon dont vous l’avez identifié ; les étapes de nettoyage appliquées, avec la version de l’outil. Hachez séparément les fichiers nettoyés et dumpés et étiquetez-les — ce sont des artefacts dérivés, pas des échantillons vus dans la nature.
- Identifiants : MVID et GUID TypeLib. Le MVID change à chaque build, mais
le GUID TypeLib (l’attribut
Guidque Visual Studio écrit dans les modèles de projet) reste stable d’un build à l’autre d’un même projet et constitue un bon pivot de regroupement et de YARA — le moduledotnetde YARA expose les deux. - Chaîne d’étapes : chaque assembly chargé, la façon dont il était stocké (ressource, tableau, téléchargement), la façon dont il a été déchiffré, et son hash.
- Configuration et IOC (indicateurs de compromission), avec le jeton de la méthode où chacun a été décodé, afin qu’un autre analyste puisse le reproduire.
Lab : une configuration cachée de deux façons
Vous allez construire un programme C# inoffensif qui cache deux fois une configuration de démonstration — un littéral Base64 et une ressource intégrée —, puis retrouver les deux sans lancer l’interface graphique d’un décompilateur. Les sorties ci-dessous sont réelles, obtenues avec Mono 6.14.1 sous macOS, Python 3.14, dnfile 0.18.0 et pefile 2024.8.26. D’autres versions peuvent présenter une mise en page légèrement différente.
-
Créez
config.txt:text interval=3600 mode=demo -
Créez
LabDemo.cs:text // LabDemo.cs: a harmless program that hides a demo config two ways using System; using System.IO; using System.Reflection; using System.Text; namespace LabDemo { static class Settings { // Base64 of "server=update.example.com" const string Encoded = "c2VydmVyPXVwZGF0ZS5leGFtcGxlLmNvbQ=="; public static string DecodeServer() { byte[] raw = Convert.FromBase64String(Encoded); return Encoding.UTF8.GetString(raw); } public static string ReadEmbedded() { Assembly self = Assembly.GetExecutingAssembly(); using (Stream s = self.GetManifestResourceStream("LabDemo.config.txt")) using (StreamReader r = new StreamReader(s)) { return r.ReadToEnd(); } } } class Program { static void Main() { Console.WriteLine("decoded: " + Settings.DecodeServer()); Console.Write("resource: " + Settings.ReadEmbedded()); } } } -
Compilez avec le compilateur C# de Mono, en intégrant la ressource sous un nom logique, puis exécutez le programme (installez Mono avec
brew install monoou le paquetmono-develde votre distribution) :bash mcs -out:LabDemo.exe -resource:config.txt,LabDemo.config.txt LabDemo.cs mono LabDemo.exe file LabDemo.exetext decoded: server=update.example.com resource: interval=3600 mode=demo LabDemo.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows -
Essayez d’abord un simple
strings:bash strings LabDemo.exe | grep -E "LabDemo|Base64|interval|Settings|mscoree"text interval=3600 LabDemo Settings FromBase64String LabDemo.config.txt LabDemo.exe mscoree.dllLes identifiants proviennent de
#Strings(UTF-8) et la ressource est stockée en clair, mais le littéral Base64 manque :#USest en UTF-16, ce qu’une passe destringslimitée à l’ASCII ignore. C’est pourquoi Chaînes et chaînes obfusquées insiste sur la recherche dans les deux encodages. -
Désassemblez en IL avec
monodis:bash monodis LabDemo.exe > LabDemo.ilUn extrait de
DecodeServer:text .field private static literal string Encoded = "c2VydmVyPXVwZGF0ZS5leGFtcGxlLmNvbQ==" ... IL_0000: ldstr "c2VydmVyPXVwZGF0ZS5leGFtcGxlLmNvbQ==" IL_0005: call unsigned int8[] class [mscorlib]System.Convert::FromBase64String(string) IL_000a: stloc.0 IL_000b: call class [mscorlib]System.Text.Encoding class [mscorlib]System.Text.Encoding::get_UTF8() IL_0010: ldloc.0 IL_0011: callvirt instance string class [mscorlib]System.Text.Encoding::GetString(unsigned int8[]) IL_0016: retLes noms de classes, de méthodes et de champs survivent, et chaque appel au framework est écrit en toutes lettres. Remarquez que la valeur
constapparaît deux fois : une fois comme constante du champ (tableConstant, stockée dans#Blob) et une fois comme opérande d’unldstrdans#US, car le compilateur recopie les constantes à chaque utilisation. -
Examinez les tas un par un et extrayez la ressource :
bash monodis --userstrings LabDemo.exe monodis --mresources LabDemo.exe && cat LabDemo.config.txttext User Strings heap contents 00: "" 01: "c2VydmVyPXVwZGF0ZS5leGFtcGxlLmNvbQ==" 4b: "LabDemo.config.txt" 71: "decoded: " 87: "resource: " ... interval=3600 mode=demo -
Scriptez la même inspection en Python. Créez un environnement virtuel pour que rien ne soit installé au niveau du système :
bash python3 -m venv venv ./venv/bin/pip install dnfile pefileEnregistrez
inspect_net.py:python import sys import dnfile import pefile path = sys.argv[1] # 1. Is it .NET? Check data directory 14 and the import table with pefile. pe = pefile.PE(path) clr = pe.OPTIONAL_HEADER.DATA_DIRECTORY[14] print(f"CLR header: rva=0x{clr.VirtualAddress:x} size={clr.Size}") for imp in pe.DIRECTORY_ENTRY_IMPORT: print("import:", imp.dll.decode(), [f.name.decode() for f in imp.imports]) # 2. Parse the CLR metadata with dnfile. dn = dnfile.dnPE(path) cor = dn.net.struct print(f"runtime {cor.MajorRuntimeVersion}.{cor.MinorRuntimeVersion}, " f"flags=0x{cor.Flags:x}, entry token=0x{cor.EntryPointTokenOrRva:08x}") print("metadata version:", dn.net.metadata.struct.Version.rstrip(b"\0").decode()) print("streams:", [s.decode() for s in dn.net.metadata.streams]) # 3. Types and methods from the TypeDef and MethodDef tables. for t in dn.net.mdtables.TypeDef: methods = [str(m.row.Name) for m in t.MethodList] print(f"type {t.TypeNamespace}.{t.TypeName}: {methods}") # 4. Referenced framework members (the .NET equivalent of imports). for m in dn.net.mdtables.MemberRef: parent = m.Class.row print(f"memberref {parent.TypeNamespace}.{parent.TypeName}::{m.Name}") # 5. The #US heap: string literals used by ldstr. us = dn.net.user_strings offset = 1 while offset < us.sizeof() and us.__data__[offset] != 0: # 0 = padding item = us.get(offset) if item is None: break if item.value: print(f"#US 0x{offset:x}: {item.value!r}") offset += item.raw_size # 6. Manifest resources, extracted to disk. for r in dn.net.resources: name = str(r.name) print(f"resource {name!r} public={r.public} size={len(r.data)}") with open("res_" + name, "wb") as f: f.write(r.data) -
Exécutez-le :
bash ./venv/bin/python inspect_net.py LabDemo.exetext CLR header: rva=0x2008 size=72 import: mscoree.dll ['_CorExeMain'] runtime 2.5, flags=0x1, entry token=0x06000004 metadata version: v4.0.30319 streams: ['#~', '#Strings', '#US', '#GUID', '#Blob'] type .<Module>: [] type LabDemo.Settings: ['DecodeServer', 'ReadEmbedded'] type LabDemo.Program: ['.ctor', 'Main'] memberref System.Convert::FromBase64String memberref System.Text.Encoding::get_UTF8 memberref System.Text.Encoding::GetString memberref System.Reflection.Assembly::GetExecutingAssembly memberref System.Reflection.Assembly::GetManifestResourceStream ... #US 0x1: 'c2VydmVyPXVwZGF0ZS5leGFtcGxlLmNvbQ==' #US 0x4b: 'LabDemo.config.txt' #US 0x71: 'decoded: ' #US 0x87: 'resource: ' resource 'LabDemo.config.txt' public=True size=24Tous les signes de reconnaissance du début de la leçon sont là : un en-tête CLR de 72 octets, un unique import
_CorExeMainet les cinq flux de métadonnées. La version de runtime de l’en-tête indique 2.5 pour tout assembly moderne ; c’est la chaîne de version des métadonnées (v4.0.30319) qui nomme le framework. Les flags0x1signifient IL uniquement. Le jeton d’entrée0x06000004est la ligne 4 deMethodDef—Main, la quatrième méthode du listing.res_LabDemo.config.txtcontient désormais la ressource sur le disque. -
Décodez le littéral pour confirmer :
bash echo c2VydmVyPXVwZGF0ZS5leGFtcGxlLmNvbQ== | base64 -d -
Sous Windows (dans votre VM de laboratoire), refaites l’analyse dans les interfaces graphiques. Dans dnSpyEx, utilisez File → Open sur
LabDemo.exe, dépliez l’assembly et l’espace de nomsLabDemo, puis cliquez surSettings: la vue C# montre la constante et les deux méthodes. Dépliez Resources, faites un clic droit surLabDemo.config.txtet choisissez Save pour l’extraire. Faites un clic droit surDecodeServeret choisissez Analyze pour voir « Used By » (Main). Posez ensuite un point d’arrêt sur la lignereturndeDecodeServer(F9), lancez le débogage (F5) et lisez la chaîne décodée dans la fenêtre Locals. Dans ILSpy, ouvrez le même fichier, parcourez la même arborescence et utilisez File → Save Code sur l’assembly pour exporter un projet C#.
Questions : Pourquoi strings a-t-il trouvé FromBase64String mais pas le
littéral Base64 ? Si l’auteur avait chiffré le littéral en AES au lieu de
l’encoder en Base64, quelles entrées MemberRef le trahiraient, et où
poseriez-vous un point d’arrêt pour lire le texte en clair ? Remplacez le
const par un champ static readonly et recompilez : où se trouve désormais le
littéral dans l’IL, et pourquoi ? Comment modifieriez-vous inspect_net.py pour
signaler toute ressource dont les deux premiers octets sont MZ ?
À retenir
- Un répertoire de données 14 non vide et un unique import de
mscoree.dll(_CorExeMainou_CorDllMain) signifient du code managé : arrêtez de désassembler du x86 et passez aux outils .NET. - Le CLR conserve des métadonnées complètes — types, méthodes, champs,
références au framework, littéraux de chaînes dans
#US—, ce qui explique que l’IL se décompile en un C# proche du source. - Utilisez dnSpyEx comme établi (décompiler, déboguer, dumper, patcher), ILSpy pour une lecture propre et l’export, de4dot pour retirer les obfuscateurs connus, et dnfile pour scripter le triage et l’extraction.
- Identifiez l’obfuscateur avant de le combattre ; quand le nettoyage statique échoue, exécutez sous le débogueur et arrêtez-vous après le déchiffrement.
- Les étapes
Assembly.Load(byte[])ne touchent jamais le disque : interceptez le tableau d’octets ou dumpez le module depuis le débogueur, puis triez chaque étape comme un nouvel échantillon. - Rapportez le protecteur, la chaîne d’étapes avec les hashs, la configuration avec la méthode qui la décode, et les identifiants stables tels que le GUID TypeLib.