Skip to content
← Tous les articles
· Delta1 Labs GuideNebula

Obfusquer un outil global .NET sans casser la surface de commandes

Un dotnet tool est un paquet NuGet contre lequel personne ne compile, vous pouvez donc renommer bien plus agressivement qu'une bibliothèque. Voici comment protéger la DLL à l'intérieur du paquet tout en gardant intacts le point d'entrée, les noms de commandes et `dotnet tool install`, avec Nebula.NET.

Un outil global .NET — une application console que vous empaquetez avec dotnet pack et PackAsTool et installez avec dotnet tool install -g — est l”une des choses les plus faciles à protéger, pour une raison qu”il vaut la peine de comprendre avant de toucher à la moindre configuration. C”est un paquet NuGet, comme une bibliothèque, mais contrairement à une bibliothèque personne ne compile contre lui. Il n”y a aucun contrat d”API publique à préserver. La seule chose de l”autre côté du paquet est un shell qui invoque un exécutable. Ce seul fait change à quel point vous pouvez renommer agressivement, et cet article parcourt la protection de l”outil de bout en bout : ce que vous pouvez renommer (presque tout), la poignée de noms qui doivent vraiment survivre, comment la surface de commandes reste intacte même quand les classes qui la sous-tendent disparaissent, et comment l”intégrer à dotnet pack pour que chaque version soit publiée protégée.

Pourquoi un outil n”est pas une bibliothèque

Quand vous obfusquez une bibliothèque que vous distribuez, la règle directrice est garder l”API publique : les consommateurs référencent vos types et membres publics par leur nom, donc les renommer casse leur compilation. Un outil global n”a pas de tels consommateurs. Son paquet contient un assembly et un manifeste ; un utilisateur tape mytool build --release et le runtime lance votre point d”entrée. Rien ne se lie à MyTool.RootCommandBuilder ni à MyTool.Handlers.BuildHandler, il n”y a donc aucune raison de laisser ces noms lisibles.

C”est pourquoi le premier réglage est une omission délibérée :

{
  "schemaVersion": 1,
  "inputs": [ "bin/Release/MyTool.1.4.0.nupkg" ],
  "outputDirectory": "protected",

  "preservePublicApi": false,

  "encryptStrings": true,
  "controlFlowObfuscation": true,

  "renaming": {
    "rules": [
      { "pattern": "MyTool.*", "action": "rename" }
    ]
  }
}

preservePublicApi: false est l”opposé de la valeur par défaut pour une bibliothèque, et c”est correct ici : laissez le renommeur réécrire les classes de commande public, les handlers et les builders d”option exactement comme il réécrit les helpers internes. La seule règle large rename balaie tout l”espace de noms MyTool.*. Il y a un nom que cela ne doit pas écraser — le point d”entrée — et c”est géré plus bas, pas par une règle de conservation.

Le seul nom qui doit survivre : le point d”entrée

Le paquet d”un outil global porte un manifeste DotnetToolSettings.xml qui indique à l”hôte d”outils .NET quel nom de commande correspond à quel assembly :

<DotNetCliTool Version="1">
  <Commands>
    <Command Name="mytool" EntryPoint="MyTool.dll" Runner="dotnet" />
  </Commands>
</DotNetCliTool>

Le point d”entrée est fixé par nom d”assembly et de module (MyTool.dll) et le CLR trouve Main via le jeton de point d”entrée de l”assembly dans l”en-tête PE — non en cherchant un type nommé Program. C”est important : Nebula préserve le nom de l”assembly et réécrit le jeton de point d”entrée pour qu”il suive la méthode renommée, de sorte que votre Program.Main peut devenir a.b et l”outil démarre toujours. Vous n”avez pas besoin d”une règle de conservation pour Program. Ce que vous ne devez pas faire, c”est renommer l”assembly ni supprimer le manifeste — et comme vous donnez à Nebula le .nupkg complet, il conserve le manifeste et l”identité de l”assembly pour vous.

dotnet packPackAsTool=trueMyTool.nupkg· MyTool.dll· DotnetToolSettings.xmlNebula.NETrenomme public + internechiffre chaînes / aplatitconserve nom d''assemblyconserve jeton d''entréeconserve manifeste.nupkg protégédotnet tool install -g$ mytool builds''exécute — les classesont disparu, les verbes restent

La surface de commandes est faite de chaînes, pas de noms

L”instinct qui empêche les gens d”obfusquer un outil agressivement est la peur que renommer les classes de commande change les verbes que les utilisateurs tapent. Il n”en sera rien, et la raison est structurelle. Dans System.CommandLine — et dans pratiquement tous les frameworks de CLI — les noms de commande et d”option sont des littéraux de chaîne que vous passez aux constructeurs, et non les noms des types qui les construisent :

var build = new Command("build", "Compile the project");
var release = new Option<bool>("--release", "Build in Release configuration");
build.AddOption(release);
build.SetHandler(ctx => BuildHandler.Run(ctx));

Renommez la classe où ceci vit de RootCommandBuilder en a, renommez BuildHandler en b, et les littéraux "build", "--release" et le texte d”aide restent intacts — ce sont des données dans le corps de la méthode, pas des identifiants de métadonnées. L”analyseur produit toujours exactement la même surface mytool build --release. Mieux encore, avec encryptStrings activé, ces littéraux ne sont plus dans le tas de chaînes à la portée d”un grep : ils sont déchiffrés à la demande lorsque l”arbre de commandes est construit. Ainsi le renommage masque comment l”outil est câblé, le chiffrement de chaînes masque le vocabulaire, et la CLI vue par l”utilisateur est identique octet pour octet.

Voici à quoi ressemble un handler après protection, décompilé. Avant :

internal static class BuildHandler
{
    public static int Run(InvocationContext ctx)
    {
        bool release = ctx.ParseResult.GetValueForOption(ReleaseOption);
        return Compiler.Invoke(release ? "Release" : "Debug");
    }
}

Après renommage, aplatissement du flux de contrôle et chiffrement de chaînes, la même méthode se décompile en une boucle de répartition sur un appel b.c(...) avec les littéraux "Release" / "Debug" remplacés par des appels à la routine de déchiffrement de chaînes — la logique est intacte et s”exécute de façon identique, mais la méthode se lit comme du bruit généré, sans noms ni constantes reconnaissables.

Surveillez les mêmes coutures que partout

« Tout renommer » comporte les mêmes quelques exceptions que n”importe quelle application .NET — les endroits où quelque chose résout un nom en tant que chaîne à l”exécution. Dans un outil elles sont généralement peu nombreuses, mais vérifiez-les :

  • Liaison de configuration. Si votre outil lit des paramètres via IConfiguration et Configure<TOptions>, les noms des propriétés liées doivent survivre. Conservez les noms de propriété publics de ces classes d”options, ou liez explicitement.
  • Sortie sérialisée. Si l”outil émet du JSON que des scripts analysent — mytool list --json — ces noms de propriété du DTO sont un contrat avec les pipelines de vos utilisateurs, même si aucun code ne compile contre eux. Fixez-les avec [JsonPropertyName("...")] pour que les noms sur le fil cessent de dépendre des noms C#, comme dans fixer un contrat JSON.
  • Réflexion et scan de DI. Un outil qui découvre les commandes en scannant une convention de nommage (tout type se terminant par Command) casse quand ces noms sont renommés. Préférez l”enregistrement explicite ; là où vous ne pouvez pas, conservez ce que le scan correspond. Le traitement complet est dans obfusquer le scan d”assemblys et la DI.

La discipline pour tous ces cas est la même discipline de liste de conservation minimale couverte dans construire une liste de conservation Nebula : ne conservez que le nom feuille que quelque chose lit en tant que chaîne, jamais l”espace de noms qui l”entoure.

Intégrez-le à dotnet pack

L”endroit propre pour protéger un outil est juste après son empaquetage, pour que l”artefact qui sort de votre build soit déjà protégé et votre code source intact. Marquez le projet comme outil comme d”habitude :

<PropertyGroup>
  <PackAsTool>true</PackAsTool>
  <ToolCommandName>mytool</ToolCommandName>
  <PackageId>MyTool</PackageId>
  <Version>1.4.0</Version>
</PropertyGroup>

Puis exécutez Nebula contre le paquet empaqueté dans votre pipeline de publication :

dotnet pack -c Release
nebula --config nebula.config.json

Le MyTool.1.4.0.nupkg protégé atterrit dans protected/, portant son manifeste et son point d”entrée, prêt pour dotnet nuget push. Pour une configuration totalement automatique, vous pouvez brancher l”obfuscation dans MSBuild pour qu”elle s”exécute automatiquement après Pack — voir MSBuild & CI — et la limiter à Release pour que votre dotnet run de boucle interne reste rapide et débogable.

Prouvez-le

Installez le paquet protégé dans un environnement jetable et exercez-le comme le ferait un utilisateur : dotnet tool install -g --add-source ./protected MyTool, puis exécutez vos vraies commandes — mytool build --release, mytool list --json — et confirmez que les verbes s”analysent, les options se lient et la sortie est inchangée. Ouvrez ensuite la DLL de l”intérieur du paquet protégé dans Glass.NET, le décompilateur statique gratuit, et confirmez l”autre moitié : vos classes de commande et handlers se lisent comme a, b, c, le tas de chaînes ne contient plus vos verbes ni votre texte d”aide, et le point d”entrée se résout toujours. Renommer de trop se manifeste par une commande cassée à l”exécution ; protéger trop peu se manifeste par une logique lisible dans le décompilateur. Quand les deux vérifications passent, l”outil que vos utilisateurs installent fait exactement ce qu”il a toujours fait, et ne révèle presque rien sur son fonctionnement.

Téléchargez Nebula.NET gratuitement et protégez votre prochaine version de dotnet tool.

Essayez Nebula.NET

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