Skip to content
← Tous les articles
· Delta1 Labs Nebula.NETObfuscationNative AOT

Obfusquer les applications .NET Native AOT et fichier unique

Publier en fichier unique ou avec Native AOT relève de l'empaquetage et de la compilation, pas de la protection. Voici ce que chaque mode fait réellement à votre code, pourquoi les applications en fichier unique livrent toujours de l'IL managé entièrement récupérable, ce que Native AOT masque et ne masque pas, et pourquoi l'obfuscation doit s'exécuter sur l'IL avant que le bundle ne soit construit ou que le compilateur AOT ne le supprime.

Une croyance rassurante circule dans les équipes .NET : si vous publiez votre application en fichier unique, ou si vous la compilez avec Native AOT, vous n”avez plus besoin de l”obfusquer. Le raisonnement semble plausible — « il n”y a plus de DLL à décompiler ». Il est aussi faux, et dans le cas du fichier unique, dangereusement faux. Le format de publication n”est pas une frontière de protection. Le fichier unique relève de l”empaquetage ; Native AOT relève de la compilation. Aucun des deux n”a été conçu pour arrêter la rétro-ingénierie, et les traiter comme tels laisse justement le code que vous vouliez le plus cacher à la vue de tous.

Cet article porte sur ce que chaque mode de publication fait réellement à votre code, ce qu”il protège et ce qu”il ne protège pas, et sur l”unique fait structurel qui décide de tout : l”obfuscation doit avoir lieu sur l”IL managé avant que le bundle ne soit construit ou que le compilateur AOT ne s”exécute, car ensuite il ne reste plus rien à transformer.

Le fichier unique est un conteneur, pas un verrou

Lorsque vous activez PublishSingleFile, le SDK ne fond pas votre code en un bloc indivisible. Il produit un exécutable hôte natif avec les fichiers de votre application — assemblys managés, configuration, dépendances natives — intégrés à l”intérieur sous forme de ressources. Au lancement, l”hôte localise ces assemblys intégrés et les charge directement depuis le bundle. Et surtout : les assemblys managés restent exactement ce que le compilateur C# a émis, des assemblys .NET ordinaires contenant de l”IL ordinaire et des métadonnées complètes.

Cela signifie que l”unique .exe est un conteneur, et ce conteneur a une structure documentée. Les extracteurs de bundle disponibles publiquement lisent cette structure et réécrivent chaque assembly intégré sur le disque sous forme de .dll normale. L”aller-retour prend quelques secondes et ne demande aucune compétence :

# un attaquant, face à votre application en fichier unique « protégée »
single-file-extractor MyApp.exe -o extracted/
# extracted/MyApp.dll, extracted/MyApp.Core.dll, ...  tout en IL ordinaire

À partir de là, c”est la même histoire que pour n”importe quel assembly non protégé : ouvrez MyApp.dll dans un décompilateur et lisez du C# quasi d”origine. Votre vérification de licence, vos clés d”API gravées dans des littéraux de chaîne, votre logique métier : tout. Le fichier unique a résolu un problème de déploiement (un seul fichier à copier) et n”a absolument rien fait pour la confidentialité. Si vous auriez obfusqué les DLL avant, vous devez toujours les obfusquer maintenant ; le bundle les cache seulement d”un listage de fichiers superficiel, pas de quiconque tape une commande.

Native AOT supprime l”IL — mais ce n”est pas une fonction de sécurité

Native AOT est une chose véritablement différente. Avec PublishAot, le compilateur d”IL (ILC) compile votre code managé en code machine natif de façon anticipée. L”artefact publié ne contient pas de JIT et, pour le code entièrement compilé en AOT, il n”y a pas d”IL ni de corps de méthode managé à récupérer. Pointez un décompilateur .NET dessus et il n”y aura rien à lire : il ne comprend que l”IL. La rétro-ingénierie devient alors du désassemblage natif, ce qui représente une vraie montée en coût. C”est une propriété agréable.

C”est aussi un effet secondaire, pas une garantie, et cela s”accompagne de limites importantes que les équipes négligent systématiquement :

  • AOT n”est pas disponible pour toutes les charges de travail. Il interdit la génération de code à l”exécution, est strict sur la réflexion et le chargement dynamique, et de nombreux frameworks, architectures à plugins et applications gourmandes en réflexion ne peuvent tout simplement pas être compilés ainsi. La grande majorité des applications .NET réelles aujourd”hui ne sont pas entièrement AOT, ce qui veut dire que cette « protection » n”existe pas pour elles.
  • Les déploiements mixtes et partiels livrent toujours de l”IL managé. Si une partie de votre produit est une bibliothèque de classes consommée ailleurs, un plugin ou un composant hors de l”image AOT, cette partie redevient de l”IL ordinaire et décompilable.
  • Les données survivent à la compilation. Les littéraux de chaîne, les ressources intégrées et la forme de vos constantes restent généralement présents dans l”image native. Un désassembleur plus un vidage des chaînes laisse toujours fuiter le préfixe "LIC-" et l”URL du point de terminaison même quand la logique environnante est native.
  • Ce n”est pas documenté comme une protection. Compter sur AOT pour cacher votre algorithme, c”est compter sur le comportement incident d”un compilateur que Microsoft n”a jamais promis de maintenir. Ce n”est pas une posture de sécurité sur laquelle bâtir l”application des licences.

Donc Native AOT relève la barre là où il s”applique, et c”est une couche supplémentaire bienvenue. Ce n”est pas un substitut à la protection de la couche managée — surtout pas pour les nombreuses applications et composants qui ne peuvent pas être compilés en AOT dès le départ.

Le fait structurel : obfusquez l”IL avant qu”il ne disparaisse

Voici le point dont tout dépend. L”obfuscation — renommage, chiffrement de chaînes, aplatissement du flux de contrôle, virtualisation de méthodes — est une transformation d”IL vers IL. Elle lit vos assemblys compilés, réécrit l”IL et réécrit des assemblys protégés. Elle a besoin d”IL à la fois en entrée et en sortie.

Regardez maintenant où l”IL existe dans le pipeline de publication :

  • Le compilateur C# transforme votre source en assemblys managés (IL). ← l”obfuscation peut s”exécuter ici
  • dotnet publish en fichier unique intègre ces assemblys IL dans l”hôte. L”IL existe toujours, mais il est désormais empaqueté, et si vous n”obfusquez qu”après cela, il faudrait désempaqueter, transformer et réempaqueter, ce qui ruine l”idée.
  • dotnet publish avec Native AOT compile cet IL en code natif et le jette. Après cette étape, il n”y a plus aucun IL. Il ne reste rien à obfusquer.

La conclusion est incontournable : la protection doit porter sur les assemblys entre la compilation et la publication. Obfusquez d”abord l”IL ; ensuite laissez l”empaqueteur l”empaqueter ou le compilateur AOT le consommer. Nebula.NET est conçu pour s”insérer exactement dans ce créneau : il opère sur les assemblys compilés, avant la publication.

Source .csIL managé(depuis csc)Nebulaobfusque l''ILBundle fichier uniqueIL intégré en ressourcesCompilation Native AOTIL compilé en natifL''obfuscation s''exécute sur l''IL, avant la construction du bundle ou sa suppression par AOT.

L”intégrer dans un build de publication

Le .csproj d”un projet en fichier unique ressemble à ceci :

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>net8.0</TargetFramework>
  <PublishSingleFile>true</PublishSingleFile>
  <SelfContained>true</SelfContained>
  <!-- ou, pour Native AOT : -->
  <!-- <PublishAot>true</PublishAot> -->
</PropertyGroup>

Le travail consiste à insérer une étape d”obfuscation après la construction de vos assemblys et avant que publish ne les empaquette ou ne les compile en AOT. Vous avez deux options propres.

Option A — protéger la sortie du build dans la CI, puis publier. Compilez normalement, exécutez Nebula sur les assemblys de sortie et pointez publish vers les copies protégées :

dotnet build -c Release
nebula --config nebula.config.json          # réécrit l'IL sur place / dans protected/
dotnet publish -c Release --no-build -o out  # empaquette ou compile en AOT l'IL protégé

avec un nebula.config.json qui préserve tout ce dont AOT et la réflexion ont besoin :

{
  "schemaVersion": 1,
  "inputs": ["bin/Release/net8.0/MyApp.dll", "bin/Release/net8.0/MyApp.Core.dll"],
  "outputDirectory": "bin/Release/net8.0",
  "preservePublicApi": true,
  "encryptStrings": true,
  "controlFlowObfuscation": true,
  "autoProtectLicensing": true
}

Option B — un hook MSBuild qui s”exécute entre Build et Publish, de sorte qu”un simple dotnet publish soit déjà protégé :

<Target Name="NebulaProtect" AfterTargets="Build" BeforeTargets="Publish">
  <Exec Command="nebula --config $(ProjectDir)nebula.config.json" />
</Target>

Dans les deux cas, le compilateur AOT ou l”empaqueteur de fichier unique consomme désormais de l”IL déjà obfusqué. Pour AOT en particulier, gardez à l”esprit la même discipline que pour le trimming : le renommage doit préserver votre surface publique et tout membre atteint par réflexion ou sérialisation, car la chaîne d”outils AOT est stricte sur les deux. Consultez la configuration pour les règles de préservation, et le parcours général dans comment obfusquer un assembly .NET.

Avant et après

Le test est toujours le même : regardez l”artefact livré à travers les outils d”un attaquant.

Sans obfuscation, le cas du fichier unique est le pire dénouement : un faux sentiment de sécurité sur du code trivialement récupérable :

// après avoir lancé un extracteur de bundle sur le .exe en fichier unique, puis un décompilateur :
public bool IsLicensed(string key)
{
    if (!key.StartsWith("LIC-")) return false;
    return ValidateSignature(key, "https://api.example.com/v1/activate");
}

Avec l”obfuscation appliquée d”abord à l”IL, le code extrait (ou natif) ne raconte plus son histoire : la méthode est un répartiteur aplati, le préfixe et l”URL sont déchiffrés à l”exécution au lieu d”être imprimés, et en Enterprise la méthode d”application des licences est virtualisée en un bytecode sans décompilateur :

public bool a(string A_0)
{
    int s = 0;
    while (true) { switch (s) { /* répartiteur sur des blocs réordonnés */
        case 0: if (!A_0.StartsWith(Strings.Get(0x3f1))) { s = 9; break; } s = 4; break;
        case 9: return false;
        /* ... */ } }
}

Le fichier unique a laissé la première version parfaitement lisible. AOT aurait rendu le cas natif plus difficile à décompiler, mais seulement pour le code entièrement compilé, seulement là où AOT est même une option, et jamais pour les chaînes. Obfusquer d”abord l”IL est ce qui protège réellement la logique d”application des licences dans les deux mondes.

À retenir

Le fichier unique et Native AOT sont de bons choix d”ingénierie pour la distribution et les performances de démarrage. Aucun n”est une stratégie de protection. Une application en fichier unique livre votre IL intact à l”intérieur d”un conteneur que n”importe qui peut ouvrir ; une application Native AOT durcit la sortie native par effet secondaire mais ne peut pas être utilisée partout et laisse toujours fuiter vos données. Le seul endroit fiable pour protéger votre code est l”IL managé, entre la compilation et la publication : obfusquez-le là, puis laissez l”empaqueteur ou le compilateur AOT faire leur travail. Si la protection s”exécute après l”application du format, il ne reste plus rien à protéger.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.