PE Format for Developers
Malware development is constant PE manipulation — you hollow processes, reflectively load DLLs, fix import tables, stomp headers, and inject into sections. Every one of those operations requires reading and writing the Portable Executable format at the byte level. This chapter builds the mental model you'll reach for every time you touch an executable file.
What a PE File Actually Is
A PE (Portable Executable) file is the binary format Windows uses for
executables (.exe), dynamic libraries (.dll),
kernel drivers (.sys), and several other file types. Every
one of them has the same structure. The format is decades old but still
exactly what ships on every Windows system today.
At the highest level, a PE file on disk is a map of what the Windows loader should put into virtual memory, and how to configure the process around it. The headers describe the layout. The sections contain the content. The data directories point to special tables — imports, exports, relocations, resources — that the loader processes to make the file executable.
ON DISK IN MEMORY (after loader maps it)
┌──────────────────────┐ ┌──────────────────────────────────┐
│ DOS Header (64 B) │ │ DOS Header │
│ MZ magic at 0x00 │ │ (sometimes stomped by malware) │
│ e_lfanew at 0x3C │─────────│─► offset to NT headers │
├──────────────────────┤ ├──────────────────────────────────┤
│ DOS Stub │ │ DOS Stub │
│ (tiny 16-bit prog) │ │ "This program cannot be run..." │
├──────────────────────┤ ├──────────────────────────────────┤
│ PE Signature │ │ PE Signature ("PE\0\0") │
│ "PE\0\0" │ ├──────────────────────────────────┤
├──────────────────────┤ │ File Header (COFF) │
│ File Header │ │ Optional Header │
│ Optional Header │ │ DataDirectory[16] │
│ DataDirectory[16] │ ├──────────────────────────────────┤
├──────────────────────┤ │ Section Headers │
│ Section Headers │ │ (one per section) │
├──────────────────────┤ ├──────────────────────────────────┤
│ .text (raw data) │ map │ .text mapped at VirtualAddress │
│ [file-aligned] │ ──────► │ [section-aligned, padded] │
├──────────────────────┤ ├──────────────────────────────────┤
│ .rdata (raw data) │ map │ .rdata mapped at VirtualAddress │
├──────────────────────┤ ──────► ├──────────────────────────────────┤
│ .data (raw data) │ map │ .data mapped at VirtualAddress │
└──────────────────────┘ ──────► └──────────────────────────────────┘
Key difference: on disk, sections are FileAlignment-aligned (usually 0x200).
In memory, sections are SectionAlignment-aligned (usually 0x1000 = 4KB page).
The loader maps each section from its file offset to its virtual address.
The Header Chain
Navigating a PE always starts from the very first byte of the file and
follows a fixed chain of offsets. In C, every step is a simple pointer
cast — the Windows SDK provides all the structure definitions in
<windows.h>.
DOS Header — IMAGE_DOS_HEADER
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
// dos->e_magic == 0x5A4D ('MZ' — Mark Zbikowski's initials)
// dos->e_lfanew == byte offset from base to the PE signature
The DOS header exists purely for backwards compatibility with MS-DOS.
The only field that matters for modern PE parsing is e_lfanew:
it holds the offset to the actual PE signature. Everything between the DOS
header and that offset is the DOS stub — the small 16-bit program that
prints "This program cannot be run in DOS mode." Most malware stomps the
MZ magic bytes and the DOS stub after loading into memory (Part 7), because
memory scanners search for these patterns to identify PE images.
NT Headers — IMAGE_NT_HEADERS64
IMAGE_NT_HEADERS64* nt = (IMAGE_NT_HEADERS64*)((BYTE*)base + dos->e_lfanew);
// nt->Signature == 0x00004550 ('PE\0\0')
// nt->FileHeader (IMAGE_FILE_HEADER)
// nt->OptionalHeader (IMAGE_OPTIONAL_HEADER64)
Field Size Meaning
──────────────────────────────────────────────────────────────────
Machine WORD Architecture: 0x8664 = x64, 0x014C = x86
NumberOfSections WORD How many sections follow the headers
TimeDateStamp DWORD Compile timestamp (often zeroed in malware)
SizeOfOptionalHeader WORD Size of the Optional Header (varies)
Characteristics WORD Flags: 0x0002 = executable file,
0x2000 = DLL, 0x0100 = 32-bit only
Field Size Meaning
──────────────────────────────────────────────────────────────────
Magic WORD 0x020B = PE32+ (64-bit)
0x010B = PE32 (32-bit)
AddressOfEntryPoint DWORD RVA of the entry point function
ImageBase ULONGLONG Preferred load address (usually
0x140000000 for EXE, 0x180000000 for DLL)
SectionAlignment DWORD Section alignment in memory (usually 0x1000)
FileAlignment DWORD Section alignment on disk (usually 0x200)
SizeOfImage DWORD Total size needed in virtual memory
SizeOfHeaders DWORD Combined size of all headers (file-aligned)
CheckSum DWORD Integrity checksum (only validated for drivers)
Subsystem WORD 2 = GUI, 3 = console, 1 = native (drivers)
DllCharacteristics WORD Flags: ASLR, DEP, CFG, etc.
DataDirectory[16] array 16 pointers to special tables (imports, exports, etc.)
Section Headers — IMAGE_SECTION_HEADER
Immediately after the Optional Header comes an array of section headers —
one per section, NumberOfSections entries total.
// First section header starts right after the NT headers
IMAGE_SECTION_HEADER* sections = (IMAGE_SECTION_HEADER*)(
(BYTE*)&nt->OptionalHeader + nt->FileHeader.SizeOfOptionalHeader
);
// sections[0] = first section (.text usually)
// sections[1] = second section (.rdata usually)
// ... up to sections[nt->FileHeader.NumberOfSections - 1]
Field Size Meaning
──────────────────────────────────────────────────────────────────
Name 8 bytes ASCII name (NOT null-terminated if exactly 8 chars)
Common names: .text, .data, .rdata, .bss, .rsrc, .reloc
VirtualSize DWORD Actual size of section content in memory
VirtualAddress DWORD RVA where this section maps in memory
SizeOfRawData DWORD Size of section on disk (file-aligned)
PointerToRawData DWORD File offset where section data begins on disk
Characteristics DWORD Permission flags:
0x20000000 = executable
0x40000000 = readable
0x80000000 = writable
Combine: code section = 0x60000020
data section = 0xC0000040
RVA vs VA vs File Offset — The Most Common Source of Confusion
Nearly every number inside a PE header is a Relative Virtual Address (RVA). This is an offset from the image base — not from the start of the file, and not an absolute memory address. Getting confused between these three coordinate systems is responsible for most PE parsing bugs.
RVA (Relative Virtual Address)
──────────────────────────────────────────────────────────────────
What it is: Offset from the start of the image when loaded in memory.
Example: AddressOfEntryPoint = 0x1000
Means: the entry point is 0x1000 bytes past the image base.
To get VA: VA = ImageBase + RVA
If image loaded at 0x140000000: entry at 0x140001000
VA (Virtual Address)
──────────────────────────────────────────────────────────────────
What it is: Absolute address in the process's virtual address space.
Used for: Pointers at runtime, setting thread context (RIP/EIP).
Note: The image may NOT be at its preferred ImageBase (ASLR).
Always compute VA from actual load address, not ImageBase.
File Offset
──────────────────────────────────────────────────────────────────
What it is: Byte offset from the very start of the PE file on disk.
Used for: Reading sections from disk (process hollowing, reflective load).
NOT the same as RVA — sections are laid out differently on disk vs memory.
Converting RVA → File Offset:
──────────────────────────────────────────────────────────────────
For each section header:
if (RVA >= section.VirtualAddress &&
RVA < section.VirtualAddress + section.SizeOfRawData):
FileOffset = RVA - section.VirtualAddress + section.PointerToRawData
Converting RVA → VA (at runtime, image loaded at actual_base):
──────────────────────────────────────────────────────────────────
VA = (BYTE*)actual_base + RVA
// RVA to file offset conversion — used when reading a PE from disk
DWORD rva_to_file_offset(IMAGE_NT_HEADERS64* nt, DWORD rva) {
IMAGE_SECTION_HEADER* sec = (IMAGE_SECTION_HEADER*)(
(BYTE*)&nt->OptionalHeader + nt->FileHeader.SizeOfOptionalHeader);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++) {
DWORD sec_start = sec[i].VirtualAddress;
DWORD sec_end = sec_start + sec[i].SizeOfRawData;
if (rva >= sec_start && rva < sec_end)
return rva - sec_start + sec[i].PointerToRawData;
}
return 0; // not found in any section — may be in the header area
}
// RVA to virtual address (runtime, image already mapped)
// base = actual load address (DllBase from LDR entry, or allocation base)
void* rva_to_va(void* base, DWORD rva) {
return (BYTE*)base + rva;
}
Data Directories
The Optional Header's DataDirectory array has 16 entries.
Each entry holds an RVA and a size pointing to a special table. You will
work with almost all of these at some point in this book:
Index Name When you use it
──────────────────────────────────────────────────────────────────────
0 IMAGE_DIRECTORY_ENTRY_EXPORT Ch03 (PEB walk), Ch06 (shellcode)
1 IMAGE_DIRECTORY_ENTRY_IMPORT Right now (fixing IAT in loaders)
2 IMAGE_DIRECTORY_ENTRY_RESOURCE Ch07 (PE resource spoofing)
3 IMAGE_DIRECTORY_ENTRY_EXCEPTION Stack unwinding (.pdata for x64)
4 IMAGE_DIRECTORY_ENTRY_SECURITY Authenticode signature (Ch07)
5 IMAGE_DIRECTORY_ENTRY_BASERELOC Relocation table (reflective loader)
6 IMAGE_DIRECTORY_ENTRY_DEBUG Debug info / PDB path (stomp this)
7 IMAGE_DIRECTORY_ENTRY_ARCHITECTURE (unused on x64)
8 IMAGE_DIRECTORY_ENTRY_GLOBALPTR (unused on x64)
9 IMAGE_DIRECTORY_ENTRY_TLS TLS callbacks (anti-debug tricks)
10 IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG CFG, SEH handler table, stack cookie
11 IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (legacy pre-binding, ignore)
12 IMAGE_DIRECTORY_ENTRY_IAT Import Address Table hint
13 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT Delay-loaded imports
14 IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR .NET CLR header (Ch16)
15 (reserved)
// Accessing a data directory
IMAGE_DATA_DIRECTORY* dir = &nt->OptionalHeader.DataDirectory[1]; // imports
if (dir->VirtualAddress == 0) {
// This directory doesn't exist in this PE
}
IMAGE_IMPORT_DESCRIPTOR* imports =
(IMAGE_IMPORT_DESCRIPTOR*)((BYTE*)base + dir->VirtualAddress);
The Import Table — IAT vs INT
The import directory (DataDirectory[1]) is the most important structure for shellcode and injection work. It describes every DLL the PE depends on and every function it imports. Understanding the difference between the Import Name Table (INT) and the Import Address Table (IAT) is critical — they look identical when the file is on disk but have very different roles at runtime.
IMAGE_IMPORT_DESCRIPTOR (one per imported DLL, null-terminated array)
┌──────────────────────────────────────────────────────────────┐
│ OriginalFirstThunk DWORD (RVA) → Import Name Table (INT) │
│ TimeDateStamp DWORD (0 before binding) │
│ ForwarderChain DWORD (ignore for now) │
│ Name DWORD (RVA) → "kernel32.dll\0" │
│ FirstThunk DWORD (RVA) → Import Address Table (IAT)│
└──────────────────────────────────────────────────────────────┘
Both INT and IAT point to arrays of IMAGE_THUNK_DATA64 (8-byte entries):
Before loading (on disk): After loading (in memory):
┌────────────────────────┐ ┌────────────────────────┐
│ INT[0] RVA→"VirtualAlloc\0"│ │ INT[0] (unchanged) │
│ INT[1] RVA→"VirtualFree\0" │ │ INT[1] (unchanged) │
│ INT[2] 0 (terminator) │ │ INT[2] 0 │
└────────────────────────┘ └────────────────────────┘
┌────────────────────────┐ ┌────────────────────────┐
│ IAT[0] RVA→"VirtualAlloc\0"│ ──► │ IAT[0] 0x7FFB1234ABCD│ ← actual addr
│ IAT[1] RVA→"VirtualFree\0" │ ──► │ IAT[1] 0x7FFB1234EF01│ ← actual addr
│ IAT[2] 0 (terminator) │ │ IAT[2] 0 │
└────────────────────────┘ └────────────────────────┘
The Windows loader overwrites the IAT entries with real addresses.
The INT stays unchanged — it's the original reference that tells the
loader what to look for.
At runtime, your call to VirtualAlloc goes:
CALL [IAT entry for VirtualAlloc] → 0x7FFB1234ABCD (actual VirtualAlloc)
Each IMAGE_THUNK_DATA64 entry is a union with two meanings.
If the high bit (bit 63 on x64) is set, the remaining 63 bits are an
ordinal number. If the high bit is clear, the value is an RVA
pointing to an IMAGE_IMPORT_BY_NAME structure containing
a hint (ordinal hint for fast lookup) followed by the function name string.
// Walking the import table — what a reflective loader does
void fix_imports(void* base) {
IMAGE_NT_HEADERS64* nt = get_nt_headers(base);
IMAGE_IMPORT_DESCRIPTOR* imp = (IMAGE_IMPORT_DESCRIPTOR*)(
(BYTE*)base + nt->OptionalHeader.DataDirectory[1].VirtualAddress);
// Walk each imported DLL (array ends with a null entry)
for (; imp->Name; imp++) {
char* dll_name = (char*)((BYTE*)base + imp->Name);
HMODULE dll = LoadLibraryA(dll_name);
if (!dll) continue;
// Walk the IAT entries for this DLL
IMAGE_THUNK_DATA64* iat = (IMAGE_THUNK_DATA64*)(
(BYTE*)base + imp->FirstThunk);
IMAGE_THUNK_DATA64* intt = (IMAGE_THUNK_DATA64*)(
(BYTE*)base + imp->OriginalFirstThunk);
for (; intt->u1.AddressOfData; iat++, intt++) {
FARPROC func;
if (IMAGE_SNAP_BY_ORDINAL64(intt->u1.Ordinal)) {
// Import by ordinal
func = GetProcAddress(dll,
(LPCSTR)(ULONG_PTR)IMAGE_ORDINAL64(intt->u1.Ordinal));
} else {
// Import by name
IMAGE_IMPORT_BY_NAME* ibn = (IMAGE_IMPORT_BY_NAME*)(
(BYTE*)base + intt->u1.AddressOfData);
func = GetProcAddress(dll, (LPCSTR)ibn->Name);
}
// Write the resolved address into the IAT
iat->u1.Function = (ULONGLONG)func;
}
}
}
This is the import-fixing loop at the core of every reflective DLL loader — the code that maps a PE into memory and manually does what the Windows loader would normally do. You will write variations of this function many times across Parts 3 and 4.
Relocations — When the Image Isn't Where It Expected to Be
Every PE has a preferred ImageBase — the address where it
assumes it will be loaded. All absolute addresses in the binary's code and
data are compiled with that assumption. If the image actually loads at a
different address (because ASLR moved it, or because you injected it
somewhere else), every one of those absolute references is wrong.
The relocation table (DataDirectory[5]) is the fix: it lists every location in the image that contains an absolute address that needs to be adjusted. The loader applies a delta (actual load address − preferred ImageBase) to each location.
// Apply relocations — required when mapping a PE to an arbitrary address
// actual_base: where you actually mapped the image
void apply_relocations(void* actual_base) {
IMAGE_NT_HEADERS64* nt = get_nt_headers(actual_base);
ULONGLONG delta = (ULONGLONG)actual_base - nt->OptionalHeader.ImageBase;
if (delta == 0) return; // loaded at preferred base, no fixup needed
IMAGE_DATA_DIRECTORY* reloc_dir =
&nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
if (!reloc_dir->VirtualAddress) return; // no relocation table
IMAGE_BASE_RELOCATION* reloc = (IMAGE_BASE_RELOCATION*)(
(BYTE*)actual_base + reloc_dir->VirtualAddress);
// Each block covers a 4KB page
while (reloc->VirtualAddress) {
DWORD count = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;
WORD* entries = (WORD*)((BYTE*)reloc + sizeof(IMAGE_BASE_RELOCATION));
for (DWORD i = 0; i < count; i++) {
WORD type = entries[i] >> 12; // top 4 bits = type
WORD offset = entries[i] & 0x0FFF; // low 12 bits = offset within page
if (type == IMAGE_REL_BASED_DIR64) { // type 10 = 64-bit address
ULONGLONG* ptr = (ULONGLONG*)(
(BYTE*)actual_base + reloc->VirtualAddress + offset);
*ptr += delta; // apply the delta
}
// type 0 = padding, skip
}
// Advance to the next relocation block
reloc = (IMAGE_BASE_RELOCATION*)((BYTE*)reloc + reloc->SizeOfBlock);
}
}
If a PE is compiled with
/FIXED (MSVC) or
-no-pie (GCC), it has no relocation table and
cannot load at any address other than its preferred
ImageBase. The loader will fail if that address is unavailable.
For injection, this means such a PE can only be used with process hollowing
at its exact preferred base — not reflective injection at an arbitrary address.
Most modern PE files are position-independent by default (ASLR-compatible)
and do have a relocation table.
A Complete PE Reader
Here is a utility that takes a PE loaded in memory and prints its key properties. This is the kind of diagnostic code you'll write in the early stages of every new tool to verify your parsing is correct:
// pe_info.c — print PE structure information (CRT-free, for x64)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
// We need printf for output here — link against CRT just for this diagnostic tool
#include <stdio.h>
void print_pe_info(void* base) {
BYTE* b = (BYTE*)base;
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)b;
IMAGE_NT_HEADERS64* nt = (IMAGE_NT_HEADERS64*)(b + dos->e_lfanew);
IMAGE_OPTIONAL_HEADER64* opt = &nt->OptionalHeader;
printf("=== PE Information ===\n");
printf("Architecture: %s\n",
nt->FileHeader.Machine == 0x8664 ? "x64" : "x86/other");
printf("Entry point RVA: 0x%08X\n", opt->AddressOfEntryPoint);
printf("ImageBase: 0x%016llX\n", opt->ImageBase);
printf("SizeOfImage: 0x%08X\n", opt->SizeOfImage);
printf("Sections: %d\n\n", nt->FileHeader.NumberOfSections);
// Print sections
IMAGE_SECTION_HEADER* sec = (IMAGE_SECTION_HEADER*)(
(BYTE*)opt + nt->FileHeader.SizeOfOptionalHeader);
printf("%-10s %-10s %-10s %-10s %s\n",
"Name", "VirtAddr", "VirtSize", "FileOff", "Flags");
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++) {
char name[9] = {0};
for (int j = 0; j < 8 && sec[i].Name[j]; j++) name[j] = sec[i].Name[j];
printf("%-10s 0x%08X 0x%08X 0x%08X 0x%08X\n",
name, sec[i].VirtualAddress, sec[i].Misc.VirtualSize,
sec[i].PointerToRawData, sec[i].Characteristics);
}
// Print data directories
printf("\n=== Data Directories ===\n");
const char* dir_names[] = {
"Export", "Import", "Resource", "Exception",
"Security", "BaseReloc", "Debug", "Architecture",
"GlobalPtr", "TLS", "LoadConfig", "BoundImport",
"IAT", "DelayImport", "CLR Header", "Reserved"
};
for (int i = 0; i < 16; i++) {
IMAGE_DATA_DIRECTORY* d = &opt->DataDirectory[i];
if (d->VirtualAddress)
printf(" [%2d] %-14s RVA=0x%08X Size=0x%X\n",
i, dir_names[i], d->VirtualAddress, d->Size);
}
// Print imports
if (opt->DataDirectory[1].VirtualAddress) {
printf("\n=== Imports ===\n");
IMAGE_IMPORT_DESCRIPTOR* imp = (IMAGE_IMPORT_DESCRIPTOR*)(
b + opt->DataDirectory[1].VirtualAddress);
for (; imp->Name; imp++) {
printf(" %s\n", b + imp->Name);
IMAGE_THUNK_DATA64* thunk = (IMAGE_THUNK_DATA64*)(
b + imp->OriginalFirstThunk);
for (; thunk->u1.AddressOfData; thunk++) {
if (!IMAGE_SNAP_BY_ORDINAL64(thunk->u1.Ordinal)) {
IMAGE_IMPORT_BY_NAME* ibn = (IMAGE_IMPORT_BY_NAME*)(
b + thunk->u1.AddressOfData);
printf(" %s\n", ibn->Name);
} else {
printf(" #%llu (ordinal)\n",
IMAGE_ORDINAL64(thunk->u1.Ordinal));
}
}
}
}
}
int main(void) {
// Print info about ourselves
print_pe_info(GetModuleHandleA(NULL));
return 0;
}
Run this and compare the output against PE-bear on the same binary. Every section, data directory, and import should match exactly. This validation step — writing your own parser and comparing against a trusted tool — is how you build confidence in PE manipulation code before you rely on it inside an injector or reflective loader.
Key Offsets to Memorize
When debugging PE manipulation code in x64dbg, you'll be reading raw memory at specific offsets constantly. These are the offsets you'll reach for most often:
From base (DOS header start):
┌──────────────────────────────────────────────────────────────┐
│ base+0x00 e_magic (0x5A4D = 'MZ') │
│ base+0x3C e_lfanew (offset to PE signature) │
│ base+[e_lfanew]+0x00 PE signature (0x4550) │
│ base+[e_lfanew]+0x04 Machine (0x8664 = x64) │
│ base+[e_lfanew]+0x06 NumberOfSections │
│ base+[e_lfanew]+0x14 SizeOfOptionalHeader │
│ base+[e_lfanew]+0x18 Optional Header start │
│ base+[e_lfanew]+0x18+0x10 AddressOfEntryPoint (RVA) │
│ base+[e_lfanew]+0x18+0x18 ImageBase │
│ base+[e_lfanew]+0x18+0x70 DataDirectory[0] (Export) │
│ base+[e_lfanew]+0x18+0x78 DataDirectory[1] (Import) │
│ base+[e_lfanew]+0x18+0x98 DataDirectory[5] (BaseReloc) │
│ base+[e_lfanew]+0x18+0xA0 DataDirectory[6] (Debug) │
└──────────────────────────────────────────────────────────────┘
From PEB (via GS:[0x60]):
┌──────────────────────────────────────────────────────────────┐
│ PEB+0x02 BeingDebugged flag │
│ PEB+0x10 ImageBaseAddress (base of the EXE) │
│ PEB+0x18 Ldr pointer → PEB_LDR_DATA │
│ PEB+0x20 ProcessParameters │
│ PEB+0x68 NtGlobalFlag (used in anti-debug checks) │
└──────────────────────────────────────────────────────────────┘
Questions & Answers
Why does a PE file start with the letters MZ?
MZ are the initials of Mark Zbikowski, one of the original designers of
the MS-DOS executable format in the early 1980s. The PE format is a
superset of the older MZ format — it kept the same header magic so that
tools written for MS-DOS executables could at least recognize the file type
even if they couldn't run it. The full DOS stub (the "cannot be run in DOS
mode" program) is a legitimate 16-bit DOS program that prints that message
and exits. Malware developers sometimes replace it with arbitrary data,
but PE parsers don't care what's there as long as e_lfanew
points correctly to the PE signature.
Why are there two tables (INT and IAT) instead of just one?
The INT (OriginalFirstThunk) is read-only — the loader never modifies it. It's the record of what function was originally requested. The IAT (FirstThunk) starts as a copy of the INT but gets overwritten by the loader with real addresses. Having both means: (1) you can always go back to the original import names even after the loader has overwritten the IAT, and (2) security tools can compare the current IAT values against what the INT says they should point to — a mismatch indicates IAT hooking (a technique EDRs use, and a technique you'll defeat in Part 5). When you write a reflective loader, you must rebuild the IAT; the INT is your source of truth for what to look up.
What happens if I try to inject a PE that has no relocation table?
If the PE was compiled with /FIXED and has no relocation table,
and you try to map it to any address other than its preferred
ImageBase, any absolute addresses in its code will be wrong.
Calls to global variables, indirect function calls through pointer tables,
and any hardcoded addresses will read from or write to the wrong locations —
usually causing an access violation or silent data corruption. The fix is
either: (1) map the PE at exactly its preferred ImageBase (use process
hollowing and allocate at that address), or (2) don't use such PEs for
injection — recompile with ASLR support (/DYNAMICBASE), which
generates a relocation table. Modern compilers generate relocations by default.
What is the SizeOfImage and why does it matter for injection?
SizeOfImage in the Optional Header is the total size of the
image as it should appear in virtual memory — all headers and all sections
combined, rounded up to SectionAlignment. When you manually
map a PE into memory (reflective loading, process hollowing), you must
allocate at least SizeOfImage bytes. If you allocate less,
the later sections won't fit and you'll write out of bounds. In practice,
always use VirtualAllocEx with SizeOfImage as the
size, starting at the preferred ImageBase (or whatever address
you choose), before copying the headers and sections in.