Skip to Content
Nuova versione 12 disponibile 🎉
ObfuscatorCifratura del codiceAssociazione a dongle hardware

Associazione a dongle hardware

Associa un’applicazione .NET a un dongle hardware di licenza, in modo che le DLL del dongle sostituite o simulate non possano aggirare la cifratura del codice.

I produttori di dongle hardware di licenza (KEYLOK, SafeNet Sentinel, Wibu CodeMeter, Marx CrypToken e simili) forniscono una DLL nativa che i loro clienti richiamano da .NET tramite P/Invoke per autenticarsi presso il dispositivo fisico. La minaccia principale per queste integrazioni è la sostituzione della DLL: un attaccante sostituisce la DLL del produttore con uno stub che restituisce “licenza valida” a ogni chiamata, e l’applicazione protetta prosegue senza accorgersi di nulla.

Questo articolo descrive uno schema che neutralizza questo attacco combinando la cifratura del codice di Babel Obfuscator con tre strati difensivi aggiuntivi. Alla fine della pagina puoi scaricare un PoC di riferimento eseguibile.

Il modello mentale sbagliato

L’approccio ingenuo consiste nell’usare il dongle come controllo booleano della licenza:

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

Questo approccio crolla con una sostituzione della DLL che richiede una sola riga. Una Keylok.dll falsa che restituisce true a ogni chiamata vanifica del tutto il controllo. Nemmeno cifrare il solo wrapper IsAuthenticated aiuta, perché quel wrapper deve comunque chiamare la DLL nativa: se la DLL è falsa, il wrapper non ha nulla con cui confrontarsi.

Il modello mentale corretto

Il dongle non è un verificatore di licenze. È un oracolo di decifratura.

La cifratura del codice trasforma i corpi IL dei metodi in bytecode della Babel VM e li cifra con una chiave AES derivata da una password. La password viene fornita in fase di esecuzione da un metodo di callback della password. In un’integrazione vincolata all’hardware, quella callback chiede la password al dongle anziché leggerla da un file di licenza:

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

Una DLL sostituita non è più un problema. Può mentire su qualsiasi cosa, ma non può fabbricare la password: senza di essa la Babel Virtual Machine non ha nulla da decifrare e l’applicazione diventa inerte.

La DLL nativa non può essere cifrata da Babel: è codice non gestito ed è sotto il controllo del produttore del dongle. Il punto è che non serve cifrarla: cifrare la logica di business gestita che usa il dongle raggiunge lo stesso obiettivo, perché una DLL falsa non è in grado di rilasciare la password di decifratura.

Glossario

Il resto dell’articolo fa riferimento a un piccolo insieme di simboli e operazioni. Scorri la tabella una volta prima di leggere gli strati descritti più avanti: tutto il resto si basa su di essa.

SimboloNomeDove si trovaChi lo creaChe cos’è
KPassword Babel (core)Sul dongle (incapsulata), rilasciata in fase di esecuzioneLo sviluppatore, al momento del confezionamentoLa password che Babel usa per cifrare e decifrare i metodi di business. Una per cliente in produzione.
HKChiave hardwareNel silicio del dongle, mai leggibileIl produttore del dongle, al momento del provisioningLa chiave del dongle resistente alle manomissioni. Il dongle la usa per le operazioni di wrap e unwrap dei dati sul dispositivo stesso.
tokenPassword incapsulataRisorsa incorporata nell’assembly offuscatoLo sviluppatore, al momento del confezionamentotoken = wrap(K, HK). Inutile senza il dongle, l’unica entità in grado di recuperare K.
BPassword di bootstrapConsumata all’offuscamento; attributo [Obfuscation] rimosso dall’outputLo sviluppatore, una volta per prodottoPassword fissa che cifra il codice di recupero della password (lo strato di cifratura concatenata).
LPassword di livenessConsumata all’offuscamento; attributo [Obfuscation] rimosso dall’outputLo sviluppatore, una volta per prodottoPassword fissa che cifra il codice del verificatore challenge/response.
IPassword di integritàConsumata all’offuscamento; attributo [Obfuscation] rimosso dall’outputLo sviluppatore, una volta per prodottoPassword fissa che cifra il codice che verifica la firma fissata della DLL nativa.
nonceByte casuali sempre nuoviGenerato in memoria a ogni chiamataL’applicazione, in fase di esecuzioneUna challenge casuale di 16 byte inviata al dongle. Diversa a ogni chiamata, quindi le risposte non possono essere riutilizzate.
K_i / HK_iVarianti per clienteUna coppia per ogni licenza consegnataLo sviluppatore, al momento della consegnaOgni cliente riceve una K_i univoca nella propria build e una HK_i univoca caricata sul proprio dongle.

Alcuni termini relativi alle operazioni:

  • wrap / unwrap: la coppia di operazioni crittografiche del dongle. wrap(K, HK) produce il token al momento del confezionamento (di norma una cifratura AES con HK come chiave); il dongle esegue la corrispondente unwrap(token, HK) in fase di esecuzione e restituisce K. La DLL nativa del produttore fa solo da corriere; le operazioni crittografiche vere e proprie avvengono all’interno del dongle.
  • origine (source): un concetto di Babel, cioè un gruppo con nome di metodi cifrati che condividono una password. Ogni attributo [Obfuscation(Feature="msil encryption:source=<name>;...")] assegna il proprio metodo a un’origine. Questo articolo usa quattro origini: core (codice di business, password K), bootstrap (recupero della password, password B), liveness (verificatore, password L) e integrity (controllo della DLL, password I).
  • callback della password: il metodo statico che Babel chiama in fase di esecuzione per ottenere la password di una determinata origine. È contrassegnato con [Obfuscation(Feature="msil encryption get password")]. Vedi Codice protetto da password per il meccanismo di base.

Quattro strati difensivi

Un’integrazione adatta alla produzione combina quattro strati.

1. Cifratura del codice con una password fornita dal dongle

Applica msil encryption ai metodi critici per il business usando una password specifica del prodotto, poi implementa la callback della password di Babel in modo che chieda al dongle di eseguire l’unwrap di un token memorizzato e ottenere 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 legge un testo cifrato (il token) da una risorsa incorporata, lo passa al dongle e restituisce la password in chiaro. Al momento del confezionamento lo sviluppatore calcola una sola volta token = wrap(K, HK), dove HK è la chiave protetta dall’hardware già caricata sul dongle del cliente.

2. Cifratura concatenata della callback stessa

La callback Inner è a sua volta un bersaglio appetibile per un attaccante che vuole scavalcare il dongle. Cifrala con una seconda password, fissa:

[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 viene fornita a Babel al momento dell’offuscamento e lì viene consumata: Babel rimuove del tutto l’attributo [Obfuscation] dall’assembly di output. La password non compare nel binario distribuito né come metadato, né come blob di un attributo personalizzato, né come stringa in chiaro che un decompilatore potrebbe far emergere. Per recuperarla bisogna superare la protezione interna che Babel applica al corpo del metodo cifrato, un compito ben più difficile che leggere una costante da una .dll. Il suo unico scopo è proteggere dall’analisi statica l’algoritmo che interroga il dongle: insieme all’offuscamento del flusso di controllo e alla cifratura delle stringhe, basta a scoraggiare tutti tranne i reverse engineer più determinati.

3. Controllo di liveness a ogni chiamata all’interno del codice cifrato

È lo strato che neutralizza l’attacco sniff-and-replay:

Un attaccante prende in prestito un dongle reale, intercetta il codice gestito, cattura la K ottenuta dall’unwrap e distribuisce una DLL falsa che restituisce direttamente K. La decifratura di Babel riesce con la K catturata e i metodi di business sembrano funzionare.

La soluzione è far eseguire ai metodi di business stessi (già cifrati con source=core) un nuovo scambio challenge/response con il dongle a ogni invocazione:

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

La K catturata permette a Babel di decifrare anche Verify, ma non contiene il segreto challenge/response del dongle: senza il dongle non è possibile rispondere al nuovo nonce. Con i dongle asimmetrici (ECDSA, RSA) la protezione è a tenuta stagna; i dongle simmetrici alzano comunque l’asticella in modo significativo, perché la chiave del verificatore esiste solo nel codice cifrato.

Due accorgimenti importanti all’interno di Verify:

  • Un nonce nuovo a ogni chiamata. Nessuna memoizzazione, nessuna cache.
  • Sabotaggio silenzioso in caso di errore (restituisci un valore sbagliato, un risultato vuoto, un byte corrotto), non throw. Un’eccezione rivela la posizione del controllo; un numero sbagliato no. L’attaccante scopre il controllo solo confrontando molte esecuzioni con un riferimento.

4. Integrità della DLL nativa con certificato fissato

Indipendentemente dalla correttezza del dongle, l’applicazione può verificare l’identità della DLL che sta per chiamare:

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

L’impronta del certificato di firma del produttore (KEYLOK Inc., Thales-SafeNet, Wibu-Systems, …) è fissata all’interno di un’origine integrity cifrata. Questo blocca la sostituzione con qualsiasi DLL non firmata dal produttore legittimo, comprese le DLL che superano il controllo di liveness perché inoltrano di nascosto le chiamate a un dongle reale.

Insieme al rilevamento delle manomissioni di Babel, che inietta nell’assembly stesso controlli di integrità successivi all’offuscamento, si ottengono cinque difese attive contro l’attacco di modifica e ridistribuzione.

Chiavi per cliente

Un’unica K condivisa tra tutti i clienti crea un rischio di violazione dell’intera classe: una sola installazione violata con successo espone l’intera famiglia di prodotti. La distribuzione consigliata prevede build Babel per cliente:

Esegui il provisioning del dongle

Carica sul dongle del cliente una chiave hardware HK_i e una password Babel K_i, entrambe generate in modo univoco.

Ricompila con la password del cliente

Al momento della consegna, esegui di nuovo Babel sull’albero dei sorgenti di quel cliente, con K_i inserita negli attributi [Obfuscation] (o fornita tramite regole XML).

Incorpora il token incapsulato

Calcola token_i = wrap(K_i, HK_i) e incorporalo nella build.

Per violare il cliente i bisogna estrarre K_i dalla sua installazione e HK_i dal suo dongle. Nessuna delle due serve contro il cliente j. Il costo operativo è una build Babel per ogni consegna, automatizzabile in CI in pochi minuti per cliente.

Riepilogo del modello delle minacce

MinacciaNeutralizzata da
Decompilazione statica della logica di businessCifratura del codice (core)
Sostituzione con una DLL stub return successCifratura del codice + liveness
Hook sulla callback gestita della passwordCifratura concatenata + rilevamento delle manomissioni
Cattura di K seguita da una DLL falsa con K fissa nel codiceControllo di liveness
Patch dell’IL per rimuovere le chiamate a Verify()Rilevamento delle manomissioni
Sostituzione con una DLL firmata da un altro soggettoCertificato Authenticode fissato
Compromissione che si propaga tra i clientiChiavi per cliente
Esclusione completa della Babel Virtual MachineI metodi svolgono lavoro reale, non fanno solo da controllo

Elenco di controllo pratico

  • Applica msil encryption con source=core ai metodi di business.
  • Applica msil encryption con source=bootstrap al recupero della password.
  • Applica msil encryption con source=liveness al verificatore.
  • Applica msil encryption con source=integrity al controllo della DLL.
  • Implementa [Obfuscation(Feature="msil encryption get password")].
  • Incorpora il token incapsulato come risorsa.
  • Compila con --tamperingdetection --antidebugging --controlflow --stringencryption.
  • Firma l’assembly (SignAssembly=true).
  • Fissa l’impronta Authenticode della DLL nativa.
  • Emetti build per cliente con K_i univoche.

Implementazione di riferimento

Scarica: HardwareDongleBinding.zip (20 KB)

Lo zip contiene un proof of concept autonomo ed eseguibile. Usa una DLL nativa fittizia con primitive XOR, così si compila senza dipendenze esterne; la struttura è identica a quella di un’integrazione di produzione, in cui la DLL fittizia viene sostituita dalla vera libreria nativa del produttore.

Prerequisiti

Il PoC si compila e si esegue su Windows, macOS e Linux. Scegli la riga della tua piattaforma:

PiattaformaToolchain CPowerShell.NETBabel
WindowsVisual Studio 2022 Developer Command Promptpowershell.exe integrato.NET 8 SDKbabel.exe nel PATH
macOSXcode Command Line Tools (xcode-select --install)pwsh (brew install powershell).NET 8 SDKbabel da un pacchetto zip babel_net80/babel_net90/babel_net100, oppure lo strumento dotnet Babel.Obfuscator.Tool, nel PATH
Linuxcc / clang (apt install build-essential)pwsh.NET 8 SDKbabel nel PATH

Compilazione

# 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

Su Windows apri prima un Visual Studio Developer Command Prompt, così cl.exe è nel PATH; su macOS / Linux esegui da una shell qualsiasi. build.ps1 esegue cinque passi:

  1. Esegue il wrap della password Babel di ogni origine con la HK del dongle fittizio e scrive i file di token risultanti in DongleBindingDemo/Tokens/*.bin.
  2. Compila il progetto gestito con dotnet build -c Release.
  3. Offusca l’assembly risultante con Babel: --tamperingdetection --antidebugging --controlflow --stringencryption.
  4. Compila le tre varianti native con il compilatore C della piattaforma:
    • Windows: DongleMock.dll, DongleMock_Fake.dll, DongleMock_Sniff.dll tramite cl.exe.
    • macOS: libDongleMock.dylib, libDongleMock_Fake.dylib, libDongleMock_Sniff.dylib tramite clang.
    • Linux: libDongleMock.so, libDongleMock_Fake.so, libDongleMock_Sniff.so tramite cc.
  5. Copia la libreria autentica accanto all’assembly offuscato.

In C#, [DllImport("DongleMock")] viene risolto automaticamente nel nome di file adatto alla piattaforma.

Tre scenari

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

Esecuzione legittima: il dongle fittizio autentico è al suo posto; i metodi di business vengono decifrati ed eseguiti:

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

Attacco di sostituzione: sostituisce DongleMock.dll con uno stub che restituisce zeri a ogni chiamata. Babel ricava da quei byte a zero la password sbagliata e non può decifrare i corpi dei metodi:

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

Attacco sniff/replay: è l’attacco più interessante. La DLL falsa restituisce direttamente la password Babel catturata, quindi la decifratura riesce. Il controllo di liveness eseguito a ogni chiamata in ciascun metodo di business rileva comunque l’assenza del dongle (la DLL falsa non può rispondere a una nuova challenge) e ogni metodo devia subito nella propria diramazione di sabotaggio silenzioso:

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

Ogni scenario corrisponde a una riga della tabella del modello delle minacce riportata sopra.

Adattare l’esempio a un dongle reale

Per trasformare il PoC in un’integrazione di produzione, modifica quattro file:

FileChe cosa modificare
DongleBindingDemo/KeylokInterop.csSostituisci DongleMock con il nome della DLL del produttore; allinea le firme all’API del produttore.
DongleBindingDemo/DongleAuth.csSostituisci l’unwrap basato su XOR con la chiamata di protezione in lettura AES del produttore.
DongleBindingDemo/DongleLiveness.csSostituisci la challenge XOR con l’API challenge/response del produttore (HMAC, ECDSA, …).
DongleBindingDemo/ProtectedLogic.csSposta i tuoi metodi di business reali sotto source=core; mantieni il controllo DongleLiveness.Verify().

Gli attributi [Obfuscation] di Babel, l’hook di manomissione e gli script di build non richiedono modifiche.

Last updated on