Memory corruption vulnerabilities—buffer overflows, use-after-free, type confusion—have been the bread and butter of exploitation for decades. In response, Windows has developed multiple overlapping layers of memory protection, each designed to make exploitation progressively more difficult. Understanding these protections is essential for anyone working in offensive security, as bypassing them is often required for successful exploitation. For defenders, understanding these mechanisms reveals what they protect against and where gaps remain.
This chapter examines each protection layer, how it works, and the techniques attackers use to circumvent it.
Modern Windows systems implement memory protection as a layered defense, with each layer addressing different aspects of the exploitation process:
┌─────────────────────────────────────────────────────────────────────────┐
│ MEMORY PROTECTION LAYERS │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Hardware: Intel CET (Shadow Stack + IBT) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ VBS: Hypervisor-enforced Code Integrity (HVCI) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Kernel: DEP (NX bit), KASLR, SMEP, SMAP │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ User Mode: DEP, ASLR, CFG, Stack Canaries, SafeSEH │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Application: ACG, CIG, Child Process Policy │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Each layer makes different assumptions about what an attacker can and cannot do. DEP assumes attackers can corrupt memory but shouldn't execute it. ASLR assumes attackers can execute code but don't know where things are. CFG assumes attackers might redirect control flow but shouldn't reach arbitrary destinations. Together, these create defense in depth that requires attackers to solve multiple problems simultaneously.
The classic exploitation technique involved placing shellcode in a buffer (data), corrupting a return address or function pointer, and redirecting execution to the shellcode. DEP (Data Execution Prevention) breaks this model by marking data pages as non-executable. When the CPU attempts to execute code from a non-executable page, it triggers an exception and terminates the process.
MITRE ATT&CK Mitigation: M1050 - Exploit Protection
DEP leverages the NX (No-eXecute) bit in page table entries, a hardware feature available on all modern CPUs. When the NX bit is set, the page cannot be executed regardless of other permissions.
Windows offers several DEP configurations:
| Mode | Description |
|---|---|
| Hardware DEP | Uses CPU NX bit |
| Software DEP | Emulated (legacy) |
| OptIn | Only system processes and opted-in apps |
| OptOut | All processes except opted-out apps |
| AlwaysOn | All processes, no exceptions |
| AlwaysOff | DEP disabled (not recommended) |
Modern systems typically use OptOut or AlwaysOn, ensuring most processes receive DEP protection by default.
# Check DEP status
wmic OS Get DataExecutionPrevention_SupportPolicy
# Values:
# 0 = AlwaysOff
# 1 = AlwaysOn
# 2 = OptIn (default)
# 3 = OptOut
# Configure via bcdedit
bcdedit /set {current} nx AlwaysOn
Return-Oriented Programming (ROP) revolutionized exploitation when DEP made direct shellcode execution impossible. Instead of injecting new code, ROP chains together existing code snippets—called "gadgets"—that end with a ret instruction. By carefully constructing a sequence of return addresses on the stack, an attacker can perform arbitrary computations using only existing executable code.
DEP prevents direct shellcode execution
ROP chains existing code gadgets
Example ROP Chain:
1. pop rdi; ret → Set RDI = address
2. pop rsi; ret → Set RSI = size
3. pop rdx; ret → Set RDX = PAGE_EXECUTE_READWRITE
4. call VirtualProtect → Make memory executable
5. ret to shellcode → Execute payload
The typical ROP strategy is to call VirtualProtect or VirtualAlloc to create an executable memory region, then copy shellcode there and execute it. This requires finding appropriate gadgets in loaded modules—a task automated by tools like ROPgadget and ropper:
# Using ROPgadget
ROPgadget --binary target.exe --ropchain
# Using ropper
ropper --file target.exe --search "pop rdi"
JIT Spraying
Just-In-Time compilers present another DEP bypass opportunity. JIT compilers generate executable code at runtime, and an attacker can influence what code gets generated:
Abuse Just-In-Time compilers
1. Create JavaScript/ActionScript
2. Embed shellcode in constants
3. JIT compiles to executable code
4. Redirect execution to JIT code
// Query DEP policy for process
DWORD Flags;
BOOL Permanent;
GetProcessDEPPolicy(GetCurrentProcess(), &Flags, &Permanent);
// Flags:
// 0 = DEP disabled
// 1 = DEP enabled
// Permanent = Cannot be changed
DEP forces attackers to use existing code, but they still need to know where that code is located. ASLR (Address Space Layout Randomization) addresses this by randomizing the locations of key memory regions:
Without knowing where things are, attackers cannot reliably construct ROP chains or target specific functions.
The effectiveness of ASLR depends on how much randomization it provides—measured in bits of entropy:
| Component | 32-bit Entropy | 64-bit Entropy |
|---|---|---|
| Executable | 8 bits | 17 bits |
| DLL (High) | 14 bits | 19 bits |
| Stack | 14 bits | 17 bits |
| Heap | 5 bits | 17 bits |
64-bit systems benefit significantly from ASLR because the larger address space allows more randomization. On 32-bit systems, the limited address space constrains entropy, making brute-force attacks more feasible.
┌─────────────────────────────────────────────────────────────────────┐
│ ASLR EVOLUTION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Basic ASLR (Vista+) │
│ └─ Image randomization at boot │
│ └─ Same addresses per boot cycle │
│ │
│ ASLR + High Entropy (Windows 8+) │
│ └─ Requires /HIGHENTROPYVA linker flag │
│ └─ More randomization bits │
│ │
│ Force ASLR (Windows 8+) │
│ └─ Relocates even images without /DYNAMICBASE │
│ └─ System-wide setting │
│ │
│ Bottom-Up ASLR │
│ └─ Randomizes heap and stack bases │
│ └─ Independent of image ASLR │
│ │
└─────────────────────────────────────────────────────────────────────┘
High Entropy ASLR is particularly important—programs must be compiled with the /HIGHENTROPYVA flag to receive full 64-bit randomization. Without this flag, even 64-bit programs receive reduced entropy for compatibility reasons.
Information Disclosure
The most reliable ASLR bypass is leaking a pointer. If an attacker can read a memory address through any vulnerability (format string, out-of-bounds read, uninitialized memory), they can calculate base addresses:
// Leak pointer to calculate base address
PVOID leaked_ptr = GetLeakedPointer();
PVOID base = (PVOID)((ULONG_PTR)leaked_ptr - KNOWN_OFFSET);
Partial Overwrite
ASLR randomizes upper bits, but lower bits are often predictable due to page alignment:
ASLR only randomizes upper bits
Lower 12-16 bits often predictable
Attack: Overwrite only lower bytes
Result: Jump within same page
This technique works when the attacker can control only part of a pointer—the corruption redirects execution to a different offset within the same or nearby memory region.
Non-ASLR Modules
Legacy or poorly configured DLLs may lack ASLR:
Find DLL without ASLR:
- Legacy DLLs
- Third-party modules
- Java/Flash components (legacy)
Use fixed addresses from non-ASLR module
Heap Spraying
Heap spraying fills memory with shellcode copies, increasing the probability that a corrupted pointer lands on executable code:
// Fill heap with shellcode
var spray = new Array(0x1000);
for (var i = 0; i < spray.length; i++) {
spray[i] = nop_sled + shellcode;
}
// Probabilistically hit shellcode
# Check if image has ASLR
dumpbin /headers file.exe | findstr "Dynamic base"
# Using PowerShell
$pe = [System.IO.File]::ReadAllBytes("file.exe")
# Check DLL characteristics for DYNAMIC_BASE (0x40)
Even with DEP and ASLR, attackers can redirect execution to existing code. Control Flow Guard (CFG) addresses this by validating that indirect calls (calls through function pointers) target legitimate functions, not arbitrary code.
MITRE ATT&CK Mitigation: M1050 - Exploit Protection
CFG works by maintaining a bitmap of valid call targets. When the compiler encounters an indirect call, it inserts a validation check. At runtime, the target address is checked against the bitmap—if it's not a valid target, the process terminates.
┌─────────────────────────────────────────────────────────────────────┐
│ CFG MECHANISM │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Compilation: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 1. Compiler generates CFG bitmap of valid call targets │ │
│ │ 2. Indirect calls instrumented with guard check │ │
│ │ 3. Bitmap stored in PE image │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Runtime: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ call [target] │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ __guard_dispatch_icall_fptr(target) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ntdll!LdrpValidateUserCallTarget │ │
│ │ │ │ │
│ │ ├── Valid: Continue call │ │
│ │ └── Invalid: Terminate process │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
The bitmap has a granularity of 8 bytes—each bit represents whether the corresponding 8-byte aligned address is a valid call target:
// CFG bitmap structure
// Each bit represents 8 bytes (granularity)
// 1 = Valid call target
// 0 = Invalid call target
// Bitmap lookup:
BOOL IsValidTarget(PVOID target) {
ULONG_PTR offset = (ULONG_PTR)target >> 3; // Divide by 8
ULONG_PTR bit = offset & 0x7; // Bit within byte
ULONG_PTR byte_offset = offset >> 3; // Byte in bitmap
return (bitmap[byte_offset] >> bit) & 1;
}
Bitmap Corruption
With an arbitrary write primitive, an attacker can mark their target as valid:
// If attacker has arbitrary write:
// Modify CFG bitmap to mark target as valid
PVOID cfg_bitmap = GetCfgBitmap();
ULONG_PTR target_offset = (ULONG_PTR)target >> 3;
ULONG_PTR byte_offset = target_offset >> 3;
ULONG_PTR bit = target_offset & 0x7;
// Set bit in bitmap
cfg_bitmap[byte_offset] |= (1 << bit);
Valid Target Abuse
Many legitimate functions are marked as valid call targets but can be abused:
Find valid call targets that are useful:
- VirtualProtect (change memory protections)
- WinExec (execute commands)
- LoadLibrary (load arbitrary DLLs)
Chain valid targets for exploitation
JIT Code
JIT-compiled code is automatically added to the CFG bitmap:
JIT-compiled code is added to CFG bitmap
1. Trigger JIT compilation
2. Shellcode compiled as JIT code
3. JIT code is marked valid by CFG
4. Call JIT code (CFG allows)
Exception Handlers
Exception handlers may not be fully protected:
Exception handlers may not be CFG-protected
1. Corrupt exception handler chain
2. Trigger exception
3. Execute arbitrary handler
XFG extends CFG with type-based checking—the target must not only be a valid function but must have the correct function signature:
XFG adds type-based checking:
- Call targets must match function prototype
- Hash of function signature validated
- Prevents calling wrong function type
Bypass: Find function with matching signature
XFG significantly narrows the attack surface by preventing attackers from calling arbitrary valid functions.
Intel Control-flow Enforcement Technology (CET) provides hardware-enforced control flow integrity. While CFG is implemented in software, CET uses CPU features that cannot be bypassed through memory corruption alone.
CET has two components:
The shadow stack is a hardware-maintained copy of return addresses. When a function is called, the return address is pushed to both the regular stack and the shadow stack. On return, both values are compared—if they don't match, the CPU raises an exception.
┌─────────────────────────────────────────────────────────────────────┐
│ SHADOW STACK │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Regular Stack Shadow Stack │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Local Vars │ │ │ │
│ │ Saved RBP │ │ │ │
│ │ Return Addr ├───────▶│ Return Addr │ ← Mirrored │
│ │ Parameters │ │ │ │
│ ├─────────────┤ ├─────────────┤ │
│ │ Local Vars │ │ │ │
│ │ Saved RBP │ │ │ │
│ │ Return Addr ├───────▶│ Return Addr │ ← Mirrored │
│ │ Parameters │ │ │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ On RET: │
│ 1. Pop return address from regular stack │
│ 2. Pop return address from shadow stack │
│ 3. Compare: If mismatch → #CP exception │
│ │
└─────────────────────────────────────────────────────────────────────┘
This effectively kills ROP—even if an attacker corrupts return addresses on the regular stack, the shadow stack retains the original values.
IBT ensures indirect jumps and calls land on valid targets marked with the ENDBR instruction:
ENDBR64/ENDBR32 instruction:
- Must be at valid indirect branch targets
- CPU tracks indirect branches
- Landing site must have ENDBR
- Otherwise: #CP exception
Compiler inserts ENDBR at:
- Function entry points
- Jump table targets
- Other indirect targets
Shadow Stack Attacks
The shadow stack is extremely difficult to attack:
Shadow stack is hardware-protected
- Cannot be directly modified
- Separate memory region
- Kernel-managed
Attack surface:
- Race conditions during context switch
- Kernel vulnerabilities
- Hypervisor attacks
IBT Attacks
IBT can potentially be bypassed by finding useful code after ENDBR instructions:
All valid targets have ENDBR
Attack: Find ENDBR gadgets
ENDBR32 = 0xF3 0x0F 0x1E 0xFB
ENDBR64 = 0xF3 0x0F 0x1E 0xFA
Look for useful code after ENDBR instruction
However, the limited availability of useful ENDBR gadgets significantly constrains attackers.
# Check if CPU supports CET
Get-CimInstance Win32_Processor | Select-Object Name, Caption
# Check if process uses CET
# Requires Windows 11+
Stack canaries (the /GS compiler flag) place a random value between local variables and the return address. Buffer overflows that corrupt the return address will also corrupt the canary, which is detected before the function returns:
// Compiler inserts canary before return address
// /GS compiler flag
void VulnerableFunction(char* input) {
// Compiler adds:
// DWORD cookie = __security_cookie ^ RBP;
// [stack layout: cookie | locals | saved_rbp | return_addr]
char buffer[64];
strcpy(buffer, input); // Overflow possible
// Compiler adds check before return:
// if (cookie != (__security_cookie ^ RBP)) {
// __report_gsfailure();
// }
}
Information Leak
If the canary value can be leaked through another vulnerability, the attacker can include it in their payload:
// Leak canary value through separate vulnerability
// Format string, out-of-bounds read, etc.
printf(user_input); // If input = "%p %p %p %p"
// May leak canary from stack
Canary Prediction
On Windows, canaries are derived from __security_cookie:
On Windows:
- Canary derived from __security_cookie
- Cookie initialized at process start
- Can be predicted if:
- Process restart known
- Fork child inherits cookie
Exception Handler Overwrite
SEH handlers are checked before the canary:
SEH handlers stored before canary check
1. Overflow past canary and return address
2. Overwrite SEH handler
3. Trigger exception
4. Handler executed before canary check
SafeSEH maintains a list of valid exception handlers and terminates the process if an invalid handler is invoked:
Structured Exception Handler Overwrite Protection
- /SAFESEH linker flag
- Maintains table of valid handlers
- Invalid handler causes termination
Bypass:
- Non-SafeSEH module in process
- Heap-based handlers
- Direct code execution via other means
SEHOP (SEH Overwrite Protection) validates the integrity of the entire exception handler chain:
Validates SEH chain integrity
- Last handler must point to ntdll!FinalExceptionHandler
- Chain must be valid linked list
- OS-level protection (Vista+)
Bypass:
- Requires precise chain reconstruction
- Must know handler addresses
Stack spoofing creates fake call stacks to evade detection. EDR products often inspect thread stacks to identify suspicious activity—a thread that appears to originate from shellcode or unknown memory is suspicious. Stack spoofing makes malicious threads appear to have normal call stacks.
// Build fake stack frame for detection evasion
typedef struct _SPOOF_CONTEXT {
PVOID ReturnAddress; // Fake return address
PVOID Rbp; // Fake frame pointer
PVOID Rsp; // Stack pointer
PVOID Function; // Real function to call
PVOID Arguments[4]; // Function arguments
} SPOOF_CONTEXT;
// Stack layout after spoofing:
// ┌─────────────────────────────────┐
// │ Fake return addr (ntdll func) │
// │ Fake RBP → next fake frame │
// │ ... legitimate-looking data ... │
// │ Fake return addr (kernel32) │
// │ Fake RBP → next fake frame │
// │ ... more fake frames ... │
// │ RtlUserThreadStart │ ← Final frame
// └─────────────────────────────────┘
Creating convincing fake frames requires understanding how Windows unwinds stacks. The PE exception directory contains UNWIND_INFO structures that describe each function's stack layout:
// Parse PE exception directory for accurate frame sizes
typedef struct _UNWIND_INFO {
UCHAR Version : 3;
UCHAR Flags : 5;
UCHAR SizeOfProlog;
UCHAR CountOfCodes;
UCHAR FrameRegister : 4;
UCHAR FrameOffset : 4;
UNWIND_CODE UnwindCode[1];
} UNWIND_INFO;
// Calculate frame size from unwind codes
SIZE_T CalculateFrameSize(PUNWIND_INFO UnwindInfo) {
SIZE_T size = 0;
for (int i = 0; i < UnwindInfo->CountOfCodes; i++) {
// Process each unwind code
// UWOP_ALLOC_LARGE, UWOP_ALLOC_SMALL, etc.
}
return size;
}
Good fake return addresses appear in legitimate thread stacks:
Good return addresses for spoofing:
1. NtWaitForSingleObject + offset
- Common in legitimate thread stacks
2. WaitForSingleObjectEx + offset
- Win32 wait wrapper
3. BaseThreadInitThunk + offset
- Thread entry point
4. RtlUserThreadStart + offset
- Final frame in all stacks
ACG prevents dynamic code generation entirely:
Prevents dynamic code generation:
- No PAGE_EXECUTE_READWRITE
- No PAGE_EXECUTE_WRITECOPY
- JIT compilation restrictions
SetProcessMitigationPolicy(ProcessDynamicCodePolicy, ...)
Applications using ACG cannot create executable memory at runtime, effectively blocking JIT-based attacks and many shellcode injection techniques.
CIG ensures only properly signed code can execute:
Only signed code can execute:
- Blocks unsigned DLL injection
- Blocks reflective loading
SetProcessMitigationPolicy(ProcessSignaturePolicy, ...)
These kernel-mode protections prevent the kernel from executing or accessing user-mode memory:
SMEP (Supervisor Mode Execution Prevention):
- Kernel cannot execute user-mode pages
- Prevents ret2usr attacks
SMAP (Supervisor Mode Access Prevention):
- Kernel cannot access user-mode pages
- Prevents data-only kernel attacks
# Check process mitigations
Get-ProcessMitigation -Name process.exe
# Check system-wide defaults
Get-ProcessMitigation -System
EDR products employ various techniques to detect ROP:
EDR detections:
1. Stack pivot detection (RSP far from TEB stack)
2. Return address validation
3. Gadget pattern detection
4. CFG bypass detection
| Event | Description |
|---|---|
| Windows Defender Exploit Guard | Block/Audit mode events |
| ETW: Microsoft-Windows-Security-Mitigations | Mitigation telemetry |
| Application crash (0xC0000409) | Stack buffer overrun |
| Application crash (#CP) | CET violation |
Memory protection has evolved from simple DEP to comprehensive hardware-enforced solutions. Each protection addresses specific exploitation techniques:
| Protection | Bypass | Difficulty | Coverage |
|---|---|---|---|
| DEP | ROP chains | Medium | Code pages |
| ASLR | Info leak | Medium | Address layout |
| CFG | Valid target abuse | Medium | Indirect calls |
| CET Shadow Stack | None practical | High | Return addresses |
| CET IBT | ENDBR gadgets | High | Indirect branches |
| Stack Canaries | Leak/bypass | Low | Stack overflow |
| SafeSEH/SEHOP | Non-SafeSEH DLL | Medium | SEH exploitation |
| ACG | External JIT | High | Dynamic code |
The trend is clear: hardware-based protections like CET are increasingly difficult to bypass. Exploitation is moving toward logic vulnerabilities, type confusion, and other attacks that don't rely on corrupting control flow. For defenders, enabling all available mitigations—particularly CET and HVCI on supported hardware—dramatically raises the bar for attackers.
For offensive security professionals, understanding these protections guides technique selection and reveals where creative solutions are needed. The era of simple buffer overflows is long past; modern exploitation requires chaining multiple bugs and bypassing multiple mitigations simultaneously.