Obfuskierte Namen über Releases hinweg stabil halten mit der Seed-Map von Nebula
Standardmäßig benennt jeder Obfuskierungslauf von Grund auf neu, sodass eine Methode, die in 1.4.0 „a" hieß, in 1.4.1 zu „q" wird und jede archivierte Deobfuskierungs-Map in dem Moment verrottet, in dem Sie ausliefern. seedMapFile von Nebula.NET fixiert die Ausgabe an ihrem Platz: Unveränderte Member behalten den obfuskierten Namen vom letzten Release, nur neuer Code bekommt neue Namen. So funktioniert die Symbol-Map, warum stabile Namen Crash-Symbolisierung und Binär-Diffs handhabbar machen, und wie Sie inkrementelle Obfuskierung in CI verdrahten.
Liefern Sie einen obfuskierten .NET-Build aus, und Sie erhalten eine Zuordnung: MyApp.Billing.Invoice.Recalculate wurde zu n.a.b, und Sie legen die Map beiseite, um die Crash-Reports zu dekodieren, die eintreffen werden. Liefern Sie eine Woche später 1.4.1 mit einem Einzeiler-Fix aus, und dieselbe Methode heißt jetzt q.c.f. Nichts an ihr hat sich geändert, aber ihr obfuskierter Name schon — weil der Standard-Obfuskierungslauf das gesamte Assembly von Grund auf neu benennt und sich die Nummerierung des Renamers in dem Moment verschoben hat, in dem Sie drei Klassen weiter ein Feld hinzugefügt haben.
Dieses Rauschen ist still und leise teuer. Ihre 1.4.0-Map kann keinen 1.4.1-Stacktrace lesen. Ein Binär-Diff zwischen den beiden Releases ist ein Meer aus Umbenennungen, in dem Ihr eigentlicher Fix begraben liegt. Und Sie können nie einen rohen obfuskierten Trace ansehen und denken „ach, das ist wieder der Invoice-Bug”, weil der Name in jedem Build anders ist. Die Seed-Map von Nebula.NET beseitigt das Rauschen: Speisen Sie die Map des letzten Release zurück ein, und unveränderte Member behalten die Namen, die sie bereits hatten. Dieser Beitrag erklärt, wie das funktioniert und warum es sich lohnt, es einzurichten.
Was ein Lauf bereits ausgibt
Sie müssen nichts einschalten, um das Rohmaterial zu bekommen. Jeder Nebula-Lauf schreibt eine Symbol-Map neben das obfuskierte Assembly — MyApp.dll erzeugt MyApp.symbols.json — und zeichnet jede durchgeführte Umbenennung auf:
{
"tool": "Nebula.NET",
"toolVersion": "1.1.6",
"generatedUtc": "2026-10-10T09:00:00Z",
"assembly": "MyApp",
"entries": [
{ "kind": "Method", "original": "MyApp.Billing.Invoice.Recalculate", "obfuscated": "n.a.b", "token": "0x06000123" },
{ "kind": "Type", "original": "MyApp.Billing.Invoice", "obfuscated": "n.a", "token": "0x02000011" },
{ "kind": "Field", "original": "MyApp.Billing.Invoice._subtotal", "obfuscated": "n.a.d", "token": "0x04000045" }
]
}
Jeder Eintrag trägt vier Dinge: die Symbolart kind, den vollqualifizierten original-Namen, den obfuscated-Namen, den es erhalten hat, und das Metadaten-token — die MethodDef-/TypeDef-/FieldDef-RID im Ausgabemodul. Dieses token macht die Map zu einem präzisen Zwei-Wege-Index statt einer unscharfen Namensliste, und es ist das, was nebula deobfuscate --map MyApp.symbols.json liest, um einen obfuskierten Stacktrace zurück in echte Namen zu verwandeln. So weit, so normal — Sie archivieren diese Datei mit jedem Release und dekodieren Traces dagegen.
Das Problem ist, dass die Map des nächsten Release eine andere Datei mit anderen obfuscated-Werten für dieselben original-Namen ist. Das Archiv wächst um eine inkompatible Map pro Release.
Seeding: die Namen weitertragen
Die Seed-Map schließt den Kreis. Zeigen Sie mit seedMapFile auf die .symbols.json des vorherigen Release, und Nebula konsultiert sie, bevor es Namen vergibt:
{
"preservePublicApi": true,
"controlFlowObfuscation": true,
"encryptStrings": true,
"obfuscateConstants": true,
"metadataHardening": true,
"seedMapFile": "maps/MyApp-1.4.0.symbols.json"
}
Jetzt läuft der Renamer in zwei Phasen. Für jedes Member, das unter demselben ursprünglichen Namen noch existiert, schlägt er den obfuskierten Namen nach, den der Seed zugewiesen hat, und verwendet ihn wörtlich wieder. Nur Member ohne Eintrag im Seed — wirklich neuer Code oder Code, dessen Identität sich geändert hat — fallen zur frischen Namensgenerierung durch, und diese frischen Namen werden so gewählt, dass sie mit keinem Namen kollidieren, den der Seed bereits vergeben hat. Das Ergebnis ist ein Assembly, in dem die Umbenennung inkrementell ist: Der Diff gegen das letzte Release ist Ihre tatsächliche Änderung, keine globale Neunummerierung.
Warum sich stabile Namen auszahlen
Crash-Symbolisierung über Versionen hinweg. Der praktische Gewinn ist, dass eine archivierte Map mehr als einen Build dekodiert. Wenn Recalculate in 1.4.0, 1.4.1 und 1.4.2 n.a.b ist, löst sich ein Trace, der in n.a.b landet, gegen jede dieser Maps auf, und Sie lernen, den obfuskierten Namen auf Anhieb zu erkennen — n.a.b ist „die Rechnungsneuberechnung”, Punkt. Eine Crash-Report-Pipeline kann nach obfuskiertem Frame gruppieren und Ihnen zeigen, dass dieselbe Methode über eine Spanne von Releases verantwortlich ist, bevor jemand nebula deobfuscate ausführt, weil das Symbol die stabile Identität ist, gegen die Sie die ganze Zeit gesammelt haben.
Diffs, die die Änderung zeigen, nicht das Rauschen. Delta-Patcher (ClickOnce, Squirrel, Ihr eigener Binär-Diff-Updater) erzeugen nur dann winzige Updates, wenn die meisten Bytes des Assemblys unverändert sind. Eine Umbenennung von Grund auf verschiebt nahezu jeden Namen und damit nahezu jedes Byte, sodass ein Einzeiler-Fix als fast vollständiger Download ausgeliefert wird. Geseedete Obfuskierung hält die unveränderten Member Byte für Byte vergleichbar, sodass der Patch proportional zur tatsächlichen Änderung ist. Dasselbe gilt, wenn ein Mensch zwei dekompilierte Builds nebeneinander prüft, um zu bestätigen, dass ein Hotfix nur das getan hat, was er behauptet: Mit stabilen Namen ist der Diff lesbar; ohne sie ist er Rauschen.
Eine erkennbare Zuordnung, über die Sie nachdenken können. Über eine Release-Serie wird die Map zu einem überwiegend anhängenden Verzeichnis. Neue Einträge erscheinen, wenn Sie Code hinzufügen; bestehende Einträge bleiben, wo sie sind. Sie können zwei Maps diffen und in Klartext ablesen, welche Member in diesem Release genau neu sind — ein für sich genommen überraschend nützliches Signal.
In CI verdrahten
Die Mechanik: bauen, geseedet aus der Map des letzten Release obfuskieren, und die Map dieses Builds archivieren, damit der nächste Build daraus seeden kann. Die Map lebt außerhalb des Quellbaums, weil sie Ihre Symbole benennt; ein Artefaktspeicher oder ein geschützter Branch ist das übliche Zuhause.
# Pseudocode für einen Schritt einer Release-Pipeline.
steps:
- run: dotnet build -c Release
# Die Map des vorherigen Release holen; beim allerersten Release ist das ein No-op,
# und Nebula benennt von Grund auf neu (es gibt noch nichts, woraus geseedet werden könnte).
- run: fetch-artifact MyApp-latest.symbols.json -> maps/MyApp-prev.symbols.json
- run: nebula protect --config nebula.json # Konfiguration setzt seedMapFile: maps/MyApp-prev.symbols.json
# Die Map DIESES Builds als neues "latest" archivieren, damit das nächste Release daraus seedet,
# und eine versionsgestempelte Kopie zum Dekodieren der Crash-Reports dieses Builds behalten.
- run: publish-artifact MyApp.symbols.json as MyApp-latest.symbols.json
- run: publish-artifact MyApp.symbols.json as MyApp-${VERSION}.symbols.json
Zwei praktische Hinweise. Erstens hat das allererste Release keinen Seed — das ist in Ordnung; Nebula benennt von Grund auf neu und gibt die erste Map aus, die zum Seed für Release zwei wird. Zweitens: Behalten Sie zusätzlich zum rollierenden latest eine versionsgestempelte Kopie jeder Map (MyApp-1.4.1.symbols.json), denn sie ist das, was Crash-Reports aus genau diesem Build dekodiert. Das rollierende latest dient dem Seeding des nächsten Builds; die gestempelten Kopien dienen der Symbolisierung für immer.
Was Seeding nicht tut
Seeding ist ein operatives Werkzeug, kein Sicherheitsregler, und es lohnt sich, dabei präzise zu sein. Es fixiert, welchen obfuskierten Namen ein Member bekommt; es schwächt nicht die Obfuskierung eines einzelnen Builds. Ein Angreifer, der Ihr 1.4.0-Assembly besitzt, hat bereits eine vollständig geschützte Binärdatei — String-verschlüsselt, Kontrollfluss-abgeflacht, Metadaten-gehärtet. Dieselben Namen in 1.4.1 wiederzuverwenden verrät diesem Angreifer nichts, was ihm die 1.4.0-Binärdatei nicht schon verraten hätte, denn die Namen sind in beiden undurchsichtig. Die Umbenennung bleibt deterministisch und kollisionsfrei, jeder andere Durchlauf läuft genau wie konfiguriert, und es gibt keinen Leak ursprünglicher Namen in der Ausgabe — die Map, die die Originale enthält, gehört Ihnen, ist auf Ihrer Seite archiviert und wird niemals im Assembly ausgeliefert. Was Sie gewinnen, liegt ausschließlich auf Ihrer Seite des Zauns: Maps, die weiter funktionieren, Diffs, die klein bleiben, und Crash-Reports, die Sie über eine ganze Release-Serie hinweg auf einen Blick lesen können.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.