Patching binary code. Part 3

The previous part is here. In this part, we will investigate more exotic cases of intercepting function calls.

VMT for Case 1

VMT stands for virtual method table. VMT is created by the compiler when the class introduces a virtual method or overrides one from any parent class.

Finding VMT depends on the language and the compiler, so I will not explain it here. For most languages, the first 4 bytes in x86 or 8 bytes in x64 points to VMT. Then you need to know where is pointer to the function is in this table.

Then you need to save this address somewhere. Then write a different address into VMT using the WriteProcessMemory function. After that, any call to that particular function will call your function.

But you need to know that in certain cases compiler may optimize the call of that virtual function and call it directly and this way it will eliminate the call through VMT and as a result, the InterceptedXYZ function will not be called. Also for C++, there could be quite complex cases with multiple and virtual inheritances. I just give you an idea of what to look for.

To call the original XYZ function you need to declare it as a pointer to the function but you need to add a hidden this (or self) parameter to the appropriate place to make it work correctly.

Also, you need to keep in mind that you intercept calls to XYZ for all instances of that class and all inherited classes that do not override any functions. Many developers overlook this fact. And you probably already guessed this approach can work only for code you can control because if the class changes then your application will crash because most likely it will change the wrong function.

IAT for Windows API

As I wrote in previous parts, intercepting the Windows API function by overwriting the first bytes of that function is not a good idea in general. While it can work, there is usually a better idea that involves an import address table.

In Windows when any module needs to call any function from another DLL that module needs to add that function to its import table. When the compiler generates code, it typically never calls the import function directly because it simply does not know where it will be. Instead, it generates code that will use the address from the import address table.

When Windows loads the module, it will resolve all imports and place the correct address in the import table, and then call to that function will call the imported function. So typically code looks like this:

call        qword ptr [__imp_GetWindowWord (07FF66B761228h)]

So all we need to save the pointer from address 0x07FF66B761228 and then write a different address there to intercept any function there. To do this we need to scan the import address table, find the module and function, save it, and then replace the address with the address of our intercept function. I was playing with this in my personal project and as a result, I can publish code:

#include <iostream>
#include <windows.h>

static void GetImportDescriptor(HANDLE hProcess, LPVOID lpBaseAddress, PIMAGE_IMPORT_DESCRIPTOR& pImportDesc, DWORD& dwImportSize)
{
    pImportDesc = NULL;

    // Read the DOS header
    IMAGE_DOS_HEADER dosHeader;
    SIZE_T bytes;
    if (!ReadProcessMemory(hProcess, lpBaseAddress, &dosHeader, sizeof(dosHeader), NULL))
    {
        std::cerr << "Failed to read DOS header" << std::endl;
        return;
    }

    // Read the NT header
    IMAGE_NT_HEADERS ntHeaders;
    if (!ReadProcessMemory(hProcess, (LPBYTE)lpBaseAddress + dosHeader.e_lfanew, &ntHeaders, sizeof(ntHeaders), NULL) )
    {
        std::cerr << "Failed to read NT headers" << std::endl;
        return;
    }

    // Check if it's a valid PE file
    if (ntHeaders.Signature != IMAGE_NT_SIGNATURE)
    {
        std::cerr << "Invalid PE file" << std::endl;
        return;
    }

    // Get the RVA and size of the import table
    DWORD dwImportRva = ntHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    dwImportSize = ntHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size;

    if (dwImportRva == 0 || dwImportSize == 0)
    {
        std::cerr << "No import table found" << std::endl;
        return;
    }

    // Allocate memory to store the import table
    pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)VirtualAlloc(NULL, dwImportSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (pImportDesc == NULL)
    {
        std::cerr << "Failed to allocate memory for import table" << std::endl;
        return;
    }

    // Read the import table from the target process's memory
    if (!ReadProcessMemory(hProcess, (LPBYTE)lpBaseAddress + dwImportRva, pImportDesc, dwImportSize, NULL))
    {
        std::cerr << "Failed to read import table" << std::endl;
        VirtualFree(pImportDesc, 0, MEM_RELEASE);
        return;
    }
}

static LPVOID GetModuleExportedFunc(HANDLE hProcess, LPVOID lpBaseAddress, LPVOID newAddress, PIMAGE_IMPORT_DESCRIPTOR pImportDesc, const char* moduleName, const char* functionName)
{
    if (pImportDesc == NULL)
    {
        std::cerr << "Import descriptor is NULL" << std::endl;
        return NULL;
    }

    // Loop through the import descriptor until we find the specified module name
    while (pImportDesc->Name != 0)
    {
        // Get the module name from the target process's memory
        CHAR name[4096];
        if (!ReadProcessMemory(hProcess, (LPBYTE)lpBaseAddress + pImportDesc->Name, name, sizeof(name), NULL))
        {
            std::cerr << "Failed to read module name" << std::endl;
            return NULL;
        }

        // Compare module names
        if (_strnicmp(name, moduleName, sizeof(name) / sizeof(name[0])) == 0)
        {
            // Found the specified module
            PBYTE thunkAddr = (LPBYTE)lpBaseAddress + pImportDesc->OriginalFirstThunk;

            // Loop through the thunk data until we find the specified function name
            for (int index = 0; ; index++)
            {
                // Get the thunk data for this module
                IMAGE_THUNK_DATA thunk;
                if (!ReadProcessMemory(hProcess, thunkAddr, &thunk, sizeof(IMAGE_THUNK_DATA), NULL))
                {
                    std::cerr << "Failed to read thunk data" << std::endl;
                    return NULL;
                }

                if (thunk.u1.AddressOfData == 0) break; // We reached end of ILT

                // Check if this is an ordinal import
                if (IMAGE_SNAP_BY_ORDINAL(thunk.u1.Ordinal))
                {
                    // Ordinal import - skip
                    thunkAddr += sizeof(thunk);
                    continue;
                }

                // Get the import function name from the import address table
                IMAGE_IMPORT_BY_NAME importByName;
                if (!ReadProcessMemory(hProcess, (LPBYTE)lpBaseAddress + thunk.u1.AddressOfData + sizeof(importByName.Hint), name, sizeof(name), NULL))
                {
                    std::cerr << "Failed to read function name" << std::endl;
                    return NULL;
                }

                // Compare function names
                if (_stricmp(name, functionName) == 0)
                {
                    LPVOID* addr = (LPVOID*)((LPBYTE)lpBaseAddress + pImportDesc->FirstThunk + index * sizeof(IMAGE_THUNK_DATA));
                    LPVOID pFunctionAddress;
                    if (!ReadProcessMemory(hProcess, addr, &pFunctionAddress, sizeof(pFunctionAddress), NULL))
                    {
                        std::cerr << "Failed to read function address" << std::endl;
                        return NULL;
                    }

                    // Change page attributes
                    DWORD oldProtect;
                    if (!VirtualProtect(addr, sizeof(newAddress), PAGE_EXECUTE_READWRITE, &oldProtect))
                    {
                        std::cerr << "Failed to call VirtualProtect" << std::endl;
                        return NULL;
                    }

                    // Replace the function pointer
                    *addr = newAddress;

                    // Restore the original protection
                    if(!VirtualProtect(addr, sizeof(newAddress), oldProtect, &oldProtect))
                    {
                        std::cerr << "Failed to call VirtualProtect to restore" << std::endl;
                        return NULL;
                    }

                    return pFunctionAddress;
                }

                // Move to the next thunk
                thunkAddr += sizeof(thunk);
            }

            return NULL;
        }

        // Move to the next import descriptor
        ++pImportDesc;
    }

    // Module not found in the import descriptor
    return NULL;
}


typedef HMODULE(WINAPI* LoadLibraryWFunc)(LPCWSTR lpLibFileName);
LoadLibraryWFunc loadLibraryAddr;

static HMODULE WINAPI InterceptedLoadLibrary(LPCWSTR lpLibFileName)
{
    std::cerr << "Intercepted Load Library before" << std::endl;
    HMODULE result = (*loadLibraryAddr)(lpLibFileName);
    std::cerr << "Intercepted Load Library after" << std::endl;
    return result;
}

int main()
{
    HMODULE hModule1 = LoadLibraryW(L"kernel32.dll");

    HINSTANCE hInstance = GetModuleHandle(NULL);
    LPVOID baseAddr = (LPVOID)hInstance;

    PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
    DWORD dwImportSize;
    GetImportDescriptor(GetCurrentProcess(), baseAddr, pImportDesc, dwImportSize);

    if (pImportDesc == NULL)
    {
        std::cerr << "Failed to get import descriptor" << std::endl;
        return 1;
    }

    loadLibraryAddr = (LoadLibraryWFunc)GetModuleExportedFunc(GetCurrentProcess(), baseAddr, InterceptedLoadLibrary, pImportDesc, "kernel32.dll", "LoadLibraryW");
    if (loadLibraryAddr == NULL)
    {
        std::cerr << "Failed to get address of LoadLibrary" << std::endl;
        return 1;
    }

    HMODULE hModule2 = LoadLibraryW(L"kernel32.dll");
}

Remember that if we intercept any Windows API function that accepts a string as a parameter then you need to add W or A depending on what version of the function you want to intercept.

This way of intercepting Windows API is much safer and does not change Microsoft code. It is also thread-safe and in general, it is a much more robust way.

But if you import function by ordinal then you need to change this code.

Conclusion

Note: I used the WriteProcessMemory function but in some cases, you will also need to call the VirtualProtect function to give yourself a write permission. And remember to restore it later as I did in the code above.

As you can see there are many ways of intercepting a function in code. But remember that it must be only a last resort when you have no other way to fix a problem. Very often you can have strange problems like a wrong calling convention, some hidden parameters, etc. You need to know what are doing.

I hope it helps someone.