Inside EAF+: How Windows Exploit Guard Stops Shellcode From Resolving APIs
Windows Exploit Guard is a suite of anti-exploit and anti-malware mitigations applied on a per-process basis. Exploit Guard includes Exploit Protection, which mitigates attacks such as ROP/JOP and stack pivoting by implementing protections like DEP, CFG and ASLR.
First look at a protected process
At runtime, a process with Exploit Guard enabled has a DLL named payloadrestrictions.dll loaded by verifier.dll.

A few intrusive aspects of this feature are already visible:
- A background thread starting from
MitLibReportingThreadProc

- A hook placed on the main thread start address.

Initialization: querying the mitigation policy
verifier.dll detects that mitigations are active for a process and calls PayloadRestrictions!MitLibInitialize.
The first thing it does is determine which mitigations are enabled in the process, by calling NtQueryInformationProcess with ProcessMitigationPolicy as ProcessInformationClass:

Two global variables matter here:
g_MitLibState: stores the mitigation flags and the base addresses of the protected DLLsg_MitLibRandomSeed: a random seed derived from the_KUSER_SHARED_DATAstructure, used to randomize the in-memory layout of the structures
Both are stored in .mrdata: RtlMrdataAcquireSectionWriteAccess temporarily takes RW permissions on the section in order to allocate the two variables.

.mrdata (Mutable Read Data) is a special section that not every executable has. It is used when important structures or variables need to be stored somewhere that cannot simply stay read-only. Interaction with .mrdata happens through these APIs:
RtlpMrdataObtainSection: resolves.mrdatain memoryRtlMrdataAcquireSectionWriteAccess: acquires the lock on.mrdata(to avoid race conditions between threads) and makes it RWRtlMrdataReleaseSectionWriteAccess: releases the lockMitLibMrdataProtectVm: makes it read-only again
Staying notified about runtime DLL loads
payloadrestrictions.dll also needs a way to be notified about DLLs loaded at runtime that it has to protect. For this it resolves and calls LdrRegisterDllNotification, passing MitLibDllNotification as the callback.

MitLibHandleDllLoadEvent is then called to handle the DLL load event.
From this point on, every time a DLL is loaded MitLibDllNotification fires, which in turn executes MitLibHandleDllLoadEvent. This mechanism is necessary because payloadrestrictions covers several modules that may not be loaded into the process from the start.
Hooking the critical APIs
Hooks are installed on critical functions by calling MitLibInitializeHookedApis.
MitLibHookModuleTable is a global that defines the standard modules to protect: kernel32.dll, kernelbase.dll and ntdll.dll.

MitLibHookTable, on the other hand, defines an array of critical APIs to protect, exported by those standard modules.

For each function in MitLibHookTable, DetourAttachEx is called and overwrites the first bytes of the function with a hook (jmp PayloadRestrictions.dll!0xdeadbeef). payloadrestrictions.dll uses Microsoft Detours to perform the hooking.
Following the hook: from WinExec to ShangalCallApi
Hooked WinExec:
![]()
Let’s follow the jump:

The hook tail-jumps to ShangalCommonStub, which in turn calls ShangalCStub passing MitLibHooksDispatcher as the third argument.
MitLibHooksDispatcher passed in rcx to ShangalCStub:

MitLibHooksDispatcher then makes the call to ShangalCallApi:

Once the hook checks pass, ShangalCallApi executes the API that was originally called.
This explains the main thread’s call stack:

The complete flow, from the overwritten prologue down to the syscall:

EAF+ overview
EAF is a mitigation designed to prevent shellcode from reading the Export Address Table of sensitive DLLs (ntdll, kernelbase and kernel32). It does so by placing a guard page on the memory region of interest and raising an exception whenever a read occurs.
EAF+ extends EAF by placing more aggressive guard pages, and in more places. From what we have been able to observe:
-
ntdll.dll,kernel32.dllandkernelbase.dll: the entire memory page preceding the.textsection, meaning every header (DOS header, stub, optional header and so on) is protected. What does this mean? -> Shellcode can no longer rely on classic PE parsing and runtime function resolution.
kernel32.dllandkernelbase.dll: the.rdatasection, to prevent reads of the EAT and IAT.
- Other DLLs hardcoded inside
payloadrestrictions.dllare protected by EAF+ as well.
EAF enforcement
When we run an ordinary shellcode under EAF+, the shellcode tries to read the NT header and triggers the PAGE GUARD resulting in a process crash:

We know it was payloadrestrictions that placed the PAGE GUARD, so it most likely also has logic to handle this exception through a VEH. We can therefore look for where a VEH is registered in the code.
MitLibHandleDllLoadEvent is the first function to register an exception handler; this API is called by the callback registered earlier, MitLibDllNotification.

MitLibHandleDllLoadEvent does not register the handler blindly: it first verifies that mitigations such as IAF/EAF+ are enabled by calling MitLibIsEAFPlusModule. MitLibIsEAFPlusModule iterates over the DLLs in memory and compares them against a hardcoded list of DLLs to protect.

If a protected DLL is found, MitLibAddProtectedModule is called. This function only takes care of parsing the PE headers at the BaseAddress, locating the EAT, and updating g_MitLibState in .mrdata.

MitLibProtectModule is then called on the module in question, which in turn calls MitLibGuardProtectPage. MitLibGuardProtectPage does the heavy lifting: it places the guard page by calling ZwProtectVirtualMemory on the relevant memory regions.

VEHs and MitLibExceptionHandler
Vectored Exception Handlers are a mechanism that lets applications register handlers that are global to the process (unlike SEH, which is per thread). A handler is responsible for handling an exception.
When an exception is triggered, the handler receives an EXCEPTION structure as its first argument. Offset +0: 80000001, the exception code to handle Offset +16: the address that caused the guard page violation (RIP) Offset +40: what it attempted to read.

Every VEH is stored as an encoded pointer in a doubly linked list whose head lives in the .mrdata section of ntdll.dll (ntdll!LdrpVectorHandlerList) with the nodes living on the heap. That heap is allocated by LdrpMrdataHeap.
Global struct in .mrdata:
typedef struct _LDRP_VECTOR_HANDLER_LIST {
PSRWLOCK LdrpVehLock; // Lock for thread-safe modification of the VEH list
LIST_ENTRY LdrpVehList; // Head of the doubly linked list for VEHs
PSRWLOCK LdrpVchLock; // Lock for thread-safe modification of the VCH list
LIST_ENTRY LdrpVchList; // Head of the doubly linked list for VCHs
} LDRP_VECTOR_HANDLER_LIST, *PLDRP_VECTOR_HANDLER_LIST;
At LDRP_VECTOR_HANDLER_LIST + 8 sits LIST_ENTRY LdrpVehList, allocated in .mrdata, with this definition:
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY,*PLIST_ENTRY,*RESTRICTED_POINTER PRLIST_ENTRY;
A classic doubly linked list.

Following LdrpVectorHandlerList->flink lands us in the heap, where the encoded pointer (the VEH) belonging to payloadrestrictions resides.
Each subsequent node in the doubly linked list has this struct:
typedef struct _VECTOR_HANDLER_ENTRY {
LIST_ENTRY ListEntry;
PLONG64 pRefCount; // ProcessHeap allocated, initialized with 1
DWORD unk_0; // always 0
DWORD pad_0;
PVOID EncodedHandler;
} VECTOR_HANDLER_ENTRY, * PVECTOR_HANDLER_ENTRY;

ntdll!RtlAddVectoredExceptionHandler adds a handler to this linked list. When a handler is added, the following happens:
- Pointer encoding:
RtlEncodePointerXORs the address with a 4-byte cookie, unique per process, stored at PEB+0x28.
The returned PEB cookie is obtained via
NtQueryInformationProcess.Routine to decode the exception pointer:

-
NtProtectVirtualMemoryis called on.mrdatabyLdrProtectMrdatato change the protection to RW and add the encoded pointer. -
.mrdatais set back to read-only (https://www.unknowncheats.me/forum/c-and-c-/567151-vectored-exception-handlers-x64-windows.html, https://bruteratel.com/research/2024/10/20/Exception-Junction/)
MitLibExceptionHandler is the exception handler responsible for errors code 80000001 and 80000004:
STATUS_GUARD_PAGE_VIOLATIONSTATUS_SINGLE_STEP

In our case, with EAF+ the 80000001h path is always taken and MitLibValidateAccessToProtectedPage is invoked, receiving the EXCEPTION structure as its argument.
As execution continues past the guard page exception, a SINGLE_STEP exception (80000004h) is triggered.

payloadrestrictions uses a clever trick to reapply the guard page: on Windows, a guard page removes itself automatically every time it is triggered.

In the bottom right, the base address of ntdll no longer carries the protection; NtProtectVirtualMemory is called by MitLibExceptionHandler to put it back, following the 80000004h path. This is why MitLibValidateAccessToProtectedPage has two paths.
Inside MitLibValidateAccessToProtectedPage
MitLibValidateAccessToProtectedPage performs several checks:
-
If the instruction pointer (whatever attempted the read that caused the guard page exception) lies inside a backed module,
MitLibMemReaderGadgetCheckis called.
MitLibMemReaderGadgetCheckdisassembles the faulting IP and checks which instruction read from the guard page; if a reader-gadget pattern is detected, the process is killed. Examples of blacklisted gadgets:mov rax, qword ptr [rax + N] ; retmov rax, qword ptr [rax] ; ret -
A stack pivot check, verifying that the saved RSP falls within the stack limits.
-
Another check verifies that the saved RIP really does come from a backed memory region.

Any check that fails ends up in a call to MitLibReportAddressFilterViolation, which leads to MitLibTriggerFailFast and ultimately kills the process.

Conclusion
Exploit Protection bundles a number of mitigations, some effective and some less so. Conceptually EAF+ is a good idea, since it tries to cut the problem off at the root: shellcode resolving APIs at runtime. In its current state, however, it is easily bypassed simply by avoiding the already-known gadgets.
Are you curious to know more about how ZAIUX® Framework bypasses these mitigations?