Import Address Table Hooking

Import Address Table hooking is a method to redirect execution of a function.

I’ve previously covered inline hooking, so be sure to read that as well.

PE format

Portable Executable is the file format used on Windows for executables, libraries, and certainly more things I can’t remember right now.

It’s an SVG, so open it in a new page and zoom in if needed.

This format contains all the information Windows needs to successfully load an image (an executable) into memory.

Import Address Table

On Windows, our software can make use of functions that are part of a shared library (dynamic link library).

The advantage of dynamic libraries is that they can be loaded into memory only once, saving memory.

However, when mapping these libraries onto a process, the same address can not always be used. This is where the Import Address Table comes in.

You can see the IAT on the image above close to the bottom left.

The IAT is a lookup table, and when loading an image into memory, the loader will fill in the IAT with the correct addresses.

Therefore, instead of the program using addresses to call a function directly, it will call function printf on the IAT and it will perform an indirect call.

Describing the the process of import resolution in great detail is not my intention here, but please read the following article to learn more http://sandsprite.com/CodeStuff/Understanding_imports.html.

If we overwrite the function pointers on the IAT with a pointer to our own function, we can redirect execution whenever the original function is called.

Viewing imports

Here is some example code.

1
2
3
4
5
6
7
8
#include <Windows.h>

void a() {}

int main()
{
    MessageBox(nullptr, L"Hello, world", L"Hello, world", MB_OK);
}

Yes, I am aware of how creative I am.

Visualizing the imports on PE-bear, I can see MessageBoxW is imported from USER32.dll.

You can also notice that libraries from dynamic libraries and functions that parte part of the program are called differently.

_main   PROC
        push    ebp
        mov     ebp, esp
        push    0
        push    OFFSET $SG89621
        push    OFFSET $SG89622
        push    0
        call    DWORD PTR [email protected]
        call    void a(void)                         ; a
        xor     eax, eax
        pop     ebp
        ret     0
_main   ENDP

Hooking

We will hook a process IAT by injecting a DLL onto it. After injecting, we will parse the PE header to find our target function in the IAT, and then overwrite the address with our own function.

We need a way to retrieve the original function, so let’s save it before we do any hooking and then have our new function call the old one with modified parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
typedef int(__stdcall *MessageBoxFuncPtr)(
    HWND    hWnd,
    LPCTSTR lpText,
    LPCTSTR lpCaption,
    UINT    uType
);

MessageBoxFuncPtr MessageBoxOriginal = MessageBoxW;

int __stdcall MessageBoxNew(HWND    hWnd,
    LPCTSTR lpText,
    LPCTSTR lpCaption,
    UINT    uType
)
{
    return MessageBoxOriginal(hWnd, L"Hooked", L"Hooked", uType);
}

Parsing the PE is not the easiest thing in the world since it’s a complex format, but it can be done with a little bit of patience.

The best way to learn is to look at the above picture while either reading the code or looking through it manually on an hex editor. We will need the base address of the module before anything, which can be obtained using GetModuleHandle.

At the base address we have the DOS header, and we can find the PE header by following e_lfanew.

1
2
auto dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
auto peHeader = (PIMAGE_NT_HEADERS)(baseAddress + dosHeader->e_lfanew);

Next we need the optional header and the Import directory:

1
2
auto optionalHeader = peHeader->OptionalHeader;
auto importDirectory = optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];

We get the address of the import descriptor by adding the RVA to the base address:

1
2
auto descriptorRVA = importDirectory.VirtualAddress;
auto importDescriptor = baseAddress + descriptorRVA;

Next we will iterate over the import descriptor until we find our DLL.

Then, we will iterate over each entry in originalFirstThunk, check whether the function is imported by name, and if it matches the function we want. We look at its address on firstThunk at the same position, change the memory protection so we can write to it, write the address of our new function, and then restore the old protection.

From then on, each call to MessageBoxW will call our hooked function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
while (importDescriptor->Name != NULL) {
    auto dllName = baseAddress + importDescriptor->Name;
    if (!strcmp((const char*)dllName, "USER32.dll")) {
        auto firstThunk = (PIMAGE_THUNK_DATA)(baseAddress + importDescriptor->FirstThunk);
        auto originalFirstThunk = (PIMAGE_THUNK_DATA)(baseAddress + importDescriptor->OriginalFirstThunk);
        while (originalFirstThunk->u1.AddressOfData != 0) {
            if (originalFirstThunk->u1.Ordinal == IMAGE_ORDINAL_FLAG) {
                originalFirstThunk++;
                firstThunk++;
                continue;
            }
            auto function = (PIMAGE_IMPORT_BY_NAME)(baseAddress + originalFirstThunk->u1.AddressOfData);
            if (!strcmp(function->Name, "MessageBoxW")) {
                DWORD oldProtect;
                VirtualProtect((LPVOID)&(firstThunk->u1.Function), 4, PAGE_READWRITE, &oldProtect);
                firstThunk->u1.Function = (DWORD)MessageBoxNew;
                VirtualProtect((LPVOID)(&firstThunk->u1.Function), 4, oldProtect, &oldProtect);
            }
            originalFirstThunk++;
            firstThunk++;
        }
    }
    importDescriptor++;
}

Calling the function now:

1
MessageBoxW(nullptr, L"Hello world", L"Hello world", MB_OK);

Results in our hooked function being called:

updatedupdated2023-03-192023-03-19