Skip to content

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_MAGIC

No 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 @BASE where 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:

macroindexdescription
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE7Architecture-specific data
IMAGE_DIRECTORY_ENTRY_BASERELOC5Base relocation table
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT11Bound import directory
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR14COM descriptor table
IMAGE_DIRECTORY_ENTRY_DEBUG6Debug directory
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT13Delay import table
IMAGE_DIRECTORY_ENTRY_EXCEPTION3Exception directory
IMAGE_DIRECTORY_ENTRY_EXPORT0Export directory
IMAGE_DIRECTORY_ENTRY_GLOBALPTR8The relative virtual address of global pointer
IMAGE_DIRECTORY_ENTRY_IAT12Import address table
IMAGE_DIRECTORY_ENTRY_IMPORT1Import directory
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG10Load configuration directory
IMAGE_DIRECTORY_ENTRY_RESOURCE2Resource directory
IMAGE_DIRECTORY_ENTRY_SECURITY4Security directory
IMAGE_DIRECTORY_ENTRY_TLS9Thread 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.