Eine Nebula-Keep-List aufbauen: was von der Umbenennung ausnehmen (und warum)
Jeder Obfuskator lässt Sie Namen von der Umbenennung ausnehmen, und der erste Reflex, wenn etwas bricht, ist, zu viel auszunehmen. Eine Keep-List ist eine entworfene, minimale Grenze, keine Abstellkammer für alles, was gebrochen ist. Hier ist die Disziplin: was wirklich unbenannt bleiben muss (öffentliche API, Reflexionsziele, serialisierte DTOs, Options-Klassen, DI-Scanning, XAML, Einstiegspunkte eines Hosts), wie Sie es in nebula.config.json ausdrücken, wie Sie jeden Eintrag auf das geringste Mögliche verengen und wie Sie das Ergebnis in Glass.NET beweisen.
Jeder Obfuskator gibt Ihnen eine Möglichkeit zu sagen „das hier nicht umbenennen”. Die Versuchung, wenn zum ersten Mal etwas bricht, ist, großzügig danach zu greifen: den Typ ausnehmen, den Namespace ausnehmen, alles ausnehmen, was fragil aussieht, und weitermachen. Das funktioniert, und es demontiert still Ihren Schutz. Eine Keep-List ist keine Abstellkammer für alles, was gebrochen ist; sie ist eine entworfene, minimale Grenze zwischen den Namen, die Ihr Programm zur Laufzeit als Strings auflöst, und allem anderen. In diesem Beitrag geht es um die Disziplin, diese Grenze zu bauen und klein zu halten: was wirklich unbenannt bleiben muss, wie man es in der Konfiguration sagt, wie man jeden Eintrag auf das geringste Mögliche verengt und wie man das Ergebnis in einem Decompiler beweist.
Wenn Sie den Katalog dessen, was bricht und warum wollen, das ist Obfuskierung, Reflexion und Serialisierungs-Fallstricke. Dies ist das begleitende Prozess-Stück: der Fallstricke-Beitrag nennt das Symptom; dieser hier zeigt, wie man das Regelwerk schreibt und pflegt, das es verhindert — ohne zu viel auszunehmen und den Schutz zu verschenken.
Warum die Keep-List ein Kostenfaktor ist, kein Sicherheitsnetz
Beginnen Sie mit dem richtigen mentalen Modell: Jeder Name auf Ihrer Keep-List ist ein Name, den Sie einem Angreifer im Klartext übergeben. Umbenennung ist der billigste, breiteste Schutz, den Sie haben — sie verwandelt ValidateLicenseKey im ganzen Assembly in a. Jeder Keep-Eintrag gräbt ein lesbares Loch zurück hinein. Behalten Sie einen ganzen Namespace „zur Sicherheit”, haben Sie jeden Typ-, Methoden- und Feldnamen dieses Namespace in den Metadaten liegen lassen, bereit für jeden mit einem Decompiler zu lesen.
Die leitende Regel ist daher Minimalismus: Behalten Sie einen Namen nur, wenn etwas ihn zur Laufzeit über seinen String auflöst, und behalten Sie nur diesen Namen — nicht den Typ darum, nicht den Namespace um den Typ. Eine nach dieser Regel gebaute Keep-List ist kurz, und kurz ist der ganze Sinn.
Was wirklich bleiben muss
Über reale .NET-Apps hinweg kehren dieselben Kategorien wieder. Jede ist eine Stelle, an der ein Name zur Laufzeit als String gelesen wird — von Ihrem eigenen Code, einem Framework, einem Serializer oder einem Host — sodass ihn umzubenennen einen Vertrag ändert, statt bloß einen Bezeichner zu verbergen.
Die öffentliche API-Oberfläche einer ausgelieferten Bibliothek. Wenn andere gegen Ihr Assembly kompilieren, ist jeder öffentliche Typ- und Membername Teil Ihres Vertrags; benennen Sie ihn um, und ihr Code linkt nicht mehr. Dies ist die eine Kategorie, die Sie fast nie von Hand schreiben sollten: Schalten Sie preservePublicApi ein, und der Obfuskator behält genau die von außen sichtbare Oberfläche, während er alles Interne umbenennt. Der Fall der ganzen Bibliothek ist in eine ausgelieferte .NET-SDK oder -Bibliothek schützen behandelt.
Über Reflexion erreichte Typen und Member. Alles, was über Type.GetType("..."), GetMethod("..."), GetProperty("...") oder Activator.CreateInstance gefunden wird, wird über einen String aufgelöst, den die Umbenennung nicht berührt. Behalten Sie diese konkreten Member — nicht reflexartig ihren ganzen deklarierenden Typ.
Die Member-Namen serialisierter DTOs. System.Text.Json, Newtonsoft.Json und die XML-Serializer bauen ihr Übertragungsformat aus den Eigenschaftsnamen. Benennen Sie Invoice.Total um, und das JSON wird still zu {"a":…}, was jeden Konsumenten und jedes gespeicherte Dokument bricht. Behalten Sie diese — oder, weit besser, fixieren Sie sie mit [JsonPropertyName("total")], damit der Übertragungsname nicht mehr vom C#-Namen abhängt, wie in einen JSON-Vertrag mit JsonPropertyName fixieren.
Aus der Konfiguration gebundene Options-Klassen. Configure<TOptions> ordnet Konfigurationsschlüssel über den Binder den Eigenschaftsnamen zu. Das ist die am häufigsten vergessene Kategorie, weil Options-Klassen meist internal POCOs sind, die preservePublicApi nicht abdeckt — sie werden also still umbenannt und fallen in der Produktion auf die Standardwerte zurück. Behalten Sie ihre gebundenen Eigenschaftsnamen.
Über DI-Konvention gescannte Typen. Wenn ein Container Dienste registriert, indem er nach einem Namensmuster scannt — jeder Typ, dessen Name auf Service endet, etwa —, löscht die Umbenennung die Konvention und nichts wird registriert. Ziehen Sie es vor, die Registrierung explizit oder marker-basiert zu machen; wo Sie das nicht können, behalten Sie die Namen, die der Scan trifft. Die vollständige Behandlung steht in Assembly-Scanning und Dependency Injection obfuskieren.
In XAML oder Markup über den Namen referenzierte Typen. Ein {Binding CustomerName}, ein x:Class, eine Typreferenz in einem DataTemplate, ein im Markup benannter Konverter: alle lösen zur Laufzeit Strings auf. Behalten Sie die Eigenschafts- und Typnamen, die das Markup benennt.
Einstiegspunkte, die ein Host über den Namen lädt. Ein Plugin, das der Host über den Typnamen aktiviert, eine Startup-Klasse, die ein Host nachschlägt, ein Modul, das ein Framework aus einem Manifest-String lädt: Wenn etwas außerhalb Ihres Assemblys den Einstiegspunkt benennt, behalten Sie ihn.
Als Regeln ausdrücken
Nebula.NET liest eine nebula.config.json mit einem von oben nach unten ausgewerteten Umbenennungs-Regelwerk — das erste passende Muster gewinnt —, also setzen Sie Ihre engen keep-Regeln über die breite rename-Auffangregel. Hier ist eine Keep-List für eine ausgelieferte Bibliothek, aufgeteilt in eine öffentliche Oberfläche, einen Namespace serialisierter Verträge, einen Options-Namespace und einen internen Motor:
{
"schemaVersion": 1,
"inputs": [ "bin/Release/net8.0/Acme.Sdk.dll" ],
"outputDirectory": "protected",
"preservePublicApi": true,
"encryptStrings": true,
"controlFlowObfuscation": true,
"renaming": {
"rules": [
{ "pattern": "Acme.Sdk.Contracts.*", "action": "keep" },
{ "pattern": "Acme.Sdk.Options.*", "action": "keep", "members": "public" },
{ "pattern": "Acme.Sdk.*", "action": "rename" }
]
}
}
preservePublicApi trägt die von außen sichtbare API, daher gibt es keine Regel dafür. Die beiden keep-Regeln fixieren die serialisierten Verträge und die gebundenen Options; der Qualifizierer members: "public" verengt das Options-Keep auf die öffentlichen Eigenschaftsnamen und lässt private Felder umbenennen. Alles andere im Assembly fällt auf die rename-Auffangregel.
Für ein einzelnes Reflexionsziel ist ein Konfigurationsmuster ein grobes Werkzeug — ein Attribut am genauen Member ist präziser und lebt neben dem Code, der davon abhängt:
public sealed class ReportEngine
{
[Obfuscation(Feature = "renaming", Exclude = true)] // über Namen via Reflexion aufgelöst
public ReportResult Run(string reportName) { /* ... */ }
private decimal Accumulate(ReadOnlySpan<Line> lines) { /* ... */ } // umbenannt — niemand schlägt es nach
}
Das Attribut behält den Namen einer Methode; Accumulate und der Rest der Klasse werden normal umbenannt. Greifen Sie zu Attributen bei einzelnen, chirurgischen Keeps und zu Konfigurationsmustern bei ganzen Namespaces, die von Entwurf her ein Vertrag sind.
Verengen Sie jeden Eintrag
Der Abstand zwischen einer schwachen und einer starken Keep-List ist fast ausschließlich eine Frage des Verengens. Drei Züge erledigen den Großteil der Arbeit.
Behalten Sie den Namen, schützen Sie trotzdem den Rumpf. Einen Member von der Umbenennung auszunehmen heißt nicht, ihn vom Schutz auszunehmen. Ein behaltenes DTO oder eine behaltene Options-Klasse bekommt ihre Methodenrümpfe weiterhin kontrollfluss-abgeflacht, ihre Strings verschlüsselt und ihre Konstanten maskiert — Sie haben eine Transformation bei einem Namen aufgegeben, nicht die gesamte Behandlung des Codes. Zögern Sie also nie, einen Namen zu behalten, den Sie behalten müssen; der Preis ist nur die Lesbarkeit dieses einen Namens.
Behalten Sie Member, benennen Sie die Interna um. Ein durch Serialisierung gebundener Typ braucht, dass seine öffentlichen Eigenschaftsnamen behalten werden, nicht seine privaten Backing-Felder, seine Konstruktoren oder seine Helfer-Methoden. Begrenzen Sie das Keep auf die Member, die ein Serializer oder Binder tatsächlich liest, und lassen Sie den Rest umbenennen — den Qualifizierer members: "public" oben oder ein Attribut nur an den exponierten Membern.
Behalten Sie das Blatt, nicht den Ast. Die häufigste Über-Ausnahme ist, einen ganzen Namespace zu behalten, weil über einen Typ darin reflektiert wird, oder einen ganzen Typ, weil über eine Methode reflektiert wird. Führen Sie es auf den einen Namen zurück, der als String aufgelöst wird. Eine behaltene Methode in einer Klasse mit dreißig Membern lässt neunundzwanzig umbenannt; ein behaltener Namespace lässt keinen.
Wo immer die Plattform es erlaubt, entfernen Sie die Abhängigkeit über den Namen ganz, statt um sie herum zu behalten: [JsonPropertyName] statt DTO-Namen zu behalten, explizite DI-Registrierung statt Konventions-Scanning, kompilierte Bindings (x:Bind) statt String-Pfade. Jede Abhängigkeit, die Sie von einem Namen zu einem Typ umwandeln, ist ein Keep-Eintrag, den Sie löschen dürfen.
Beweisen Sie das Ergebnis
Eine Keep-List ist nur dann richtig, wenn zwei Dinge zugleich wahr sind, und Sie können beim Lesen der Konfiguration nicht erkennen, welches. Bauen Sie das geschützte Assembly und prüfen Sie beides in einem Decompiler. Glass.NET ist der kostenlose, rein statische Begleiter dafür — er liest Metadaten und IL und führt das Binary nie aus.
Bestätigen Sie zuerst, dass nichts überlebt hat, was verborgen sein sollte: Durchstöbern Sie die internen Typen, prüfen Sie, dass sie sich als a, b, c lesen, dass die Methodennamen Ihres Motors weg sind und dass keine Keep-Regel versehentlich mehr getroffen hat, als Sie wollten. Bestätigen Sie zweitens, dass nichts umbenannt wurde, was überleben muss: Prüfen Sie, dass Ihre öffentliche Oberfläche, Ihre behaltenen DTO-Eigenschaften und Ihre Options-Namen intakt sind. Zu-viel-Behalten zeigt sich als lesbare Interna; Zu-wenig-Behalten als umbenannter Vertrag. Das vollständige Verfahren steht in hat Ihre Obfuskierung wirklich funktioniert?.
Führen Sie dann den Test aus, den eine Konfigurations-Durchsicht nicht ersetzen kann: Lassen Sie Ihre Integrations-Suite gegen den geschützten Build laufen — serialisieren Sie ein DTO und machen Sie einen Hin-und-Rück-Durchlauf, üben Sie einen Reflexionspfad aus, binden Sie einen Konfigurationsabschnitt. Namensauflösungs-Brüche sind zur Kompilierzeit unsichtbar und offensichtlich in dem Moment, in dem der Pfad läuft, sodass ein Test, der eine dieser Nähte durchquert, ein zu eng gefasstes Keep sofort erwischt.
Halten Sie sie über die Zeit minimal
Eine Keep-List ist keine Datei zum Einstellen-und-Vergessen. Jedes neue serialisierte DTO, jeder neue Typ, über den reflektiert wird, jede neue Options-Klasse ist ein Kandidat für einen Eintrag — und jedes Refactoring, das eine Abhängigkeit über den Namen entfernt, ist ein Eintrag, den Sie löschen können. Prüfen Sie sie wie Code: Wenn Sie ein Keep hinzufügen, schreiben Sie daneben warum — welcher Serializer, welcher Reflexionsaufruf, welches Binding —, damit die nächste Person feststellen kann, ob es seinen Platz noch verdient. Eine Keep-List, deren jede Zeile einen Grund hat, ist eine, die Sie kurz halten können, und eine kurze Keep-List ist der ganze Einsatz: die kleinstmögliche Menge an Namen, die im Klartext bleiben, während Nebula.NET alles andere umbenennt, verschlüsselt und schützt.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.