Creating hook definitions#
Whether you are writing a library, or writing a single-file mod, pyMHF provides a convenient way of declaring the information required to hook or call any given function relating to a binary, whether it be imported, exported or a function defined within the binary.
This functionality is provided by the function_hook
and static_function_hook
functions, to decorate non-static and static methods/functions respectively.
These decorators do a few things when applied:
The decorated function/method is inspected and the argument names and type hints are collected and used to construct a
FuncDef
object which is used by pyMHF to tell minhook how to hook the required function.It also enables calling the function or method directly (more on that below).
Finally, the decorators transform the function or method into a decorator which can be applied to the method in our mod which we wish to use as a detour.
Because of this first point, the decorated function MUST have correct type hints. If they are not correct, then the hook will likely fail, produce incorrect results, or even cause the program to crash.
The best way to see how these decorators are used is with a few code examples.
Imported function hooks#
1import ctypes.wintypes as wintypes
2from logging import getLogger
3
4from pymhf import Mod
5from pymhf.core.hooking import static_function_hook
6
7logger = getLogger()
8
9
10@static_function_hook(imported_name="Kernel32.ReadFile")
11def ReadFile(
12 hFile: wintypes.HANDLE,
13 lpBuffer: wintypes.LPVOID,
14 nNumberOfBytesToRead: wintypes.DWORD,
15 lpNumberOfBytesRead: wintypes.LPDWORD,
16 lpOverlapped: wintypes.LPVOID,
17) -> wintypes.BOOL:
18 pass
19
20
21class ReadFileMod(Mod):
22 @ReadFile.after
23 def after_read_file(self, *args):
24 logger.info(f"after readfile: {args}")
Here we can see that we have defined a function ReadFile
which has the same definition as the function with the same name in the Kernel32
windows system dll.
As seen here, the static_function_hook
decorator transforms the ReadFile
itself into a decorator which we then used to specify that the after_read_file
method is to be used as the detour run after the Kernel32.ReadFile
function.
Exported function hooks#
Exported functions are those which are provided by the binary itself. There are often not many, and often less which may be useful, but if you get lucky there may be some where are useful to hook or even call.
1import ctypes
2import logging
3
4from pymhf import Mod
5from pymhf.core.hooking import static_function_hook
6from pymhf.core.utils import set_main_window_active
7from pymhf.gui.decorators import gui_button
8
9logger = logging.getLogger()
10
11FUNC_NAME = "?PostEvent@SoundEngine@AK@@YAII_KIP6AXW4AkCallbackType@@PEAUAkCallbackInfo@@@ZPEAXIPEAUAkExternalSourceInfo@@I@Z"
12
13
14class AK():
15 class SoundEngine():
16 @static_function_hook(exported_name=FUNC_NAME)
17 def PostEvent(
18 in_ulEventID: ctypes.c_uint32,
19 in_GameObjID: ctypes.c_uint64,
20 in_uiFlags: ctypes.c_uint32 = 0,
21 callback: ctypes.c_uint64 = 0,
22 in_pCookie: ctypes.c_void_p = 0,
23 in_cExternals: ctypes.c_uint32 = 0,
24 in_pExternalSources: ctypes.c_uint64 = 0,
25 in_PlayingID: ctypes.c_uint32 = 0,
26 ) -> ctypes.c_uint64:
27 pass
28
29
30class AudioNames(Mod):
31 def __init__(self):
32 super().__init__()
33 self.event_id = None
34 self.obj_id = None
35
36 @gui_button("Play sound")
37 def play_sound(self):
38 if self.event_id and self.obj_id:
39 set_main_window_active()
40 AK.SoundEngine.PostEvent(self.event_id, self.obj_id, 0, 0, 0, 0, 0, 0)
41
42 @AK.SoundEngine.PostEvent.after
43 def play_event(self, *args):
44 self.event_id = args[0]
45 self.obj_id = args[1]
46 logger.info(f"{args}")
In the above example, we are hooking the AK::SoundEngine::PostEvent
function which the No Man’s Sky binary includes as an export (as many games which use the AudioKinetic library likely also do).
The mod will also provide a button which, when pressed will play the last played audio by the game.
There are a few thihngs to note in this example:
The
exported_name
argument tostatic_function_hook
is the “mangled” name. This is the recommended way to provide this and it should be used over the “unmangled” version since it means there is no ambiguity or confusion when doing a lookup by name in the exe.The
static_function_hook
decorator is applied to a method of the nested classes. For static methods this isn’t really required, however it is nice since it adds some structure to these function calls (this point is NOT true for non-static methods as you will see in the next section!).We can call the static method by caling the method directly despite there being no function body. The actual implementation of the calling is done by pyMHF itself so you don’t need to worry about it.
Normal function hooks#
Normal functions are just functions which are provided by the binary but not exported. It is these functions that would generally require a bit of reverse engineering experience to determine the function signature of so that they can be hooked correctly.
Defining functions to hook is done in much the same way as above, however, we simply provide either the relative offset within the binary, or a byte pattern known as a signature which can be used to uniquely find the start of the function within the binary.
Hint
When to use signature
or offset
?
If your binary never changes (ie. is never updated by the developers etc), then use offset
as it’s trivial to obtain for every single function in a binary.
If the binary receives updates, then the signature
is the only option as offset
values will change as the binary does.
1import ctypes
2import logging
3from typing import Annotated, Optional
4
5from pymhf import Mod
6from pymhf.core.hooking import Structure, function_hook
7from pymhf.core.memutils import map_struct
8from pymhf.core.utils import set_main_window_active
9from pymhf.gui.decorators import gui_button
10from pymhf.utils.partial_struct import Field, partial_struct
11
12logger = logging.getLogger()
13
14
15@partial_struct
16class TkAudioID(ctypes.Structure):
17 mpacName: Annotated[Optional[str], Field(ctypes.c_char_p)]
18 muID: Annotated[int, Field(ctypes.c_uint32)]
19 mbValid: Annotated[bool, Field(ctypes.c_bool)]
20
21
22class cTkAudioManager(Structure):
23 @function_hook("48 83 EC ? 33 C9 4C 8B D2 89 4C 24 ? 49 8B C0 48 89 4C 24 ? 45 33 C9")
24 def Play(
25 self,
26 this: ctypes.c_ulonglong,
27 event: ctypes.c_ulonglong,
28 object: ctypes.c_int64,
29 ) -> ctypes.c_bool:
30 pass
31
32
33class AudioNames(Mod):
34 def __init__(self):
35 super().__init__()
36 self.event_id = 0
37 self.obj_id = 0
38 self.audio_manager = None
39 self.count = 0
40
41 @gui_button("Play sound")
42 def play_sound(self):
43 if self.event_id and self.obj_id and self.audio_manager:
44 set_main_window_active()
45 audioid = TkAudioID()
46 audioid.muID = self.event_id
47 self.audio_manager.Play(event=ctypes.addressof(audioid), object=self.obj_id)
48
49 @cTkAudioManager.Play.after
50 def after_play(self, this, event, object_):
51 audioID = map_struct(event, TkAudioID)
52 logger.info(f"After play; this: {this}, {audioID.muID}, object: {object_}")
53 self.audio_manager = map_struct(this, cTkAudioManager)
54 self.event_id = audioID.muID
55 self.obj_id = object_
In the above we have defined the cTkAudioManager
class with the Play
method.
This method uses the function_hook
decorator, not the static_function_hook
decorator for the simple fact that this is not a static method. This means that if you want to call the method you need to call it on the instance of the class, not the class type (see line 47).
One implication of the above is that the first argument of the method decorated with the function_hook
decorator should always be this
(generally typed as ctypes.c_uint64
for a 64 bit process, or ctypes.c_uint32
for a 32 bit process). On the other hand, any function decorated with static_function_hook
will not have this
as an argument.
Important
The function_hook
decorator MUST be applied to methods of a Structure
. This class is a thin wrapper around the ctypes.Structure
class, but we require this for the calling functionality to work correctly (check out the source code if you are curious why!)
The static_function_hook
doesn’t have this restriction (but it is permissible)
Because of this, you cannot use the function_hook
decorator on a plain function, it MUST be used on a method!
We can see that when we call the function we can either use positional arguments or keyword arguments. This function can be called the exact same way any function would be called, and we can in fact define default values for some arguments so that we don’t need to specify the arguments when calling (see for example exported_hook_mod.py lines 20-25).
Note
When calling functions we DO NOT provide the this
argument to non-static functions. Your IDE will only show the arguments after that argument, and the value is automatically added by pyMHF internally.
Note
Often one of the trickiest things when writing a mod is getting a pointer to the instance of the class that you are interested in. You generally will get this from the first argument of some function that you hook (as it is the this
argument), but sometimes other structs may contain this pointer. It is really up the binary in question.
Overloads#
It is possible to define function overloads however there are two methods, each with their own pro’s and con’s.
Using the overload
method and overload_id
argument#
The static_function_hook
and function_hook
decorators both have an overload_id
argument which is used to uniquely identify the overload (this will be required later when we want to call or hook this function).
The methods also need the typing.overload
decorator. Note that pyMHF actually monkeypatches this decorator so that it doesn’t remove information from the original function that we need.
To hook or call a function with an overload, append .overload(overload_id: str)
to the original function. This will refer to the overloaded function.
1import ctypes
2import logging
3from typing import Annotated, Optional, overload
4
5import pymhf.core._internal as _internal
6from pymhf import Mod
7from pymhf.core.hooking import Structure, function_hook
8from pymhf.core.memutils import map_struct
9from pymhf.core.utils import set_main_window_active
10from pymhf.gui.decorators import gui_button
11from pymhf.utils.partial_struct import Field, partial_struct
12
13logger = logging.getLogger()
14
15
16@partial_struct
17class TkAudioID(ctypes.Structure):
18 mpacName: Annotated[Optional[str], Field(ctypes.c_char_p)]
19 muID: Annotated[int, Field(ctypes.c_uint32)]
20 mbValid: Annotated[bool, Field(ctypes.c_bool)]
21
22
23@partial_struct
24class cTkAudioManager(Structure):
25 @function_hook("48 89 5C 24 ? 48 89 6C 24 ? 56 48 83 EC ? 48 8B F1 48 8B C2", overload_id="attenuated")
26 @overload
27 def Play(
28 self,
29 this: ctypes.c_ulonglong,
30 event: ctypes.c_ulonglong,
31 position: ctypes.c_ulonglong,
32 object: ctypes.c_int64,
33 attenuationScale: ctypes.c_float,
34 ) -> ctypes.c_bool:
35 pass
36
37 @function_hook("48 83 EC ? 33 C9 4C 8B D2 89 4C 24 ? 49 8B C0 48 89 4C 24 ? 45 33 C9", overload_id="normal")
38 @overload
39 def Play(
40 self,
41 this: ctypes.c_ulonglong,
42 event: ctypes.c_ulonglong,
43 object: ctypes.c_int64,
44 ) -> ctypes.c_bool:
45 pass
46
47
48class AudioNames(Mod):
49 def __init__(self):
50 super().__init__()
51 self.audio_manager = None
52 self.event_id = None
53 self.obj_id = None
54
55 @gui_button("Play sound")
56 def play_sound(self):
57 if self.event_id and self.obj_id and self.audio_manager:
58 set_main_window_active()
59 audioid = TkAudioID()
60 audioid.muID = self.event_id
61 self.audio_manager.Play.overload("normal")(event=ctypes.addressof(audioid), object=self.obj_id)
62
63 @cTkAudioManager.Play.overload("normal").after
64 def after_play(self, this, event, object_):
65 audioID = map_struct(event, TkAudioID)
66 logger.info(f"After play; this: {this}, {audioID.muID}, object: {object_}")
67 self.audio_manager = map_struct(this, cTkAudioManager)
68 self.event_id = audioID.muID
69 self.obj_id = object_
70
71 @cTkAudioManager.Play.overload("attenuated").after
72 def after_play_attenuated(self, *args):
73 logger.info(f"Just played an attenuated sound: {args}")
- Pros:
It provides a nice clean way to reference the overloaded functions.
- Cons:
Type hinting is lost when doing function calls.
Name overloaded functions differently#
The other option for overloaded functions is to simply give them different names. So for example if you had two functions play_song(id: int, volume: float)
and play_song(id: int, volume: float, position: vector3)
, you might call one play_song
, and then call the other play_song_at_pos
.
- Pro’s:
No need to use the
.overload
method oroverload_id
.Function calls are type hinted.
- Con’s:
Doesn’t stay accurate to actual function names (if known).
Tips and hints#
The above can see a bit daunting at first, but once you get a handle on it it can be very easy to create new functions, and this can even potentially be automated to some degree with scripts for IDA or ghidra.
There are a few useful things to consider or keep in mind however:
When to use static_function_hook
or function_hook
?#
Often when you start to reverse engineer a program, you will not know whether or not some function is just a function, or a method bound to some class. Because of this you will often start out with a collection of plain functions with the static_function_hook
decorator.
Once you start to realise that the functions are actually associated with some class, you will likely start to structure these methods so that they belong to this class which may have some known fields.
Using before
and after
methods#
The .before
and .after
method of the functions decorated by the function_hook
or static_function_hook
is required to be used when using this as a decorator to tell pyMHF whether to run the detour before or after the original function. If this is not included then an error will be raised.
Function type hints#
As mentioned at the start of this document, it is critical that the functions which are decorated with these two decorators have correct and complete type hints.
These types MUST be either a ctypes plain type (eg. ctypes.c_uint32
), a ctypes pointer to some type, or a class which inherits from ctypes.Structure
. Note that the Structure
inherits from this so a type inheriting from this type is also permissible.
Further, you will have seen above that none of these functions have any actual body. This is because even when we call this function, we don’t actually execute the code contained within it.
Because of this it’s recommended that you simply add pass
to the body of the function as above.
Any docstrings which are included as part of the body will be shown in your IDE of choice, so if you are writing a library it’s recommended that you add docstrings if convenient so that users may know what the function does.
Warning
It seems to due to how the ctypes.c_char_p
is implemented, using it as an arg type is not recommended as it can cause issues with the data passed to the argument which can cause program crashes.
Instead, use either ctypes.c_ulong
or ctypes.c_ulonglong
depending on whether you are hooking a 32 or 64 bit process respectively, then do arg_value = ctypes.c_char_p(addr).value
. This will get the value as a bytes
object.
Note
Variadic functions are not supported by pyMHF. You may attempt to hook them with some success, but they will generally end up causing the program to crash.
Note
Python has issues with correctly type hinting ctypes pointers. The correct way to specify a pointer of some type is to use ctypes.POINTER(type)
, however static typing tools won’t accept this as a correct type even though this returns a type. To get around this the recommended way to type pointers is to use ctypes._Pointer[type]
, and include from __future__ import annotations
on the first line of your script.
Internally pyMHF does fix this issue so if this line isn’t included your code should still run.