Manually Implementing Inline Function Hooking

This post covers the general process of implementing a simple inline function hook for an x86 Win32 API.

Introduction

Malware and EDR agents commonly hook Win32 API functions so that they can alter and/or monitor calls made to these APIs. In the case of malware, it may be beneficial for the attacker to hook functions that handle credential information (or other sensitive information) so that they can extract credentials entered by a user. Equally, EDRs will often hook APIs that are commonly abused by attackers (an example API would be VirtualAllocEx) so that they gain visibility into when a process uses the API, and what content is passed to the API with the goal of being able to detect malicious activity.

Although there are more stable and standardised methods of implementing function hooking (such as Microsoft Detours) it's still a valuable learning experience to look at how we can implement a hook without relying on libraries that do a lot of the heavy lifting. This blog post will cover the process of implementing an inline function hook for the MessageBoxA function.

This blog post only covers x86 inline hooks, the process for hooking x64 functions requires a different approach

What is an Inline Function Hook?

Normal Operation Flow (No Hook)

Hooked Operation Flow

As you can see from the images above, an inline function is the process of redirecting the execution flow away from the legitimate function that was called, executing some "hook" code, and then passing execution back to the legitimate function so that execution continues as normal. The interesting stuff happens in the "hook" code, where an attacker can change the information being sent to the function, or an EDR agent can inspect and log the data being passed to the function.

Specifically with an inline function hook, you can see from the image above that the high level process for implementing an inline function hook is:

  • The first n bytes of the target function (the function we want to hook) are copied to a memory location often referred to as a "trampoline".

  • n bytes of the target function are overwritten by a relative JMP command which will pass execution to our user defined code.

  • After executing our "hook" code, execution needs to return back to the original function. To do this we pass execution to our trampoline which contains the bytes that we overwrote in the original function.

  • After executing the overwritten bytes, a relative JMP takes the execution back to the original function at an offset after our added JMP command to prevent recursion.

Programming The Function Hook

Step 1 - Build The Trampoline

The first step in the process is to set up our trampoline. This is a section of memory that will store a copy of the bytes that overwrite in the MessageBoxA function and will also contain the JMP back to an offset in the original MessageBoxA function:

// Get the address of the MessageBoxA function
origFunctionAddress = (BYTE *)GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");
​
// Allocate some memory to store our trampoline
trampolineAddress = (BYTE*)VirtualAlloc(NULL, 20, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (trampolineAddress == NULL) {
return Error("Failed to allocate memory for trampoline");
}
​
int numOfBytesToCopy = 5;
char trampoline[10] = {};
​
// Copy bytes from the original MessageBox function to our trampoline
memcpy_s(trampoline, numOfBytesToCopy, origFunctionAddress,5);
​
// At the the end of the copied bytes we want to JMP back to the original function
// 0xE9 is the JMP opcode.
// It needs a 4 byte relative address to get back to the original function
*(DWORD*)(trampoline + numOfBytesToCopy) = 0xE9;
​
// Calculate how far we need to jump to get back to the original function (+ offset)
uintptr_t jumpAddress = (BYTE*)origFunctionAddress - trampolineAddress - numOfBytesToCopy;
​
// Write the relative address to our trampoline to complete the JMP statement
*(uintptr_t*)((uintptr_t)trampoline + numOfBytesToCopy + 1) = jumpAddress;
​
// Write the trampoline to the allocated trampoline memory region
if (!WriteProcessMemory(GetCurrentProcess(), trampolineAddress, trampoline, sizeof(trampoline), NULL)) {
return Error("Error while writing process memory to trampoline");
}

A high level overview of the code snippet above does the following:

  1. Gets the address of the MessageBoxA function from the user32.dll DLL.

  2. Allocates some memory using VirtualAlloc to store our trampoline code.

  3. Uses memcpy_s to copy 5 bytes from the start of the MessageBoxA function to the start of the trampoline.

  4. At the end of the trampoline (which is now filled with the copied bytes), add a JMP opcode (0xE9) which will take a relative address to JMP to the original MessageBoxA function + an offset.

  5. Calculates the size of the JMP that needs to be made to go from the trampoline memory region, to the MessageBoxA function + offset. This is a relative JMP so to get the size we can just subtract the trampoline memory address from the address of the messageBoxA function.

Why do we copy 5 bytes?

When we add our hook code to the original MessageBoxA function in the next step, our hook will be in the form of a JMP (0xE9) opcode (1 byte) and a relative address (4 bytes) which will take us to our area of user-defined code. This means that we will overwrite a total of 5 bytes at the start of the MessageBoxA function.

Inspecting the 5 bytes at the start of the MessageBoxA function shows us that this forms 3 complete operations:

  1. mov edi,edi

  2. push ebp

  3. move ebp,esp

Disassembly of the start of the MessageBoxA function

This means that we are safe to overwrite exactly 5 bytes with out JMP command in the case of MessageBoxA , but if we were to target other functions we may have to pad out our overwrite with Nops (0x90) to make sure the we overwrite complete instructions.

Step 2 - Add Our Hook Code

Now that we've created our trampoline to get back to the original MessageBoxA function we can add our "hook" code that we want to execute when the MessageBoxA function is called. This is where we can do our evil activity if we wanted to:

// Set up a function to call that will pass execution to our trampoline code
// to ultimately pass execution back to the read MessageBoxA code
typedef int(__stdcall *tdOrigMessageBoxA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
tdOrigMessageBoxA messageBoxATrampoline;
​
// This is our function hook
// This is what we will execute before passing execution back to the real function
​
int __stdcall HookedMessageBox(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
// Overwrite the text in the messagebox with our message
lpText = "Hooked";
​
// Pass execution to our trampoline which will ultimately return back to the original function
return messageBoxATrampoline(hWnd,lpText,lpCaption,uType);
}

The code above is a fairly simple example of how to handle the function that you want to hook. Lines 3 & 4 create a simple typedef with the same return type (int), calling convention (stdcall) and parameters as the original MessageBoxA function. This function - messageBoxATrampoline - will just point to the memory location of our trampoline, this allows us to "return" to this address after we run our hooked code (as seen on line 15).

Lines 9 - 16 are our "hook" code, this is the code that will be executed before the real MessageBoxA function is executed. The example here is fairly boring, we just change the text that will be displayed in the message box to "Hooked".

On line 15 we "return" to the messageBoxATrampoline function which is our trampoline. If you remember from the previous step, the trampoline contains the 5 bytes that we copied from the original MessageBoxA function, and a relative JMP to the original MessageBoxA function.

Step 3 - Hook The Original Function

The next step is to patch the original MessageBoxA function so that execution is redirected to our 'hook' code in the HookedMessageBox function. This is relatively straight forward to do, we just overwrite the first 5 bytes of the MessageBoxA function with a relative JMP to the HookedMessageBox function:

// Change memory protection on messageBox code to make sure it's writable
DWORD oldProtectVal;
VirtualProtect(origFunctionAddress,6,PAGE_READWRITE,&oldProtectVal);
​
// Patch the original MessageBox code
// First we replace the first BYTE with a JMP instruction
*(BYTE *)origFunctionAddress = 0xE9;
​
// Then we calculate the relative address to JMP to our Hook function
intptr_t hookAddress = (intptr_t)((CHAR*)HookedMessageBox - (intptr_t)origFunctionAddress) - 5;
​
// Write the relative address to the original MessageBoxA function
*(intptr_t*)((intptr_t)origFunctionAddress + 1) = hookAddress;
​
// Restore original memory protection on messageBox code
VirtualProtect(origFunctionAddress,6,oldProtectVal,&oldProtectVal);

Step 4 - Map The Trampoline To A Function

Now that we have most of the pieces of the puzzle together, the final step is to map our trampoline code to the messageBoxATrampoline function:

// Cast the trampoline address to a function
messageBoxATrampoline = (tdOrigMessageBoxA)trampolineAddress;

Step 5 - Putting It All Together

At this point our hook should be fully working except for some other bits of generic code like includes, error handling and general structure. Adding these in and combining the steps above gives us the code shown below.

#include <iostream>
#include <Windows.h>
​
// Set up a function to call that will pass execution to our trampoline code
// to ultimately pass execution back to the read MessageBoxA code
typedef int(__stdcall *tdOrigMessageBoxA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
tdOrigMessageBoxA messageBoxATrampoline;
​
// This is our function hook
// This is what we will execute before passing execution back to the real function
int __stdcall HookedMessageBox(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
​
// Overwrite the text in the messagebox with our message
lpText = "Hooked";
​
// Pass execution to our trampoline which will ultimately return back to the original function
return messageBoxATrampoline(hWnd,lpText,lpCaption,uType);
}
​
int Error(const char* msg) {
printf("%s (%u)", msg, GetLastError());
return 1;
}
​
int main()
{
BYTE* origFunctionAddress = NULL;
BYTE* trampolineAddress = NULL;
​
// Call MessageBoxA before hooking to show original functionality
MessageBoxA(NULL, "hi", "hi", MB_OK);
​
origFunctionAddress = (BYTE *)GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");
​
// Allocate some memory to store the start of the original function
trampolineAddress = (BYTE*)VirtualAlloc(NULL, 20, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (trampolineAddress == NULL) {
Error("Failed to allocate memory for trampoline");
}
​
int numOfBytesToCopy = 5;
char trampoline[10] = {};
// Copy bytes from the original MessageBox function to our trampoline
memcpy_s(trampoline, numOfBytesToCopy, origFunctionAddress,5);
// The the end of the copied bytes we want to JMP back to the original hooked function
// 0xE9 is the JMP opcode here. It needs to be given a 4 bytes address
*(DWORD*)(trampoline + numOfBytesToCopy) = 0xE9;
// Calculate where we want to jump back to in the original hooked fuction
uintptr_t jumpAddress = (BYTE*)origFunctionAddress - trampolineAddress - numOfBytesToCopy;
// Write the JMP address to our trampoline
*(uintptr_t*)((uintptr_t)trampoline + numOfBytesToCopy + 1) = jumpAddress;
​
// Write the trampoline to the allocated trampoline memory region
if (!WriteProcessMemory(GetCurrentProcess(), trampolineAddress, trampoline, sizeof(trampoline), NULL)) {
return Error("Error while writing process memory to trampoline");
}
​
// Change memory protection on messageBox code to make sure it's writable
DWORD oldProtectVal;
VirtualProtect(origFunctionAddress,6,PAGE_READWRITE,&oldProtectVal);
​
// Patch the original MessageBox code
// First we replace the first BYTE with a JMP instruction
*(BYTE *)origFunctionAddress = 0xE9;
// Then we calculate the relative address to JMP to our Hook function
intptr_t hookAddress = (intptr_t)((CHAR*)HookedMessageBox - (intptr_t)origFunctionAddress) - 5;
// Write the relative address to the original MessageBoxA function
*(intptr_t*)((intptr_t)origFunctionAddress + 1) = hookAddress;
​
// Restore original memory protection on messageBox code
VirtualProtect(origFunctionAddress,6,oldProtectVal,&oldProtectVal);
​
// Cast the trampoline address to a function
messageBoxATrampoline = (tdOrigMessageBoxA)trampolineAddress;
// The hook should now be complete
// Call message box again to test the hook
MessageBoxA(NULL, "hi", "hi", MB_OK);
​
return 0;
}

Note that we call the MessageBoxA function twice, first before we add the hook to show how it should work and then again after we've added the hook.

Demo

References