aidenpearce369
Published on

Windows x64 - PE Structure

Prologue

In our previous blog post, we learned how to craft a shellcode that invoked the MessageBoxA Win API. However, there was a limitation to the approach. We used hardcoded memory addresses, making our shellcode rigid and less adaptable to different environments and machines. To truly master the art of shellcoding, we need to adapt and overcome the challenges posed by dynamically changing addresses. Dynamic addressing, as the name suggests, is the art of crafting shellcodes that can adapt to unpredictable memory locations by resolving the required addresses from the executable itself. For that we need to understand the structure of PE files.

Portable Executable (PE)

A PE (Portable Executable) file in the Windows operating system is a file format used for executable and object code. It serves as the standard file format for executable files, libraries, and other binary files in Windows. The PE file format forms the foundation of Windows executables and plays a crucial role in the functioning of the Windows operating system.

To be a better red teamer, you should understand the PE structure and it's internals to craft a stealthy payload/malware. Everything which can be executed in Windows revolves around with this PE structure. This executable is CPU independent, which means it can work smoothly under Intel, ARM and other processors.

To be precise, not only .exe files come under PE structure. Files like .dll Dynamic Link Library, .srv Kernel Modules and other file formats also follow PE structure.

This is the simple structure of the PE file format.

1. DOS Header: The DOS Header is a small data structure that appears at the beginning of every PE file. This header is a relic from the MS-DOS era used for backward compatibility. The DOS Header contains information such as the initial jump instruction to the start of the executable code, a pointer to the DOS Program (stub), and additional fields. The DOS Header is primarily included for historical reasons, as modern Windows operating systems no longer rely on it for execution.

2. DOS Stub: The DOS Stub is a small program included in the DOS Header. In the past, it was executed when a user attempted to run a program designed for MS-DOS. This stub program often displayed a message or performed other simple tasks. In contemporary Windows systems, the DOS Stub serves no functional purpose and is generally ignored. It is retained for backward compatibility to print the message This program cannot be run in DOS mode when ran in MS-DOS mode and needed to be executed in a Windows environment..

3. PE Header: The PE Header is also known as NT Header (New Technology Header) immediately follows the DOS Header & DOS Stub in a PE file. It is also known as the PE Signature. The NT Header is a crucial part of the PE file structure. It contains metadata and pointers to various important data structures within the file, allowing the Windows loader to understand and load the file properly. This header consists of PE Signature, File Header and Optional Headers.

4. Section Table: The Section Table is a data structure within the PE file that provides information about the various sections or segments of the executable. Each section typically represents a portion of the file, such as code, data, resources, or imports. For each section, the Section Table contains information like the name, size, offset within the file, memory address, characteristics, and more. Sections are essential for organizing and protecting different parts of the executable. They help the Windows loader load and map the necessary components into memory.

5. Sections: Sections in the PE file are distinct segments that hold specific types of data or code. Common sections in a PE file include the .text section (for code), .data section (for initialized data), .rsrc section (for resources), and .idata section (for import information) and others. Sections provide a structured way to organize the content of the executable. They are individually mapped into memory when the program is executed. Sections can also be protected with attributes like read-only, read-write, or executable, allowing for fine-grained control over memory protection.

The PE file format in Windows comprises multiple components, with the DOS Header and DOS Stub serving historical purposes, the NT Header facilitating the loading and execution of the file, the Section Table organizing the sections, and the Sections containing specific types of data and code. These components work together to define the structure and functionality of PE files, making them executable in the Windows operating system.

Let's write a simple code and analyze it's PE structure.

#include <stdio.h>

int main() {
	printf("I'm a simple program");
	return 0;
}

The file format can be verified using a simple file command from unix.

ubuntu-wsl@ra:/mnt/e/WinAPI/PE-Struct$ file SimpleProgram.exe
SimpleProgram.exe: PE32+ executable (console) x86-64, for MS Windows

Let's use PE-Bear to analyze the PE structure.

PE-Structure-SimpleProgram

We can see that PE structure consists of the above discussed components.

The above shown strucutre holds key details in order to make the executable work. Everything is linked together and cannot be directly accessed from the top layer. To get past the section table, the code flow should begin from the DOS Header.

For more on PE format visit the official Microsoft documentation

DOS Header

Let's analyze the structure of DOS Header.

	
struct IMAGE_DOS_HEADER
typedef struct _IMAGE_DOS_HEADER
{
     WORD e_magic;
     WORD e_cblp;
     WORD e_cp;
     WORD e_crlc;
     WORD e_cparhdr;
     WORD e_minalloc;
     WORD e_maxalloc;
     WORD e_ss;
     WORD e_sp;
     WORD e_csum;
     WORD e_ip;
     WORD e_cs;
     WORD e_lfarlc;
     WORD e_ovno;
     WORD e_res[4];
     WORD e_oemid;
     WORD e_oeminfo;
     WORD e_res2[10];
     LONG e_lfanew;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

The whole _IMAGE_DOS_HEADER structure becomes important when the PE is being ran in MS-DOS environments, but when we run it in Windows environment there are two fields which we need to keep our eye on. e_magic and e_lfanew plays a crucial role in execution of the PE in modern Windows systems. e_magic is a WORD that stores 2 bytes known as magic bytes with the value of 0x5A4D (HEX) / MX (ASCII) which validates the file signature for MS-DOS executable. e_lfanew is an extension for modern Windows NT loaders, which can be used to provide required information. e_lfanew known as File address of new exe header stores the 4 byte address of PE/NT Header in _IMAGE_NT_HEADERS.

PE-DOS-Header

PE-DOS-Header

DOS Stub

DOS Stub just prints the message This program cannot be run in DOS mode when it is ran in Windows loader.

PE-DOS-Header

The message can also be customized during the compilation. The default message will be displayed from a default executable code. It can be handled, if we provide a target MS-DOS executable with /STUB: flag during compilation. For detailed steps, kindly visit this documentation

Also this blog explains it in details, https://osandamalith.com/2020/07/19/exploring-the-ms-dos-stub/.

PE Header

PE/NT Header consists of three components: PE Signature, File Header and Optional Header. This PE/NT header also follows certain strcuture to store these information. The structure definintion of the PE/NT header differ based on the PE32/PE64 format. But lets see it in the generic way.

typedef struct _IMAGE_NT_HEADERS
{
     ULONG Signature;
     IMAGE_FILE_HEADER FileHeader;
     IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;

For more visit the documentation

The Signature holds the value for PE Signature. By default the value will be predefined as a 4 byte value (0x50450000) of PE\0\0.

PE-Signature

These signatures are defined within the winnt.h file.

#define IMAGE_DOS_SIGNATURE                 0x5A4D      // MZ
#define IMAGE_OS2_SIGNATURE                 0x454E      // NE
#define IMAGE_OS2_SIGNATURE_LE              0x454C      // LE
#define IMAGE_VXD_SIGNATURE                 0x454C      // LE
#define IMAGE_NT_SIGNATURE                  0x00004550  // PE00

The File Header follows the structure of IMAGE_FILE_HEADER as shown below.

typedef struct _IMAGE_FILE_HEADER {
  WORD  Machine;
  WORD  NumberOfSections;
  DWORD TimeDateStamp;
  DWORD PointerToSymbolTable;
  DWORD NumberOfSymbols;
  WORD  SizeOfOptionalHeader;
  WORD  Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

For more visit the documentation

The File Header is also known as COFF(Common Object File Format) Header with seven fields in it.

1. Machine: A 2 byte WORD used to store information about the target machine type for which the PE is built for. To know more about the supported machine type, visit this page.

2. NumberOfSections: A 2 byte WORD to store the size of the section table, which immediately follows the headers.

3. TimeDateStamp: The low 32 bits of the number of seconds since 00:00 January 1, 1970 (a C run-time time_t value), which indicates when the file was created.

4. PointerToSymbolTable: The file offset of the COFF symbol table, or zero if no COFF symbol table is present. This value should be zero for an image because COFF debugging information is deprecated.

5. NumberOfSymbols: The number of entries in the symbol table. This data can be used to locate the string table, which immediately follows the symbol table. This value should be zero for an image because COFF debugging information is deprecated.

6. SizeOfOptionalHeader: The size of the optional header, which is required for executable files but not for object files. This value should be zero for an object file.

7. Characteristics: The flags that indicate the attributes of the file. For more on characteristics value, visit this page.

PE-NT-File-Header

The Optional Header follows the structure of IMAGE_OPTIONAL_HEADER as shown below

typedef struct _IMAGE_OPTIONAL_HEADER {
  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_HEADER, *PIMAGE_OPTIONAL_HEADER;

Let's disuss about few important fields,

PE-Optional-Header

1. BaseOfCode: A pointer to the beginning of the code section, relative to the image base.

2. AddressOfEntryPoint: A pointer to the entry point function, relative to the image base address. For executable files, this is the starting address. For device drivers, this is the address of the initialization function. The entry point function is optional for DLLs. When no entry point is present, this member is zero.

3. Subsystem: The subsystem required to run this image.

4. DataDirectory: The data directory serves as a guide to locate essential components of executable information within the file. Essentially, it consists of an array of IMAGE_DATA_DIRECTORY structures, situated at the conclusion of the optional header structure. IMAGE_NUMBEROF_DIRECTORY_ENTRIES is a constant defined with the value 16, meaning that this array can have up to 16 IMAGE_DATA_DIRECTORY entries.

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

DataEntry

There are few standard Directory Entries defined in winnt.h,

// Directory Entries

#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
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
#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

Every data directory entry provides information about its size and relative virtual address within the directory. To locate a specific directory, you calculate its relative address by referring to the data directory array within the optional header. Next, you utilize the virtual address to identify the section housing the directory. Once you identify the section containing the directory, you can then utilize the section header for that section to pinpoint the precise file offset location of the data directory.

For more detailed info on IMAGE_OPTIONAL_HEADER, visit the documentation

Section Header

Sections are the important data from an executable. This will be present after the section headers and covers the whole PE after it. For more on sections, visit this page.

Some of the commonly used sections are,

  • .text: Stores the program's executable code.
  • .data: Holds initialized data.
  • .bss: Contains uninitialized data.
  • .rdata: Houses read-only initialized data.
  • .edata: Contains export tables.
  • .idata: Stores import tables.
  • .reloc: Preserves image relocation information.
  • .rsrc: Safeguards program resources, including images, icons, or embedded binaries.
  • .tls: Serves as storage for each executing thread of the program, implementing Thread Local Storage.

In order to map and load these sections correctly into a loader from a PE, we need a header to store the required information. That's where Section Header comes into play.

The strucuture of Section Header in the format of IMAGE_SECTION_HEADER is,

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;

Section segments must be found by their name. That is why the PE stores its name along with its RVA in this header.

PE-Sections

For more detailed information, visit this page

References

PE File Format

PE 101 Detailed Image