Inline Function Hooking

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.