Vinculación a dongle de hardware
Vincule una aplicación .NET a un dongle de hardware de licencias para que las DLL del dongle sustituidas o simuladas no puedan eludir el cifrado de código.
Los fabricantes de dongles de hardware para licencias (KEYLOK, SafeNet Sentinel, Wibu CodeMeter, Marx CrypToken y otros similares) distribuyen una DLL nativa a la que sus clientes llaman desde .NET mediante P/Invoke para autenticarse ante el dispositivo físico. La principal amenaza contra estas integraciones es la sustitución de la DLL: un atacante reemplaza la DLL del fabricante por un stub que devuelve «licencia válida» en todas las llamadas, y la aplicación protegida continúa sin más.
Este artículo describe un patrón que cierra ese ataque combinando el cifrado de código de Babel Obfuscator con tres capas defensivas adicionales. Al final de la página puede descargarse una prueba de concepto de referencia ejecutable.
El modelo mental equivocado
El planteamiento ingenuo consiste en usar el dongle como una comprobación booleana de la licencia:
if (!Keylok.IsAuthenticated()) Environment.Exit(1);
RunBusinessLogic();Este planteamiento se viene abajo con una sustitución de la DLL de una sola línea. Una Keylok.dll falsa que devuelve true en todas las llamadas anula por completo la comprobación. Cifrar solo el envoltorio IsAuthenticated tampoco ayuda, porque ese envoltorio tiene que llamar a la DLL nativa de un modo u otro: si la DLL es falsa, el envoltorio no tiene nada con lo que verificar.
El modelo mental correcto
El dongle no es un verificador de licencias. Es un oráculo de descifrado.
El cifrado de código transforma los cuerpos IL de los métodos en código de bytes de la máquina virtual de Babel y los cifra con una clave AES derivada de una contraseña. La contraseña la proporciona en tiempo de ejecución un método de devolución de llamada de contraseña. En una integración vinculada al hardware, esa devolución de llamada pide la contraseña al dongle en lugar de leerla de un archivo de licencia:
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 sustituida deja de ser un problema. Puede mentir sobre lo que quiera, pero no puede fabricar la contraseña; sin ella, la máquina virtual de Babel no tiene nada que descifrar y la aplicación queda inerte.
Babel no puede cifrar la DLL nativa: es código no administrado y la controla el fabricante del dongle. La clave está en que la DLL nativa no necesita cifrarse: cifrar la lógica de negocio administrada que usa el dongle consigue el mismo objetivo, porque una DLL falsa no puede entregar la contraseña de descifrado.
Glosario
El resto de este artículo hace referencia a un conjunto reducido de símbolos y operaciones. Repase la tabla una vez antes de leer las capas que siguen; todo lo demás se apoya en ella.
| Símbolo | Nombre | Dónde reside | Quién lo crea | Qué es |
|---|---|---|---|---|
| K | Contraseña de Babel (core) | En el dongle (envuelta), se entrega en tiempo de ejecución | El desarrollador, al empaquetar | La contraseña que Babel usa para cifrar y descifrar los métodos de negocio. Una por cliente en producción. |
| HK | Clave de hardware | Dentro del silicio del dongle, nunca legible | El fabricante del dongle, al aprovisionarlo | La clave del dongle, resistente a la manipulación. El dongle la usa para envolver y desenvolver datos en el propio dispositivo. |
| token | Contraseña envuelta | Recurso incrustado en el ensamblado ofuscado | El desarrollador, al empaquetar | token = wrap(K, HK). Inútil sin el dongle, que es la única entidad que puede recuperar K. |
| B | Contraseña de arranque | Se consume durante la ofuscación; el atributo [Obfuscation] se quita de la salida | El desarrollador, una vez por producto | Contraseña fija que cifra el código que obtiene la contraseña (la capa de cifrado encadenado). |
| L | Contraseña de presencia | Se consume durante la ofuscación; el atributo [Obfuscation] se quita de la salida | El desarrollador, una vez por producto | Contraseña fija que cifra el código del verificador de desafío y respuesta. |
| I | Contraseña de integridad | Se consume durante la ofuscación; el atributo [Obfuscation] se quita de la salida | El desarrollador, una vez por producto | Contraseña fija que cifra el código que fija la firma de la DLL nativa. |
| nonce | Bytes aleatorios nuevos | Se genera en memoria en cada llamada | La aplicación, en tiempo de ejecución | Un desafío aleatorio de 16 bytes que se envía al dongle. Es distinto en cada llamada, por lo que las respuestas no pueden reproducirse. |
| K_i / HK_i | Variantes por cliente | Un par por cada licencia entregada | El desarrollador, en el momento de la entrega | Cada cliente recibe una K_i única en su compilación y una HK_i única aprovisionada en su dongle. |
Algunos términos sobre las operaciones en sí:
- wrap / unwrap (envolver y desenvolver): el par de operaciones criptográficas del dongle.
wrap(K, HK)genera el token al empaquetar (normalmente un cifrado AES conHKcomo clave); el dongle realiza la operación correspondienteunwrap(token, HK)en tiempo de ejecución y devuelveK. La DLL nativa del fabricante es el mensajero; la criptografía real se ejecuta dentro del dongle. - origen (source): un concepto de Babel, un grupo con nombre de métodos cifrados que comparten una contraseña. Cada atributo
[Obfuscation(Feature="msil encryption:source=<name>;...")]asigna su método a un origen. Este artículo usa cuatro orígenes:core(código de negocio, contraseñaK),bootstrap(obtención de la contraseña, contraseñaB),liveness(verificador, contraseñaL) eintegrity(comprobación de la DLL, contraseñaI). - devolución de llamada de contraseña: el método estático al que Babel llama en tiempo de ejecución para obtener la contraseña de un origen determinado. Se marca con
[Obfuscation(Feature="msil encryption get password")]. Consulte Código protegido por contraseña para conocer el mecanismo básico.
Cuatro capas defensivas
Una integración con calidad de producción combina cuatro capas.
1. Cifrado de código con una contraseña suministrada por el dongle
Aplique msil encryption a los métodos críticos para el negocio con una contraseña por producto y, después, implemente la devolución de llamada de contraseña de Babel de modo que pida al dongle que desenvuelva un token almacenado para obtener 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 lee un texto cifrado (el token) de un recurso incrustado, lo pasa al dongle y devuelve la contraseña en texto sin cifrar. Al empaquetar, el desarrollador calcula token = wrap(K, HK) una sola vez, donde HK es la clave protegida por hardware ya aprovisionada en el dongle del cliente.
2. Cifrado encadenado de la propia devolución de llamada
La devolución de llamada Inner es a su vez un objetivo apetecible para un atacante que quiera saltarse el dongle. Cífrela con una segunda contraseña fija:
[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 se suministra a Babel durante la ofuscación y se consume ahí: Babel quita por completo el atributo [Obfuscation] del ensamblado de salida. La contraseña no aparece en el binario distribuido ni como metadatos, ni como blob de un atributo personalizado, ni como una cadena de texto sin cifrar que un descompilador pudiera mostrar. Recuperarla exige vencer la protección interna que Babel aplica al cuerpo del método cifrado, una tarea bastante más difícil que leer una constante de una .dll. Su única misión es proteger del análisis estático el algoritmo de consulta al dongle: combinada con la ofuscación del flujo de control y el cifrado de cadenas, basta para disuadir a todos salvo al especialista en ingeniería inversa más decidido.
3. Comprobación de presencia en cada llamada dentro del código cifrado
Esta es la capa que cierra el ataque de captura y reproducción:
Un atacante toma prestado un dongle real, intercepta el código administrado, captura la
Kdesenvuelta y distribuye una DLL falsa que devuelveKdirectamente. El descifrado de Babel funciona con laKcapturada y los métodos de negocio parecen ejecutarse.
La solución es que los propios métodos de negocio (ya cifrados con source=core) realicen un nuevo desafío y respuesta con el dongle en cada invocación:
[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 capturada permite a Babel descifrar el propio Verify, pero no contiene el secreto de desafío y respuesta del dongle: sin el dongle no se puede responder al nonce nuevo. Los dongles asimétricos (ECDSA, RSA) hacen que esta defensa sea hermética; los dongles simétricos elevan igualmente el listón de forma notable, porque la clave del verificador solo existe en código cifrado.
Dos tácticas importantes dentro de Verify:
- Un nonce nuevo en cada llamada. Sin memoización y sin caché.
- Sabotaje silencioso en caso de fallo (devolver un valor incorrecto, un resultado vacío, un byte dañado), no
throw. Una excepción delata la ubicación de la comprobación; un número incorrecto, no. El atacante solo descubre la comprobación comparando muchas ejecuciones con una referencia.
4. Fijación de la integridad de la DLL nativa
Con independencia de que el dongle funcione correctamente, la aplicación puede verificar la identidad de la DLL a la que está a punto de llamar:
[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);
}La huella digital del certificado del editor del fabricante (KEYLOK Inc., Thales-SafeNet, Wibu-Systems, …) se fija dentro de un origen integrity cifrado. Esto bloquea la sustitución por cualquier DLL que no esté firmada por el fabricante legítimo, incluidas las DLL que superan la comprobación de presencia porque reenvían en secreto las llamadas a un dongle real.
Junto con la detección de manipulación de Babel, que inyecta en el propio ensamblado comprobaciones de integridad posteriores a la ofuscación, se obtienen cinco defensas activas contra el ataque de modificación y redistribución.
Claves por cliente
Una única K compartida por todos los clientes crea un riesgo de ruptura de clase (class break): si se consigue romper una instalación, queda expuesta toda la familia de productos. El despliegue recomendado consiste en compilaciones de Babel por cliente:
Aprovisionar el dongle
Aprovisione el dongle del cliente con una clave de hardware HK_i y una contraseña de Babel K_i, ambas generadas de forma única.
Recompilar con la contraseña del cliente
En el momento de la entrega, vuelva a ejecutar Babel sobre el árbol de código fuente de ese cliente con K_i sustituida en los atributos [Obfuscation] (o suministrada mediante reglas XML).
Incrustar el token envuelto
Calcule token_i = wrap(K_i, HK_i) e incrústelo en la compilación.
Para romper la protección del cliente i hay que extraer K_i de su instalación y HK_i de su dongle. Ninguna de las dos sirve para el cliente j. El coste operativo es una compilación de Babel por entrega, que puede automatizarse en CI en unos minutos por cliente.
Resumen del modelo de amenazas
| Amenaza | Cerrada por |
|---|---|
| Descompilación estática de la lógica de negocio | Cifrado de código (core) |
Sustitución por una DLL stub de tipo return success | Cifrado de código + comprobación de presencia |
| Interceptación de la devolución de llamada de contraseña administrada | Cifrado encadenado + detección de manipulación |
Captura de K seguida de una DLL falsa con K escrita en el código | Comprobación de presencia |
Modificación del IL para quitar las llamadas a Verify() | Detección de manipulación |
| Sustitución por una DLL firmada con otro certificado | Fijación de Authenticode |
| Propagación de una vulneración de un cliente a otros | Claves por cliente |
| Omisión total de la máquina virtual de Babel | Los métodos hacen un trabajo real, no una mera comprobación |
Lista de comprobación práctica
- Aplique
msil encryptionconsource=corea los métodos de negocio. - Aplique
msil encryptionconsource=bootstrapa la obtención de la contraseña. - Aplique
msil encryptionconsource=livenessal verificador. - Aplique
msil encryptionconsource=integritya la comprobación de la DLL. - Implemente
[Obfuscation(Feature="msil encryption get password")]. - Incruste el token envuelto como recurso.
- Compile con
--tamperingdetection --antidebugging --controlflow --stringencryption. - Firme el ensamblado (
SignAssembly=true). - Fije la huella digital de Authenticode de la DLL nativa.
- Emita compilaciones por cliente con una
K_iúnica.
Implementación de referencia
Descarga: HardwareDongleBinding.zip (20 KB)
El zip contiene una prueba de concepto ejecutable y autónoma. Usa una DLL nativa simulada con primitivas XOR para que pueda compilarse sin dependencias externas; la estructura es idéntica a la de una integración de producción que sustituye la simulación por la biblioteca nativa real del fabricante.
Requisitos previos
La prueba de concepto se compila y se ejecuta en Windows, macOS y Linux. Elija la fila de su plataforma:
| Plataforma | Cadena de herramientas de C | PowerShell | .NET | Babel |
|---|---|---|---|---|
| Windows | Visual Studio 2022 Developer Command Prompt (símbolo del sistema para desarrolladores) | powershell.exe integrado | SDK de .NET 8 | babel.exe en PATH |
| macOS | Xcode Command Line Tools (xcode-select --install) | pwsh (brew install powershell) | SDK de .NET 8 | babel de un paquete zip babel_net80/babel_net90/babel_net100, o la herramienta de dotnet Babel.Obfuscator.Tool, en PATH |
| Linux | cc / clang (apt install build-essential) | pwsh | SDK de .NET 8 | babel en PATH |
Compilación
# 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.ps1En Windows, abra primero un Visual Studio Developer Command Prompt para que cl.exe esté en PATH; en macOS y Linux puede ejecutarlo desde cualquier shell. build.ps1 realiza cinco pasos:
- Envuelve la contraseña de Babel de cada origen con la
HKdel dongle simulado y escribe los archivos de token resultantes enDongleBindingDemo/Tokens/*.bin. - Compila el proyecto administrado con
dotnet build -c Release. - Ofusca el ensamblado resultante con Babel:
--tamperingdetection --antidebugging --controlflow --stringencryption. - Compila las tres variantes nativas con el compilador de C de la plataforma:
- Windows:
DongleMock.dll,DongleMock_Fake.dll,DongleMock_Sniff.dllmediantecl.exe. - macOS:
libDongleMock.dylib,libDongleMock_Fake.dylib,libDongleMock_Sniff.dylibmedianteclang. - Linux:
libDongleMock.so,libDongleMock_Fake.so,libDongleMock_Sniff.somediantecc.
- Windows:
- Coloca la biblioteca auténtica junto al ensamblado ofuscado.
El [DllImport("DongleMock")] de C# se resuelve automáticamente en el nombre de archivo adecuado para la plataforma.
Tres escenarios
./Scripts/run-legit.ps1 # OK
./Scripts/run-swap-attack.ps1 # BLOCKED: Babel cannot decrypt
./Scripts/run-sniff-attack.ps1 # BLOCKED: liveness check failsEjecución legítima: el dongle simulado auténtico está en su sitio; los métodos de negocio se descifran y se ejecutan:
ComputeQuote(120, 250, 0.15) = 24225.00
GenerateReportToken(ACME) = REPORT-ACME CORP-XXXXXXXX
Result: OKAtaque de sustitución: reemplaza DongleMock.dll por un stub que devuelve ceros en todas las llamadas. Babel deriva una contraseña incorrecta a partir de esos bytes con valor cero y no puede descifrar los cuerpos de los métodos:
[EXCEPTION] ...: BVM decryption failed
Result: BLOCKEDAtaque de captura y reproducción: es el ataque más interesante. La DLL falsa devuelve directamente la contraseña de Babel capturada, por lo que el descifrado funciona. Aun así, la comprobación de presencia en cada llamada, dentro de cada método de negocio, detecta que falta el dongle (la DLL falsa no puede responder a un desafío nuevo) y cada método deriva hacia su bifurcación de sabotaje silencioso:
ComputeQuote(120, 250, 0.15) = -1
GenerateReportToken(ACME) = REPORT-UNAVAILABLE
Result: BLOCKEDCada escenario corresponde a una fila de la tabla del modelo de amenazas de más arriba.
Adaptación a un dongle real
Para convertir la prueba de concepto en una integración de producción, cambie cuatro archivos:
| Archivo | Qué hay que cambiar |
|---|---|
DongleBindingDemo/KeylokInterop.cs | Sustituya DongleMock por el nombre de la DLL del fabricante; ajuste las firmas a la API del fabricante. |
DongleBindingDemo/DongleAuth.cs | Sustituya la operación unwrap de tipo XOR por la llamada de protección de lectura AES del fabricante. |
DongleBindingDemo/DongleLiveness.cs | Sustituya el desafío XOR por la API de desafío y respuesta del fabricante (HMAC, ECDSA, …). |
DongleBindingDemo/ProtectedLogic.cs | Mueva sus métodos de negocio reales a source=core; mantenga la protección DongleLiveness.Verify(). |
Los atributos [Obfuscation] de Babel, el hook de manipulación y los scripts de compilación no requieren cambios.