Parcheo en tiempo de ejecución: el ataque que tu comprobación de integridad nunca ve
La anti-manipulación que hashea la DLL en disco detecta un archivo parcheado — pero no un método reescrito en memoria en tiempo de ejecución. Bibliotecas como Harmony desvían tus métodos sin tocar el archivo, que es como funcionan las evasiones de licencia y los cheats modernos. Aquí tienes cómo funciona el parcheo en memoria y qué lo detecta de verdad.
La mayoría de la anti-manipulación de .NET hace una cosa: al inicio hashea el ensamblado y compara el hash con un valor conocido bueno, de modo que si alguien editó la DLL para borrar una comprobación de licencia, la app lo nota y se niega a ejecutarse. Esa es una defensa real y útil — contra la manipulación de archivos. Pero hay toda una clase de ataque que no puede ver, porque nunca toca el archivo. El método se reescribe en memoria, después de que el runtime ya lo haya cargado y compilado con el JIT. Los bytes en disco son idénticos; el comportamiento no.
El método que la comprobación de hash nunca inspecciona
Un método de .NET pasa por dos formas. En disco es IL dentro del ensamblado — eso es lo que cubre un hash de archivo. En tiempo de ejecución el JIT compila ese IL a código máquina nativo en la memoria del proceso, y ese código nativo es lo que la CPU realmente ejecuta. El parcheo en memoria apunta a la segunda forma. Deja que el método se cargue y compile con el JIT con normalidad — pasando cualquier comprobación de archivo en tiempo de carga — y luego reescribe el método compilado, o redirige su punto de entrada, para que las llamadas aterricen en otra parte.
La herramienta gestionada más popular para esto es Harmony. Es una biblioteca legítima y excelente (los mods de juegos dependen de ella), pero la misma capacidad que permite a un mod cambiar el comportamiento de un juego permite a un atacante neutralizar una comprobación de licencia. Un parche de Harmony de tu control se ve así:
// Attacker's patch assembly, loaded into your process (via a mod loader,
// an injected AppDomain, or a wrapper exe):
[HarmonyPatch(typeof(LicenseGate), nameof(LicenseGate.IsLicensed))]
static class Bypass
{
// A "prefix" runs before your method and can skip it entirely.
static bool Prefix(ref bool __result)
{
__result = true; // force IsLicensed() to return true
return false; // skip the original method body
}
}
var harmony = new Harmony("bypass");
harmony.PatchAll(); // detours LicenseGate.IsLicensed in memory
Tras PatchAll(), cada llamada a LicenseGate.IsLicensed() devuelve true sin que tu código se ejecute. Harmony hizo esto reescribiendo el método compilado por el JIT con un desvío al prefix — no se modificó ningún archivo. Tu firma Authenticode sigue siendo válida. Tu SHA-256 de la DLL sigue coincidiendo. La comprobación de integridad en tiempo de carga pasa, y el control queda abierto de par en par.
Detecta la herramienta, no el archivo
Si no puedes atrapar el cambio hasheando el archivo, atrapa las condiciones que el ataque necesita. La reescritura en memoria en .NET depende de que una de dos cosas esté presente en tu proceso, y ambas son observables al inicio con código gestionado corriente.
Un profiler del CLR. El runtime expone una API de profiling/instrumentación que puede reescribir IL mientras se compila con el JIT — la forma sancionada y potente de desviar métodos gestionados. El runtime la habilita mediante variables de entorno, así que su presencia es una señal fuerte de que algo está posicionado para reescribir tus métodos:
static bool ProfilerAttached() =>
Environment.GetEnvironmentVariable("CORECLR_ENABLE_PROFILING") == "1" ||
Environment.GetEnvironmentVariable("COR_ENABLE_PROFILING") == "1";
Un framework de parcheo cargado en el proceso. Harmony tiene que estar dentro de tu proceso para parchearlo, y se distribuye como un ensamblado específico (0Harmony). No necesitas enumerar cada ensamblado — pregunta al runtime si su tipo de entrada se resuelve:
static bool HarmonyLoaded() =>
Type.GetType("HarmonyLib.Harmony, 0Harmony", throwOnError: false) != null
|| Type.GetType("Harmony.HarmonyInstance, 0Harmony", throwOnError: false) != null; // legacy 1.x
Ejecuta ambas al inicio (en el inicializador del módulo, para que se disparen antes que tu propio código) y reacciona como reaccionas ante cualquier otra señal de manipulación — lanzar, salir o delegar en tu propio controlador. Ninguna comprobación es cara, y fundamentalmente son gestionadas y multiplataforma, así que el mismo guardián funciona en Windows, Linux, macOS y el runtime de WASM.
Qué te da esto y qué no
Sé honesto sobre el modelo de amenaza, porque sobrevender una defensa es como se consigue una falsa sensación de seguridad. Estas comprobaciones elevan el coste de una evasión por desvío casual — el atacante que agarra Harmony y escribe un prefix de diez líneas ahora tiene también que derrotar a un detector antes de que su parche siquiera se ejecute. Eso detiene a la mayoría oportunista. No derrotan a un atacante decidido: alguien que controla el entorno puede renombrar el ensamblado parcheador, parchear tu detector primero o adjuntar un profiler que la comprobación no busca. No hay comprobación en código gestionado que un atacante suficientemente motivado con control total de la máquina no pueda eliminar al final — la misma salvedad que aplica a toda protección del lado del cliente.
La postura correcta es por capas. La anti-manipulación de integridad de archivo atrapa el ataque de binario parcheado; la detección de profiler y parcheador atrapa el ataque de desvío en memoria; y lo que ambas protegen — tu control de licencia — debería ser él mismo difícil de encontrar y difícil de leer, para que un atacante no pueda localizar fácilmente el método a parchear en primer lugar. Un control enterrado dentro de código aplanado por flujo de control y virtualizado, que devuelve un valor que alimenta lógica real en vez de un pulcro bool IsLicensed(), es mucho más difícil de desviar limpiamente que un único método bien nombrado. La detección te dice que hay una herramienta de ataque presente; la ofuscación hace que el objetivo al que apunta sea caro de adquirir.
Juntándolo todo
Si tu historia de anti-manipulación es «hasheo la DLL al inicio», estás defendiendo una puerta y dejando otra abierta. El parche en memoria es la puerta por la que las evasiones de licencia y los cheats modernos realmente entran, precisamente porque deja el archivo — y su firma — impoluto. Añade detección al inicio de las dos cosas que un parche en memoria necesita (un hook de profiler, un parcheador cargado), reacciona en tus términos, y combínalo con ofuscación que haga que el objetivo parcheado sea difícil de encontrar. Nebula inyecta tanto la comprobación de integridad de archivo como la detección de profiler/parcheador por ti, así que la compilación protegida vigila la puerta que el hash no puede ver — sin que escribas nada del código de detección a mano.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.