Page Protection
PAGE_* constants, DEP/NX enforcement, guard pages for stack growth, VirtualProtect for shellcode staging, and Control-flow Enforcement Technology as the modern defense
Classic shellcode injection: allocate with PAGE_READWRITE, write shellcode, flip to PAGE_EXECUTE_READ, run it. EDRs detect the protection change — the transition from non-executable to executable on a private allocation is the exact fingerprint. Understanding page protection mechanics tells you both why the attack works and why the detection works.
PAGE_* Protection Constants
| Constant | Value | Read | Write | Execute |
|---|---|---|---|---|
PAGE_NOACCESS | 0x01 | ✗ | ✗ | ✗ |
PAGE_READONLY | 0x02 | ✓ | ✗ | ✗ |
PAGE_READWRITE | 0x04 | ✓ | ✓ | ✗ |
PAGE_WRITECOPY | 0x08 | ✓ | ✓ (CoW) | ✗ |
PAGE_EXECUTE | 0x10 | ✗ | ✗ | ✓ |
PAGE_EXECUTE_READ | 0x20 | ✓ | ✗ | ✓ |
PAGE_EXECUTE_READWRITE | 0x40 | ✓ | ✓ | ✓ |
PAGE_EXECUTE_WRITECOPY | 0x80 | ✓ | ✓ (CoW) | ✓ |
Modifier flags (ORed with a base constant):
PAGE_GUARD(0x100): Guard page — triggers a STATUS_GUARD_PAGE exception on first access, then the guard is removedPAGE_NOCACHE(0x200): Disable CPU cache for this page (hardware I/O mapping)PAGE_WRITECOMBINE(0x400): Write-combining (GPU/video memory optimization)
Data Execution Prevention (DEP / NX)
DEP (Data Execution Prevention) is a hardware-enforced security feature that marks memory pages as either executable or non-executable — but not both — at the CPU level. The x86-64 CPU implements this via the NX (No-Execute) bit in page table entries. When a CPU attempts to execute code from a page with the NX bit set, it generates a protection fault.
On 64-bit Windows, DEP is always enabled for 64-bit processes. For 32-bit processes (WOW64), it's enforced when the executable is compiled with /NXCOMPAT (sets NXCOMPAT in DllCharacteristics) or when the system policy forces it.
DEP prevents: executing shellcode in the heap, stack, or other data regions. It does not prevent ROP (Return-Oriented Programming), where the attacker chains together existing executable code gadgets to achieve arbitrary computation without injecting new code.
Guard Pages
Guard pages (PAGE_GUARD) serve as memory tripwires. When any access (read or write) touches a guard page, Windows raises a STATUS_GUARD_PAGE_VIOLATION (first-chance) exception. After the exception is delivered, the guard attribute is automatically removed from the page — it fires only once per access.
Windows uses guard pages for automatic stack growth: the committed stack portion has a single guard page at its lower boundary. When the stack grows into the guard page, the exception handler maps one more page, moves the guard page down, and resumes execution — transparent stack expansion without reserving all memory upfront.
A stack overflow (recursion too deep) occurs when execution hits the guard page and there's no more room to expand — the region below is already the committed stack limit. A stack buffer overflow is a different attack: overwriting the return address or local variables via a buffer that exceeds its bounds. Guard pages protect against accidental overflow-by-recursion, not against deliberate smashing of adjacent stack memory.
VirtualProtect — Changing Page Permissions
VirtualProtect (or VirtualProtectEx for remote processes) changes the protection flags on a committed region of virtual memory:
// Classic shellcode staging pattern (detected by most EDRs)
void* pMem = VirtualAlloc(NULL, shellcodeLen, MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE); // Step 1: alloc as RW
memcpy(pMem, shellcode, shellcodeLen); // Step 2: write shellcode
DWORD oldProt;
VirtualProtect(pMem, shellcodeLen,
PAGE_EXECUTE_READ, &oldProt); // Step 3: flip to RX
((void(*)())pMem)(); // Step 4: execute
// Detection: VirtualProtect call that changes a private region
// from non-executable to executable. ETW captures this.
// oldProt contains the previous protection for rollback.
Protection Change as Detection Signal
The pattern VirtualAlloc(RW) → write → VirtualProtect(RX) is the canonical shellcode execution pattern, and it's well-monitored:
| Stage | Protection | Detection Source |
|---|---|---|
| Allocation | PAGE_READWRITE | ETW memory allocation event; MEM_PRIVATE region appears |
| Protection change | → PAGE_EXECUTE_READ | ETW VirtualProtect event; Sysmon Event 10 (process access); EDR user-mode hook on VirtualProtect |
| Execution | PAGE_EXECUTE_READ | Thread context switch with IP in private memory; ETW thread start at unusual address |
Evasion attempts: allocate directly as PAGE_EXECUTE_READWRITE (RWX) — skips the flip but a single RWX allocation is an even bigger red flag. Or use MapViewOfFile from a section object — may show as MEM_MAPPED instead of MEM_PRIVATE, reducing suspicion in some scanners.
CET — Control-flow Enforcement Technology
Intel CET (available from Ice Lake CPUs, Windows 10 20H1+) adds hardware enforcement of return addresses through a shadow stack. Every CALL instruction pushes the return address onto both the regular stack (RSP) and a separate, hardware-protected shadow stack (SSP). The shadow stack is read-only from user mode — software cannot modify it. When a RET instruction executes, the CPU compares the return address on the regular stack against the shadow stack. A mismatch triggers a control protection fault.
CET defeats ROP attacks that overwrite return addresses on the stack — the shadow stack still has the correct return address, and the tampered regular stack return address is caught. Malware targeting CET-enabled processes must use forward-edge control-flow techniques (indirect CALL/JMP), which Control Flow Guard already restricts.
| Feature | Protects Against | Opt-in Flag |
|---|---|---|
| DEP / NX | Shellcode in data pages | NXCOMPAT in DllCharacteristics |
| ASLR | Fixed-address exploits | DYNAMICBASE in DllCharacteristics |
| Stack canaries (/GS) | Stack buffer overflows | Compiler switch /GS |
| Control Flow Guard (CFG) | Forward-edge (indirect call) hijacking | GUARD_CF in DllCharacteristics |
| CET Shadow Stack | Return-address (backward-edge) hijacking / ROP | CETCOMPAT or ProcessMitigationPolicy |
Detecting Protection Abuse
import ctypes, struct
def find_rwx_regions(pid: int):
k32 = ctypes.WinDLL("kernel32")
hProc = k32.OpenProcess(0x1FFFFF, False, pid)
MBI_SIZE = 48
addr = 0
suspicious = []
while addr < 0x7FFFFFFFFFFF:
mbi = ctypes.create_string_buffer(MBI_SIZE)
if k32.VirtualQueryEx(hProc, addr, mbi, MBI_SIZE) != MBI_SIZE:
break
base, _, _, region_size, state, protect, mem_type = \
struct.unpack_from("QQIIIII", mbi.raw)
if state == 0x1000: # MEM_COMMIT
if protect == 0x40 and mem_type == 0x20000: # RWX + PRIVATE
suspicious.append(f"RWX private region at {hex(base)} size={hex(region_size)}")
addr = base + region_size
k32.CloseHandle(hProc)
return suspicious
Q & A
Why doesn't DEP stop ROP attacks?
DEP prevents executing new code in data pages. ROP (Return-Oriented Programming) doesn't inject new code — it reuses existing executable code that's already in memory (typically the .text sections of loaded DLLs). An attacker finds small sequences of instructions ending in a RET instruction (called "gadgets") across the loaded DLLs: pop rax; ret, mov [rbx], rax; ret, syscall; ret, etc. By carefully controlling the stack, the attacker chains these gadgets together — each RET pops the next gadget's address off the stack, creating a sequence of computation without any new code. The CPU is executing code that's already in the .text section (DEP doesn't flag it) and using the legitimate return mechanism. This is why CET was developed: it enforces that every RET instruction returns to a legitimately-called function by checking the shadow stack, which the attacker can't modify.
Can shellcode avoid the RW→RX flip by allocating as RWX directly?
Yes — allocating PAGE_EXECUTE_READWRITE from the start skips the VirtualProtect call that EDRs monitor. However: (1) A freshly-allocated private RWX region is itself a high-severity indicator. Modern EDRs flag any VirtualAlloc that returns an executable private region, regardless of whether a subsequent VirtualProtect was called. (2) ETW (Event Tracing for Windows) captures memory allocation events including the initial protection, so the RWX allocation itself is logged. (3) The memory scan check from Chapter 17 catches it: MEM_PRIVATE + PAGE_EXECUTE_READWRITE. (4) Some process mitigation policies can block PAGE_EXECUTE_WRITECOPY and PAGE_EXECUTE_READWRITE allocations entirely via PROCESS_MITIGATION_DYNAMIC_CODE_POLICY. So allocating as RWX avoids one detection signal but is itself a stronger one. The evasion battle here has moved to using mapped sections, module stomping, and other approaches that avoid creating obviously suspicious private executable regions.