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é 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:
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:
.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:
"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.
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:
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.