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
| Property | Detail |
|---|---|
| Scope | Per-function (tied to the __try/__except block) |
| Registration | Compiler-generated; uses stack-based exception frames (EXCEPTION_REGISTRATION_RECORD) |
| Syntax | __try { ... } __except(filter) { ... } in MSVC |
| Dispatch order | Innermost (most recent) to outermost frame |
| Unwinding | Supports 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
| Property | Detail |
|---|---|
| Scope | Process-wide — catches exceptions from ANY thread, ANY function |
| Registration | AddVectoredExceptionHandler(Order, HandlerFunc) |
| Storage | Internal linked list (not on the stack) |
| Dispatch priority | Called BEFORE SEH handlers |
| Removal | RemoveVectoredExceptionHandler(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:
| Value | Constant | Meaning |
|---|---|---|
| 1 | CALL_FIRST | Insert at the front of the handler list (called first) |
| 0 | CALL_LAST | Insert 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
(CPU trap/fault)
to user mode
Dispatcher
(first to last)
(innermost to outermost)
ExceptionFilter
(if still unhandled)
Dispatch Details
- CPU exception occurs (e.g., access violation at address 0x0)
- Kernel saves the full CPU state into a
CONTEXTstructure - Kernel dispatches to user-mode
ntdll!KiUserExceptionDispatcher - VEH handlers are called in order (front-to-back in the linked list)
- If no VEH handler returns
EXCEPTION_CONTINUE_EXECUTION, SEH handlers are tried (innermost frame to outermost) - If still unhandled,
UnhandledExceptionFilteris called (this is where WER/crash dialogs appear) - 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
| Field | Type | Description |
|---|---|---|
ExceptionCode | DWORD | The exception type (e.g., 0xC0000005 = ACCESS_VIOLATION) |
ExceptionFlags | DWORD | 0 = continuable, 1 = non-continuable |
ExceptionAddress | PVOID | Address of the instruction that caused the exception |
NumberParameters | DWORD | Number of parameters in the ExceptionInformation array |
ExceptionInformation | ULONG_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:
- Change RIP to redirect execution to a different address
- Change RAX to swap the syscall number
- Change RCX, RDX, R8, R9 to change function arguments
- Change RSP to switch stacks
- Set Dr0–Dr3 and Dr7 to install hardware breakpoints
- Set the Trap Flag (EFlags bit 8) to enable single-stepping
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:
- CPU raises an access violation exception
- Windows dispatches it to VEH handlers
- LayeredSyscall's
AddHwBphandler catches it - Handler reads the desired syscall info from global state
- Handler sets hardware breakpoints on the target syscall stub
- Handler advances RIP past the faulting instruction to avoid re-triggering
- 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
| Handler | Priority | Handles | Purpose |
|---|---|---|---|
AddHwBp | CALL_FIRST | EXCEPTION_ACCESS_VIOLATION(0xC0000005) | Triggered by the intentional null deref. Sets up hardware breakpoints on the target syscall stub and calls the legitimate API. |
HandlerHwBp | CALL_FIRST | EXCEPTION_SINGLE_STEP(0x80000004) | Triggered by hardware breakpoints. Swaps SSN, marshals arguments, manages the single-step chain for call stack construction. |
Dual-Handler Flow (Preview)
ACCESS_VIOLATION
Sets Dr0/Dr1, calls API
kernel32 → ntdll
SINGLE_STEP
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
| Code | Hex | Name | LayeredSyscall Use |
|---|---|---|---|
| ACCESS_VIOLATION | 0xC0000005 | Read/write to invalid address | Intentional trigger to enter AddHwBp handler |
| SINGLE_STEP | 0x80000004 | Hardware breakpoint or trap flag | Fired by Dr0/Dr1 breakpoints and trap flag single-stepping |
| BREAKPOINT | 0x80000003 | INT3 software breakpoint | Not used by LayeredSyscall (detectable) |
| ILLEGAL_INSTRUCTION | 0xC000001D | Invalid opcode | Not used |
| GUARD_PAGE | 0x80000001 | Access to a guard page | Not used (sometimes used by other VEH syscall tools) |
Module 3 Quiz: Exception Handling
Q1: What is the key difference between VEH and SEH?
__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?
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.