Skip to content
← Alle Beiträge
· Delta1 Labs Obfuskierung.NETLeitfaden

Was Obfuskierung kaputt macht: Reflection, Serialisierung und was auszuschließen ist

Wenn Sie eine funktionierende App zum ersten Mal obfuskieren, kann sie auf überraschende Weise brechen: Das JSON kommt falsch heraus, Reflection liefert null, das Data-Binding hört auf. Die Ursache ist immer dieselbe: Etwas hat ein umbenanntes Mitglied über seinen Namen aufgelöst. Hier steht genau, was bricht, warum, und wie man die richtigen Dinge ausschließt, ohne den Schutz zu schwächen.

Sie fügen einer funktionierenden .NET-App einen Obfuskator hinzu, bauen, führen aus — und etwas, das gestern lief, ist kaputt. Das JSON, das Ihre API zurückgibt, hat Felder namens a und b. Ein Aufruf von Type.GetMethod("Process") liefert null. Ihr WPF-Fenster bindet an nichts. Die Dependency Injection findet einen Dienst nicht. Nichts davon ist ein Fehler des Obfuskators; es ist der häufigste Obfuskierungsfehler, und er hat immer dieselbe Grundursache: etwas hat zur Laufzeit ein umbenanntes Mitglied über seinen Namen aufgelöst.

Warum Namen die Bruchlinie sind

Umbenennen — der Kern der meisten Obfuskierung — macht aus CustomerName ein a, aus CalculateTotal ein b. Der kompilierte Code, der diese Mitglieder aufruft, wird passend umgeschrieben, sodass direkte Aufrufe weiter funktionieren; der Compiler hat sie bereits zu Tokens aufgelöst, nicht zu Namen. Die Gefahr ist jeder Codepfad, der ein Mitglied über seinen String-Namen zur Laufzeit sucht, denn diese Suche fragt weiterhin nach dem Originalnamen, der in der Assembly nicht mehr existiert.

Das ist der gesamte Fehlermodus. Alles Folgende ist eine konkrete Ausprägung davon.

Die üblichen Opfer

Serialisierung. Ein Serialisierer bildet zwischen Mitgliedsnamen und einem Drahtformat ab. Benennen Sie die Mitglieder um, und die Ausgabe wird mit ihnen umbenannt:

public class Invoice { public decimal Total { get; set; } public string Customer { get; set; } }

JsonSerializer.Serialize(new Invoice { Total = 42, Customer = "Acme" });
// Expected: {"Total":42,"Customer":"Acme"}
// After renaming: {"a":42,"b":"Acme"}   ← your API contract just changed

Schlimmer noch: Die Deserialisierung zuvor gespeicherter Daten schlägt beim Binden nun fehl, weil das JSON Total sagt und die Eigenschaft a heißt. Jeder Typ, der eine Serialisierungsgrenze überquert — API-DTOs, Konfigurationsdateien, gespeicherte Dokumente, Nachrichten-Payloads — ist gefährdet.

Reflection. Jede Suche über den Namen liefert das falsche Mitglied oder nichts:

var mi = typeof(Report).GetMethod("Generate"); // null — it's called 'a' now
typeof(Plugin).GetProperty("Name")?.GetValue(obj); // null
Activator.CreateInstance(Type.GetType("MyApp.Widgets.Chart")); // type name changed → null
Enum.Parse<Status>("Active"); // enum member renamed → throws

Data-Binding ist verkleidete Reflection. {Binding CustomerName} in WPF/XAML, @bind in Blazor und die Auto-Spalten eines DataGrid lösen Eigenschaftsnamen zur Laufzeit als Strings auf; benennen Sie die Eigenschaft um, und das Binding zeigt still nichts an.

Konventionsbasierte Frameworks. DI, die IFooService → FooService per Name abbildet, EF Core, das eine Eigenschaft per Name auf eine Spalte abbildet, AutoMapper, das Mitglieder per Name paart, MVC-Model-Binding: Alles, dessen „Magie” Namensabgleich ist, bricht, wenn sich die Namen ändern.

Die Regel: schließen Sie den Vertrag aus, nicht die Logik

Der Fix ist nicht, weniger zu obfuskieren; es ist, vom Umbenennen genau die Namen auszuschließen, die über den Namen aufgelöst werden, und sonst nichts. In der Praxis ist das eine kleine, identifizierbare Menge:

  • Serialisierte Typen (DTOs, Konfiguration, Nachrichtenverträge).
  • Über Reflection erreichte Mitglieder, einschließlich allem, was ein Framework per Name auflöst.
  • Aus XAML oder anderem Markup referenzierte Typen/Mitglieder.
  • Öffentliche API-Oberfläche einer Bibliothek, gegen die andere kompilieren.

Sie markieren diese, um das Umbenennen zu überspringen — mit einem Attribut oder einer Konfigurationsregel. Nebula.NET nimmt Umbenennungs-Ausschlussregeln nach Namespace, Typ oder Attribut, sodass Sie eine saubere Grenze behalten: Ihr Contracts-Namespace behält seine Namen, der Rest der Assembly wird frei umbenannt.

// Exclude a serialized contract from RENAMING, keep everything else renamed.
[Obfuscation(Feature = "renaming", Exclude = true, ApplyToMembers = true)]
public class Invoice
{
    public decimal Total { get; set; }
    public string Customer { get; set; }
}
Per Name aufgelöst — Namen BEHALTENDTOs, Reflection-Ziele, XAML, öffentliche APIInterne Logik — frei UMBENENNENalles, was direkt im Code aufgerufen wirdBeide erhalten weiterhin:Kontrollfluss-GlättungString-VerschlüsselungKonstanten-Maskierung — Ausschluss nur für Namen

Der robustere Fix: den Drahtnamen entkoppeln

Typen auszuschließen funktioniert, aber für die Serialisierung gibt es ein besseres Muster, das die Fragilität ganz beseitigt: fixieren Sie die serialisierten Namen explizit, sodass sie nie vom Mitgliedsnamen abhängen.

public class Invoice
{
    [JsonPropertyName("total")]    public decimal Total { get; set; }
    [JsonPropertyName("customer")] public string Customer { get; set; }
}

Nun sagt das JSON total und customer, egal wie die Eigenschaft im IL heißt; benennen Sie Total in a um, und das Drahtformat ist unverändert. Das entkoppelt Ihren Vertrag von Ihrem Code, was unabhängig von Obfuskierung gutes Engineering ist (ein einfaches C#-Umbenennungs-Refactoring ändert Ihre API ebenfalls nicht still). Dasselbe Denken gilt anderswo: Referenzieren Sie Enum-Werte über den Enum-Typ, nicht Enum.Parse auf einem String; lösen Sie DI per expliziter Registrierung auf, nicht per Konvention; bevorzugen Sie kompilierte Bindings (x:Bind in UWP/WinUI) gegenüber String-Pfaden, wo die Plattform es anbietet.

Ausschluss ist auf Namensebene, nicht auf Schutzebene

Die wichtigste Nuance: Einen Typ vom Umbenennen auszuschließen lässt ihn nicht ungeschützt. Umbenennen ist eine Transformation unter mehreren. Ein DTO, dessen Eigenschaftsnamen Sie behalten, kann weiterhin seine Methodenrümpfe kontrollfluss-geglättet, seine String-Literale verschlüsselt und seine Konstanten maskiert bekommen. Sie sagen dem Obfuskator „ändere diese Namen nicht”, nicht „schütze diesen Code nicht”. Die Kosten, die Ausschlüsse richtig zu machen, sind also in Schutzbegriffen im Wesentlichen null: Sie behalten volle Abdeckung über die Logik und verzichten nur bei der Handvoll Namen auf das Umbenennen, die die Runtime als Strings sucht.

Ein zuverlässiger Arbeitsablauf

Obfuskieren Sie, und führen Sie dann Ihre echte Testsuite gegen den obfuskierten Build aus — nicht nur gegen den nicht obfuskierten. Namensauflösungs-Brüche sind zur Compile-Zeit unsichtbar und offensichtlich in dem Moment, in dem ein Serialisierungs-Round-Trip oder ein Reflection-Pfad ausgeführt wird, also fängt ein Integrationstest, der ein DTO serialisiert, eine API aufruft oder ein Plugin ausübt, sie sofort. Beheben Sie jeden, indem Sie genau diesen Vertrag ausschließen (oder seine Drahtnamen fixieren), nie indem Sie das Umbenennen pauschal abschalten. Tun Sie das einmal, und „Obfuskierung hat meine App kaputt gemacht” wird zu einem einmaligen Einrichtungsschritt statt zu einem wiederkehrenden Rätsel: Ihre Logik bleibt schwer lesbar, und die Namen, von denen Ihr Programm abhängt, bleiben genau dort, wo es sie erwartet.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.