Liaison à un dongle matériel
Lier une application .NET à un dongle matériel de licence, afin que des DLL de dongle substituées ou factices ne puissent pas contourner le chiffrement du code.
Les fournisseurs de dongles matériels de licence (KEYLOK, SafeNet Sentinel, Wibu CodeMeter, Marx CrypToken et d’autres) livrent une DLL native que leurs clients appellent depuis .NET par P/Invoke pour s’authentifier auprès du périphérique physique. La principale menace qui pèse sur ces intégrations est le remplacement de la DLL : un attaquant substitue à la DLL du fournisseur une DLL factice qui renvoie « licence valide » à chaque appel, et l’application protégée poursuit son exécution sans rien remarquer.
Cet article décrit un mécanisme qui pare cette attaque en combinant le chiffrement du code de Babel Obfuscator avec trois couches défensives supplémentaires. Une preuve de concept (PoC) de référence exécutable peut être téléchargée à la fin de la page.
Le mauvais modèle mental
L’approche naïve consiste à utiliser le dongle comme une condition de licence booléenne :
if (!Keylok.IsAuthenticated()) Environment.Exit(1);
RunBusinessLogic();Cette approche s’effondre devant une substitution de DLL d’une seule ligne. Une fausse Keylok.dll qui renvoie true à chaque appel neutralise entièrement le contrôle. Chiffrer uniquement le wrapper IsAuthenticated n’aide pas davantage, car ce wrapper doit d’une manière ou d’une autre appeler la DLL native : si la DLL est fausse, le wrapper ne dispose de rien pour effectuer sa vérification.
Le bon modèle mental
Le dongle n’est pas un vérificateur de licence. C’est un oracle de déchiffrement.
Le chiffrement du code transforme les corps de méthode IL en bytecode de la machine virtuelle Babel et les chiffre avec une clé AES dérivée d’un mot de passe. Ce mot de passe est fourni à l’exécution par une méthode de rappel du mot de passe. Dans une intégration liée au matériel, cette méthode de rappel demande le mot de passe au dongle au lieu de le lire dans un fichier de licence :
managed business code --(encrypted with password K)
native dongle DLL --(plain, talks to the dongle hardware)
dongle hardware --(stores K behind tamper-resistant crypto)Une DLL substituée n’est plus un problème. Elle peut mentir sur tout ce qu’elle veut, mais elle ne peut pas fabriquer le mot de passe ; sans lui, la machine virtuelle Babel n’a rien à déchiffrer et l’application devient inerte.
La DLL native ne peut pas être chiffrée par Babel : c’est du code non managé, et c’est le fournisseur du dongle qui la contrôle. L’astuce est qu’elle n’a pas besoin de l’être : chiffrer la logique métier managée qui utilise le dongle atteint le même but, car une fausse DLL est incapable de fournir le mot de passe de déchiffrement.
Glossaire
La suite de cet article s’appuie sur un petit ensemble de symboles et d’opérations. Parcourez le tableau une fois avant de lire les couches ci-dessous ; tout le reste en découle.
| Symbole | Nom | Où il se trouve | Qui le crée | Ce que c’est |
|---|---|---|---|---|
| K | Mot de passe Babel (core) | Sur le dongle (encapsulé), fourni à l’exécution | Le développeur, à l’empaquetage | Le mot de passe que Babel utilise pour chiffrer et déchiffrer les méthodes métier. Propre à chaque client en production. |
| HK | Clé matérielle | Dans le silicium du dongle, jamais lisible | Le fournisseur du dongle, au provisionnement | La clé inviolable du dongle. Le dongle s’en sert pour encapsuler et désencapsuler des données sur le périphérique lui-même. |
| token | Mot de passe encapsulé | Ressource incorporée dans l’assembly obfusqué | Le développeur, à l’empaquetage | token = wrap(K, HK). Inutile sans le dongle, seule entité capable de retrouver K. |
| B | Mot de passe d’amorçage | Consommé à l’obfuscation ; l’attribut [Obfuscation] est retiré de la sortie | Le développeur, une fois par produit | Mot de passe fixe qui chiffre le code de récupération du mot de passe (la couche de chiffrement chaîné). |
| L | Mot de passe de présence | Consommé à l’obfuscation ; l’attribut [Obfuscation] est retiré de la sortie | Le développeur, une fois par produit | Mot de passe fixe qui chiffre le code du vérificateur de défi-réponse. |
| I | Mot de passe d’intégrité | Consommé à l’obfuscation ; l’attribut [Obfuscation] est retiré de la sortie | Le développeur, une fois par produit | Mot de passe fixe qui chiffre le code d’épinglage de la signature de la DLL native. |
| nonce | Nouveaux octets aléatoires | Généré en mémoire à chaque appel | L’application, à l’exécution | Un défi aléatoire de 16 octets envoyé au dongle. Différent à chaque appel, de sorte que les réponses ne peuvent pas être rejouées. |
| K_i / HK_i | Variantes propres à un client | Une paire par licence livrée | Le développeur, à la livraison | Chaque client reçoit un K_i unique dans son build et un HK_i unique provisionné sur son dongle. |
Quelques termes concernant les opérations elles-mêmes :
- wrap / unwrap : la paire d’opérations cryptographiques du dongle.
wrap(K, HK)produit le jeton à l’empaquetage (généralement un chiffrement AES avecHKcomme clé) ; le dongle effectue l’opérationunwrap(token, HK)correspondante à l’exécution et renvoieK. La DLL native du fournisseur n’est que le coursier ; la cryptographie proprement dite s’exécute dans le dongle. - source : un concept Babel, à savoir un groupe nommé de méthodes chiffrées qui partagent un mot de passe. Chaque attribut
[Obfuscation(Feature="msil encryption:source=<name>;...")]affecte sa méthode à une source. Cet article utilise quatre sources :core(code métier, mot de passeK),bootstrap(récupération du mot de passe, mot de passeB),liveness(vérificateur, mot de passeL) etintegrity(contrôle de la DLL, mot de passeI). - méthode de rappel du mot de passe : la méthode statique que Babel appelle à l’exécution pour obtenir le mot de passe d’une source donnée. Elle est marquée avec
[Obfuscation(Feature="msil encryption get password")]. Voir Code protégé par mot de passe pour le mécanisme de base.
Quatre couches défensives
Une intégration de qualité production combine quatre couches.
1. Chiffrement du code avec un mot de passe fourni par le dongle
Appliquez msil encryption aux méthodes métier critiques avec un mot de passe propre au produit, puis implémentez la méthode de rappel du mot de passe de Babel de sorte qu’elle demande au dongle de désencapsuler un jeton stocké pour obtenir 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 lit un texte chiffré (le jeton) dans une ressource incorporée, le transmet au dongle et renvoie le mot de passe en clair. À l’empaquetage, le développeur calcule une seule fois token = wrap(K, HK), où HK est la clé protégée par le matériel déjà provisionnée sur le dongle du client.
2. Chiffrement chaîné de la méthode de rappel elle-même
La méthode de rappel Inner est elle-même une cible de choix pour un attaquant qui veut court-circuiter le dongle. Chiffrez-la avec un second mot de passe, fixe :
[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 est fourni à Babel au moment de l’obfuscation et consommé à ce moment-là : Babel retire entièrement l’attribut [Obfuscation] de l’assembly de sortie. Le mot de passe n’apparaît dans le binaire livré ni sous forme de métadonnées, ni sous forme de blob d’attribut personnalisé, ni sous forme de chaîne en clair qu’un décompilateur pourrait faire apparaître. Pour le retrouver, il faut venir à bout de la protection interne que Babel applique au corps de méthode chiffré, une tâche nettement plus difficile que de lire une constante dans une .dll. Son seul rôle est de protéger l’algorithme d’interrogation du dongle contre l’analyse statique : associé à l’obfuscation du flux de contrôle et au chiffrement des chaînes, cela suffit à décourager tous les spécialistes de la rétro-ingénierie, sauf les plus déterminés.
3. Contrôle de présence à chaque appel dans le code chiffré
C’est la couche qui pare l’attaque par capture et rejeu :
Un attaquant emprunte un vrai dongle, intercepte le code managé, capture le
Kdésencapsulé et livre une fausse DLL qui renvoie directementK. Le déchiffrement de Babel réussit avec leKcapturé et les méthodes métier semblent s’exécuter.
La solution consiste à faire effectuer par les méthodes métier elles-mêmes (déjà chiffrées sous source=core) un nouveau défi-réponse avec le dongle à chaque appel :
[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);
}Le K capturé permet à Babel de déchiffrer Verify lui-même, mais il ne contient pas le secret de défi-réponse du dongle : sans le dongle, il est impossible de répondre au nouveau nonce. Les dongles asymétriques (ECDSA, RSA) rendent ce mécanisme hermétique ; les dongles symétriques élèvent tout de même nettement le niveau de difficulté, car la clé du vérificateur ne se trouve que dans du code chiffré.
Deux tactiques importantes dans Verify :
- Un nouveau nonce à chaque appel. Aucune mémoïsation, aucune mise en cache.
- Sabotage silencieux en cas d’échec (renvoyer une valeur fausse, un résultat vide, un octet corrompu), et non
throw. Une exception trahit l’emplacement du contrôle ; un nombre faux ne le trahit pas. L’attaquant ne découvre le contrôle qu’en comparant de nombreuses exécutions à une référence.
4. Épinglage de l’intégrité de la DLL native
Indépendamment du bon fonctionnement du dongle, l’application peut vérifier l’identité de la DLL qu’elle s’apprête à appeler :
[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’empreinte numérique du certificat de l’éditeur du fournisseur (KEYLOK Inc., Thales-SafeNet, Wibu-Systems, …) est épinglée dans une source integrity chiffrée. Cela bloque la substitution par toute DLL non signée par le fournisseur légitime, y compris les DLL qui passent le contrôle de présence parce qu’elles relaient secrètement les appels vers un vrai dongle.
Avec la détection de falsification de Babel, qui injecte dans l’assembly lui-même des contrôles d’intégrité après l’obfuscation, cela donne cinq défenses actives contre l’attaque par modification et redistribution.
Clés propres à chaque client
Un K unique partagé par tous les clients crée un risque de compromission globale : le piratage réussi d’une installation compromet toute la famille de produits. Le déploiement recommandé repose sur des builds Babel propres à chaque client :
Provisionner le dongle
Provisionnez le dongle du client avec une clé matérielle HK_i et un mot de passe Babel K_i, tous deux générés de façon unique.
Régénérer avec le mot de passe du client
À la livraison, exécutez de nouveau Babel sur l’arborescence source de ce client, avec K_i substitué dans les attributs [Obfuscation] (ou fourni au moyen de règles XML).
Incorporer le jeton encapsulé
Calculez token_i = wrap(K_i, HK_i) et incorporez-le dans le build.
Pour compromettre le client i, il faut extraire K_i de son installation et HK_i de son dongle. Ni l’un ni l’autre n’aide pour le client j. Le coût opérationnel est d’un build Babel par livraison, ce qui s’automatise en CI en quelques minutes par client.
Synthèse du modèle de menace
| Menace | Parée par |
|---|---|
| Décompilation statique de la logique métier | Chiffrement du code (core) |
Remplacement par une DLL factice de type return success | Chiffrement du code + contrôle de présence |
| Interception de la méthode de rappel managée du mot de passe | Chiffrement chaîné + détection de falsification |
Capture de K suivie d’une fausse DLL contenant K codé en dur | Contrôle de présence |
Modification de l’IL pour supprimer les appels à Verify() | Détection de falsification |
| Remplacement par une DLL signée différemment | Épinglage Authenticode |
| Propagation d’une compromission d’un client à l’autre | Clés propres à chaque client |
| Contournement complet de la machine virtuelle Babel | Les méthodes effectuent un vrai travail, pas un simple contrôle d’accès |
Liste de contrôle pratique
- Appliquez
msil encryptionavecsource=coreaux méthodes métier. - Appliquez
msil encryptionavecsource=bootstrapà la récupération du mot de passe. - Appliquez
msil encryptionavecsource=livenessau vérificateur. - Appliquez
msil encryptionavecsource=integrityau contrôle de la DLL. - Implémentez
[Obfuscation(Feature="msil encryption get password")]. - Incorporez le jeton encapsulé en tant que ressource.
- Générez avec
--tamperingdetection --antidebugging --controlflow --stringencryption. - Signez l’assembly (
SignAssembly=true). - Épinglez l’empreinte numérique Authenticode de la DLL native.
- Émettez des builds propres à chaque client avec un
K_iunique.
Implémentation de référence
Téléchargement : HardwareDongleBinding.zip (20 Ko)
L’archive zip contient une preuve de concept autonome et exécutable. Elle utilise une DLL native simulée, avec des primitives XOR, afin de pouvoir être générée sans aucune dépendance externe ; sa structure est identique à celle d’une intégration de production qui remplace la DLL simulée par la vraie bibliothèque native du fournisseur.
Prérequis
Le PoC se génère et s’exécute sous Windows, macOS et Linux. Choisissez la ligne correspondant à votre plateforme :
| Plateforme | Chaîne d’outils C | PowerShell | .NET | Babel |
|---|---|---|---|---|
| Windows | Visual Studio 2022 Developer Command Prompt | powershell.exe intégré | SDK .NET 8 | babel.exe dans le PATH |
| macOS | Xcode Command Line Tools (xcode-select --install) | pwsh (brew install powershell) | SDK .NET 8 | babel provenant d’une archive zip babel_net80/babel_net90/babel_net100, ou l’outil dotnet Babel.Obfuscator.Tool, dans le PATH |
| Linux | cc / clang (apt install build-essential) | pwsh | SDK .NET 8 | babel dans le 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.ps1Sous Windows, ouvrez d’abord une Visual Studio Developer Command Prompt pour que cl.exe soit dans le PATH ; sous macOS et Linux, lancez le script depuis n’importe quel shell. build.ps1 effectue cinq étapes :
- Encapsule le mot de passe Babel de chaque source avec la clé
HKdu dongle simulé et écrit les fichiers de jeton obtenus dansDongleBindingDemo/Tokens/*.bin. - Génère le projet managé avec
dotnet build -c Release. - Obfusque l’assembly obtenu avec Babel :
--tamperingdetection --antidebugging --controlflow --stringencryption. - Compile les trois variantes natives avec le compilateur C de la plateforme :
- Windows :
DongleMock.dll,DongleMock_Fake.dll,DongleMock_Sniff.dllaveccl.exe. - macOS :
libDongleMock.dylib,libDongleMock_Fake.dylib,libDongleMock_Sniff.dylibavecclang. - Linux :
libDongleMock.so,libDongleMock_Fake.so,libDongleMock_Sniff.soaveccc.
- Windows :
- Place la bibliothèque authentique à côté de l’assembly obfusqué.
L’attribut C# [DllImport("DongleMock")] se résout automatiquement vers le nom de fichier propre à la plateforme.
Trois scénarios
./Scripts/run-legit.ps1 # OK
./Scripts/run-swap-attack.ps1 # BLOCKED: Babel cannot decrypt
./Scripts/run-sniff-attack.ps1 # BLOCKED: liveness check failsExécution légitime : le dongle simulé authentique est en place ; les méthodes métier sont déchiffrées et s’exécutent :
ComputeQuote(120, 250, 0.15) = 24225.00
GenerateReportToken(ACME) = REPORT-ACME CORP-XXXXXXXX
Result: OKAttaque par substitution : DongleMock.dll est remplacée par une DLL factice qui renvoie des zéros à chaque appel. Babel dérive un mot de passe erroné de ces octets nuls et ne peut pas déchiffrer les corps de méthode :
[EXCEPTION] ...: BVM decryption failed
Result: BLOCKEDAttaque par capture et rejeu : c’est l’attaque la plus intéressante. La fausse DLL renvoie directement le mot de passe Babel capturé, si bien que le déchiffrement réussit. Le contrôle de présence effectué à chaque appel dans chaque méthode métier détecte néanmoins l’absence du dongle (la fausse DLL ne peut pas répondre à un nouveau défi) et chaque méthode bascule vers son chemin de sabotage silencieux :
ComputeQuote(120, 250, 0.15) = -1
GenerateReportToken(ACME) = REPORT-UNAVAILABLE
Result: BLOCKEDChaque scénario correspond à une ligne du tableau du modèle de menace ci-dessus.
Adapter à un vrai dongle
Pour transformer le PoC en intégration de production, modifiez quatre fichiers :
| Fichier | Ce qu’il faut modifier |
|---|---|
DongleBindingDemo/KeylokInterop.cs | Remplacez DongleMock par le nom de la DLL du fournisseur ; alignez les signatures sur l’API du fournisseur. |
DongleBindingDemo/DongleAuth.cs | Remplacez la désencapsulation de type XOR par l’appel de protection en lecture AES du fournisseur. |
DongleBindingDemo/DongleLiveness.cs | Remplacez le défi XOR par l’API de défi-réponse du fournisseur (HMAC, ECDSA, …). |
DongleBindingDemo/ProtectedLogic.cs | Placez vos vraies méthodes métier sous source=core ; conservez la garde DongleLiveness.Verify(). |
Les attributs [Obfuscation] de Babel, le gestionnaire de falsification et les scripts de build ne nécessitent aucune modification.