Cada ensamblado .NET compilado lleva consigo un plano casi perfecto de sí mismo. A diferencia de C o Rust, que compilan a código máquina puro, C# compila a IL — un conjunto de instrucciones de alto nivel y totalmente tipado que mantiene intactos los límites de los métodos, las variables locales, las regiones de excepción y la información de tipos. Eso es maravilloso para las herramientas y terrible para el secreto: un decompilador gratuito como ILSpy, dnSpy o nuestro propio Glass.NET convierte ese plano de nuevo en C# legible en segundos.
Para la mayoría del código eso está bien. Pero algunos métodos son el producto — una comprobación de licencia, una rutina de derivación de claves, un algoritmo de precios, un test anti-trampas. Publícalos como IL corriente y se los habrás entregado a cualquiera que tenga un decompilador. La pregunta que responde este artículo es: ¿cómo publicas un método cuya lógica simplemente no está ahí para leerse?
La escalera de la protección
La ofuscación no es una sola cosa; es una escalera de técnicas, cada una de las cuales eleva un poco más el coste de entender el código. El renombrado quita los nombres. El aplanado del flujo de control destruye la estructura. El cifrado de cadenas oculta los literales reveladores. Cada una es valiosa — y cada una deja todavía IL real en la página para que un lector decidido (o una herramienta automática) la reconstruya. La virtualización es un tipo de paso distinto: se lleva el IL.
Qué recupera realmente un decompilador
Considera una pequeña y representativa puerta de licencia. En el código fuente es evidente:
public static bool IsValid(string key)
{
if (string.IsNullOrEmpty(key)) return false;
int sum = 0;
foreach (char c in key) sum = sum * 31 + c;
return (sum & 0xFFFF) == 0x4D2; // the magic the attacker wants
}
Renómbralo y aplánalo, y un decompilador ya no muestra el bucle ordenado — pero sigue recuperando C# funcional: un despachador while(true) sobre un switch, con cada operación real (sum * 31 + c, el & 0xFFFF, la constante mágica) ahí mismo, en los casos. Un lector paciente, o un desofuscador que reconstruye la máquina de estados, recupera la lógica.
Virtualiza el mismo método y el decompilador recupera esto — y solo esto:
public static bool IsValid(string key)
{
return VirtualMachine.Run(
typeof(LicenseGate).TypeHandle, 100663302, /* packed args */);
} El bucle desapareció. La constante desapareció. La comparación desapareció. Lo que queda es un stub que entrega un id de método opaco y los argumentos a una máquina virtual. El algoritmo sigue ejecutándose — de forma idéntica bit a bit — pero ya no existe como instrucciones que un decompilador pueda leer. Existe como bytecode, en un recurso, en un lenguaje que el atacante nunca ha visto.
«Un decompilador es un traductor de idiomas. La virtualización cambia el idioma a uno que no conoce — y solo tu compilación conoce el diccionario.»
Cómo virtualiza Nebula un método
En tiempo de compilación, Nebula compila el IL del método a un conjunto de instrucciones personalizado y compacto, almacena ese bytecode en un recurso embebido y reescribe el cuerpo del método como un stub que invoca la VM. Nada de tu proyecto cambia; ocurre sobre la salida compilada.
El bytecode en sí es un conjunto de instrucciones pequeño y basado en pila — cargar argumento, cargar constante, multiplicar, xor, comparar, saltar, retornar — codificado como números opacos. Un fragmento de la puerta de licencia anterior, conceptualmente, se convierte en:
LDARG 0 ; key length already resolved
LDC 0
STLOC 0 ; sum = 0
loop: LDLOC 0 LDC 31 MUL ; sum * 31
LDARG 1 ADD STLOC 0 ; + c → sum
... BRTRUE loop
LDLOC 0 LDC 0xFFFF AND
LDC 0x4D2 CEQ RET En tiempo de ejecución, la VM inyectada lee ese flujo y lo ejecuta sobre su propia pila de operandos. La VM de Nebula es deliberadamente exacta con enteros — refleja el modelo de pila de evaluación del CLR, de modo que la aritmética (incluido el desbordamiento de 32 bits) se comporta de forma idéntica al método original. Por eso la virtualización preserva el comportamiento: las mismas entradas producen las mismas salidas, siempre. Solo se virtualizan los métodos que Nebula puede compilar con una semántica demostrablemente idéntica; cualquier otro se deja intacto.
Por qué es tan difícil de revertir
Un decompilador funciona porque entiende un idioma: IL. Apúntalo a un método virtualizado y no tiene nada que decompilar — el IL es una llamada a VirtualMachine.Run. Para recuperar el algoritmo, en cambio, un atacante debe:
Eso supone un salto enorme en esfuerzo. Leer IL aplanado es una tarde; revertir una VM de bytecode y reconstruir un programa a partir de ella es un proyecto serio y especializado — para un único método, sin garantía de que la VM de la siguiente compilación tenga el mismo aspecto. Para el puñado de métodos que de verdad son tu producto, esa asimetría es justamente el objetivo.
Cuándo usarla
La virtualización es más pesada que el renombrado o el aplanado, así que está pensada para los pocos métodos que un atacante ataca primero — el validador de licencia, la puerta «es-pro», un paso de derivación de claves, un kernel propietario. Apunta Nebula a esos por su nombre y deja que las demás capas protejan el resto. Combinada con un flujo de control resistente a de4dot y con el cifrado de cadenas, esos métodos se vuelven impracticables de recuperar por cualquier medio automático.
Y como el runtime de la VM está destinado a .NET Standard 2.0, los ensamblados virtualizados se ejecutan allá donde lo hace .NET — .NET Framework 4.6.1+, .NET 6–10, incluso Blazor WebAssembly enviado al navegador, donde la exposición es la mayor de todas.
Protege el código que es tu producto.
La virtualización de código forma parte de Nebula.NET Enterprise. Consulta la guía técnica o prueba el ofuscador completo gratis.