Hacking .NET – rewriting code you don’t control

Have you ever encountered a code library dependency that you didn’t own but wanted to alter its behavior? Often times the method is non-public and there’s no good way to override its behavior. You can see how it works (because you’re awesome and use a .NET decompiler like the one that comes with Resharper or Rider, right?), you just can’t change it. And you really need to change it because… reasons.

There are a few options available for you:

  1. Get original source code either via decompiling or download the source code (if it’s available in the first place). This is usually a rabbit hole adventure as it will often come with the complex build process, many dependencies, and now you’re responsible for maintaining a whole fork of the library even though you wanted to make just a tiny change
  2. Decompile the app using ILDasm, patch the IL code directly and assemble it back using ILAsm. In many ways, this is better as you can create a strategic surgical incision rather than a full-on “make from scratch” approach. The downside is you have to implement your method entirely in IL, which is a nontrivial adventure.

Both of the above methods will also not work if you’re dealing with signed libraries.

Now let’s look at approach #3 – memory patching of methods. This is the same techniques used for decades by game cheating engines which attach to a running process, find memory locations and alter their behavior. Sounds complicated? It’s actually far easier to do in .NET then it sounds. We going to use a library called Harmony, available on NuGet via “Harmony.Lib” package. This is a memory patching engine for .NET mainly targeting games built in Unity.

Today I’m going to show you how to alter something you didn’t think was possible – we going to change what DateTime.Now returns.

After creating a project and adding a dependency to Harmony.Lib, we going to build a patch class. Harmony doesn’t use any base classes or interfaces but instead relies on special “patch” classes marked with [Harmony] attribute with methods of particular signature. First, we need to add a static method that identifies the target method we want to rewrite. We can use a helper class AccessTools to assist with reflection.

[Harmony]
class Patch
{
    static MethodBase TargetMethod() 
    {
        return AccessTools.Property(typeof(DateTime), nameof(DateTime.Now)).GetMethod;
    }

}

Now let’s write a new implementation for Now property getter method in C# that will return time in PST time zone regardless of what is set on the system.

static DateTime MyCustomDateTime() =>
  TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"));

Finally, we need to rewrite the method body of the existing method to call our new implementation. Harmony exposes a hook via Transpiler method passing the existing method body as a collection of IL instructions and replace it with new instructions returned by the method.

static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instr) =>
new[]
{
    new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Patch), nameof(MyCustomDateTime))),
    new CodeInstruction(OpCodes.Ret)
};

While this may look scary and complicated, it’s actually not. The CodeInstruction invokes the IL Call instruction which has a parameter accepting method to execute, in this case it’s our own easy-to-read C# implementation. Second instruction just returns the value of the Call method.

The final step is to instruct Harmony to apply the patches. We can do it in our startup code as following

static void Main(string[] args)
{
    Console.WriteLine($"Before patch: {DateTime.Now}");
    var harmony = HarmonyInstance.Create("harmony");
    harmony.PatchAll(Assembly.GetExecutingAssembly());
    Console.WriteLine($"After patch: {DateTime.Now}");
}
Before patch: 4/26/2019 4:56:06 PM
After patch: 4/26/2019 1:56:06 PM

As you can see, this offers a crazy amount of new possibilities. Remember, with great power comes great responsibility. Because you’re overriding behavior in a way not intended by the original developer, there are no guarantees that your patch code will work if they release a new version of their code.

Helpful tips

  • If you need to pass parameters into your method, you need to load them on to the stack before the Call instruction (usually via Ldarg_* OpCodes). If the parameters are using non-public types, accept them as object type in your own method and work with them through reflection. See Harmony utilities to simplify working with reflection or cast type dynamic
  • The first parameter of the instance method will always be the object instance it’s declared on. This will correspond to OpCodes.Ldarg_0.
  • If you are not comfortable writing IL by hand, fear not. If you don’t already have it, get yourself a copy of LINQPad (which by the way is awesome and is invaluable for .NET prototyping). You can write a method that matches what you’re trying to do in C# and let LINQPad show you what it looks like in IL.

You can find the full source code for this article over at the GitHub repo.

4 Comments

  1. Pingback: Hacking .NET – rewriting BCL methods with memory patching - How to Code .NET

    1. Andrew Stakhov April 27, 2019 at 12:54 pm

      You’re right, but in your case, the original method still runs, you’re just running it’s output through another method and potentially altering the return value. This will work for many use-cases, but not all. I’ve used the above technique to completely replace the SSPI layer in WCF that is used in Integrated Windows Authentication to instead use MIT Kerberos instead of embedded windows provider. The end result I can actually deploy legacy apps, in containers, without domain joining the host or doing any special config to container startup (like GMSA) and still allow them to talk using IWA.

    2. Andrew Stakhov April 27, 2019 at 1:00 pm

      I just read Harmony docs again, and you’re completely right, this could have been accomplished with a Prefix patch.

Leave a comment

Your email address will not be published. Required fields are marked *