Skip to content

Knowledge Dump Dynamic Module Loading on Windows

Mike Lewis edited this page Aug 15, 2017 · 1 revision

Epoch Knowledge Dump Series

Dynamic Module Loading on Windows

Introduction

When programs are executed on Windows, they often rely on external libraries, known as dynamic link libraries or DLLs. Dynamic linking is a process by which code needed by a program can be stored in a different physical file than the program itself. In other words, DLLs allow Windows programs to store common code in a single place (the DLL file) instead of repeating it in every program's EXE file.

Epoch programs will almost always rely on multiple DLLs, whether they are OS-provided libraries, Epoch runtime libraries, or provided by some other source. This is pretty typical of any language running on a full-featured OS - for example, on Windows there is the C Runtime, the C++ Runtime, the .NET Runtime, Java's JVM, and so on.

Modules

In Windows parlance, each chunk of executable code packaged in a file is referred to as a module. Typically a process starts when an .EXE file's module is loaded into memory by the OS's loader. The loader does some work on the data in the on-disk file to make it ready to run from memory. You may recognize the term HMODULE from the Windows APIs - this is in fact a "handle" to a module in exactly the sense we're using the term here.

Traditionally a loadable module on Windows is stored in the Portable Executable (PE) format. This is somewhat changed in later versions of the OS with the introduction of Universal Windows Platform (UWP) apps but I'm not familiar with UWP so I will stick to what I know!

Modules are all loaded at a base address. This address is someplace in user-land, has enough contiguous range to store the module's code and some other related fragments of data, and can change wildly from one run of the program to the next. A major cause of this is Address Space Layout Randomization (ASLR), a technique designed to defeat security attacks that rely on knowing exactly what addresses a program will load into.

However, even without ASLR, DLLs may not load at their so-called "preferred" base address. This is because two DLLs might ask to load at, say, 0x100000 in the same process. The virtual memory system can't possibly store both DLLs in one place, so one has to relocate to a different base address.

As with many aspects of the Windows module loader, relocation is largely invisible magic to most programs (and programmers!). Thankfully we only need to know a little bit about it to do a lot of interesting stuff.

Exports

Any module can freely export code. This is done using a special table which is stashed in the module's PE file headers. Actually constructing an export table is beyond the scope of this article, but the format is well documented across the web and particularly MSDN (see for example here and here as well as other resources).

Essentially the export table is a list of function names that can be explicitly called by other modules. This table is what powers the GetProcAddress function. It maps a string name to a location in the module that contains the associated code.

To understand the mechanics, you'll want to look up how the PE format handles relocations and particularly about Relative Virtual Addresses. The nutshell version is that the number stored on disk in the export table corresponds only loosely to the actual address space that the code will live in once the loader has loaded the module.

Thankfully, the loader is smart enough to take care of resolving that mapping for us, so it's actually pretty easy to make it all work.

Imports

Once we have a module that exports code, we can import the functions into a second module. Typically the arrangement is that a DLL will export and an EXE will import, but there's actually nothing keeping you from doing it the other way around. That however is a tale for another day.

Much like the export table, the PE headers contain a description of how to find the Import Address Table (IAT) and Import Lookup Table (ILT) in a module. The ILT contains a list of modules (not necessarily DLLs!) that the module wants to import from, and a list of functions to import from each other module. For example, Foo.exe might import ComplexOperation() from Bar.dll.

One thing the loader does is fill in the IAT itself in memory (not on disk). Keep in mind that loading is complex and modules may not appear at the addresses you expect for a variety of reasons. The loader figures out how to find the relevant module's export table (from Bar.dll), grabs the address of ComplexOperation, and then stores a pointer to ComplexOperation inside Foo.exe's IAT.

Calling DLLs

Recall that the IAT is basically a bunch of function pointers. The loader essentially calls GetProcAddress for you to populate this table.

When you call a DLL function from the compiled (EXE) program, the code will look at the IAT, which has a known fixed RVA. Since the IAT is static from a certain point of view, we can precompute an address (subject to RVA rules) which is always going to be in the IAT. (There is a whole set of fixups that I won't go into that translate from RVAs to the actual addresses that modules load into, keeping in mind ASLR and other relocations.)

Once we know the address of a slot in the IAT, we can simply "load" (dereference) that slot to yield a function pointer to the corresponding DLL's exported function! And since the IAT slot's address is static, we can hard-code it into the EXE, and the loader will take care of matchmaking our imports with the DLL's exports and filling in that IAT slot.

Additional Suggested Research

String names are not the only way to locate DLL exports or imports. Look into ordinal hints as well. These are essentially short numbers that roughly hint at where in the DLL's export table to find a function's name. This helps avoid "slow" string comparisons and was much more useful in the 16-bit era, but can still be used on occasion today.

Actually calling into foreign code is fragile in many ways. You can't safely assume that global state will be shared between modules (it probably won't) so things like memory allocation have to be handled carefully since memory allocations require global state!

Another important factor to calling foreign code is calling conventions. Thankfully on 64-bit Windows there's pretty much one convention to think about, so it's hard to mess up. On 32-bit Windows though there are many conventions, sometimes defined by obscure vendor rules or strange niche optimization strategies. But yeah do some research on this as it'll give you a lot of insight into what the compiler does under the hood whenever a function call occurs.

Clone this wiki locally