Difficulty: Beginner

Module 3: Windows Exception Handling & VEH

The mechanism that lets LayeredSyscall intercept and redirect CPU execution.

Module Objective

LayeredSyscall's entire architecture is built on Vectored Exception Handling (VEH). This module explains how Windows dispatches exceptions, the difference between SEH and VEH, the structures your handler receives, and how modifying the CONTEXT record lets you redirect execution to arbitrary code.

1. Structured Exception Handling (SEH)

SEH is the traditional Windows exception handling mechanism. It is frame-based and per-function, meaning handlers are tied to specific stack frames and function scopes.

SEH Characteristics

PropertyDetail
ScopePer-function (tied to the __try/__except block)
RegistrationCompiler-generated; uses stack-based exception frames (EXCEPTION_REGISTRATION_RECORD)
Syntax__try { ... } __except(filter) { ... } in MSVC
Dispatch orderInnermost (most recent) to outermost frame
UnwindingSupports stack unwinding via __finally blocks
C// SEH example - handler is scoped to this function
__try {
    // Code that might fault
    int *p = NULL;
    int x = *p;  // ACCESS_VIOLATION
}
__except (EXCEPTION_EXECUTE_HANDLER) {
    // Only catches exceptions from within this __try block
    printf("Caught exception in this function\n");
}

SEH handlers are registered on the stack and automatically cleaned up when the function returns. They only handle exceptions that occur within their __try scope. This makes SEH unsuitable for LayeredSyscall, which needs to intercept exceptions that occur deep inside ntdll — far from any __try block in its own code.

2. Vectored Exception Handling (VEH)

VEH is a process-wide exception handling mechanism introduced in Windows XP. Unlike SEH, VEH handlers are not tied to any stack frame. They are stored in an internal linked list and are invoked for every exception in the process, regardless of where it occurs.

VEH Characteristics

PropertyDetail
ScopeProcess-wide — catches exceptions from ANY thread, ANY function
RegistrationAddVectoredExceptionHandler(Order, HandlerFunc)
StorageInternal linked list (not on the stack)
Dispatch priorityCalled BEFORE SEH handlers
RemovalRemoveVectoredExceptionHandler(Handle)
C++// VEH handler - process-wide, catches everything
LONG WINAPI MyVehHandler(PEXCEPTION_POINTERS ExceptionInfo) {
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
        // Handle the exception...
        return EXCEPTION_CONTINUE_EXECUTION;  // Resume execution
    }
    return EXCEPTION_CONTINUE_SEARCH;  // Not ours, pass to next handler
}

// Registration - CALL_FIRST (1) means this handler goes to the front of the list
PVOID hVeh = AddVectoredExceptionHandler(1, MyVehHandler);

The Order parameter controls placement in the handler chain:

ValueConstantMeaning
1CALL_FIRSTInsert at the front of the handler list (called first)
0CALL_LASTInsert at the end of the handler list (called last)

LayeredSyscall registers its handlers with CALL_FIRST to ensure they execute before any other exception handlers in the process.

3. Exception Dispatch Flow

When a CPU exception occurs (access violation, breakpoint, single-step, etc.), Windows follows a well-defined dispatch sequence:

Windows Exception Dispatch Order

Exception occurs
(CPU trap/fault)
Kernel dispatches
to user mode
KiUserException-
Dispatcher
VEH handlers
(first to last)
SEH handlers
(innermost to outermost)
Unhandled-
ExceptionFilter
Process terminates
(if still unhandled)

Dispatch Details

  1. CPU exception occurs (e.g., access violation at address 0x0)
  2. Kernel saves the full CPU state into a CONTEXT structure
  3. Kernel dispatches to user-mode ntdll!KiUserExceptionDispatcher
  4. VEH handlers are called in order (front-to-back in the linked list)
  5. If no VEH handler returns EXCEPTION_CONTINUE_EXECUTION, SEH handlers are tried (innermost frame to outermost)
  6. If still unhandled, UnhandledExceptionFilter is called (this is where WER/crash dialogs appear)
  7. If the filter doesn't handle it, the process is terminated

The critical insight for LayeredSyscall: VEH handlers run before everything else. This means LayeredSyscall's handler gets first crack at every exception in the process, including hardware breakpoint traps fired inside ntdll.

4. The EXCEPTION_POINTERS Structure

Every VEH handler receives a pointer to EXCEPTION_POINTERS, which bundles two critical pieces of information:

C++typedef struct _EXCEPTION_POINTERS {
    PEXCEPTION_RECORD ExceptionRecord;  // What happened
    PCONTEXT          ContextRecord;     // CPU state when it happened
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

ExceptionRecord — What Happened

FieldTypeDescription
ExceptionCodeDWORDThe exception type (e.g., 0xC0000005 = ACCESS_VIOLATION)
ExceptionFlagsDWORD0 = continuable, 1 = non-continuable
ExceptionAddressPVOIDAddress of the instruction that caused the exception
NumberParametersDWORDNumber of parameters in the ExceptionInformation array
ExceptionInformationULONG_PTR[]Extra info (for ACCESS_VIOLATION: [0]=read/write, [1]=target address)

ContextRecord — Full CPU State

The CONTEXT structure contains the complete register state of the CPU at the time of the exception. This is the key to LayeredSyscall's power:

C++// Key fields in CONTEXT (x64) relevant to LayeredSyscall
typedef struct _CONTEXT {
    // Control registers
    DWORD64 Rip;        // Instruction pointer (where to resume)
    DWORD64 Rsp;        // Stack pointer
    DWORD   EFlags;     // CPU flags (includes Trap Flag at bit 8)

    // Integer registers (function arguments in x64 calling convention)
    DWORD64 Rcx;        // 1st argument
    DWORD64 Rdx;        // 2nd argument
    DWORD64 R8;         // 3rd argument
    DWORD64 R9;         // 4th argument
    DWORD64 Rax;        // Return value / SSN for syscall
    DWORD64 R10;        // Saved 1st arg (after mov r10, rcx)

    // Additional integer registers
    DWORD64 Rbx, Rbp, Rsi, Rdi;
    DWORD64 R11, R12, R13, R14, R15;

    // Debug registers (hardware breakpoints!)
    DWORD64 Dr0;        // Breakpoint address 0
    DWORD64 Dr1;        // Breakpoint address 1
    DWORD64 Dr2;        // Breakpoint address 2
    DWORD64 Dr3;        // Breakpoint address 3
    DWORD64 Dr6;        // Debug status
    DWORD64 Dr7;        // Debug control (enable/disable)
    // ... floating point, SSE, etc.
} CONTEXT;

The Key Insight: Modifying ContextRecord Changes Execution

When a VEH handler returns EXCEPTION_CONTINUE_EXECUTION, Windows resumes execution using the values in the CONTEXT structure. If you modify the context before returning, execution resumes at the new values. This means you can:

This is the fundamental mechanism LayeredSyscall uses. It installs VEH handlers that intercept hardware breakpoint exceptions and modify the CPU context to hijack syscall execution.

5. Handler Return Values

A VEH handler must return one of two values, which tells the exception dispatcher what to do next:

EXCEPTION_CONTINUE_EXECUTION (-1)

Resume execution at the address specified by ContextRecord->Rip. If you modified any registers in the context, those modifications take effect. The exception is considered handled.

LayeredSyscall uses this to resume execution after swapping SSN, arguments, and setting/clearing hardware breakpoints.

EXCEPTION_CONTINUE_SEARCH (0)

This handler does not handle the exception. Pass it to the next handler in the chain (next VEH handler, then SEH handlers). If no handler handles it, the process crashes.

LayeredSyscall returns this when the exception is not one it recognizes (not the right exception code or address).

C++LONG WINAPI ExampleHandler(PEXCEPTION_POINTERS ExceptionInfo) {
    DWORD code = ExceptionInfo->ExceptionRecord->ExceptionCode;

    if (code == EXCEPTION_SINGLE_STEP) {
        // This is our hardware breakpoint!
        // Modify context...
        ExceptionInfo->ContextRecord->Rax = newSsn;
        return EXCEPTION_CONTINUE_EXECUTION;  // Resume with our changes
    }

    // Not our exception, let someone else handle it
    return EXCEPTION_CONTINUE_SEARCH;
}

6. Triggering Exceptions Intentionally

LayeredSyscall doesn't wait for exceptions to happen naturally. It deliberately triggers an EXCEPTION_ACCESS_VIOLATION (code 0xC0000005) to enter its VEH handler on demand:

C++// From LayeredSyscall's code - intentional null pointer dereference
#define TRIGGER_ACCESS_VIOLOATION_EXCEPTION  int *a = 0; int b = *a;

// Usage in a wrapped API function:
NTSTATUS LayeredNtAllocateVirtualMemory(...) {
    // Set up arguments in global state
    SetupSyscallArgs(SSN_NtAllocateVirtualMemory, args...);

    // Deliberately crash to enter VEH handler
    TRIGGER_ACCESS_VIOLOATION_EXCEPTION

    // Execution never reaches here normally -
    // the VEH handler redirects RIP
}

When the null dereference occurs:

  1. CPU raises an access violation exception
  2. Windows dispatches it to VEH handlers
  3. LayeredSyscall's AddHwBp handler catches it
  4. Handler reads the desired syscall info from global state
  5. Handler sets hardware breakpoints on the target syscall stub
  6. Handler advances RIP past the faulting instruction to avoid re-triggering
  7. Handler calls a legitimate API (e.g., WriteFile) that will eventually hit the breakpoint

Advancing RIP Past the Fault

The faulting instruction (mov eax, [0x0]) is typically 2–3 bytes. The handler must advance ContextRecord->Rip past it, otherwise execution would resume at the same faulting instruction and trigger an infinite loop:

C++// Skip past the faulting instruction
ExceptionInfo->ContextRecord->Rip += INSTRUCTION_SIZE;
return EXCEPTION_CONTINUE_EXECUTION;

7. LayeredSyscall's VEH Setup

LayeredSyscall registers two VEH handlers during initialization, each with a different role:

C++// From InitializeHooks() in LayeredSyscall
PVOID h1 = AddVectoredExceptionHandler(CALL_FIRST, AddHwBp);
PVOID h2 = AddVectoredExceptionHandler(CALL_FIRST, HandlerHwBp);

Handler Roles

HandlerPriorityHandlesPurpose
AddHwBpCALL_FIRSTEXCEPTION_ACCESS_VIOLATION
(0xC0000005)
Triggered by the intentional null deref. Sets up hardware breakpoints on the target syscall stub and calls the legitimate API.
HandlerHwBpCALL_FIRSTEXCEPTION_SINGLE_STEP
(0x80000004)
Triggered by hardware breakpoints. Swaps SSN, marshals arguments, manages the single-step chain for call stack construction.

Dual-Handler Flow (Preview)

Trigger null deref
ACCESS_VIOLATION
AddHwBp handler
Sets Dr0/Dr1, calls API
Legitimate API runs
kernel32 → ntdll
HW breakpoint fires
SINGLE_STEP
HandlerHwBp
Swaps SSN + args

This dual-handler architecture is explored in detail in Module 5. For now, the key takeaway is that VEH provides the mechanism for intercepting CPU exceptions process-wide and modifying the execution context — which is the foundation of everything LayeredSyscall does.

Registration Order Matters

Both handlers are registered with CALL_FIRST. Since AddVectoredExceptionHandler(CALL_FIRST, ...) inserts at the front of the list, HandlerHwBp (registered second) actually ends up before AddHwBp in the list. This is important because both handlers check the exception code to determine if the exception is theirs — HandlerHwBp checks for EXCEPTION_SINGLE_STEP and AddHwBp checks for EXCEPTION_ACCESS_VIOLATION, so they don't interfere with each other.

Common Exception Codes Reference

CodeHexNameLayeredSyscall Use
ACCESS_VIOLATION0xC0000005Read/write to invalid addressIntentional trigger to enter AddHwBp handler
SINGLE_STEP0x80000004Hardware breakpoint or trap flagFired by Dr0/Dr1 breakpoints and trap flag single-stepping
BREAKPOINT0x80000003INT3 software breakpointNot used by LayeredSyscall (detectable)
ILLEGAL_INSTRUCTION0xC000001DInvalid opcodeNot used
GUARD_PAGE0x80000001Access to a guard pageNot used (sometimes used by other VEH syscall tools)

Module 3 Quiz: Exception Handling

Q1: What is the key difference between VEH and SEH?

VEH handlers are registered process-wide and stored in a global linked list. They are called BEFORE any SEH handlers when an exception occurs. SEH handlers are frame-based, tied to specific __try/__except blocks, and only handle exceptions within their scope.

Q2: What happens when a VEH handler modifies ContextRecord->Rip and returns EXCEPTION_CONTINUE_EXECUTION?

When a handler returns EXCEPTION_CONTINUE_EXECUTION, Windows restores the CPU registers from the CONTEXT structure. If the handler modified RIP (or any other register), those changes take effect. Execution resumes at whatever address RIP now points to. This is the mechanism LayeredSyscall uses to redirect control flow.

Q3: Why does LayeredSyscall register its handlers with CALL_FIRST (1)?

CALL_FIRST inserts the handler at the front of the VEH linked list. This ensures LayeredSyscall's handlers get to process exceptions before any other handlers (including EDR-registered VEH handlers or debugger hooks). If another handler processed the exception first, it could interfere with LayeredSyscall's breakpoint mechanism.