Function hooking allows redirecting the execution flow of a program. In this article, we will be creating a 64-bit DLL that can be injected into a remote process to modify it’s behaviour.
In our example, we will just intercept and modify calls to MessageBoxA, but the same technique could be applied to any function.
Creating Our Hooking DLL
Our hooking DLL will be composed of multiple files. Firstly, dllmain.cpp. This just ensures our hooking code will be executed when the DLL is executed.
dllmain.cpp
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include "hooking.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
HookMe();
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Next, we need a header file to specify the functions we will be defining.
hooking.h
#include <Windows.h>
#include <stdint.h>
#include <stdio.h>
#include <memoryapi.h>
#include <iostream>
#include <iomanip>
#ifndef MYHEADER_H
#define MYHEADER_H
uint8_t* InstallHook(void* hooked_function, void* target_function);
int __stdcall MessageBoxB(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
void RemoveHook(void* hooked_function, uint8_t* backupBuffer);
int HookMe();
extern uint8_t* backupBuffer;
#endif
Finally we specify our hooking code in hooking.cpp. This code is just finding a pointer to our target function, and overwriting the function prologue with a set of assembly instructions that reroute execution to our code.
mov r10, <our_function>
jmp r10
hooking.cpp
#include "pch.h"
#include <Windows.h>
#include <stdint.h>
#include <stdio.h>
#include <memoryapi.h>
#include <iostream>
#include <iomanip>
uint8_t* backupBuffer;
void RemoveHook(void* hooked_function, uint8_t* backupBuffer)
{
std::cout << "Restoring memory\n";
memcpy(hooked_function, backupBuffer, 40);
}
int __stdcall MessageBoxB(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
// Make sure we don't call our own hook!
RemoveHook(MessageBoxA, backupBuffer);
MessageBoxA(0, "Hooked!", lpCaption, MB_ICONWARNING);
return true;
}
uint8_t* InstallHook(void* hooked_function, void* target_function)
{
DWORD oldProtect;
VirtualProtect(hooked_function, 1024, PAGE_EXECUTE_READWRITE, &oldProtect);
uint8_t jmp_instructions[] = { 0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0xFF, 0xE2 }; //mov r10, addr ; jmp r10 - 0x00 will be populated with our memory address
const uint64_t jmp_dest = (uint64_t)target_function;
printf("JMP Destination: %llx\n", jmp_dest);
memcpy(&jmp_instructions[2], &jmp_dest, sizeof(jmp_dest));
std::cout << "Patched instructions\n";
for (size_t i = 0; i < sizeof(jmp_instructions); ++i) {
std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(jmp_instructions[i]) << " ";
}
std::cout << std::endl;
// Backup the function
uint8_t* backupBuffer = new uint8_t[40];
memcpy(backupBuffer, hooked_function, 40);
std::cout << "Before patching\n";
for (size_t i = 0; i < sizeof(backupBuffer); ++i) {
std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(backupBuffer[i]) << " ";
}
std::cout << std::endl;
std::cout << "Patching memory\n";
memcpy(hooked_function, jmp_instructions, sizeof(jmp_instructions));
return backupBuffer;
}
int HookMe()
{
backupBuffer = InstallHook(MessageBoxA, MessageBoxB);
return 0;
}
Any time MessageBoxA is called in our target application, MessageBoxB from our code will be executed.
DLL Injection Code
With the DLL compiled, we can inject it into a remote process using our previously created DLL injection code;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
int main(int argc, char* argv[]) {
char sampleDLL[] = "C:\\SampleDLL.dll";
HANDLE process_handle;
//Get a handle to our remote process
process_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
// Allocate memory in the remote process
LPVOID buffer = VirtualAllocEx(process_handle, NULL, sizeof(sampleDLL), (MEM_RESERVE | MEM_COMMIT), PAGE_READWRITE);
// Write our DLL to the remote process
WriteProcessMemory(process_handle, buffer, sampleDLL, sizeof(sampleDLL), NULL);
//Retrieve the memory address of LoadLibraryA function
HMODULE k32_handle = GetModuleHandle(L"Kernel32");
VOID* load_library = GetProcAddress(k32_handle, "LoadLibraryA");
//Execute the DLL in a new remote thread
HANDLE remote_thread = CreateRemoteThread(process_handle, NULL, 0, (LPTHREAD_START_ROUTINE)load_library, buffer, 0, NULL);
CloseHandle(process_handle);
return 0;
}
Running the Code
To demonstrate what happening, we can use WinDBG to examine the remote processes memory before and after the InstallHook function has triggered.
0:000> u user32!MessageBoxA
USER32!MessageBoxA:
00007ffd`a5e59980 4883ec38 sub rsp,38h
00007ffd`a5e59984 4533db xor r11d,r11d
00007ffd`a5e59987 44391d62890300 cmp dword ptr [USER32!gfEMIEnable (00007ffd`a5e922f0)],r11d
00007ffd`a5e5998e 742e je USER32!MessageBoxA+0x3e (00007ffd`a5e599be)
00007ffd`a5e59990 65488b042530000000 mov rax,qword ptr gs:[30h]
00007ffd`a5e59999 4c8b5048 mov r10,qword ptr [rax+48h]
00007ffd`a5e5999d 33c0 xor eax,eax
00007ffd`a5e5999f f04c0fb115f8940300 lock cmpxchg qword ptr [USER32!gdwEMIThreadID (00007ffd`a5e92ea0)],r10
0:000> u user32!MessageBoxA
USER32!MessageBoxA:
00007ffd`a5e59980 49ba9a11e210f77f0000 mov r10,offset Hooking!ILT+405(?MessageBoxBYAHPEAUHWND__PEBD1IZ) (00007ff7`10e2119a)
00007ffd`a5e5998a 41ffe2 jmp r10
00007ffd`a5e5998d 00742e65 add byte ptr [rsi+rbp+65h],dh
00007ffd`a5e59991 488b042530000000 mov rax,qword ptr [30h]
00007ffd`a5e59999 4c8b5048 mov r10,qword ptr [rax+48h]
00007ffd`a5e5999d 33c0 xor eax,eax
00007ffd`a5e5999f f04c0fb115f8940300 lock cmpxchg qword ptr [USER32!gdwEMIThreadID (00007ffd`a5e92ea0)],r10
00007ffd`a5e599a8 4c8b15f9940300 mov r10,qword ptr [USER32!gpReturnAddr (00007ffd`a5e92ea8)]
We can see the original instructions have been replaced with a jump to our code. Next, let’s create a sample application, and see the results of our hooking.
#include <Windows.h>
#include <iostream>
int main()
{
for (;;) {
MessageBoxA(NULL, "Hello, World!", "Message", MB_OK | MB_ICONINFORMATION);
}
}
Running the application as expected displays a message box;
Next we inject our DLL into the target application, using it’s PID and can see the MessageBox call has been successfully intercepted.
tasklist | findstr /i targetapp
TargetApplication.exe 6852 Console 1 9,232 K
dll_injection.exe 6852
In Conclusion
The method of re-routing execution used in the article could be slightly improved. The assembly instructions used consume 13 bytes of memory, which may end up going past memory allocated for a function call stub. We could reduce the number of bytes used to 5 by first implementing a short jump to existing jump, although this process is fairly involved on x64 systems.