Skip to content
← All posts
· Delta1 Labs Glass.NETDecompilerObfuscationNebula.NET

Did your obfuscation actually work? Check it in Glass.NET

A clean build is not proof your code is protected. Open the obfuscated assembly in Glass.NET — the free, static-only .NET decompiler — and read it the way an attacker will: confirm renaming, string encryption, control-flow and virtualization held, and that you did not over-protect your public API.

You ran your obfuscator. The build succeeded, the tests are green, the package is signed. So your code is protected now — right?

Maybe. A successful build tells you the obfuscator didn’t crash. It tells you nothing about whether the output is actually hard to read. The only way to know that is to look at the assembly the way an attacker will: drop it into a decompiler and see what comes back. If the C# is still legible, you shipped a false sense of security.

The right tool for that check is a decompiler you trust and that never runs the binary. Glass.NET is free, and it is strictly static — it reads metadata and IL and reconstructs C#, and never executes the assembly it inspects. That makes it the natural companion to an obfuscator like Nebula.NET: you protect with one, you verify with the other, and you never have to run a build you are not sure about.

Why “the build succeeded” is not verification

Obfuscation is a set of transformations applied to IL. Any of them can be misconfigured, scoped too narrowly, or silently skipped. A rename rule that excludes too much, a string-encryption pass that never ran, a control-flow setting left off, a method you meant to virtualize but forgot to mark — none of those produce a build error. They produce a binary that looks protected in your release folder and reads like source in a decompiler.

So treat verification as a required step, not an afterthought. The procedure is simple: decompile the assembly before and after protection, and compare.

Set up a before-and-after comparison

Keep a copy of the unobfuscated build. Open it in Glass.NET first and browse to a class you care about — a licensing check, a pricing routine, anything you would not want copied. You will almost certainly be startled by how readable it is; for an unprotected assembly, the reconstructed C# is often close enough to recompile.

Now open the protected build. Glass.NET has a side-by-side assembly compare with a line diff, so you can put the two versions next to each other and read the difference directly rather than flipping between windows. Go method by method through the code you expected the obfuscator to touch and ask, for each one, the specific questions below.

1. Did renaming hold?

The cheapest protection is identifier renaming — turning ValidateLicenseKey, _customerBalance and ComputeDiscount into meaningless symbols. In the unprotected build you will see your real names. In the protected build, internal types and members should read like a, b, c or similar noise:

// before
internal sealed class LicenseValidator
{
    public bool ValidateLicenseKey(string key) { ... }
}

// after
internal sealed class a
{
    public bool b(string A_0) { ... }
}

Two things to confirm. First, that renaming happened at all — if your names survived, the pass did not run or your keep-rules are far too broad. Second, and just as important, that it did not go too far: your public API, reflection targets and serialization contracts must stay intact, or your library breaks for callers. Use Go To All (Ctrl+T) and the assembly-information view to confirm your public surface still has its real names while everything internal is gone. Over-obfuscation is as real a bug as under-obfuscation; Glass lets you see both.

2. Are the strings gone?

Decompiled code leaks badly through plain string literals — URLs, SQL, error messages, format strings, feature-flag names. String encryption stores those encrypted and decrypts them only at use. To verify, search the decompiled output for a literal you know was in the source — a URL, an error message, a magic constant. In a correctly encrypted build you will not find it as text; you will find a call that returns the string at runtime instead.

Be honest about what this proves. The literal is still decrypted in memory when the code runs, so a determined attacker with a debugger can recover it. What you have confirmed is that the trivial attack — grepping the binary or the decompiled source for a keyword — now returns nothing. That alone removes a large amount of low-effort exposure.

3. Is the control flow a tangle?

Renaming hides what things are called; control-flow obfuscation hides what the code does. A protected method should no longer decompile to clean, linear C#. Instead of a readable if/else, you should see flattened logic — a switch on a state variable, opaque branches, gotos — the shape a decompiler produces when it can no longer recover the original structure:

// after control-flow obfuscation, decompiled
int num = 0;
while (true)
{
    switch (num)
    {
        case 0: num = (cond ? 2 : 1); continue;
        case 1: result = a(); num = 3; continue;
        case 2: result = b(); num = 3; continue;
        case 3: return result;
    }
}

If the body still reads like the source you wrote, control-flow protection is not reaching that method. The bar worth aiming for is not merely “confuses a human” but “survives automated deobfuscators,” so it is reasonable to run a deobfuscator over the build as well — but the first, free check is simply reading it in Glass.

4. Did your sensitive methods actually virtualize?

Code virtualization is the strongest common protection, and in Nebula.NET it is a Pro feature. It converts a method’s IL into bytecode for a small virtual machine embedded in the assembly, so there is no ordinary IL left for a decompiler to reconstruct. This is the one check where the result is unmistakable. Switch Glass.NET to its IL view on a method you virtualized. You should not see your algorithm. You should see a call into the VM interpreter with an opaque data argument — the bytecode that means nothing without the matching runtime:

// IL view of a virtualized method
IL_0000: ldarg.0
IL_0001: ldsfld     uint8[] '<VM>'::'data_7f3a'
IL_0006: call       object '<VM>'::Run(object, uint8[])
IL_000b: ret

The C# view for the same method collapses to a single opaque call too. If, instead, you can still read your logic in either view, that method was never virtualized — check that you marked the right methods, because virtualization is applied selectively (it is slower at runtime, so you reserve it for your most sensitive code, not the whole assembly).

Read the exposure report, then decide

Glass.NET also produces an exposure report that summarizes how much of an assembly is readable — a quick, high-level gauge to run on both builds. It is a useful triage signal, but it does not replace reading the methods you care about most. A report can tell you the ratio improved; only your own eyes on the decompiled ValidateLicenseKey can tell you the thing you were actually trying to protect is no longer sitting in the open.

None of this makes your code impossible to reverse — nothing does, and any tool that promises “uncrackable” is selling you something. What this workflow gives you is the truth: you can see exactly how high you raised the bar, catch the passes that silently did nothing, and confirm you did not accidentally rename your public API into oblivion. Protect with Nebula.NET, verify with Glass.NET — which is free, static-only, and the honest second opinion on your own build. New to the decompiler side of this? Start with how to decompile a .NET DLL to C#.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.