Sep 7, 20266 min read/2026/09/07/patching-dotnet-assemblies-how-license-cracks-work/

Patching .NET Assemblies: How License Cracks Actually Work — and Why Client-Side DRM Is Theater

If the client machine runs your code, the client controls your code. Everything else is a speed bump.

I've written before about decompiling and recompiling .NET, and the uncomfortable takeaway from that piece deserves its own article: a compiled .NET assembly is not a locked box. It's IL plus metadata, and it decompiles almost perfectly back to readable C#. Names, control flow, string literals — most of it survives the round trip through the compiler. Which means any logic you ship to a customer's machine, including the code that decides whether they've paid you, is sitting right there to be read and edited.

This is a defender's article. I'm going to show you how license checks actually get patched — the real techniques, not hand-waving — because you cannot reason about a threat you don't understand. Every example runs against a toy license check I wrote myself; nothing here is aimed at anyone's product, and cracking commercial software you don't own is both illegal and a lousy thing to do. The point is the opposite: to convince you that the check you're relying on is weaker than you think, and to send you toward the design that actually holds.

The target: a perfectly ordinary license check

Here's the shape of the thing, and it's the shape of almost every client-side license check I've ever seen:

public static class License
{
    public static bool IsValid(string key)
    {
        // hash the key, compare to an embedded value, check an expiry... whatever
        return Verify(key);
    }
}

// somewhere in the app:
if (!License.IsValid(userKey))
{
    ShowNagScreen();
    return;   // premium features stay locked
}
EnablePremiumFeatures();

The business's entire revenue model comes down to one bool computed on a machine the attacker owns. Let me show you five ways that bool becomes true without a key.

1. Flip the branch

Open the assembly in dnSpyEx (the maintained fork of dnSpy) or ILSpy. Find License.IsValid. In dnSpy you can literally right-click → Edit Method (C#), change the body to return true;, compile, and File → Save Module. Done — the recompiled assembly always reports a valid license.

If you'd rather work at the instruction level, the check compiles to something like:

call   bool License::IsValid(string)
brfalse.s  IL_002a      // if false, jump to the nag screen

Change brfalse (branch if false) to brtrue and the logic inverts. Or nop out the branch entirely so it never jumps to the failure path. One instruction, and the gate is gone.

2. Stub the method

Why patch every call site when you can patch the method once? Replace the entire body of IsValid with two instructions:

ldc.i4.1     // push the integer 1 (true)
ret          // return it

Now every caller — current and future, however many there are — gets true. The same trick no-ops an enforcement method: replace its body with a bare ret and the "phone home and shut down if unpaid" routine becomes a function that does nothing.

3. The ildasm / ilasm round-trip

No GUI required, and this is the old-school approach that predates the fancy tools. The .NET SDK ships a disassembler and an assembler:

ildasm app.dll /out=app.il      # assembly  -> human-readable IL text
# edit app.il: change ldc.i4.0 to ldc.i4.1, flip a beq/bne, delete a call...
ilasm app.il /dll /output=app.dll   # IL text -> assembly again

The IL is plain text in between. You search for the method, change the constant or the comparison, reassemble. Because it's text, it scripts trivially — which is exactly how "one-click" patchers get built.

4. Mono.Cecil — patching as a program

Everything above, done programmatically. Mono.Cecil reads and writes assemblies as object graphs, so a patcher is just a small console app:

var asm = AssemblyDefinition.ReadAssembly("app.dll", new() { ReadWrite = true });
var method = asm.MainModule.Types
    .SelectMany(t => t.Methods)
    .First(m => m.Name == "IsValid");

var il = method.Body.GetILProcessor();
method.Body.Instructions.Clear();
il.Append(il.Create(OpCodes.Ldc_I4_1));   // true
il.Append(il.Create(OpCodes.Ret));
asm.Write();

This is the important one, because it shows the real economics. Writing the patch takes minutes; running it takes milliseconds and requires zero skill. A cracker does the hard part once and ships a tool that anyone can double-click. Your protection has to win every single time; theirs has to win once.

5. Never touch the file at all

You can leave the assembly on disk completely untouched and patch it at runtime. A tiny loader uses reflection to reach in and set the private _isLicensed field to true after the app starts, or a library like Harmony — built for game modding — replaces IsValid with a hook that returns true, in memory, without ever modifying the executable. File-integrity checks that hash the DLL on disk see nothing wrong, because on disk nothing is wrong.

"But I strong-name / sign / obfuscate it"

Every developer's first instinct, and here's why each one buys less than you hope:

  • Strong naming. People believe the strong-name signature makes an assembly tamper-proof. It mostly doesn't. For an app loaded from a normal folder (not the GAC), the runtime generally doesn't re-verify the signature at load time, and even where it would, the cracker simply strips the strong name and fixes up the references, or re-signs everything with their own key. A signature proves who built it, not that it still says true things.
  • Authenticode (the certificate on the .exe) is the same story one level up: patch the binary and the signature just shows as invalid — the program still runs. It's a trust signal for users, not an integrity lock on your logic.
  • Obfuscation — renaming, control-flow flattening, string encryption, anti-tamper self-checksums — raises the cost of finding the check. It does not change the math. The anti-tamper routine is itself code shipped to the attacker, so it can be found and patched like anything else. Obfuscation turns a five-minute crack into a five-hour one. Against a motivated cracker, five hours is nothing, and it only has to happen once.

Notice the pattern under all of it: every defense is more code running on the attacker's machine, and any code running on the attacker's machine can be modified by the attacker. That's not a bug in a particular tool. It's the ground truth of client-side execution.

The lesson that actually holds

You cannot win a fight where the opponent owns the CPU, the memory, and the debugger. So stop trying to win it there. The designs that genuinely protect revenue move the trust boundary off the client:

  • Keep the valuable part on the server. If the premium feature is a computation or a dataset, run it behind an authenticated API. The client becomes a thin renderer that has nothing worth cracking — there's no local bool to flip because the capability never shipped.
  • Gate on server-signed tokens, not local booleans. Online activation where the server issues a short-lived, signed entitlement that unlocks real functionality is far stronger than any IsValid(), because forging it means breaking cryptography, not flipping a branch.
  • Design the business for the world as it is. Anything you ship offline will eventually be cracked if enough people want it. The companies that thrive treat legitimate licensing as a convenience — updates, support, cloud sync, the path of least resistance — rather than a wall. Make paying easier than pirating and most people pay.

Client-side license checks aren't worthless; they keep honest people honest and stop casual sharing. Just price them correctly in your head: they are a speed bump, not a gate. Understand the five techniques above not so you can use them, but so you stop betting your product on a line of code that the customer's machine is free to rewrite.