Home > Blog > VEHguard Internals
VEHguard Internals
Posted: 2026-08-01 | Author: Siddharth | Category: Reverse Engineering

$\textit{Dynamic Analysis of VEH Dispatch and}$
$\textit{Exception-Context RIP Modification}$
$\textit{Using ROP Gadgets in }\texttt{ntdll.dll}$

Before going on this, I'd want to let the basics of VirtualAlloc and VirtualAllocEx be known to the reader, if you already know this, skip to the next section.

https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc

VirtualAlloc is the primary user mode API for allocating pages in the "current process", if you wanna allocate in another process then use VirtualAllocEx.

I like this comment - https://news.ycombinator.com/item?id=31964221, read about this as well.

Every call at the end reaches NtAllocateVirtualMemory which manipulates the VAD and can also manipulate the page table itself.

One thing to keep in mind is that allocation on the NT kernel is a 2 step process, reserve and commit. Read this if you wanna know how VAD works: SANS - Analysis of User Data VADs. In short when you reserve you're asking the NT kernel to give you a memory address, you have a bunch of virtual memory pages which you wanna allocate.

The other one is mem-commit which the windows documentation explains very well: "The function also guarantees that when the caller later initially accesses the memory, the contents will be zero. Actual physical pages are not allocated unless/until the virtual addresses are actually accessed". Something interesting worth noting is that a VirtualAlloc-returned page doesn't belong to any DLL and has no PDB symbols, and the fun thing we will use is that it has no entry in the process's RUNTIME_FUNCTION.

Keep this in mind later - To reserve and commit pages in one step, call VirtualAlloc with MEM_COMMIT | MEM_RESERVE.

VEHguard internals

https://learn.microsoft.com/en-us/windows/win32/debug/vectored-exception-handling

If that confuses or doesn't explain it properly, it's basically a callback that windows uses when an exception occurs before any __try/catch tries to run. To add one we use the AddVectoredExceptionHandler function (errhandlingapi.h), it takes a (FirstHandler, YourFunction) - passing 1 to it makes our handler go at the front of the internal chain which manages the order of what is to be called, and 0 puts it at the back.

Every process in windows has ntdll.dll loaded onto it, it is what every syscall needs to be ran through to communicate to the kernel via syscalls. This thread has some answers for any more things that pique the reader's interest: r/sysadmin - Windows undocumented emergency restart

Now we can try to see how exactly these get resolved and the context switching happens, load up ntdll into IDA and let it finish downloading symbols.

ntdll VEH handler list

See the LdrpVectorHandlerList, this is what keeps the list of what the priority order is for calling it.

Decompile and let's search for the loop where it has to

VEH dispatch loop decompiled

Handlers are encoded (RtlDecodePointer) as an anti-corruption mitigation, and returning -1 breaks the loop, returns 1, which is ended up as "handled".

VEH handler encoding

See the entry block! RSP now points at an EXCEPTION_RECORD + CONTEXT the kernel prepared, and then it checks if it's a WOW64 (32-bit-on-64-bit) process by testing if Wow64PrepareForException is set.

Block 3 is the main dispatch, RtlDispatchException walks the VEH list (through RtlpCallVectoredHandlers which we just reversed) and returns 1 if handled, 0 if not.

RtlDispatchException entry block

Now we should see how the handled patch will be dispatched throughout.

RtlDispatchException main dispatch

After VehHandler returns -1, execution reaches this block, and RtlGuardRestoreContext reads the CONTEXT* (still on rsp) and marks every register.

Moving onto how RtlGuardRestoreContext passes the modified RIP onto the CPU.

RtlGuardRestoreContext

Pretty huge, but let's start by tracing the CFG from the first entry basic block.

CFG entry basic block

The first arg is the struct we will need to modify in our entry VEH-handler.

VEH-handler struct argument

Next instructions are pretty explanatory; KiUserExceptionDispatcher calls with EDX = 0 (a null ExceptionRecord pointer), then jumps straight to loc_180035A17. For a deeper walkthrough of this dispatcher and its stack layout, read Maurice Heumann's “A journey through KiUserExceptionDispatcher”. A related implementation is brew02's KiUserExceptionDispatcherHook repository.

KiUserExceptionDispatcher call

Now this is a wrapper around RtlRestoreContext, the core primitive, these are just CFG validations or checking some things mostly.

RtlRestoreContext wrapper

But if you look at the right blocks of these, the context struct is being copied to a local stack buffer before being restored. This is so the restore process can safely trash registers without losing track of the source.

Below that is RcFrameConsolidation which is where the actual register-by-register restoration happens.

From all these we can try to form a mini mental model of VEH: the RtlRestoreContext passes context on to the CPU registers which then copies the caller-supplied CONTEXT into a local buffer then jumps to RcFrameConsolidation which restores every register. When we set ctx->Rip = g_RetGadget inside VehHandler, our context passes to RtlDispatchException, which passes to RtlGuardRestoreContext for validation, which passes to the RtlRestoreContext copy, which passes to RcFrameConsolidation, which sets the RIP register. See this Stack Overflow explanation.

$$ \begin{array}{c} \text{VehHandler}(ctx) \\[4pt] \big\downarrow\ \scriptstyle ctx\text{-}\!>\!Rip \leftarrow g\_RetGadget \\[4pt] \text{RtlDispatchException}(ctx) \\[4pt] \downarrow \\[4pt] \text{RtlGuardRestoreContext}(ctx) \\[4pt] \downarrow\ \scriptstyle \text{(validation)} \\[4pt] \text{RtlRestoreContext}(ctx) \\[4pt] \downarrow\ \scriptstyle ctx \to \text{local stack buffer (rep movsq)} \\[4pt] \text{RcFrameConsolidation}(ctx) \\[4pt] \downarrow \\[4pt] RIP \leftarrow ctx.Rip \end{array} $$

Dynamic Analysis

Notes: WinDbg exception controls are documented in Microsoft's Set Exceptions documentation.

When opening WinDbg stops at its initial debugger break in ntdll!LdrpDoDebuggerBreak and dw about this and just continue running

WinDbg initial startup breakpoint while loading VEHguard

After enabling first-chance guard-page breaks with sxe gp it stops on STATUS_GUARD_PAGE_VIOLATION (0x80000001). The exception address is our payload page, and u can see the test payload EB FE, an infinite short jump to itself. the PAGE_GUARD intercepted the first execution attempt before the handler redirects control flow

WinDbg catching the VEHguard guard-page violation at the payload

then continue from the first-chance exception and setting bu VEHguard!VehHandler breaks at the user-defined handler. see in the command window that the guard-page exception has been dispatched to VehHandler. as per __fastcall x64 Windows, the first argument is passed in RCXbut since we overwrote it ,it is the EXCEPTION_POINTERS* that contains the exception record .

WinDbg breaking at the VEHguard exception handler

WinDbg inspecting the guard-page exception and context pointer

0:000> r rcx
rcx=000000fd`9f7aeb60
0:000> dt ntdll!_EXCEPTION_POINTERS @rcx
Symbol ntdll!_EXCEPTION_POINTERS not found.
0:000> dt ntdll!_EXCEPTION_RECORD poi(@rcx)
   +0x000 ExceptionCode    : 0n-2147483647
   +0x004 ExceptionFlags   : 0
   +0x008 ExceptionRecord  : (null)
   +0x010 ExceptionAddress : 0x0000022f`236f0000 Void
   +0x018 NumberParameters : 2
   +0x020 ExceptionInformation : [15] 8
0:000> dt ntdll!_CONTEXT poi(@rcx+8) Rip Rsp
   +0x098 Rsp : 0x000000fd`9f7af508
   +0x0f8 Rip : 0x0000022f`236f0000

here RCX contains the handler's EXCEPTION_POINTERS*. in the exception record 0x80000001 (STATUS_GUARD_PAGE_VIOLATION) the allocated payload page as the exception address. But context record still has RIP which is pointing at that payload address, ie the state before VehHandler applies its redirect.

step through the handler,and we can see the redirect has been applied:

WinDbg showing the modified RIP and RSP after the VEH handler redirect

0:000> dt ntdll!_CONTEXT poi(000000fd`9f7aeb60+8) Rip Rsp
   +0x098 Rsp : 0x000000fd`9f7af500
   +0x0f8 Rip : 0x00007ffa`54fc10ea

RSP has moved down by 8 bytes,for matching the handler's stack adjustment, while RIP now points at the selected RET gadget instead of the payload page.

New Call Frame

Now lets use x64dbg for viewing the same event from the thread's pov to see the call-stack getting called from ntdll's ret gadget. Load VEHguard.exe, let the process run, and with the Call Stack pane, Set a bp on VEHguard!VehHandler .

in the call stack below shows the main thread and the transition back through the program's startup/runtime frames. The imp detail is that the guard-page exception is handled before normal user code resumes since we added it to the first priority.

x64dbg call stack for the VEHguard main thread

TAfter the handler modifies the context,we can compare the current RIP with the payload address and follow RSP in the dump pane.

x64dbg CPU, registers, and stack memory while examining VEHguard

In x64dbg, pause at the handler, step over the writes to ctx->Rsp and ctx->Rip, then inspect the call stack, which now returns through the ntdll.dll ROP gadget.

Source code: VEHguard on GitHub.

Start
🎵 now playing: ruby
12:00 PM