Deep Dive into Windows PE Internals
Introduction
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:
- 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
- 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 == 0x5A4Dande_lfanewpoints to valid memory - Evasion: Malware may use unusual
e_lfanewvalues 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
BaseOfDatafield 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()orWinMain() - 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
.relocsection - 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
.rdataor.idatasection
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:
- PE specifies DLLs and functions in Import Directory
- Windows loader loads specified DLLs
- Loader resolves function addresses
- 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/OFindFirstFile,FindNextFile→ Directory enumerationDeleteFile,MoveFile→ File manipulation
Network Activity:
WSAStartup,socket,connect,send,recv→ Network communicationInternetOpen,InternetConnect→ HTTP operationsGetAdaptersInfo→ Network reconnaissance
Process/Memory:
CreateProcess,CreateRemoteThread→ Process injectionVirtualAlloc,VirtualProtect→ Memory manipulationWriteProcessMemory→ Code injectionOpenProcess→ Process access
Registry:
RegOpenKeyEx,RegSetValueEx→ Registry modificationRegCreateKeyEx→ Persistence mechanism
Anti-Analysis:
IsDebuggerPresent,CheckRemoteDebuggerPresent→ Debugger detectionGetTickCount,QueryPerformanceCounter→ Timing checksFindWindow→ 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:
- By Name:
GetProcAddress(hDll, "FunctionName") - 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, skipIMAGE_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
.relocsection
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 configurationGuardCFFunctionTable: 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:
- Extract certificate from PE
- Verify signature chain to trusted root
- Check certificate not revoked
- 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.exeGet-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
.textsection (IL code) - Metadata tables
- Different analysis approach (IL vs native)
Mixed-Mode:
- Both native and managed code
/CLRIMAGETYPE:IJWflag- 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:
- Structure Matters: Headers determine loading behavior and security features
- Sections Define Behavior: Code, data, and resources reveal functionality
- Imports Show Intent: API calls indicate program capabilities
- Security Flags: Modern protections are crucial (ASLR, DEP, CFG)
- Anomalies Signal Threats: Unusual structures often indicate malicious intent
Next Steps
- Practice: Analyze legitimate and malicious samples
- Code: Write your own PE parser
- Experiment: Try packing/unpacking exercises
- Deep Dive: Study specific topics (relocations, exceptions, etc.)
- 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