Obtaining unexported function addresses using exceptions

I had an interesting thought at work as I was digging into x64 exceptions, because it is so simple to obtain a function address (or determine that an address is within the range of a function) thanks to RUNTIME_FUNCTION entries in .pdata, you can do some interesting tricks.

One such use is, as I found out recently, to obtain start addresses of functions that crash in code you do not control.

I wanted the address to a function inside of a common windows module (user32.dll), there were exports that invoke the internal function regularly but there was no way to reliably (across operating systems) get the address to this without resorting to disassembly, essentially -

It struck me that I could cause an exception in that function, and use the RUNTIME_FUNCTION entries to determine the address.

The function for the purpose of this exercise is "ValidateHwnd", a function that takes a valid HWND and returns the internal tagWND structure.

tagWND* ValidateHwnd(HWND hWnd)  

I looked to see what functions might call this early, and ran into an obsolete function called "GetWindowWord", msdn doesn't even have easily available documentation for this function, but it does exist up into Windows 10.

WORD WINAPI GetWindowWord(HWND hWnd, int nIndex)  

I know the ValidateHwnd function uses gSharedInfo/g_sharedAheList/g_shared_heEntrySize to cycle through the undocumented USER_HANDLE_TABLE entries - but I also know to obtain the tagWND associated with those entries you must access the TEB->Win32ClientInfo entries for the "desktop heap" and "client delta"

Versions I've tested this on, Windows 7 and Windows 10 at least, seem to operate on the assumption the pointer to the desktop heap pointer for that TEB is always valid.

Disassembly of ValidateHwnd

We can cause it to crash exploiting this assumption.

The code below is the implementation of this theory, it works!

#include "stdafx.h"
#include <Windows.h>
#include "ntpebteb.h"

ULONG_PTR GetFunctionAddressFromAddress(ULONG_PTR pModuleAddress, ULONG_PTR pAddress)  
{
    UINT8* base = (UINT8 *)pModuleAddress;

    DWORD offset = (DWORD)((UINT8 *)pAddress - base);

    IMAGE_NT_HEADERS *nts = (IMAGE_NT_HEADERS *)(base + ((IMAGE_DOS_HEADER *)base)->e_lfanew);

    IMAGE_DATA_DIRECTORY *pData = &nts->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];

    SIZE_T numberOfEntries = pData->Size / sizeof(RUNTIME_FUNCTION);

    const RUNTIME_FUNCTION *runtimeBase =
        (const RUNTIME_FUNCTION *)(base + pData->VirtualAddress);

    for (SIZE_T i = 0; i < numberOfEntries; i++)
    {
        const RUNTIME_FUNCTION *rt = &runtimeBase[i];

        UNWIND_INFO *info = (UNWIND_INFO *)(base + rt->UnwindInfoAddress);

        if (offset >= rt->BeginAddress && offset < rt->EndAddress) {

            // If it's UNW_FLAG_CHAININFO, we walk back to the first not in the chain
            while (info->Flags == UNW_FLAG_CHAININFO) {
                rt--;

                info = (UNWIND_INFO *)(base + rt->UnwindInfoAddress);
            }

            return (ULONG_PTR)(base + rt->BeginAddress);
        }
    }

    return 0;
}

int ExceptionFilter(_EXCEPTION_POINTERS *ep, ULONG_PTR *pReturnValue)  
{
    HMODULE hUser32 = GetModuleHandleA("user32.dll");

    *pReturnValue = GetFunctionAddressFromAddress((ULONG_PTR)hUser32, (ULONG_PTR)ep->ExceptionRecord->ExceptionAddress);

    return EXCEPTION_EXECUTE_HANDLER;
}

ULONG_PTR GetValidateHwndAddress()  
{
    LPVOID lpInvalidMemory = VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_NOACCESS);
    if (!lpInvalidMemory) {
        printf("Unable to allocate invalid memory...\n");
        return 0;
    }

    TEB *pTeb = NtCurrentTeb();

    ULONG_PTR pOldDesktopHeap = pTeb->Win32ClientInfo[4];

    pTeb->Win32ClientInfo[4] = (ULONG_PTR)lpInvalidMemory;

    ULONG_PTR returnValue = 0;

    __try {
        // Explaination: 
        // ValidateHwnd uses the DESKTOP HEAP from the Win32ClientInfo
        // It's also the first function called in GetWindowWord, reliably across versions
        // So we can crash it and hijack the exception to reveal the function start.
        /*
        .text:0000000180075CB0 ; WORD __stdcall GetWindowWord(HWND hWnd, int nIndex)
        .text:0000000180075CB0                 public GetWindowWord
        .text:0000000180075CB0 GetWindowWord   proc near               ; DATA XREF: .rdata:0000000180084D79↓o
        .text:0000000180075CB0                                         ; .rdata:off_180090BC8↓o ...
        .text:0000000180075CB0                 push    rbx
        .text:0000000180075CB2                 sub     rsp, 20h
        .text:0000000180075CB6                 mov     ebx, edx
        .text:0000000180075CB8                 call    ValidateHwnd

        ->

        .text:000000018000DB23                 cmp     byte ptr [rcx+18h], 1
        .text:000000018000DB27                 jnz     short loc_18000DB42
        .text:000000018000DB29                 mov     rax, [rsi+20h]               ; here
        .text:000000018000DB2D                 test    rax, rax
        .text:000000018000DB30                 jz      short loc_18000DB94
        .text:000000018000DB32                 mov     rax, [rax]
        .text:000000018000DB35                 cmp     [rcx+10h], rax
        .text:000000018000DB39                 jnz     short loc_18000DB94
        .text:000000018000DB3B                 mov     rdi, [rsi+28h]
        */

        GetWindowWord(GetConsoleWindow(), 16);
    }
    __except (ExceptionFilter(GetExceptionInformation(), &returnValue)) { 
        printf("Exception was hit...\n");
    }

    // Restore the desktop heap pointer.
    pTeb->Win32ClientInfo[4] = pOldDesktopHeap;

    if (!VirtualFree(lpInvalidMemory, 0, MEM_DECOMMIT)) {
        printf("Unable to deallocate memory!\n");
    }

    return returnValue;
}

// I employ a trick here - ValidateHwnd is actually one argument
// but on Windows 7 and some prior OSes, the function we will capture is actually
// "HMValidateHandle" which requires the second argument be "1", but is otherwise the same
// Extra arguments don't really matter on x64, it'll just set a register to a value.
typedef PVOID(WINAPI *ValidateHwnd_t)(HWND hWnd, UCHAR Unk1);

int main()  
{
    ULONG_PTR pAddress = GetValidateHwndAddress();
    if (!pAddress) {
        printf("Unable to obtain address of ValidateHwnd.\n");
        return 1;
    }

    printf("Address: 0x%I64X\n", pAddress);

    ValidateHwnd_t pValidateHwnd = (ValidateHwnd_t)pAddress;

    HWND hWnd = GetConsoleWindow();
    PVOID pWnd = pValidateHwnd(hWnd, 1);

    printf("tagWND for console window = 0x%p\n", pWnd);

    return 0;
}

There's no real side-effects, it can work across multiple versions of a function if the bug you find to cause a crash persists across versions of windows.

So yeah, pretty neat.

Andrew Artz

Read more posts by this author.