Skip to content
← Todas las publicaciones
· Delta1 Labs Ofuscación.NETSeguridad

Ofuscación más allá del renombrado: por qué el .NET renombrado todavía se lee como código fuente

El renombrado de identificadores es la capa de ofuscación más débil: un decompilador sobre código solo renombrado sigue mostrando tu lógica, tus cadenas y tus constantes tal cual. Esto es lo que el renombrado oculta y lo que no, y las capas —cifrado de cadenas, enmascaramiento de constantes, aplanamiento del flujo de control y virtualización— que de verdad hacen difícil de leer el .NET.

Abre casi cualquier app .NET en un decompilador y lo primero que notas es lo legible que es. Los decompiladores reconstruyen C# muy cercano al original, porque el compilador preserva en el IL mucha más estructura de la que la mayoría de los desarrolladores espera. La respuesta habitual es «ofúscalo», y la ofuscación habitual es el renombrado: convertir ValidateLicense en a y expiryDate en b. Ayuda un poco. También engaña a muchos equipos haciéndoles creer que están protegidos, porque nunca miran el resultado a través de un decompilador. Cuando lo haces, la ilusión se rompe.

Qué deja el renombrado

Considera una puerta de licenciamiento trivial:

public bool IsActivated(string key)
{
    if (string.IsNullOrEmpty(key)) return false;
    var parts = key.Split('-');
    if (parts.Length != 4 || parts[0] != "LIC") return false;
    long expiry = long.Parse(parts[3]);
    return expiry > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}

Tras una ofuscación de solo renombrado, un decompilador da algo así:

public bool a(string A_0)
{
    if (string.IsNullOrEmpty(A_0)) return false;
    string[] b = A_0.Split('-');
    if (b.Length != 4 || b[0] != "LIC") return false;
    long c = long.Parse(b[3]);
    return c > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}

Los nombres desaparecieron. Nada más. El flujo de control es idéntico, la llamada a DateTimeOffset.UtcNow.ToUnixTimeSeconds() está a la vista (los nombres del framework no se pueden renombrar), la estructura de una clave de licencia está explicada, y la cadena mágica "LIC" está ahí mismo diciéndole al atacante exactamente qué falsificar. Un lector no necesita tus nombres de variables para entender este método; necesita treinta segundos. El renombrado elevó el coste de lectura de «trivial» a «un poco menos trivial».

Ese es el techo del renombrado. Oculta etiquetas. Tu lógica, tus literales y tus constantes no son etiquetas.

Capa 1 — cifrado de cadenas

Los literales de cadena son lo más valioso que recoge un lector, porque se autodocumentan: mensajes de error, rutas del registro, endpoints de API, el prefijo "LIC" de arriba. El cifrado de cadenas las saca de los metadatos y reemplaza cada literal por una llamada que lo descifra en tiempo de ejecución:

if (b.Length != 4 || b[0] != Strings.Get(0x4a1)) return false;

Ahora un decompilador muestra Strings.Get(0x4a1) en lugar de "LIC". El valor sigue existiendo en memoria cuando el programa se ejecuta, así que esto no es criptografía contra un depurador en vivo —pero derrota el ataque abrumadoramente común de leer el binario y hacer grep de sus cadenas. La lista de literales que antes mapeaba todo tu programa desapareció.

Capa 2 — enmascaramiento de constantes

El siguiente regalo gratuito al lector son las constantes numéricas: límites de puestos, timeouts, umbrales de funciones, el 4 y el 3 que describen tu formato de clave. El enmascaramiento de constantes reemplaza los números literales por pequeñas expresiones evaluadas en tiempo de ejecución, de modo que parts.Length != 4 se convierte en algo que el decompilador representa como aritmética sobre valores opacos en vez de un 4 impreso. Individualmente menor; en conjunto, elimina la segunda forma más fácil de entender qué comprueba un método.

Capa 3 — aplanamiento del flujo de control

El renombrado, el cifrado de cadenas y el enmascaramiento de constantes dejan intacta la forma del método: el decompilador sigue imprimiendo bloques if/else/while limpios. El aplanamiento del flujo de control rompe eso. Reescribe el método como un único bucle alrededor de un switch de máquina de estados, donde cada bloque básico original se convierte en un caso y una variable despachadora decide qué se ejecuta a continuación:

int state = 0;
while (true)
{
    switch (state)
    {
        case 0: /* original block A */ state = 7; break;
        case 7: /* original block B */ state = 2; break;
        // ... blocks reordered, linked only by the dispatcher
        case 2: return result;
    }
}

El decompilador ya no puede reconstruir la estructura original if/while, porque esa estructura se disolvió en datos. Un humano todavía puede trazarlo, pero «trazar una máquina de estados aplanada con cadenas cifradas y constantes enmascaradas» es una tarde distinta a «leer el método renombrado».

Renaming+ String enc.+ Const. mask+ CF flatten+ Virtualize

Capa 4 — virtualización, para el código que importa

Todo lo anterior sigue compilando a IL, y el IL tiene excelentes decompiladores. Para el puñado de métodos que de verdad vale la pena proteger —la comprobación de licencia, un algoritmo de precios, un secreto comercial central— la virtualización de métodos va más allá: reemplaza el IL del método por un bytecode propio y envía un pequeño intérprete que lo ejecuta. No hay decompilador para un bytecode que no existía hasta que tu compilación lo produjo. Un atacante debe primero hacer ingeniería inversa de la máquina virtual y luego del programa que corre sobre ella. Eso es caro, que es justo el objetivo.

La virtualización es la transformación más pesada, así que la aplicas de forma acotada —a los pocos métodos donde el coste de lectura debería medirse en días, no en minutos— y dejas el resto del ensamblado en las capas más ligeras.

La conclusión práctica

El renombrado es una buena primera capa y una mala última capa. Trata la protección como una pila: renombra ampliamente, cifra cadenas y enmascara constantes en todo lo sensible, aplana el flujo de control de tu código de aplicación de reglas y de PI, y virtualiza los pocos métodos cuya lógica más necesitas conservar. La prueba de si funcionó es simple y la única que importa: abre tu compilación protegida en un decompilador y léela. Si tu comprobación de licencia todavía cuenta su propia historia, te detuviste en el renombrado.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.