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.
| Simbolo | Nome | Dove si trova | Chi lo crea | Che cos’è |
|---|---|---|---|---|
| K | Password Babel (core) | Sul dongle (incapsulata), rilasciata in fase di esecuzione | Lo sviluppatore, al momento del confezionamento | La password che Babel usa per cifrare e decifrare i metodi di business. Una per cliente in produzione. |
| HK | Chiave hardware | Nel silicio del dongle, mai leggibile | Il produttore del dongle, al momento del provisioning | La chiave del dongle resistente alle manomissioni. Il dongle la usa per le operazioni di wrap e unwrap dei dati sul dispositivo stesso. |
| token | Password incapsulata | Risorsa incorporata nell’assembly offuscato | Lo sviluppatore, al momento del confezionamento | token = wrap(K, HK). Inutile senza il dongle, l’unica entità in grado di recuperare K. |
| B | Password di bootstrap | Consumata all’offuscamento; attributo [Obfuscation] rimosso dall’output | Lo sviluppatore, una volta per prodotto | Password fissa che cifra il codice di recupero della password (lo strato di cifratura concatenata). |
| L | Password di liveness | Consumata all’offuscamento; attributo [Obfuscation] rimosso dall’output | Lo sviluppatore, una volta per prodotto | Password fissa che cifra il codice del verificatore challenge/response. |
| I | Password di integrità | Consumata all’offuscamento; attributo [Obfuscation] rimosso dall’output | Lo sviluppatore, una volta per prodotto | Password fissa che cifra il codice che verifica la firma fissata della DLL nativa. |
| nonce | Byte casuali sempre nuovi | Generato in memoria a ogni chiamata | L’applicazione, in fase di esecuzione | Una challenge casuale di 16 byte inviata al dongle. Diversa a ogni chiamata, quindi le risposte non possono essere riutilizzate. |
| K_i / HK_i | Varianti per cliente | Una coppia per ogni licenza consegnata | Lo sviluppatore, al momento della consegna | Ogni 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 conHKcome chiave); il dongle esegue la corrispondenteunwrap(token, HK)in fase di esecuzione e restituisceK. 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, passwordK),bootstrap(recupero della password, passwordB),liveness(verificatore, passwordL) eintegrity(controllo della DLL, passwordI). - 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
Kottenuta dall’unwrap e distribuisce una DLL falsa che restituisce direttamenteK. La decifratura di Babel riesce con laKcatturata 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
| Minaccia | Neutralizzata da |
|---|---|
| Decompilazione statica della logica di business | Cifratura del codice (core) |
Sostituzione con una DLL stub return success | Cifratura del codice + liveness |
| Hook sulla callback gestita della password | Cifratura concatenata + rilevamento delle manomissioni |
Cattura di K seguita da una DLL falsa con K fissa nel codice | Controllo di liveness |
Patch dell’IL per rimuovere le chiamate a Verify() | Rilevamento delle manomissioni |
| Sostituzione con una DLL firmata da un altro soggetto | Certificato Authenticode fissato |
| Compromissione che si propaga tra i clienti | Chiavi per cliente |
| Esclusione completa della Babel Virtual Machine | I metodi svolgono lavoro reale, non fanno solo da controllo |
Elenco di controllo pratico
- Applica
msil encryptionconsource=coreai metodi di business. - Applica
msil encryptionconsource=bootstrapal recupero della password. - Applica
msil encryptionconsource=livenessal verificatore. - Applica
msil encryptionconsource=integrityal 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_iunivoche.
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:
| Piattaforma | Toolchain C | PowerShell | .NET | Babel |
|---|---|---|---|---|
| Windows | Visual Studio 2022 Developer Command Prompt | powershell.exe integrato | .NET 8 SDK | babel.exe nel PATH |
| macOS | Xcode Command Line Tools (xcode-select --install) | pwsh (brew install powershell) | .NET 8 SDK | babel da un pacchetto zip babel_net80/babel_net90/babel_net100, oppure lo strumento dotnet Babel.Obfuscator.Tool, nel PATH |
| Linux | cc / clang (apt install build-essential) | pwsh | .NET 8 SDK | babel 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.ps1Su 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:
- Esegue il wrap della password Babel di ogni origine con la
HKdel dongle fittizio e scrive i file di token risultanti inDongleBindingDemo/Tokens/*.bin. - Compila il progetto gestito con
dotnet build -c Release. - Offusca l’assembly risultante con Babel:
--tamperingdetection --antidebugging --controlflow --stringencryption. - Compila le tre varianti native con il compilatore C della piattaforma:
- Windows:
DongleMock.dll,DongleMock_Fake.dll,DongleMock_Sniff.dlltramitecl.exe. - macOS:
libDongleMock.dylib,libDongleMock_Fake.dylib,libDongleMock_Sniff.dylibtramiteclang. - Linux:
libDongleMock.so,libDongleMock_Fake.so,libDongleMock_Sniff.sotramitecc.
- Windows:
- 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 failsEsecuzione 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: OKAttacco 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: BLOCKEDAttacco 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: BLOCKEDOgni 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:
| File | Che cosa modificare |
|---|---|
DongleBindingDemo/KeylokInterop.cs | Sostituisci DongleMock con il nome della DLL del produttore; allinea le firme all’API del produttore. |
DongleBindingDemo/DongleAuth.cs | Sostituisci l’unwrap basato su XOR con la chiamata di protezione in lettura AES del produttore. |
DongleBindingDemo/DongleLiveness.cs | Sostituisci la challenge XOR con l’API challenge/response del produttore (HMAC, ECDSA, …). |
DongleBindingDemo/ProtectedLogic.cs | Sposta 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.