Parsing PE Headers
Understanding how PEs (portable executables) work is a crucial part of Windows exploitation. Whether we're developing evasive malware or reverse engineering, we need to understand how they work.
PE Structure
Every header shown is a struct that holds information about the PE file.

Access Local PEB (x64)
MSVC Intrinsic Function
#include <winternl.h>
// Get PEB structure
#ifdef _WIN64
PPEB pPeb = (PPEB)__readgsqword(0x60);
#elif _WIN32
PPEB pPeb = (PPEB)__readfsdword(0x30);
#endif // _WIN64
// HELPER FUNCTIONS
// Get of current process (call pLdr->DllBase to get base address)
PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)(pPeb->Ldr->InMemoryOrderModuleList.Flink) - 0x10);
// pLdr->DllBase (Entrypoint to .exe)
NtCurrentTeb
We will need to include our own PEB & TEB objects (PTEB_A & PPEB_A)
// 64 bit
P_TEB teb = (P_TEB)NtCurrentTeb();
P_PEB peb = (P_PEB)teb->ProcessEnvironmentBlock;
// Getting Base Address
PVOID pBaseAddress = peb->ImageBaseAddress;
// Getting Ldr
PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(peb->Ldr);
NtQueryInformationProcess
typedef NTSTATUS (NTAPI *fnNtQueryInformationProcess)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
int main(void) {
fnNtQueryInformationProcess ntQueryPE = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationProcess");
PROCESS_BASIC_INFORMATION pbi;
ULONG ulRetLength;
NTSTATUS STATUS = ntQueryPE(GetCurrentProcess(), ProcessBasicInformation, &pbi, sizeof(pbi), &ulRetLength);
if (STATUS != 0) {
printf("NtQueryInformation Failed %ld", STATUS);
}
PPEB pPeb = (PPEB)pbi.PebBaseAddress;
PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)(pPeb->Ldr->InMemoryOrderModuleList.Flink) - 0x10);
printf("%p", pLdr->DllBase); // Print base address of current process
}
PE Sections & Headeres
Parsing PE Headers
PE
Parsing PE Sections
Get Built In Sections
// Dos Header
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pPE;
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE){
return -1;
}
// Nt Header
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pPE + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE) {
return -1;
}
// IMAGE_FILE_HEADER
IMAGE_FILE_HEADER ImgFileHdr = pImgNtHdrs->FileHeader;
// POINTER FILE HEADER
PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)(pBaseAddress + pDosHeader->e_lfanew + sizeof(DWORD));
PIMAGE_OPTIONAL_HEADER pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)(pBaseAddress + pDosHeader->e_lfanew + sizeof(DWORD)+sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)(pBaseAddress + pDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS));
Loop through all sections
Adding PE Sections
Structuring our code
One common way to structure code when parsing a Portable Executable is to create a struct that holds all data and headers for the PE.
typedef struct _PE_HDRS
{
PBYTE pFileBuffer; // Buffer from ReadFile
DWORD dwFileSize; // Size of file from GetFileSize
PIMAGE_NT_HEADERS pImgNtHdrs;
PIMAGE_SECTION_HEADER pImgSecHdr;
PIMAGE_DATA_DIRECTORY pEntryImportDataDir;
PIMAGE_DATA_DIRECTORY pEntryBaseRelocDataDir;
PIMAGE_DATA_DIRECTORY pEntryTLSDataDir;
PIMAGE_DATA_DIRECTORY pEntryExceptionDataDir;
PIMAGE_DATA_DIRECTORY pEntryExportDataDir;
BOOL bIsDLLFile;
} PE_HDRS, *PPE_HDRS;
Then when we parse each header we add it to the struct.
Populating _PE_HDRS struct
Here's an example of a function we can use to parse the PE filebuffer we are working with into our struct above. For more information read below.
BOOL InitializePeStruct(OUT PPE_HDRS pPeHdrs, IN PBYTE pFileBuffer, IN DWORD dwFileSize) {
if (!pPeHdrs || !pFileBuffer || !dwFileSize)
return FALSE;
pPeHdrs->pFileBuffer = pFileBuffer;
pPeHdrs->dwFileSize = dwFileSize;
pPeHdrs->pImgNtHdrs = (PIMAGE_NT_HEADERS)(pFileBuffer + ((PIMAGE_DOS_HEADER)pFileBuffer)->e_lfanew);
if (pPeHdrs->pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
pPeHdrs->bIsDLLFile = (pPeHdrs->pImgNtHdrs->FileHeader.Characteristics & IMAGE_FILE_DLL) ? TRUE : FALSE;
pPeHdrs->pImgSecHdr = IMAGE_FIRST_SECTION(pPeHdrs->pImgNtHdrs);
pPeHdrs->pEntryImportDataDir = &pPeHdrs->pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
pPeHdrs->pEntryBaseRelocDataDir = &pPeHdrs->pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
pPeHdrs->pEntryTLSDataDir = &pPeHdrs->pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS];
pPeHdrs->pEntryExceptionDataDir = &pPeHdrs->pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
pPeHdrs->pEntryExportDataDir = &pPeHdrs->pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
return TRUE;
}
Relative Virtual Addresses (RVAs)
Relative Virtual Addresses are addresses used to reference locations within a PE file. For example, specifying the location of code, data, and resources.
IMPORTANT: An RVA is a 32-bit value that specifies the offset of a data structure or section from the beginning of the PE file. Hence relative because it specifies the offset from the beginning of the file, rather than an absolute memory address.
The PE header contains several RVAs that specify the location of the code and data sections, the import and export tables, and other important data structures.
DOS Header (IMAGE_DOS_HEADER)
The DOS header is located at the beginning of the PE file and contains information about the file, such as its size, and characteristics. But most importantly, it contains the RVA (offset) to the NT header.
Retrieve DOS Header (IMAGE_DOS_HEADER):
// Pointer to the structure
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pPE;
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE){
return -1;
}
NOTE: Since the DOS header is at the very beginning of a PE file, retrieving the value is only a matter of getting a pointer. (pPE).
NT Header (IMAGE_NT_HEADER)
The e_lfanew
member of the DOS header is an RVA to the IMAGE_NT_HEADERS
structure.
Retrieve NT Header (IMAGE_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)
Since the file header is a member of the IMAGE_NT_HEADERS
structure, it can be accessed using the following line of code.
IMAGE_FILE_HEADER ImgFileHdr = pImgNtHdrs->FileHeader;
Optional Header (IMAGE_OPTIONAL_HEADER)
Since the optional header is a member of the IMAGE_NT_HEADERS
structure, it is can be accessed using the following code.
Retrieve Optional Header (IMAGE_OPTIONAL_HEADER):
IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;
if (ImgOptHdr.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC) {
return -1;
}
NOTE: Depending on the compiler architecture, the IMAGE_NT_OPTIONAL_HDR_MAGIC
constant will automatically expand to the correct value:
IMAGE_NT_OPTIONAL_HDR32_MAGIC
- 32-bitIMAGE_NT_OPTIONAL_HDR64_MAGIC
- 64-bit
DataDirectory (IMAGE_DATA_DIRECTORY)
The Data Directory can be accessed from the optional's header last member. This is an array of IMAGE_DATA_DIRECTORY
meaning each element in the array is an IMAGE_DATA_DIRECTORY
structure that references a special data directory. The IMAGE_DATA_DIRECTORY
structure is shown below.
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
The fields of the structure contain information such as:
VirtualAddress
- Specifies the virtual address of the specified structure in the PE file, these areRVAs
.Size
- Specifies the size of the data directory.
Export Table (IMAGE_EXPORT_DIRECTORY)
This structure is not officially documented by Microsoft. You will need to use unofficial documentation.\
Export Table Structure
The export table is a structure defined as IMAGE_EXPORT_DIRECTORY
which is shown below.
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Retrieving The Export Table
The IMAGE_EXPORT_DIRECTORY
structure is used to store information about the functions and data that are exported from a PE file. This information is stored in the data directory array with the index IMAGE_DIRECTORY_ENTRY_EXPORT
. To fetch it from the IMAGE_OPTIONAL_HEADER
structure:
Retrieve Export Table:
PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
Import Table (IMAGE_IMPORT_DIRECTORY)
The import address table is an array of IMAGE_IMPORT_DESCRIPTOR
structures with each one being for a DLL file that contains the functions that were used from these DLLs.
Import Address Table Structure
The IMAGE_IMPORT_DESCRIPTOR
structure is also not officially documented by Microsoft although it is defined in the Winnt.h Header File as follows:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
Retrieving The Import Address Table
IMAGE_IMPORT_DESCRIPTOR* pImgImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
Parse Import Address Table
Here is an example of crudely parsing an IAT. To iterate through the IAT we add the size of IMAGE_DATA_DIRECTORY with every iteration until we reach a NULLified version. (Represents the end). (View Local PE Injection & Reflective DLL Injection for reference).
BOOL ParseImportAddressTable(IN PIMAGE_DATA_DIRECTORY pEntryImportDataDir, IN PBYTE pPeBaseAddress) {
for (SIZE_T i =0; i < pEntryImportDataDir->Size; i+=sizeof(IMAGE_IMPORT_DESCRIPTOR)) {
PIMAGE_IMPORT_DESCRIPTOR currentImport = (PIMAGE_IMPORT_DESCRIPTOR)(pPeBaseAddress + pEntryImportDataDir->VirtualAddress + i);
wprintf(L"%s\n", pPeBaseAddress + currentImport->Name);
if (currentImport-> FirstThunk == 0 && currentImport->OriginalFirstThunk == 0)
break
}
}
Alternatively we can use the common SectionFromRVA & Rva2Offset method which determines what section an RVA resides in by iterating through each section and calculating the difference.
Additional Undocumented Structures
Several undocumented structures can be accessed via the IMAGE_DATA_DIRECTORY
array in the optional header but are not documented in the Winnt.h header file.
IMAGE_TLS_DIRECTORY
- This structure is used to store information about Thread-Local Storage (TLS) data in the PE file.
PIMAGE_TLS_DIRECTORY pImgTlsDir = (PIMAGE_TLS_DIRECTORY)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress);
IMAGE_RUNTIME_FUNCTION_ENTRY
- This structure is used to store information about a runtime function in the PE file.
PIMAGE_RUNTIME_FUNCTION_ENTRY pImgRunFuncEntry = (PIMAGE_RUNTIME_FUNCTION_ENTRY)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress);
IMAGE_BASE_RELOCATION
- This structure is used to store information about the base relocations in the PE file.
PIMAGE_BASE_RELOCATION pImgBaseReloc = (PIMAGE_BASE_RELOCATION)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
PE Sections (IMPORTANT)
Structure of a PE Sections (.text
, .data
, .reloc
, .rsrc)
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;
IMPORTANT:** IMAGE_SECTION_HEADER Important Members**
Some of IMAGE_SECTION_HEADER's most important members;
Name
- A null-terminated ASCII string that specifies the name of the section.VirtualAddress
- The virtual address of the section in memory, this is anRVA
.SizeOfRawData
- The size of the section in the PE file in bytes.PointerToRelocations
- The file offset of the relocations for the section.NumberOfRelocations
- The number of relocations for the section.Characteristics
- Contains flags that specify the characteristics of the section.
Retrieving The IMAGE_SECTION_HEADER Structure
The IMAGE_SECTION_HEADER
structure is stored in an array within the PE file's headers. To access the first element, skip past the IMAGE_NT_HEADERS
since the sections are located immediately after the NT headers. The following snippet shows how to retrieve the IMAGE_SECTION_HEADER
structure, where pImgNtHdrs
is a pointer to IMAGE_NT_HEADERS
structure.
PIMAGE_SECTION_HEADER pImgSectionHdr = (PIMAGE_SECTION_HEADER)(((PBYTE)pImgNtHdrs) + sizeof(IMAGE_NT_HEADERS));
Looping Through The Array
Looping through the array requires the array size which can be retrieved from the IMAGE_FILE_HEADER.NumberOfSections
member. The subsequent elements in the array are located at an interval of sizeof(IMAGE_SECTION_HEADER)
from the current element.
PIMAGE_SECTION_HEADER pImgSectionHdr = (PIMAGE_SECTION_HEADER)(((PBYTE)pImgNtHdrs) + sizeof(IMAGE_NT_HEADERS));
for (size_t i = 0; i < pImgNtHdrs->FileHeader.NumberOfSections; i++) {
// pImgSectionHdr is a pointer to section 1
pImgSectionHdr = (PIMAGE_SECTION_HEADER)((PBYTE)pImgSectionHdr + (DWORD)sizeof(IMAGE_SECTION_HEADER));
// pImgSectionHdr is a pointer to section 2
}
Last updated