Function Hooking
The Vezel.Ruptura.Memory package provides an easy-to-use FunctionHook class that takes care of some common requirements such as placing the trampoline near the target function, atomically toggling an installed hook, associating state with the hook function, and avoiding certain cases of deadlock or stack overflow.
Usage looks like this:
var kernel32 = NativeLibrary.Load("kernel32.dll");
try
{
using var manager = new PageCodeManager();
var setThreadDescription = (delegate* unmanaged[Stdcall]<nint, char*, int>)NativeLibrary.GetExport(
kernel32, "SetThreadDescription");
var setThreadDescriptionHook = (delegate* unmanaged[Stdcall]<nint, char*, int>)&SetThreadDescriptionHook;
using var hook = FunctionHook.Create(manager, setThreadDescription, setThreadDescriptionHook, "bar");
hook.IsActive = true;
fixed (char* ptr = "foo")
_ = setThreadDescription(-1, ptr);
}
finally
{
NativeLibrary.Free(kernel32);
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
static int SetThreadDescriptionHook(nint hThread, char* lpThreadDescription)
{
Console.WriteLine(new string(lpThreadDescription) + FunctionHook.Current.State);
return 0;
}This example will print foobar to the console. There is a lot going on here, so let us go over each part:
The
NativeLibraryAPI is used to retrieve a function pointer to theSetThreadDescriptionfunction.A
PageCodeManagerinstance is created to manage code allocations. This is used byFunctionHookto allocate its internal trampoline near the target function.FunctionHook.Create()is called to hookSetThreadDescriptionwith theSetThreadDescriptionHookmethod. The string object"bar"is passed as the hook's associated state so that it can be accessed in the hook method.A created hook is fully functional with all code emission and patching done, but calls will not actually be intercepted until the
IsActiveproperty is set totrue, so that is then done.The function pointer for the target function is invoked with some dummy arguments. Since the hook is active, the
SetThreadDescriptionHookmethod will be called.SetThreadDescriptionHookaccesses theFunctionHook.Currentproperty and retrieves itsStateproperty (i.e. the"bar"string passed earlier). It concatenates it with the string passed in aslpThreadDescriptionand prints the result ("foobar") to the console.
This example is fairly contrived, but you can hook almost any function, whether it comes from the operating system, a normal library, or an executable. Of course, you can also associate more useful state objects with the hook so that you can access your application's state.
It is important that the hook method matches the target function exactly in calling convention (cdecl, stdcall, etc), return type, and parameter types. Mismatches can result in unpredictable bugs and crashes. Also, managed exceptions thrown from within a hook method must be handled; native call frames cannot be correctly unwound by CoreCLR.
Hook Gate
Internally, FunctionHook uses a so-called hook gate as part of its trampoline to guard calls to the hook method. The most user-visible effects of this are:
The
FunctionHookinstance for the most recent hook method in the call stack can be accessed through theCurrentproperty.Hook recursion does not invoke the hook method a second time, preventing common issues like deadlocks and stack overflows when calling arbitrary code in a hook.
Hooks are not invoked while the Windows loader lock is held since managed code is virtually impossible to run reliably when that is the case.
Last updated