5 minute read

I wanted to experiment with Vectored Exception Handlers (VEH) on Windows. This was borne from discovering you can’t mess too much with SEH on 64-bit windows, and discovering that VEH are a very similar construct, and actually get resolved before SEH handlers!

I just wanted to learn about VEH by poking it, and then wound up combining it with some other annoying things to RE. I ended with an obfuscation technique, most of which is focused on messing with control flow. Here’s a summary of the techniques I’ll demonstrate:

- Using Vectored Exception Handlers (VEH) to redirect control flow
- Adding/Removing the VEH handlers without using any API calls OR writing custom functions
- Using `.CRT$XCAA` to play with Windows init-array
- Using hash-based API resolution to avoid generating imports. This means I control the import address table.
- Hiding XREFs in Ghidra to make the control flow more difficult to parse
- Using __try/__except blocks to emit misleading pseudocode

1. Walkthrough and RE

The source code is a wombo-combo of all the above techniques, so I’m just going to go through what it looks like in Ghidra in order of execution (ie, the actual control-flow).

Here’s a diagram showing the order of execution:

control_flow_diagram

2. Windows _initterm()

This code uses the Windows equivalent of .init_array, which is called by a CRT function _initterm. It has an argument commonly known as “pfbegin”, which marks the beginning of the init array:

initterm_pfbegin

Stuffing your code here ensures my_custom_init runs before main. This is a known trick, but when RE-ing in Ghidra the analyst just has to know to look for this structure.

Here’s what it looks like in source:

#pragma section(".CRT$XCAA", read)
__declspec(allocate(".CRT$XCAA")) void (*pfn_my_init)(void) = my_custom_init;


void my_custom_init(void) {
    __try {
         volatile int* p = NULL;
         *p = 0;
    }
     __except(EXCEPTION_EXECUTE_HANDLER) {
        AddHandler();
    }
}

You’ll notice The function my_custom_init is spiced up a little bit with a __try/__except block. This block messes up the decompilation, which eliminates XREFs to the AddHandler() function. Here’s what this looks like in Ghidra by default:

my_custom_init

The 4 XREFs you are seeing aren’t really “real”. Two of them are just because this function happens to be the ImageEntry address, the third is an SEH structure called _IMAGE_RUNTIME_FUNCTION_ENTRY:

my_custom_init_imagertfunctionentry

And finally the last one is the actual pfbegin struct.

Note that none of this is labeled by default, so if you are unfamiliar with the structures and when/where they are called this would be highly confusing.

3. AddHandler and RtlAddVectoredExceptionHandler

AddHandler is what calls RtlAddVectoredExceptionHandler, except not quite:

__declspec(noinline) void AddHandler(void)
{
    dprintf("Adding handler.\n");
    void* ntdll_addr = NoAPIGetBaseAddress(HASH_ROR13_ntdll_dll);
    void* VEH = NoAPIGetProcAddress(ntdll_addr, HASH_ROR13_RtlAddVectoredExceptionHandler);

    RtlAddVectoredExceptionHandler_t func = (RtlAddVectoredExceptionHandler_t)((char*)VEH+0x10);
    gVEHHandlerPointer = func(1, (unsigned long long)veh_handler, 0);
}

You will notice that I am adding 0x10 to the address that is returned by HASH_ROR13_RtlAddVectoredExceptionHandler. That’s because I noticed in ntdll.dll that both RtlAddVectoredExceptionHandler and RtlRemoveVectoredExceptionHandler actually just do some small setup, then JMP to the real functionality:

malcat_16byte_jump

4. Add VEH Handler

We set out our VEH handler for access violations, which we will trigger in main(). When you are in a VEH handler, you have control over the registers through a CONTEXT structure. This allows you to set the instruction pointer. So, instead of handling the error we will set RIP to our “payload” function, which is hidden_function. The VEH handler will get called twice though, since we don’t actually handle the exception. So, on the first exception we jump to our payload, and on the second exception (when the payload returns) we actually handle the exception and return execution back to main().

LONG WINAPI veh_handler(PEXCEPTION_POINTERS ExceptionInfo) {
    dprintf("VEH handler called\n");
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
        if (counter == 0) {
            originalRIP = ExceptionInfo->ContextRecord->Rip;
            dprintf("Original RIP: %llx...\n", originalRIP);
            // Redirect execution to our hidden function
            ExceptionInfo->ContextRecord->Rip = (DWORD64)&hidden_function;
            counter++;
        }
        else
        {
            // Remove the exception handler and restore execution past the faulting instruction (6 byte instruction)
            void* ntdll_addr = NoAPIGetBaseAddress(HASH_ROR13_ntdll_dll);
            void* VEH = NoAPIGetProcAddress(ntdll_addr, HASH_ROR13_RtlRemoveVectoredExceptionHandler);
            RtlRemoveVectoredExceptionHandler_t RtlRemoveVectoredExceptionHandler = (RtlRemoveVectoredExceptionHandler_t)((char*)VEH+0x10);
            RtlRemoveVectoredExceptionHandler(gVEHHandlerPointer, 0);
            dprintf("Adjust RIP back to main: %llx %llx\n", originalRIP, originalRIP + 6);
            ExceptionInfo->ContextRecord->Rip = originalRIP + 6;
            ExceptionInfo->ContextRecord->Rsp -= 8; // Adjust stack back to normal
        }
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

Here’s how it works step-by-step:

On first execution, the handler:

- Saves the original instruction pointer (RIP)

- Redirects execution to our payload, `hidden_function()`

- Leaves the exception unresolved, so execution comes back to the exception handler

On the second exception, after hidden_function returns, it:

- Removes the VEH

- Repairs the stack for normal execution

- Restores RIP to originalRIP + 6 (6 is the length of the faulting instruction)

This creates an execution path that isn’t immediately obvious. This is helped by eliminating XREFs at the my_custom_init and AddHandler points.

5. Payload Execution

This payload is just a POC to prove it works, called hidden_function(). It simply creates a thread that displays a Message Box:

    void hidden_function(void) {
        void* kernel32_addr = NoAPIGetBaseAddress(HASH_ROR13_KERNEL32_DLL);
        CreateThread_t fCreateThread = (CreateThread_t)NoAPIGetProcAddress(kernel32_addr, HASH_ROR13_CreateThread);
        WaitForSingleObject_t fWaitForSingleObject = (WaitForSingleObject_t)NoAPIGetProcAddress(kernel32_addr, HASH_ROR13_WaitForSingleObject);

        HANDLE hThread = fCreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
        if (hThread != NULL) {
            fWaitForSingleObject(hThread, INFINITE);
        }
    }

    DWORD WINAPI ThreadProc(LPVOID lpParam) {
        void* user32_addr = NoAPIGetBaseAddress(HASH_ROR13_USER32_dll);
        MessageBoxA_t fMessageBox = (MessageBoxA_t)NoAPIGetProcAddress(user32_addr, HASH_ROR13_MessageBoxA);
        fMessageBox(NULL, "Hacked!", "lol", MB_OK | MB_ICONINFORMATION);
        return 0;
    }

exe_running

6. API Resolution with Hashed Lookups (No Imports)

To control the Import Address Table and hide specific Windows APIs, I’m using some functionality from one of my other projects (mglib):

    NoAPIGetBaseAddress(DWORD target_dll_hash);
    NoAPIGetProcAddress(void* module_base, DWORD target_hash);

This is a PIC implementation of LoadLibrary and GetProcAddress that crawls the PEB and locates labels by hash. In decompilation, this is what the calls to CreateThread and WaitForSingleObject look like:

noapi_decompilation

This makes the resulting binary have no imports for:

- MessageBoxA  
- CreateThread  
- WaitForSingleObject  
- RtlAddVectoredExceptionHandler  
- RtlRemoveVectoredExceptionHandler

For example, here’s what my imports looks like for user32.dll, which has MessageBoxA, and kernel32.dll, which has CreateThread:

iat_combined

While this functionality is similar to LoadLibrary and GetProcAddress, I wrote the NoAPI functions for use in PIC blobs. Since they are hash-based, they have the added advantage of not including strings in your binary. Most of the time, there’s no way to know what functions and dlls the hashes refer to until you either run the code dynamically or brute-force the hashes. Shout-out to one of my favorite RE tools though, Malcat. If the hash is well-known, it might auto-identify and display what the hash resolves to:

malcat_identifying_hash

7. Recap

In summary, you can use a combination of init arrays, __try/__except, VEH handlers, and NoAPI functions to really mess with control flow and obfuscate a binary:

- Prevent XREFs to sensitive functions.
- Hide executable code using __try/__except blocks, which Ghidra fails to decompile by default.
- Hide setup code in the .CRT$XCAA section
- NoAPI implementations of LoadLibrary/GetProcAddress to eliminate meaningful strings and control the Import Address Table
  1. Full Source Code

Coming soon, will push into a repo. In the meantime, https://github.com/guffre/mglib/ has the NoAPI code.

Updated: