Basic 64bit PE headers

The Portable Executable (PE) format is a type of file format utilized for executables, object code, DLLs, and other files within both 32-bit and 64-bit Windows operating systems. The PE format serves as a data structure that contains essential information required by the Windows OS loader to oversee the enclosed executable code.

Important headers

The PE format is a complex executable format. For the brevity of this article, we will only cover the essential aspects needed for unpacking. There are a few differences between the 32-bit and 64-bit formats. In this article, we will describe the 64-bit format. Most of the relevant structures are mentioned in the winnt.h header file

The PE format starts with the DOS header. The first field is the 0x4D5A signature, and the pointer to the PEHeader leads us to the base of the FileHeader within a PE header.

which is encapsulated in

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

IMAGE_OPTIONAL_HEADER64 Fields

IMAGE_OPTIONAL_HEADER64.Machine can be used to determine the machine the binary is supposed to run on. IMAGE_OPTIONAL_HEADER64 has four fields of different sizes than the IMAGE_OPTIONAL_HEADER (32-bit):

  • ImageBase
  • SizeOfStackReserve
  • SizeOfStackCommit
  • SizeOfHeapReserve
  • SizeOfHeapCommit

StackReserve: Reserving memory refers to the process of setting aside a portion of the virtual address space for a specific application or process. This reserved memory does not necessarily correspond to actual physical RAM; instead, it establishes a region in the process's virtual address space that can be used for future memory allocations.

StackCommit: Committing memory is an explicit request by the application to make use of the previously reserved virtual address space. Once committed, the operating system ensures that the required physical memory is available for storing data. If there is not enough physical RAM available, the OS might use a combination of RAM and disk space (paging or swapping) to fulfill the commitment.

If values are not present, the Windows kernel sets the default values as 4000h and 0FFFFh, which is done in RtlCreateUserStack.

nt!RtlCreateUserStack+0x61:
fffff806`76e29231 498b4308        mov     rax,qword ptr [r11+8]
fffff806`76e29235 4885c0          test    rax,rax
fffff806`76e29238 0f8477010000    je      nt!RtlCreateUserStack+0x1e5 (fffff806`76e293b5)
fffff806`76e2923e 498b4b10        mov     rcx,qword ptr [r11+10h]
fffff806`76e29242 4885c9          test    rcx,rcx
fffff806`76e29245 0f846a010000    je      nt!RtlCreateUserStack+0x1e5 (fffff806`76e293b5)
fffff806`76e2924b ba00400000      mov     edx,4000h
fffff806`76e29250 4885c0          test    rax,rax

nt!RtlCreateUserStack+0xa1:
fffff806`76e29271 488d99ffff0000  lea     rbx,[rcx+0FFFFh]
fffff806`76e29278 4881e30000ffff  and     rbx,0FFFFFFFFFFFF0000h
fffff806`76e2927f 498b8518030000  mov     rax,qword ptr [r13+318h]
fffff806`76e29286 4889442448      mov     qword ptr [rsp+48h],rax
fffff806`76e2928b eb05            jmp     nt!RtlCreateUserStack+0xc2 (fffff806`76e29292)
fffff806`76e2928d e910010000      jmp     nt!RtlCreateUserStack+0x1d2 (fffff806`76e293a2)
fffff806`76e29292 4885c0          test    rax,rax
fffff806`76e29295 0f8598010000    jne     nt!RtlCreateUserStack+0x263 (fffff806`76e29433)

Following is an example in C to parse headers of a PE 64bit file

int parsePEHeader(const char* peFilePath) {
    FILE* file = fopen(peFilePath, "rb");
    if (file == NULL) {
        perror("Unable to open file");
        return 1;
    }

    IMAGE_DOS_HEADER dosHeader;
    IMAGE_NT_HEADERS64 ntHeaders;

    // Read the DOS header
    fread(&dosHeader, sizeof(IMAGE_DOS_HEADER), 1, file);

    // Check for the DOS signature
    if (dosHeader.e_magic != IMAGE_DOS_SIGNATURE) {
        fclose(file);
        fprintf(stderr, "Not a valid PE file (DOS header signature not found)\n");
        return 1;
    }

    // Seek to the offset of IMAGE_NT_HEADERS64
    fseek(file, dosHeader.e_lfanew, SEEK_SET);

    // Read the IMAGE_NT_HEADERS64 structure
    fread(&ntHeaders, sizeof(IMAGE_NT_HEADERS64), 1, file);

    // Check for the NT signature
    if (ntHeaders.Signature != IMAGE_NT_SIGNATURE) {
        fclose(file);
        fprintf(stderr, "Not a valid PE file (NT header signature not found)\n");
        return 1;
    }

    // Access the information from IMAGE_OPTIONAL_HEADER64
    printf("ImageBase: 0x%llX\n", ntHeaders.OptionalHeader.ImageBase);
    printf("SizeOfImage: %lu\n", ntHeaders.OptionalHeader.SizeOfImage);
    printf("SizeOfHeaders: %lu\n", ntHeaders.OptionalHeader.SizeOfStackReserve);
    printf("SizeOfCommit: %lu\n", ntHeaders.OptionalHeader.SizeOfStackCommit);

    fclose(file);
    return 0;
}

MalwareID