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

Obfuskierung jenseits der Umbenennung: Warum umbenanntes .NET sich noch wie Quellcode liest

Das Umbenennen von Bezeichnern ist die schwächste Obfuskierungsschicht: Ein Decompiler auf nur umbenanntem Code zeigt Ihre Logik, Ihre Strings und Ihre Konstanten weiterhin unverändert. Hier steht, was das Umbenennen verbirgt und was nicht — und die Schichten (String-Verschlüsselung, Konstanten-Maskierung, Kontrollfluss-Glättung und Virtualisierung), die .NET wirklich schwer lesbar machen.

Öffnen Sie fast jede .NET-App in einem Decompiler, und das Erste, was auffällt, ist, wie lesbar sie ist. Decompiler rekonstruieren C#, das dem Original sehr nahekommt, denn der Compiler bewahrt im IL weit mehr Struktur, als die meisten Entwickler erwarten. Die übliche Antwort lautet „obfuskiere es”, und die übliche Obfuskierung ist Umbenennen: aus ValidateLicense wird a, aus expiryDate wird b. Das hilft ein wenig. Es täuscht auch viele Teams, die sich geschützt wähnen, weil sie das Ergebnis nie durch einen Decompiler betrachten. Tun Sie es, zerbricht die Illusion.

Was das Umbenennen zurücklässt

Nehmen wir eine triviale Lizenz-Prüfung:

public bool IsActivated(string key)
{
    if (string.IsNullOrEmpty(key)) return false;
    var parts = key.Split('-');
    if (parts.Length != 4 || parts[0] != "LIC") return false;
    long expiry = long.Parse(parts[3]);
    return expiry > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}

Nach reiner Umbenennungs-Obfuskierung liefert ein Decompiler etwa dies:

public bool a(string A_0)
{
    if (string.IsNullOrEmpty(A_0)) return false;
    string[] b = A_0.Split('-');
    if (b.Length != 4 || b[0] != "LIC") return false;
    long c = long.Parse(b[3]);
    return c > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}

Die Namen sind weg. Sonst nichts. Der Kontrollfluss ist identisch, der Aufruf von DateTimeOffset.UtcNow.ToUnixTimeSeconds() steht im Klartext (Framework-Namen lassen sich nicht umbenennen), die Struktur eines Lizenzschlüssels ist ausbuchstabiert, und der magische String "LIC" sitzt da und sagt dem Angreifer genau, was zu fälschen ist. Ein Leser braucht Ihre Variablennamen nicht, um diese Methode zu verstehen; er braucht dreißig Sekunden. Das Umbenennen hob die Lesekosten von „trivial” auf „etwas weniger trivial”.

Das ist die Obergrenze des Umbenennens. Es verbirgt Beschriftungen. Ihre Logik, Ihre Literale und Ihre Konstanten sind keine Beschriftungen.

Schicht 1 — String-Verschlüsselung

String-Literale sind das Wertvollste, was ein Leser erntet, denn sie dokumentieren sich selbst: Fehlermeldungen, Registry-Pfade, API-Endpunkte, das "LIC"-Präfix oben. Die String-Verschlüsselung entfernt sie aus den Metadaten und ersetzt jedes Literal durch einen Aufruf, der es zur Laufzeit entschlüsselt:

if (b.Length != 4 || b[0] != Strings.Get(0x4a1)) return false;

Nun zeigt ein Decompiler Strings.Get(0x4a1) statt "LIC". Der Wert existiert zur Laufzeit weiterhin im Speicher, das ist also keine Kryptografie gegen einen aktiven Debugger — aber es vereitelt den überwältigend häufigen Angriff, die Binärdatei zu lesen und ihre Strings zu grep-en. Die Liste der Literale, die früher Ihr ganzes Programm kartierte, ist weg.

Schicht 2 — Konstanten-Maskierung

Das nächste kostenlose Geschenk an den Leser sind numerische Konstanten: Sitzplatz-Limits, Timeouts, Feature-Schwellen, die 4 und die 3, die Ihr Schlüsselformat beschreiben. Die Konstanten-Maskierung ersetzt literale Zahlen durch kleine zur Laufzeit ausgewertete Ausdrücke, sodass aus parts.Length != 4 etwas wird, das der Decompiler als Arithmetik über undurchsichtige Werte darstellt statt als gedruckte 4. Einzeln geringfügig; zusammen entfernt es die zweiteinfachste Art zu verstehen, worauf eine Methode prüft.

Schicht 3 — Kontrollfluss-Glättung

Umbenennen, String-Verschlüsselung und Konstanten-Maskierung lassen die Form der Methode intakt: Der Decompiler druckt weiterhin saubere if/else/while-Blöcke. Die Kontrollfluss-Glättung bricht das. Sie schreibt die Methode als eine einzige Schleife um einen Zustandsautomaten-switch um, wo jeder ursprüngliche Basisblock zu einem Fall wird und eine Dispatcher-Variable entscheidet, was als Nächstes läuft:

int state = 0;
while (true)
{
    switch (state)
    {
        case 0: /* original block A */ state = 7; break;
        case 7: /* original block B */ state = 2; break;
        // ... blocks reordered, linked only by the dispatcher
        case 2: return result;
    }
}

Der Decompiler kann die ursprüngliche if/while-Struktur nicht mehr rekonstruieren, weil diese Struktur in Daten aufgelöst wurde. Ein Mensch kann sie noch nachvollziehen, aber „einen geglätteten Zustandsautomaten mit verschlüsselten Strings und maskierten Konstanten nachvollziehen” ist ein anderer Nachmittag als „die umbenannte Methode lesen”.

Renaming+ String enc.+ Const. mask+ CF flatten+ Virtualize

Schicht 4 — Virtualisierung, für den Code, auf den es ankommt

Alles Obige kompiliert weiterhin zu IL, und IL hat exzellente Decompiler. Für die Handvoll Methoden, die wirklich schützenswert sind — die Lizenzprüfung, ein Preisalgorithmus, ein zentrales Geschäftsgeheimnis — geht die Methoden-Virtualisierung weiter: Sie ersetzt das IL der Methode durch einen eigenen Bytecode und liefert einen winzigen Interpreter mit, der ihn ausführt. Für einen Bytecode, den es vor Ihrem Build nicht gab, existiert kein Decompiler. Ein Angreifer muss zuerst die virtuelle Maschine rückentwickeln und dann das darauf laufende Programm. Das ist teuer — und genau das ist der Zweck.

Die Virtualisierung ist die schwerste Transformation, also wenden Sie sie gezielt an — auf die wenigen Methoden, bei denen die Lesekosten in Tagen statt Minuten gemessen werden sollten — und lassen Sie den Rest der Assembly auf den leichteren Schichten.

Die praktische Erkenntnis

Umbenennen ist eine gute erste und eine schlechte letzte Schicht. Behandeln Sie Schutz als Stapel: benennen Sie breit um, verschlüsseln Sie Strings und maskieren Sie Konstanten über alles Sensible, glätten Sie den Kontrollfluss Ihres Durchsetzungs- und IP-Codes, und virtualisieren Sie die wenigen Methoden, deren Logik Sie am dringendsten bewahren müssen. Der Test, ob es funktioniert hat, ist einfach und der einzige, der zählt: Öffnen Sie Ihren geschützten Build in einem Decompiler und lesen Sie ihn. Wenn Ihre Lizenzprüfung noch ihre eigene Geschichte erzählt, haben Sie beim Umbenennen aufgehört.

Nebula.NET testen

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