Konstanten-Verschlüsselung in .NET: die Zahlen verbergen, die Decompiler gratis lesen
Die String-Verschlüsselung bekommt die Aufmerksamkeit, aber die numerischen Literale in Ihrem Code — Schwellenwerte, Opcodes, magische Zahlen, Offsets — kompilieren zu ldc-Instruktionen, die ein Decompiler sofort liest. Hier steht, wie numerische Konstanten Logik verraten, wie die Konstanten-Verschlüsselung sie durch zur Laufzeit decodierte Werte ersetzt und wo sie neben der String-Verschlüsselung und dem Kontrollfluss-Schutz einzuordnen ist.
Die String-Verschlüsselung bekommt die ganze Aufmerksamkeit im .NET-Schutz, und das zu Recht — die String-Liste eines Decompilers ist eine Landkarte. Aber Strings sind nur die Hälfte der Literale in Ihrem Code. Die andere Hälfte sind Zahlen, und die konkreten Zahlen sind sehr oft die Logik selbst: der Schwellenwert, gegen den eine Lizenzprüfung vergleicht, der Opcode in einem Protokoll-Switch, die Bitmaske, die eine Funktion auswählt, der magische Wert, der einen Header validiert. Sie alle kompilieren zu nackten, sofort lesbaren Lade-Instruktionen. Die Konstanten-Verschlüsselung ist das numerische Gegenstück zur String-Verschlüsselung, und bei sicherheitsrelevantem Code zählt sie genauso.
Wie ein numerisches Literal kompiliert
Wenn Sie ein numerisches Literal schreiben, gibt der C#-Compiler eine der ldc-Instruktionen („load constant”) aus, mit dem Wert direkt dort im IL. Betrachten Sie eine triviale Prüfung:
if (attempts > 3 && headerMagic == 0x5A4D)
Reject();
Das IL trägt beide Zahlen im Klartext:
ldloc.0 // attempts
ldc.i4.3 // the literal 3
ble.s IL_0014
ldloc.1 // headerMagic
ldc.i4 0x5A4D // the literal "MZ" PE signature
bne.un.s IL_0014
call Reject
Ein Decompiler rekonstruiert das genau wie den Quellcode, den Sie geschrieben haben — 3 und 0x5A4D und alles. Die zweite Konstante ist besonders aufschlussreich: 0x5A4D ist die MZ-Signatur des DOS-Headers, sodass ein Leser sofort weiß, dass dieser Code eine PE-Datei beschnüffelt, ohne einen einzigen Kommentar oder String zur Hilfe. Kleine Integer haben eigene kurze Opcodes (ldc.i4.0 bis ldc.i4.8, ldc.i4.s für ein Byte), größere nutzen das vollständige ldc.i4/ldc.i8/ldc.r8, aber in jedem Fall ist der Wert ein literaler Operand, den jeder lesen kann.
Was die Konstanten-Verschlüsselung tut
Die Konstanten-Verschlüsselung schreibt diese literalen Ladevorgänge so um, dass der echte Wert nie im IL erscheint. Jede geschützte Konstante wird in codierter Form gespeichert und an der Aufrufstelle durch eine kleine Decode-Routine ersetzt, die sie zur Laufzeit rekonstruiert. Konzeptuell wird das ldc zu einem Aufruf:
// before:
ldc.i4 0x5A4D
// after (conceptually):
ldc.i4 0x9C31 // an encoded token, not the real value
call int32 <Decode>::N(int32) // returns 0x5A4D at runtime
Die Decode-Routine wird vom Obfuskator erzeugt und ist selbst verschleiert — es ist kein einzelnes offensichtliches xor mit einem sichtbaren Schlüssel direkt neben den Daten. Der Effekt ist, dass ein statischer Decompiler Decode.N(0x9C31) sieht, wo der Quellcode 0x5A4D hatte. Die Zahl ist aus dem Listing verschwunden; sie wiederzugewinnen heißt, die Decode-Logik tatsächlich auszuführen (oder getreu zu emulieren), was genau die Arbeit ist, die statische Analyse zu vermeiden versucht.
Die Zahlen, die es wert sind, verborgen zu werden
Nicht jeder Integer ist interessant — eine Schleife, die bis arr.Length zählt, verrät nichts. Was die Konstanten-Verschlüsselung schützt, ist die Klasse von Literalen, die das Verhalten sind:
- Schwellenwerte und Limits in Prüfungen — die Wiederholungsgrenze
> 3, eine Testtage-Zahl, ein Sitzlimit. Die Zahl zu sehen sagt dem Angreifer genau, gegen welche Grenze er drücken muss. - Magische Zahlen und Signaturen — Header-Bytes wie
0x5A4D, Formatmarker, Checksummen-Seeds. Diese etikettieren genau, was der Code parst oder validiert. - Protokoll- und Zustands-Opcodes — die Integer-Fälle eines
switch, der eine Zustandsmaschine oder ein Drahtprotokoll steuert. Im Klartext ist das ganze Protokoll aus der Sprungtabelle aufzählbar. - Funktions-Flag-Bitmasken — ein Wert wie
0x04, per AND gegen ein Berechtigungsfeld verknüpft, benennt das exakte Bit, das eine Funktion freischaltet, was eine Einladung ist, es zu erzwingen. - Status- und Ergebniscodes — der konkrete Wert, den eine Sicherheitsroutine bei Erfolg zurückgibt, den ein Angreifer liebend gern den Code bedingungslos erzeugen ließe.
In jedem Fall trägt die Zahl die Bedeutung, und sie aus dem statischen Listing zu entfernen erhöht die Kosten, die Prüfung zu verstehen oder zu manipulieren.
Wo sie zu den anderen Transformationen passt
Die Konstanten-Verschlüsselung ist eine Schicht, und sie ist in Kombination am stärksten. Für sich verbirgt sie Werte, lässt aber die umgebende Form intakt; gepaart mit der String-Verschlüsselung schließt sie die andere Hälfte des Literal-Lecks, und gepaart mit der Kontrollfluss-Obfuskierung verbirgt sie nicht nur, was die Konstanten sind, sondern wo die Vergleiche stattfinden. Eine Lizenzprüfung, die von allen dreien geschützt ist, präsentiert dem Leser kein sauberes if (days > 30) gegen ein sichtbares Literal mehr — der Wert wird zur Laufzeit decodiert, die String-Meldungen sind verschlüsselt, und der Zweig selbst ist in einem abgeflachten Kontrollgraphen vergraben.
Sie fügt sich auch mit dem Schutz auf Methodenebene zusammen: Eine Konstante, die nur innerhalb einer verschlüsselten oder virtualisierten Methode decodiert wird, existiert in der ausgelieferten Assembly nie in nackter Form, weil der Methodenkörper, der den Decode-Aufruf trägt, selbst bis zur Ausführung geschützt ist.
Eine Anmerkung zu Kosten und Umfang
Es gibt keinen kostenlosen Schutz. Ein nacktes ldc.i4 ist eine einzige Instruktion; eine decodierte Konstante ist ein Methodenaufruf, also tauscht jedes geschützte Literal ein direktes Laden gegen ein wenig Laufzeitarbeit. Für gewöhnlichen Code ist das unsichtbar. Der einzige Ort zum Nachdenken ist eine wirklich heiße numerische Schleife, die dieselbe Konstante millionenfach liest — dort kann pauschale Konstanten-Verschlüsselung in einem Profil auftauchen. Die Antwort ist Umfang, nicht Vermeidung: Wenden Sie die Konstanten-Verschlüsselung auf den Code an, wo die Zahlen Geheimnisse wert sind — Lizenzierung, Protokollbehandlung, Validierung, Berechtigungsprüfungen — und lassen Sie die arithmetischen Kernel in Ruhe, die keine sensiblen Literale enthalten. Nebula lässt Sie den Schutz auf die Typen und Methoden richten, die ihn verdienen, sodass die sicherheitsrelevanten Konstanten verborgen sind, während die performancekritische Mathematik ein nacktes Laden bleibt.
Die Erkenntnis ist einfach: Ein Decompiler liest Ihre Zahlen ebenso bereitwillig wie Ihre Strings, und bei sicherheitsrelevantem Code sind die Zahlen häufig die ganze Geschichte. Verschlüsseln Sie die Konstanten, die Ihre Logik codieren, kombinieren Sie die Transformation mit String- und Kontrollfluss-Schutz, und beschränken Sie sie auf das, wo sie ihre Kosten verdient — und die sauberen, selbsterklärenden Literale, die einem Leser früher Ihre Prüfungen reichten, verschwinden aus dem Listing.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.