0x40010006 Ways to Hook Your APIs

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 for GetProcAddressForCaller in kernelbase.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, calls ZwRaiseException 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

invlpg

a blog about exploring reverse engineering, hypervisor development and software programming.


Exploring a novel Windows API hooking technique using debug exceptions and stack manipulation to intercept GetProcAddress without modifying code

By koyz, 2025-07-12