Table of Contents:
- Inline Hooks (1/7)
- Trampolines & Detours (2/7)
- Hardware Breakpoints (3/7)
- Software Breakpoints (4/7)
- Virtual Table Hooks (5/7)
- Import Address Table Hooks (6/7)
- Export Address Table Hooks (7/7)
Function hooking is a technique that lets you intercept function calls and redirect them to another location. The true power of function hooking is the ability to take full control over the program’s runtime behavior. With function hooking, you define a hook function that carries out your desired logic; for example, adding functionality to log function calls, inspecting or overwriting function parameters, changing return values, or redefining functions in their entirety. After defining the hook function, you change the program to redirect control flow to it. There are many techniques to do this, which this series of posts will cover.
The best way to really understand function hooking is to try it out for yourself. Below is a demo program that will be used as the demonstration for the next several function hooking techniques.
// Disable warning for usage of ctime
#define _CRT_SECURE_NO_WARNINGS
#include <chrono>
#include <ctime>
#include <format>
#include <iostream>
#include <string>
#include <thread>
void DisplayMessageOnInterval(const std::string& message) {
const auto currentTime{ std::chrono::system_clock::to_time_t(
std::chrono::system_clock::now()) };
std::cout << std::format("{} @ {}",
message, std::ctime(¤tTime)) << std::endl;
}
int main(int argc, char* argv[]) {
while (true) {
DisplayMessageOnInterval("Hello World!");
std::this_thread::sleep_for(std::chrono::seconds(5));
}
return 0;
}
The program is rather simple; there is a DisplayMessageOnInterval function that takes in a message to print out. This function gets called in a loop where it will print out the message and the current time, then execution will pause for five seconds before looping.
Inline Hooks
Inline hooks are a type of function hook that involve changing a program’s control flow by directly patching its instruction bytes. When a function gets called, instead of executing the expected prologue instructions, it will execute an unconditional jump that was patched in at runtime. This unconditional jump will redirect control flow over to a function that you have written – the hook function. From this hook function, you have full control: you can change the values of the arguments and call the original function, you can call the original function and modify the return value, or you can choose to not call the original function at all and execute something entirely different!
Inspecting the target function
To see the assembly instructions of the DisplayMessageOnInterval function, set a breakpoint on the function by highlighting the line with the function’s name and pressing F9 in Visual Studio. After setting the breakpoint, execute the program with the debugger attached by pressing F5. Your breakpoint should be hit almost immediately after the program begins execution. At this point, the debugger is in a broken state and you can view the assembly instructions. In Visual Studio, navigate to the Disassembly window (select Debug -> Windows -> Disassembly from the menu bar at the top). This will bring you to the disassembly listing, which will show a mapping of the source code to the runtime addresses and assembly instructions (see the Running the demo below section for screenshots). The instructions that are displayed in that window should be similar to the ones shown below, though with different runtime addresses.
Runtime Address | Instruction Bytes | Instruction |
---|---|---|
0x00007FF7FEF7A8A0 | 48 89 4C 24 08 | mov qword ptr [rsp+0x8], rcx |
0x00007FF7FEF7A8A5 | 55 | push rbp |
0x00007FF7FEF7A8A6 | 56 | push rsi |
0x00007FF7FEF7A8A7 | 57 | push rdi |
0x00007FF7FEF7A8A8 | 48 81 EC 10 02 00 00 | sub rsp, 0x210 |
0x00007FF7FEF7A8AF | 48 8D 6C 24 20 | lea rbp, [rsp+0x20] |
… | … | … |
In the Disassembly window, you can perform single stepping of the instructions by pressing F10 to Step Into and F11 to Step Over an instruction. From here you can observe the state of the program as the function executes, starting at the prologue, moving to the actual function logic, and wrapping up with the epilogue instructions.
Creating the jump stub
Installing an inline hook is just a matter of overwriting these instructions with an unconditional jump to your hook function. Setting the hook can be accomplished by writing in the instructions below at the start of the function, though the specific use of unconditional jumps is just one technique of many.
Instruction Bytes | Instruction |
---|---|
48 B8 CC CC CC CC CC CC CC CC | mov rax, 0xCCCCCCCC |
FF E0 | jmp rax |
Here the value address at 0xCCCCCCCC is a placeholder and will be overwritten with the runtime address of the hook function. You can define a function to generate these instructions and substitute in the desired address, as is shown below:
std::array<unsigned char, 12> CreateJumpBytes(const void* const destinationAddress) {
std::array<unsigned char, 12> jumpBytes{ {
/*mov rax, 0xCCCCCCCCCCCCCCCC*/
0x48, 0xB8, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
/*jmp rax*/
0xFF, 0xE0
} };
// Replace placeholder value with the actual hook address
const auto address{ reinterpret_cast<size_t>(destinationAddress) };
std::memcpy(&jumpBytes[2], &address, sizeof(void *));
return jumpBytes;
}
The CreateJumpBytes function generates the unconditional jump stub and writes in the hook address over the placeholder address.
Installing the hook
After building this stub, you can write it in place of the original instructions. The code for doing this is shown below. One important thing to note is that the memory page protections of the target address must be changed to PAGE_EXECUTE_READWRITE to allow the new instructions to be written in. Failure to do so will result in an access violation at runtime.
void InstallInlineHook(void* const targetAddress, const void* const hookAddress) {
const auto hookBytes{ CreateJumpBytes(hookAddress) };
const auto oldProtections{ ChangeMemoryPermissions(
targetAddress, hookBytes.size(), PAGE_EXECUTE_READWRITE) };
std::memcpy(targetAddress, hookBytes.data(), hookBytes.size());
ChangeMemoryPermissions(targetAddress, hookBytes.size(), oldProtections);
FlushInstructionCache(GetCurrentProcess(), nullptr, 0);
}
The InstallInlineHook function overwrites the instructions at the target address with the generated unconditional jump stub.
At this point, all that is left is to write the hook function. To keep it simple, the hook function, defined as HookDisplayMessageOnInterval, does nothing more than print out a message to the console.
void HookDisplayMessageOnInterval(const std::string& message) {
std::cout << "HookDisplayMessageOnInterval function called!"
<< std::endl;
}
The definition of the hook function that will be called.
After executing the InstallInlineHook function, with DisplayMessageOnInterval as the target address and HookDisplayMessageOnInterval as the hook address, the assembly instructions at the start of DisplayMessageOnInterval will look similar to those in the table below, although with different runtime addresses.
Runtime Address | Instruction Bytes | Instruction |
---|---|---|
0x00007FF6F0BEA8A0 | 48 B8 00 AA BE F0 F6 7F 00 00 | mov rax, 0x7FF7FEF7AA00 |
0x00007FF6F0BEA8AA | FF E0 | jmp rax |
0x00007FF6F0BEA8AC | 02 00 | add al, byte ptr [rax] |
0x00007FF6F0BEA8AE | 00 48 8D | add byte ptr [rax-0x73], cl |
0x00007FF6F0BEA8B1 | 6C | ins byte ptr [rdi], dx |
0x00007FF6F0BEA8B2 | 24 20 | and al, 0x20 |
… | … | … |
Verifying the hook
Now when the program runs, the message should get printed out to the console from the hook function instead of the original. At this point, control flow of the original function has changed to the defined hook function, and as shown, any code that is present in the hook function will get executed. However, since the original instructions were overwritten, control cannot get passed back to the original function. To resolve this, the original instructions must be saved and relocated elsewhere in the program’s address space. This technique is called creating a trampoline function and is covered in the next post.
Running the demo
The InlineHook project provides the full implementation that was presented in this section. The best way to see what is happening is to set a breakpoint on the line that installs the hook, and the line immediately afterward as shown below.
After setting the breakpoints, launch the executable in Visual Studio. The breakpoint should be hit immediately. Navigate to the Disassembly window and type in DisplayMessageOnInterval in the Address text box. After doing this, you should see the assembly instructions for the function.
Continue execution so that the next breakpoint gets hit. At this point, the hook has been installed and you can navigate back to the Disassembly window. When looking at DisplayMessageOnInterval in this window again, you should see different instructions that show the hook address being moved into the RAX register, followed by an unconditional jump to RAX.
Now the inline hook is in place and any call to DisplayMessageOnInterval will instead be re-routed to HookDisplayMessageOnInterval, as you can see for yourself by continuing execution of the program.
Thank You For Help.. very detailed documents. good for beginner.
Comment by Htoo — June 23, 2023 @ 5:51 AM