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.
What a decompiler actually recovers
Consider a small, representative license gate. In source it's obvious:
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:
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.
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:
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:
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.