Skip to Content
Neue Version 12 verfügbar 🎉
ObfuscatorCodeverschlüsselungHardware-Dongle-Bindung

Hardware-Dongle-Bindung

Eine .NET-Anwendung an einen Hardware-Lizenzdongle binden, sodass ausgetauschte oder durch Stubs ersetzte Dongle-DLLs die Codeverschlüsselung nicht umgehen können.

Hersteller von Hardware-Lizenzdongles wie KEYLOK, SafeNet Sentinel, Wibu CodeMeter, Marx CrypToken und ähnlichen liefern eine native DLL aus, die ihre Kunden aus .NET per P/Invoke aufrufen, um sich gegenüber dem physischen Gerät zu authentifizieren. Die wichtigste Bedrohung für solche Integrationen ist der DLL-Austausch: Ein Angreifer ersetzt die DLL des Herstellers durch einen Stub, der bei jedem Aufruf „Lizenz gültig“ zurückgibt, und die geschützte Anwendung läuft unbekümmert weiter.

Dieser Artikel beschreibt ein Muster, das diesen Angriff abwehrt, indem es die Codeverschlüsselung von Babel Obfuscator mit drei weiteren Schutzebenen kombiniert. Ein lauffähiger Referenz-PoC steht am Ende der Seite zum Download bereit.

Das falsche Denkmodell

Der naive Ansatz verwendet den Dongle als boolesche Lizenzschranke:

if (!Keylok.IsAuthenticated()) Environment.Exit(1); RunBusinessLogic();

Das bricht schon unter einem DLL-Austausch zusammen, für den eine einzige Zeile genügt. Eine gefälschte Keylok.dll, die bei jedem Aufruf true zurückgibt, hebelt die Prüfung vollständig aus. Nur den Wrapper IsAuthenticated zu verschlüsseln, hilft ebenfalls nicht, weil dieser Wrapper die native DLL so oder so aufrufen muss. Ist die DLL gefälscht, hat der Wrapper nichts, wogegen er prüfen könnte.

Das richtige Denkmodell

Der Dongle ist kein Lizenzprüfer. Er ist ein Entschlüsselungsorakel.

Die Codeverschlüsselung wandelt IL-Methodenrümpfe in Bytecode der Babel-VM um und verschlüsselt sie mit einem AES-Schlüssel, der aus einem Passwort abgeleitet wird. Das Passwort liefert zur Laufzeit eine Passwort-Callback-Methode. In einer hardwaregebundenen Integration fragt dieser Callback das Passwort beim Dongle ab, statt es aus einer Lizenzdatei zu lesen:

managed business code --(encrypted with password K) native dongle DLL --(plain, talks to the dongle hardware) dongle hardware --(stores K behind tamper-resistant crypto)

Eine ausgetauschte DLL ist damit kein Problem mehr. Sie kann über alles lügen, aber sie kann das Passwort nicht erfinden. Ohne das Passwort hat die Babel Virtual Machine nichts zu entschlüsseln, und die Anwendung bleibt funktionslos.

Die native DLL kann Babel nicht verschlüsseln: Sie ist nicht verwalteter Code und unterliegt der Kontrolle des Dongle-Herstellers. Der Kniff ist, dass die native DLL gar nicht verschlüsselt werden muss. Die verwaltete Geschäftslogik zu verschlüsseln, die den Dongle verwendet, erreicht dasselbe Ziel, weil eine gefälschte DLL das Entschlüsselungspasswort nicht herausgeben kann.

Glossar

Der Rest dieses Artikels verwendet eine kleine Menge von Symbolen und Operationen. Überfliegen Sie die Tabelle einmal, bevor Sie die Schutzebenen unten lesen. Alles Weitere baut darauf auf.

SymbolNameWo es liegtWer es erzeugtWas es ist
KBabel-Passwort (core)Auf dem Dongle (verpackt), zur Laufzeit herausgegebenDer Entwickler, beim PaketierenDas Passwort, mit dem Babel die Geschäftsmethoden verschlüsselt und entschlüsselt. In der Produktion pro Kunde.
HKHardwareschlüsselIm Chip des Dongles, nie auslesbarDer Dongle-Hersteller, bei der ProvisionierungDer manipulationssichere Schlüssel des Dongles. Mit ihm verpackt und entpackt der Dongle Daten auf dem Gerät selbst.
tokenVerpacktes PasswortEingebettete Ressource in der verschleierten AssemblyDer Entwickler, beim Paketierentoken = wrap(K, HK). Nutzlos ohne den Dongle, der als Einziger K wiederherstellen kann.
BBootstrap-PasswortBei der Verschleierung verbraucht; das Attribut [Obfuscation] wird aus der Ausgabe entferntDer Entwickler, einmal pro ProduktFestes Passwort, das den Code zum Abrufen des Passworts verschlüsselt (die Ebene der verketteten Verschlüsselung).
LLiveness-PasswortBei der Verschleierung verbraucht; das Attribut [Obfuscation] wird aus der Ausgabe entferntDer Entwickler, einmal pro ProduktFestes Passwort, das den Code des Challenge-Response-Prüfers verschlüsselt.
IIntegritätspasswortBei der Verschleierung verbraucht; das Attribut [Obfuscation] wird aus der Ausgabe entferntDer Entwickler, einmal pro ProduktFestes Passwort, das den Code für das Signatur-Pinning der nativen DLL verschlüsselt.
nonceFrische ZufallsbytesBei jedem Aufruf im Speicher erzeugtDie Anwendung, zur LaufzeitEine zufällige Challenge von 16 Byte, die an den Dongle gesendet wird. Bei jedem Aufruf anders, sodass Antworten nicht erneut eingespielt werden können.
K_i / HK_iVarianten pro KundeEin Paar pro ausgelieferter LizenzDer Entwickler, bei der AuslieferungJeder Kunde erhält ein eindeutiges K_i in seinem Build und ein eindeutiges HK_i, das auf seinem Dongle provisioniert wird.

Einige Begriffe zu den Operationen selbst:

  • wrap / unwrap (verpacken und entpacken): das Paar kryptografischer Operationen des Dongles. wrap(K, HK) erzeugt beim Paketieren das Token (in der Regel eine AES-Verschlüsselung mit HK als Schlüssel). Der Dongle führt zur Laufzeit das passende unwrap(token, HK) aus und gibt K zurück. Die native DLL des Herstellers ist nur der Bote, die eigentliche Kryptografie läuft im Dongle.
  • Quelle (source): ein Babel-Konzept, eine benannte Gruppe verschlüsselter Methoden, die sich ein Passwort teilen. Jedes Attribut [Obfuscation(Feature="msil encryption:source=<name>;...")] weist seine Methode einer Quelle zu. Dieser Artikel verwendet vier Quellen: core (Geschäftscode, Passwort K), bootstrap (Abruf des Passworts, Passwort B), liveness (Prüfer, Passwort L) und integrity (DLL-Prüfung, Passwort I).
  • Passwort-Callback: die statische Methode, die Babel zur Laufzeit aufruft, um das Passwort einer bestimmten Quelle zu erhalten. Sie ist mit [Obfuscation(Feature="msil encryption get password")] gekennzeichnet. Den grundlegenden Mechanismus beschreibt Passwortgeschützter Code.

Vier Schutzebenen

Eine produktionsreife Integration kombiniert vier Schutzebenen.

1. Codeverschlüsselung mit einem Passwort aus dem Dongle

Wenden Sie msil encryption mit einem produktspezifischen Passwort auf die geschäftskritischen Methoden an. Implementieren Sie dann den Passwort-Callback von Babel so, dass er den Dongle auffordert, ein gespeichertes Token zu K zu entpacken:

[Obfuscation( Feature = "msil encryption:source=core;password=<K>;internal=true", Exclude = false)] public static decimal ComputeQuote(decimal basePrice, int quantity) { // real business logic } [Obfuscation(Feature = "msil encryption get password", Exclude = false)] internal static string GetEncryptionPassword(string source) { return Inner(source); }

Inner liest einen Chiffretext (das Token) aus einer eingebetteten Ressource, übergibt ihn dem Dongle und gibt das Passwort im Klartext zurück. Beim Paketieren berechnet der Entwickler einmalig token = wrap(K, HK), wobei HK der hardwaregeschützte Schlüssel ist, der bereits auf dem Dongle des Kunden provisioniert ist.

2. Verkettete Verschlüsselung des Callbacks selbst

Der Callback Inner ist selbst ein lohnendes Ziel für einen Angreifer, der den Dongle kurzschließen will. Verschlüsseln Sie ihn mit einem zweiten, festen Passwort:

[Obfuscation( Feature = "msil encryption:source=bootstrap;password=<B>;internal=true", Exclude = false)] private static string Inner(string source) { byte[] token = LoadToken(source); byte[] plain = new byte[token.Length]; int rc = KeylokInterop.DongleUnwrap( token, token.Length, plain, plain.Length, out int written); if (rc != 0) throw new InvalidOperationException("Dongle unwrap failed"); return Encoding.ASCII.GetString(plain, 0, written); }

B wird Babel bei der Verschleierung übergeben und dort verbraucht: Babel entfernt das Attribut [Obfuscation] vollständig aus der Ausgabe-Assembly. Das Passwort erscheint in der ausgelieferten Binärdatei weder als Metadaten noch als Blob eines benutzerdefinierten Attributs noch als Klartext-Zeichenfolge, die ein Decompiler zutage fördern könnte. Um es wiederherzustellen, müsste ein Angreifer den internen Schutz überwinden, mit dem Babel den verschlüsselten Methodenrumpf sichert. Das ist deutlich schwerer, als eine Konstante aus einer .dll zu lesen. Die einzige Aufgabe dieses Passworts ist es, den Algorithmus der Dongle-Abfrage vor statischer Analyse zu schützen: In Verbindung mit Kontrollflussverschleierung und Zeichenfolgenverschlüsselung genügt das, um alle bis auf die hartnäckigsten Reverse Engineers abzuschrecken.

3. Liveness-Prüfung bei jedem Aufruf in verschlüsseltem Code

Diese Ebene wehrt den Sniff-and-Replay-Angriff ab:

Ein Angreifer leiht sich einen echten Dongle, hängt sich in den verwalteten Code ein, fängt das entpackte K ab und liefert eine gefälschte DLL aus, die K direkt zurückgibt. Die Entschlüsselung von Babel gelingt mit dem abgefangenen K, und die Geschäftsmethoden scheinen zu laufen.

Die Abhilfe: Die Geschäftsmethoden selbst (bereits unter source=core verschlüsselt) führen bei jedem Aufruf ein frisches Challenge-Response-Verfahren mit dem Dongle durch:

[Obfuscation(Feature = "msil encryption:source=core;password=<K>;internal=true", Exclude = false)] public static decimal ComputeQuote(decimal basePrice, int quantity) { if (!DongleLiveness.Verify()) return -1m; // silent sabotage // real business logic } [Obfuscation(Feature = "msil encryption:source=liveness;password=<L>;internal=true", Exclude = false)] internal static bool Verify() { byte[] nonce = RandomNumberGenerator.GetBytes(16); byte[] resp = new byte[16]; int rc = KeylokInterop.DongleChallenge(nonce, resp); if (rc != 0) return false; byte[] expected = ComputeExpectedMac(nonce); // uses a public verifier return CryptographicOperations.FixedTimeEquals(resp, expected); }

Mit dem abgefangenen K kann Babel zwar Verify selbst entschlüsseln, es enthält aber nicht das Challenge-Response-Geheimnis des Dongles. Die frische Nonce lässt sich ohne den Dongle nicht beantworten. Asymmetrische Dongles (ECDSA, RSA) machen das lückenlos. Symmetrische Dongles legen die Hürde immer noch deutlich höher, weil der Prüfschlüssel nur in verschlüsseltem Code vorliegt.

Zwei wichtige Taktiken in Verify:

  • Frische Nonce bei jedem Aufruf. Keine Memoisierung, kein Caching.
  • Stille Sabotage im Fehlerfall (einen falschen Wert, ein leeres Ergebnis oder ein verfälschtes Byte zurückgeben), kein throw. Eine Ausnahme verrät die Stelle der Prüfung, eine falsche Zahl nicht. Der Angreifer entdeckt die Prüfung nur, indem er viele Läufe mit einer Referenz vergleicht.

4. Integritäts-Pinning der nativen DLL

Unabhängig davon, ob der Dongle korrekt arbeitet, kann die Anwendung die Identität der DLL prüfen, die sie gleich aufrufen wird:

[Obfuscation(Feature = "msil encryption:source=integrity;password=<I>;internal=true", Exclude = false)] internal static bool VerifyNativeDll(string path) { var cert = X509Certificate.CreateFromSignedFile(path); const string EXPECTED_THUMBPRINT = "AABBCC..."; // pinned at build time return WinTrust.VerifyAuthenticode(path) && cert.GetCertHashString().Equals(EXPECTED_THUMBPRINT, StringComparison.OrdinalIgnoreCase); }

Der Zertifikatfingerabdruck des Herausgebers, also des Herstellers (KEYLOK Inc., Thales-SafeNet, Wibu-Systems, …), ist in einer verschlüsselten Quelle integrity gepinnt. Das verhindert den Austausch gegen jede DLL, die nicht vom rechtmäßigen Hersteller signiert ist, auch gegen DLLs, die die Liveness-Prüfung bestehen, weil sie heimlich an einen echten Dongle weiterleiten.

Zusammen mit der Manipulationserkennung von Babel, die nach der Verschleierung Integritätsprüfungen in die Assembly selbst einfügt, ergibt das fünf aktive Abwehrmaßnahmen gegen den Angriff durch Verändern und Weiterverbreiten.

Schlüssel pro Kunde

Ein einziges, von allen Kunden geteiltes K birgt das Risiko eines Class Break: Wird eine Installation erfolgreich geknackt, ist die gesamte Produktfamilie offen. Empfohlen wird die Bereitstellung mit Babel-Builds pro Kunde:

Dongle provisionieren

Provisionieren Sie den Dongle des Kunden mit einem eindeutig erzeugten Hardwareschlüssel HK_i und einem eindeutig erzeugten Babel-Passwort K_i.

Mit dem Passwort des Kunden neu erstellen

Führen Sie Babel bei der Auslieferung erneut für den Quellbaum dieses Kunden aus, wobei K_i in die [Obfuscation]-Attribute eingesetzt (oder über XML-Regeln übergeben) wird.

Verpacktes Token einbetten

Berechnen Sie token_i = wrap(K_i, HK_i) und betten Sie es in den Build ein.

Um Kunde i zu knacken, muss ein Angreifer K_i aus dessen Installation und HK_i aus dessen Dongle extrahieren. Beides hilft bei Kunde j nicht weiter. Der betriebliche Aufwand ist ein Babel-Build pro Auslieferung, der sich in CI automatisieren lässt und wenige Minuten pro Kunde dauert.

Zusammenfassung des Bedrohungsmodells

BedrohungAbgewehrt durch
Statische Dekompilierung der GeschäftslogikCodeverschlüsselung (core)
Austausch gegen eine Stub-DLL mit return successCodeverschlüsselung + Liveness-Prüfung
Einhängen in den verwalteten Passwort-CallbackVerkettete Verschlüsselung + Manipulationserkennung
Abfangen von K, gefolgt von einer gefälschten DLL mit fest codiertem KLiveness-Prüfung
Patchen des IL-Codes, um Aufrufe von Verify() zu entfernenManipulationserkennung
Austausch gegen eine anders signierte DLLAuthenticode-Pinning
Ausbreitung einer Kompromittierung über Kunden hinwegSchlüssel pro Kunde
Vollständiges Umgehen der Babel Virtual MachineMethoden leisten echte Arbeit, statt nur den Zugang zu sperren

Praktische Checkliste

  • Wenden Sie msil encryption mit source=core auf die Geschäftsmethoden an.
  • Wenden Sie msil encryption mit source=bootstrap auf den Abruf des Passworts an.
  • Wenden Sie msil encryption mit source=liveness auf den Prüfer an.
  • Wenden Sie msil encryption mit source=integrity auf die DLL-Prüfung an.
  • Implementieren Sie [Obfuscation(Feature="msil encryption get password")].
  • Betten Sie das verpackte Token als Ressource ein.
  • Erstellen Sie den Build mit --tamperingdetection --antidebugging --controlflow --stringencryption.
  • Signieren Sie die Assembly (SignAssembly=true).
  • Pinnen Sie den Authenticode-Fingerabdruck der nativen DLL.
  • Stellen Sie Builds pro Kunde mit eindeutigem K_i aus.

Referenzimplementierung

Download: HardwareDongleBinding.zip (20 KB)

Die Zip-Datei enthält einen eigenständigen, lauffähigen Proof of Concept. Er verwendet eine nachgebildete native DLL (Mock) mit XOR-Primitiven, sodass er sich ohne externe Abhängigkeiten erstellen lässt. Die Struktur ist identisch mit einer Produktionsintegration, die den Mock durch die echte native Bibliothek des Herstellers ersetzt.

Voraussetzungen

Der PoC lässt sich unter Windows, macOS und Linux erstellen und ausführen. Wählen Sie die Zeile für Ihre Plattform:

PlattformC-ToolchainPowerShell.NETBabel
WindowsVisual Studio 2022 Developer Command Promptintegrierte powershell.exe.NET 8 SDKbabel.exe im PATH
macOSXcode Command Line Tools (xcode-select --install)pwsh (brew install powershell).NET 8 SDKbabel aus einem Zip-Paket babel_net80/babel_net90/babel_net100 oder das dotnet-Tool Babel.Obfuscator.Tool, im PATH
Linuxcc / clang (apt install build-essential)pwsh.NET 8 SDKbabel im PATH

Build

# 1. Extract the zip Expand-Archive HardwareDongleBinding.zip -DestinationPath . # Windows # or: unzip HardwareDongleBinding.zip # macOS / Linux cd HardwareDongleBinding # 2. End-to-end build: tokens, managed app, obfuscation, native libs ./Scripts/build.ps1

Öffnen Sie unter Windows zuerst einen Visual Studio Developer Command Prompt, damit cl.exe im PATH liegt. Unter macOS und Linux können Sie jede Shell verwenden. build.ps1 führt fünf Schritte aus:

  1. Jedes Babel-Passwort der einzelnen Quellen mit dem HK des Mock-Dongles verpacken und die entstehenden Tokendateien nach DongleBindingDemo/Tokens/*.bin schreiben.
  2. Das verwaltete Projekt mit dotnet build -c Release erstellen.
  3. Die entstandene Assembly mit Babel verschleiern: --tamperingdetection --antidebugging --controlflow --stringencryption.
  4. Die drei nativen Varianten mit dem C-Compiler der Plattform kompilieren:
    • Windows: DongleMock.dll, DongleMock_Fake.dll, DongleMock_Sniff.dll mit cl.exe.
    • macOS: libDongleMock.dylib, libDongleMock_Fake.dylib, libDongleMock_Sniff.dylib mit clang.
    • Linux: libDongleMock.so, libDongleMock_Fake.so, libDongleMock_Sniff.so mit cc.
  5. Die echte Bibliothek neben der verschleierten Assembly ablegen.

Das [DllImport("DongleMock")] in C# wird automatisch in den zur Plattform passenden Dateinamen aufgelöst.

Drei Szenarien

./Scripts/run-legit.ps1 # OK ./Scripts/run-swap-attack.ps1 # BLOCKED: Babel cannot decrypt ./Scripts/run-sniff-attack.ps1 # BLOCKED: liveness check fails

Legitimer Lauf: Der echte Mock-Dongle ist vorhanden, die Geschäftsmethoden werden entschlüsselt und ausgeführt:

ComputeQuote(120, 250, 0.15) = 24225.00 GenerateReportToken(ACME) = REPORT-ACME CORP-XXXXXXXX Result: OK

Austauschangriff: ersetzt DongleMock.dll durch einen Stub, der bei jedem Aufruf Nullen zurückgibt. Babel leitet aus diesen Nullbytes das falsche Passwort ab und kann die Methodenrümpfe nicht entschlüsseln:

[EXCEPTION] ...: BVM decryption failed Result: BLOCKED

Sniff-and-Replay-Angriff: der interessantere Angriff. Die gefälschte DLL gibt das abgefangene Babel-Passwort direkt zurück, sodass die Entschlüsselung gelingt. Die Liveness-Prüfung bei jedem Aufruf in jeder Geschäftsmethode erkennt dennoch den fehlenden Dongle (die Fälschung kann eine frische Challenge nicht beantworten), und jede Methode springt direkt in ihren Zweig der stillen Sabotage:

ComputeQuote(120, 250, 0.15) = -1 GenerateReportToken(ACME) = REPORT-UNAVAILABLE Result: BLOCKED

Jedes Szenario entspricht einer Zeile der Tabelle des Bedrohungsmodells weiter oben.

An einen echten Dongle anpassen

Um aus dem PoC eine Produktionsintegration zu machen, ändern Sie vier Dateien:

DateiWas zu ändern ist
DongleBindingDemo/KeylokInterop.csErsetzen Sie DongleMock durch den DLL-Namen des Herstellers und gleichen Sie die Signaturen an die API des Herstellers an.
DongleBindingDemo/DongleAuth.csErsetzen Sie das Entpacken im XOR-Stil durch den Aufruf des AES-Leseschutzes des Herstellers.
DongleBindingDemo/DongleLiveness.csErsetzen Sie die XOR-Challenge durch die Challenge-Response-API des Herstellers (HMAC, ECDSA, …).
DongleBindingDemo/ProtectedLogic.csVerschieben Sie Ihre echten Geschäftsmethoden unter source=core und behalten Sie die Prüfung DongleLiveness.Verify() bei.

Die [Obfuscation]-Attribute von Babel, der Handler der Manipulationserkennung und die Buildskripte müssen nicht geändert werden.

Last updated on