0x40010006 is the Windows exception code for DBG_PRINTEXCEPTION_C, a debug print exception that we’ll repurpose for API hooking.
INTRODUCTION
In this blog post, we will learn more about the inner workings of exceptions, how the loader’s debug logging engine works, and how to work with the stack.
As a result, we achieve a patch-less novel hooking method for intercepting APIs like GetProcAddress
.
CONCEPT
Traditional hooking methods modify code (inline hooks), data structures (IAT/VMT), or require tooling with a lower privilege level. This technique exploits Windows’ debug infrastructure without modifying any code or data.
The key is LdrpDebugFlags
, an internal NTDLL variable that controls loader debug logging. When certain bits are set, it forces the loader through a debug print path that raises exception 0x40010006
. We catch this exception with a VEH handler and manipulate the stack to redirect execution flow.
No patches. No modifications. Just stack manipulation during legitimate Windows exception handling.
Why GetProcAddress?
As previously mentioned, this blog post will focus on hooking GetProcAddress
. I chose this API because it’s easy to demonstrate and clearly illustrates why this technique can cause problems for security-related products.
However, it is possible to hook several other Windows APIs with this. Further down, you will find a complete list of all functions vulnerable to this technique.
DISCOVERY
After noticing the pattern we will uncover now in IDA, I started analyzing the call chain of GetProcAddress
.
GetProcAddress
is actually just a stub forGetProcAddressForCaller
inkernelbase.dll
; henceforth, we will refer to it as such.
GetProcAddressForCaller (kernelbase.dll)
; ...
call LdrGetProcedureAddressForCaller ; Call into ntdll.dll
; ...
ret
LdrGetProcedureAddressForCaller (ntdll.dll)
; ...
call LdrpResolveProcedureAddress
; ...
ret
LdrpResolveProcedureAddress (ntdll.dll)
; ...
call LdrpGetProcedureAddress
; ...
ret
The LdrpDebugFlags Discovery
LdrpGetProcedureAddress (ntdll.dll)
; ...
mov esi, cs:LdrpDebugFlags
test sil, 5 ; Check bits 0 and 2.
jz skip_debug_log
; ...
lea rax, aLocatingProced_0 ; "Locating procedure \"%s\" by name\n"
; ... Setting up more parameters for LdrpLogDbgPrint.
call LdrpLogDbgPrint
I had never previously worked with or even examined the NTDLL loader’s debug logging system; however, I don’t seem to be the only one in this case.
When searching for LdrpDebugFlags
on any search engine, the results were few and far between, which made me even more curious.
This reveals that once we tamper with LdrpDebugFlags
, we can force the loader to take the logging path.
Understanding LdrpLogDbgPrint
Working with LdrpLogDbgPrint
, or even just getting the debug logs to appear, was pretty easy once I examined the decompilation.
_TEB* LdrpLogDbgPrint(
DWORD a1,
DWORD a2,
const char *a3,
int a4,
const char *a5,
... )
{
_TEB *result;
char pszDest[256];
va_list va;
va_start(va, a5);
// Key check: Skip if TEB flags indicate we're in a special state.
// Skip if SkipThreadAttach (bit 3) is NOT set OR RanProcessInit (bit 5) IS set.
if ((NtCurrentTeb()->SameTebFlags & 8) == 0 ||
(result = NtCurrentTeb(), (result->SameTebFlags & 0x20) != 0))
{
// Format the debug string.
StringCbPrintfA(pszDest, 0x100, "%04x:%04x @ %08d - %s - %s: ",
LODWORD(NtCurrentTeb()->ClientId.UniqueProcess),
LODWORD(NtCurrentTeb()->ClientId.UniqueThread),
(KUSER_SHARED_DATA.TickCountQuad * KUSER_SHARED_DATA.TickCountMultiplier) >> 24,
a3,
(&off_18011D398)[2 * a4]);
// Call internal print function with special flag.
return vDbgPrintExWithPrefixInternal(pszDest, 85, 0, a5, va, 1);
}
return result;
}
The function only performs some checks on the TEB
, specifically the SkipThreadAttach
and RanProcessInit
bits in the SameTebFlags
bit-field.
During development, I have always fulfilled these checks.
The bits of the bit-field can be found here: VergiliusProject
Those, however, were not the checks that cost me a few hours of critical thinking, verging on insanity…
As we can see, LdrpLogDbgPrint
is actually calling vDbgPrintExWithPrefixInternal
to deliver the debug message/exception.
Understanding vDbgPrintExWithPrefixInternal
int64_t __fastcall vDbgPrintExWithPrefixInternal(
BYTE *a1,
unsigned int a2,
unsigned int a3,
char *a4,
va_list a5,
char a6 )
{
// ... Setup Code ...
// Critical check for exception.
if (NtCurrentPeb()->FastPebLock &&
(NtCurrentPeb()->BeingDebugged ||
(KUSER_SHARED_DATA.KdDebuggerEnabled & 3) != 3))
{
ExceptionRecord.ExceptionCode = 0x40010006;
ExceptionRecord.ExceptionRecord = 0;
ExceptionRecord.NumberParameters = 2;
ExceptionRecord.ExceptionFlags = 0;
ExceptionRecord.ExceptionInformation[0] = (uint16_t)v14 + 1;
ExceptionRecord.ExceptionInformation[1] = (uint64_t)v11;
RtlRaiseException(&ExceptionRecord);
v8->SameTebFlags &= ~2;
return 0LL;
}
// ... DebugPrint without exception ...
}
As we can see, this function raises an exception (what we want) under certain conditions, namely the following:
PEB->FastPebLock
is not NULL (this is normally always true) AND- Either
PEB->BeingDebugged
is true OR KUSER_SHARED_DATA.KdDebuggerEnabled & 3
is not 3
The KdDebuggerEnabled
field checks bits 0 and 1:
- Bit 0: Kernel debugger is enabled
- Bit 1: Kernel debugger not present
When both bits are set (value 3), it means “debugger enabled but not present”, a normal state. Any other value (0, 1, or 2) indicates an active debugging session.
Now, smart and inexperienced me set out to just set PEB->BeingDebugged
to true and rush through to make the first PoC.
Little did koyz know, he had just fucked up.
This is where I spent too many hours trying to figure out why, after setting LdrpDebugFlags
to 5 and setting PEB->BeingDebugged
to true, I was only receiving the debug messages but not the exceptions.
Some readers who have already worked with exception handling in Ring-3 might know how Vectored-Exception Handling works.
Vectored-Exception Handling
In our case we expected that the exception would be created and dispatched in vDbgPrintExWithPrefixInternal
, as it calls RtlRaiseException
.
This function is fairly large and it would take up a considerable amount of this blog, so here is a small rundown:
- Captures the current thread context (registers, stack pointer, etc.).
- Performs stack unwinding to find the caller’s frame.
- Sets the exception address to the return address of the caller.
- If
PEB->BeingDebugged
is true, callsZwRaiseException
directly (kernel delivers to debugger). - Otherwise, calls
RtlDispatchException
to dispatch to user-mode handlers (VEH/SEH). - If no handler processed it, calls
ZwRaiseException
as last resort. - If a handler processed it, calls
RtlRestoreContext
to resume execution.
As the keen-eyed among you might have noticed, there is a small, teeny-tiny if in this function. Basically, if PEB->BeingDebugged
is set, it skips RtlDispatchException
.
This is a big problem for our use case, as ZwRaiseException
tries to deliver the exception to the debugger, but not to any Vectored-Exception Handlers.
After discovering this, I managed to gracefully hit my desk with my head a few times, set PEB->BeingDebugged
to false, and started receiving the exceptions in my Vectored-Exception Handler. It truly is that easy!
PROOF OF CONCEPT
Now that we are aware of how to trigger the exceptions, we need to catch them, identify the ones we are interested in, and manipulate the stack.
From now on, most signatures and offsets will be version-dependent. Everything you will see from here on was tested on 19045.5854
.
First, I wrote a small test program that simply executed GetProcAddress
with kernel32.dll
as the module and LoadLibraryA
as the function name.
Then I went into WinDbg to verify everything we had seen so far.
Call Chain and Stack Analysis
There are 4 functions we are really interested in: LdrpGetProcedureAddress
, LdrpLogDbgPrint
, vDbgPrintExWithPrefixInternal
, and RtlRaiseException
.
All I did was set breakpoints on these functions to see if they would get hit.
LdrpDebugFlags was already set to 5 at this point, and
PEB->BeingDebugged
was set to false.
0:000> bp ntdll!LdrpGetProcedureAddress
0:000> bp ntdll!LdrpLogDbgPrint
0:000> bp ntdll!vDbgPrintExWithPrefixInternal
0:000> bp ntdll!RtlRaiseException
0:000> g
Breakpoint 1 hit
ntdll!LdrpGetProcedureAddress:
00007ffc`a0442130 48895c2410 mov qword ptr [rsp+10h],rbx
0:000> g
Breakpoint 2 hit
ntdll!LdrpLogDbgPrint:
00007ffc`a04ddb18 48895c2408 mov qword ptr [rsp+8],rbx
0:000> g
Breakpoint 3 hit
ntdll!vDbgPrintExWithPrefixInternal:
00007ffc`a0461b08 48895c2410 mov qword ptr [rsp+10h],rbx
0:000> g
Breakpoint 4 hit
ntdll!RtlRaiseException:
00007ffc`a04620d0 4055 push rbp
Now, once we hit RtlRaiseException
I started checking out the _EXCEPTION_RECORD
.
0:000> r rcx
rcx=000000ae`f5f0f4e0
0:000> dt ntdll!_EXCEPTION_RECORD @rcx
+0x000 ExceptionCode : 0x40010006
+0x004 ExceptionFlags : 0
+0x008 ExceptionRecord : (null)
+0x010 ExceptionAddress : 0x00007ffc`a0461d3b
+0x018 NumberParameters : 2
+0x020 ExceptionInformation : [15] 0x62
This confirmed that the call chain we were observing for so long was correct and that we are looking out for the correct ExceptionCode
.
Just to be sure I checked out the call stack:
0:000> k
# Child-SP RetAddr Call Site
00 000000ae`f5f0f108 00007ffc`a0461d3b ntdll!RtlRaiseException
01 000000ae`f5f0f110 00007ffc`a04ddbf0 ntdll!vDbgPrintExWithPrefixInternal+0x233
02 000000ae`f5f0f2f0 00007ffc`a04c3f7d ntdll!LdrpLogDbgPrint+0xd8
03 000000ae`f5f0f450 00007ffc`a04404a8 ntdll!LdrpGetProcedureAddress+0x81dbd
04 000000ae`f5f0f4d0 00007ffc`a04400a5 ntdll!LdrpResolveProcedureAddress+0xb4
05 000000ae`f5f0f620 00007ffc`9db3fa6c ntdll!LdrGetProcedureAddressForCaller+0x2e5
06 000000ae`f5f0f780 00007ff7`94db11c1 KERNELBASE!GetProcAddressForCaller+0x6c
07 000000ae`f5f0f7d0 00007ff7`94db1949 testExe!main+0xb1
This confirmed that we were on the right track. However, it also told us that the return address value we would be searching for very shortly would be KERNELBASE!GetProcAddressForCaller+0x6c
.
This made it easy to go and search through the stack:
0:000> dps @rsp L200
...
000000ae`f5f0f778 00007ffc`9db3fa6c KERNELBASE!GetProcAddressForCaller+0x6c
000000ae`f5f0f780 00007ffc`9f5e0000 KERNEL32!RtlVirtualUnwindStub
...
Before I wanted to jump into writing the finished PoC, I needed to test whether my theory could be translated into practice.
This is why I simply overwrote the return address at 000000aef5f0f778
with a test function that prints something if it gets called.
0:000> x testExe!hkd__get_proc_address
00007ff7`51061000 testExe!hkd__get_proc_address
0:000> eq 000000ae`f5f0f778 00007ff7`51061000
0:000> dps 000000ae`f5f0f778 L1
000000ae`f5f0f778 00007ff7`51061000 testExe!hkd__get_proc_address
And sure it did! Once I continued execution the console printed:
*** HOOKED! GetProcAddress intercepted! ***
*** Module: D6166E54448C0000, Ordinal: 970 ***
This will also show up in the final PoC further down below.
True, the parameters don’t really look like they should. As I said earlier, I called them with the base address of kernel32.dll
and LoadLibraryA
as the function name.
D6166E54448C0000
is definitely not the base address, and my function detected an integer rather than a string, hence the interpretation as an Ordinal.
This all makes sense, of course. The stack has not magically been preserved for us; at this point, the values we are searching for are not where they were before.
Finding the Parameters
First, I wanted to look for the string “LoadLibraryA”:
0:000> s -a 0 L?0xffffffff "LoadLibraryA"
...
000000ae`f5f0f58b 4c 6f 61 64 4c 69 62 72-61 72 79 41 22 20 62 79 LoadLibraryA" by
...
This, however, turned out to be the debug string from LdrpLogDbgPrint
. While not completely useless, as we could extract it as a substring, finding the original values was more important.
Hence, I went back to examining my main function:
0:000> u testExe!main+0xb1-20 testExe!main+0xb1
testExe!main+0x91:
00007ff7`94db11a3 488d0dc12f0000 lea rcx,[testExe!`string' (00007ff7`94db416b)]
00007ff7`94db11aa e851000000 call testExe!printf
00007ff7`94db11af 488b4c2430 mov rcx,qword ptr [rsp+30h] ; hModule
00007ff7`94db11b4 488d15e02f0000 lea rdx,[testExe!`string' (00007ff7`94db419b)]
00007ff7`94db11bb ff15f7350000 call qword ptr [testExe!_imp_GetProcAddress]
This showed us:
- RCX = hModule (from [rsp+30h])
- RDX = lpProcName (address of “LoadLibraryA” string)
Then I started systematically searching for the parameters:
0:000> dps testExe!`string' L5
00007ff7`94db419b LoadLibraryA
0:000> s -q @rsp L2000 00007ff7`94db419b
000000ae`f5f0f6f0 00007ff7`94db419b
000000ae`f5f0f888 00007ff7`94db419b
...
0:000> lm m kernel32
start end module name
00007ffc`9f5e0000 00007ffc`9f6a2000 KERNEL32
0:000> s -q @rsp L2000 00007ffc`9f5e0000
000000ae`f5f0f6c8 00007ffc`9f5e0000
000000ae`f5f0fb68 00007ffc`9f5e0000
...
I specifically looked for locations where both parameters appeared close together and found:
0:000> dps 000000ae`f5f0fb60 L10
000000ae`f5f0fb60 00000000`00000000
000000ae`f5f0fb68 00007ffc`9f5e0000 KERNEL32!RtlVirtualUnwindStub
000000ae`f5f0fb70 00007ff7`94db419b testExe!`string' ; "LoadLibraryA"
000000ae`f5f0fb78 00000000`00000000
And there we have them, at offsets 0xb68
and 0xb70
, KERNEL32!RtlVirtualUnwindStub
may not look like the base address, but WinDbg seemed to misinterpret something here.
Why the above values are correct, but not quite
While double-checking the exception values and trying to get the exception context’s RSP, I noticed something strange:
0:000> !teb
...
0:000> .exr -1
ExceptionAddress: 00007ffc`a0461d3b
ExceptionCode: 40010006
ExceptionContextRecord: 000000ae`f5f0e3c0
0:000> dt ntdll!_CONTEXT 000000ae`f5f0e3c0 Rsp
+0x098 Rsp : 0x000000ae`f5f0f110
0:000> ? 000000ae`f5f0fb68 - 0x000000ae`f5f0f110
Evaluate expression: 2648 = 00000000`00000a58
These values don’t match up with our final values at all! This, however, was quite an easy fix. I noticed that when raising the exception, these values obviously don’t reflect the correct/final values as they would if caught inside an exception handler.
Simple enough, I quickly whipped up a dummy VEH like this:
LONG
vectored_handler(
PEXCEPTION_POINTERS exception_info
)
{
if (exception_info->ExceptionRecord->ExceptionCode == DBG_PRINTEXCEPTION_C)
{
__debugbreak();
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
Once the exception was dispatched, WinDbg would break inside the VEH, which allowed us to examine the correct values. After breaking in, I got right to work.
0:000> dv
exception_info = 0x000000e1`882fe480
0:000> dt _EXCEPTION_POINTERS 0x000000e1`882fe480
+0x000 ExceptionRecord : 0x000000e1`882ff610 _EXCEPTION_RECORD
+0x008 ContextRecord : 0x000000e1`882fe730 _CONTEXT
0:000> dt _CONTEXT 0x000000e1`882fe730 Rsp
+0x098 Rsp : 0x000000e1`882ff520
0:000> dps 0x000000e1`882ff520 L200
...
(searching through the output)
...
0:000> s -a 0x000000e1`882ff520 L2000 "LoadLibraryA"
000000e1`882ff58b LoadLibraryA" by
0:000> s -q 0x000000e1`882ff520 L2000 00007ffc`9f5e0000
000000e1`882ffb68 00007ffc`9f5e0000 00007ff7`94db419b
0:000> dps 0x000000e1`882ffb68-8 L4
000000e1`882ffb60 00000000`00000000
000000e1`882ffb68 00007ffc`9f5e0000 KERNEL32!RtlVirtualUnwindStub
000000e1`882ffb70 00007ff7`94db419b testExe!`string'
000000e1`882ffb78 00000000`00000000
0:000> ? 0x000000e1`882ffb68 - 0x000000e1`882ff520
Evaluate expression: 1608 = 00000000`00000648
0:000> ? 0x000000e1`882ffb70 - 0x000000e1`882ff520
Evaluate expression: 1616 = 00000000`00000650
Proving that our final offsets to the original parameters are at 0x648
and 0x650
respectively. This is also reflected in the source code of the PoC.
CODE WALKTHROUGH
Now that we finally have everything we need, I can walk you through the PoC. I will only be covering functions here that don’t immediately reveal what they do.
Starting off with the Vectored-Exception Handler.
We are only interested in exceptions that have the ExceptionCode
we are searching for; anything else gets forwarded via the EXCEPTION_CONTINUE_SEARCH
define.
LONG WINAPI vectored_exception_handler( const PEXCEPTION_POINTERS exception_info )
{
if ( exception_info->ExceptionRecord->ExceptionCode == DBG_PRINTEXCEPTION_C )
{
The next step includes getting the context and stack_pointer
from the exception_info
.
Furthermore, we get and read the original parameters from the aforementioned 0x648
and 0x650
offsets. Once we read them, we save them in a global captured_context
variable for later use.
printf( "[VEH] Caught debug print exception\n" );
const auto* context = exception_info->ContextRecord;
auto* stack_pointer = ( uint64_t* )context->Rsp;
auto* original_module = ( HMODULE ) * ( stack_pointer + 0x648 / 8 );
auto* original_proc_name = ( LPCSTR ) * ( stack_pointer + 0x650 / 8 );
captured_context.module = original_module;
captured_context.name = original_proc_name;
The next step is to retrieve information about kernelbase.dll
and use that information to get the address of GetProcAddressForCaller
.
const auto kernel_base = GetModuleHandleA( "kernelbase.dll" );
if ( !kernel_base )
{
return EXCEPTION_CONTINUE_EXECUTION;
}
auto* kernel_base_start = ( uint8_t* )kernel_base;
const auto* dos_header = ( PIMAGE_DOS_HEADER )kernel_base;
const auto* nt_headers = ( PIMAGE_NT_HEADERS )( kernel_base_start + dos_header->e_lfanew );
const auto kernel_base_size = nt_headers->OptionalHeader.SizeOfImage;
const auto get_proc_address_for_caller_pattern = utils::find_pattern(
( uint64_t )kernel_base_start,
kernel_base_size,
"\x48\x8B\xC4\x48\x89\x58\x00\x48\x89\x68\x00\x48\x89\x70\x00\x57\x48\x83\xEC\x00\x49\x8B\xE8\x48\x8B\xF2\x48\x8B\xF9",
"xxxxxx?xxx?xxx?xxxx?xxxxxxxxx"
);
if ( !get_proc_address_for_caller_pattern )
{
return EXCEPTION_CONTINUE_EXECUTION;
}
The last step includes the just-queried address of GetProcAddressForCallerPattern
.
Since we know that on the Windows version used, the return address is always at GetProcAddressForCaller+0x6C
, we search for it in the stack.
If we find it, we overwrite the return address on the stack with our hook_trampoline
function.
const auto target_return = get_proc_address_for_caller_pattern + 0x6C;
for ( int i = 0; i < 0x200; i++ )
{
const auto possible_return = stack_pointer[ i ];
if ( possible_return == target_return )
{
printf( "[VEH] Found GetProcAddressForCaller+0x6C at RSP+%X: %llX\n",
i * 8, ( void* )possible_return );
printf( "[VEH] Current RAX: %llX\n", ( void* )context->Rax );
stack_pointer[ i ] = ( uint64_t )hook_trampoline;
break;
}
}
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
Arguably, the next most important function is the trampoline we use to redirect to our “hooked” GetProcAddress
function.
Starting off with saving the original return address and all volatile registers.
extern "C" void hkd__hook_trampoline()
{
__asm {
pop rax
mov [o__return_address], rax
push rax
push rcx
push rdx
push r8
push r9
push r10
push r11
Next, we align the stack and reserve some space on the stack for our function.
Calling hkd__get_proc_address_for_caller
is the next step, which will then take the original parameters we saved and return a potentially different return value.
More about this in a moment.
sub rsp, 0x20
call hkd__get_proc_address_for_caller
add rsp, 0x20
After executing our hook logic, we need to restore all volatile registers (excluding rax, as it stores our return value) and jump back to the original return address.
pop r11
pop r10
pop r9
pop r8
pop rdx
pop rcx
add rsp, 8
jmp [o__return_address]
}
}
Last but not least, we have to talk about how the hooking part actually works. The logic for returning a different-than-expected return value is in hkd__get_proc_address_for_caller
.
As already mentioned, this is where we take advantage of saving the module and function name to the global struct instance captured_context
.
Calling the original GetProcAddress
retrieves the original return value.
extern "C" FARPROC hkd__get_proc_address_for_caller()
{
auto* module = captured_context.module;
auto* name = captured_context.name;
auto* original_result = o__get_proc_address( module, name );
The next steps are pretty straightforward: checking if the name
is actually a string, and finally checking if the API that is currently being queried is one we are interested in.
If these checks pass, we return a tampered value—in this case, a dummy value 0xDEADBEEFCAFEBABE
.
In case we are not interested in this API, we return the original value.
These checks are in no way comprehensive; they are only used to explain the general idea. I explain how to improve this PoC further below.
printf( "[HOOK] GetProcAddress intercepted\n" );
// Check if name is a string (not ordinal)
if ( HIWORD( name ) )
{
printf( "[HOOK] Module: %llX, Function: %s\n", module, name );
printf( "[HOOK] Original result: %llX\n", original_result );
// Example: Replace LoadLibraryA with custom implementation
if ( strcmp( name, "LoadLibraryA" ) == 0 )
{
printf( "[HOOK] Returning custom LoadLibraryA\n" );
return ( FARPROC )0xDEADBEEFCAFEBABE;
}
}
else
{
// Ordinal import
printf( "[HOOK] Module: %llX, Ordinal: %X\n", module, LOWORD( name ) );
}
return original_result;
}
And that’s pretty much it, this is how you can hook GetProcAddress
through debug exception messages.
IMPROVEMENTS
This whole PoC is by no means perfect, versatile, or usable except for demonstrating this technique. However, in my opinion, it is pretty easy to expand upon. With minimal effort, this can be turned into something more or less viable.
- Dynamic array/vector of
captured_context
, making this viable for multi-threaded applications. - Array of targets and return values.
- Dynamic stack traversal to find target strings and module bases without needing to hardcode offsets.
- PDB parsing to find
LdrpDebugFlags
.
With some simple modifications, this can be turned into a truly viable technique that can fool several security-related products.
However, I have to acknowledge that this is by no means a good and reliable hooking method; it’s just a fun gimmick that can be used at the expense of sacrificing reliability and stability.
The Proof-of-Concept can be found here: https://github.com/koyzdev/DebugExceptionHook
LIMITATIONS
This technique obviously comes with some limitations, as every hooking method does.
- Shadow Stacks render this method completely useless.
- Simple checks on
LdrpDebugFlags
or protecting this variable in some way also render this method unusable. - Windows updates will most likely cause instability.
These are just some limitations I could hastily think of; however, I’m sure there are many more.
VULNERABLE FUNCTIONS
LdrResolveDelayLoadedAPI
LdrpLoadDependentModule
LdrpCallTlsInitializers
LdrpHandleProtectedDelayload
LdrShutdownProcess
LdrpProcessWork
LdrpInitializeNode
_LdrpInitialize
LdrpCompleteMapModule
LdrpMinimalMapModule
LdrpFindDllActivationContext
LdrpFindKnownDll
LdrGetDllHandleEx
LdrpInitializeDllPath
LdrLoadDll
LdrpPreprocessDllName
LdrpFindLoadedDllInternal
LdrpLoadDllInternal
LdrpGetProcedureAddress
LdrpSnapModule
LdrpPrepareModuleForExecution
LdrpDynamicShimModule
LdrpSendPostSnapNotifications
LdrpReportError
LdrpInitializeTls
LdrpWriteBackProtectedDelayLoad
LdrpDoPostSnapWork
LdrpAllocateTls
LdrpSearchPath
LdrpResolveDllName
LdrpComputeLazyDllPath
LdrpDetectDetour
LdrpUnloadNode
LdrpProcessDetachNode
LdrpFindOrPrepareLoadingModule
LdrpInitShimEngine
LdrpLoadShimEngine
LdrpInitializeShimDllDependencies
LdrpGetShimEngineInterface
LdrpMergeNodes
LdrpRedirectDelayloadFailure
LdrpInitializeImportRedirection
LdrpInitializePerUserWindowsDirectory
LdrpRelocateImage
LdrpProtectAndRelocateImage
LdrpLoadWow64
LdrGetKnownDllSectionHandle
LdrInitShimEngineDynamic
LdrpGetProcApphelpCheckModule
LdrpInitializationFailure
LdrpInitializeApplicationVerifierPackage
LdrpInitializeExecutionOptions
LdrpInitializeProcess
LdrpInitializeProcessHeap
LdrpInitializeProcessWrapperFilter
LdrpIsSubstringFound
LdrpIsVerifierActivationFilterMatched
LdrpCheckRedirection
LdrpGenericExceptionFilter