Appearance
Last review: Sept 15,2025
NT header
The NT header is the most important header and is defined as follows:
CPP
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;It is split into two sub-headers, which I will explain in more detail:
- The file header
- The Optional Header
File Header
The file header provides the number of sections in the PE file. This is used by a PE loader to iterate over each section header, which provides information on how to map each section in memory. The following describes the File Header, where the field NumberOfSections has a rather explicit name:
CPP
typedef struct _IMAGE_FILE_HEADER {
//...
DWORD NumberOfSections;
//...
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;Optional Header
The optional header is actually NOT optional at all. I have no idea why it is called that, but it is probably the most important header to consider when developing a loader. It provides information on how to find important sections and the location of the entry point of the executable.
The following structure describes this header:
CPP
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
typedef IMAGE_OPTIONAL_HEADER64 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64 PIMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR64_MAGICNo fewer than six fields are mandatory for a PE loader:
- The size of the image (
SizeOfImage): This is used to allocate enough memory space for the image. It can be different from the size of the file. - The size of header (
SizeOfHeader). The headers of the PE file has to be copied at@BASE. Thus, this field help to know how many byte have to be copied. - The image base (
ImageBase): This is the address where the image is expected to be loaded. In an ideal world,@BASE = ImageBase. In practice, it is not always possible to respect this choice. More details will be given later. - The entry point address (
AddressOfEntryPoint): This is an offset, from the base memory@BASE, to the entry point. The first instruction to be executed is located at@BASE + AddressOfEntryPoint. - DataDirectory: This is a table that will be described later in this document
With this information, a loader can allocate memory to store the image and copy the headers:
C++
PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)base_file;
PIMAGE_NT_HEADERS nt_header = (PIMAGE_NT_HEADERS)((char*)dos_header + dos_header->e_lfanew);
PIMAGE_SECTION_HEADER sections_headers = (PIMAGE_SECTION_HEADER)(nt_header + 1);// +1 means sizeof(IMAGE_NT_HEADERS)
// Allocate memory
unsigned char *base_mem = (unsigned char*)VirtualAlloc(NULL, nt_header->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// Copying the headers
memcpy(base_mem, base_file, nt_header->OptionalHeader.SizeOfHeaders);Eventually, a loader needs to copy the sections. However, they must be copied in a very specific way. This is where the section headers become important:
C++
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;- PointerToRawData: This is an offset relative to
@MEMFILE, used to locate the section in the PE file - SizeOfRawData: The size of the section. This is obviously important to know how many bytes need to be copied.
- VirtualAddress : This is an offset relative to
@BASEwhere the section have to be copied.
Through these fields, the following code can then be used to copy the sections:
CPP
for (int section_idx = 0; section_idx < nt_header->FileHeader.NumberOfSections; section_idx++) {
memcpy(base_mem + sections_headers[section_idx].VirtualAddress, base_file + sections_headers[section_idx].PointerToRawData, sections_headers[section_idx].SizeOfRawData);
}Data directory
The Data Directory is a table that contains information to locate important sections. This information is redundant with what is found in the section headers. However, this table is indexed, making it a faster way to access the sections. For example, index 0 is for the so-called export section, and only for it. winnt.h provides macros to help us:
| macro | index | description |
|---|---|---|
| IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 7 | Architecture-specific data |
| IMAGE_DIRECTORY_ENTRY_BASERELOC | 5 | Base relocation table |
| IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 11 | Bound import directory |
| IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | 14 | COM descriptor table |
| IMAGE_DIRECTORY_ENTRY_DEBUG | 6 | Debug directory |
| IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 13 | Delay import table |
| IMAGE_DIRECTORY_ENTRY_EXCEPTION | 3 | Exception directory |
| IMAGE_DIRECTORY_ENTRY_EXPORT | 0 | Export directory |
| IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 8 | The relative virtual address of global pointer |
| IMAGE_DIRECTORY_ENTRY_IAT | 12 | Import address table |
| IMAGE_DIRECTORY_ENTRY_IMPORT | 1 | Import directory |
| IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 10 | Load configuration directory |
| IMAGE_DIRECTORY_ENTRY_RESOURCE | 2 | Resource directory |
| IMAGE_DIRECTORY_ENTRY_SECURITY | 4 | Security directory |
| IMAGE_DIRECTORY_ENTRY_TLS | 9 | Thread local storage directory |
In the following sections of this article, we will show many of these directories.
Each element of this table is defined by the following structure:
CPP
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;The VirtualAddress field is an offset from @BASE to locate the expected section. Each section has its own structure.