Ein globales .NET-Tool obfuskieren, ohne die Befehlsoberfläche zu zerstören
Ein dotnet tool ist ein NuGet-Paket, gegen das niemand kompiliert — Sie können also weit aggressiver umbenennen als bei einer Bibliothek. Hier erfahren Sie, wie Sie die DLL im Paket schützen und dabei den Einstiegspunkt, die Befehlsnamen und `dotnet tool install` funktionsfähig halten, mit Nebula.NET.
Ein globales .NET-Tool — eine Konsolenanwendung, die Sie mit dotnet pack und PackAsTool packen und mit dotnet tool install -g installieren — ist eines der einfacheren Dinge, die man schützen kann, aus einem Grund, den man verstehen sollte, bevor man irgendeine Konfiguration anfasst. Es ist ein NuGet-Paket, wie eine Bibliothek, aber anders als eine Bibliothek kompiliert niemand dagegen. Es gibt keinen öffentlichen API-Vertrag zu bewahren. Das Einzige auf der anderen Seite des Pakets ist eine Shell, die eine ausführbare Datei aufruft. Diese eine Tatsache ändert, wie aggressiv Sie umbenennen können, und dieser Beitrag geht den Schutz des Tools von Anfang bis Ende durch: was Sie umbenennen können (fast alles), die Handvoll Namen, die wirklich überleben müssen, wie die Befehlsoberfläche intakt bleibt, selbst wenn die dahinterliegenden Klassen verschwinden, und wie Sie es in dotnet pack einbinden, damit jede Version geschützt veröffentlicht wird.
Warum ein Tool keine Bibliothek ist
Wenn Sie eine ausgelieferte Bibliothek obfuskieren, lautet die leitende Regel die öffentliche API bewahren: Verbraucher referenzieren Ihre öffentlichen Typen und Member über ihren Namen, sodass das Umbenennen ihren Build bricht. Ein globales Tool hat keine solchen Verbraucher. Sein Paket enthält eine Assembly und ein Manifest; ein Benutzer tippt mytool build --release, und die Laufzeit startet Ihren Einstiegspunkt. Nichts verlinkt gegen MyTool.RootCommandBuilder oder MyTool.Handlers.BuildHandler, es gibt also keinen Grund, diese Namen lesbar zu lassen.
Deshalb ist die erste Einstellung eine bewusste Auslassung:
{
"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 ist das Gegenteil der Vorgabe für eine Bibliothek, und hier ist es korrekt: Lassen Sie den Umbenenner die public-Befehlsklassen, Handler und Options-Builder genauso umschreiben, wie er interne Helfer umschreibt. Die eine breite rename-Regel erfasst den gesamten Namespace MyTool.*. Es gibt einen Namen, den dies nicht zerschlagen darf — den Einstiegspunkt — und das wird weiter unten behandelt, nicht durch eine Beibehaltungsregel.
Der einzige Name, der überleben muss: der Einstiegspunkt
Das Paket eines globalen Tools trägt ein DotnetToolSettings.xml-Manifest, das dem .NET-Tool-Host mitteilt, welcher Befehlsname welcher Assembly zugeordnet ist:
<DotNetCliTool Version="1">
<Commands>
<Command Name="mytool" EntryPoint="MyTool.dll" Runner="dotnet" />
</Commands>
</DotNetCliTool>
Der Einstiegspunkt wird über Assembly- und Modulnamen (MyTool.dll) fixiert, und die CLR findet Main über das Einstiegspunkt-Token der Assembly im PE-Header — nicht durch das Nachschlagen eines Typs namens Program. Das ist wichtig: Nebula bewahrt den Assembly-Namen und schreibt das Einstiegspunkt-Token so um, dass es der umbenannten Methode folgt, sodass Ihr Program.Main zu a.b werden kann und das Tool trotzdem startet. Sie brauchen keine Beibehaltungsregel für Program. Was Sie nicht tun dürfen, ist die Assembly umzubenennen oder das Manifest zu verwerfen — und da Sie Nebula das vollständige .nupkg übergeben, behält es Manifest und Assembly-Identität für Sie bei.
Die Befehlsoberfläche besteht aus Strings, nicht aus Namen
Der Instinkt, der Leute davon abhält, ein Tool aggressiv zu obfuskieren, ist die Angst, dass das Umbenennen der Befehlsklassen die Verben ändert, die Benutzer tippen. Das wird es nicht, und der Grund ist struktureller Natur. In System.CommandLine — und in praktisch jedem CLI-Framework — sind die Befehls- und Optionsnamen String-Literale, die Sie an die Konstruktoren übergeben, nicht die Namen der Typen, die sie erstellen:
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));
Benennen Sie die Klasse, in der dies lebt, von RootCommandBuilder in a um, benennen Sie BuildHandler in b um, und die Literale "build", "--release" und der Hilfetext bleiben unberührt — sie sind Daten im Methodenrumpf, keine Metadaten-Bezeichner. Der Parser erzeugt weiterhin genau dieselbe Oberfläche mytool build --release. Noch besser: Mit aktiviertem encryptStrings liegen diese Literale nicht mehr im String-Heap, wo jemand sie per grep finden könnte: Sie werden bei Bedarf entschlüsselt, wenn der Befehlsbaum aufgebaut wird. So verbirgt das Umbenennen wie das Tool verdrahtet ist, die String-Verschlüsselung verbirgt das Vokabular, und die vom Benutzer gesehene CLI ist Byte für Byte identisch.
So sieht ein Handler nach dem Schutz aus, dekompiliert. Vorher:
internal static class BuildHandler
{
public static int Run(InvocationContext ctx)
{
bool release = ctx.ParseResult.GetValueForOption(ReleaseOption);
return Compiler.Invoke(release ? "Release" : "Debug");
}
}
Nach Umbenennung, Kontrollfluss-Abflachung und String-Verschlüsselung dekompiliert dieselbe Methode zu einer Dispatcher-Schleife über einen b.c(...)-Aufruf, wobei die Literale "Release" / "Debug" durch Aufrufe der String-Entschlüsselungsroutine ersetzt sind — die Logik ist intakt und läuft identisch, aber die Methode liest sich wie generiertes Rauschen, ohne erkennbare Namen oder Konstanten.
Achten Sie auf dieselben Nahtstellen wie überall
„Alles umbenennen” hat dieselben wenigen Ausnahmen wie jede .NET-Anwendung — die Stellen, an denen etwas einen Namen zur Laufzeit als String auflöst. In einem Tool sind es meist wenige, aber prüfen Sie sie:
- Konfigurationsbindung. Wenn Ihr Tool Einstellungen über
IConfigurationundConfigure<TOptions>liest, müssen die Namen der gebundenen Eigenschaften überleben. Behalten Sie die öffentlichen Eigenschaftsnamen dieser Options-Klassen bei, oder binden Sie explizit. - Serialisierte Ausgabe. Wenn das Tool JSON ausgibt, das Skripte parsen —
mytool list --json— sind diese DTO-Eigenschaftsnamen ein Vertrag mit den Pipelines Ihrer Benutzer, auch wenn kein Code gegen sie kompiliert. Fixieren Sie sie mit[JsonPropertyName("...")], damit die Namen auf dem Draht nicht mehr von den C#-Namen abhängen, wie in einen JSON-Vertrag fixieren. - Reflexion und DI-Scanning. Ein Tool, das Befehle durch Scannen einer Namenskonvention entdeckt (jeder Typ, der auf
Commandendet), bricht, wenn diese Namen umbenannt werden. Bevorzugen Sie explizite Registrierung; wo Sie das nicht können, behalten Sie bei, was der Scan abgleicht. Die vollständige Behandlung finden Sie in Assembly-Scanning und DI obfuskieren.
Die Disziplin für all diese Fälle ist dieselbe minimale Beibehaltungslisten-Disziplin, die in eine Nebula-Beibehaltungsliste erstellen behandelt wird: Behalten Sie nur den Blattnamen bei, den etwas als String liest, niemals den Namespace drumherum.
Binden Sie es in dotnet pack ein
Der saubere Ort, um ein Tool zu schützen, ist direkt nachdem es gepackt wurde, damit das Artefakt, das Ihren Build verlässt, bereits geschützt ist und Ihr Quellcode unberührt bleibt. Markieren Sie das Projekt wie üblich als Tool:
<PropertyGroup>
<PackAsTool>true</PackAsTool>
<ToolCommandName>mytool</ToolCommandName>
<PackageId>MyTool</PackageId>
<Version>1.4.0</Version>
</PropertyGroup>
Führen Sie dann Nebula gegen das gepackte Paket in Ihrer Release-Pipeline aus:
dotnet pack -c Release
nebula --config nebula.config.json
Das geschützte MyTool.1.4.0.nupkg landet in protected/, trägt sein Manifest und seinen Einstiegspunkt und ist bereit für dotnet nuget push. Für eine völlig unbeaufsichtigte Einrichtung können Sie die Obfuskierung in MSBuild einhängen, damit sie automatisch nach Pack läuft — siehe MSBuild & CI — und sie auf Release beschränken, damit Ihr dotnet run der inneren Schleife schnell und debugbar bleibt.
Beweisen Sie es
Installieren Sie das geschützte Paket in einer Wegwerf-Umgebung und üben Sie es so aus, wie es ein Benutzer täte: dotnet tool install -g --add-source ./protected MyTool, und führen Sie dann Ihre echten Befehle aus — mytool build --release, mytool list --json — und bestätigen Sie, dass die Verben parsen, die Optionen binden und die Ausgabe unverändert ist. Öffnen Sie dann die DLL aus dem geschützten Paket in Glass.NET, dem kostenlosen statischen Decompiler, und bestätigen Sie die andere Hälfte: Ihre Befehlsklassen und Handler lesen sich als a, b, c, der String-Heap enthält nicht mehr Ihre Verben und Ihren Hilfetext, und der Einstiegspunkt löst sich weiterhin auf. Zu viel Umbenennen zeigt sich als gebrochener Befehl zur Laufzeit; zu wenig Schutz zeigt sich als lesbare Logik im Decompiler. Wenn beide Prüfungen bestehen, tut das Tool, das Ihre Benutzer installieren, genau das, was es immer getan hat, und verrät fast nichts darüber, wie.
Laden Sie Nebula.NET kostenlos herunter und schützen Sie Ihre nächste dotnet tool-Version.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.