Anti-depuración en .NET: cómo funciona y dónde termina
Un depurador es como un atacante observa tus cadenas descifradas y pone un breakpoint en tu comprobación de licencia. Aquí están las formas gestionadas, nativas y basadas en tiempo en que una app .NET puede detectar uno —con código real— y una explicación honesta de por qué la anti-depuración es un reductor de velocidad, no un muro.
Si la ofuscación trata de impedir que alguien lea tu código, la anti-depuración trata de impedir que lo observe en ejecución. Los dos ataques son distintos. Un ingeniero inverso derrotado por un método aplanado y con cadenas cifradas no se rinde: conecta un depurador, pone un breakpoint después de que las cadenas se descifran y lee el texto plano directamente de la memoria. Pone un breakpoint en el if del final de tu comprobación de licencia e invierte el booleano. Un depurador convierte tu proceso en ejecución en una caja transparente. La anti-depuración hace esa caja más difícil de abrir.
Qué está haciendo el atacante
El ataque con depurador sobre una app protegida suele ser uno de dos movimientos: observar o alterar. Observar significa dejar que el programa descifre una cadena, calcule una clave o construya un veredicto de licencia, y luego leer ese valor de un registro o una local en el momento en que existe, sin necesidad de entender el código ofuscado que lo produjo. Alterar significa poner un breakpoint en una decisión y cambiar el resultado: forzar IsValid a true, saltar el throw, pasar por encima de la comprobación. Ambos requieren un depurador en vivo conectado a tu proceso, que es justo lo que puedes detectar.
Detección gestionada
Las comprobaciones más baratas vienen en la BCL y atrapan Visual Studio, la mayoría de depuradores gestionados y el flujo «Depurar → Adjuntar al proceso»:
using System.Diagnostics;
static bool ManagedDebuggerPresent()
=> Debugger.IsAttached || Debugger.IsLogging();
Debugger.IsAttached es la obvia. Debugger.IsLogging() devuelve true cuando un depurador escucha la salida de Debugger.Log, atrapando algunos casos en que IsAttached ha sido falseado. Son una sola propiedad cada una —triviales de localizar e invertir para un atacante— así que trátalas como el primer cable trampa, nunca como toda la valla.
Detección nativa
Las comprobaciones gestionadas no ven depuradores nativos (WinDbg, x64dbg) ni nada que se conecte por debajo del CLR. El sistema operativo lo sabe, y lo expone:
using System.Runtime.InteropServices;
static class Native
{
[DllImport("kernel32.dll")]
static extern bool IsDebuggerPresent();
[DllImport("kernel32.dll")]
static extern bool CheckRemoteDebuggerPresent(IntPtr hProcess, ref bool present);
public static bool DebuggerPresent()
{
if (IsDebuggerPresent()) return true;
bool present = false;
CheckRemoteDebuggerPresent(Process.GetCurrentProcess().Handle, ref present);
return present;
}
}
IsDebuggerPresent lee el byte BeingDebugged en el PEB del propio proceso —rápido, pero fácil de parchear—. CheckRemoteDebuggerPresent le pregunta al kernel por el proceso actual, lo que atrapa depuradores que limpiaron el flag del PEB pero siguen conectados. Ejecutar ambas cierra huecos que cualquiera por separado deja abiertos.
Detección por tiempo
Los dos enfoques anteriores prueban un flag. Un depurador que va paso a paso o guiado por breakpoints deja otro tipo de evidencia: tiempo. Código que debería tardar microsegundos tarda segundos cuando un humano lo recorre. Mide un bloque apretado, sin efectos secundarios, y reacciona si corrió de forma inverosímilmente lenta:
static bool SteppingDetected()
{
var sw = Stopwatch.StartNew();
// A short deterministic workload with no I/O and no awaits.
long acc = 0;
for (int i = 0; i < 1000; i++) acc += i * 31 + (acc & 7);
sw.Stop();
// Microseconds normally; a stepping debugger inflates this by orders of magnitude.
return sw.Elapsed.TotalMilliseconds > 50 && acc != long.MinValue;
}
El tiempo es ruidoso —una máquina cargada o una pausa de GC puede dispararlo— así que usa un umbral generoso y nunca hagas fatal una sola lectura de tiempo. Su fortaleza es que detecta el acto de ir paso a paso, que las comprobaciones de flags no pueden.
Reaccionar sin delatarte
Cómo reaccionas importa tanto como cómo detectas. El instinto es lanzar "¡Depurador detectado!"; no lo hagas. Eso le da al atacante una cadena que buscar y una traza de pila que apunta directo a tu comprobación; pone un breakpoint en el throw y retrocede hasta la detección, y luego la parchea. Mejores respuestas son silenciosas y diferidas:
- Pliega el resultado en el estado normal —degrada a la edición sin licencia, devuelve datos sutilmente incorrectos, o deja que una función posterior no relacionada falle— en vez de reaccionar en el sitio de la detección.
- Retrasa la reacción para que el síntoma esté lejos de la comprobación, en código y en tiempo.
- Nunca uses un mensaje que nombre la depuración; nunca centralices todas las comprobaciones en un único método
AntiDebugque el atacante pueda neutralizar de una vez.
El límite honesto
Cada técnica aquí se puede vencer de forma aislada. IsDebuggerPresent se puede hookear para que siempre devuelva false. Debugger.IsAttached es un callvirt IL que un atacante invierte con un editor hexadecimal. Las comprobaciones de tiempo se pueden sustituir para devolver un valor pequeño fijo. Por sí sola, la anti-depuración detiene al atacante casual con un depurador abierto y poco más.
Su verdadero valor es acumulativo. Cuando la detección está ella misma aplanada en su flujo de control y sus cadenas cifradas, encontrar la comprobación es trabajo. Cuando varias comprobaciones independientes están dispersas y sus reacciones diferidas, neutralizar una no ayuda. Cuando la anti-manipulación verifica la integridad del método, parchear una comprobación se detecta a sí mismo. La anti-depuración no es la cerradura; es una de las varias cosas que hay que forzar a la vez, y de ahí viene realmente la protección: Nebula.NET aplica esto como capas en vez de un solo interruptor. Usada así, la caja transparente se vuelve mucho más difícil de ver por dentro.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.