de4dot is the de-facto automatic deobfuscator for .NET, and it is very good. Point it at an assembly protected by a mainstream obfuscator and, in a single command, it recognises the scheme and reverses it — restoring names, decrypting strings inline, rebuilding flattened control flow, and stripping the anti-tamper. Minutes of work, fully automated. Which means the honest test of any obfuscator isn't "does the output look scary" — it's does the output survive de4dot? If a tool folds to de4dot app.dll, it sold you a speed bump.
Nebula is engineered to answer that test. Not by being unrecognised by accident, but by defeating each specific technique de4dot's automatic pass relies on. To see why, you first have to see what de4dot actually does.
How de4dot works
de4dot runs a pipeline. First it fingerprints the assembly against a library of known obfuscators; a match unlocks a scheme-specific un-transform. For anything it doesn't recognise it falls back to generic techniques that work against the common patterns obfuscators use, regardless of vendor. Those generic stages are the interesting ones, because they're what a novel obfuscator has to beat:
Stage: control flow it can't constant-fold
de4dot un-flattens a method by treating the dispatcher's state variable as a value it can constant-propagate: it walks the switch, computes the next state at each block statically, and rebuilds the original edges. That works when the next state is a literal assignment — which, in most obfuscators, it is:
int state = 0;
while (true) switch (state)
{
case 0: DoA(); state = 2; break; // next state is a constant →
case 2: DoB(); state = 5; break; // de4dot propagates it and
case 5: return; // rebuilds: DoA(); DoB();
}
Nebula's flattening masks the selector with a value only known at runtime. The stored states aren't the case labels; the case labels are state ^ key, and key is computed by a loop de4dot can't evaluate statically:
long key = ComputeKeyAtRuntime(); // opaque; not a compile-time constant
int state = 0x51ED2704;
while (true) switch (state ^ key) // selector depends on runtime key
{
case 0x51ED2701: /* … */ state = 0x51ED2705; break;
case 0x51ED2705: /* … */ state = 0x51ED27F0; break;
case 0x51ED27F0: /* decoy: unreachable, but not provably dead */ break;
// …
}
Because the selector is state ^ key and key isn't a constant, there's nothing to propagate — de4dot's control-flow deobfuscator runs and the method stays flattened. The Enterprise aggressive tier adds decoy states that can't be proven dead and pruned, so even partial reconstruction is polluted.
Stage: string encryption it can't auto-strip
de4dot's headline win is string decryption. It recognises the canonical decrypter — a static method that maps a token to a plaintext string — invokes it directly for every call site, and replaces ldc.i4 token; call Decrypt with the recovered literal. The critical assumption is that the decrypter is a pure function of its argument, so de4dot can call it in isolation:
static string Decrypt(int token) { … } // depends only on 'token'
// de4dot just calls Decrypt(0x1A2B) itself and inlines the result. Nebula's decryption takes a per-call-site salt. Each site passes its own value, and the routine's result depends on where it was called from — so there's no single pure function de4dot can invoke to reproduce the strings:
x.d("\uded4\uded4\ude98…", 52751); // salt differs per site
x.d("\uf5f7\uf5f3\uf5f9…", 58659);
x.d("\uecf7\uece4\ueced…", 64565);
// no single Decrypt(token) reproduces these; the auto-decrypt pass fails.
That signature isn't the string(int) shape de4dot auto-detects, and calling it blindly with the wrong site context yields garbage — so the literals stay encrypted in the output.
Stage: a call graph it can't resolve
de4dot's proxy-call fixer reverses known delegate/proxy schemes — it recognises the specific way a given obfuscator forwards calls and rewrites the site back to the real target. Nebula's reference obfuscation routes calls through injected, unbranded proxy methods that match no catalogued scheme, so the generic fixer has no pattern to apply. Neither de4dot nor a decompiler can tell which external or BCL method a given site actually invokes.
Stage: methods with no IL at all
Deobfuscation, by definition, operates on obfuscated IL. Code virtualization leaves de4dot nothing to operate on: the method body is a call into an embedded VM, and the logic lives as custom bytecode in a resource. There's no flattened graph to rebuild, no decrypter to invoke, no proxy to resolve — just a language de4dot has never seen.
“A deobfuscator reverses transformations it recognises. Nebula's job is to make sure the automatic pass recognises nothing — and, where it matters, finds no IL at all.”
The proof — run it yourself
We don't ask you to take this on faith; we test it every release. We build a sample, protect it with Nebula, and run it through de4dot — including forcing the dedicated control-flow deobfuscator with --f cflow. The outcome is consistent:
> de4dot sample.dll
Detected Unknown Obfuscator
Cleaning sample.dll
Saving "sample-cleaned.dll"
> ilspycmd sample-cleaned.dll # inspect the "cleaned" output
• control flow: still flattened (state ^ key intact)
• strings: still encrypted (per-site salts intact)
• virtualized: still a VirtualMachine.Run(...) stub
de4dot runs to completion and reports success — but the "cleaned" assembly is functionally as protected as the input. You can reproduce it in three steps: protect your build with nebula --config nebula.config.json, run de4dot yourapp.dll, then open the result in Glass.NET or ILSpy and confirm the layers are intact.
What reaches each edition
Automatic-deobfuscation resistance begins at the licensed (Pro) tier and is strongest at Enterprise:
| Technique | Free | Licensed | Enterprise |
|---|---|---|---|
| Non-foldable control-flow dispatch | — | ✓ | ✓ |
| Per-call-site string encryption | — | ✓ | ✓ |
| Proxy-call (call-graph) obfuscation | — | ✓ | ✓ |
| Aggressive tier (decoy states) | — | — | ✓ |
| Code virtualization | — | — | ✓ |
Ship output that survives the one-click strip.
Nebula.NET's layered protection is designed to defeat de4dot's automatic deobfuscation — and we verify it every release.