Why is IAT Resolution Necessary?
DLLs rarely exist in isolation; they almost always utilize functions provided by other DLLs. For instance, nearly any Windows DLL will need functions from kernel32.dll
(for core OS services like memory management, process/thread control) and potentially others like user32.dll
(for UI elements) or ntdll.dll
(for lower-level system calls).
When a DLL calls a function from another DLL, it doesn’t usually call that function’s absolute address directly (as that address can change). Instead, the compiler generates an indirect call through a pointer stored within the calling DLL’s memory space. The collection of these pointers are known as the Import Address Table (IAT). Each entry in the IAT holds the actual memory address of an imported function.
How LoadLibrary Creates the IAT
When the standard Windows loader loads a DLL via LoadLibrary
, one of its crucial jobs is to:
- Identify all the external DLLs the new DLL depends on.
- Load those dependency DLLs into memory (if they aren’t already loaded).
- Find the addresses of all the required functions within those dependency DLLs.
- Write these resolved addresses into the new DLL’s IAT.
This “patches” the IAT so that when the new DLL’s code calls an imported function indirectly through an IAT entry, it jumps to the correct location in the dependency DLL.
In reflective loading, since we bypass the OS loader, we become responsible for performing this IAT resolution process manually. Without this step, any attempt by our reflectively loaded DLL to call an external function would fail, likely causing a crash, because the IAT entries would still contain placeholder information instead of valid function addresses.
How the PE Format Supports Imports
The PE format provides the necessary information for resolving imports in the Import Directory. The IMAGE_OPTIONAL_HEADER
’s DataDirectory
array entry at index IMAGE_DIRECTORY_ENTRY_IMPORT
(index 1) points to the start of this directory.
The Import Directory is essentially an array of IMAGE_IMPORT_DESCRIPTOR
structures. Each structure corresponds to a single DLL that our manually loaded DLL depends on (e.g., one descriptor for kernel32.dll
, another for user32.dll
, etc.). The array is terminated by an IMAGE_IMPORT_DESCRIPTOR
structure filled with nulls.
The key fields within an IMAGE_IMPORT_DESCRIPTOR
are:
Name
An RVA pointing to a null-terminated string that holds the name of the required dependency DLL (e.g., “kernel32.dll”).
OriginalFirstThunk`(OFT)
An RVA pointing to an array of IMAGE_THUNK_DATA
entries (essentially pointer-sized values). This array, often called the Import Lookup Table (ILT), lists the specific functions to be imported from this dependency DLL. Each entry either encodes an ordinal number or points (via RVA) to an IMAGE_IMPORT_BY_NAME
structure.
The IMAGE_IMPORT_BY_NAME
structure (referenced by ILT entries when importing by name) simply contains a 16-bit “Hint” (an index suggestion for faster lookups in the exporting DLL) followed by the null-terminated string name of the function to import.
FirstThunk (FT)
An RVA pointing to another array of IMAGE_THUNK_DATA
entries. This array is the Import Address Table (IAT) itself. Initially (before loading), the IAT often mirrors the ILT. The loader’s job is to overwrite each entry in the IAT with the actual resolved address of the corresponding imported function.
The IAT Resolution Process
A reflective loader must iterate through the Import Directory and resolve the imports for each required DLL.
The process generally follows these steps.
Step 1: Locate the Import Directory
Find the RVA of the Import Directory from the DataDirectory
(index 1) and calculate its VA (ImportDirVA = ActualAllocatedBase + ImportDirRVA
).
Step 2: Iterate Through Descriptors
Starting at ImportDirVA
, process the array of IMAGE_IMPORT_DESCRIPTOR
structures one by one until a null descriptor is encountered.
Step 3: Then, for Each Descriptor
- Get Dependency DLL Name: Read the
Name
RVA, calculate the VA (DllNameVA = ActualAllocatedBase + NameRVA
), and read the null-terminated string name of the required DLL. - Load Dependency: Crucially, use the standard Windows API function
LoadLibrary
(e.g.,windows.LoadLibrary
in Go) to load this dependency DLL into the current process’s address space. This is necessary because we need the dependency DLL mapped correctly by the OS itself to find its exported functions. Keep the returnedHMODULE
handle. - Find ILT and IAT: Calculate the base VAs of the Import Lookup Table (
ILT_VA = ActualAllocatedBase + OriginalFirstThunk
) and the Import Address Table (IAT_VA = ActualAllocatedBase + FirstThunk
). (Note: IfOriginalFirstThunk
is zero, the ILT and IAT are the same, pointed to byFirstThunk
). - Iterate Through Imports: Loop through the entries in the ILT and IAT arrays in parallel (they correspond one-to-one). The loop terminates when an entry in the ILT is null (zero).
- For each entry pair (
i
): - Read the value from the ILT entry (
ILT_VA + i * sizeof(uintptr)
). Let’s call thislookupValue
. - Determine if importing by Ordinal or Name:
- By Ordinal: If the most significant bit of
lookupValue
is set (IMAGE_ORDINAL_FLAG64
for 64-bit), the lower 16 bits represent the ordinal number of the function to import. - By Name: Otherwise,
lookupValue
is an RVA to anIMAGE_IMPORT_BY_NAME
structure. Calculate its VA (HintNameVA = ActualAllocatedBase + lookupValue
), read the function name string starting 2 bytes afterHintNameVA
. - Get Function Address: Use the standard Windows API function
GetProcAddress
(e.g.,windows.GetProcAddress
or a syscall equivalent in Go), passing theHMODULE
of the loaded dependency DLL and either the function name string or the ordinal number. This returns the actual VA of the required function within the loaded dependency DLL. Handle errors ifGetProcAddress
fails (this is usually fatal for the loader). - Patch the IAT: Write the function address obtained from
GetProcAddress
directly into the corresponding entry in the IAT (IAT_VA + i * sizeof(uintptr)
) within our reflectively mapped DLL’s memory.
- For each entry pair (
- Repeat: Continue processing descriptors until the null terminator is found.
After this process completes, the Import Address Table within our reflectively mapped DLL has been fully patched. All entries now point to the correct memory locations of the functions imported from external DLLs. The DLL’s code can now successfully call these external functions via the patched IAT pointers.
Conclusion
With both internal address references (relocations) and external dependencies (imports) resolved, the DLL is almost ready for execution. The final steps involve potentially calling its entry point (DllMain
) and then invoking any specific exported function we need.
We’ll cover this in Module 5, for now let’s dip into some practical labs where we’ll ensure our reflective loader is capable of relocations, and constructing an IAT table.