Skip to content
← All posts
· Delta1 Labs Hardening.NETSecurity

Runtime patching: the attack your file-integrity check never sees

Anti-tamper that hashes the DLL on disk catches a patched file — but not a method rewritten in memory at runtime. Libraries like Harmony detour your methods without touching the file, which is how modern license bypasses and cheats work. Here is how in-memory patching works and what actually detects it.

Most .NET anti-tamper does one thing: at startup it hashes the assembly and checks the hash against a known-good value, so if someone edited the DLL to delete a license check, the app notices and refuses to run. That is a real and useful defence — against file tampering. But there is a whole class of attack it cannot see, because it never touches the file. The method gets rewritten in memory, after the runtime has already loaded and JIT-compiled it. The bytes on disk are identical; the behaviour is not.

The method the hash check never inspects

A .NET method goes through two forms. On disk it is IL inside the assembly — that is what a file hash covers. At runtime the JIT compiles that IL to native machine code in the process’s memory, and that native code is what the CPU actually executes. In-memory patching targets the second form. It lets the method load and JIT normally — passing any load-time file check — and then rewrites the compiled method, or redirects its entry point, so calls land somewhere else.

The most popular managed tool for this is Harmony. It is a legitimate and excellent library (game mods rely on it), but the same capability that lets a mod change game behaviour lets an attacker neutralise a license check. A Harmony patch of your gate looks like this:

// Attacker's patch assembly, loaded into your process (via a mod loader,
// an injected AppDomain, or a wrapper exe):
[HarmonyPatch(typeof(LicenseGate), nameof(LicenseGate.IsLicensed))]
static class Bypass
{
    // A "prefix" runs before your method and can skip it entirely.
    static bool Prefix(ref bool __result)
    {
        __result = true;   // force IsLicensed() to return true
        return false;      // skip the original method body
    }
}

var harmony = new Harmony("bypass");
harmony.PatchAll();        // detours LicenseGate.IsLicensed in memory

After PatchAll(), every call to LicenseGate.IsLicensed() returns true without your code running. Harmony did this by rewriting the JIT-compiled method with a detour to the prefix — no file was modified. Your Authenticode signature is still valid. Your SHA-256 of the DLL still matches. The load-time integrity check passes, and the gate is wide open.

DLL on diskIL · unchangedHash check: PASSfile is byte-identicalJIT'd methodin process memorydetourAttacker's replacementIsLicensed() → truenever seen by the file hash

Detect the tool, not the file

If you cannot catch the change by hashing the file, catch the conditions the attack needs. In-memory rewriting in .NET relies on one of two things being present in your process, and both are observable at startup with ordinary managed code.

A CLR profiler. The runtime exposes a profiling/instrumentation API that can rewrite IL as it’s JIT-compiled — the sanctioned, powerful way to detour managed methods. The runtime enables it through environment variables, so their presence is a strong signal that something is positioned to rewrite your methods:

static bool ProfilerAttached() =>
    Environment.GetEnvironmentVariable("CORECLR_ENABLE_PROFILING") == "1" ||
    Environment.GetEnvironmentVariable("COR_ENABLE_PROFILING") == "1";

A patching framework loaded in-process. Harmony has to be in your process to patch it, and it ships as a specific assembly (0Harmony). You don’t need to enumerate every assembly — ask the runtime whether its entry type resolves:

static bool HarmonyLoaded() =>
    Type.GetType("HarmonyLib.Harmony, 0Harmony", throwOnError: false) != null
    || Type.GetType("Harmony.HarmonyInstance, 0Harmony", throwOnError: false) != null; // legacy 1.x

Run both at startup (in the module initializer, so they fire before your own code) and react the way you react to any other tampering signal — throw, exit, or hand off to your own handler. Neither check is expensive, and crucially they are managed and cross-platform, so the same guard works on Windows, Linux, macOS and the WASM runtime.

What this does and does not buy you

Be honest about the threat model, because overselling a defence is how you get a false sense of security. These checks raise the cost of a casual detour bypass — the attacker who grabs Harmony and writes a ten-line prefix now has to also defeat a detector before their patch even runs. That stops the opportunistic majority. They do not defeat a determined attacker: someone who controls the environment can rename the patcher assembly, patch out your detector first, or attach a profiler the check doesn’t look for. There is no managed-code check that a sufficiently motivated attacker with full control of the machine cannot eventually remove — the same caveat that applies to every client-side protection.

The right posture is layering. File-integrity anti-tamper catches the patched-binary attack; profiler and patcher detection catches the in-memory-detour attack; and the thing both are protecting — your license gate — should itself be hard to find and hard to read, so an attacker can’t easily locate the method to patch in the first place. A gate buried inside control-flow-flattened and virtualized code, returning a value that feeds into real logic rather than a tidy bool IsLicensed(), is far harder to detour cleanly than a single well-named method. Detection tells you an attack tool is present; obfuscation makes the target it’s aiming at expensive to acquire.

Putting it together

If your anti-tamper story is “I hash the DLL at startup,” you are defending one door and leaving another open. The in-memory patch is the door that modern license bypasses and cheats actually walk through, precisely because it leaves the file — and its signature — pristine. Add startup detection for the two things an in-memory patch needs (a profiler hook, a loaded patcher), react on your terms, and combine it with obfuscation that makes the patched target hard to find. Nebula injects both the file-integrity check and the profiler/patcher detection for you, so the protected build watches the door the hash check can’t see — without you writing any of the detection code by hand.

Try Nebula.NET

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