Difficulty: Beginner

Module 3: Vectored Exception Handling Deep Dive

The process-wide exception handler that intercepts every breakpoint before anyone else sees it.

Module Objective

This module provides a deep understanding of Windows Vectored Exception Handling (VEH): how handlers are registered with AddVectoredExceptionHandler, the priority system that ensures ShellGhost's handler runs first, the EXCEPTION_POINTERS structure that provides access to exception details and thread context, and how the handler manipulates the CONTEXT structure to control execution flow. VEH is the interception mechanism that gives ShellGhost control over every INT3 exception.

1. VEH vs SEH: Why VEH?

Windows provides two exception handling mechanisms that user-mode code can use. Understanding the difference is critical to understanding why ShellGhost uses VEH.

FeatureStructured Exception Handling (SEH)Vectored Exception Handling (VEH)
ScopePer-function / per-thread (frame-based)Process-wide (global)
PriorityCalled after VEH handlersCalled before SEH handlers
RegistrationCompiler-generated (__try/__except)Runtime API (AddVectoredExceptionHandler)
Stack-based?Yes — tied to stack framesNo — stored in a global linked list
Thread scopePer-thread handler chainAll threads share the same handlers
Can modify CONTEXT?Yes (via GetExceptionInformation())Yes (via EXCEPTION_POINTERS)

VEH Advantage for ShellGhost

VEH handlers are called before the SEH chain is walked. This means ShellGhost's VEH handler gets first crack at every exception in the process, regardless of which thread generates it and regardless of any __try/__except blocks in the call stack. Since ShellGhost needs to intercept every single INT3 exception generated by the 0xCC-filled execution buffer, VEH's process-wide, first-priority nature is exactly what is needed.

2. The Exception Dispatch Order

When an exception occurs, Windows dispatches it through a defined sequence. Understanding this order explains why VEH guarantees ShellGhost's handler runs first:

User-Mode Exception Dispatch Order

1. VEH Handlers
Global, first priority
2. SEH Chain
Per-frame, stack walk
3. VCH Handlers
Vectored Continue
4. Unhandled Filter
Last chance
5. Process Crash
Termination
  1. Vectored Exception Handlers (VEH): Called first. If any handler returns EXCEPTION_CONTINUE_EXECUTION, dispatch stops and execution resumes with the (potentially modified) context.
  2. Structured Exception Handlers (SEH): Only reached if no VEH handler handled the exception. Walks the per-thread stack-based handler chain.
  3. Vectored Continue Handlers (VCH): Called after an SEH handler returns EXCEPTION_CONTINUE_EXECUTION. Rarely used.
  4. Unhandled Exception Filter: The last-resort handler registered via SetUnhandledExceptionFilter.
  5. Process Termination: If nothing handles the exception, the process crashes.

3. Registering a VEH Handler

The AddVectoredExceptionHandler API registers a new VEH handler. Its First parameter controls whether the handler is inserted at the beginning or end of the VEH list:

C// Function signature (from winnt.h / errhandlingapi.h)
PVOID AddVectoredExceptionHandler(
    ULONG First,                          // 1 = first in list, 0 = last
    PVECTORED_EXCEPTION_HANDLER Handler   // pointer to handler function
);

// Handler function signature
LONG CALLBACK VectoredHandler(
    PEXCEPTION_POINTERS ExceptionInfo     // exception + context
);

// Return values from the handler:
#define EXCEPTION_CONTINUE_EXECUTION  (-1)  // resume execution
#define EXCEPTION_CONTINUE_SEARCH      (0)  // pass to next handler

The First Parameter

When First = 1, the handler is inserted at the head of the VEH linked list, meaning it will be the first handler called for any exception. ShellGhost registers its handler with First = 1 to ensure it processes breakpoint exceptions before any other VEH handler in the process. If multiple handlers are registered with First = 1, the most recently registered one is called first (LIFO order).

C// ShellGhost's VEH registration
PVOID hVeh = AddVectoredExceptionHandler(
    1,              // First = 1: highest priority
    ShellGhostHandler
);

if (hVeh == NULL) {
    // Registration failed
    return -1;
}

// Later, when done:
RemoveVectoredExceptionHandler(hVeh);

4. The EXCEPTION_POINTERS Structure

The VEH handler receives a single argument: a pointer to an EXCEPTION_POINTERS structure. This structure contains everything the handler needs to understand the exception and modify the execution state:

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

ExceptionRecord Fields

Ctypedef struct _EXCEPTION_RECORD {
    DWORD ExceptionCode;          // e.g., 0x80000003 (BREAKPOINT)
    DWORD ExceptionFlags;         // 0 = first-chance, 1 = non-continuable
    struct _EXCEPTION_RECORD *ExceptionRecord;  // nested exception
    PVOID ExceptionAddress;       // instruction that caused the exception
    DWORD NumberParameters;       // number of params in array below
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

For ShellGhost, the two critical fields are ExceptionCode (to identify breakpoint exceptions) and ExceptionAddress (to know which instruction in the execution buffer triggered the exception).

5. The CONTEXT Structure

The CONTEXT structure represents the complete CPU register state at the moment of the exception. This is the mechanism that allows the VEH handler to change execution flow:

C// Key fields of the x64 CONTEXT structure (simplified)
typedef struct _CONTEXT {
    // Control registers
    DWORD64 Rip;        // Instruction pointer - where to resume
    DWORD64 Rsp;        // Stack pointer
    DWORD   EFlags;     // Flags register (includes Trap Flag at bit 8)
    WORD    SegCs;      // Code segment

    // General-purpose registers
    DWORD64 Rax, Rcx, Rdx, Rbx;
    DWORD64 Rbp, Rsi, Rdi;
    DWORD64 R8, R9, R10, R11, R12, R13, R14, R15;

    // ... SSE registers, debug registers, etc.
} CONTEXT;

CONTEXT Manipulation in ShellGhost

The VEH handler uses the CONTEXT before returning EXCEPTION_CONTINUE_EXECUTION:

When the handler returns EXCEPTION_CONTINUE_EXECUTION, the kernel restores the CONTEXT to the CPU, and execution resumes at Rip where the decrypted instruction now resides.

6. Handler Return Values

A VEH handler must return one of two values:

Return ValueConstantEffect
-1EXCEPTION_CONTINUE_EXECUTIONStop dispatch. Restore the (potentially modified) CONTEXT and resume execution. This is what ShellGhost returns for handled breakpoint exceptions.
0EXCEPTION_CONTINUE_SEARCHContinue dispatch. Pass the exception to the next handler in the VEH list, then to SEH. ShellGhost returns this for exceptions it does not handle (e.g., access violations from the shellcode itself).
C// ShellGhost's handler logic (simplified)
LONG CALLBACK ShellGhostHandler(PEXCEPTION_POINTERS ep) {
    DWORD code = ep->ExceptionRecord->ExceptionCode;

    if (code == EXCEPTION_BREAKPOINT) {
        // Re-encrypt previous instruction (if any),
        // decrypt current instruction, toggle RW/RX
        // ... (covered in Module 6)
        return EXCEPTION_CONTINUE_EXECUTION;
    }

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

7. VEH Internals: The Handler List

Internally, Windows maintains VEH handlers in a doubly-linked list protected by a critical section. Understanding this structure helps explain registration and removal behavior:

C// Internal structure (ntdll, not officially documented)
typedef struct _VECTORED_HANDLER_ENTRY {
    LIST_ENTRY                    List;       // Doubly-linked list pointers
    ULONG                         Refs;       // Reference count
    PVECTORED_EXCEPTION_HANDLER   Handler;    // The handler function pointer
    // Note: In modern Windows, the handler pointer is
    // encoded with RtlEncodePointer for security
} VECTORED_HANDLER_ENTRY;

// The global list head lives in ntdll:
// ntdll!LdrpVectorHandlerList (exception handlers)
// ntdll!LdrpVectorHandlerList + sizeof(LIST_ENTRY) (continue handlers)

Pointer Encoding

Since Windows Vista, the handler function pointers stored in the VEH list are encoded using RtlEncodePointer (which XORs the pointer with a per-process secret cookie). This prevents an attacker from overwriting a VEH handler pointer in memory to redirect execution. The encoding is transparent to normal API usage — AddVectoredExceptionHandler encodes, and the dispatch code decodes before calling.

8. VEH in the ShellGhost Architecture

Here is how VEH fits into the overall ShellGhost execution model:

VEH's Role in the Execution Cycle

CPU hits 0xCC
EXCEPTION_BREAKPOINT
VEH Handler Called
First priority
Re-encrypt prev, Decrypt curr
Toggle RW↔RX
CONTINUE_EXECUTION
Resume at Rip
C// Complete VEH setup for ShellGhost (simplified)
#include <windows.h>
#include <stdio.h>

// Forward declaration of the handler
LONG CALLBACK GhostHandler(PEXCEPTION_POINTERS ep);

int main() {
    // Step 1: Prepare the per-instruction encrypted shellcode data
    // (generated by ShellGhost_mapping.py preprocessing script)
    unsigned char encrypted_sc[] = { /* per-instruction encrypted data */ };
    SIZE_T sc_size = sizeof(encrypted_sc);

    // Step 2: Allocate execution buffer filled with 0xCC (RW, not RWX)
    LPVOID exec_buf = VirtualAlloc(NULL, sc_size,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    memset(exec_buf, 0xCC, sc_size);

    // Step 3: Register VEH handler with highest priority
    PVOID hVeh = AddVectoredExceptionHandler(1, GhostHandler);

    // Step 4: Create a thread whose entry point is at the end of .text
    // ResolveEndofTextSegment() finds null bytes in .text to use as entry
    // The first 0xCC triggers EXCEPTION_BREAKPOINT
    LPVOID entry = ResolveEndofTextSegment();
    HANDLE hThread = CreateThread(NULL, 0,
        (LPTHREAD_START_ROUTINE)entry, NULL, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);

    // Step 5: Cleanup (if shellcode returns)
    RemoveVectoredExceptionHandler(hVeh);
    VirtualFree(exec_buf, 0, MEM_RELEASE);
    return 0;
}

Thread Safety Consideration

Because VEH is process-wide, the handler will be invoked for exceptions on any thread, not just the thread executing shellcode. ShellGhost's handler must verify that the exception address falls within the execution buffer before processing it. Exceptions from other threads (or from addresses outside the buffer) should be passed through by returning EXCEPTION_CONTINUE_SEARCH.

Knowledge Check

Q1: In the Windows exception dispatch order, when are VEH handlers called relative to SEH handlers?

A) After SEH handlers
B) Before SEH handlers
C) At the same time as SEH handlers
D) Only if SEH handlers fail

Q2: What does AddVectoredExceptionHandler return when registration succeeds?

A) TRUE (1)
B) The exception code
C) A handle to the registered handler (PVOID)
D) The handler's position in the list

Q3: What must the VEH handler return to resume execution with the modified CONTEXT?

A) EXCEPTION_CONTINUE_EXECUTION (-1)
B) EXCEPTION_CONTINUE_SEARCH (0)
C) TRUE (1)
D) STATUS_SUCCESS (0)