PEs & DLLs

Portable Executable (PE) Format

Portable Executable (PE) is the file format for executables on Windows.

Not only .exe files are PE files, dynamic link libraries (.dll), Kernel modules (.srv), Control panel applications (.cpl) and many others are also PE files.

Executables (e.g. EXEs, DLLs) are often refered as "Images".

PE Structure

- DOS Header (IMAGE_DOS_HEADER)

The first part of a PE (Portable Executable) file is called the DOS (Disk Operating System) header, which starts with the signature "MZ" (0x4D 0x5A) to confirm it's a valid PE file. The DOS header structure includes two key elements:

e_magic: A 2-byte field always set to 0x5A4D (or "MZ"), confirming the DOS header.

e_lfanew: A 4-byte field that stores the offset to the start of the NT Header, which is where the actual PE file information begins. This offset is consistently located at an offset of 0x3C from the start of the DOS header.

To retrieve the DOS Header:

// Pointer to the structure 
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pPE;		
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE){
	return -1;
}

- DOS Stub

DOS stub which an error message that prints "This program cannot be run in DOS mode" in case the program is loaded in DOS mode or "Disk Operating Mode". It is worth noting that the error message can be changed by the programmer at compile time.

This is not a PE header, but it's good to be aware of it.

- NT Header (IMAGE_NT_HEADERS)

The NT Header (IMAGE_NT_HEADERS) in a PE (Portable Executable) file is a crucial part that contains two other headers: FileHeader and OptionalHeader, providing extensive details about the file. It's identified by the "PE" signature, represented as 0x50450000, with "PE" padded by two null bytes. You can access the NT Header using the e_lfanew member within the DOS Header.

There are two versions of the NT Header based on the machine's architecture:

32-bit Version (IMAGE_NT_HEADERS32):

typedef struct _IMAGE_NT_HEADERS32 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

64-bit Version (IMAGE_NT_HEADERS64):

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

The primary difference between these versions is the structure of the OptionalHeader, which can be IMAGE_OPTIONAL_HEADER32 for 32-bit or IMAGE_OPTIONAL_HEADER64 for 64-bit architectures.

To retrieve the NT Header:

// Pointer to the structure
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pPE + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE) {
	return -1;
}

- File Header (IMAGE_FILE_HEADER)

The File Header (IMAGE_FILE_HEADER) in a PE file, accessible from the NT Header, includes information about the file's characteristics.

The most important struct members are:

  • NumberOfSections - The number of sections in the PE file (discussed later).

  • Characteristics - Flags that specify certain attributes about the executable file, such as whether it is a dynamic-link library (DLL) or a console application.

  • SizeOfOptionalHeader - The size of the following optional header

To retrieve the File Header:

IMAGE_FILE_HEADER		ImgFileHdr	= pImgNtHdrs->FileHeader;


if (ImgFileHdr.Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) {

	printf("[i] Executable File Detected As : ");

	if (ImgFileHdr.Characteristics & IMAGE_FILE_DLL)
		printf("DLL\n");
	else if (ImgFileHdr.Characteristics & IMAGE_SUBSYSTEM_NATIVE)
		printf("SYS\n");
	else
		printf("EXE\n");
}

/*
The members of the IMAGE_FILE_HEADER structure are described below.

Machine - The type of machine for which the PE file or object file is intended.

NumberOfSections - The number of sections in the PE file or object file.

TimeDateStamp - Time and date when the PE file or object file was created.

PointerToSymbolTable - Offset in the file to the symbol table, if it exists.

NumberOfSymbols - Number of symbols in the symbol table.

SizeOfOptionalHeader - The size of the optional header.

Characteristics - The characteristics of the PE file or object file. The values of this field are defined by the IMAGE_FILE_* constants; these specify the type of the PE file (.exe, .dll, .sys).
*/

printf("[i] File Arch : %s \n", ImgFileHdr.Machine == IMAGE_FILE_MACHINE_I386 ? "x32" : "x64");
printf("[i] Number Of Sections : %d \n", ImgFileHdr.NumberOfSections);
printf("[i] Size Of The Optional Header : %d Byte \n", ImgFileHdr.SizeOfOptionalHeader);

- Optional Header (IMAGE_OPTIONAL_HEADER)

The Optional Header it's essential for the execution of the PE file. It is referred to as optional because some file types do not have it.

The optional header contains a ton of information that can be used. Below are some of

the struct members that are commonly used:

  • Magic - Describes the state of the image file (32 or 64-bit image)

  • MajorOperatingSystemVersion - The major version number of the required operating

  • system (e.g. 11, 10)

  • MinorOperatingSystemVersion - The minor version number of the required operating system (e.g. 1511, 1507, 1607)

  • SizeOfCode - The size of the .text section (Discussed later)

  • AddressOfEntryPoint - Offset to the entry point of the file (Typically the main function)

  • BaseOfCode - Offset to the start of the .text section

  • SizeOfImage - The size of the image file in bytes

  • ImageBase - It specifies the preferred address at which the application is to be loaded into memory when it is executed. However, due to Window's memory protection mechanisms like ASLR, it's rare to see an image mapped to its preferred address because the Windows PE Loader maps the file to a different address. This random allocation will cause issues in the implementation of red teaming techniques because some addresses that are considered constant were changed. The Windows PE loader will then go through PE relocation to fix these addresses.

  • DataDirectory - One of the most important members in the optional header. This is an array of IMAGE_DATA_DIRECTORY, which contains the directories in a PE file.

To retrieve the Optional Header:

IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
if (ImgOptHdr.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC) {
	return;
}

printf("[i] File Arch (Second way) : %s \n", ImgOptHdr.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC ? "x32" : "x64");

printf("[+] Size Of Code Section : %d \n", ImgOptHdr.SizeOfCode);
printf("[+] Address Of Code Section : 0x%p \n\t\t[RVA : 0x%0.8X]\n", (PVOID)(pPE + ImgOptHdr.BaseOfCode), ImgOptHdr.BaseOfCode);
printf("[+] Size Of Initialized Data : %d \n", ImgOptHdr.SizeOfInitializedData);
printf("[+] Size Of Unitialized Data : %d \n", ImgOptHdr.SizeOfUninitializedData);
printf("[+] Preferable Mapping Address : 0x%p \n", (PVOID)ImgOptHdr.ImageBase);
printf("[+] Required Version : %d.%d \n", ImgOptHdr.MajorOperatingSystemVersion, ImgOptHdr.MinorOperatingSystemVersion);
printf("[+] Address Of The Entry Point : 0x%p \n\t\t[RVA : 0x%0.8X]\n", (PVOID)(pPE + ImgOptHdr.AddressOfEntryPoint), ImgOptHdr.AddressOfEntryPoint);
printf("[+] Size Of The Image : %d \n", ImgOptHdr.SizeOfImage);
printf("[+] File CheckSum : 0x%0.8X \n", ImgOptHdr.CheckSum);
printf("[+] Number of entries in the DataDirectory array : %d \n", ImgOptHdr.NumberOfRvaAndSizes); // this is the same as `IMAGE_NUMBEROF_DIRECTORY_ENTRIES` - `16`

- Data Directory

The Data Directory can be accessed from the optional's header last member.

A specific data directory can be accessed the following index:

#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor

Export Directory: A PE's export directory is a data structure that contains information about functions and variables that are exported from the executable. It contains the addresses of the exported functions and variables, which can be used by other executable files to access the functions and data. The export directory is generally found in DLLs that export functions (e.g. kernel32.dll exporting CreateFileA).

Import Address Table: The import address table is a data structure in a PE that contains information about the addresses of functions imported from other executable files. The addresses are used to access the functions and data in the other executables (e.g. Application.exe importing CreateFileA from kernel32.dll).

To access the data directory:

/*
The data directories can be accessed using the following line of code:
IMAGE_DATA_DIRECTORY DataDir = ImgOptHdr.DataDirectory[#INDEX IN THE ARRAY#];
*/

// For example, to retrieve the data directory of the export directory:
printf("[*] Export Directory At 0x%p Of Size : %d \n\t\t[RVA : 0x%0.8X]\n", 
	(PVOID)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress),
	ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size,
ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

- PE Sections

PE sections contain the code and data used to create an executable program. Each PE section is given a unique name and typically contains executable code, data, or resource information. There is no constant number of PE sections because different compilers can add, remove or merge sections depending on the configuration.

The following PE sections are the most important ones and exist in almost every PE.

  • .text - Contains the executable code which is the written code.

  • .data - Contains initialized data which are variables initialized in the code.

  • .rdata - Contains read-only data. These are constant variables prefixed with const .

  • .idata - Contains the import tables. These are tables of information related to the functions called using the code. This is used by the Windows PE Loader to determine which DLL files to load to the process, along with what functions are being used from each DLL.

  • .reloc - Contains information on how to fix up memory addresses so that the program can be loaded into memory without any errors.

  • .rsrc - Used to store resources such as icons and bitmaps

Each PE section has an IMAGE_SECTION_HEADER data structure that contains valuable information about it. These structures are saved under the NT headers in a PE file and are stacked above each other where each structure represents a section.

To access the sections:

PIMAGE_SECTION_HEADER pImgSectionHdr = (PIMAGE_SECTION_HEADER)(((PBYTE)pImgNtHdrs) + sizeof(IMAGE_NT_HEADERS));
for (size_t i = 0; i < pImgNtHdrs->FileHeader.NumberOfSections; i++) {
	printf("[#] %s \n", (CHAR*)pImgSectionHdr->Name);
	printf("\tSize : %d \n", pImgSectionHdr->SizeOfRawData);
	printf("\tRVA : 0x%0.8X \n", pImgSectionHdr->VirtualAddress);
	printf("\tAddress : 0x%p \n", (PVOID)(pPE + pImgSectionHdr->VirtualAddress));
	printf("\tRelocations : %d \n", pImgSectionHdr->NumberOfRelocations);
	printf("\tPermissions : ");
	if (pImgSectionHdr->Characteristics & IMAGE_SCN_MEM_READ)
		printf("PAGE_READONLY | ");
	if (pImgSectionHdr->Characteristics & IMAGE_SCN_MEM_WRITE && pImgSectionHdr->Characteristics & IMAGE_SCN_MEM_READ)
		printf("PAGE_READWRITE | ");
	if (pImgSectionHdr->Characteristics & IMAGE_SCN_MEM_EXECUTE)
		printf("PAGE_EXECUTE | ");
	if (pImgSectionHdr->Characteristics & IMAGE_SCN_MEM_EXECUTE && pImgSectionHdr->Characteristics & IMAGE_SCN_MEM_READ)
		printf("PAGE_EXECUTE_READWRITE");
	printf("\n\n");

	pImgSectionHdr = (PIMAGE_SECTION_HEADER)((PBYTE)pImgSectionHdr + (DWORD)sizeof(IMAGE_SECTION_HEADER));
}

Both .exe and .dll file types are considered portable executable formats but there are differences between the two.

DLLs are shared libraries of executable functions or data that can be used by multiple applications simultaneously. They are used to export functions to be used by a process.

Unlike EXE files, DLL files cannot execute code on their own. Instead, DLL libraries need to be invoked by other programs to execute the code.

- System-Wide DLL Base Address

The Windows OS uses a system-wide DLL base address to load some DLLs at the same base address in the virtual address space of all processes on a given machine to optimize memory usage and improve system performance.

- Reasons why DLLs are very often used in Windows

1. Modularization of Code - Instead of having one massive executable that contains the entire functionality, the code is divided into several independent libraries with each library being focused on specific functionality.

2. Code Reuse - DLLs promote code reuse since a library can be invoked by multiple processes.

3. Efficient Memory Usage - When several processes need the same DLL, they can save memory by sharing that DLL instead of loading it into the process's memory.

- DLL Entry Point

DLLs can optionally specify an entry point function that executes code when a certain task occurs such as when a process loads the DLL library.

Sample DLL Code:

BOOL APIENTRY DllMain(
    HANDLE hModule,          // Handle to DLL module
    DWORD ul_reason_for_call, // Reason for calling function
    LPVOID lpReserved         // Reserved
) {
    switch (ul_reason_for_call) {
        case DLL_PROCESS_ATTACHED: // A process is loading the DLL.
            // Do something here
            break;
        case DLL_THREAD_ATTACHED: // A process is creating a new thread.
            // Do something here
            break;
        case DLL_THREAD_DETACH: // A thread exits normally.
            // Do something here
            break;
        case DLL_PROCESS_DETACH: // A process unloads the DLL.
            // Do something here
            break;
    }
    return TRUE;
}

- Exporting a Function

Exporting a function in a Dynamic Link Library (DLL) allows other programs or processes to use that function. To export a function, you declare it with the extern and __declspec(dllexport) keywords.

// sampleDLL.dll
extern __declspec(dllexport) void HelloWorld() {
    // Function code here
}

By using extern and __declspec(dllexport), you indicate that HelloWorld is intended to be used by other programs, making it accessible to applications that use this DLL.

Dynamic Linking

It's possible to use the LoadLibrary, GetModuleHandle and GetProcAddress WinAPIs to import a function from a DLL.

This is a method of loading and linking code (DLLs) at runtime rather than linking them at compile time using the linker and import address table.

- Loading a DLL

Calling a common function will force the Windows OS to load the DLL exporting the function into the calling process's memory address space. This loads the dll automatically by the OS when the process started and not by the code.

If the application doesn't have the dll we want (maybe one created by us) loaded into memory, it would require the usage of the LoadLibrary:

HMODULE hModule = LoadLibraryA("sampleDLL.dll"); // hModule now contain sampleDLL.dll's handle

- Retrieving a DLL's Handle

If the dll is already loaded into the application's memory, one can retrieve its handle via the GetModuleHandle WinAPI function without leveraging the LoadLibrary function:

HMODULE hModule = GetModuleHandleA("sampleDLL.dll");

- Retrieving a Function's Address

Once the DLL is loaded into memory and the handle is retrieved, the next step is to retrieve the function's address. This is done withGetProcAddress:

PVOID pHelloWorld = GetProcAddress(hModule, "HelloWorld");

- Invoking The Function

// Constructing a new data type that represents HelloWorld's function pointer
typedef void (WINAPI* HelloWorldFunctionPointer)();

void call() {
    HMODULE hModule = LoadLibraryA("sampleDLL.dll");
    PVOID pHelloWorld = GetProcAddress(hModule, "HelloWorld");
    
    // Type-casting the 'pHelloWorld' variable to be of type 'HelloWorldFunctionPointer'
    HelloWorldFunctionPointer HelloWorld = (HelloWorldFunctionPointer)pHelloWorld;
    
    HelloWorld(); // Calling the 'HelloWorld' function via its function pointer
}

- Dynamic Linking Example

The code assumes that user32.dll , the DLL that exports that function, isn't loaded into memory.

typedef int (WINAPI* MessageBoxAFunctionPointer)( // Constructing a new data type, that will represent MessageBoxA's function pointer
    HWND hWnd,
    LPCSTR lpText,
    LPCSTR lpCaption,
    UINT uType
);

void call() {
    // Retrieving MessageBox's address, and saving it to 'pMessageBoxA' (MessageBoxA's function pointer)
    MessageBoxAFunctionPointer pMessageBoxA = (MessageBoxAFunctionPointer)GetProcAddress(LoadLibraryA("user32.dll"), "MessageBoxA");

    if (pMessageBoxA != NULL) {
        // Calling MessageBox via its function pointer if not null
        pMessageBoxA(NULL, "MessageBox's Text", "MessageBox's Caption", MB_OK);
    }
}

*The above MessageBoxAFunctionPointer data type could be represented as fnMessageBoxA

- Rundll32.exe

Rundll32.exe can be used to run an exported function of a DLL file:

rundll32.exe <dllname>, <function exported to run>

For example, User32.dll exports the function LockWorkStation which locks the machine. To run the function, use the following command:

rundll32.exe user32.dll,LockWorkStation

- DLL Creation in VS

Launch Visual studio and create a new project.

Select the Dynamic-Link Library (DLL) option.

The provided DLL template comes with framework.h , pch.h and pch.cpp which are known as Precompiled Headers. These are files used to make the project compilation faster for large projects. To delete them, highlight them and delete. After deleting the precompiled headers, the compiler's default settings must be changed to confirm that precompiled headers should not be used in the project. Go to C/C++ > Advanced Tab, Change the 'Precompiled Header' option to 'Not Using Precompiled Headers' and press 'Apply'.

Last updated