Prevent and understand hooking with Harmony and MonoMod
A quick writeup on how to detect hooking and patching done with Harmony and MonoMod.
Introduction
Hooking is a widely known technique which can be used for countless different purposes.
But how many people know that you can patch any .Net method at runtime and hook them to modify parameters, return values or even supress calls?
This can not only be used to bypass DRM and licensing methods but also to view encrypted https traffic within your app.
And thats just what came into my head when i first thought about possible use cases.
In order to prevent people analysing or modifying our application we need to know how Harmony and the underlying MonoMod Framework work.
With the gained knowledge we will try to address multiple ways on how to detect the presence of a hooked (or patched) method at runtime.
What is Harmony
Harmony is an easy to use open source library with the ability to alter and extend the functionality of all available assemblies in a managed application.
It is based upon the MonoMod Framework which basically is the “swiss army knife” when it comes to modding a .Net assembly.
You can read more about those libraries her:
How does it work
Basically, harmony takes any method and copies all the opcodes into a new dynamicly created method to create a backup of it.
This is done because MonoMod will override the first bytes of the compiled original method with a jump to a newly created wrapper method.
The wrapper method will then call a prefix
method right before the copied original method will be called and a postfix
method right after.
The prefix
and postfix
methods are the ones implemented by the user of the library which of course can alter and use the parameters and return values provided to the initial value.
Detecting a hooked method
To detect a hooked method we would first need to optain a pointer to the compiled code.
Most of us probably know the highly useful Marshal
class when you have dealt with unmanaged code or structures already.
It contains the Marshal.GetFunctionPointerForDelegate method which converts a given delegate to a function pointer.
When you read the above documentation it may be clear that a conversion is not what we want. The returned function pointer isn’t of any use for us because it is a pointer to a P/Invoke wrapper for the given delegate which can be used with platform interop.
What we need is a pointer to the compiled managed method.
We want to use the same pointer MonoMod
used to patch the method.
To get what we need we can use the RuntimeMethodHandle.GetFunctionPointer method.
A generic method with the delegate constraint allows us to pass any method as a parameter to our function!
private static IntPtr GetMethodStart<T>(T target) where T : Delegate
{
var method = target.Method;
RuntimeHelpers.PrepareMethod(method.MethodHandle);
return method.MethodHandle.GetFunctionPointer();
}
Edge cases
MonoMod handles some other cases within their GetMethodStart
function which i do not want to explain in this post.
To implement the same behavior we need to use the Ldftn IL-opcode on some methods.
I used a simple cache and a DynamicMethod to retreive the function pointer on those special ones.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
private static IntPtr GetMethodStart<T>(T target) where T : Delegate
{
var method = target.Method;
if (method.IsVirtual && (method.DeclaringType?.IsValueType ?? false))
{
RuntimeHelpers.PrepareDelegate(target);
var dm = DynamicHelper.GetDynamicLdftnMethodDelegate(method);
return dm();
}
else
{
RuntimeHelpers.PrepareMethod(method.MethodHandle);
return method.MethodHandle.GetFunctionPointer();
}
}
private static class DynamicHelper
{
private static readonly Dictionary<MethodInfo, Func<IntPtr>> _cache = new Dictionary<MethodInfo, Func<IntPtr>>();
private static readonly object _lock = new object();
public static Func<IntPtr> GetDynamicLdftnMethodDelegate(MethodInfo info)
{
if (info == null) throw new ArgumentNullException(nameof(info));
lock (_lock)
{
if (_cache.ContainsKey(info))
{
return _cache[info];
}
// setting the owner type and skipVisibility is very important!
var dm = new DynamicMethod(string.Empty, typeof(IntPtr), Type.EmptyTypes, typeof(DynamicHelper), true);
var gen = dm.GetILGenerator();
gen.Emit(OpCodes.Ldftn, info);
gen.Emit(OpCodes.Ret);
_cache[info] = (Func<IntPtr>)dm.CreateDelegate(typeof(Func<IntPtr>));
return _cache[info];
}
}
}
Checking a single method
With the information we got we can now check if a given method is altered by the MonoMod framework.
To do so we simply need to copy the first bytes of the given method and check them against the used JMP
assembler opcodes.
This is pretty straightforward so i only leave the code here for you.
This will also return true when the app is started within visual studio!
public static bool IsPatched<T>(T target) where T : Delegate
{
var address = GetMethodStart(target);
// We only need the first 5 bytes to perform our check
var buffer = new byte[5];
Marshal.Copy(address, buffer, 0, buffer.Length);
// Those 3 cases represent the different opcodes used by MonoMod to place a jump
return buffer[0] == 0xE9
|| (buffer[0] == 0x68 && buffer[4] == 0xC3)
|| (buffer[0] == 0xFF && buffer[1] == 0x25);
}
Detecting strings
Another simple approach to detect the usage of MonoMod or Harmony is to search the text (strings) within loaded modules in our application.
I came up with the following lists of strings which were the most obvious candidates for me.
Module names
0Harmony
HarmonySharedState
MonoMod.Utils.Cil.ILGeneratorProxy
MonoMod.RuntimeDetour
Namespaces
HarmonyLib
MonoMod
Types
MethodPatcher
NativeDetourData
ILGeneratorProxy
They can be detected by using the Assembly type and the Reflection namespace but that is up to you.
Environment Variables
MonoMod can be configured globally on any system using Environment Variables
.
The presence may not be enough to be sure it is actually used within the application.
If you are paranoid you may want to search all variables for string starting with MONOMOD
.
Since retrieving all environment variables at once isn’t really straightforward i’ll leave it here for you to use!
Conclusion
It doesn’t matter if it’s about protecting critical parts of your own application or analysing another assembly. Knowing about this technique and frameworks will not only help you but also save your valuable time.
Make sure to remember what you have learned today on your next project.