DLL injection is one of those techniques that never really gets old. There are several well-known approaches: CreateRemoteThread, APCs via QueueUserAPC, SetWindowsHookEx. Each one has its own trade-offs and quirks. In this video I want to show you another one that is a bit less known: using the Windows Application Verifier infrastructure.
What makes this technique interesting is not just that it loads your DLL into a target process. It loads it very early, right after ntdll, before essentially anything else. And on top of that, the same infrastructure gives you a built-in way to hook APIs without any separate hooking library. That’s two capabilities for the price of one.
What Is Application Verifier?
Application Verifier is a tool that ships with the Windows SDK. Its intended purpose is testing and inspecting applications for memory issues, handle leaks, heap problems, and similar bugs. You point it at an executable by name, configure a set of tests, and every time that executable launches the verifier DLL gets loaded into the process automatically.
From the verifier’s perspective, it runs a set of provider DLLs that each perform a category of checks. The interesting side effect, from a security and internals perspective, is that this mechanism is entirely configurable via the registry, and it supports loading arbitrary DLLs into any named executable.
The downside, which I’ll come back to at the end, is that your DLL must live in System32. That’s a hard constraint, not a coincidence.
Configuring Application Verifier
Using the Application Verifier UI
The Application Verifier UI ships with the Windows SDK. You add an executable by name (no path, just the name, for example mmc.exe), choose which test providers to enable, and click Save. That’s it from a UI perspective.
Once you do that, you can run the executable and it will have the verifier DLLs loaded into its address space. You can verify this in Process Explorer by searching for the process and looking at its loaded DLLs: you’ll find verifier.dll, vfbasics.dll, vfcore.dll, and whatever other provider DLLs you enabled.
What Gets Written to the Registry
The UI is just a front end. What actually controls the behavior is a registry key: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options.
Under that key you’ll find a subkey for each executable you’ve configured. For mmc.exe, for example, you’ll see a couple of values:
- GlobalFlag: set to
0x100(decimal 256), which tells the system that Application Verifier is enabled for this executable. The GFlags tool from Debugging Tools for Windows shows this as “Application Verifier” when you look up the executable. - VerifierDlls: a space-separated list of DLL names to load. These are the provider DLLs. Importantly, there are no paths here, just names. The DLLs must be in
System32.
So if you want to inject your own DLL, you add its name to VerifierDlls. That’s all the configuration you need. The rest happens automatically when the process starts.
Creating a Verifier DLL
The DLL Project
The DLL itself is a standard Windows DLL project. In Visual Studio, create a new project using the Windows Desktop Wizard template, select Dynamic Link Library, and you’ll get a project with a DllMain entry point already set up.
There’s one important thing to do after you build it: copy it to System32. Since the verifier infrastructure requires the DLL to be there, there’s no way around it. That also means you need administrator privileges to put it there.
DllMain and the Verifier Reason Code
Here’s where things get a bit unexpected. If you just ship a bare DLL with a DllMain that handles DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH, and the thread reasons (0, 1, 2, 3), you’ll find that mmc.exe (or whatever executable you chose) just silently disappears when launched. No window, no error.
The reason is that verifier DLLs receive an additional call to DllMain with a reason code that is not part of the standard documented set. The standard codes are:
DLL_PROCESS_ATTACH(1)DLL_THREAD_ATTACH(2)DLL_THREAD_DETACH(3)DLL_PROCESS_DETACH(0)
But the verifier infrastructure calls DllMain with reason code 4. If you don’t handle it, the process crashes and disappears.
When that call comes in, the lpReserved parameter, which is normally unused or indicates whether the DLL is being loaded statically, is actually a pointer-to-pointer. The verifier infrastructure expects you to fill it with a pointer to an RTL_VERIFIER_PROVIDER_DESCRIPTOR structure. If you don’t, it dereferences something invalid and you get an access violation.
So the first thing to fix is adding reason code 4 to your switch statement:
#define DLL_VERIFIER 4
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
case DLL_VERIFIER:
// fill in lpReserved here
break;
}
return TRUE;
}The RTL_VERIFIER_PROVIDER_DESCRIPTOR Structure
The RTL_VERIFIER_PROVIDER_DESCRIPTOR structure is not officially documented. You can find it in the ReactOS source code (which reverse-engineers it from the actual Windows implementation) and in the MinGW headers. The structure is fairly large, but you don’t need to fill in most of it. The essential fields are:
Length: the size of the structure. Fill this in first.ProviderDlls: pointer to anRTL_VERIFIER_DLL_DESCRIPTORarray. Set this to point to an empty descriptor (all zeros, with a null-terminated sentinel entry) if you don’t want to hook anything, or to your actual hook list if you do.ProviderDllLoadCallbackandProviderDllUnloadCallback: callbacks invoked when DLLs load and unload in the process. These can be empty lambdas, but they should not be null.ProviderHeapFreeCallback: another callback, also non-null.
In practice, a minimal working implementation looks like this:
static RTL_VERIFIER_DLL_DESCRIPTOR emptyDlls[] = {
{ nullptr, 0, nullptr, nullptr }
};
static RTL_VERIFIER_PROVIDER_DESCRIPTOR g_verifierDesc = {};
// Inside DLL_VERIFIER:
g_verifierDesc.Length = sizeof(RTL_VERIFIER_PROVIDER_DESCRIPTOR);
g_verifierDesc.ProviderDlls = emptyDlls;
g_verifierDesc.ProviderDllLoadCallback = [](auto, auto, auto, auto) {};
g_verifierDesc.ProviderDllUnloadCallback = [](auto, auto, auto) {};
g_verifierDesc.ProviderHeapFreeCallback = [](auto, auto) {};
*(void**)lpReserved = &g_verifierDesc;Once you do this and copy the DLL to System32, the target executable loads correctly. You can verify in Process Explorer that your DLL appears in the process’s loaded modules.
Hooking APIs with the Verifier Infrastructure
RTL_VERIFIER_DLL_DESCRIPTOR
Now for the more interesting part. The verifier infrastructure gives you a built-in API hooking mechanism. You specify the DLLs you want to hook and the functions within each DLL, and the infrastructure patches the function pointers for you. You get the original address handed back so you can call through to the real implementation.
The data structure involved is RTL_VERIFIER_DLL_DESCRIPTOR. It describes one DLL from which you want to hook functions, and contains:
DllName: the DLL name with extension (e.g.,L"kernel32.dll"). The extension is required.DllFlags: set to zero.DllAddress: filled in by the infrastructure. Leave it alone.DllThunks: pointer to an array ofRTL_VERIFIER_THUNK_DESCRIPTORentries. The last entry must be all zeros (null sentinel).
Each RTL_VERIFIER_THUNK_DESCRIPTOR has three fields:
ThunkName: the exported function name to hook.ThunkOldAddress: a placeholder. The infrastructure writes the original function address here after patching.ThunkNewAddress: your hook function. This is what the infrastructure patches in.
For example, to hook VirtualAlloc from kernel32.dll:
// Forward declaration
LPVOID WINAPI HookVirtualAlloc(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);
static RTL_VERIFIER_THUNK_DESCRIPTOR g_funcs[] = {
{ (LPSTR)"VirtualAlloc", nullptr, HookVirtualAlloc },
{ nullptr, 0, nullptr } // sentinel
};
static RTL_VERIFIER_DLL_DESCRIPTOR g_dlls[] = {
{ (PWSTR)L"kernel32.dll", 0, nullptr, g_funcs },
{ nullptr, 0, nullptr, nullptr } // sentinel
};You then set g_verifierDesc.ProviderDlls = g_dlls; instead of the empty descriptor from before.
Implementing the Hook
Your hook function has the same signature as the original. To call through to the original, use the ThunkOldAddress field that the infrastructure filled in. Since it’s a void*, you need to cast it to the right type. A static local variable works well here to avoid repeating the cast:
LPVOID WINAPI HookVirtualAlloc(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect) {
static const auto originalFn = (decltype(&VirtualAlloc))g_funcs[0].ThunkOldAddress;
auto msg = std::format("VirtualAlloc: tid={} size={} type={:#x} prot={:#x} lp={}\n",
GetCurrentThreadId(), dwSize, flAllocationType, flProtect, lpAddress);
OutputDebugStringA(msg.c_str());
LPVOID result = originalFn(lpAddress, dwSize, flAllocationType, flProtect);
auto msg2 = std::format("VirtualAlloc result: {}\n", result);
OutputDebugStringA(msg2.c_str());
return result;
}Note the \n at the end of the format string. OutputDebugStringA does not add a newline automatically, so without it all the output runs together in DebugView (unless you set DebugView to add \n automatically).
You can extend g_funcs with as many functions from kernel32.dll as you like. If you want to hook functions from a different DLL, add another RTL_VERIFIER_DLL_DESCRIPTOR entry to g_dlls with its own thunk list.
Testing with DebugView
To see the hook output without attaching a debugger, use DebugView from Sysinternals. Make sure to enable “Capture Global Win32” so that output from elevated processes (like MMC) is captured.
With DebugView running, launch the target executable. You’ll immediately see VirtualAlloc calls appearing in the log, along with the parameters and return values. Every allocation the process makes goes through your hook.
In this case it’s just logging, but you can do anything in the hook: block allocations, redirect them, track them, modify parameters. The mechanism is general.
Limitations and Use Cases
The main limitation is the System32 requirement. Because arbitrary DLLs in arbitrary locations would be a significant security risk (any user could place a DLL that gets loaded into a privileged process), the verifier infrastructure restricts loading to System32 only. Getting a DLL into System32 requires administrator privileges, which limits the contexts where this technique is practical.
The second limitation is that targeting is by executable name, not by path. If you configure Application Verifier for mmc.exe, it applies to every mmc.exe that launches on the system, regardless of where it is located.
Those trade-offs aside, this technique is genuinely useful for:
- Security tooling and EDR prototypes that need to observe process behavior from very early in startup
- Debugging and instrumentation of processes that are difficult to attach to conventionally
- Research into DLL loading order and process initialization behavior
- Understanding how tools like Application Verifier and GFlags are implemented internally
The verifier infrastructure is also interesting from a defensive perspective: if your EDR or AVAM solution supports it, you can use the VerifierDlls key as an indicator of potentially suspicious configuration.
Keep Learning
If you want to go deeper on the mechanics behind this technique, the following TrainSec courses cover the relevant internals:
- Windows Internals Bundle: covers process and DLL loading internals in depth across all five days
- Windows Internals: Day 3: includes DLL loading, DllMain, and implicit/explicit DLL loading.
- EDR Internals: R&D: covers injection techniques and detection strategies from a security engineering perspective
- Malware Analysis and Development: covers DLL injection from the offensive and analysis perspective