Export Address Table , Export Forwarding and DLL side-loading
The Export Address Table (EAT) is a component of the Portable Executable (PE) file format, which is the executable file format used by Windows for 32-bit and 64-bit versions of the operating system. The PE format is utilized for executable files, object code, DLLs (Dynamic Link Libraries), and others.
The Export Address Table is specifically related to the exports section of a PE file. When a dynamic-link library (DLL) or executable file exports functions or symbols, it means that these functions or symbols are made available for use by other programs or modules. The Export Address Table contains the addresses of these exported functions or symbols.
Here's a brief overview of how the Export Address Table works:
Export Directory: The PE format includes an export directory that contains information about the exported symbols. This directory is typically located in the data directory of the PE file.
Export Address Table (EAT): The EAT is an array of addresses that correspond to the exported functions or symbols. Each entry in the EAT points to the memory address where the exported function resides.
When a program or module dynamically links to a DLL, it can use the information in the EAT to locate and invoke the exported functions. The EAT essentially serves as a lookup table for the addresses of exported functions, making it possible for other programs to use these functions without having to know their exact memory addresses.
It's important to note that the PE file format is a complex structure with various sections and tables that play crucial roles in the execution of programs on the Windows operating system. The Export Address Table is just one component of this larger structure.
The Export table address can be located in PE headers
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
and for 64bit PE files
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
The Virtual Address of Export directory can be obtained from OptionalHeader.
using the field
An array of IMAGE_DATA_DIRECTORY
structures named DataDirectory
is defined, with a size of IMAGE_NUMBEROF_DIRECTORY_ENTRIES
. The index number of the directory entry IMAGE_DIRECTORY_ENTRY_EXPORT
is then utilized to access the Export table address.
Within the IMAGE_DATA_DIRECTORY
structure, two fields are present: VirtualAddress
, which provides the Relative Virtual Address (RVA) in memory of the Export table address, and the size
field.
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
In order to access the Export table address
in the loaded binary, add IMAGE_DATA_DIRECTORY.VirtualAddress
to the BaseAddress
of the binary. This action leads to the actual Export Address Table.
typedef struct _IMAGE_EXPORT_DIRECTORY
{
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
PDWORD *AddressOfFunctions;
PDWORD *AddressOfNames;
PWORD *AddressOfNameOrdinals;
}
IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
lets try to explore a basic skeleton of IMAGE_EXPORT_DIRECTORY
which hosts a simple function PlainFuncName
#define WIN32_LEAN_AND_MEAN
#include
BOOL WINAPI PlainFuncName( void * lpParams)
{
return TRUE;
}
BOOL WINAPI DllMain(
HINSTANCE hinstDLL,
DWORD fdwReason,
LPVOID lpvReserved )
{
switch( fdwReason )
{
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
if (lpvReserved != nullptr)
{
break;
}
break;
}
return TRUE;
}
To mark exports in the dll we will use DEF file
A .def file, short for Module-Definition File, is a text file that contains instructions for the linker. It provides additional information about the DLL (Dynamic Link Library) or executable file being built, including details such as exported functions, imported functions, and other settings.
; DEF File
EXPORTS
PlainFuncName
For compilation we will use MSVC Link
link filename.obj /DLL /nodefaultlib /noentry /def:filename.def
now lets try to examine how the structure looks in hex
Pretty basic one .
- Name - Corresponds to the name of the DLL .
- AddressOfFunctions - Address of Subroutines
- AddressOfNames - Address of Names of the functions
- AddressOfNameOrdinals - Address of Ordinals ( discussed later )
DEF format primer
In a .def (Module-Definition) file for a DLL (Dynamic Link Library) in the MSVC (Microsoft Visual C++) compiler, you can control various aspects related to exported functions. Here's a summary of what you can do with exported functions in a .def file:
1. Export Function Names:
LIBRARY MyLibrary
EXPORTS
MyFunction1
MyFunction2
This exports MyFunction1 and MyFunction2 from the DLL. These functions will be accessible to other modules that link against the DLL.
2. Specify Ordinals:
LIBRARY MyLibrary
EXPORTS
MyFunction1 @1
MyFunction2 @2
You can assign explicit ordinals to exported functions. This sets the ordinals for MyFunction1 and MyFunction2 to 1 and 2, respectively.
3. Decorate Names:
LIBRARY MyLibrary
EXPORTS
MyFunction1 @1
"?DecoratedFunction2@@YAXXZ" @2
You can specify the decorated names for functions, which can be useful when interfacing with functions written in languages other than C or C++.
3. Forwarding Functions:
LIBRARY MyLibrary
EXPORTS
MyFunction1 = OtherLibrary.MyFunction1
You can use a .def file to create forwarding entries, redirecting calls from one function to another. This forwards calls to MyFunction1 in your DLL to OtherLibrary.MyFunction1.
Exports by Ordinal
One of the features of EAT (Export Address Table) is the creation of exports by ordinal instead of a name. It can be easily defined in a DEF file using
EXPORTS
PlainFuncName @1 NONAME
In the PE file:
NumberOfNames
AddressOfNames
AddressOfNameOrdinals
will all be set to 0; only AddressOfFunctions
RVA would be present.
An export with an ordinal can be called using the following code:
#include
#include
int main() {
// Load the DLL
HMODULE hDll = LoadLibrary(L"YourDllName.dll");
if (hDll != NULL) {
// Ordinal value of the function you want to call
// Replace 1 with the actual ordinal value
DWORD ordinalValue = 1;
// Get the function pointer by ordinal
FARPROC functionPointer = GetProcAddress(hDll, MAKEINTRESOURCEA(ordinalValue));
if (functionPointer != NULL) {
// Define the function signature
typedef void(__stdcall* MyFunctionType)();
// Cast the function pointer to the correct type
MyFunctionType myFunction = (MyFunctionType)functionPointer;
// Call the function
myFunction();
} else {
printf("GetProcAddress failed with error code: %lu\n", GetLastError());
}
// Unload the DLL
FreeLibrary(hDll);
} else {
printf("LoadLibrary failed with error code: %lu\n", GetLastError());
}
return 0;
}
Export Forwarding
One of the hallmarks of the export address table in Windows PE is the ability to forward exports in another DLL.
Export forwarding in Portable Executable (PE) files refers to a mechanism by which an exported function from a dynamic-link library (DLL) can be redirected to another DLL. It allows one DLL to act as a proxy or intermediary, forwarding the exported functionality to another DLL. This can be used for various purposes, including version compatibility, flexibility in choosing implementations, or modularizing code. However it has been abused by APT to take leverage of legit programs to execute malicious code
Loading Process:
- When the application starts, it searches for the required DLL.
- If the DLL is not found in the application's directory, the system directory, or the PATH, it will search the current directory and other directories in the specified order.
- If a malicious DLL with the same name is found in one of these directories, it will be loaded instead of the legitimate DLL.
- A legitimate application calls a function in a DLL, and it is proxied to the legitimate DLL. Moreover, malicious code is executed.
All of this is made possible using Export Forwarding
EXPORTS
MyFunction1 = user32.MessageBeep
The above DEF file will create an export named MyFunction1
and forward it to the user32.dll
MessageBeep
function. Any call to MyFunction1
will eventually land on MessageBeep()
.
The changes we see in the Export Address table is that the AddressOfFunctions
value now points to a pointer to an ASCII string, which is the dll.functionname
(i.e., user32.MessageBeep
).
Wrapping up
Here is a small script to parse any forwarder exports in a dll
import pefile
def parse_dll_forward_exports(file_path):
try:
pe = pefile.PE(file_path)
if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
export_directory = pe.DIRECTORY_ENTRY_EXPORT
for export in export_directory.symbols:
if export.forwarder:
print(f"Function: {export.name} (Forwarded to: {export.forwarder})")
except Exception as e:
print(f"Error parsing PE file: {e}")
if __name__ == "__main__":
# Replace 'your_dll_file.dll' with the path to your PE file
dll_file_path = 'your_dll_file.dll'
parse_dll_forward_exports(dll_file_path)