← Back to list

Deep Dive into Windows PE Internals

Introduction

trmz · 2026-02-09 06:58 · 0 claps · 16.7 min read
#windows-internals #portable-executable #internal-system
Open on Medium ↗

Deep Dive into Windows PE Internals

Introduction

Every time you double-click an .exe file on Windows, launch a DLL, or run a system driver, you're interacting with the Portable Executable (PE) format. For security researchers, understanding PE internals isn't just academic—it's fundamental to malware analysis, reverse engineering, exploit development, and understanding how Windows loads and executes code.

In this comprehensive guide, we’ll dissect the PE format layer by layer, exploring each component’s purpose, structure, and security implications. Whether you’re analyzing suspicious executables or building security tools, this knowledge forms the bedrock of Windows binary analysis.

What is the PE Format?

The Portable Executable format is Microsoft’s standard executable file format for Windows operating systems. Introduced with Windows NT, it’s used for:

  • Executable files (.exe)
  • Dynamic Link Libraries (.dll)
  • Kernel drivers (.sys)
  • ActiveX controls (.ocx)
  • Control Panel applets (.cpl)

The term “portable” refers to its design goal of being CPU-architecture independent, though in practice, each PE file is compiled for a specific architecture (x86, x64, ARM, etc.).

Why PE Internals Matter for Security

Understanding PE structure is crucial for:

  • Malware Analysis: Identifying packed/obfuscated code, analyzing imports/exports, detecting code injection
  • Reverse Engineering: Locating entry points, understanding program structure, finding API calls
  • Exploit Development: Understanding memory layout, DEP/ASLR mechanisms, section permissions
  • Forensics: Validating file integrity, detecting tampering, analyzing execution artifacts
  • Tool Development: Building parsers, debuggers, and analysis frameworks

The PE File Structure: A 30,000-Foot View

Before diving into specifics, let’s understand the overall structure:

┌─────────────────────────────────┐
│      DOS Header (64 bytes)      │  ← Legacy compatibility
├─────────────────────────────────┤
│      DOS Stub Program           │  ← "This program cannot be run..."
├─────────────────────────────────┤
│      PE Signature (4 bytes)     │  ← "PE\0\0" magic number
├─────────────────────────────────┤
│      COFF File Header           │  ← Machine type, section count
├─────────────────────────────────┤
│    Optional Header (PE32/PE32+) │  ← Entry point, image base, subsystem
├─────────────────────────────────┤
│      Data Directories           │  ← Import/Export tables, resources
├─────────────────────────────────┤
│      Section Headers            │  ← .text, .data, .rdata, etc.
├─────────────────────────────────┤
│      Section Data (.text)       │  ← Executable code
├─────────────────────────────────┤
│      Section Data (.data)       │  ← Initialized data
├─────────────────────────────────┤
│      Section Data (.rdata)      │  ← Read-only data, imports
├─────────────────────────────────┤
│      Section Data (.rsrc)       │  ← Resources (icons, dialogs)
├─────────────────────────────────┤
│      Other Sections...          │  ← .reloc, .debug, custom sections
└─────────────────────────────────┘

1. DOS Header and DOS Stub

The DOS Header (IMAGE_DOS_HEADER)

Every PE file begins with a 64-byte DOS header, a relic from the MS-DOS era maintained for backward compatibility. While most fields are obsolete, two are critical:

Structure:

typedef struct _IMAGE_DOS_HEADER {
    WORD  e_magic;      // Magic number "MZ" (0x5A4D)
    WORD  e_cblp;       // Bytes on last page of file
    WORD  e_cp;         // Pages in file
    WORD  e_crlc;       // Relocations
    WORD  e_cparhdr;    // Size of header in paragraphs
    WORD  e_minalloc;   // Minimum extra paragraphs
    WORD  e_maxalloc;   // Maximum extra paragraphs
    WORD  e_ss;         // Initial SS value
    WORD  e_sp;         // Initial SP value
    WORD  e_csum;       // Checksum
    WORD  e_ip;         // Initial IP value
    WORD  e_cs;         // Initial CS value
    WORD  e_lfarlc;     // File address of relocation table
    WORD  e_ovno;       // Overlay number
    WORD  e_res[4];     // Reserved words
    WORD  e_oemid;      // OEM identifier
    WORD  e_oeminfo;    // OEM information
    WORD  e_res2[10];   // Reserved words
    LONG  e_lfanew;     // File offset to PE header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Key Fields:

  1. e_magic (0x00): Must be 0x5A4D ("MZ" in ASCII, Mark Zbikowski's initials)
  • First validation check when parsing PE files
  • Malware sometimes modifies this to evade simple detection
  1. e_lfanew (0x3C): Offset to the PE header
  • Most important field for modern executables
  • Allows DOS stub to be variable length
  • Jump point to actual PE structure

Security Considerations:

  • Validation: Always verify e_magic == 0x5A4D and e_lfanew points to valid memory
  • Evasion: Malware may use unusual e_lfanew values or modify other DOS header fields
  • Polyglot Files: Can craft files valid as both PE and other formats (e.g., ZIP)

The DOS Stub

Following the DOS header is the DOS stub — a small 16-bit program that runs if you execute the PE file in DOS mode. Typically displays:

This program cannot be run in DOS mode.

Default Stub (hex):

0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68
69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F
74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20
6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00

Security Analysis Points:

  • Custom Stubs: Malware may include custom code here
  • Steganography: Unused space can hide data
  • Size Variations: Abnormally large stubs warrant investigation
  • Code Execution: Rarely, actual functional DOS code may be present

2. PE Signature

At the offset specified by e_lfanew, you'll find the PE signature—a 4-byte magic number:

Offset: e_lfanew
Value:  0x50 0x45 0x00 0x00  ("PE\0\0")

This confirms the file is a valid PE executable. Simple but critical validation step.

3. COFF File Header (IMAGE_FILE_HEADER)

Immediately following the PE signature is the COFF (Common Object File Format) header, providing fundamental information about the executable:

Structure:

typedef struct _IMAGE_FILE_HEADER {
    WORD  Machine;              // Target CPU architecture
    WORD  NumberOfSections;     // Number of section headers
    DWORD TimeDateStamp;        // Compilation timestamp (Unix epoch)
    DWORD PointerToSymbolTable; // Deprecated, usually 0
    DWORD NumberOfSymbols;      // Deprecated, usually 0
    WORD  SizeOfOptionalHeader; // Size of optional header
    WORD  Characteristics;      // File characteristics flags
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Key Fields Explained

1. Machine (2 bytes)

Specifies the target CPU architecture:

Value Architecture Notes 0x014C IMAGE_FILE_MACHINE_I386 32-bit Intel x86 0x8664 IMAGE_FILE_MACHINE_AMD64 64-bit x64 (Intel/AMD) 0x01C4 IMAGE_FILE_MACHINE_ARMNT ARM little-endian 0xAA64 IMAGE_FILE_MACHINE_ARM64 ARM64 little-endian 0x0200 IMAGE_FILE_MACHINE_IA64 Intel Itanium

Security Note: Mismatches between declared architecture and actual code indicate packing or obfuscation.

2. NumberOfSections (2 bytes)

Count of section headers following the Optional Header. Typical values:

  • 3–6 sections: Normal executables
  • 1–2 sections: Potentially packed
  • 10+ sections: May indicate custom packing or .NET assemblies

3. TimeDateStamp (4 bytes)

Unix timestamp of compilation. Can be used to:

  • Correlate files from same build
  • Identify compilation timeframe
  • Detect tampering (though easily forged)
  • Note: Often zeroed or randomized by packers/protectors

4. Characteristics (2 bytes)

Bit flags describing file properties:

Flag Value Meaning Security Implications IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 File is executable Must be set for valid PE IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 Can handle >2GB addresses Affects memory analysis IMAGE_FILE_32BIT_MACHINE 0x0100 32-bit architecture Important for architecture detection IMAGE_FILE_DLL 0x2000 File is a DLL Changes loading behavior IMAGE_FILE_SYSTEM 0x1000 System file (driver) Kernel mode execution

Example Analysis:

Characteristics: 0x0122
= 0x0002 (EXECUTABLE_IMAGE)
| 0x0020 (LARGE_ADDRESS_AWARE)  
| 0x0100 (32BIT_MACHINE)
→ 32-bit executable supporting large address space

4. Optional Header (IMAGE_OPTIONAL_HEADER)

Despite its name, this header is mandatory for executables. It contains critical information for the Windows loader:

Structure (PE32):

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD  Magic;                    // 0x10B (PE32) or 0x20B (PE32+)
    BYTE  MajorLinkerVersion;
    BYTE  MinorLinkerVersion;
    DWORD SizeOfCode;               // Size of .text section(s)
    DWORD SizeOfInitializedData;    // Size of .data section(s)
    DWORD SizeOfUninitializedData;  // Size of .bss section
    DWORD AddressOfEntryPoint;      // RVA of entry point
    DWORD BaseOfCode;               // RVA of code section
    DWORD BaseOfData;               // RVA of data section (PE32 only)

    // NT-specific fields
    DWORD ImageBase;                // Preferred load address
    DWORD SectionAlignment;         // Section alignment in memory
    DWORD FileAlignment;            // Section alignment on disk
    WORD  MajorOperatingSystemVersion;
    WORD  MinorOperatingSystemVersion;
    WORD  MajorImageVersion;
    WORD  MinorImageVersion;
    WORD  MajorSubsystemVersion;
    WORD  MinorSubsystemVersion;
    DWORD Win32VersionValue;        // Reserved, must be 0
    DWORD SizeOfImage;              // Size of loaded image in memory
    DWORD SizeOfHeaders;            // Size of all headers
    DWORD CheckSum;                 // Image checksum
    WORD  Subsystem;                // Required subsystem
    WORD  DllCharacteristics;       // DLL characteristics
    DWORD SizeOfStackReserve;       // Stack reserve size
    DWORD SizeOfStackCommit;        // Stack commit size
    DWORD SizeOfHeapReserve;        // Heap reserve size
    DWORD SizeOfHeapCommit;         // Heap commit size
    DWORD LoaderFlags;              // Obsolete
    DWORD NumberOfRvaAndSizes;      // Number of data directories
    IMAGE_DATA_DIRECTORY DataDirectory[16]; // Data directories
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

PE32+ (64-bit) Differences:

  • Magic = 0x20B
  • No BaseOfData field
  • ImageBase, stack, and heap sizes are 8 bytes (ULONGLONG)

Critical Fields for Security Research

1. Magic (0x10B or 0x20B)

  • Distinguishes 32-bit vs 64-bit executables
  • First field to check when parsing

2. AddressOfEntryPoint (RVA)

  • Relative Virtual Address where execution begins
  • NOT the first byte of code necessarily
  • Points to startup code that calls main() or WinMain()
  • Malware Note: Packers often modify this to point to unpacking stub

3. ImageBase

  • Preferred virtual address for loading
  • Default values:
  • EXEs: 0x00400000 (32-bit), 0x0000000140000000 (64-bit)
  • DLLs: 0x10000000 (32-bit), 0x0000000180000000 (64-bit)
  • If unavailable, Windows relocates using .reloc section
  • ASLR Note: Modern Windows randomizes this

4. SectionAlignment vs FileAlignment

  • SectionAlignment: Alignment in memory (usually 0x1000 = 4KB)
  • FileAlignment: Alignment on disk (usually 0x200 = 512 bytes)
  • Why Different?: Memory pages are 4KB; disk sectors are 512 bytes
  • Security Check: Values must be powers of 2; SectionAlignment ≥ FileAlignment

5. SizeOfImage

  • Total size of PE image in memory (including headers)
  • Must be multiple of SectionAlignment
  • Used to allocate virtual memory
  • Validation: Should equal aligned size of all sections + headers

6. SizeOfHeaders

  • Combined size of DOS header, PE headers, and section headers
  • Rounded up to FileAlignment
  • First section data starts here

7. Subsystem

Indicates the Windows subsystem required:

Value Subsystem Description 1 IMAGE_SUBSYSTEM_NATIVE Device driver/native system process 2 IMAGE_SUBSYSTEM_WINDOWS_GUI GUI application 3 IMAGE_SUBSYSTEM_WINDOWS_CUI Console application 5 IMAGE_SUBSYSTEM_OS2_CUI OS/2 console (obsolete) 7 IMAGE_SUBSYSTEM_POSIX_CUI POSIX console 9 IMAGE_SUBSYSTEM_WINDOWS_CE_GUI Windows CE 10 IMAGE_SUBSYSTEM_EFI_APPLICATION EFI application 16 IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION Boot application

8. DllCharacteristics

Security-related flags (despite the name, applies to both DLLs and EXEs):

Flag Value Feature Impact IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 0x0040 ASLR Randomizes image base IMAGE_DLLCHARACTERISTICS_NX_COMPAT 0x0100 DEP/NX Marks stack/heap non-executable IMAGE_DLLCHARACTERISTICS_NO_SEH 0x0400 No SEH Disables SEH (safer) IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE 0x8000 TS Aware Multi-session support IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA 0x0020 High Entropy ASLR 64-bit ASLR improvement IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY 0x0080 Code Signing Must be signed IMAGE_DLLCHARACTERISTICS_GUARD_CF 0x4000 Control Flow Guard CFG protection

Modern Security Baseline:

Expected flags for secure modern executable:
0x4160 = DYNAMIC_BASE | NX_COMPAT | NO_SEH | GUARD_CF

5. Data Directories

The last part of the Optional Header is an array of 16 data directories pointing to important structures:

Structure:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD VirtualAddress;  // RVA of the table
    DWORD Size;            // Size of the table in bytes
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

Standard Data Directories:

Index Directory Purpose Security Relevance 0 Export Table Exported functions (DLLs) API hooks, function analysis 1 Import Table Imported functions API dependencies, behavior hints 2 Resource Table Icons, dialogs, strings Embedded files, version info 3 Exception Table Exception handling (x64) SEH analysis, control flow 4 Certificate Table Digital signatures Authenticity verification 5 Base Relocation Table Relocation information ASLR support 6 Debug Directory Debug information PDB paths, timestamps 7 Architecture Reserved (0) — 8 Global Ptr Global pointer (MIPS) Rare 9 TLS Table Thread Local Storage Anti-debugging tricks 10 Load Config Table Load configuration Security cookies, CFG 11 Bound Import Bound import table Optimization 12 IAT Import Address Table API hooking target 13 Delay Import Delay-loaded DLLs Lazy loading 14 CLR Header .NET metadata Managed code 15 Reserved Must be 0 -

Key Data Directories for Analysis

Import Address Table (IAT)

  • Initially contains pointers to import names
  • Windows loader fills with actual function addresses
  • Hooking Target: Malware often modifies IAT entries
  • Located in .rdata or .idata section

Base Relocation Table

  • Needed when ImageBase is unavailable
  • Contains fixup information for absolute addresses
  • If missing and ASLR enabled → won’t load
  • Packer Indicator: Sometimes stripped by packers

Load Configuration Directory

  • Security cookies (stack canaries)
  • Control Flow Guard (CFG) settings
  • Safe SEH handler list
  • Modern Security: Check for presence and configuration

6. Section Headers

Following the Optional Header are section headers — one for each section defined in the COFF header:

Structure:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE  Name[8];              // Section name (null-terminated)
    union {
        DWORD PhysicalAddress;
        DWORD VirtualSize;      // Size in memory
    } Misc;
    DWORD VirtualAddress;       // RVA where section loads
    DWORD SizeOfRawData;        // Size on disk (aligned)
    DWORD PointerToRawData;     // File offset to section data
    DWORD PointerToRelocations; // Deprecated
    DWORD PointerToLinenumbers; // Deprecated
    WORD  NumberOfRelocations;  // Deprecated
    WORD  NumberOfLinenumbers;  // Deprecated
    DWORD Characteristics;      // Section attributes
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Understanding Section Mappings

Key Concept: Sections exist both on disk (file) and in memory, with different sizes and locations:

Disk (File):                    Memory (Process):
PointerToRawData ─────────────→ VirtualAddress (RVA)
SizeOfRawData                   VirtualSize (unaligned)
(FileAlignment)                 (SectionAlignment)

Example:

.text section:
  VirtualAddress:   0x1000      (loads at RVA 0x1000)
  VirtualSize:      0x2C48      (11,336 bytes actual code)
  PointerToRawData: 0x400       (offset in file = 1024)
  SizeOfRawData:    0x3000      (12,288 bytes on disk, aligned)

Why VirtualSize ≠ SizeOfRawData?

  • FileAlignment adds padding on disk
  • VirtualSize may include uninitialized data (.bss)
  • SectionAlignment adds padding in memory

Section Characteristics (Flags)

Critical permission bits:

Flag Value Meaning Typical Sections IMAGE_SCN_CNT_CODE 0x00000020 Contains code .text IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 Contains initialized data .data, .rdata IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 Contains BSS data .bss IMAGE_SCN_MEM_EXECUTE 0x20000000 Executable .text IMAGE_SCN_MEM_READ 0x40000000 Readable All sections IMAGE_SCN_MEM_WRITE 0x80000000 Writable .data IMAGE_SCN_MEM_DISCARDABLE 0x02000000 Can be discarded .reloc IMAGE_SCN_MEM_SHARED 0x10000000 Shared across processes Shared sections

Common Combinations:

.text:  0x60000020 = CODE | EXECUTE | READ
.data:  0xC0000040 = INITIALIZED_DATA | READ | WRITE
.rdata: 0x40000040 = INITIALIZED_DATA | READ

7. Common Sections Deep Dive

.text Section

  • Contains: Executable code
  • Permissions: Read + Execute (RX)
  • Entry Point: Usually points here
  • Security Analysis:
  • Check for unexpected WRITE permission
  • Monitor for code injection
  • Analyze instruction patterns
  • Look for packed/encrypted code

.data Section

  • Contains: Initialized global/static variables
  • Permissions: Read + Write (RW)
  • Examples: Global arrays, static strings
  • Security Notes:
  • Writable data can be modified at runtime
  • May contain decryption keys
  • Shellcode injection target

.rdata Section

  • Contains: Read-only data
  • Permissions: Read only (R)
  • Examples:
  • String literals
  • Const variables
  • Import table data
  • Virtual function tables (vtables)
  • Why Separate?: DEP protection; prevents modification

.bss Section

  • Contains: Uninitialized data
  • Disk Size: 0 bytes (not stored in file!)
  • Memory Size: Allocated at runtime, zero-initialized
  • Permissions: Read + Write (RW)
  • Note: Modern linkers often merge into .data

.rsrc Section

  • Contains: Resources (hierarchical structure)
  • Icons, bitmaps, cursors
  • Dialog templates
  • String tables
  • Version information
  • Embedded files
  • Format: Tree structure (Type → Name → Language)
  • Security Analysis:
  • Check for suspicious embedded executables
  • Analyze version info for legitimacy
  • Extract embedded configurations
  • Look for steganography

.reloc Section

  • Contains: Base relocation table
  • Purpose: Fix absolute addresses when ASLR relocates image
  • Structure: Page RVA + block size + type/offset entries
  • Security:
  • Can be stripped if ASLR disabled
  • Presence indicates ASLR compatibility
  • Sometimes removed by packers

.idata Section

  • Contains: Import information
  • Structure:
  • Import Directory Table
  • Import Lookup Tables (ILT)
  • Hint/Name table
  • Import Address Table (IAT)
  • Modern: Often merged into .rdata
  • Analysis Focus: API calls reveal program behavior

.edata Section

  • Contains: Export information (DLLs)
  • Structure:
  • Export Address Table
  • Name Pointer Table
  • Ordinal Table
  • Security: API hooking, DLL hijacking analysis

Custom Sections

  • UPX: UPX0, UPX1 (UPX packer)
  • .themida: Themida protector
  • .aspack: ASPack packer
  • .pdata: Exception data (x64)
  • .tls: Thread Local Storage

Red Flags:

  • Executable + Writable sections
  • Unusually large sections
  • Sections with entropy > 7.0 (likely compressed/encrypted)
  • Misspelled standard section names (e.g., .txet)
  • Section RVA overlaps

8. Import Table Structure

The Import Table tells Windows which DLLs and functions the executable needs:

Process:

  1. PE specifies DLLs and functions in Import Directory
  2. Windows loader loads specified DLLs
  3. Loader resolves function addresses
  4. Loader updates IAT with actual addresses

Structure:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD Characteristics;
        DWORD OriginalFirstThunk;  // RVA to ILT (Import Lookup Table)
    };
    DWORD TimeDateStamp;           // Bind timestamp
    DWORD ForwarderChain;          // Forwarding info
    DWORD Name;                    // RVA to DLL name
    DWORD FirstThunk;              // RVA to IAT (Import Address Table)
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;

Import Name Table Entry:

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD  Hint;        // Index into export table
    CHAR  Name[1];     // Function name (null-terminated)
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

Security Analysis of Imports

Behavior Indicators:

Certain API patterns indicate specific behaviors:

File Operations:

  • CreateFile, ReadFile, WriteFile → File I/O
  • FindFirstFile, FindNextFile → Directory enumeration
  • DeleteFile, MoveFile → File manipulation

Network Activity:

  • WSAStartup, socket, connect, send, recv → Network communication
  • InternetOpen, InternetConnect → HTTP operations
  • GetAdaptersInfo → Network reconnaissance

Process/Memory:

  • CreateProcess, CreateRemoteThread → Process injection
  • VirtualAlloc, VirtualProtect → Memory manipulation
  • WriteProcessMemory → Code injection
  • OpenProcess → Process access

Registry:

  • RegOpenKeyEx, RegSetValueEx → Registry modification
  • RegCreateKeyEx → Persistence mechanism

Anti-Analysis:

  • IsDebuggerPresent, CheckRemoteDebuggerPresent → Debugger detection
  • GetTickCount, QueryPerformanceCounter → Timing checks
  • FindWindow → Looking for analysis tools

Suspicious Patterns:

  • No imports (custom loader/PIC code)
  • Only LoadLibrary + GetProcAddress (dynamic resolution)
  • Imports from unusual DLLs
  • Heavy use of ntdll.dll (bypassing API hooks)

9. Export Table (DLLs)

DLLs expose functions via the Export Directory:

Structure:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD Characteristics;
    DWORD TimeDateStamp;
    WORD  MajorVersion;
    WORD  MinorVersion;
    DWORD Name;                    // RVA to DLL name
    DWORD Base;                    // Starting ordinal number
    DWORD NumberOfFunctions;       // Number of entries in EAT
    DWORD NumberOfNames;           // Number of entries in NPT
    DWORD AddressOfFunctions;      // RVA to Export Address Table
    DWORD AddressOfNames;          // RVA to Name Pointer Table
    DWORD AddressOfNameOrdinals;   // RVA to Ordinal Table
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Export Methods:

  1. By Name: GetProcAddress(hDll, "FunctionName")
  2. By Ordinal: GetProcAddress(hDll, MAKEINTRESOURCE(ordinal))

Forwarded Exports:

  • Export points to another DLL
  • Format: “DllName.FunctionName” (e.g., “ntdll.RtlCreateUnicodeString”)
  • Enables API redirection

Security Analysis:

  • Undocumented exports may indicate backdoors
  • Ordinal-only exports complicate analysis
  • Exported functions are API hooking targets
  • Check for suspicious function names

10. Relocations and ASLR

Base Relocations

When Windows can’t load a PE at its preferred ImageBase, it must fix up absolute addresses:

Relocation Block Structure:

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD VirtualAddress;  // Page RVA
    DWORD SizeOfBlock;     // Block size including header
    // Followed by type/offset entries
} IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;

Relocation Entry (2 bytes):

Bits 15-12: Type
Bits 11-0:  Offset within page

Common Types:

  • IMAGE_REL_BASED_ABSOLUTE (0): Padding, skip
  • IMAGE_REL_BASED_HIGHLOW (3): 32-bit address (x86)
  • IMAGE_REL_BASED_DIR64 (10): 64-bit address (x64)

Relocation Process:

actual_address = ImageBase + RVA
if (ImageBase != PreferredImageBase):
    delta = ImageBase - PreferredImageBase
    for each relocation:
        *(base + page_rva + offset) += delta

ASLR Implementation

  • IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE flag enables ASLR
  • Windows randomizes ImageBase on each load
  • Entropy: 8 bits (256 positions) for 32-bit; 24+ bits for 64-bit
  • High Entropy ASLR: 64-bit only, better randomization
  • Requires functional .reloc section

Bypasses:

  • Non-ASLR modules in process
  • Information leaks
  • Partial overwrites
  • Brute forcing (32-bit)

11. Thread Local Storage (TLS)

TLS allows per-thread data storage and provides TLS callbacks — functions executed before the entry point:

Structure:

typedef struct _IMAGE_TLS_DIRECTORY {
    ULONGLONG StartAddressOfRawData;  // VA of TLS data
    ULONGLONG EndAddressOfRawData;    // VA of TLS data end
    ULONGLONG AddressOfIndex;         // VA of TLS index
    ULONGLONG AddressOfCallBacks;     // VA to array of callbacks
    DWORD     SizeOfZeroFill;         // Extra space to allocate
    DWORD     Characteristics;        // Reserved
} IMAGE_TLS_DIRECTORY64, *PIMAGE_TLS_DIRECTORY64;

TLS Callbacks:

  • Array of function pointers (null-terminated)
  • Called for: process attach/detach, thread attach/detach
  • Executed BEFORE entry point

Anti-Debugging Use:

  • Malware uses TLS callbacks for:
  • Debugger detection before main code
  • Unpacking before analysis
  • Decryption before execution
  • Many debuggers don’t auto-break on TLS callbacks

Analysis:

  • Always check for TLS directory
  • Set breakpoints on TLS callbacks
  • Monitor for anti-debugging code

12. Load Configuration Directory

Modern security features are configured here:

Structure (simplified):

typedef struct _IMAGE_LOAD_CONFIG_DIRECTORY {
    DWORD Size;
    DWORD TimeDateStamp;
    WORD  MajorVersion;
    WORD  MinorVersion;
    DWORD GlobalFlagsClear;
    DWORD GlobalFlagsSet;
    DWORD CriticalSectionDefaultTimeout;
    // ... many fields ...
    ULONGLONG SecurityCookie;              // Stack cookie (GS)
    ULONGLONG SEHandlerTable;              // Safe SEH table
    ULONGLONG SEHandlerCount;
    ULONGLONG GuardCFCheckFunctionPointer; // CFG check function
    ULONGLONG GuardCFDispatchFunctionPointer;
    ULONGLONG GuardCFFunctionTable;        // CFG function table
    ULONGLONG GuardCFFunctionCount;
    DWORD     GuardFlags;                  // CFG settings
    // ... more fields ...
} IMAGE_LOAD_CONFIG_DIRECTORY, *PIMAGE_LOAD_CONFIG_DIRECTORY;

Security Features:

1. Stack Canaries (/GS)

  • SecurityCookie: Random value placed on stack
  • Checked before function return
  • Detects buffer overflows

2. Safe SEH

  • SEHandlerTable: Whitelist of valid exception handlers
  • Prevents SEH overwrites
  • Windows validates handlers against this table

3. Control Flow Guard (CFG)

  • GuardFlags: CFG configuration
  • GuardCFFunctionTable: Valid indirect call targets
  • Prevents arbitrary code execution via function pointers
  • Modern exploit mitigation

4. Return Flow Guard (RFG)

  • Validates return addresses
  • Prevents ROP attacks
  • Windows 10+ feature

Analysis:

  • Presence indicates modern compilation
  • Absence = potential vulnerability
  • Check GuardFlags for CFG status

13. Exception Handling (x64)

x64 uses table-based exception handling instead of SEH chains:

Runtime Function Entry:

typedef struct _RUNTIME_FUNCTION {
    DWORD BeginAddress;     // RVA of function start
    DWORD EndAddress;       // RVA of function end
    DWORD UnwindInfoAddress; // RVA to unwind info
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

Unwind Info:

typedef struct _UNWIND_INFO {
    BYTE Version : 3;
    BYTE Flags : 5;
    BYTE SizeOfProlog;
    BYTE CountOfCodes;
    BYTE FrameRegister : 4;
    BYTE FrameOffset : 4;
    UNWIND_CODE UnwindCode[1];
    // Optionally followed by exception handler
} UNWIND_INFO, *PUNWIND_INFO;

Purpose:

  • Stack unwinding during exceptions
  • Debugger stack traces
  • Exception dispatching

Security:

  • More secure than SEH (no writeable chains)
  • Harder to exploit
  • Still vulnerable if RW memory contains handlers

14. Digital Signatures

The Certificate Table contains Authenticode signatures:

Structure:

typedef struct _WIN_CERTIFICATE {
    DWORD dwLength;          // Certificate length
    WORD  wRevision;         // Revision (0x0200)
    WORD  wCertificateType;  // Type (0x0002 = PKCS#7)
    BYTE  bCertificate[1];   // PKCS#7 SignedData
} WIN_CERTIFICATE, *PWIN_CERTIFICATE;

Validation:

  1. Extract certificate from PE
  2. Verify signature chain to trusted root
  3. Check certificate not revoked
  4. Verify hash matches PE image

Security Notes:

  • Certificate != Safe: Malware can be signed
  • Stolen Certificates: Compromised code signing certs
  • Self-Signed: Zero trust value
  • Validation: Check issuer, timestamp, revocation status
  • Appended Data: Some malware appends data after signature

Tools:

  • signtool verify /pa /v file.exe
  • Get-AuthenticodeSignature (PowerShell)

15. .NET Assemblies (CLR Header)

.NET executables are PE files with managed code:

CLR Header (Data Directory 14):

typedef struct _IMAGE_COR20_HEADER {
    DWORD cb;                    // Header size
    WORD  MajorRuntimeVersion;
    WORD  MinorRuntimeVersion;
    IMAGE_DATA_DIRECTORY MetaData;  // Metadata directory
    DWORD Flags;                 // Flags (e.g., IL only)
    DWORD EntryPointToken;       // Metadata token
    IMAGE_DATA_DIRECTORY Resources;
    IMAGE_DATA_DIRECTORY StrongNameSignature;
    // ... more fields ...
} IMAGE_COR20_HEADER, *PIMAGE_COR20_HEADER;

Characteristics:

  • Small native stub (jumps to CLR)
  • Large .text section (IL code)
  • Metadata tables
  • Different analysis approach (IL vs native)

Mixed-Mode:

  • Both native and managed code
  • /CLRIMAGETYPE:IJW flag
  • More complex to analyze

16. Common Packing Indicators

Packers compress/encrypt executables to evade detection:

Structural Indicators

Section Anomalies:

  • Few sections (1–2 instead of 4–6)
  • High entropy sections (>7.0 = encrypted)
  • Unusual section names (UPX0, .themida, .aspack)
  • Writable + Executable sections
  • Virtual Size >> Raw Size (unpacking space)

Import Table:

  • Very few imports (LoadLibrary, GetProcAddress only)
  • No imports at all (custom loader)
  • Only kernel32.dll

Entry Point:

  • Points to last section (common packer technique)
  • Points to unusual location

High Entropy:

def calculate_entropy(data):
    import math
    from collections import Counter
    counter = Counter(data)
    length = len(data)
    entropy = -sum((count/length) * math.log2(count/length) 
                   for count in counter.values())
    return entropy
# Entropy ranges:
# 0-3: Low (plain text, code)
# 3-5: Medium (normal executable)
# 5-7: High (compressed)
# 7-8: Very high (encrypted/packed)

Common Packers:

  • UPX (free, open-source)
  • ASPack
  • Themida/WinLicense (strong protector)
  • VMProtect (virtualization)
  • Enigma Protector
  • PECompact
  • Armadillo

Detection Tools:

  • PEiD
  • Detect It Easy (DIE)
  • Exeinfo PE
  • PEID

17. Analysis Workflow

Step 1: Initial Validation

1. Check DOS signature (MZ)
2. Validate e_lfanew offset
3. Check PE signature (PE\0\0)
4. Verify machine type matches architecture
5. Validate section count
6. Check optional header magic

Step 2: Security Features Check

1. DllCharacteristics flags:
   - ASLR (DYNAMIC_BASE)
   - DEP (NX_COMPAT)
   - CFG (GUARD_CF)
   - No SEH (NO_SEH)
2. Load Config Directory:
   - Security Cookie
   - CFG table
   - Safe SEH handlers
3. Certificate Table:
   - Signature present?
   - Valid signature?
   - Trusted issuer?

Step 3: Behavioral Analysis

1. Analyze Import Table:
   - Categorize APIs (file, network, process, registry)
   - Look for suspicious combinations
   - Check for dynamic resolution only
2. Check for TLS callbacks
3. Examine resource section
4. Calculate section entropies

Step 4: Packing Detection

1. Section analysis:
   - Count (too few?)
   - Names (standard?)
   - Entropy (too high?)
   - Permissions (RWX?)
2. Import sparsity
3. Entry point location
4. Overlay data (data after last section)

Step 5: Deep Dive

1. Disassemble entry point
2. Trace execution flow
3. Identify unpacking routines
4. Locate original entry point (OEP)
5. Dump unpacked code
6. Rebuild import table

18. Essential Tools

Parsing & Analysis

  • PE-bear: Visual PE editor/viewer
  • CFF Explorer: Comprehensive PE editor
  • PEview: Simple PE structure viewer
  • pestudio: Malware-focused PE analyzer
  • Detect It Easy: Packer/compiler detection

Programming Libraries

  • Python: pefile, LIEF
  • C/C++: Windows API, LIEF
  • Rust: goblin, pelite
  • .NET: dnlib, AsmResolver

Disassemblers/Debuggers

  • IDA Pro: Industry standard
  • Ghidra: Free, powerful
  • x64dbg: Modern debugger
  • WinDbg: Kernel debugging
  • Binary Ninja: Modern analysis platform

Unpacking

  • UPX: For UPX packed files (upx -d)
  • Universal PE Unpacker Plugin: OllyDbg/x64dbg
  • Generic Unpacker: Automated unpacking

19. Practical Exercise: Parsing PE Headers

Here’s a Python example using pefile:

import pefile
import hashlib
def analyze_pe(filepath):
    try:
        pe = pefile.PE(filepath)

        print(f"[+] Analyzing: {filepath}\n")

        # Basic Info
        print("=== BASIC INFO ===")
        print(f"Machine: {hex(pe.FILE_HEADER.Machine)}")
        print(f"Sections: {pe.FILE_HEADER.NumberOfSections}")
        print(f"Timestamp: {pe.FILE_HEADER.TimeDateStamp}")
        print(f"Subsystem: {pe.OPTIONAL_HEADER.Subsystem}")

        # Architecture
        is_64bit = pe.OPTIONAL_HEADER.Magic == 0x20B
        print(f"Architecture: {'64-bit' if is_64bit else '32-bit'}")

        # Security Features
        print("\n=== SECURITY FEATURES ===")
        dll_chars = pe.OPTIONAL_HEADER.DllCharacteristics
        print(f"ASLR: {bool(dll_chars & 0x0040)}")
        print(f"DEP/NX: {bool(dll_chars & 0x0100)}")
        print(f"CFG: {bool(dll_chars & 0x4000)}")
        print(f"No SEH: {bool(dll_chars & 0x0400)}")

        # Sections
        print("\n=== SECTIONS ===")
        for section in pe.sections:
            name = section.Name.decode().rstrip('\x00')
            entropy = section.get_entropy()
            print(f"{name:10} RVA: {hex(section.VirtualAddress):10} "
                  f"Size: {hex(section.Misc_VirtualSize):10} "
                  f"Entropy: {entropy:.2f}")

        # Imports
        print("\n=== IMPORTS ===")
        if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
            for entry in pe.DIRECTORY_ENTRY_IMPORT:
                print(f"\n{entry.dll.decode()}:")
                for imp in entry.imports[:5]:  # First 5
                    if imp.name:
                        print(f"  - {imp.name.decode()}")
                if len(entry.imports) > 5:
                    print(f"  ... ({len(entry.imports)} total)")

        # Hashes
        print("\n=== HASHES ===")
        with open(filepath, 'rb') as f:
            data = f.read()
            print(f"MD5:    {hashlib.md5(data).hexdigest()}")
            print(f"SHA256: {hashlib.sha256(data).hexdigest()}")

        # Packing indicators
        print("\n=== PACKING INDICATORS ===")
        suspicious = False

        if pe.FILE_HEADER.NumberOfSections < 3:
            print("[!] Low section count")
            suspicious = True

        for section in pe.sections:
            if section.get_entropy() > 7.0:
                print(f"[!] High entropy in {section.Name.decode().rstrip(chr(0))}")
                suspicious = True

        if not hasattr(pe, 'DIRECTORY_ENTRY_IMPORT') or \
           len(pe.DIRECTORY_ENTRY_IMPORT) < 2:
            print("[!] Very few imports")
            suspicious = True

        if not suspicious:
            print("[+] No obvious packing indicators")

        pe.close()

    except Exception as e:
        print(f"[-] Error: {e}")
# Usage
analyze_pe("sample.exe")

20. Advanced Topics for Further Study

Reflective DLL Injection

  • Loading DLLs without Windows API
  • Manual PE mapping
  • Custom loader implementation

Process Hollowing

  • Creating suspended process
  • Unmapping original image
  • Mapping malicious PE
  • Resuming execution

Manual Mapping

  • Bypassing Windows loader
  • Custom import resolution
  • Manual relocation
  • TLS callback handling

PE Infection Techniques

  • Code cave injection
  • Section appending
  • Entry point redirection
  • Import table hooking

Anti-Forensics

  • Timestomping
  • Checksum manipulation
  • Header manipulation
  • Overlay data

Conclusion

Understanding PE internals is fundamental to Windows security research. From the legacy DOS header to modern CFG implementations, each component tells a story about the executable’s purpose, capabilities, and potential risks.

Key takeaways:

  1. Structure Matters: Headers determine loading behavior and security features
  2. Sections Define Behavior: Code, data, and resources reveal functionality
  3. Imports Show Intent: API calls indicate program capabilities
  4. Security Flags: Modern protections are crucial (ASLR, DEP, CFG)
  5. Anomalies Signal Threats: Unusual structures often indicate malicious intent

Next Steps

  1. Practice: Analyze legitimate and malicious samples
  2. Code: Write your own PE parser
  3. Experiment: Try packing/unpacking exercises
  4. Deep Dive: Study specific topics (relocations, exceptions, etc.)
  5. Stay Updated: PE format evolves with new security features

Resources

Documentation:

  • Microsoft PE/COFF Specification
  • Corkami PE101/102 posters
  • Windows Internals books (Russinovich)

Communities:

  • r/ReverseEngineering
  • OALabs (YouTube)
  • MalwareTech blog
  • hasherezade’s blog

Practice:

  • Crackmes.one
  • Flare-On challenges
  • Malware samples (malware bazaar)

Happy reversing, and remember: always analyze malware in isolated environments!


메타데이터
post_id
c0d0ca6e1813
slug
deep-dive-into-windows-pe-internals-c0d0ca6e1813
url
https://medium.com/@trmz/deep-dive-into-windows-pe-internals-c0d0ca6e1813
canonical_url
https://medium.com/@trmz/deep-dive-into-windows-pe-internals-c0d0ca6e1813
author_url
https://medium.com/@trmz
status
ok
fetched_at
2026-07-06 21:09:28