Skip to content

Deep Dive · Nebula.NET

Code that ships with nothing to read

Renaming hides what things are called. Control-flow flattening scrambles how they run. Code virtualization removes the code entirely — replacing a method's IL with a custom bytecode only an embedded virtual machine understands. Here's what that means, how it works, and why it defeats the decompiler.

By Delta1 Labs Engineering September 2026 9 min read Enterprise feature

Every compiled .NET assembly carries a near-perfect blueprint of itself. Unlike C or Rust, which compile to raw machine code, C# compiles to IL — a high-level, fully-typed instruction set that keeps method boundaries, local variables, exception regions and type information intact. That is wonderful for tooling and terrible for secrecy: a free decompiler such as ILSpy, dnSpy or our own Glass.NET turns that blueprint back into readable C# in seconds.

For most code that's fine. But some methods are the product — a license check, a key-derivation routine, a pricing algorithm, an anti-cheat test. Ship those as ordinary IL and you've handed them to anyone with a decompiler. The question this article answers is: how do you ship a method whose logic simply isn't there to read?

The protection ladder

Obfuscation isn't one thing; it's a ladder of techniques, each raising the cost of understanding the code a little higher. Renaming strips the names. Control-flow flattening destroys the structure. String encryption hides the giveaway literals. Each is valuable — and each still leaves real IL on the page for a determined reader (or an automated tool) to work back through. Virtualization is a different kind of step: it takes the IL away.

cost to reverse → Renaming Control-flow + String enc. Virtualization no IL to read
Figure 1 — Each technique raises the cost of understanding a method. Virtualization is the top rung: there is no original IL left to analyze.

What a decompiler actually recovers

Consider a small, representative license gate. In source it's obvious:

LicenseGate.cs — original source
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
}

Rename and flatten it, and a decompiler no longer shows the tidy loop — but it still recovers working C#: a while(true) dispatcher over a switch, with every real operation (sum * 31 + c, the & 0xFFFF, the magic constant) sitting right there in the cases. A patient reader, or a deobfuscator that rebuilds the state machine, gets the logic back.

Virtualize the same method and the decompiler recovers this — and only this:

LicenseGate.dll — decompiled after virtualizationprotected
public static bool IsValid(string key)
{
    return VirtualMachine.Run(
        typeof(LicenseGate).TypeHandle, 100663302, /* packed args */);
}

The loop is gone. The constant is gone. The comparison is gone. What remains is a stub that hands an opaque method id and the arguments to a virtual machine. The algorithm still runs — bit-for-bit identically — but it no longer exists as instructions a decompiler can read. It exists as bytecode, in a resource, in a language the attacker has never seen.

“A decompiler is a language translator. Virtualization changes the language to one it doesn't know — and only your build knows the dictionary.”

How Nebula virtualizes a method

At build time, Nebula compiles the method's IL into a compact custom instruction set, stores that bytecode in an embedded resource, and rewrites the method body as a stub that invokes the VM. Nothing about your project changes; it happens on the compiled output.

C# source your project IL readable blueprint Nebula IL → bytecode Custom bytecode embedded resource Stub → VM.Run(id) replaces the method body
Figure 2 — The build-time pipeline. Eligible methods become bytecode in a resource; the method body becomes a one-line stub that calls the embedded VM.

The bytecode itself is a small, stack-based instruction set — load argument, load constant, multiply, xor, compare, branch, return — encoded as opaque numbers. A fragment of the license gate above, conceptually, becomes:

VM bytecode (illustrative)
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

At runtime the injected VM reads that stream and executes it on its own operand stack. Nebula's VM is deliberately integer-exact — it mirrors the CLR's evaluation-stack model, so arithmetic (including 32-bit overflow) behaves identically to the original method. That's why virtualization is behaviour-preserving: the same inputs produce the same outputs, always. Only methods Nebula can compile with provably identical semantics are virtualized; anything else is left untouched.

Why it's so hard to reverse

A decompiler works because it understands one language: IL. Point it at a virtualized method and it has nothing to decompile — the IL is a call into VirtualMachine.Run. To recover the algorithm, an attacker must instead:

The VM’s inner loop fetch decode execute advance To read your method, an attacker must first… 1. reverse-engineer the VM interpreter 2. recover the per-build bytecode format 3. extract the encrypted bytecode from the resource 4. rebuild a disassembler and reconstruct the program …before reading a single line of your logic.
Figure 3 — There is no shortcut. The decompiler is useless against bytecode it doesn't understand; the work moves from “decompile a method” to “reverse-engineer a virtual machine.”

That is an enormous jump in effort. Reading flattened IL is an afternoon; reversing a bytecode VM and reconstructing a program from it is a serious, specialised project — for a single method, with no guarantee the next build's VM looks the same. For the handful of methods that genuinely are your product, that asymmetry is the whole point.

When to use it

Virtualization is heavier than renaming or flattening, so it's meant for the few methods an attacker attacks first — the license validator, the “is-pro” gate, a key-derivation step, a proprietary kernel. Point Nebula at those by name and let the other layers protect the rest. Combined with de4dot-resistant control flow and string encryption, those methods become impractical to recover by any automated means.

And because the VM runtime targets .NET Standard 2.0, virtualized assemblies run everywhere .NET does — .NET Framework 4.6.1+, .NET 6–10, even Blazor WebAssembly shipped to the browser, where exposure is highest of all.

Protect the code that is your product.

Code virtualization is part of Nebula.NET Enterprise. See the technical guide, or try the full obfuscator free.