A complete reference for the Windows Portable Executable (PE) file format. Covers every header, section, data directory, address translation, and tooling used in reverse engineering, malware analysis, and exploit development. Applies to PE32 (x86) and PE32+ (x64) binaries.
1. Core Concepts & Layout
PE FILE ON DISK
──────────────────────────────────────────────────────────────────────────────
Offset 0x00
┌──────────────────────────────────────────────────────────────┐
│ DOS Header │
│ • 64 bytes │
│ • Starts with "MZ" │
├──────────────────────────────────────────────────────────────┤
│ DOS Stub │
│ • "This program cannot be run in DOS mode" │
├──────────────────────────────────────────────────────────────┤
│ Rich Header (optional) │
│ • Undocumented Microsoft compiler metadata │
├──────────────────────────────────────────────────────────────┤
│ NT Headers │
│ • Signature ("PE\0\0") │
│ • IMAGE_FILE_HEADER │
│ • IMAGE_OPTIONAL_HEADER │
├──────────────────────────────────────────────────────────────┤
│ Section Headers │
│ • Array of IMAGE_SECTION_HEADER structures │
├──────────────────────────────────────────────────────────────┤
│ Section Data (Raw File Data) │
│ • .text │
│ • .data │
│ • .rdata │
│ • .rsrc │
│ • .reloc │
│ • ... │
└──────────────────────────────────────────────────────────────┘
^
|
e_lfanew
PE FILE IN MEMORY (After Windows Loader Maps It)
──────────────────────────────────────────────────────────────────────────────
ImageBase
┌──────────────────────────────────────────────────────────────┐
│ DOS Header │
├──────────────────────────────────────────────────────────────┤
│ NT Headers │
├──────────────────────────────────────────────────────────────┤
│ Section Headers │
├──────────────────────────────────────────────────────────────┤
│ .text (Executable Code) │
│ Aligned to SectionAlignment │
├──────────────────────────────────────────────────────────────┤
│ .rdata (Read-Only Data) │
├──────────────────────────────────────────────────────────────┤
│ .data (Writable Data) │
├──────────────────────────────────────────────────────────────┤
│ .rsrc (Resources) │
├──────────────────────────────────────────────────────────────┤
│ .reloc (Base Relocation Table, if present) │
└──────────────────────────────────────────────────────────────┘
Key Terms:
- RVA — Relative Virtual Address — offset from ImageBase in memory.
- VA — Virtual Address — absolute memory address = ImageBase + RVA.
- File Offset — raw byte offset from start of file on disk.
- ImageBase — preferred load address in memory (default 0x400000 for EXE, 0x10000000 for DLL).
- SectionAlignment — alignment of sections in memory (usually 0x1000 = 4 KB).
- FileAlignment — alignment of raw section data on disk (usually 0x200 = 512 bytes).
- PE32 — 32-bit PE (x86). Optional Header magic = 0x010B.
- PE32+ — 64-bit PE (x64). Optional Header magic = 0x020B. No 32-bit BaseOfData field.
2. DOS Header & DOS Stub
Structure: IMAGE_DOS_HEADER (64 bytes)
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // 0x00 — Magic number: 0x5A4D ("MZ")
WORD e_cblp; // 0x02 — Bytes on last page of file
WORD e_cp; // 0x04 — Pages in file
WORD e_crlc; // 0x06 — Relocations
WORD e_cparhdr; // 0x08 — Size of header in paragraphs
WORD e_minalloc; // 0x0A — Minimum extra paragraphs needed
WORD e_maxalloc; // 0x0C — Maximum extra paragraphs needed
WORD e_ss; // 0x0E — Initial (relative) SS value
WORD e_sp; // 0x10 — Initial SP value
WORD e_csum; // 0x12 — Checksum
WORD e_ip; // 0x14 — Initial IP value
WORD e_cs; // 0x16 — Initial (relative) CS value
WORD e_lfarlc; // 0x18 — File address of relocation table
WORD e_ovno; // 0x1A — Overlay number
WORD e_res; // 0x1C — Reserved words[1]
WORD e_oemid; // 0x24 — OEM identifier
WORD e_oeminfo; // 0x26 — OEM information
WORD e_res2; // 0x28 — Reserved words[2]
LONG e_lfanew; // 0x3C — ★ File offset of PE Header (NT Headers)
} IMAGE_DOS_HEADER;
Critical fields:
- e_magic = 0x5A4D (MZ) — first 2 bytes of every PE file, always.
- e_lfanew = file offset to IMAGE_NT_HEADERS — the only truly important field.
DOS Stub
Bytes immediately after the DOS Header.
A tiny 16-bit DOS program that prints:
"This program cannot be run in DOS mode."
then exits.
Purpose: backward compatibility with MS-DOS.
In practice: usually ignored entirely.
In malware: sometimes replaced with custom content or zeroed out.
Read in Python (pefile)
import pefile
pe = pefile.PE("sample.exe")
print(hex(pe.DOS_HEADER.e_magic)) # 0x5a4d
print(hex(pe.DOS_HEADER.e_lfanew)) # e.g. 0xf0
3. Rich Header
WHAT IT IS:
Undocumented structure inserted by Microsoft linker between
the DOS Stub and the NT Headers.
Contains metadata about build tools used:
→ Visual Studio version
→ Compiler version
→ Number of objects compiled with each tool
→ Product IDs (MASM, LINK, C compiler, etc.)
Structure:
[DanS marker (XOR'd)] + [tool entries] + [Rich marker] + [XOR key]
The entire header is XOR'd with a 32-bit key (found after "Rich" marker).
To decode: XOR all DWORD values with the key after "Rich".
# Parse Rich Header with pefile
import pefile
pe = pefile.PE("sample.exe")
if hasattr(pe, 'RICH_HEADER'):
print(pe.RICH_HEADER)
for entry in pe.RICH_HEADER.values:
print(f" ProductID: {entry:#06x} Build: {entry} Count: {entry}")[3][4]
Uses in malware analysis: - Fingerprint threat actor toolchains across samples. - Detect mismatches (Rich Header says VS2010 but code uses Win10 APIs → suspicious). - Some malware zeroes out or corrupts the Rich Header to avoid fingerprinting.
4. NT Headers
Structure: IMAGE_NT_HEADERS
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // 0x00 — "PE\0\0" = 0x00004550
IMAGE_FILE_HEADER FileHeader; // 0x04 — COFF File Header (20 bytes)
IMAGE_OPTIONAL_HEADER OptionalHeader; // 0x18 — Optional Header (96/112+ bytes)
} IMAGE_NT_HEADERS;
- Located at
e_lfanewoffset from start of file. Signaturemust be0x50450000(PE\0\0) — validates it is a PE file.- Followed directly by
IMAGE_FILE_HEADER(COFF header).
print(hex(pe.NT_HEADERS.Signature)) # 0x4550 (PE)
print(pe.FILE_HEADER.Machine) # 0x14c = x86, 0x8664 = x64
print(hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint))
5. File Header (COFF)
Structure: IMAGE_FILE_HEADER (20 bytes)
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 0x00 — Target CPU architecture
WORD NumberOfSections; // 0x02 — Number of section headers
DWORD TimeDateStamp; // 0x04 — Unix timestamp of link time
DWORD PointerToSymbolTable; // 0x08 — Offset to COFF symbol table (usually 0)
DWORD NumberOfSymbols; // 0x0C — Number of symbols (usually 0)
WORD SizeOfOptionalHeader; // 0x10 — Size of Optional Header
WORD Characteristics; // 0x12 — Flags describing the file
} IMAGE_FILE_HEADER;
Machine Values
| Value | Architecture |
|---|---|
0x014C |
x86 (i386) |
0x8664 |
x64 (AMD64) |
0x01C4 |
ARM (Thumb-2) |
0xAA64 |
ARM64 |
0x0200 |
Intel Itanium |
Characteristics Flags
Bit 0x0002 IMAGE_FILE_EXECUTABLE_IMAGE — Is a valid executable
Bit 0x0020 IMAGE_FILE_LARGE_ADDRESS_AWARE — Can use > 2 GB address space
Bit 0x0100 IMAGE_FILE_32BIT_MACHINE — 32-bit machine
Bit 0x2000 IMAGE_FILE_DLL — File is a DLL
Bit 0x0200 IMAGE_FILE_DEBUG_STRIPPED — Debug info stripped
print(pe.FILE_HEADER.NumberOfSections) # e.g. 5
print(pe.FILE_HEADER.TimeDateStamp) # Unix timestamp
import datetime
ts = datetime.datetime.utcfromtimestamp(pe.FILE_HEADER.TimeDateStamp)
print(ts) # e.g. 2019-06-13 12:34:56
# Malware often zeros or fakes this timestamp
6. Optional Header
Called "optional" historically — it is mandatory for executables.
Structure Overview (PE32)
typedef struct _IMAGE_OPTIONAL_HEADER {
// ── Standard Fields ──────────────────────────────────────────
WORD Magic; // 0x000 — 0x010B = PE32, 0x020B = PE32+
BYTE MajorLinkerVersion; // 0x002
BYTE MinorLinkerVersion; // 0x003
DWORD SizeOfCode; // 0x004 — Total size of code sections
DWORD SizeOfInitializedData; // 0x008
DWORD SizeOfUninitializedData; // 0x00C
DWORD AddressOfEntryPoint; // 0x010 — ★ RVA of entry point (OEP)
DWORD BaseOfCode; // 0x014 — RVA of start of .text
DWORD BaseOfData; // 0x018 — RVA of start of .data (PE32 only)
// ── Windows-Specific Fields ──────────────────────────────────
DWORD ImageBase; // 0x01C — ★ Preferred load VA (0x400000 default)
DWORD SectionAlignment; // 0x020 — Section alignment in memory (0x1000)
DWORD FileAlignment; // 0x024 — Raw section alignment on disk (0x200)
WORD MajorOSVersion; // 0x028
WORD MinorOSVersion; // 0x02A
WORD MajorImageVersion; // 0x02C
WORD MinorImageVersion; // 0x02E
WORD MajorSubsystemVersion; // 0x030
WORD MinorSubsystemVersion; // 0x032
DWORD Win32VersionValue; // 0x034 — Reserved, must be 0
DWORD SizeOfImage; // 0x038 — ★ Total size of image in memory
DWORD SizeOfHeaders; // 0x03C — Size of all headers + section table
DWORD CheckSum; // 0x040 — CRC checksum (required for drivers)
WORD Subsystem; // 0x044 — Windows / Console / Native etc.
WORD DllCharacteristics; // 0x046 — Security flags (ASLR, NX, CFG etc.)
DWORD SizeOfStackReserve; // 0x048
DWORD SizeOfStackCommit; // 0x04C
DWORD SizeOfHeapReserve; // 0x050
DWORD SizeOfHeapCommit; // 0x054
DWORD LoaderFlags; // 0x058 — Obsolete
DWORD NumberOfRvaAndSizes; // 0x05C — Number of Data Directory entries (16)
// ── Data Directory ───────────────────────────────────────────
IMAGE_DATA_DIRECTORY DataDirectory; // 0x060[5]
} IMAGE_OPTIONAL_HEADER32;
Key Fields
AddressOfEntryPoint (OEP):
→ RVA of first instruction executed
→ In EXE: main/WinMain stub
→ In DLL: DllMain (or 0 if none)
→ Packers redirect this to unpacking stub
ImageBase:
→ Preferred load address
→ Loader uses this if address is free; otherwise ASLR relocates
→ EXE default: 0x400000
→ DLL default: 0x10000000
SizeOfImage:
→ Must be multiple of SectionAlignment
→ Entire memory footprint of loaded PE
SectionAlignment vs FileAlignment:
→ SectionAlignment: how sections align in memory (usually 0x1000)
→ FileAlignment: how sections align on disk (usually 0x200)
→ Sections are "padded" on disk to FileAlignment
→ Then "expanded" in memory to SectionAlignment
Subsystem Values
| Value | Meaning |
|---|---|
1 |
Native (drivers, NT internals) |
2 |
Windows GUI application |
3 |
Windows Console (CUI) |
9 |
WinCE GUI |
10 |
EFI Application |
14 |
Xbox |
DllCharacteristics Flags
0x0020 HIGH_ENTROPY_VA — 64-bit ASLR
0x0040 DYNAMIC_BASE — ASLR enabled
0x0080 FORCE_INTEGRITY — Code integrity check required
0x0100 NX_COMPAT — NX (DEP) compatible
0x0200 NO_ISOLATION — No SxS manifest isolation
0x0400 NO_SEH — No SEH handlers (use SafeSEH)
0x0800 NO_BIND — Do not bind imports
0x1000 APPCONTAINER — Must run in AppContainer
0x2000 WDM_DRIVER — WDM kernel driver
0x4000 GUARD_CF — Control Flow Guard enabled
0x8000 TERMINAL_SERVER_AWARE
print(hex(pe.OPTIONAL_HEADER.Magic)) # 0x10b or 0x20b
print(hex(pe.OPTIONAL_HEADER.ImageBase)) # e.g. 0x400000
print(hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint)) # OEP RVA
print(pe.OPTIONAL_HEADER.Subsystem) # 2=GUI, 3=Console
print(hex(pe.OPTIONAL_HEADER.DllCharacteristics)) # security flags
7. Data Directories
WHAT THEY ARE:
Array of 16 IMAGE_DATA_DIRECTORY structures at the end of the
Optional Header. Each entry is:
{ DWORD VirtualAddress; // RVA of the structure
DWORD Size; // Size in bytes }
They tell the loader WHERE important tables are in memory.
All 16 Data Directory Entries
| Index | Name | Description |
|---|---|---|
0 |
IMAGE_DIRECTORY_ENTRY_EXPORT |
Export table — functions DLL exposes |
1 |
IMAGE_DIRECTORY_ENTRY_IMPORT |
Import table — DLLs and functions used |
2 |
IMAGE_DIRECTORY_ENTRY_RESOURCE |
Resource table — icons, strings, dialogs |
3 |
IMAGE_DIRECTORY_ENTRY_EXCEPTION |
Exception table — structured exception info |
4 |
IMAGE_DIRECTORY_ENTRY_SECURITY |
Certificate / signature data (Authenticode) |
5 |
IMAGE_DIRECTORY_ENTRY_BASERELOC |
Base relocation table (.reloc section) |
6 |
IMAGE_DIRECTORY_ENTRY_DEBUG |
Debug info (PDB path, timestamps) |
7 |
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE |
CPU-specific data |
8 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR |
RVA of Global Pointer register value |
9 |
IMAGE_DIRECTORY_ENTRY_TLS |
Thread Local Storage table |
10 |
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG |
Load configuration (SafeSEH, CFG, ASLR) |
11 |
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT |
Bound import data |
12 |
IMAGE_DIRECTORY_ENTRY_IAT |
Import Address Table |
13 |
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT |
Delay-loaded imports |
14 |
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR |
.NET / CLR runtime header |
15 |
Reserved | Must be zero |
# Iterate all data directories
for entry in pe.OPTIONAL_HEADER.DATA_DIRECTORY:
if entry.VirtualAddress:
print(f" {entry.name:<50} RVA={entry.VirtualAddress:#010x} Size={entry.Size:#010x}")
8. Section Headers
Structure: IMAGE_SECTION_HEADER (40 bytes each)
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name; // 0x00 — Section name (null-padded, NOT null-terminated)[6]
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // 0x08 — ★ Actual size of data in memory (before padding)
} Misc;
DWORD VirtualAddress; // 0x0C — ★ RVA in memory where section starts
DWORD SizeOfRawData; // 0x10 — ★ Size on disk (aligned to FileAlignment)
DWORD PointerToRawData; // 0x14 — ★ File offset of section data on disk
DWORD PointerToRelocations; // 0x18 — Offset to COFF relocs (object files only)
DWORD PointerToLinenumbers; // 0x1C — Obsolete
WORD NumberOfRelocations; // 0x20
WORD NumberOfLinenumbers; // 0x22
DWORD Characteristics; // 0x24 — ★ Section flags (read/write/exec/etc.)
} IMAGE_SECTION_HEADER;
Section Characteristics Flags
0x00000020 IMAGE_SCN_CNT_CODE — Contains executable code
0x00000040 IMAGE_SCN_CNT_INITIALIZED_DATA — Contains initialized data
0x00000080 IMAGE_SCN_CNT_UNINITIALIZED_DATA — Contains uninitialized data (BSS)
0x02000000 IMAGE_SCN_MEM_DISCARDABLE — Can be discarded after load
0x10000000 IMAGE_SCN_MEM_SHARED — Can be shared in memory
0x20000000 IMAGE_SCN_MEM_EXECUTE — Executable
0x40000000 IMAGE_SCN_MEM_READ — Readable
0x80000000 IMAGE_SCN_MEM_WRITE — Writable
# Enumerate all sections
for section in pe.sections:
print(f" Name: {section.Name.decode().strip(chr(0))}")
print(f" VirtAddr: {section.VirtualAddress:#010x}")
print(f" VirtSize: {section.Misc_VirtualSize:#010x}")
print(f" RawSize: {section.SizeOfRawData:#010x}")
print(f" RawPtr: {section.PointerToRawData:#010x}")
print(f" Flags: {section.Characteristics:#010x}")
print()
VirtualSize vs SizeOfRawData
VirtualSize = actual content size in memory (unpadded)
SizeOfRawData = size on disk (padded to FileAlignment)
If VirtualSize > SizeOfRawData → extra space is zero-filled in memory
If SizeOfRawData >> VirtualSize → suspicious (packed content hiding in padding)
9. Common Sections
Standard Section Reference
| Section | Characteristics | Purpose |
|---|---|---|
.text |
RX (execute + read) | Compiled machine code — main program logic |
.data |
RW (read + write) | Global/static initialized variables |
.rdata |
R (read-only) | Constants, read-only strings, import/export tables in release |
.bss |
RW uninitialized | Uninitialized global variables — zero-filled at load |
.idata |
RW | Import tables (IDT, IAT) — often merged into .rdata |
.edata |
R | Export tables — present in DLLs |
.rsrc |
R | Resources — icons, manifests, dialogs, version info |
.reloc |
Discardable | Base relocation table — used if ASLR moves ImageBase |
.tls |
RW | Thread Local Storage data and callbacks |
.pdata |
R | Exception handlers (x64 only — structured exception data) |
.debug |
Discardable | Debug info (stripped in release builds) |
.CRT |
RW | C runtime initializers (atexit, ctor tables) |
Suspicious Section Names (Malware Indicators)
UPX0 / UPX1 → UPX packer
.ndata → NSIS installer
.themida → Themida packer / protector
.vmp0 / .vmp1 → VMProtect
.aspack → Aspack packer
MPRESS1 / MPRESS2 → MPRESS packer
(blank section name) → manual packing or obfuscation
10. Import Table
HOW IMPORTS WORK:
When a PE calls an API (e.g. CreateFile), it doesn't embed
the address. Instead, it reads the address from a table at runtime.
Three key structures:
┌──────────────────────────────────────────────────────────────────────┐
│ IDT — Import Descriptor Table │
│ │
│ • One IMAGE_IMPORT_DESCRIPTOR per imported DLL │
├──────────────────────────────────────────────────────────────────────┤
│ ILT — Import Lookup Table (a.k.a. INT) │
│ │
│ • List of imported function names or ordinals │
├──────────────────────────────────────────────────────────────────────┤
│ IAT — Import Address Table │
│ │
│ • Patched by the Windows loader with real function addresses │
│ • Executable code calls imported functions through this table │
└──────────────────────────────────────────────────────────────────────┘
IMAGE_IMPORT_DESCRIPTOR (20 bytes per DLL)
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // 0x00 — RVA to ILT (Import Lookup Table)
};
DWORD TimeDateStamp; // 0x04 — 0 if not bound, -1 if all bound
DWORD ForwarderChain; // 0x08 — Chained forwarding (-1 if none)
DWORD Name; // 0x0C — RVA to DLL name string
DWORD FirstThunk; // 0x10 — RVA to IAT (patched by loader)
} IMAGE_IMPORT_DESCRIPTOR;
// Terminated by a null entry (all zeros)
Import Lookup Table Entries
Each entry in ILT is a DWORD (PE32) or QWORD (PE32+):
Bit 31 (PE32) / Bit 63 (PE32+):
= 1 → import by ORDINAL — lower 16 bits = ordinal number
= 0 → import by NAME — lower bits = RVA to IMAGE_IMPORT_BY_NAME
IMAGE_IMPORT_BY_NAME:
WORD Hint; — index into export name table (hint only, loader verifies)
BYTE Name[]; — null-terminated function name string
# Enumerate imports
for entry in pe.DIRECTORY_ENTRY_IMPORT:
print(f"\n DLL: {entry.dll.decode()}")
for imp in entry.imports:
if imp.name:
print(f" [{imp.address:#010x}] {imp.name.decode()} (hint={imp.hint})")
else:
print(f" [{imp.address:#010x}] Ordinal #{imp.ordinal}")
Bound Imports
When TimeDateStamp == -1 (0xFFFFFFFF):
→ Import is "bound" — IAT already has resolved addresses hardcoded
→ Only valid if DLL loads at its preferred ImageBase AND hasn't changed
→ Modern Windows ignores bound imports (ASLR invalidates them)
→ Malware sometimes uses this to confuse analysis tools
Delay Imports
Delay-loaded DLLs are not loaded at startup.
First call triggers DLL load + address resolution.
Structure: IMAGE_DELAY_IMPORT_DESCRIPTOR (Data Directory entry 13)
Used for: optional DLLs, startup performance optimization
11. Export Table
PURPOSE:
DLLs use export tables to advertise functions to callers.
Functions can be exported by:
→ NAME — string name (most common)
→ ORDINAL — index number (more efficient, harder to analyze)
→ BOTH — both ordinal and name
EXEs rarely have exports (except when used as hybrid host).
IMAGE_EXPORT_DIRECTORY
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 0x00 — Reserved, 0
DWORD TimeDateStamp; // 0x04 — Timestamp
WORD MajorVersion; // 0x08
WORD MinorVersion; // 0x0A
DWORD Name; // 0x0C — RVA to DLL name string
DWORD Base; // 0x10 — Starting ordinal number (usually 1)
DWORD NumberOfFunctions; // 0x14 — Total number of exported functions
DWORD NumberOfNames; // 0x18 — Number of named exports
DWORD AddressOfFunctions; // 0x1C — RVA to Export Address Table (EAT)
DWORD AddressOfNames; // 0x20 — RVA to array of name RVAs
DWORD AddressOfNameOrdinals;// 0x24 — RVA to ordinal table (maps names→ordinals)
} IMAGE_EXPORT_DIRECTORY;
LOOKUP BY NAME:
1. Search AddressOfNames[] for matching name string
2. Get index i where match found
3. Get ordinal = AddressOfNameOrdinals[i]
4. Get function RVA = AddressOfFunctions[ordinal - Base]
5. VA = ImageBase + function RVA
LOOKUP BY ORDINAL:
1. ordinal_index = ordinal - Base
2. function RVA = AddressOfFunctions[ordinal_index]
# Enumerate exports
if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
name = exp.name.decode() if exp.name else f"Ordinal #{exp.ordinal}"
print(f" [{exp.address:#010x}] Ordinal={exp.ordinal} Name={name}")
Forwarded Exports
If the function address falls WITHIN the export section (not pointing to code):
→ It is a "forwarded export"
→ The value is a string like "NTDLL.RtlAllocateHeap"
→ Caller redirected to another DLL's export
→ Example: kernel32!HeapAlloc → forwards to ntdll!RtlAllocateHeap
12. Base Relocations
WHY THEY EXIST:
When code is compiled it assumes a fixed ImageBase.
Absolute addresses get hardcoded as VAs = ImageBase + RVA.
If the loader maps the image at a different address (ASLR / conflict):
→ All hardcoded absolute addresses are now WRONG
→ Base relocations tell the loader which values to patch
Formula:
new_value = old_value - preferred_ImageBase + actual_load_address
delta = actual_load_base - preferred_ImageBase
DLLs always have .reloc (they MUST support relocation).
EXEs often omit it when ASLR is disabled (IMAGE_FILE_RELOCS_STRIPPED).
IMAGE_BASE_RELOCATION (block header)
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // RVA of the 4 KB page this block covers
DWORD SizeOfBlock; // Total size of this block (including header)
// Followed by (SizeOfBlock - 8) / 2 entries:
WORD TypeOffset[]; // High 4 bits = type, Low 12 bits = offset within page
} IMAGE_BASE_RELOCATION;
Relocation Types
0x0 IMAGE_REL_BASED_ABSOLUTE — Padding / no-op
0x3 IMAGE_REL_BASED_HIGHLOW — Apply delta to full 32-bit value (PE32)
0xA IMAGE_REL_BASED_DIR64 — Apply delta to full 64-bit value (PE32+)
# Enumerate relocations
if hasattr(pe, 'DIRECTORY_ENTRY_BASERELOC'):
for base_reloc in pe.DIRECTORY_ENTRY_BASERELOC:
print(f" Block RVA: {base_reloc.struct.VirtualAddress:#010x}")
for entry in base_reloc.entries:
print(f" Type={entry.type} Offset={entry.rva:#010x}")
13. TLS Table
# Enumerate relocations
if hasattr(pe, 'DIRECTORY_ENTRY_BASERELOC'):
for base_reloc in pe.DIRECTORY_ENTRY_BASERELOC:
print(f" Block RVA: {base_reloc.struct.VirtualAddress:#010x}")
for entry in base_reloc.entries:
print(f" Type={entry.type} Offset={entry.rva:#010x}")
13. TLS Table
TLS = Thread Local Storage:
TLS allows each thread to have its own copy of a variable.
But from a security/RE perspective, TLS CALLBACKS are the key feature:
TLS Callbacks are functions that run BEFORE the OEP (AddressOfEntryPoint).
They run:
→ When a new process is created (DLL_PROCESS_ATTACH)
→ When a new thread is created (DLL_THREAD_ATTACH)
→ When a thread exits (DLL_THREAD_DETACH)
→ When the process exits (DLL_PROCESS_DETACH)
USED BY MALWARE TO:
→ Execute code before debugger attaches (anti-debug)
→ Decrypt payload before OEP runs
→ Check for analysis environment early
IMAGE_TLS_DIRECTORY
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData; // VA of TLS data start
DWORD EndAddressOfRawData; // VA of TLS data end
DWORD AddressOfIndex; // VA of TLS index slot
DWORD AddressOfCallBacks; // ★ VA of array of TLS callback function VAs
DWORD SizeOfZeroFill;
DWORD Characteristics;
} IMAGE_TLS_DIRECTORY32;
# Check for TLS callbacks
if hasattr(pe, 'DIRECTORY_ENTRY_TLS'):
tls = pe.DIRECTORY_ENTRY_TLS
print(f" TLS Directory at: {tls.struct.AddressOfCallBacks:#010x}")
# TLS callbacks are VAs, not RVAs!
14. Resources
STRUCTURE — Three-level tree:
Level 1: Type (RT_ICON, RT_STRING, RT_VERSION, RT_MANIFEST, etc.)
Level 2: Name or ID
Level 3: Language (LANGID)
Each leaf node points to actual resource data.
Common Resource Types
| RT Value | Constant | Description |
|---|---|---|
1 |
RT_CURSOR |
Cursor image |
2 |
RT_BITMAP |
Bitmap image |
3 |
RT_ICON |
Icon image |
4 |
RT_MENU |
Menu |
5 |
RT_DIALOG |
Dialog template |
6 |
RT_STRING |
String table |
9 |
RT_ACCELERATOR |
Keyboard accelerators |
10 |
RT_RCDATA |
★ Raw binary data — malware hides payloads here |
11 |
RT_MESSAGETABLE |
Message table |
14 |
RT_GROUP_ICON |
Icon group |
16 |
RT_VERSION |
★ Version information (VS_VERSIONINFO) |
24 |
RT_MANIFEST |
Application manifest (XML) |
# Enumerate resources
if hasattr(pe, 'DIRECTORY_ENTRY_RESOURCE'):
for res_type in pe.DIRECTORY_ENTRY_RESOURCE.entries:
type_name = str(res_type.id) if not res_type.name else res_type.name.string.decode()
print(f"\n Type: {type_name}")
for res_id in res_type.directory.entries:
for res_lang in res_id.directory.entries:
data = pe.get_data(
res_lang.data.struct.OffsetToData,
res_lang.data.struct.Size
)
print(f" ID={res_id.id} Lang={res_lang.id} Size={len(data):#x}")
Extracting Version Info
if hasattr(pe, 'VS_VERSIONINFO'):
if hasattr(pe, 'FileInfo'):
for fi in pe.FileInfo:
for entry in fi:
if hasattr(entry, 'StringTable'):
for st in entry.StringTable:
for k, v in st.entries.items():
print(f" {k.decode()}: {v.decode()}")
15. Address Translation (VA / RVA / File Offset)
THREE ADDRESS SPACES:
FILE OFFSET — byte position in the raw file on disk
RVA — offset from ImageBase when loaded in memory
VA — absolute virtual address in memory = ImageBase + RVA
Formulas
VA → RVA:
RVA = VA - ImageBase
RVA → VA:
VA = ImageBase + RVA
RVA → File Offset:
1. Find which section contains the RVA:
section.VirtualAddress <= RVA < section.VirtualAddress + section.VirtualSize
-
offset_within_section = RVA - section.VirtualAddress
-
file_offset = section.PointerToRawData + offset_within_section
File Offset → RVA:
1. Find which section contains the file offset:
section.PointerToRawData <= offset < section.PointerToRawData + section.SizeOfRawData
-
offset_within_section = file_offset - section.PointerToRawData
-
RVA = section.VirtualAddress + offset_within_section
Python Helpers
# RVA → file offset
def rva_to_offset(pe, rva):
for section in pe.sections:
if section.VirtualAddress <= rva < section.VirtualAddress + section.Misc_VirtualSize:
return section.PointerToRawData + (rva - section.VirtualAddress)
return None
# File offset → RVA
def offset_to_rva(pe, offset):
for section in pe.sections:
if section.PointerToRawData <= offset < section.PointerToRawData + section.SizeOfRawData:
return section.VirtualAddress + (offset - section.PointerToRawData)
return None
# VA → file offset (assumes default ImageBase)
def va_to_offset(pe, va):
rva = va - pe.OPTIONAL_HEADER.ImageBase
return rva_to_offset(pe, rva)
# pefile built-in helpers
offset = pe.get_offset_from_rva(0x1000)
rva = pe.get_rva_from_offset(0x400)
data = pe.get_data(rva=0x1000, length=64)
16. PE Analysis Tools
Static Analysis
# ── pefile (Python) — gold standard for scripting ──
pip install pefile
python3 -c "import pefile; pe=pefile.PE('sample.exe'); print(pe.dump_info())"
# ── PE-bear — free GUI PE viewer ──
# https://github.com/hasherezade/pe-bear
# Shows: all headers, sections, imports, exports, rich header, hex view
# ── CFF Explorer — Windows GUI ──
# https://ntcore.com/cff-explorer/
# Excellent for manual struct inspection
# ── pestudio — malware-first PE viewer ──
# https://www.winitor.com/
# Flags suspicious imports, entropy, blacklisted strings, VirusTotal
# ── DIE — Detect It Easy — packer / compiler fingerprinting ──
# https://github.com/horsicq/Detect-It-Easy
die sample.exe
# ── exiftool — metadata extraction ──
exiftool sample.exe
# Shows: compile time, linker version, subsystem, original filename, version info
# ── strings — extract printable strings ──
strings -n 6 sample.exe | grep -i "http\|cmd\|powershell\|C:\\"
strings -e l sample.exe # Unicode strings
# ── readpe (libpe tools) ──
readpe --headers sample.exe
readpe --imports sample.exe
readpe --exports sample.exe
readpe --sections sample.exe
Entropy Analysis
# High entropy (> 7.0) in a section = packed / encrypted / compressed content
# Normal .text = 5.5–6.5
# Normal .rdata = 4.0–5.5
# Packed binary .text = 7.8–8.0
# Python entropy calculation
import math
def entropy(data):
if not data: return 0
counts = [data.count(bytes([i])) for i in range(256)]
total = len(data)
return -sum((c/total) * math.log2(c/total) for c in counts if c)
import pefile
pe = pefile.PE("sample.exe")
for section in pe.sections:
data = section.get_data()
e = entropy(data)
name = section.Name.decode().strip('\x00')
print(f" {name:<10} entropy={e:.2f} size={len(data):#x}")
Dynamic / Debugging Tools
# ── x64dbg / x32dbg — best free Windows debugger ──
# PE-aware, symbol support, scripting, plugin ecosystem
# ── WinDbg — Microsoft debugger ──
windbg -o sample.exe
# ── Process Hacker / System Informer ──
# Inspect loaded PE modules, memory regions, loaded sections in real-time
# ── Ghidra — free NSA decompiler / disassembler ──
# Full PE support, import/export/xrefs, decompiler output
# ── IDA Pro / IDA Free ──
# Industry standard disassembler / decompiler
Python pefile — Common Recipes
import pefile, math
pe = pefile.PE("sample.exe")
# ── Architecture ──
arch = "x64" if pe.OPTIONAL_HEADER.Magic == 0x20b else "x86"
print(f"Arch: {arch}")
# ── Compile timestamp ──
import datetime
ts = datetime.datetime.utcfromtimestamp(pe.FILE_HEADER.TimeDateStamp)
print(f"Compiled: {ts} UTC")
# ── OEP ──
oep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
print(f"OEP RVA: {oep:#010x} VA: {pe.OPTIONAL_HEADER.ImageBase + oep:#010x}")
# ── Is DLL? ──
is_dll = pe.FILE_HEADER.Characteristics & 0x2000
print(f"Is DLL: {bool(is_dll)}")
# ── Has ASLR? ──
aslr = pe.OPTIONAL_HEADER.DllCharacteristics & 0x0040
print(f"ASLR: {bool(aslr)}")
# ── Has NX/DEP? ──
nx = pe.OPTIONAL_HEADER.DllCharacteristics & 0x0100
print(f"NX/DEP: {bool(nx)}")
# ── Has CFG? ──
cfg = pe.OPTIONAL_HEADER.DllCharacteristics & 0x4000
print(f"CFG: {bool(cfg)}")
# ── All imports as flat list ──
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
for dll in pe.DIRECTORY_ENTRY_IMPORT:
for fn in dll.imports:
name = fn.name.decode() if fn.name else f"ord#{fn.ordinal}"
print(f" {dll.dll.decode()}!{name}")
# ── Dump all strings ──
import re
raw = pe.__data__
ascii_strs = re.findall(b'[\x20-\x7e]{4,}', raw)
for s in ascii_strs[:50]:
print(s.decode())
17. Malware Indicators in PE Files
Compile Time Analysis
Timestamp = 0x00000000 → stripped / zeroed (evasion)
Timestamp = 0xFFFFFFFF → impossible value (evasion / Delphi compiler quirk)
Timestamp in future → fake / wrong TZ used by attacker
Timestamp very old → recycled binary or false dating
Rich Header mismatch → if RH says VS2008 but imports use Win10 APIs → suspect
Section Analysis
Check every section for:
→ Entropy > 7.0 → packed / encrypted content
→ Executable + Writable sections → self-modifying code
→ VirtualSize >> SizeOfRawData → unpacking in memory (loader inflates)
→ Non-standard section names → packer signatures
→ .text section with 0 raw size → entire code is in memory only (reflective injection)
→ Sections with no data, only VA → memory-only code injection
Import Table Red Flags
Suspicious import combinations:
→ VirtualAlloc + WriteProcessMemory + CreateRemoteThread → classic shellcode injection
→ OpenProcess + VirtualAllocEx + WriteProcessMemory → process injection
→ CreateService + StartService → persistence
→ RegSetValueEx + HKLM Software\Microsoft\Windows\Run → autorun
→ WinInet / WinHTTP functions → C2 communication
→ CryptEncrypt / CryptDecrypt → payload encryption
→ IsDebuggerPresent / CheckRemoteDebuggerPresent → anti-debug
Very few imports (< 3 DLLs):
→ Packed binary — real imports resolved at runtime
→ Only LoadLibrary + GetProcAddress → dynamic import resolution
IAT hook detection:
→ IAT entry VAs pointing outside the named DLL's memory range
Strings to Hunt
strings -n 4 sample.exe | grep -Ei \
"cmd\.exe|powershell|wscript|cscript|\
http://|https://|/update|/gate|/check|\
TEMP\\|APPDATA\\|System32\\\
schtasks|reg add|net user|net localgroup|\
CreateRemoteThread|VirtualAlloc|LoadLibrary|\
base64|mimikatz|sekurlsa|lsass"
Overlay / Appended Data
Overlay = data appended AFTER the last section's raw data.
Not mapped into memory by loader.
Legitimate uses: SFX archives, digital signatures, installers
Malicious uses: hidden payload, second-stage dropper, config data
Detect:
last_section_end = max(s.PointerToRawData + s.SizeOfRawData for s in pe.sections)
if os.path.getsize("sample.exe") > last_section_end:
print(f"Overlay detected: {os.path.getsize('sample.exe') - last_section_end} bytes")
18. Master Cheat Sheet
Quick PE Size Map
DOS Header 64 bytes (always at offset 0)
Rich Header variable (between DOS Stub and NT Headers — optional)
NT Headers 248 bytes (PE32) / 264 bytes (PE32+)
Signature 4 bytes
File Header 20 bytes
Optional Header 224 bytes (PE32) / 240 bytes (PE32+)
Section Headers 40 bytes × NumberOfSections
Section Data variable
Magic Numbers
Offset 0x00 5A 4D → MZ (DOS Header magic)
e_lfanew variable → offset to PE Header
PE Signature 50 45 00 00 → "PE\0\0"
Optional Hdr 0B 01 → PE32 (32-bit)
Optional Hdr 0B 02 → PE32+ (64-bit)
Critical Fields at a Glance
| Field | Location | Purpose |
|---|---|---|
e_magic |
DOS Header +0x00 | Must be MZ (0x5A4D) |
e_lfanew |
DOS Header +0x3C | Offset to NT Headers |
Signature |
NT Headers +0x00 | Must be PE\0\0 (0x00004550) |
Machine |
File Header +0x00 | CPU arch (x86=0x14C, x64=0x8664) |
NumberOfSections |
File Header +0x02 | How many sections follow |
TimeDateStamp |
File Header +0x04 | Linker timestamp (can be faked) |
AddressOfEntryPoint |
Optional Hdr +0x10 | OEP — first instruction RVA |
ImageBase |
Optional Hdr +0x1C | Preferred load address |
SectionAlignment |
Optional Hdr +0x20 | Section alignment in memory |
FileAlignment |
Optional Hdr +0x24 | Section alignment on disk |
SizeOfImage |
Optional Hdr +0x38 | Total in-memory footprint |
DllCharacteristics |
Optional Hdr +0x46 | ASLR, NX, CFG flags |
DataDirectory[0] |
Optional Hdr +0x60 | Export Table RVA + Size |
DataDirectory[1] |
Optional Hdr +0x68 | Import Table RVA + Size |
DataDirectory[5] |
Optional Hdr +0x88 | Base Relocation RVA + Size |
DataDirectory[9] |
Optional Hdr +0xA8 | TLS Table RVA + Size |
Hashcat Mode Reference (for cracking exports/creds found via PE)
NTLM Hash → -m 1000
NetNTLMv2 → -m 5600
Kerberoast RC4 → -m 13100
Kerberoast AES128 → -m 19700
AS-REP → -m 18200
Tools Decision Tree
Got a PE sample?
│
├── Unknown packer / compiler?
│ └── DIE (Detect It Easy) → identifies packer, compiler, overlay
│
├── Quick static overview?
│ └── pestudio → headers, imports, strings, entropy, VirusTotal
│
├── Manual struct inspection (GUI)?
│ └── PE-bear or CFF Explorer
│
├── Script / automate analysis?
│ └── pefile in Python
│
├── Disassemble / decompile?
│ └── Ghidra (free) or IDA Pro
│
├── Dynamic analysis / debugging?
│ └── x64dbg + ScyllaHide plugin (anti-anti-debug)
│
├── Detect high-entropy packed sections?
│ └── pefile entropy calculation or pestudio
│
├── Hunt suspicious imports?
│ └── pestudio flagging + manual review of injection-related APIs
│
└── Check for TLS callbacks (pre-OEP execution)?
└── pefile DIRECTORY_ENTRY_TLS + x64dbg TLS breakpoints