Skip to content

Análisis a fondo · Nebula.NET

Código que se publica sin nada que leer

El renombrado oculta cómo se llaman las cosas. El aplanado del flujo de control desordena cómo se ejecutan. La virtualización de código elimina el código por completo — reemplazando el IL de un método por un bytecode personalizado que solo entiende una máquina virtual embebida. Esto es lo que significa, cómo funciona y por qué derrota al decompilador.

Por el equipo de ingeniería de Delta1 Labs Septiembre de 2026 9 min de lectura Función Enterprise

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.

coste de revertir → Renombrado Flujo de control + Cifr. cadenas Virtualización sin IL que leer
Figura 1 — Cada técnica eleva el coste de entender un método. La virtualización es el peldaño más alto: no queda IL original que analizar.

Qué recupera realmente un decompilador

Considera una pequeña y representativa puerta de licencia. En el código fuente es evidente:

LicenseGate.cs — código fuente original
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:

LicenseGate.dll — decompilado tras la virtualizaciónprotegido
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.

Código C# tu proyecto IL plano legible Nebula IL → bytecode Bytecode personalizado recurso embebido Stub → VM.Run(id) reemplaza el cuerpo del método
Figura 2 — El pipeline en tiempo de compilación. Los métodos elegibles se convierten en bytecode dentro de un recurso; el cuerpo del método pasa a ser un stub de una línea que llama a la VM embebida.

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:

Bytecode de la VM (ilustrativo)
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:

El bucle interno de la VM captar decodificar ejecutar avanzar Para leer tu método, un atacante primero debe… 1. aplicar ingeniería inversa al intérprete de la VM 2. recuperar el formato del bytecode por compilación 3. extraer el bytecode cifrado del recurso 4. reconstruir un desensamblador y rehacer el programa …antes de leer una sola línea de tu lógica.
Figura 3 — No hay atajo. El decompilador es inútil frente a un bytecode que no entiende; el trabajo pasa de «decompilar un método» a «aplicar ingeniería inversa a una máquina virtual».

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.