Difficulty: Beginner

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).

ConstantValueReadWriteExecuteUse Case
PAGE_NOACCESS0x01NoNoNoGuard pages, decommitted memory
PAGE_READONLY0x02YesNoNoRead-only data sections
PAGE_READWRITE0x04YesYesNoHeap, stack, writable data
PAGE_EXECUTE0x10NoNoYesRare in practice
PAGE_EXECUTE_READ0x20YesNoYesNormal code sections (.text)
PAGE_EXECUTE_READWRITE0x40YesYesYesSelf-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

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

RX + Plaintext
Executing
RW + Plaintext
Brief transition
RW + Encrypted
SLEEPING (safe)
RW + Plaintext
Brief transition
RX + Plaintext
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

Mode 2 Disadvantages

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:

ApproachDuring SleepDuring ExecutionDetection Risk
Always RWXRWX + CleartextRWX + CleartextMaximum — RWX is the strongest IOC
Toggle RWX/RWRW + EncryptedRWX + CleartextHigh — RWX still present during execution
Toggle RW/RX (Mode 1)RW + EncryptedRX + CleartextMinimal — no RWX at any time
Toggle NA/RX (Mode 2)NA + EncryptedRX + CleartextMinimal — 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 MethodWhat It CatchesMitigation
ETW eventsVirtualProtect calls logged via ETW (Microsoft-Windows-Kernel-Memory)ETW patching (separate technique, not part of ShellcodeFluctuation)
Ntdll hooksEDR hook on NtProtectVirtualMemory sees protection changesDirect syscalls to bypass ntdll hooks
Kernel callbacksPsSetCreateProcessNotifyRoutine and related callbacksNot easily mitigated from user mode
Pattern analysisPeriodic RW/RX toggling correlated with Sleep intervalsJitter 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)?

A) RWX is not supported on modern Windows
B) RWX is the strongest memory scanner IOC - simultaneous read/write/execute is almost never legitimate
C) RWX memory cannot be encrypted
D) VirtualProtect does not support changing to RWX

Q2: How does Mode 2 (PAGE_NOACCESS) recover when the shellcode thread wakes up?

A) The OS automatically restores RX permissions after sleep
B) A timer callback restores permissions
C) The thread calls VirtualProtect before executing
D) A Vectored Exception Handler catches the access violation and decrypts/restores permissions

Q3: What side effect does hooking kernel32!Sleep produce that Moneta can detect?

A) kernel32.dll is unloaded from memory
B) The Sleep function is removed from the export table
C) Copy-on-write creates a private page in kernel32's .text section, flagged as "Modified code"
D) kernel32.dll's digital signature is invalidated