Skip to content

Deep Dive · Nebula.NET

How .NET licensing gets cracked — and how to stop it

Adding a license check to a .NET app takes an afternoon. Making it survive contact with an attacker is the part most teams underestimate. Here are the three attacks that break license gates — down to the individual IL bytes — and the specific defense each one requires.

By Delta1 Labs Engineering September 2026 10 min read Software licensing

You wrote a clean license gate. It validates the key, checks the seat count, flips the app to "Pro." It compiles, it works, you ship it. And in an unprotected .NET assembly an attacker neutralises it in about the time it took you to read this paragraph — not because your logic is weak, but because the check ships as readable, editable IL, and the runtime executes whatever the assembly says. The lock is fine. It's bolted to a glass door.

Cracking a license isn't magic, and it isn't one technique. It's three, each targeting a different weakness, and each with a specific, well-understood defense. Understand the attack and the protection follows.

Anatomy of a license gate

Here's a representative check — nothing naïve, the shape most in-house licensing takes:

LicenseManager.cs
public bool IsLicensed()
{
    if (!ValidateKey(_key)) return false;          // signature / checksum
    if (SeatsInUse > _license.Seats) return false; // seat count
    return true;
}

public bool IsPro => IsLicensed() && _license.Tier >= Tier.Pro;

Every one of the three attacks below starts from the decompiled form of this method — because, as covered in what a decompiler recovers, an unprotected build hands it over intact.

Attack 1 — Patch the check

The most common crack doesn't even read your logic; it edits one instruction. In IL, IsLicensed ends with a conditional branch that decides licensed-vs-not. The attacker opens the assembly in dnSpy, finds that branch, and inverts it — or replaces the entire method body with two bytes that mean return true. The opcodes are tiny and well known:

The one edit — IL opcode bytesexposed
; invert the decision — flip the branch opcode:
  brfalse.s  (0x2C)   →   brtrue.s  (0x2D)     // one byte

; or blow the method away entirely — overwrite the body with:
  ldc.i4.1   (0x17)   ; push 1  (true)
  ret        (0x2A)   ; return it
; "IsLicensed()" now unconditionally returns true.

That's it. No understanding of your key format required. The app is now "licensed" against nothing.

Defense: anti-tamper. Nebula injects a load-time integrity check. At build time it hashes the protected method bodies and stores the digests, encrypted, in the assembly. At startup the runtime component rehashes the loaded code and compares. Change one byte — flip that branch, stub that method — and the hashes diverge, and the assembly refuses to run (or reacts however you configure). The patch attack is detected by the binary itself.

At build method body IsLicensed() hash → H₀ stored, encrypted At load patched body 2C → 2D rehash → H₁ recomputed H₀ ≠ H₁ tamper detected → halt
Figure 1 — Anti-tamper turns the binary into its own witness. The build-time digest and the load-time rehash only agree if not a byte was changed; a patched branch trips the check.

Attack 2 — Read the algorithm, write a key-gen

Patching produces a cracked copy. A key-gen produces something worse: keys that your own code accepts as genuine. If ValidateKey is readable, an attacker decompiles it, recovers the format and the checksum, and writes a generator. No files are modified — every install is "legitimately" licensed against forged keys, and anti-tamper never fires because nothing was tampered with.

ValidateKey — recovered by decompilationexposed
private static bool ValidateKey(string key)
{
    // groups of 5, Base32, last group is a checksum of the first three
    var parts = key.Split('-');
    int sum = 0;
    foreach (var p in parts.Take(3))
        foreach (char c in p) sum = sum * 33 + Decode(c);
    return Base32(sum & 0x3FFFFFF) == parts[3];   // ← the whole scheme
}

With that in hand the key-gen is a dozen lines. The problem isn't the algorithm's strength — it's that the algorithm is visible.

Defense: hide the algorithm. This is exactly what code virtualization is for. Virtualize ValidateKey and its IL is gone — replaced by a call into an embedded VM running custom bytecode. There's no readable checksum to reimplement; recovering it means first reverse-engineering the VM. Pair that with string encryption so the Base32 alphabet and magic masks aren't sitting in the #US heap, and control-flow flattening on the surrounding code, and rebuilding the scheme turns from an afternoon into a research project.

“Licensing is a security feature. Shipping its algorithm as readable IL is like printing the key-cutting instructions on the back of the lock.”

Attack 3 — Rip out the gate entirely

The subtlest attack ignores the check altogether. Rather than defeat IsLicensed, the attacker studies how IsPro unlocks features and simply calls the paid paths directly, or rebuilds the app with the licensing module deleted. If your gating is one boolean consulted in one place, removing it is trivial once the code is readable.

Defense: make the module unreadable and the wiring untraceable. Rename everything internal so there's no LicenseManager.IsPro to find; flatten the control flow so the gate isn't a clean branch to delete; and use reference (proxy-call) obfuscation so the calls into gated features route through injected, unbranded proxies — the attacker can't grep the call graph for "the unlock." And critically, don't let the client be the sole authority: pair Nebula's protection with server-side activation so the decision that matters is made where the attacker can't edit it.

Every attack path, and what closes it 1 · Patch the branch flip 2C→2D, or stub return true Anti-tamper binary rehashes itself → mismatch halts 2 · Key-gen from algorithm read checksum, forge valid keys Virtualization + string encryption no readable algorithm to reimplement 3 · Rip out the gate delete module, call paid paths Rename + proxy calls + server auth no traceable wiring; authority off-box Layered — no single path breaks the gate
Figure 2 — Each attack maps to a defense. Real protection is applying all of them, so no single technique — and no single edit — breaks the gate.

The defense stack

A crack-resistant .NET license gate is never one trick; it's the layering:

  • Anti-tamper so the binary can't be patched without detecting itself.
  • Virtualization on the validator so the algorithm can't be read or reimplemented.
  • String encryption + control-flow flattening so the scheme, its constants and its messages are hidden.
  • Proxy-call obfuscation + renaming so the "unlock" path can't be located or lifted.
  • Server-side activation for anything that must be authoritative — so the client isn't the only line of defense.

None of this makes cracking impossible — nothing client-side does. It makes it expensive enough that it isn't worth it, which for commercial software is the entire game.

Make your licensing hold up.

Nebula.NET hardens the code that enforces your license — anti-tamper, string encryption, control-flow, and Enterprise code virtualization.