Skip to content

Análisis a fondo · Nebula.NET

¿Se puede decompilar el código .NET?

Sí — y de forma mucho más completa de lo que la mayoría de los desarrolladores espera. Un ensamblado gestionado no es una caja negra compilada; es una descripción totalmente indexada de tu programa. Esto es exactamente lo que un decompilador extrae de un DLL, por qué el runtime conserva esa información y qué la recupera de verdad.

By Delta1 Labs Engineering Septiembre de 2026 9 min read Ingeniería inversa

Publica un binario de C++ o Rust y un ingeniero inverso parte del código máquina: sin nombres, sin tipos, sin límites de métodos, solo registros y saltos — una escalada dura y con pérdidas. Publica un ensamblado .NET y parte de algo extraordinariamente más amable. C# no compila a código máquina en absoluto; compila a CIL, un conjunto de instrucciones portable, totalmente tipado y basado en pila, y envuelve ese IL en un rico directorio de metadatos que el runtime necesita para cargarlo, verificarlo y aplicarle JIT. Una herramienta gratuita lee ambos de vuelta y reconstruye un C# que a menudo refleja lo que escribiste.

Eso sorprende a la gente, así que este artículo es deliberadamente concreto: qué se puede recuperar, por qué lo conserva el CLR y las cosas específicas que puedes hacer para quitarlo de nuevo sin romper tu programa.

Un DLL es un directorio etiquetado, no una caja negra

Un ensamblado gestionado es un archivo PE cuya carga útil es una imagen CLI: un conjunto de tablas de metadatos más los cuerpos de método IL a los que apuntan. Las tablas son lo revelador. TypeDef y MethodDef contienen los nombres de tus tipos y métodos; Param contiene los nombres de los parámetros; Field contiene los nombres de los campos; el heap #Strings almacena cada uno de esos identificadores como UTF-8, y el heap #US (user-string) almacena cada literal de cadena de tu código. El runtime lo necesita todo — para resolver un Type.GetMethod("Foo"), para enlazar una sobrecarga, para serializar por nombre de propiedad, para imprimir un stack trace. Un decompilador simplemente lee el mismo directorio.

Qué se publica dentro de Your.dll Cabecera PE + CLI punto de entrada, directorio de tablas Tablas de metadatos TypeDef → Pricing, Order MethodDef → Discount, IsVip Param → o, key, total Field → _rate, _customer Heaps #Strings → nombres de identificadores #US → "VIP rule", claves API… Cuerpos de método IL ldarg.1 callvirt get_Total ldc.i4 1000 bgt.s IL_0021 ldstr "VIP rule" ... tipados, estructurados, regiones de excepción intactas
Figura 1 — Las tablas de metadatos nombran cada tipo, método, campo y parámetro; los heaps contienen tus identificadores y literales de cadena; los cuerpos IL conservan los tipos, la estructura y las regiones de excepción. Un decompilador lee el directorio del que depende el propio runtime.

Qué recupera realmente un decompilador

Apunta ILSpy, dnSpy o nuestro gratuito Glass.NET a una compilación de release corriente y no obtienes una aproximación — obtienes tu programa. Toma una pequeña regla de precios, representativa:

Pricing.cs — lo que escribiste
public decimal Discount(Order o)
{
    if (o.Total > 1000m && o.Customer.IsVip)
        return o.Total * 0.15m;      // VIP rule
    return o.Total * 0.05m;
}

El compilador reduce eso a IL. Incluso a nivel de IL, antes de cualquier decompilación, la lógica es legible — los nombres, la constante 1000, las llamadas a propiedades y la ramificación están todos ahí mismo:

Discount — IL tal como lo lee cualquier desensamblador
.method public hidebysig instance valuetype System.Decimal
        Discount(class Order o) cil managed
{
  ldarg.1
  callvirt   instance valuetype System.Decimal Order::get_Total()
  ldc.i4     1000
  ...
  callvirt   instance bool Customer::get_IsVip()
  brfalse.s  IL_0021          // skip VIP branch if not VIP
  ldarg.1
  callvirt   Order::get_Total()
  ldc.r8     0.15             // the VIP rate
  ...
}

Dale eso a un decompilador y reconstruye el flujo de control en if/return reales, repliega las constantes, restaura los nombres desde los metadatos y te entrega código fuente que vuelve a compilar. La salida es esencialmente tu original — regla de negocio, tasa y todo lo demás. Peor aún, los literales de cadena quedan a la vista en el heap #US, así que una cadena de conexión o una clave API embebida está a una sola búsqueda de distancia:

Lo que «Find strings» arroja en una compilación sin protegerexpuesto
"Server=prod-sql;Database=Billing;User Id=svc;Password=…"
"sk_live_9f2a…"                       // an API key compiled into the DLL
"VIP rule"                            // even your comments-turned-messages
«Un DLL de .NET no es una caja cerrada. Es un manual etiquetado e indexado de tu software — a menos que le quites las etiquetas.»

Por qué el CLR conserva todo esto

No es un descuido; es el contrato. El IL se compila con JIT a código máquina en la máquina de destino, así que el ensamblado debe llevar suficiente estructura tipada para que el JIT pueda verificarlo y compilarlo. La reflexión, la serialización, la inyección de dependencias, el enlace de datos y los stack traces resuelven todos los miembros por su nombre en tiempo de ejecución — así que los nombres deben sobrevivir. Esa riqueza es exactamente lo que consume un decompilador. Lo cual apunta directamente al remedio: la ofuscación es la práctica de degradar lo que el archivo revela — renombrando, reestructurando, cifrando — sin romper cómo el runtime lo carga y lo ejecuta.

Qué lo recupera de verdad

No puedes hacer que el código del lado del cliente sea ilegible para siempre, pero puedes subir una escalera que convierta «abre el DLL y léelo» en «dedícale semanas y probablemente ríndete». Cada peldaño elimina algo de lo que el decompilador dependía.

Cada capa elimina algo que el decompilador necesita Renombrado elimina identificadores → a.b(c) Aplanado del flujo de control destruye la estructura if/for Cifrado de cadenas vacía de secretos el heap #US Anti-manipulación / anti-depuración cierra la vía de parcheo y depuración paso a paso Virtualización de código elimina el propio IL — nada que leer
Figura 2 — La protección es sustractiva. Cada peldaño se lleva una entrada concreta de la que dependía el decompilador; la virtualización elimina el IL por completo.

Pasa el mismo método Discount por el renombrado, el aplanado del flujo de control y el cifrado de cadenas y el decompilador ya no muestra una regla ordenada — muestra una máquina de estados opaca con los nombres desaparecidos y la tasa ya no como un literal visible:

Discount — decompilado tras renombrado + aplanado + cifrado de cadenasprotegido
public decimal a(b o)
{
    int s = 3;
    while (true)
        switch (s ^ k)            // runtime-keyed dispatcher; not constant-foldable
        {
            case 0x2F1A: /* … */ s = 0x77C4; break;
            case 0x77C4: /* … opaque blocks; 0.15m masked; "VIP rule" decrypted lazily … */
            // de4dot can't rebuild the original blocks; the rate isn't a plain literal
        }
}

Y para el puñado de métodos que de verdad son el producto — una comprobación de licencia, un paso de derivación de claves, un kernel de precios — la virtualización de código va aún más lejos: elimina por completo el IL del método, reemplazándolo por una llamada a una VM embebida que ejecuta un bytecode personalizado. No queda IL que un decompilador pueda leer.

Míralo primero en tu propio binario

La forma más rápida de apreciar la exposición es mirarla. Abre uno de tus propios DLL de release en el decompilador gratuito Glass.NET y lee un método que consideres propietario — los nombres, las constantes, la estructura estarán todos ahí. Luego protege la compilación con Nebula.NET y ábrelo de nuevo. La diferencia entre una compilación desnuda y una endurecida, en los mismos métodos que te importan, es el argumento entero.

Mira qué revela tu DLL — y luego ciérralo.

Abre tu compilación en el decompilador gratuito Glass.NET para ver exactamente qué queda expuesto, y luego recupéralo con Nebula.NET.