I’ve shown off a load of inline hooks in previous posts but I don’t think I’ve ever done any mid-function hooking on this blog before. Shocking, because there’s a ton of cool stuff you can do with it and, as luck would have it, I recently came across a library that makes both inline and mid-function hooking easier and safer than anything I’ve been using in the past.
SafetyHook is, as you’ve might’ve guessed already, a hooking library. With the most notable safe part being how it suspends any running threads when you create or delete your hooks. I must admit it’s been great being able to inject and unload stuff without worrying about the game crashing anymore. Really makes me question how I was dealing with that before.
It does a bunch of other stuff too, but rather than reiterate the documentation, let’s jump straight into some usage.
Here’s the music select scene from beatmania IIDX INFINITAS.
On the left you can see the song genre, title, and the artist. One of my projects features a custom chart loader, which allows you to switch out and play user-made variations. Often these would add extra notes… a lot of extra notes, so it seemed like a good idea to quickly show the note count difference to the original chart, along with the new total on the select screen.
I opted to replace the genre text for this, which in this instance is “HANDZ UP”.
I found the function responsible for drawing the text by simply scanning for the genre string, then setting a breakpoint on access. There was only one hit, and replacing the call with nop
bytes made the text disappear, confirming it as the correct target.
An inline hook isn’t really suitable here. Not only is the bm2dx.exe+1D3DA0
function called in a hundred other places, we only want to affect the genre text on the music select screen and leave all other instances untouched. It’s time for a mid-function hook.
SafetyMidHook genre_hook;
void install()
{
auto genre_hook_target = (bm2dx->scan("BA 07 00 00 00 44 88 74 24 20 E8 ? ? ? ? 33 D2 49 8B CD") + 10); // bm2dx.exe+C0997
auto factory = SafetyHookFactory::init();
auto builder = factory->acquire();
genre_hook = builder.create_mid(genre_hook_target, [] (safetyhook::Context& ctx)
{ spdlog::info("Hello from genre text mid-function hook."); });
}
All other threads will be suspended after the call to acquire
, so I’ve moved all the hook installing stuff into its own function.
You’ll notice that the lambda function doesn’t receive the arguments you’d usually get with an inline hook. While I’ve put this hook over a call
instruction, it really could’ve been placed anywhere, so that makes sense. Instead, we get a Context
object containing the values of each register before the call occurs. It’s not a const
reference, so we’re free to change any of the values as we see fit.
So yeah, our hook there works but it doesn’t do anything useful yet. We’ll need to figure out where the genre text is first.
Switching back to the debugger, I’ve now set a breakpoint where the mid-function hook is currently placed.
You’ll notice that it’s been changed from a call
into a jmp
, but more importantly, it looks like the rsi
register contains the address to the genre text… so it should be as easy as swapping out that address with one of our own, right?
Not quite. The text string is actually the 7th argument to the function, meaning it won’t be in a register, but on the stack instead. Since we know it’s already in rsi
, we just need to look back a few instructions to see where on the stack it got pushed to.
bm2dx.exe+C0980 - 48 89 74 24 30 - mov [rsp+30],rsi
If we jump to rsp
(0x7FF4B0
) in the memory viewer, we can confirm that the genre string appears +0x30
(48) bytes in.
The buffer for genre text is usually only 64 bytes in length, but we can go beyond that now.
genre_hook = builder.create_mid(genre_hook_target, [] (safetyhook::Context& ctx)
{
auto const replacement = "<color 70ff58ff>Check</color> "
"<color 58baffff>out</color> "
"<color ffb658ff>this</color> "
"<color ff5858ff>genre</color> "
"<color b658ffff>text!</color>";
*reinterpret_cast<char const**>(ctx.rsp + 0x30) = replacement;
});
So, that’s essentially how mid-function hooking can be done… but there’s a problem. The game doesn’t always use text for genres. There are a few characters in these strings that the in-game fonts can’t render properly. For such cases, a texture is used instead.
Luckily, the check for this occurs in the same function, so we just need to scroll up a bit and add another mid-function hook.
bm2dx.exe+C07C6 - 44 39 B0 08010000 - cmp [rax+00000108],r14d
bm2dx.exe+C07CD - 0F84 91000000 - je bm2dx.exe+C0864
rax
here is pointing to the song data structure. There’s a flag at +0x108
which determines whether to use a custom texture for the genre text or not. In this example, ‘Ristaccia’ uses it because of the “Ü” character, which is internally replaced with a “?”.
To override this, we just need to force ZF
after the cmp
instruction to always be set to 1
. Fortunately, Context
contains rflags
, and according to
Wikipedia, the 6th bit is the zero flag, so our second hook will look something like this:
auto genre_texture_hook_target = bm2dx->scan("0F 84 91 00 00 00 44 8B 88 70 02 00 00"); // bm2dx.exe+C07CD
genre_texture_hook = builder.create_mid(genre_texture_hook_target, [] (safetyhook::Context& ctx)
{ ctx.rflags |= 0x40; });
So far, so good. But what if we wanted to go one step further and replace not only the genre, but the title and artist text too?
Those other two are handled in the same function we’ve placed our existing two hooks in. Just like the genre text, they have the same texture check that will need bypassing, meaning it’ll require six mid-function hooks in total.
Rather than scan for six addresses, we can cut that down to just two and use a disassembler for the rest. Here’s what we need:
Address of the text rendering function our mid-function hook has been intercepting:
bm2dx.exe+1D3DA0
.Start address of the function we’ve been placing hooks in:
bm2dx.exe+C0740
.
We’ll disassemble the entire function, looking for any call
s to the text rendering function. When we find one, we simply place a hook on it. Of course, we’ll also need to hook the cmp
instructions for the texture checks too.
SafetyHook already includes a disassembler as a dependency, so just #include <bddisasm.h>
and we’re good to go.
We’ll start with a for loop at bm2dx.exe+C0740
, our “target” function. After every iteration of the loop, the instruction pointer will advance until we hit the C3
byte, which is the ret
instruction, indicating the end of the function.
auto const text_render_function = bm2dx->scan("48 8B C4 44 89 48 20 44 89 40 18 89 50 10 48 89 48 08 55"); // bm2dx.exe+1D3DA0
auto const mselect_target_function = bm2dx->scan("4C 8B DC 55 41 57 49 8D AB F8 FE FF FF"); // bm2dx.exe+C0740
INSTRUX ix;
for (auto ip = mselect_target_function; *ip != 0xC3; ip += ix.Length)
{
After we decode the next instruction, we first check if it’s a cmp
. There are a lot of these that aren’t relevant, so we’ll need to narrow things down a bit. In this case, only the texture checks had r14d
as the second operand, so we can check for that.
if (!ND_SUCCESS(NdDecode(&ix, reinterpret_cast<const std::uint8_t*>(ip), ND_CODE_64, ND_DATA_64)))
break;
if (ix.Instruction == ND_INS_CMP)
{
if (!ND_IS_OP_REG(&ix.Operands[1], ND_REG_GPR, ND_SIZE_32BIT, NDR_R14D))
continue;
// place the hook _after_ the cmp occurs
hooks.emplace_back(builder.create_mid(ip + ix.Length, [] (safetyhook::Context& ctx)
{ ctx.rflags |= 0x40; }));
}
Now for the calls. These are easier, as we only need to confirm that the call
address matches the text rendering function.
if (ix.Instruction == ND_INS_CALLNR)
{
// account for rip relative addressing
auto const address = (ip + std::int32_t(ix.RelativeOffset) + ix.Length);
if (address != text_render_function)
continue;
hooks.emplace_back(builder.create_mid(ip, [] (safetyhook::Context& ctx) -> void
{ *reinterpret_cast<char const**>(ctx.rsp + 0x30) = "Test text"; }));
}
}
}
Pretty easy, huh? While this does work, we don’t know whether we’re drawing genre, title, or artist text in our hook now. We’ll need to gather some more context so that we can differentiate the types of text in our hook function.
Referring back to the disassembly, let’s see where the strings being passed to the text rendering function are coming from.
As it turns out, the song data structure containing the strings, can always be found at r15+0x10
throughout the entire function, so as long as we have that address, and the address in rsp+0x30
, we can figure out which one is currently being rendered.
struct music_t
{
char const title[64]; // +0x000
char const title_ticker[64]; // +0x040 (unused)
char const genre[64]; // +0x080
char const artist[64]; // +0x0C0
std::int32_t use_texture_title; // +0x100
std::int32_t use_texture_artist; // +0x104
std::int32_t use_texture_genre; // +0x108
};
auto const text_address = *reinterpret_cast<std::uintptr_t*>(ctx.rsp + 0x30);
auto const song_address = *reinterpret_cast<std::uintptr_t*>(ctx.r15 + 0x10);
auto const is_title = (text_address - song_address == offsetof(music_t, title));
auto const is_genre = (text_address - song_address == offsetof(music_t, genre));
auto const is_artist = (text_address - song_address == offsetof(music_t, artist));
Before we finish off, here’s a quick tip that you might find useful when working with bddisasm. Since this was my first time using a disassembler library, I initially wasn’t sure what to check against when I was trying to filter out the irrelevant comparisons.
When in doubt, a combination of IDE breakpoints, inspecting structures, and the disasmtool
from any of the available releases can help you figure it out. Let’s use one of the instructions from earlier as an example:
44 39 B0 08010000 - cmp [rax+00000108],r14d
I only checked that the second operand was r14d
before, but we could be a bit more specific if we wanted to.
Copy the 7 opcode bytes to a separate file (let’s say input.bin
), then run the following:
disasmtool.exe -exi -b64 -f input.bin
0000000000000000 4439b008010000 CMP dword ptr [rax+0x108], r14d
DSIZE: 32, ASIZE: 64, VLEN: -
ISA Set: I86, Ins cat: ARITH, CET tracked: no
FLAGS access
CF: m, PF: m, AF: m, ZF: m, SF: m, OF: m,
Valid modes
R0: yes, R1: yes, R2: yes, R3: yes
Real: yes, V8086: yes, Prot: yes, Compat: yes, Long: yes
SMM on: yes, SMM off: yes, SGX on: yes, SGX off: yes, TSX on: yes, TSX off: yes
VMXRoot: yes, VMXNonRoot: yes, VMXRoot SEAM: yes, VMXNonRoot SEAM: yes, VMX off: yes
Valid prefixes
REP: no, REPcc: no, LOCK: no
HLE: no, XACQUIRE only: no, XRELEASE only: no
BND: no, BHINT: no, DNT: no
Operand: 0, Acc: R-, Type: Memory, Size: 4, RawSize: 4, Encoding: M,
Segment: 3, Base: 0, Displacement: 0x0000000000000108,
Operand: 1, Acc: R-, Type: Register, Size: 4, RawSize: 4, Encoding: R, RegType: General Purpose, RegSize: 4, RegId: 14, RegCount: 1
Operand: 2, Acc: -W, Type: Register, Size: 4, RawSize: 4, Encoding: S, RegType: Flags, RegSize: 4, RegId: 0, RegCount: 1
Since we know the texture flag offsets were always 0x100
, 0x104
, and 0x108
, we could add an additional check.
// Operand: 0, Acc: R-, Type: Memory, Size: 4, RawSize: 4, Encoding: M,
// Segment: 3, Base: 0, Displacement: 0x0000000000000108,
if (ix.Operands[0].Type != ND_OP_MEM || ix.Operands[0].Size != ND_SIZE_32BIT)
continue;
if (ix.Operands[0].Info.Memory.Disp < 0x100 || ix.Operands[0].Info.Memory.Disp > 0x108)
continue;
That about covers everything I wanted to talk about in this post. I used to write these mid-function hooks manually by hand, so you can imagine how happy I was when I discovered SafetyHook. I’ve previously always used MinHook for inline hooking before, but it looks like that won’t be the case for much longer. Big thanks to cursey over on GitHub for the excellent work!