I have a habit of looking through GitHub for mods, patches, and other ways to change the programs I use. Advertising is usually what sends me there. I hate opening an application to find that ads take up half the interface, so I look for a way to remove them when I can.
That is how I found OverwolfPatcher a few years ago.
If you have never used Overwolf, it helps to start there. Overwolf is a platform that hosts other gaming-related applications. It provides shared infrastructure: game integration, overlays, match events, video capture, and other APIs. The application I used, Outplayed, runs on top of it.
Outplayed does something I find genuinely useful. It stays open alongside the game and records the match automatically. Later, I can go back to a kill, a teamfight, or any other moment without having remembered to start recording beforehand.
I liked it quite a bit. I liked everything around it less.
Some features required a premium subscription, and the interface had ads. OverwolfPatcher changed local Overwolf checks, which in turn changed what applications such as Outplayed could see. At the time, I did not need to understand more than that. I downloaded a release, ran it, and got on with things.
I had even learned one oddly specific fact about the project: newer releases sometimes caused trouble for me, so I would fall back to an older version I knew worked.
Then I started university, played far less, and stopped using Outplayed. The patcher disappeared with it and sat forgotten for a few years.
Recently, I started playing a little more and missed being able to revisit matches. I installed Outplayed, opened it, and found that removing the watermark was locked behind premium, alongside three enormous ads on the main screen.

Figure 1. Outplayed’s main screen before the intervention, with the “GO PREMIUM” option and visible ads.
My first reaction was to look for the patcher again.
I downloaded the latest release and ran it.
It did not work.
Since that had happened before, I went straight to the old version I used to keep around. I knew it had worked for me, so I assumed this was just another regression in the patcher itself.
The old version did not work either.
That was what made me open the code.
Until then, I only wanted to recover the effect the patcher had given me years earlier. Reading the code made it clear that it modified Overwolf, specifically parts of the Core that applications on the platform queried.
The old patcher had stayed the same. Overwolf had continued to evolve.
My first hypothesis was fairly mundane. Perhaps a method had been renamed, a class had moved, a function signature had changed, or a DLL was no longer where the patcher expected it.
I expected to find an old part pointing at the wrong place.
The entire architecture had aged out.
Where the patch happened
To change Outplayed’s behavior, I had to follow the decision back to the layer that produced the information it used.
A Windows program is usually split across executables and libraries. In Overwolf, one of those libraries was OverWolf.Client.Core.dll, part of the Core queried by applications installed on the platform.
If Outplayed asks the Core something like “which subscriptions does this extension have?”, there are at least two conceptual places where the result could be changed. I could modify the Outplayed code that interprets the answer, or modify the answer supplied by the Core.
The old patcher did the latter.
Outplayed | | asks about the extension's subscription vOverWolf.Client.Core.dll | | returns the information vOutplayed decides what to doChanging the Core was interesting because it sat between the question and the decision. Instead of looking for every button or check inside Outplayed, I could change the information those decisions consumed.
But how did it change a DLL?
This is the classic on-disk patch: modify the code that will be loaded on the next run. In a native binary, that can mean replacing x86-64 instructions. Here, the target was a .NET assembly, so the patch could happen at another level.
A .NET assembly contains metadata and usually stores method bodies as IL, or Intermediate Language, an intermediate representation understood by the .NET runtime.
OverwolfPatcher used Mono.Cecil to read and rewrite that structure.
A C# method such as:
int GetNumber(){ return 7;}can become something close to this in IL:
ldc.i4.7retldc.i4.7 places the value 7 on the stack used by IL. ret returns that value.
With Cecil, the patcher could locate a method by name, erase its existing instructions, and assemble new ones in their place. An old patch shows this with almost no noise. It finds the method that displayed a Windows Insider block and replaces it with a body equivalent to return false:
MethodDefinition showInsiderBlockMessageMethod = overwolfCoreWManager.Methods .SingleOrDefault(x => x.Name == "ShowInsiderBlockMessage");
showInsiderBlockMessageMethod.Body.Instructions.Clear();showInsiderBlockMessageMethod.Body.Instructions.Add( Instruction.Create(OpCodes.Ldc_I4_0));showInsiderBlockMessageMethod.Body.Instructions.Add( Instruction.Create(OpCodes.Ret));ldc.i4.0 produces false; ret returns it. The method simply stopped having its old body and received another one.
The subscription-related patches were larger, but their mechanism was the same. The patcher built new IL bodies and, at the end, did this:
fullPath.Backup(true);overwolfCore.Write(fullPath.FullName);Console.WriteLine(Utils.Pad("Patched successfully"));First it made a backup. Then it wrote the modified assembly over the original.
The idea was simple: modify the file so that the next time Overwolf ran, it would load different behavior.
It was a persistent patch on disk. The difference from a native patch was the representation involved. Instead of editing machine code, the project rewrote the structure and IL of a .NET assembly.
That difference soon became more than a technical detail.
Why I started with the Core and only two methods
The patcher changed several parts of Overwolf. To isolate the failure, I looked for the smallest path that could still affect Outplayed.
While inspecting the application, I found a legacy subscription path that still queried two Core methods:
GetExtensionSubscriptionsGetExtensionSubscriptionsIdsIn Overwolf, an extension is essentially one of the applications installed on the platform. The first method returned detailed information about the plans associated with an extension. The second returned only their IDs.
Outplayed still registered a legacy provider that queried this information and recognized plan 61. A newer, server-backed system based on Tebex also existed, but the legacy path still contributed to the decision.
I needed to answer a smaller question: if these two methods returned what the patcher expected again, would Outplayed still react?
This was the chain I wanted to test:
Outplayed asks the Core about the subscription | vGetExtensionSubscriptions / GetExtensionSubscriptionsIds | vCore returns legacy plan 61 | vOutplayed includes that result in its local decisionIf it did not work, there would be no point rebuilding the rest of the patcher. I found the first real bug inside this minimal path.
The first fix worked, which is precisely why it misled me
The patcher built one of the price fields with Ldc_R4, which puts a 32-bit float into the IL. The property in the current version expected a 64-bit System.Double.
The entire difference fit in one instruction:
Instruction.Create(OpCodes.Ldc_R4, 1.0f)Instruction.Create(OpCodes.Ldc_R8, 0d)In IL, I was directly assembling the values that subsequent calls would consume. The type had to match what the method expected.
It was exactly the sort of incompatibility I expected to find: Overwolf had changed, while the patcher still emitted the old form.
I corrected it and reduced the patch to the two methods I wanted to study.
Before testing the complete program, I built a fixture, a small assembly with the relevant structure of those methods. It let me check in isolation whether the generated IL was valid and returned the expected result.
The tests passed.
The detailed method returned the expected plan. So did the ID method. The price used the correct type. For another extension, the original path kept working.
At that point, the story looked finished.
I had found and fixed a concrete incompatibility, and the corrected code ran.
Then I ran the patch against the real Overwolf installation.
Overwolf did not even open.
Refusing to start - deployed assembly failed verification:C:\Program Files (x86)\Overwolf\0.309.0.14\OverWolf.Client.Core.dllThis happened before Outplayed started.
I had been testing whether the modified code worked. The launcher rejected the file before that code had any chance to run.
The float to double bug was real. It simply was not the main cause.
The modified file was rejected
First I had to check whether I had merely produced an invalid assembly. An earlier attempt had exposed a strong name failure in another component, so it was important to separate two different concepts.
A strong name is part of a .NET assembly’s identity. Authenticode is a digital signature applied to an executable file on Windows. In simplified terms, the publisher cryptographically signs a hash computed from the relevant parts of the file. Someone can later compute the hash again and verify whether the protected content remains the content that was signed.
The OverWolf.Client.Core.dll I was modifying did not have a strong name. It had a valid Overwolf Authenticode signature.
This does not make a signed DLL immutable. It makes modifications detectable. An on-disk patch works for as long as the loading path accepts the modified file.
The current Overwolf had an explicit check.
Inside the launcher, I found a call to VerifyDeployedAssembliesOrFail(). Before normal initialization continued, this path used WinTrust to verify that the deployed DLLs still carried a valid Overwolf signature.
DLL on disk | vlauncher verifies the signature | +-- valid --> initialization continues | +-- invalid --> file is rejectedThe patcher needed to change the file; the launcher wanted to prove that the file had not changed since it was signed.
The crack depended on changing the file. Verification depended on proving that the file had not changed.
There was still another possible explanation: perhaps my IL was corrupting the assembly. So I removed the patch from the experiment.
I took a clean copy of the Core, opened it with Mono.Cecil, and saved it again without deliberately modifying any method.
Then I compared the methods in the original and rewritten copies and ran the same signature verification used by the launcher.
Here was the result:
| File | Accepted by WinTrust? | Differences in compared methods |
|---|---|---|
| Original Core | True |
reference |
| Core with both methods modified | False |
2 |
| Core only opened and rewritten | False |
0 |
The comparator walked through 16,992 methods and checked IL, properties, local variables, and exception regions. 0 differences does not mean byte-for-byte identity, but it showed that the DLL could fail WinTrust without my changing any of the methods I was trying to patch.
The problem was no longer the IL I wanted to put there. It was the act of rewriting the file.
The simple “open and save again” cycle was enough.
The patcher depended on this premise:
I can replace the installed DLL, and the program will load it.
That premise was no longer true in the current version. I had to stop modifying the file.

Figure 2. The on-disk patch fails before the modified code has a chance to run.
If the disk no longer worked, where could I intervene?
The boundary was now clear. Replacing the DLL before it loaded broke validation. After loading it, the runtime still had to turn the IL into something executable.
To find this interval, I only had to follow a .NET method from disk to the CPU.
The DLL contains the method body as IL. The CLR, the .NET runtime, loads the assembly. When a method needs to run, the JIT, or just-in-time compiler, normally transforms its IL into native code for the machine’s architecture. The CPU executes that native code.
DLL on disk | | contains IL vCLR loads the assembly | vJIT compiles the method | | produces native code vCPU executesThe signature protected the file checked by the launcher. It did not make the method immutable throughout the process lifetime.
The solution had to meet three requirements:
- the DLL on disk had to remain intact and validly signed;
- I had to intervene before the method’s behavior had been consolidated into the native code that would execute;
- because the Core is shared, only Outplayed should receive the modified result. Other extensions still needed to see the original behavior.
At what point has the file already been accepted while its behavior can still be changed?
The first in-memory attempt arrived too late
The first approach used an AppDomainManager to enter early in runtime initialization and redirect the two methods to implementations I generated in memory.
If I could not replace the body in the file, perhaps I could let the DLL load and then redirect the method entry point.
It half-worked in the synthetic test.
GetExtensionSubscriptions was redirected. GetExtensionSubscriptionsIds was not.
The reason was inlining.
A normal call would look like this:
caller -> GetExtensionSubscriptionsIds -> resultThere is a clear entry point to intercept.
The JIT, however, can optimize small methods by copying their bodies directly into the calling code. That is inlining.
without inlining
caller -> GetExtensionSubscriptionsIds -> result ^ can be redirected here
with inlining
caller -> [method code copied here] -> resultOnce inlined, that execution no longer passes through the redirected entry point. Marking the fixture with NoInlining made the test pass, but confirmed that the strategy was fragile. I depended on arriving before optimizations I did not control.
There was another problem. Only the Outplayed UID should receive the modified response; every other extension needed to execute the original method.
Before redirecting the entry point, I saved a delegate to the original method. I thought it would preserve the old implementation.
It did not. After the redirection, the delegate still entered through the same replaced entry point.
replacement -> not the target extension -> call "original" -> entry point is already redirected -> replacement -> call "original" -> ...The fallback that was supposed to escape the patch returned to the patch itself.
The result was a StackOverflowException.

Figure 3. Saving a delegate did not preserve an independent route to the old implementation.
I also tried saving a function pointer before the replacement. That pushed the problem into increasingly low-level details: Marshal.GetDelegateForFunctionPointer, managed-array marshaling, calling conventions, and a calli attempt that ended in an InvalidProgramException.
The errors differed, but all of them pointed to the same issue. I was trying to intervene after the runtime had already started preparing, compiling, and optimizing the method.
The right point was before the JIT
That was when the CLR Profiling API began to make sense.
Despite its name, the CLR Profiling API is not limited to measuring performance. It allows a native DLL to receive callbacks for internal runtime events.
One of those callbacks is JITCompilationStarted. The CLR invokes the profiler when it is about to compile a function.
The second piece was SetILFunctionBody. It lets a profiler give the CLR a new IL body for a method before that compilation.
The point I needed existed immediately before the JIT.
I could let the original DLL pass verification and intervene before the JIT produced native code:
original signed DLL | | launcher verifies: still valid vCLR loads the original assembly | vmethod is about to be compiled | +--> profiler supplies a different IL body | vJIT compiles the supplied body | vCPU executesThe launcher verified one representation. The runtime executed another.
The signed file stayed intact. Inside the process, the CLR received a different body for the method before compiling it.

Figure 4. The verified file stays intact; replacement happens only along the execution path.
The patch ceased to exist in the persistent artifact and existed only inside the process.
How the profiler finds the method without touching the DLL
The profiler is a native x64 DLL written in C++, loaded by the CLR alongside the expected process.
When a function reaches the JIT, the callback receives an identifier. The profiler uses it to discover the module and the method’s metadata token, then checks whether it is one of the two targets.
The new body is transferred through memory supplied by the CLR:
info->GetILFunctionBodyAllocator(module, &allocator);BYTE *destination = reinterpret_cast<BYTE *>( allocator->Alloc(static_cast<ULONG>(body.size())));CopyMemory(destination, body.data(), body.size());hr = info->SetILFunctionBody(module, method, destination);allocator->Release();This path contains no Write() and creates no new OverWolf.Client.Core.dll that has to survive WinTrust. The alternative body exists only in the process.
The behavior of other extensions still had to be preserved.
The method had to preserve its original behavior
Since the method belongs to the shared Core, other extensions can query it as well. A global return value would change the semantics of the entire platform.
The new body therefore needed two behaviors:
if this call is for Outplayed: return the experimental resultotherwise: execute the exact original bodyThis was the same fallback that had broken with the delegate. This time, I could preserve the instructions themselves.
The fallback stopped being a call and became part of the method
The profiler assembles a composite body. A prefix compares the extension UID against the target, followed by the original instructions.
ldarg.0call get_UIDldstr "target extension identifier"call string::op_Equalitybrfalse ORIGINAL
// Build and return the experiment's local result.ret
ORIGINAL:// Original instructions, copied here.If the UID matches Outplayed, the method takes the new path. Otherwise, brfalse jumps to the original instructions inside the same body, with no external delegate or pointer.

Figure 5. The modified and original paths now coexist inside the same IL body.
That removed the recursion, but copying the old code a few bytes farther ahead involved more than concatenating bytes.
Moving IL also moves the method’s internal coordinates
A .NET method body also describes local variables and exception regions (try, catch, finally, filters), which refer to positions inside the method.
When I insert a prefix of length P before the old code, every original instruction begins P bytes later.
Internal relative branches remain valid because their sources and destinations move together. Offsets measured from the start of the method need to be adjusted.
One particularly easy field to get wrong can represent either a type token for a catch or a filter offset, depending on its flags.
const ULONG classOrFilter = (clause.flags & 0x1u) != 0 ? clause.classOrFilter + static_cast<ULONG>(originalEntry) : clause.classOrFilter;If the field contains a filter offset, it has to move with the code. If it contains the token for an exception class, adding bytes would destroy the reference.
The builder also preserves the local-variable signature, the option to initialize those variables, and the rest of the header the JIT needs to interpret the method.
Preserving the instructions is not enough; the result must remain an executable method. Even so, the prefix introduces a call to the UID getter before the old body, so the tests do not prove semantic equivalence for every possible case.
A method number only makes sense within one build
The profiler identifies methods by metadata tokens. A value such as 0x060030B7 can look permanent, but it is not.
Roughly, it means “this entry in this assembly’s method table.” Another build can use the same number for a different method.
The profiler therefore validates SHA-256, architecture, MVID, and metadata before replacing any body. The MVID, or Module Version ID, helps distinguish one build from another.
The rule became conservative: unless the DLL is the exact version I investigated, I do not assume that its tokens, signatures, and formats retain the same meaning.
This does not make the patch universal. It keeps a specific experiment from pretending to be universal.
What this changes in Outplayed
It was important to separate “the crack worked” from “the entire program became premium.” The mode changes one specific local path, not the complete account state.
On the command line, premium is only the name of an instrumentation mode:
.\OverwolfPatcher.exe instrument --mode premium ` --app cghphpbjeabdkomiphingnegihoigeggcfphdofo ` --plans 61--app tells the profiler which UID should take the modified path. --plans tells it which IDs the two local methods should return on that path. For the Outplayed experiment, the observed value was legacy plan 61.
Outplayed still had a legacy provider that queried these two methods and combined the result with the newer Tebex-based path.
The profiler does not create a Tebex subscription or modify server state. It changes two local responses for the target UID.
server / Tebex ----------------------> unchanged
local CoreGetExtensionSubscriptions ------------> modified response for target UIDGetExtensionSubscriptionsIds ---------> modified response for target UID | v local decisions that use this pathIn the observed session, the “Go Premium” button disappeared and some local behavior changed, while the account still displayed Outplayed Core - Free. That fits separate paths using different sources. The experiment changed one branch of the decision, not the entire notion of a premium account.
The profiler only works if the method actually reaches the JIT
The new architecture depended on the method passing through the JIT.
Inlining was one way to miss that opportunity. Another was NGEN native images, which allow the .NET Framework to use precompiled code.
The instrumented process therefore disables inlining and native-image use.
This is a broader intervention, but it ensured that the methods passed through the point where the profiler could replace the IL.
Only then did every piece line up:
1. original file remains on disk2. launcher validates the original signature3. CLR loads the assembly4. target method reaches the JIT5. profiler supplies the composite body6. JIT compiles that body7. only the target UID follows the modified path8. all others fall through to the original instructionsEach requirement now had a specific place in the execution path where it was resolved.
Making the mechanism work does not prove the application changed because of it
The architecture made sense. That still did not demonstrate the effect in the real Outplayed application.
There were four distinct questions:
- can the CLR load the profiler?
- does
SetILFunctionBodyaccept the body I assembled? - can the JIT compile and execute that body?
- does the real Outplayed make a different decision because of it?
The x64 fixture mainly answered the first three. It exercised the mechanics of the instrumentation, not the full set of application states and callers.
The launcher therefore gained intermediate modes: a baseline without instrumentation; bootstrap to load the infrastructure; observe to watch callbacks; neutral to replace IL with bodies equivalent to the originals; and only then the mode that changed the return value.
If bootstrap fails, the new IL was never applied. If neutral fails, the problem lies in the instrumentation rather than plan 61.
The environment interfered as well. Under a restricted automation account, Overwolf failed before reaching the target methods with Failed to initialize CEF runtime. The real test required the interactive account used by Overwolf.
After that, the logs recorded the real method reaching the callback:
2026-09-08T21:09:08.275Z JIT target started function=0x7FFEC3916450 token=0x60030B72026-09-08T21:09:08.284Z premium SetILFunctionBody detailed=ok ids=ok rollback=ok2026-09-08T21:09:08.285Z Core JIT finished function=0x7FFEC3916450 token=0x60030B7 status=0x0That log proves something narrow. The real Core reached the callback, the API accepted the bodies, and the JIT finished compiling the detailed method. By itself, it does not prove the visual effects or, in this excerpt, an independent execution of the ID method.
During the tested session, the “Go Premium” button disappeared, the ads were not visible, and some local features appeared to be available. The plan screen continued to show Outplayed Core - Free.
This is consistent with the reconstructed architecture, but it does not prove that each visual change came from this path. The absence of ads can depend on other conditions, one run does not demonstrate persistence, and server-dependent benefits were outside the scope of the test.
The real run used this command:
PS C:\Users\bruno\Downloads\Compressed\OverwolfPatcher> .\OverwolfPatcher.exe instrument --mode premium --app cghphpbjeabdkomiphingnegihoigeggcfphdofo --plans 61Launched native OverwolfLauncher PID 8636; profiler targets managed Overwolf.exe only.Mode: premium | profiler log: C:\Users\bruno\Downloads\Compressed\OverwolfPatcher\artifacts\profiler\0.309.0.14-20260909-211034.logInstalled files were not modified. Remove the profiling environment by launching Overwolf normally.PS C:\Users\bruno\Downloads\Compressed\OverwolfPatcher>The resulting Outplayed session showed the local changes described above. Even though the account did not have a premium subscription, the ads were gone and the option to hide the game layout was available for free:

Figure 6. After the run, Outplayed shows premium-gated local features without a premium subscription: the ads are gone and the game layout can be hidden for free.
The problem was when to intervene
When I opened OverwolfPatcher, my question was:
which part of this patch is incompatible with the current version?
I expected an old method, a different signature, or an incorrect instruction. The float where a double was now required was a real bug, but the corrected DLL still had no chance to execute.
The decisive experiment was the one in which I changed no methods at all. Merely opening the Core with Cecil and writing it back made the launcher reject the copy.
The logical content of the patch could be corrected.
The delivery mechanism for that content had stopped being compatible with the program.
The old patch assumed it could replace the file on disk. The version I investigated first checked whether the DLL still matched the signed artifact. Persisting with an on-disk patch meant persisting with the wrong question.
The useful question became:
At what point has the file already been accepted, but the behavior has not yet become native code?
The answer was immediately before the JIT.
The file stays original. When the target method is about to be compiled, the profiler supplies a different IL body. The Outplayed UID takes the experimental path, while all others fall through to the original instructions.
I started by trying to change what was in the file. I ended by changing when the intervention happened: after verification, before the JIT.