Function Hooking - Ezekial711/MonsterHunterWorldModding GitHub Wiki
This guide will demonstra how to hook/detour functions to allow for more dynamic control-flow modification.
Note: This guide is for internal only. It is possible to hook externally but it is a much more advanced topic.
To follow this guide you should know/understand
- how to write a plugin
- decent C/C++ knowledge
Tools Used:
- Ghidra / Cheat Engine
- x64dbg
- Visual Studio 2019
The goal for this plugin will be to make all monsters completely inanimate, i.e. they will only use the IDLE action. We will also not do any AI modifications.
I recommend installing Ghidra to follow this guide. Ghidra is a static analysis tool with a disassembler and a decompiler built in. You can optionally also follow along with IDA Pro, tho if you have IDA already I doubt you'd need a guide for function hooking.
I will be using Ghidra for this guide as it's what I'm used to.
First of all, the function we're gonna be hooking is at 0x141ce0a50
, inofficially dubbed "LaunchAction". With that out of the way, to hook a function properly we need to figure out what its parameters and return types are. Ghidra takes care of both of those automatically however they can be figured out manually as well using just a disassembler, like Cheat Engine for example.
First we need to load the game's exe into ghidra. However we don't want to just import the raw exe as it is packed. We need the unpacked exe which we can only get by doing a memory-dump of the running game.
To create a reliable memory dump we need x64dbg. Go ahead and install that.
Now launch the game and x64dbg. Once launched, attach to the game using x64dbg:
In the newly opened process list select MonsterHunterWorld.exe
. Now go to Plugins > Scylla
:
Now click on IAT Autosearch
and then click Dump
. Once done you can close x64dbg.
- Create a new ghidra project
- Click
File > Import File...
- Select the executable you just dumped using x64dbg.
- Leave everything at default and hit
Ok
- Double click the exe in ghidra
Upon opening the CodeBrowser ghidra will prompt you to let it analyze the exe. You can either do that or not. Doing so will yield much better results in both decompilation and disassembly. However based on how many options you select it can take a long time. Expect it to take at least 2 hours.
If you plan on working with ghidra again in the future I highly recommend you let it do the analysis.
If you want a recommendation on which analyzers to select, here is what I usually use:
This will take a while and you cannot interrupt it. The only way of "pausing" it without losing progress is to put your PC into sleep mode. You can't save without it cancelling.
Regardless of if you have decided to let it analyze the exe or not, now we are going to get the signature of the function we are trying to hook.
First, hit G
in the CodeBrowser and enter the address of the function.
Now hit OK
. This will take you to the function we're looking for. If you can't see any disassembly, hit D
, this should force ghidra to disassemble this function.
Upon disassembling, the decompiler (on the right side of the window) should display some sort of result. The first decompilation always looks very rough and not very readable. The function signature might look something like this:
undefined FUN_141ce0a50(undefined* param_1, undefined4 param_2)
Ghidra uses these undefined
types when it can't guess the actual type of something, and only knows their size in bytes. The actual signature of the function is
bool uEnemy::LaunchAction(uint32_t action_id);
Which, when we make it a free-standing function, looks like this:
bool LaunchAction(uEnemy* this, uint32_t action_id);
When I take a look at it in ghidra, it looks like this:
This is because I have already mapped out a lot of stuff in the exe and added custom types to make reverse engineering easier.
But what we need to know is, that the first parameter is some kind of pointer type, and the second is a 32-bit integer.
Ghidra does a very nice job of telling you what the parameters or return types of a function are, but it is of course possible (and sometimes necessary) to do it manually.
Doing so requires some knowledge about the x64 calling convention.
This site states the order and the registers in which parameters are passed to a function:
Parameter Type | first | second | third | fourth | fifth+ |
---|---|---|---|---|---|
Integers | RCX | RDX | R8 | R9 | Stack |
Floating point values | XMM0 | XMM1 | XMM2 | XMM3 | Stack |
Aggregates (8-64 bits) | RCX | RDX | R8 | R9 | Stack |
Pointers | RCX | RDX | R8 | R9 | Stack |
These are the parameter registers, while the return value is always passed in RAX or XMM0.
There are two distict ways of figuring out a functions parameters:
- Where the function is called
- At the beginning of the function
And as such, there are also two ways of figuring out the return value of a function:
- After the function is called
- At the end of the function
If you see any of the parameter registers being used without first having a value put into them, then it is safe to say that those are parameters.
Example (from the LaunchAction function):
mov dword ptr[rsp+0x10],edx ; rdx/edx used uninitialized
push rbx
sub rsp,0x20
mov rbx,rcx ; rcx used uninitialized
We can see that both rcx
and rdx
are used without first being initialized with some value. This already tells us that the function most likely has two (or possibly more) parameters.
Additionally, the fact that rdx
is used as edx
tells us that the second parameter is a 32-bit type, probably an integer.
For the return value, if we head to some of the ret
s of the function, we can see that the game uses al
exclusively:
mov al,0x1
pop rbx
ret
This means that the return value is a one-byte type, which ends up being a bool
most of the time. Occasionally it's actually an 8-bit integer or a char
, but that's pretty rare, at least in this game.
If we check some calls to the function, we might see patterns like this:
mov rcx,qword ptr[rbx+0x30]
mov edx,0x93
call LaunchAction
We already see that 2 values are being put into rcx
and edx
respectively, followed by a call
. This almost always indicates parameters being passed.
MinHook is a hooking library that will make our lives a whole lot easier. With this library we don't have to go into the nitty gritty of overwriting jmp
s and creating gateways for our hooks. We can simply declare a function like normal and hook:
// Declare a function pointer which will be used to call the original function
bool(*LaunchActionOriginal)(void*, uint32_t) = nullptr;
// Declare the hook function
bool LaunchActionHook(void* monster, uint32_t action_id)
{
// do whatever
return LaunchActionOriginal(monster, action_id); // Run original function
}
BOOL APIENTRY DllMain(HMODULE hDll, DWORD dwReason, LPVOID lpReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
// Initialize MinHook
MH_Initialize();
// Create the hook
MH_CreateHook((void*)0x141ce0a50, LaunchActionHook, reinterpret_cast<void**>(&LaunchActionOriginal));
// Enable the hook
MH_EnableHook((void*)0x141ce0a50);
}
return true;
}
This might get a little annoying if you have to hook a bunch of functions. Which is why we have abstraction. Stracker happens to have made a little header file to simplify things. You can find the file here. Go ahead and include
this file in your dllmain.cpp
(or wherever you'd like to place your hook).
With this file you can write the above code like so:
CreateHook((void*)0x141ce0a50, LaunchActionHook, bool, void* monster, uint32_t action_id)
{
// do whatever
return original(monster, action_id);
}
BOOL APIENTRY DllMain(HMODULE hDll, DWORD dwReason, LPVOID lpReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
MH_Initialize();
QueueHook(LaunchActionHook);
MH_ApplyQueued();
}
return true;
}
And... well, this is basically already all of the plugin code we need. Now in our hook, all we have to do is always pass action id 1
to the original function, which is always the IDLE action. (With a few exceptions).
We can do this like so:
CreateHook((void*)0x141ce0a50, LaunchActionHook, bool, void* monster, uint32_t action_id)
{
return original(monster, 1);
}
Now we just pass 1
to the original LaunchAction function, which will make all monsters do their action id 1 exclusively. You'll have a bunch of monsters standing around doing nothing in the map.
And that's it! All done. Go ahead and compile your plugin and give it a test.