Skip to content

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é :

SigneOù regarderCe que vous voyez
En-tête CLR RuntimeRépertoire de données 14Une RVA non nulle et une taille de 72 octets (IMAGE_COR20_HEADER)
Import de mscoreeTable des importsUn seul import : _CorExeMain (EXE) ou _CorDllMain (DLL) depuis mscoree.dll
Verdict de l’outilDetect 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 :

FluxContenuIntérêt pour l’analyste
#~Tables de métadonnées : TypeDef, MethodDef, Field, MemberRef, TypeRef, ManifestResource, Constant…La structure complète du programme
#StringsIdentifiants UTF-8 : noms de types, de méthodes, de champs et d’espaces de nomsDes noms lisibles (sauf s’ils ont été renommés)
#US« User strings » : chaque littéral chargé par ldstr, en UTF-16URL, chemins, clés, configuration encodée
#GUIDGUID, dont l’identifiant de version du module (MVID)Regroupement (clustering)
#BlobSignatures, valeurs constantes, arguments d’attributs personnalisésConstantes 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

OutilTypeÀ utiliser pour
dnSpyExDé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
ILSpyDé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
de4dotDésobfuscateur (ligne de commande)Détecter et retirer les obfuscateurs connus : renommage, déchiffrement des chaînes, nettoyage du flot de contrôle
dnfileBibliothèque PythonTriage scripté et extracteurs de configuration : tables, #US, ressources, à grande échelle
monodis / ildasmDésassembleurs ILL’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 :

  1. de4dot, s’il reconnaît l’obfuscateur, déchiffre et réinjecte les chaînes automatiquement.
  2. 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.
  3. Débogueur : posez un point d’arrêt sur le return du 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 :

  1. 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.
  2. Le tas #US pour tout ce qui n’est pas chiffré — chaque littéral chargé par le code s’y trouve, encodé en UTF-16.
  3. Les tableaux d’octets statiques : un tableau initialisé à partir de données est compilé en un appel à RuntimeHelpers.InitializeArray avec un champ stocké dans le fichier lui-même (cherchez <PrivateImplementationDetails>).
  4. Les ressources, en particulier les petites, à forte entropie ou portant un nom personnalisé.
  5. 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 Guid que 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 module dotnet de 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.

  1. Créez config.txt :

    text
    interval=3600
    mode=demo
  2. 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());
            }
        }
    }
  3. 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 mono ou le paquet mono-devel de votre distribution) :

    bash
    mcs -out:LabDemo.exe -resource:config.txt,LabDemo.config.txt LabDemo.cs
    mono LabDemo.exe
    file LabDemo.exe
    text
    decoded:  server=update.example.com
    resource: interval=3600
    mode=demo
    LabDemo.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows
  4. 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.dll

    Les identifiants proviennent de #Strings (UTF-8) et la ressource est stockée en clair, mais le littéral Base64 manque : #US est en UTF-16, ce qu’une passe de strings limitée à l’ASCII ignore. C’est pourquoi Chaînes et chaînes obfusquées insiste sur la recherche dans les deux encodages.

  5. Désassemblez en IL avec monodis :

    bash
    monodis LabDemo.exe > LabDemo.il

    Un 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:  ret

    Les noms de classes, de méthodes et de champs survivent, et chaque appel au framework est écrit en toutes lettres. Remarquez que la valeur const apparaît deux fois : une fois comme constante du champ (table Constant, stockée dans #Blob) et une fois comme opérande d’un ldstr dans #US, car le compilateur recopie les constantes à chaque utilisation.

  6. Examinez les tas un par un et extrayez la ressource :

    bash
    monodis --userstrings LabDemo.exe
    monodis --mresources LabDemo.exe && cat LabDemo.config.txt
    text
    User Strings heap contents
    00: ""
    01: "c2VydmVyPXVwZGF0ZS5leGFtcGxlLmNvbQ=="
    4b: "LabDemo.config.txt"
    71: "decoded:  "
    87: "resource: "
    ...
    interval=3600
    mode=demo
  7. 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 pefile

    Enregistrez 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)
  8. Exécutez-le :

    bash
    ./venv/bin/python inspect_net.py LabDemo.exe
    text
    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=24

    Tous les signes de reconnaissance du début de la leçon sont là : un en-tête CLR de 72 octets, un unique import _CorExeMain et 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 flags 0x1 signifient IL uniquement. Le jeton d’entrée 0x06000004 est la ligne 4 de MethodDef — Main, la quatrième méthode du listing. res_LabDemo.config.txt contient désormais la ressource sur le disque.

  9. Décodez le littéral pour confirmer :

    bash
    echo c2VydmVyPXVwZGF0ZS5leGFtcGxlLmNvbQ== | base64 -d
  10. 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 noms LabDemo, puis cliquez sur Settings : la vue C# montre la constante et les deux méthodes. Dépliez Resources, faites un clic droit sur LabDemo.config.txt et choisissez Save pour l’extraire. Faites un clic droit sur DecodeServer et choisissez Analyze pour voir « Used By » (Main). Posez ensuite un point d’arrêt sur la ligne return de DecodeServer (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 (_CorExeMain ou _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.