Skip to Content
Nueva versión 12 disponible 🎉
ObfuscatorCifrado de códigoVinculación a dongle de hardware

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ímboloNombreDónde resideQuién lo creaQué es
KContraseña de Babel (core)En el dongle (envuelta), se entrega en tiempo de ejecuciónEl desarrollador, al empaquetarLa contraseña que Babel usa para cifrar y descifrar los métodos de negocio. Una por cliente en producción.
HKClave de hardwareDentro del silicio del dongle, nunca legibleEl fabricante del dongle, al aprovisionarloLa clave del dongle, resistente a la manipulación. El dongle la usa para envolver y desenvolver datos en el propio dispositivo.
tokenContraseña envueltaRecurso incrustado en el ensamblado ofuscadoEl desarrollador, al empaquetartoken = wrap(K, HK). Inútil sin el dongle, que es la única entidad que puede recuperar K.
BContraseña de arranqueSe consume durante la ofuscación; el atributo [Obfuscation] se quita de la salidaEl desarrollador, una vez por productoContraseña fija que cifra el código que obtiene la contraseña (la capa de cifrado encadenado).
LContraseña de presenciaSe consume durante la ofuscación; el atributo [Obfuscation] se quita de la salidaEl desarrollador, una vez por productoContraseña fija que cifra el código del verificador de desafío y respuesta.
IContraseña de integridadSe consume durante la ofuscación; el atributo [Obfuscation] se quita de la salidaEl desarrollador, una vez por productoContraseña fija que cifra el código que fija la firma de la DLL nativa.
nonceBytes aleatorios nuevosSe genera en memoria en cada llamadaLa aplicación, en tiempo de ejecuciónUn 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_iVariantes por clienteUn par por cada licencia entregadaEl desarrollador, en el momento de la entregaCada 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 con HK como clave); el dongle realiza la operación correspondiente unwrap(token, HK) en tiempo de ejecución y devuelve K. 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ña K), bootstrap (obtención de la contraseña, contraseña B), liveness (verificador, contraseña L) e integrity (comprobación de la DLL, contraseña I).
  • 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 K desenvuelta y distribuye una DLL falsa que devuelve K directamente. El descifrado de Babel funciona con la K capturada 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

AmenazaCerrada por
Descompilación estática de la lógica de negocioCifrado de código (core)
Sustitución por una DLL stub de tipo return successCifrado de código + comprobación de presencia
Interceptación de la devolución de llamada de contraseña administradaCifrado encadenado + detección de manipulación
Captura de K seguida de una DLL falsa con K escrita en el códigoComprobació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 certificadoFijación de Authenticode
Propagación de una vulneración de un cliente a otrosClaves por cliente
Omisión total de la máquina virtual de BabelLos métodos hacen un trabajo real, no una mera comprobación

Lista de comprobación práctica

  • Aplique msil encryption con source=core a los métodos de negocio.
  • Aplique msil encryption con source=bootstrap a la obtención de la contraseña.
  • Aplique msil encryption con source=liveness al verificador.
  • Aplique msil encryption con source=integrity a 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:

PlataformaCadena de herramientas de CPowerShell.NETBabel
WindowsVisual Studio 2022 Developer Command Prompt (símbolo del sistema para desarrolladores)powershell.exe integradoSDK de .NET 8babel.exe en PATH
macOSXcode Command Line Tools (xcode-select --install)pwsh (brew install powershell)SDK de .NET 8babel de un paquete zip babel_net80/babel_net90/babel_net100, o la herramienta de dotnet Babel.Obfuscator.Tool, en PATH
Linuxcc / clang (apt install build-essential)pwshSDK de .NET 8babel 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.ps1

En 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:

  1. Envuelve la contraseña de Babel de cada origen con la HK del dongle simulado y escribe los archivos de token resultantes en DongleBindingDemo/Tokens/*.bin.
  2. Compila el proyecto administrado con dotnet build -c Release.
  3. Ofusca el ensamblado resultante con Babel: --tamperingdetection --antidebugging --controlflow --stringencryption.
  4. Compila las tres variantes nativas con el compilador de C de la plataforma:
    • Windows: DongleMock.dll, DongleMock_Fake.dll, DongleMock_Sniff.dll mediante cl.exe.
    • macOS: libDongleMock.dylib, libDongleMock_Fake.dylib, libDongleMock_Sniff.dylib mediante clang.
    • Linux: libDongleMock.so, libDongleMock_Fake.so, libDongleMock_Sniff.so mediante cc.
  5. 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 fails

Ejecució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: OK

Ataque 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: BLOCKED

Ataque 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: BLOCKED

Cada 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:

ArchivoQué hay que cambiar
DongleBindingDemo/KeylokInterop.csSustituya DongleMock por el nombre de la DLL del fabricante; ajuste las firmas a la API del fabricante.
DongleBindingDemo/DongleAuth.csSustituya la operación unwrap de tipo XOR por la llamada de protección de lectura AES del fabricante.
DongleBindingDemo/DongleLiveness.csSustituya el desafío XOR por la API de desafío y respuesta del fabricante (HMAC, ECDSA, …).
DongleBindingDemo/ProtectedLogic.csMueva 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.

Last updated on