Windows Memory Layout: A Security Researcher's Guide
If you want to exploit software, analyze malware, or reverse engineer binaries on Windows, there’s one thing you need to understand before anything else — how memory is laid out. Where does the code live? Where’s the stack? Where does the heap start? What’s the PEB, and why does every piece of malware walk its module lists?
Every buffer overflow, every ROP chain, every process injection technique is built on top of a mental model of how memory is organized. If that model is wrong or incomplete, you’re flying blind.
In this post, we’ll walk through the Windows process memory layout from top to bottom — what each region is, why it exists, how to inspect it with real tools, and what it means for security research.
Virtual Memory — The Big Picture
Every Windows process gets its own virtual address space. The process thinks it has a flat, contiguous block of memory all to itself, starting from address 0x0. In reality, the OS and hardware collaborate to map these virtual addresses to physical RAM (or disk) through page tables.
This isolation is critical for both stability and security. Process A can’t read Process B’s memory because they have completely different page tables — the same virtual address 0x00400000 maps to different physical memory in each process.
Pages and Permissions
Memory is managed in pages — 4KB (4096 bytes) on x86/x64 by default. Each page has permissions:
| Permission | Meaning |
|---|---|
| R (Read) | The page can be read |
| W (Write) | The page can be written to |
| X (Execute) | The page can be executed as code |
| RW | Read + Write (typical for data, stack, heap) |
| RX | Read + Execute (typical for code sections) |
| RWX | Read + Write + Execute (dangerous — shellcode target) |
Modern Windows almost never creates RWX pages by default. Code pages are RX, data pages are RW. If you see an RWX page in a process, that’s either JIT compilation (like a browser’s JavaScript engine), a packer unpacking itself, or something suspicious.
32-bit vs 64-bit Address Space
On 32-bit Windows, the address space is 4GB:
- User mode:
0x00000000–0x7FFFFFFF(2GB) - Kernel mode:
0x80000000–0xFFFFFFFF(2GB)
On 64-bit Windows, the theoretical space is 16 exabytes, but only a portion is currently usable:
- User mode:
0x00000000'00000000–0x00007FFF'FFFFFFFF(128TB) - Kernel mode:
0xFFFF8000'00000000–0xFFFFFFFF'FFFFFFFF(128TB)
There’s a massive gap in the middle — the “canonical address hole” — where addresses are invalid by hardware design.
The Memory Map — What Lives Where
Here’s a map of a typical 64-bit Windows process, from low addresses to high:
┌─────────────────────────────────────────────────────┐
│ 0x00000000'00000000 - 0x00000000'0000FFFF │
│ NULL Page (64KB) — Unmapped, catches NULL derefs │
├─────────────────────────────────────────────────────┤
│ 0x00000000'00010000+ │
│ Executable Image (.exe) │
│ ┌─────────────────────────────────────────┐ │
│ │ PE Header │ │
│ │ .text (code) [RX] │ │
│ │ .rdata (read-only data) [R] │ │
│ │ .data (initialized) [RW] │ │
│ │ .bss (uninitialized) [RW] │ │
│ │ .rsrc (resources) [R] │ │
│ └─────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────┤
│ DLLs (ntdll.dll, kernel32.dll, user32.dll, ...) │
│ Each with their own .text, .data, etc. │
│ Positions randomized by ASLR │
├─────────────────────────────────────────────────────┤
│ Heap(s) │
│ Default process heap + additional heaps │
│ Grows upward [RW] │
├─────────────────────────────────────────────────────┤
│ Memory-Mapped Files / Shared Memory │
│ │
├─────────────────────────────────────────────────────┤
│ Thread Stacks (one per thread) │
│ Grow downward [RW] │
├─────────────────────────────────────────────────────┤
│ Thread Environment Blocks (TEBs) │
│ Process Environment Block (PEB) │
├─────────────────────────────────────────────────────┤
│ 0x00007FFF'FFFFFFFF │
│ End of user mode │
├═════════════════════════════════════════════════════┤
│ 0xFFFF8000'00000000+ │
│ Kernel Mode — not accessible from user mode │
│ (kernel, drivers, SSDT, IDT, etc.) │
└─────────────────────────────────────────────────────┘
Let’s explore each region in detail.
The NULL Page
The first 64KB of memory (0x00000000 – 0x0000FFFF) is intentionally unmapped. Any access to this region triggers an access violation — the famous “null pointer dereference.”
This exists to catch bugs. When a programmer forgets to check if a pointer is NULL and tries to dereference it, the crash makes the bug obvious. Without this protection, dereferencing NULL would silently read or write to address 0, corrupting memory in ways that are incredibly hard to debug.
From a security perspective, the NULL page also prevents a class of kernel exploits where an attacker maps data at address 0 and tricks a kernel-mode driver into dereferencing a NULL pointer. Modern Windows blocks user-mode allocations at low addresses to prevent this.
The Executable Image — PE Format
When you run a .exe, Windows loads it into memory starting at its preferred base address (traditionally 0x00400000 for 32-bit, though ASLR changes this). The file on disk is a PE (Portable Executable), and understanding its structure is essential.
PE Sections
The PE file is divided into sections, each with a specific purpose and permissions:
| Section | Contains | Permissions | Security Relevance |
|---|---|---|---|
.text |
Compiled machine code | RX | Where ROP gadgets live |
.rdata |
Read-only data, import tables, string constants | R | Contains IAT — a hooking target |
.data |
Initialized global/static variables | RW | Writable — can store attacker data |
.bss |
Uninitialized global/static variables | RW | Writable, initially zeroed |
.rsrc |
Resources (icons, dialogs, embedded files) | R | Malware hides payloads here |
.reloc |
Relocation entries (for ASLR) | R | Needed for base address randomization |
You can inspect these with dumpbin (from Visual Studio) or WinDbg:
0:000> !dh notepad -f
SECTION HEADER #1
.text name
8C32 virtual size
1000 virtual address
8E00 size of raw data
400 file pointer to raw data
...
60000020 flags
Code
Execute Read
SECTION HEADER #2
.rdata name
3A4C virtual size
A000 virtual address
...
40000040 flags
Initialized Data
Read Only
The Import Address Table (IAT)
The IAT is one of the most security-relevant structures in a PE. It’s a table of function pointers — when your program calls CreateFileW, it doesn’t jump directly to the function’s code. Instead, it reads the address from the IAT and jumps there.
Your code: call [IAT_entry_for_CreateFileW]
IAT entry: 0x00007FFA'12345678 → points to CreateFileW in kernel32.dll
This indirection makes the IAT a prime target:
- Malware hooks — Overwrite an IAT entry to redirect API calls through malicious code
- API monitoring — Debuggers and analysis tools hook the IAT to log function calls
- Exploitation — Overwriting an IAT entry with
system()or a ROP gadget can redirect execution
You can list imports in WinDbg:
0:000> !imports notepad
kernel32.dll
00007FF6'B2F0A000 GetModuleHandleW
00007FF6'B2F0A008 GetProcAddress
00007FF6'B2F0A010 LoadLibraryW
00007FF6'B2F0A018 VirtualAlloc
...
DLLs — Shared Libraries
Windows processes load dozens of DLLs. Some are always present:
| DLL | Role | Why It Matters |
|---|---|---|
| ntdll.dll | Lowest user-mode layer, contains syscall stubs | The gateway to the kernel. Every syscall goes through here |
| kernel32.dll | Core Win32 API (files, processes, threads) | CreateProcess, VirtualAlloc, LoadLibrary — the functions exploits need |
| kernelbase.dll | Implementation behind kernel32 (Win7+) | Many kernel32 functions are forwarded here |
| user32.dll | Window management, UI | Loaded by any GUI application |
| msvcrt.dll | C runtime library | malloc, free, printf, system |
DLLs are loaded at base addresses determined by ASLR. Each DLL has its own .text, .data, and .rdata sections, just like the main executable.
From a security research perspective, DLLs are where you find ROP gadgets — small instruction sequences ending in ret that can be chained together to build exploits when DEP prevents direct shellcode execution. Since ntdll.dll and kernel32.dll are loaded in every process, gadgets found in them are universally usable.
0:000> lm
start end module name
00007ff6'b2ea0000 00007ff6'b2edf000 notepad
00007ffa'4c8a0000 00007ffa'4ca9e000 ntdll
00007ffa'4b4a0000 00007ffa'4b5a0000 KERNEL32
00007ffa'49c10000 00007ffa'49f70000 KERNELBASE
The Stack
Each thread gets its own stack — typically 1MB by default. The stack is fundamental to how programs execute, and it’s where the most classic exploitation technique lives: the stack buffer overflow.
How the Stack Works
The stack grows downward — from high addresses to low addresses. Every function call pushes data onto the stack:
High Address
┌─────────────────────────────────────┐
│ ...caller's stack frame... │
├─────────────────────────────────────┤
│ Function arguments (pushed by caller)│
├─────────────────────────────────────┤
│ Return address (pushed by CALL) │ ◄── This is what buffer overflows target
├─────────────────────────────────────┤
│ Saved EBP/RBP (frame pointer) │
├─────────────────────────────────────┤
│ Stack cookie / canary │ ◄── Protection against overflows
├─────────────────────────────────────┤
│ Local variable: char buf[64] │ ◄── Buffer overflow starts here
├─────────────────────────────────────┤
│ Local variable: int counter │
├─────────────────────────────────────┤
│ ... │
└─────────────────────────────────────┘
Low Address (ESP/RSP points here)
A stack buffer overflow writes past the end of buf[64], overwriting the stack cookie, saved frame pointer, and ultimately the return address. When the function returns, it jumps to the attacker-controlled address instead of back to the caller.
Stack Guards
Between the local variables and the saved frame pointer, the compiler inserts a stack cookie (also called a canary or /GS protection in MSVC). This is a random value placed before the return address. Before the function returns, it checks if the cookie has been modified:
// Compiler-generated pseudocode:
void vulnerable_function() {
int cookie = __security_cookie ^ (int)&cookie; // Generate canary
char buf[64];
gets(buf); // Overflow!
if (cookie != (__security_cookie ^ (int)&cookie)) {
__report_gsfailure(); // Abort — overflow detected!
}
return; // Only reached if canary is intact
}
If an overflow overwrites the return address, it also corrupts the canary. The check fails, and the program terminates before the attacker’s address is used.
You can see the stack in WinDbg:
0:000> k
# Child-SP RetAddr Call Site
00 00000034'fd4ff7e8 00007ffa'4c93a6e0 ntdll!NtWaitForSingleObject+0x14
01 00000034'fd4ff7f0 00007ff6'b2eb1234 ntdll!RtlpWaitOnCriticalSection+0xfc
02 00000034'fd4ff890 00007ff6'b2eb5678 notepad!WinMain+0x1a4
03 00000034'fd4ff920 00007ffa'4b4b7614 notepad!__mainCRTStartup+0x1a0
0:000> !teb
TEB at 00000034fd3fe000
ExceptionList: 0000000000000000
StackBase: 00000034fd500000
StackLimit: 00000034fd4fb000
...
The Heap
The heap is where dynamic allocations live — every malloc(), new, HeapAlloc(), and VirtualAlloc() comes from here. Unlike the stack, which is structured and predictable, the heap is a complex, fragmented mess. And that complexity is what makes heap exploitation both powerful and difficult.
Heap Architecture
Windows has a layered heap architecture:
Application
│
▼
malloc() / new / HeapAlloc()
│
▼
NT Heap Manager (ntdll.dll)
├── Front End: Low Fragmentation Heap (LFH)
│ └── For frequent, small allocations (< 16KB)
└── Back End: Standard heap allocator
└── For larger or less frequent allocations
│
▼
VirtualAlloc() — for very large allocations (> 512KB)
│
▼
Kernel (NtAllocateVirtualMemory)
Heap Metadata
Every heap allocation has metadata — hidden headers that the heap manager uses to track allocated and freed blocks. These headers are what heap exploits target.
A simplified view of a heap chunk:
┌──────────────────────────────┐
│ Heap chunk header (16 bytes)│ ◄── Size, flags, encoding
├──────────────────────────────┤
│ User data │ ◄── What malloc() returns a pointer to
│ (the bytes you requested) │
├──────────────────────────────┤
│ Padding / alignment │
└──────────────────────────────┘
In heap exploitation, corrupting these headers can lead to arbitrary write primitives — the ability to write any value to any address — which is typically enough to gain code execution.
Inspecting the Heap
0:000> !heap -s
LFH Key : 0x3a7f9d1e2b6c
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR
0000023a14230000 00000002 1024 512 1024 24 12 1
0000023a14200000 00001002 64 4 64 2 1 1
0:000> !heap -a 0000023a14230000
Segment at 0000023a14230000 to 0000023a14330000
Flags: 00000002
ForceFlags: 00000000
Granularity: 16 bytes
Segment Reserve: 00100000
...
Low Fragmentation Heap (LFH)
The LFH activates automatically when the heap manager detects a pattern of similarly-sized allocations (typically after 17+ allocations of the same size bucket). The LFH changes the allocation behavior significantly:
- Allocations come from buckets of fixed sizes
- Within a bucket, allocation order is randomized — you can’t predict which slot you’ll get
- This randomization is a security feature, making heap feng shui (deterministic heap layout manipulation) much harder
This matters for exploitation because reliable heap exploits often depend on controlling the relative positions of allocations. The LFH’s randomization breaks those assumptions.
PEB and TEB — Process and Thread Metadata
These structures are goldmines for both malware and security researchers.
Thread Environment Block (TEB)
Every thread has a TEB — a per-thread data structure accessible via the GS segment register (64-bit) or FS register (32-bit).
// 64-bit: GS:[0x30] points to the TEB itself
// 32-bit: FS:[0x18] points to the TEB itself
TEB Structure (key fields):
┌──────────────────────────────────────────────┐
│ +0x000 ExceptionList (SEH chain) │
│ +0x008 StackBase │
│ +0x010 StackLimit │
│ +0x030 Self (pointer to this TEB) │
│ +0x060 PEB pointer │ ◄── How you find the PEB
│ +0x068 LastErrorValue │
│ +0x1478 DeallocationStack │
└──────────────────────────────────────────────┘
On 32-bit systems, the Structured Exception Handler (SEH) chain at offset 0x000 was historically a popular exploitation target. Overwriting an SEH handler with a controlled address allowed code execution when an exception was triggered.
Process Environment Block (PEB)
The PEB is a per-process structure that contains critical information about the running process. Accessible from the TEB at offset 0x060 (64-bit) or 0x030 (32-bit).
PEB Structure (key fields):
┌───────────────────────────────────────────────────┐
│ +0x002 BeingDebugged │ ◄── Anti-debugging check
│ +0x010 ImageBaseAddress │ ◄── Base of the .exe
│ +0x018 Ldr (PEB_LDR_DATA*) │ ◄── Loaded module lists
│ +0x020 ProcessParameters │ ◄── Command line, env vars
│ +0x030 ProcessHeap │ ◄── Default heap handle
│ +0x0EC NumberOfHeaps │
│ +0x0F0 MaximumNumberOfHeaps │
│ +0x0F8 ProcessHeaps (array of heap handles) │
└───────────────────────────────────────────────────┘
Why malware loves the PEB:
Almost every piece of shellcode and malware needs to find the addresses of Windows API functions like LoadLibrary and GetProcAddress. But shellcode can’t use import tables — it’s injected code with no PE structure. So it walks the PEB’s module lists to find loaded DLLs, then parses their export tables to find function addresses.
The classic technique:
1. Access TEB via GS:[0x30] (64-bit)
2. Read PEB pointer at TEB+0x060
3. Read Ldr at PEB+0x018
4. Walk InMemoryOrderModuleList:
- First entry: the .exe itself
- Second entry: ntdll.dll
- Third entry: kernel32.dll ◄── This is what we want
5. Get kernel32.dll base address
6. Parse its export table to find GetProcAddress
7. Use GetProcAddress to find any other function
In WinDbg:
0:000> !peb
PEB at 0000002b7e4e5000
InheritedAddressSpace: No
ReadImageFileExecOptions: No
BeingDebugged: Yes
ImageBaseAddress: 00007ff6b2ea0000
Ldr 00007ffa4c9d53c0
Ldr.InLoadOrderModuleList:
00007ff6'b2ea0000 notepad.exe
00007ffa'4c8a0000 ntdll.dll
00007ffa'4b4a0000 KERNEL32.DLL
00007ffa'49c10000 KERNELBASE.dll
...
The BeingDebugged flag at PEB+0x002 is the simplest anti-debugging check. Malware reads this byte — if it’s 1, a debugger is attached, and the malware changes behavior or exits. The Windows API IsDebuggerPresent() literally just reads this byte.
Security Mechanisms
Now let’s look at the protections that make exploiting these memory regions harder.
ASLR — Address Space Layout Randomization
ASLR randomizes the base addresses of:
- The executable image
- All loaded DLLs
- The heap
- The stack
- The PEB and TEB
Each time the process starts, everything is at a different location. An exploit that hardcodes jmp 0x7FFA4C901234 will fail because that address is only valid for one specific run.
# Run 1:
kernel32.dll base: 0x00007FFA'4B4A0000
# Run 2:
kernel32.dll base: 0x00007FFA'51230000
# Run 3:
kernel32.dll base: 0x00007FFA'4E7F0000
Bypassing ASLR requires an information leak — a vulnerability that discloses a memory address. Once you know the actual address of even one module, you can calculate the addresses of everything else in that module (the internal offsets don’t change, only the base address does).
Common info leak sources:
- Format string vulnerabilities (
%pleaks stack pointers) - Uninitialized memory (stack/heap data leaking through output)
- Out-of-bounds reads (reading past a buffer to reach adjacent pointers)
DEP — Data Execution Prevention
DEP (also called NX — No eXecute) marks memory pages as either executable or writable, but never both at the same time. The stack and heap are marked as non-executable. If you redirect execution to shellcode on the stack, the CPU raises an exception because the page doesn’t have execute permission.
Stack page: RW (read/write, no execute) → Shellcode can't run here
Code page: RX (read/execute, no write) → Can't write shellcode here
Bypassing DEP is done through code reuse attacks — primarily ROP (Return-Oriented Programming). Instead of executing injected code, the attacker chains together small snippets of existing code (called gadgets) that end in ret. Each gadget does one small operation, and the chain of return addresses on the stack sequences them together.
A ROP chain might look like:
Stack (attacker-controlled):
┌────────────────────────────────┐
│ Address of: pop rcx; ret │ ← gadget 1: load RCX
│ 0x40 (PAGE_EXECUTE_READWRITE) │ ← value for RCX (new page protection)
│ Address of: pop rdx; ret │ ← gadget 2: load RDX
│ 0x1000 (size) │ ← value for RDX
│ Address of: VirtualProtect │ ← make the stack page executable
│ Address of shellcode on stack │ ← now shellcode can run!
└────────────────────────────────┘
The chain calls VirtualProtect to change the stack page to RWX, then jumps to the shellcode. DEP bypassed.
Control Flow Guard (CFG)
CFG protects indirect calls — function calls through pointers (like virtual function tables or function pointers in structs). Without CFG, an attacker who overwrites a function pointer can redirect execution anywhere. With CFG, each indirect call is validated against a bitmap of legitimate call targets:
// Without CFG:
call [rax] // Jump wherever RAX points — no validation
// With CFG (compiler-generated):
call __guard_check_icall_fptr // Validate that [RAX] is a valid target
call [rax] // Only reached if validation passes
If the address isn’t in the bitmap of valid targets, the process terminates. This makes ROP gadgets that use indirect calls much harder to leverage.
Heap Protections
Modern Windows heaps have multiple protections:
| Protection | What It Does | What It Prevents |
|---|---|---|
| Safe Unlinking | Validates forward/back pointers before unlinking a freed chunk | Classic heap unlink exploits |
| Heap Cookies | Random values encoded in chunk headers | Metadata corruption detection |
| Guard Pages | Unmapped pages at heap segment boundaries | Heap overruns into adjacent segments |
| LFH Randomization | Randomizes allocation order within buckets | Deterministic heap layout manipulation |
| Encoded Headers | Heap chunk headers are XOR-encoded with a per-heap key | Direct metadata manipulation |
Inspecting Memory with Tools
WinDbg — The Power Tool
WinDbg is the most powerful tool for memory analysis on Windows. Essential commands:
!address — Full virtual memory map with permissions
!address -summary — Memory usage summary
!vprot <addr> — Protection flags for a specific address
lm — List loaded modules with base addresses
!dh <module> -f — PE header details of a module
!peb — Process Environment Block
!teb — Thread Environment Block
!heap -s — Heap summary
!heap -a <addr> — Detailed heap info
k — Call stack (shows return addresses)
dps <addr> — Display memory as pointers with symbols
A real !address -summary output:
0:000> !address -summary
Usage Summary
TotSize ( KB) Pct(Tots) Pct(Busy) Usage
- - - - - - - - - - - - - - - - - - - - - - - - -
1f9c000 ( 32496) 0.00% 15.48% Image
5f8000 ( 6112) 0.00% 2.91% Stack
1a14000 ( 26704) 0.00% 12.73% Heap
e568000 ( 234912) 0.00% 68.88% Other
...
VMMap — Visual Memory Analysis
Microsoft’s VMMap gives you a visual, color-coded view of the entire address space. You can see at a glance how much memory is used by images, heaps, stacks, and other regions. It’s excellent for getting a high-level overview before diving into WinDbg for specifics.
Process Hacker / Process Explorer
These tools show per-process memory usage, loaded DLLs, handles, and threads. Process Hacker can also dump memory regions and search for patterns — useful for finding shellcode or unpacked malware in memory.
Practical Applications
Malware Analysis
Understanding memory layout is essential for analyzing:
Process Hollowing — Malware creates a suspended process, unmaps its image, maps a malicious image at the same address, then resumes execution. You detect this by comparing the in-memory image against the file on disk.
DLL Injection — Malware injects a DLL into another process using VirtualAllocEx + WriteProcessMemory + CreateRemoteThread. You spot this by looking for DLLs loaded from unusual paths or memory regions with RWX permissions that contain PE headers.
Manual Mapping — A stealthier version of DLL injection where the malware manually maps a DLL into memory without calling LoadLibrary, so it doesn’t appear in the PEB’s module lists. You can still find it by scanning for PE signatures (MZ / PE) in memory regions that don’t correspond to any known module.
Reflective Loading — The injected DLL contains its own loader and resolves imports itself. No file on disk, no module list entry, no LoadLibrary call. Detection requires scanning for anomalous executable pages.
Exploit Development
When developing exploits, your mental model of memory needs to be precise:
- Find the vulnerability — Buffer overflow, use-after-free, type confusion
- Control EIP/RIP — Overwrite a return address, function pointer, or virtual table entry
- Bypass ASLR — Find an information leak to disclose a module base address
- Bypass DEP — Build a ROP chain using gadgets from known modules
- Bypass CFG — Target valid call targets or find gadgets that don’t use indirect calls
- Achieve execution — Pivot the stack, allocate RWX memory, write and jump to shellcode
Every step requires knowing where things are in memory and how the protections interact.
Final Thoughts
Windows memory layout isn’t just a topic you study once and move on. It’s the foundation that underpins everything in offensive and defensive security research on the Windows platform. Every CVE advisory for a memory corruption vulnerability, every malware technique, every mitigation bypass — they all play out in the address spaces we’ve mapped out here.
The key takeaways:
- Every process has its own virtual address space, divided into user mode and kernel mode
- The PE format defines how executables and DLLs are mapped into memory, and the IAT is a critical structure for both hooking and exploitation
- The stack grows downward and holds return addresses — the classic overflow target, now protected by canaries and ASLR
- The heap is complex, fragmented, and protected by metadata encoding and safe unlinking
- The PEB and TEB contain process/thread metadata that both malware and shellcode rely on to resolve API addresses
- ASLR, DEP, CFG, and heap protections form layers of defense — breaking one isn’t enough, you need to bypass the entire chain
If you want to go deeper, fire up WinDbg, attach it to a process, and start exploring. Run !address, walk the PEB, inspect the heap. There’s no substitute for hands-on exploration.
Keep digging.