ᛉ Yggdrasil

Binary

PE File Internals

15 Sep 2026 · 5,122 words · 23 min

Contents

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_lfanew offset from start of file.
  • Signature must be 0x50450000 (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

  1. offset_within_section = RVA - section.VirtualAddress

  2. 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

  1. offset_within_section = file_offset - section.PointerToRawData

  2. 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

← Binary top ↑