Obfusquer un assembly multi-cible : une seule config pour net48 et net8.0
Une bibliothèque qui cible net48;net8.0 se compile en deux assemblies différents, avec deux jeux de références différents et parfois deux jeux d'IL différents. N'en protéger qu'un seul revient à livrer un build non obfusqué. Voici comment Nebula.NET traite chaque framework cible comme sa propre passe d'obfuscation tout en partageant une seule politique — et les pièges par TFM qui vous mordent si vous ne le faites pas.
Ajoutez un second framework cible à un .csproj et vous avez discrètement doublé votre surface de livraison. <TargetFrameworks>net48;net8.0</TargetFrameworks> dit au compilateur de construire le projet deux fois, dans deux dossiers séparés, en deux assemblies distincts. Ils partagent un nom et une version, mais ce ne sont pas le même binaire : ils se lient à des assemblies de référence différents, et si votre code contient un seul #if NET8_0_OR_GREATER, ils ne contiennent même pas la même IL. Un client qui installe le build net48 et un client sous net8.0 exécutent des DLL différentes.
Cela compte dès l’instant où vous protégez la chose. Pointez votre obfuscateur vers un seul dossier de sortie — celui que votre machine de dev se trouve construire — et vous avez durci exactement un des deux assemblies que vous livrez. L’autre sort en clair. Cet article traite de bien le faire avec Nebula.NET : traiter chaque framework cible comme sa propre passe d’obfuscation contre ses propres références, pendant qu’une seule politique les gouverne tous, et la poignée de détails par TFM qui vous mordront si vous n’y prenez pas garde.
Ce que « multi-cible » produit réellement
Un build multi-cible n’est pas un build avec un choix à la fin. MSBuild exécute le build interne une fois par TFM et dispose les résultats côte à côte :
MyLib/bin/Release/
├─ net48/
│ └─ MyLib.dll → references mscorlib, System, System.Core, …
└─ net8.0/
└─ MyLib.dll → references System.Runtime, System.Private.CoreLib, …
Même nom MyLib.dll, deux assemblies différents. Pour un package NuGet, la même séparation apparaît sous forme de lib/net48/MyLib.dll et lib/net8.0/MyLib.dll, et le moteur de restauration choisit la meilleure correspondance pour chaque consommateur. Donc « obfusquer MyLib.dll » est ambigu — il y en a deux, et les deux doivent être protégés, chacun contre le jeu de références pour lequel il a été compilé, sinon la vérification de signature et la réécriture des références seront fausses.
La bonne manière : une politique, une passe par framework
Nebula obfusque des assemblies compilés, pas du source, donc rien de tout cela n’exige de changement de code — il s’accroche à la sortie du build. L’intégration la plus propre est le package MSBuild, qui comprend nativement le multi-ciblage. Ajoutez le package « build-only » et conditionnez-le sur un signal qui n’existe que sur votre agent de build, pour que les builds de dev restent intacts :
<ItemGroup>
<PackageReference Include="Delta1Labs.Nebula.MSBuild" Version="1.1.6" PrivateAssets="all" />
</ItemGroup>
<PropertyGroup Condition="'$(NEBULA_OBFUSCATE)' == '1'">
<NebulaObfuscate>true</NebulaObfuscate>
<NebulaConfigFile>$(MSBuildProjectDirectory)\nebula.config.json</NebulaConfigFile>
</PropertyGroup>
C’est tout le câblage pour un projet multi-cible. Le package s’exécute après chaque build interne, donc chaque framework cible est protégé dans son propre dossier de sortie nebula par TFM, automatiquement — vous ne listez pas les frameworks. La DLL net48 est obfusquée à l’intérieur du build net48 avec les références net48 à portée de main ; la DLL net8.0 à l’intérieur du build net8.0 avec ses références. Un seul nebula.config.json pilote les deux.
Et cette config ne porte aucun inputs — c’est une politique réglages seulement. Le build fournit l’assembly ; le fichier fournit les transformations et les règles :
{
"preservePublicApi": true,
"controlFlowObfuscation": true,
"encryptStrings": true,
"obfuscateConstants": true,
"metadataHardening": true,
"antiTamper": true,
"exclude": [
"MyLib.Interop.NativeMethods"
],
"excludeNamespaces": [
"MyLib.Abstractions"
]
}
Ces transformations (chiffrement illimité des chaînes/du flot de contrôle, masquage des constantes, durcissement des métadonnées, anti-tamper) sont des fonctionnalités Pro ; le renommage d’identifiants est dans toutes les éditions. Le point essentiel est que la même politique s’applique à l’identique aux deux assemblies — vous ne maintenez pas nebula.net48.json et nebula.net8.json.
Si vous préférez ne garder aucune référence de package, pilotez la CLI depuis un Directory.Build.targets et laissez la garde par TFM faire l’éventail pour vous :
<Project>
<Target Name="NebulaObfuscate" AfterTargets="Build"
Condition="'$(NEBULA_OBFUSCATE)' == '1' and '$(TargetFramework)' != ''">
<Exec Command="nebula --config "$(MSBuildThisFileDirectory)nebula.config.json" --input "$(TargetPath)" --output "$(TargetDir)nebula"" />
</Target>
</Project>
La condition '$(TargetFramework)' != '' fait ici un vrai travail. Dans un build multi-cible, la cible s’exécute une fois à l’intérieur de chaque build interne concret — où $(TargetFramework) vaut net48, puis net8.0 — et $(TargetPath) pointe vers la DLL propre à ce framework. La condition saute le build agrégé extérieur, où $(TargetFramework) est vide et où il n’y a pas d’assembly unique à traiter. Sans elle, vous exécutez soit contre rien, soit une seule fois en manquant un framework.
Les pièges par TFM
Les références se résolvent par framework — laissez le build les fournir
Le renommage et la passe finale de vérification ont tous deux besoin de voir les vraies références de l’assembly : pour réécrire une référence croisée, ou pour confirmer qu’un module transformé se résout encore. Ces références diffèrent par TFM — mscorlib/System.* pour net48, le jeu de façades System.Runtime pour net8.0. Exécuter l’obfuscation à l’intérieur de chaque build interne (les deux approches ci-dessus le font) signifie que Nebula se lie exactement au bon jeu de références pour le framework en cours. Laissez verifyOutput activé (il l’est par défaut) : il re-vérifie le module transformé pour détecter les références pendantes avant d’écrire et refuse d’émettre un module qui ne se chargerait pas — ce qui est précisément l’erreur que vous découvririez autrement seulement quand l’app du client net48 refuse de démarrer.
#if signifie que les deux assemblies n’ont pas les mêmes membres
La compilation conditionnelle est la subtile. Considérez :
public sealed class Cache
{
#if NET8_0_OR_GREATER
public void Store(ReadOnlySpan<byte> data) { /* span path */ }
#else
public void Store(byte[] data) { /* array path */ }
#endif
internal void Evict() { /* both */ }
}
L’assembly net8.0 a un Store(ReadOnlySpan<byte>) ; celui net48 a un Store(byte[]). Si vous écrivez une entrée exclude ou include pour un membre, elle matchera sous les TFM où ce membre existe et ne fera silencieusement rien sous les autres. C’est généralement correct — mais cela signifie que vous ne pouvez pas supposer qu’une liste d’autorisation complète pour net8.0 est complète pour net48. Appuyez-vous sur autoDetect (activé par défaut) pour préserver les noms requis pour la correction — cibles de réflexion, sérialisation, DI et XAML — sur chaque cible, et réservez les entrées exclude manuelles à la surface véritablement spécifique à un framework.
Préservez l’API publique de façon cohérente pour une bibliothèque livrée
Pour une bibliothèque que d’autres référencent, gardez preservePublicApi: true pour que les noms de types et de membres publics survivent dans les deux assemblies — un consommateur qui multi-cible lui-même contre vous doit voir la même surface publique quelle que soit celle de vos DLL qu’il résout. Ne renommer que les parties internes durcit quand même la logique de valeur tout en laissant l’API appelable intacte. (Pour une application d’utilisateur final que personne ne référence, désactivez-le et renommez tout ce qui est éligible.)
Chaque framework reçoit sa propre carte de symboles
Chaque passe émet une carte de symboles (de renommage) à côté de sa sortie — et parce que les deux assemblies ont été renommés indépendamment, les cartes diffèrent par TFM. Quand vous désobfusquez une pile d’appels venue du terrain, vous devez utiliser la carte du framework d’où vient le crash : passer une trace net8.0 dans la carte net48 produit du charabia. Gardez les dossiers de sortie nebula par TFM comme artefacts de build, nommés par framework, pour que la bonne carte soit toujours trouvable. La mécanique pour remonter une trace est dans Désobfusquer les piles d’appels.
Empaquetez après avoir protégé
Si vous livrez un package NuGet, les DLL lib/net48 et lib/net8.0 à l’intérieur du .nupkg doivent chacune être celle obfusquée, pas la sortie brute du compilateur. Exécutez la protection avant l’étape d’empaquetage et pointez l’empaquetage vers les assemblies protégés — la recette complète est dans Obfusquer un package NuGet. Pour un produit multi-assembly où vos TFM se référencent entre eux, obfusquez la sortie publiée en une seule étape pour que les références inter-assemblies restent cohérentes ; voir Plusieurs assemblies.
Vérifiez les deux, pas un seul
Le mode de défaillance contre lequel tout cet article vous protège est invisible : votre app tourne encore, vos tests passent encore, et l’un de vos deux assemblies livrés est en clair. Alors vérifiez les deux. Ouvrez la DLL de sortie de chaque framework dans un décompilateur et confirmez que les parties internes sont renommées et les chaînes chiffrées — le décompilateur gratuit Glass.NET est pratique pour un avant/après rapide sur chaque TFM. Confirmez qu’une carte de symboles par TFM existe pour chacun. Et gardez la garde de l’agent de build honnête, pour que le build protégé soit celui qui quitte les murs.
Le multi-ciblage est désormais la norme pour les bibliothèques, et il signifie que votre histoire d’obfuscation est une histoire par framework, que vous l’ayez prévu ou non. La bonne nouvelle est qu’il ne vous coûte presque rien de bien le faire : une politique « réglages seulement », une protection qui s’exécute à l’intérieur de chaque build interne, et la discipline de vérifier chaque TFM que vous livrez. Partez du guide MSBuild & CI et de la référence de configuration pour les propriétés et champs exacts.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.