Skip to Content
New release 11.7 available 🎉
ObfuscatorCode EncryptionHardware Dongle Binding

Hardware Dongle Binding

Bind a .NET application to a hardware licensing dongle so that swapped or stubbed dongle DLLs cannot bypass Code Encryption.

Vendors of hardware licensing dongles — KEYLOK, SafeNet Sentinel, Wibu CodeMeter, Marx CrypToken, and similar — ship a native DLL that their customers P/Invoke from .NET to authenticate against the physical device. The headline threat against such integrations is DLL replacement: an attacker substitutes the vendor’s DLL with a stub that returns “license valid” for every call, and the protected application happily proceeds.

This article describes a pattern that closes that attack by combining Babel Obfuscator’s Code Encryption with three additional defensive layers. A runnable reference PoC is available for download at the end of the page.

The wrong mental model

The naive approach is to use the dongle as a Boolean licence gate:

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

This collapses under a one-line DLL swap. A fake Keylok.dll that returns true for every call defeats the check entirely. Encrypting just the IsAuthenticated wrapper does not help either, because that wrapper has to call the native DLL one way or another — if the DLL is faked, the wrapper has nothing to verify against.

The right mental model

The dongle is not a licence checker. It is a decryption oracle.

Code Encryption transforms IL method bodies into Babel-VM bytecode and encrypts them with an AES key derived from a password. The password is provided at run time by a password callback method. In a hardware-bound integration, that callback queries the dongle for the password rather than reading it from a license file:

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

A swapped DLL is no longer a problem. It can lie about anything it likes, but it cannot fabricate the password — without it, the Babel Virtual Machine has nothing to decrypt and the application becomes inert.

The native DLL cannot be encrypted by Babel — it is unmanaged code and the dongle vendor controls it. The trick is that the native DLL does not need to be encrypted: encrypting the managed business logic that uses the dongle achieves the same goal, because a fake DLL is unable to release the decryption password.

Glossary

The rest of this article refers to a small set of symbols and operations. Skim the table once before reading the layers below; everything else builds on it.

SymbolNameWhere it livesWho creates itWhat it is
KBabel password (core)On the dongle (wrapped), released at runtimeThe developer, at packaging timeThe password Babel uses to encrypt and decrypt the business methods. Per-customer in production.
HKHardware keyInside the dongle silicon, never readableThe dongle vendor, at provisioning timeThe dongle’s tamper-resistant key. Used by the dongle to wrap and unwrap data on the device itself.
tokenWrapped passwordEmbedded resource in the obfuscated assemblyThe developer, at packaging timetoken = wrap(K, HK). Useless without the dongle, which is the only entity that can recover K.
BBootstrap passwordConsumed at obfuscation; [Obfuscation] attribute stripped from the outputThe developer, once per productFixed password that encrypts the password-fetch code (the chained-encryption layer).
LLiveness passwordConsumed at obfuscation; [Obfuscation] attribute stripped from the outputThe developer, once per productFixed password that encrypts the challenge/response verifier code.
IIntegrity passwordConsumed at obfuscation; [Obfuscation] attribute stripped from the outputThe developer, once per productFixed password that encrypts the native-DLL signature-pinning code.
nonceFresh random bytesGenerated in memory at every callThe application, at runtimeA 16-byte random challenge sent to the dongle. Different every call, so responses cannot be replayed.
K_i / HK_iPer-customer variantsOne pair per shipped licenseThe developer at delivery timeEach customer gets a unique K_i in their build and a unique HK_i provisioned on their dongle.

A few terms about the operations themselves:

  • wrap / unwrap — the dongle’s pair of cryptographic operations. wrap(K, HK) produces the token at packaging time (typically AES encryption using HK as key); the dongle performs the matching unwrap(token, HK) at runtime and returns K. The vendor’s native DLL is the courier; the actual crypto runs inside the dongle.
  • source — a Babel concept: a named group of encrypted methods that share a password. Each [Obfuscation(Feature="msil encryption:source=<name>;...")] attribute assigns its method to a source. This article uses four sources: core (business code, password K), bootstrap (password fetch, password B), liveness (verifier, password L), and integrity (DLL check, password I).
  • password callback — the static method Babel calls at runtime to obtain the password for a given source. Marked with [Obfuscation(Feature="msil encryption get password")]. See Password Protected Code for the base mechanism.

Four defensive layers

A production-quality integration combines four layers.

1. Code Encryption with a dongle-supplied password

Apply msil encryption to the business-critical methods using a per-product password, then implement Babel’s password callback so that it asks the dongle to unwrap a stored token into K:

[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 reads a ciphertext (the token) from an embedded resource, passes it to the dongle, and returns the plaintext password. At packaging time the developer computes token = wrap(K, HK) once, where HK is the hardware-protected key already provisioned on the customer’s dongle.

2. Chained encryption of the callback itself

The callback Inner is itself a juicy target for an attacker who wants to short-circuit the dongle. Encrypt it under a second, fixed password:

[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 is supplied to Babel at obfuscation time and consumed there: Babel strips the [Obfuscation] attribute from the output assembly entirely. The password does not appear in the shipped binary as metadata, as a custom-attribute blob, or as a plain-text string a decompiler could surface. Recovering it requires defeating Babel’s own internal protection of the encrypted method body — a meaningfully harder task than reading a constant out of a .dll. Its only job is to protect the dongle-query algorithm from static analysis: combined with control-flow obfuscation and string encryption, this is enough to deter all but the most determined reverse engineer.

3. Per-call liveness check inside encrypted code

This is the layer that closes the sniff-and-replay attack:

An attacker borrows a real dongle, hooks the managed code, captures the unwrapped K, and ships a fake DLL that returns K directly. Babel’s decryption succeeds with the captured K and the business methods appear to run.

The fix is to have the business methods themselves (already encrypted under source=core) perform a fresh challenge/response with the dongle on every invocation:

[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); }

The captured K lets Babel decrypt Verify itself, but does not contain the dongle’s challenge-response secret — the fresh nonce cannot be answered without the dongle. Asymmetric dongles (ECDSA, RSA) make this airtight; symmetric dongles still raise the bar significantly, because the verifier key only lives in encrypted code.

Two important tactics inside Verify:

  • Fresh nonce per call. No memoisation, no caching.
  • Silent sabotage on failure (return a wrong value, an empty result, a corrupted byte), not throw. An exception betrays the location of the check; a wrong number does not. The attacker discovers the check only by comparing many runs against a reference.

4. Native DLL integrity pinning

Independent of the dongle’s correctness, the application can verify the identity of the DLL it is about to call:

[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); }

The certificate thumbprint of the vendor’s publisher (KEYLOK Inc., Thales-SafeNet, Wibu-Systems, …) is pinned inside an encrypted integrity source. This blocks substitution with any DLL not signed by the legitimate vendor — including DLLs that pass the liveness check because they secretly forward to a real dongle.

Together with Babel’s Tampering Detection, which injects post-obfuscation integrity checks into the assembly itself, this gives five active defences against the modify-and-redistribute attack.

Per-customer keying

A single shared K across all customers creates a class-break risk: a successful crack of one installation releases the whole product family. The recommended deployment is per-customer Babel builds:

  1. Provision the customer’s dongle with a uniquely generated hardware key HK_i and a uniquely generated Babel password K_i.
  2. At delivery time, re-run Babel against that customer’s source tree with K_i substituted into the [Obfuscation] attributes (or supplied via XML rules).
  3. Compute token_i = wrap(K_i, HK_i) and embed it in the build.

Cracking customer i requires extracting K_i from their installation and HK_i from their dongle. Neither helps with customer j. The operational cost is one Babel build per delivery, which is automatable in CI in a few minutes per customer.

Threat model summary

ThreatClosed by
Static decompilation of the business logicCode Encryption (core)
Replacement with a return success stub DLLCode Encryption + Liveness
Hooking the managed password callbackChained encryption + Tampering Detection
Capture of K followed by a fake DLL with hardcoded KLiveness check
Patching the IL to remove Verify() callsTampering Detection
Replacement with a differently-signed DLLAuthenticode pinning
Compromise spreading across customersPer-customer keying
Skipping the Babel Virtual Machine entirelyMethods do real work, not gating

Practical checklist

  • Apply msil encryption with source=core to business methods.
  • Apply msil encryption with source=bootstrap to the password fetch.
  • Apply msil encryption with source=liveness to the verifier.
  • Apply msil encryption with source=integrity to the DLL check.
  • Implement [Obfuscation(Feature="msil encryption get password")].
  • Embed the wrapped token as a resource.
  • Build with --tamperingdetection --antidebugging --controlflow --stringencryption.
  • Sign the assembly (SignAssembly=true).
  • Pin the native DLL’s Authenticode thumbprint.
  • Issue per-customer builds with unique K_i.

Reference implementation

Download: HardwareDongleBinding.zip (20 KB)

The zip contains a self-contained runnable proof of concept. It uses a mock native DLL with XOR primitives so that it builds without any external dependencies; the structure is identical to a production integration that replaces the mock with the vendor’s real native library.

Prerequisites

The PoC builds and runs on Windows, macOS and Linux. Pick the row for your platform:

PlatformC toolchainPowerShell.NETBabel
WindowsVisual Studio 2022 Developer Command Promptbuilt-in powershell.exe.NET 8 SDKbabel.exe on PATH
macOSXcode Command Line Tools (xcode-select --install)pwsh (brew install powershell).NET 8 SDKbabel from the Mac build (babel_mac_11.7.0.0) on PATH
Linuxcc / clang (apt install build-essential)pwsh.NET 8 SDKbabel on 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

On Windows open a Visual Studio Developer Command Prompt first so that cl.exe is on PATH; on macOS / Linux run from any shell. build.ps1 performs five steps:

  1. Wrap each per-source Babel password with the mock dongle’s HK and write the resulting token files into DongleBindingDemo/Tokens/*.bin.
  2. Build the managed project with dotnet build -c Release.
  3. Obfuscate the resulting assembly with Babel: --tamperingdetection --antidebugging --controlflow --stringencryption.
  4. Compile the three native variants with the platform’s C compiler:
    • Windows: DongleMock.dll, DongleMock_Fake.dll, DongleMock_Sniff.dll via cl.exe.
    • macOS: libDongleMock.dylib, libDongleMock_Fake.dylib, libDongleMock_Sniff.dylib via clang.
    • Linux: libDongleMock.so, libDongleMock_Fake.so, libDongleMock_Sniff.so via cc.
  5. Stage the genuine library next to the obfuscated assembly.

The C# [DllImport("DongleMock")] resolves to the platform-appropriate filename automatically.

Three scenarios

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

Legitimate run — the genuine mock dongle is in place; business methods decrypt and execute:

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

Swap attack — replaces DongleMock.dll with a stub that returns zeros for every call. Babel derives the wrong password from those zero bytes and cannot decrypt the method bodies:

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

Sniff/replay attack — the more interesting attack: the fake DLL returns the captured Babel password directly, so decryption succeeds. The per-call liveness check inside each business method nevertheless detects the missing dongle (the fake cannot answer a fresh challenge) and each method short-circuits into its silent sabotage branch:

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

Each scenario corresponds to a row in the threat model table above.

Adapting to a real dongle

To turn the PoC into a production integration, change four files:

FileWhat to change
DongleBindingDemo/KeylokInterop.csReplace DongleMock with the vendor’s DLL name; align signatures with the vendor’s API.
DongleBindingDemo/DongleAuth.csReplace the XOR-style unwrap with the vendor’s AES read-protection call.
DongleBindingDemo/DongleLiveness.csReplace the XOR challenge with the vendor’s challenge/response API (HMAC, ECDSA, …).
DongleBindingDemo/ProtectedLogic.csMove your real business methods under source=core; keep the DongleLiveness.Verify() guard.

The Babel [Obfuscation] attributes, the tampering hook, and the build scripts require no changes.

Last updated on