RCE Endeavors 😅

May 25, 2023

Function Hooking: Export Address Table Hooks (7/7)

Filed under: Programming,Reverse Engineering — admin @ 11:44 PM

Table of Contents:

The export address table (EAT) is also another interesting candidate for installing function hooks. Similar to the import address table, the export address table contains tables with information about functions that a dynamic link library exports to callers. Specifically, there are three tables of interest: the export name table, export ordinal table, and export address table. When using the GetProcAddress function, these tables are traversed to find the appropriate address of the desired function. Hooking a function involves overwriting an entry in the export address table with the offset to your hook function. After doing this, calls to GetProcAddress will return your hook function instead of the original.

Getting the export directory

To begin getting access to the export address table, you must first get a pointer to the export directory.

IMAGE_EXPORT_DIRECTORY* GetExportDirectory(void* const moduleBaseAddress) {

    ULONG size{};
    auto* exportDirectoryAddress{ ImageDirectoryEntryToData(
        moduleBaseAddress, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &size) };

    return reinterpret_cast<IMAGE_EXPORT_DIRECTORY*>(
        exportDirectoryAddress);
}

Getting the export directory.

Getting the export directory is straightforward: the ImageDirectoryEntryToData function can be used, passing in IMAGE_DIRECTORY_ENTRY_EXPORT as the desired directory entry.

Traversing the export tables

After obtaining the directory entry, the three tables will be present. These three tables can be indexed into in parallel while searching for a target function to hook.

DWORD* GetEATEntryByName(void* const moduleBaseAddress,
    const std::string& targetFunctionName) {

    auto* exportDirectory {
        GetExportDirectory(moduleBaseAddress) };
    if (exportDirectory == nullptr) {
        std::cerr << "Could not get base address of imports directory"
            << std::endl;
        return nullptr;
    }

    auto* addressOffsets{ RvaToPointer<DWORD>(moduleBaseAddress,
        exportDirectory->AddressOfFunctions) };
    const auto* nameOffsets{ RvaToPointer<DWORD>(moduleBaseAddress,
        exportDirectory->AddressOfNames) };
    const auto* ordinalOffsets{ RvaToPointer<WORD>(moduleBaseAddress,
        exportDirectory->AddressOfNameOrdinals) };

    for (size_t index{}; index < exportDirectory->NumberOfFunctions; index++) {

        const auto exportedFunctionName{ std::string {
            RvaToPointer<char>(moduleBaseAddress,  nameOffsets[index]) } };

        if (targetFunctionName == exportedFunctionName) {
            return &addressOffsets[ordinalOffsets[index]];
        }
    }

    return nullptr;
}

Traversing the export directory tables to look for a desired function.

The code above references three tables in the export directory: addressOffsets, which holds the base of the export address table, nameOffsets, which holds the base of the export name table, and ordinalOffsets, which holds the base of the export ordinal table. The code then loops through each exported function and checks to see if the function name matches the target function name to hook. If so, then the export table entry of the function is returned to the caller. If no export matches the target function name, then a nullptr is returned.

Installing the hook

Hooking the export address table is a little more complicated than the import address table. The import address table held absolute addresses that were resolved at runtime, so they could be overwritten directly with the address of the hook function. The export address table, however, contains relative virtual addresses to the module load address. This presents a complication: the relative virtual offset has a DWORD type, but in a 64-bit process, you have a much larger address range. It is possible for your hook function to be greater than 4 GB away from the module load address, meaning that the relative address would not fit in the DWORD type. To overcome this, you can allocate memory near the module and write in an absolute jump to your hook function. The AllocateClosestAfterAddress function shown below allocates memory close to the provided module.

void* AllocateClosestAfterAddress(void* const moduleAddress, const size_t size) {

    MODULEINFO moduleInfo{};
    auto result{ GetModuleInformation(GetCurrentProcess(), 
        static_cast<HMODULE>(moduleAddress), &moduleInfo, sizeof(MODULEINFO)) };
    if (!result) {
        PrintErrorAndExit("GetModuleInformation");
    }

    auto allocAddress{ reinterpret_cast<DWORD_PTR>(
        moduleInfo.lpBaseOfDll) + moduleInfo.SizeOfImage };

    void* allocatedAddress{};
    constexpr size_t ALLOC_ALIGNMENT = 0x10000;
    do {
        allocatedAddress = VirtualAlloc(reinterpret_cast<void*>(allocAddress),
            size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        allocAddress += ALLOC_ALIGNMENT;
    } while (allocatedAddress == nullptr);

    return allocatedAddress;
}

The AllocateClosestAfterAddress will allocate memory close to a target module.

The AllocateClosestAfterAddress function uses the GetModuleInformation function to get the memory bounds of the provided module. After the bounds are found, the function tries to allocate memory immediately past the end of the module’s address range. Once an address is able to be allocated, it is returned to the caller.

With this problem resolved, you can now begin the process to overwrite the export address table entry. The InstallEATHook below function performs the full hooking logic.

template <typename OriginalFunctionPtr>
void InstallEATHook(const std::string& targetModuleName,
    const std::string& targetFunctionName, void* const hookAddress,
    OriginalFunctionPtr& originalFunction) {

    auto* moduleBaseAddress{ GetModuleHandleA(targetModuleName.c_str()) };
    if (moduleBaseAddress == nullptr) {
        moduleBaseAddress = LoadLibraryA(targetModuleName.c_str());
    }

    auto* const eatEntryRva{ GetEATEntryByName(moduleBaseAddress, targetFunctionName) };
    if (eatEntryRva == nullptr) {
        std::cerr << std::format("Export address table entry "
            "for {}:{} not found", targetModuleName, targetFunctionName)
            << std::endl;
        return;
    }

    originalFunction = reinterpret_cast<OriginalFunctionPtr>(
        RvaToPointer<void>(moduleBaseAddress, *eatEntryRva));

    const auto jumpBytes{ CreateJumpBytes(hookAddress) };
    auto* const jumpStub{ AllocateClosestAfterAddress(
        moduleBaseAddress, jumpBytes.size()) };
    if (jumpStub == nullptr) {
        PrintErrorAndExit("VirtualAlloc");
    }

    std::memcpy(jumpStub, jumpBytes.data(), jumpBytes.size());

    const auto oldProtections{ ChangeMemoryPermissions(
        eatEntryRva, sizeof(void*), PAGE_EXECUTE_READWRITE) };
    *eatEntryRva = static_cast<DWORD>(PointerToRva(jumpStub, moduleBaseAddress));
    ChangeMemoryPermissions(eatEntryRva, sizeof(void*), oldProtections);
}

Overwriting an export address table entry with an offset to the hook function.

The InstallEATHook function begins by finding the export address table entry. If an entry is found, then the address of the original function is saved and a block of memory is allocated close to the target module. The jump stub is written in to this allocated block of memory. Lastly, the export address table entry’s relative virtual address is then written in with the relative virtual address of the jump stub address. At this point, the hook is installed and ready to be tested out.

As before, the hook function will change a parameter and forward the call to the original function

using MessageBoxAPtr = int(__stdcall*)(HWND, LPCSTR, LPCSTR, UINT);
static MessageBoxAPtr OriginalMessageBoxA{};

int HookMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {

    return OriginalMessageBoxA(hWnd, "Hooked Hello World!", lpCaption, uType);
}

The hook function that will be called instead of MessageBoxA.

Verifying the hook

With the hook function defined, the export address table hook can be tested out.

int main(int argc, char* argv[]) {

    MessageBoxAPtr UnusedMessageBoxAOriginalFncPtr =
        reinterpret_cast<MessageBoxAPtr>(
            GetProcAddress(GetModuleHandleA("user32.dll"),
                "MessageBoxA"));

    InstallEATHook("user32.dll", "MessageBoxA",
        HookMessageBoxA, OriginalMessageBoxA);

    MessageBoxAPtr MessageBoxAFnc =
        reinterpret_cast<MessageBoxAPtr>(
            GetProcAddress(GetModuleHandleA("user32.dll"),
                "MessageBoxA"));

    if (MessageBoxAFnc == nullptr) {
        std::cerr << "Could not find MessageBoxA export"
            << std::endl;
        return -1;
    }

    MessageBoxAFnc(nullptr, "Hello World!", nullptr, 0);

    return 0;
}

The program that installs an export address hook on the MessageBoxA function.

This program will begin by installing the export address table hook. After installing the hook, the call to GetProcAddress will resolve a pointer to the MessageBoxA function. However, since the address was overwritten, the pointer that will be returned will be to HookMessageBoxA. Now, when the caller uses the MessageBoxAFnc pointer, they will instead be calling HookMessageBoxA, which will change “Hello World!” to “Hooked Hello World!” and call the original MessageBoxA function.

Running the demo

The ExportAddressTableHook project provides the full implementation that was presented in this section. To see the export address table hook, set breakpoints on the lines following the assignment of the MessageBoxAPtr function pointers as shown below.

Breakpoints set after assignment of the function pointers.

Launch the application and take note of the address stored in UnusedMessageBoxAOriginalFncPtr. Before the hook was installed, GetProcAddress resolved the address to MessageBoxA, as expected. You can verify this on your own by hovering your cursor over the UnusedMessageBoxAOriginalFncPtr variable and seeing the address and function that it points to.

The UnusedMessageBoxAOriginalFncPtr function pointer correctly resolves to MessageBoxA.

Continue execution and let the second breakpoint get hit. At this point, the export address table hook has been installed. Hover your mouse over the MessageBoxAFnc function pointer and notice that it has a different address.

The MessageBoxAFnc function pointer has a different address.

This time, GetProcAddress returned a different address for the MessageBoxA function. This is because GetProcAddress iterated over the exports again, but the export address for MessageBoxA was overwritten by the InstallEATHook function. If you look at this address in the Disassembly window, you will see that it points to the stub that jumps to the HookMessageBoxA function.

The generated stub that jumps to HookMessageBoxA.

This shows that the export address table hook was successful. After the hook is installed, all calls to GetProcAddress for MessageBoxA will result in the HookMessageBoxA function being called instead.

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment

 

Powered by WordPress