Guard Page Anti-Debug
Arming memory with PAGE_GUARD turns the first access into a one-shot STATUS_GUARD_PAGE_VIOLATION, exposing single-steppers and memory-scanning tools.
A guard page is memory marked with PAGE_GUARD. The first access — read, write,
or execute — raises a one-shot STATUS_GUARD_PAGE_VIOLATION (0x80000001) and
then automatically clears the guard bit. Malware abuses this as a tripwire: it
arms a page, accesses it through its own controlled path, and verifies the
exception arrives exactly when expected.
If a debugger has placed a software breakpoint on the page, or a memory-scanning tool reads it first, the guard fires out of order — or the analyst's exception handler swallows the violation — and the sample detects the interference.
How it works
The malware allocates or re-protects a page with PAGE_GUARD, installs a
vectored/SEH handler, then deliberately touches the page. On a clean machine the
handler runs exactly once; the handler is also where the real (often decrypted)
code lives, so naively skipping the fault breaks execution.
DWORD old;
BYTE *page = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);
VirtualProtect(page, 0x1000, PAGE_READWRITE | PAGE_GUARD, &old);
__try {
volatile BYTE x = page[0]; // triggers STATUS_GUARD_PAGE_VIOLATION
(void)x;
// If we reach here without the exception, something cleared the guard.
debugger_detected();
} __except (GetExceptionCode() == STATUS_GUARD_PAGE_VIOLATION
? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
// Expected on a clean run — continue real logic here.
}In disassembly, arming the page and the deliberate access that fires the guard look like this:
lea r9, [rsp+old] ; &lpflOldProtect
mov r8d, 0x104 ; PAGE_READWRITE (0x04) | PAGE_GUARD (0x100)
mov edx, 0x1000 ; dwSize = one page
mov rcx, rbx ; lpAddress = page
call VirtualProtect ; arm guard page
; SEH/VEH handler keyed on 0x80000001 already installed
mov al, byte [rbx] ; touch page -> STATUS_GUARD_PAGE_VIOLATION
; if execution falls through to here, the guard was cleared early:
call debugger_detected ; no fault means interferenceWhen the violation arrives, the handler compares the exception code:
cmp dword [rax], 0x80000001 ; ExceptionRecord->ExceptionCode
jne continue_search ; not our guard fault
; STATUS_GUARD_PAGE_VIOLATION as expected -> run real logicSingle-stepping interacts badly with guard pages: combining PAGE_GUARD with an
instruction that the debugger steps over can desynchronise the expected
exception count, which the sample tallies.
Detection & analysis
Static analysis: Look for VirtualAlloc/VirtualProtect with the
PAGE_GUARD flag (0x100) ORed into the protection constant, paired with an
SEH/VEH handler that explicitly checks for STATUS_GUARD_PAGE_VIOLATION
(0x80000001). The deliberate self-access immediately after arming is the tell.
Dynamic analysis: Configure your debugger to pass STATUS_GUARD_PAGE_VIOLATION
straight to the application's handler rather than catching it. Avoid placing
software breakpoints inside guarded pages; use hardware breakpoints outside them.
Trace the exception-count logic so you do not perturb the sample's expected
sequence.
Detection rule hint: Flag re-protection of recently-written memory to
PAGE_READWRITE | PAGE_GUARD followed within a few instructions by a read of
that same region, plus a handler keyed on exception code 0x80000001.