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.
| Symbol | Name | Wo es liegt | Wer es erzeugt | Was es ist |
|---|---|---|---|---|
| K | Babel-Passwort (core) | Auf dem Dongle (verpackt), zur Laufzeit herausgegeben | Der Entwickler, beim Paketieren | Das Passwort, mit dem Babel die Geschäftsmethoden verschlüsselt und entschlüsselt. In der Produktion pro Kunde. |
| HK | Hardwareschlüssel | Im Chip des Dongles, nie auslesbar | Der Dongle-Hersteller, bei der Provisionierung | Der manipulationssichere Schlüssel des Dongles. Mit ihm verpackt und entpackt der Dongle Daten auf dem Gerät selbst. |
| token | Verpacktes Passwort | Eingebettete Ressource in der verschleierten Assembly | Der Entwickler, beim Paketieren | token = wrap(K, HK). Nutzlos ohne den Dongle, der als Einziger K wiederherstellen kann. |
| B | Bootstrap-Passwort | Bei der Verschleierung verbraucht; das Attribut [Obfuscation] wird aus der Ausgabe entfernt | Der Entwickler, einmal pro Produkt | Festes Passwort, das den Code zum Abrufen des Passworts verschlüsselt (die Ebene der verketteten Verschlüsselung). |
| L | Liveness-Passwort | Bei der Verschleierung verbraucht; das Attribut [Obfuscation] wird aus der Ausgabe entfernt | Der Entwickler, einmal pro Produkt | Festes Passwort, das den Code des Challenge-Response-Prüfers verschlüsselt. |
| I | Integritätspasswort | Bei der Verschleierung verbraucht; das Attribut [Obfuscation] wird aus der Ausgabe entfernt | Der Entwickler, einmal pro Produkt | Festes Passwort, das den Code für das Signatur-Pinning der nativen DLL verschlüsselt. |
| nonce | Frische Zufallsbytes | Bei jedem Aufruf im Speicher erzeugt | Die Anwendung, zur Laufzeit | Eine 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_i | Varianten pro Kunde | Ein Paar pro ausgelieferter Lizenz | Der Entwickler, bei der Auslieferung | Jeder 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 mitHKals Schlüssel). Der Dongle führt zur Laufzeit das passendeunwrap(token, HK)aus und gibtKzurü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, PasswortK),bootstrap(Abruf des Passworts, PasswortB),liveness(Prüfer, PasswortL) undintegrity(DLL-Prüfung, PasswortI). - 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
Kab und liefert eine gefälschte DLL aus, dieKdirekt zurückgibt. Die Entschlüsselung von Babel gelingt mit dem abgefangenenK, 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
| Bedrohung | Abgewehrt durch |
|---|---|
| Statische Dekompilierung der Geschäftslogik | Codeverschlüsselung (core) |
Austausch gegen eine Stub-DLL mit return success | Codeverschlüsselung + Liveness-Prüfung |
| Einhängen in den verwalteten Passwort-Callback | Verkettete Verschlüsselung + Manipulationserkennung |
Abfangen von K, gefolgt von einer gefälschten DLL mit fest codiertem K | Liveness-Prüfung |
Patchen des IL-Codes, um Aufrufe von Verify() zu entfernen | Manipulationserkennung |
| Austausch gegen eine anders signierte DLL | Authenticode-Pinning |
| Ausbreitung einer Kompromittierung über Kunden hinweg | Schlüssel pro Kunde |
| Vollständiges Umgehen der Babel Virtual Machine | Methoden leisten echte Arbeit, statt nur den Zugang zu sperren |
Praktische Checkliste
- Wenden Sie
msil encryptionmitsource=coreauf die Geschäftsmethoden an. - Wenden Sie
msil encryptionmitsource=bootstrapauf den Abruf des Passworts an. - Wenden Sie
msil encryptionmitsource=livenessauf den Prüfer an. - Wenden Sie
msil encryptionmitsource=integrityauf 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_iaus.
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:
| Plattform | C-Toolchain | PowerShell | .NET | Babel |
|---|---|---|---|---|
| Windows | Visual Studio 2022 Developer Command Prompt | integrierte powershell.exe | .NET 8 SDK | babel.exe im PATH |
| macOS | Xcode Command Line Tools (xcode-select --install) | pwsh (brew install powershell) | .NET 8 SDK | babel aus einem Zip-Paket babel_net80/babel_net90/babel_net100 oder das dotnet-Tool Babel.Obfuscator.Tool, im PATH |
| Linux | cc / clang (apt install build-essential) | pwsh | .NET 8 SDK | babel 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:
- Jedes Babel-Passwort der einzelnen Quellen mit dem
HKdes Mock-Dongles verpacken und die entstehenden Tokendateien nachDongleBindingDemo/Tokens/*.binschreiben. - Das verwaltete Projekt mit
dotnet build -c Releaseerstellen. - Die entstandene Assembly mit Babel verschleiern:
--tamperingdetection --antidebugging --controlflow --stringencryption. - Die drei nativen Varianten mit dem C-Compiler der Plattform kompilieren:
- Windows:
DongleMock.dll,DongleMock_Fake.dll,DongleMock_Sniff.dllmitcl.exe. - macOS:
libDongleMock.dylib,libDongleMock_Fake.dylib,libDongleMock_Sniff.dylibmitclang. - Linux:
libDongleMock.so,libDongleMock_Fake.so,libDongleMock_Sniff.somitcc.
- Windows:
- 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 failsLegitimer 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: OKAustauschangriff: 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: BLOCKEDSniff-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: BLOCKEDJedes 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:
| Datei | Was zu ändern ist |
|---|---|
DongleBindingDemo/KeylokInterop.cs | Ersetzen Sie DongleMock durch den DLL-Namen des Herstellers und gleichen Sie die Signaturen an die API des Herstellers an. |
DongleBindingDemo/DongleAuth.cs | Ersetzen Sie das Entpacken im XOR-Stil durch den Aufruf des AES-Leseschutzes des Herstellers. |
DongleBindingDemo/DongleLiveness.cs | Ersetzen Sie die XOR-Challenge durch die Challenge-Response-API des Herstellers (HMAC, ECDSA, …). |
DongleBindingDemo/ProtectedLogic.cs | Verschieben 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.