Windows Hook Events

Many developers and researcher are faimilar with the SetWindowsHookEx API that provides ways to intercept certain operations related to user interface, such as messages targetting windows. Most of these hooks can be set on a specific thread, or all threads attached to the current desktop. A short video showing how to use this API can be found here. One of the options is to inject a DLL to the target process(es) that is invoked inline to process the relevant events.

There is another mechanism, less known, that provides various events that relate to UI, that can similarly be processed by a callback. This can be attached to a specific thread or process, or to all processes that have threads attached to the current desktop. The API in question is SetWinEventHook:

HWINEVENTHOOK SetWinEventHook(
    _In_ DWORD eventMin,
    _In_ DWORD eventMax,
    _In_opt_ HMODULE hmodWinEventProc,
    _In_ WINEVENTPROC pfnWinEventProc,
    _In_ DWORD idProcess,
    _In_ DWORD idThread,
    _In_ DWORD dwFlags);

The function allows invoking a callback (pfnWinEventProc) when an event occurs. eventMin and eventMax provide a simple way to filter events. If all events are needed, EVENT_MIN and EVENT_MAX can be used to cover every possible event. The module is needed if the function is inside a DLL, so that hmodWinEventProc is the module handle loaded into the calling process. The DLL will automatically be loaded into target process(es) as needed, very similar to the way SetWindowsHookEx works.

idProcess and idThread allow targetting a specific thread, a specific process, or all processes in the current desktop (if both IDs are zero). Targetting all processes is possible even without a DLL. In that case, the event information is marshalled back to the caller’s process and invoked there. This does require to pass the WINEVENT_OUTOFCONTEXT flag to indicate this requirement. The following example shows how to install such event monitoring for all processes/threads in the current desktop:

auto hHook = ::SetWinEventHook(EVENT_MIN, EVENT_MAX, nullptr, 
    OnEvent, 0, 0,
	WINEVENT_OUTOFCONTEXT | 
    WINEVENT_SKIPOWNPROCESS | WINEVENT_SKIPOWNTHREAD);

::GetMessage(nullptr, nullptr, 0, 0);

The last two flags indicate that events from the caller’s process should not be reported. Notice the weird-looking GetMessage call – it’s required for the event handler to be called. The weird part is that a MSG structure is not needed, contrary to the function’s SAL that requires a non-NULL pointer.

The event handler itself can do anything, however, the information provided is fundamentally different than SetWindowsHookEx callbacks. For example, there is no way to “change” anything – it’s just notifying about things that already happended. These events are related to accessibility and are not directly related to windows messaging. Here is the event handler prototype:

void CALLBACK OnEvent(HWINEVENTHOOK hWinEventHook, DWORD event, 
    HWND hwnd, LONG idObject, LONG idChild, DWORD eventTid, DWORD time);

event is the event being reported. Various such events are defined in WinUser.h and there are many values that can be used by third paries and OEMs. It’s worthwile checking the header file because every Microsoft-defined event has details as to when such an event is raised, and the meaning of idObject, idChild and hwnd for that event. eventTid is the thread ID from which the event originated. hwnd is typically the window or constrol associated with the event (if any) – some events are general enough so that no hwnd is provided.

We can get more information on the object that is associated with the event by tapping into the accessibility API. Accessibility objects implement the IAccessible COM interface at least, but may implement other interfaces as well. To get an IAccesible pointer from an event handler, we can use AccessibleObjectFromEvent:

CComPtr<IAccessible> spAcc;
CComVariant child;
::AccessibleObjectFromEvent(hwnd, idObject, idChild, &spAcc, &child);

I’ve included <atlbase.h> to get the ATL client side support (smart pointers and COM type wrappers). Other APIs that can bring an IAccessible in other contexts include AccessibleObjectFromPoint and AccessibleObjectFromWindow.

Note that you must also include <oleacc.h> and link with oleacc.lib.

IAccessible has quite a few methods and properties, the simplest of which is Name that is mandatory for implementors to provide:

CComBSTR name;
spAcc->get_accName(CComVariant(idChild), &name);

Refer to the documentation for other members of IAccessible. We can also get the details of the process associated with the event by going through the window handle or the thread ID and retrieving the executable name. Here is an example with a window handle:

DWORD pid = 0;
WCHAR exeName[MAX_PATH];
PCWSTR pExeName = L"";

if (hwnd && ::GetWindowThreadProcessId(hwnd, &pid)) {
    auto hProcess = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
    if (hProcess) {
        DWORD size = _countof(exeName);
        if (::QueryFullProcessImageName(hProcess, 0, exeName, &size))
            pExeName = wcsrchr(exeName, L'\\') + 1;
        ::CloseHandle(hProcess);
    }
}

GetWindowThreadProcessId retrieves the process ID (and thread ID) associated with a window handle. We could go with the given thread ID – call OpenThread and then GetProcessIdOfThread. The interested reader is welcome to try this approach to retrieve the process ID. Here is the full event handler for this example dumping all using printf:

void CALLBACK OnEvent(HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd,
    LONG idObject, LONG idChild, DWORD idEventThread, DWORD time) {
    CComPtr<IAccessible> spAcc;
    CComVariant child;
    ::AccessibleObjectFromEvent(hwnd, idObject, idChild, &spAcc, &child);
    CComBSTR name;
    if (spAcc)
        spAcc->get_accName(CComVariant(idChild), &name);
    DWORD pid = 0;
    WCHAR exeName[MAX_PATH];
    PCWSTR pExeName = L"";

    if (hwnd && ::GetWindowThreadProcessId(hwnd, &pid)) {
        auto hProcess = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
        if (hProcess) {
            DWORD size = _countof(exeName);
            if (::QueryFullProcessImageName(hProcess, 0, exeName, &size))
                pExeName = wcsrchr(exeName, L'\\') + 1;
            ::CloseHandle(hProcess);
        }
    }
    printf("Event: 0x%X (%s) HWND: 0x%p, ID: 0x%X Child: 0x%X TID: %u PID: %u (%ws) Time: %u Name: %ws\n",
        event, EventNameToString(event),
        hwnd, idObject, idChild, idEventThread, 
        pid, pExeName,
        time, name.m_str);
}

EventNameToString is a little helper converting some event IDs to names. If you run this code (SimpleWinEventHook project), you’ll see lots of output, because one of the reported events is EVENT_OBJECT_LOCATIONCHANGE that is raised (among other reasons) when the mouse cursor position changes:

Event: 0x800C (Name Change) HWND: 0x00000000000216F6, ID: 0xFFFFFFFC Child: 0x1DC TID: 39060 PID: 64932 (Taskmgr.exe) Time: 78492375 Name: (null)
Event: 0x8000 (Object Create) HWND: 0x00000000000216F6, ID: 0xFFFFFFFC Child: 0x1DD TID: 39060 PID: 64932 (Taskmgr.exe) Time: 78492375 Name: (null)
Event: 0x800C (Name Change) HWND: 0x00000000000216F6, ID: 0xFFFFFFFC Child: 0x1DD TID: 39060 PID: 64932 (Taskmgr.exe) Time: 78492375 Name: (null)
Event: 0x8000 (Object Create) HWND: 0x00000000000216F6, ID: 0xFFFFFFFC Child: 0x1DE TID: 39060 PID: 64932 (Taskmgr.exe) Time: 78492375 Name: (null)
Event: 0x800C (Name Change) HWND: 0x00000000000216F6, ID: 0xFFFFFFFC Child: 0x1DE TID: 39060 PID: 64932 (Taskmgr.exe) Time: 78492375 Name: (null)
...
Event: 0x800B (Location Changed) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 72172 PID: 0 () Time: 78492562 Name: Normal
Event: 0x800B (Location Changed) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 72172 PID: 0 () Time: 78492562 Name: Normal
...
Event: 0x800B (Location Changed) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 72172 PID: 0 () Time: 78492718 Name: Vertical size
Event: 0x800B (Location Changed) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 72172 PID: 0 () Time: 78492734 Name: Vertical size
Event: 0x800C (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 72172 PID: 0 () Time: 78492734 Name: Normal
Event: 0x800A (State Changed) HWND: 0x000000000001019E, ID: 0xFFFFFFFC Child: 0x16 TID: 15636 PID: 14060 (explorer.exe) Time: 78493000 Name: (null)
Event: 0x800A (State Changed) HWND: 0x00000000000101B0, ID: 0xFFFFFFFC Child: 0x6 TID: 15636 PID: 14060 (explorer.exe) Time: 78493000 Name: (null)
Event: 0x8004 () HWND: 0x0000000000010010, ID: 0xFFFFFFFC Child: 0x0 TID: 72172 PID: 1756 () Time: 78493000 Name: Desktop
Event: 0x8 (Capture Start) HWND: 0x0000000000271D5A, ID: 0x0 Child: 0x0 TID: 72172 PID: 67928 (WindowsTerminal.exe) Time: 78493000 Name: c:\Dev\Temp\WinEventHooks\x64\Debug\SimpleWinEventHook.exe
Event: 0x800B (Location Changed) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 72172 PID: 0 () Time: 78493093 Name: Normal
Event: 0x8001 (Object Destroy) HWND: 0x00000000000216F6, ID: 0xFFFFFFFC Child: 0x45 TID: 39060 PID: 64932 (Taskmgr.exe) Time: 78493093 Name: (null)
Event: 0x8001 (Object Destroy) HWND: 0x00000000000216F6, ID: 0xFFFFFFFC Child: 0xB0 TID: 39060 PID: 64932 (Taskmgr.exe) Time: 78493093 Name: (null)
...
Event: 0x800C (Name Change) HWND: 0x00000000000216F6, ID: 0xFFFFFFFC Child: 0x1A TID: 39060 PID: 64932 (Taskmgr.exe) Time: 78493093 Name: (null)
Event: 0x800C (Name Change) HWND: 0x00000000000216F6, ID: 0xFFFFFFFC Child: 0x1B TID: 39060 PID: 64932 (Taskmgr.exe) Time: 78493109 Name: (null)
Event: 0x800B (Location Changed) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 72172 PID: 0 () Time: 78493109 Name: Normal
Event: 0x9 (Capture End) HWND: 0x0000000000271D5A, ID: 0x0 Child: 0x0 TID: 72172 PID: 67928 (WindowsTerminal.exe) Time: 78493109 Name: c:\Dev\Temp\WinEventHooks\x64\Debug\SimpleWinEventHook.exe

DLL Injection

Instead of getting events on the SetWinEventHook caller’s thread, a DLL can be injected. Such a DLL must export the event handler so that the process setting up the handler can locate the function with GetProcAddress.

As an example, I created a simple DLL that implements the event handler similarly to the previous example (without the process name) like so:

extern "C" __declspec(dllexport)
void CALLBACK OnEvent(HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd,
	LONG idObject, LONG idChild, DWORD idEventThread, DWORD time) {
	CComPtr<IAccessible> spAcc;
	CComVariant child;
	::AccessibleObjectFromEvent(hwnd, idObject, idChild, &spAcc, &child);
	CComBSTR name;
	if (spAcc)
		spAcc->get_accName(CComVariant(idChild), &name);

	printf("Event: 0x%X (%s) HWND: 0x%p, ID: 0x%X Child: 0x%X TID: %u Time: %u Name: %ws\n",
		event, EventNameToString(event),
        hwnd, idObject, idChild, idEventThread,
		time, name.m_str);
}

Note the function is exported. The code uses printf, but there is no guarantee that a target process has a console to use. The DllMain function creates such a console and attached the standard output handle to it (otherwise printf wouldn’t have an output handle, since the process wasn’t bootstraped with a console):

HANDLE hConsole;

BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, PVOID lpReserved) {
	switch (reason) {
		case DLL_PROCESS_DETACH:
			if (hConsole)   // be nice
				::CloseHandle(hConsole);
			break;

		case DLL_PROCESS_ATTACH:
			if (::AllocConsole()) {
				auto hConsole = ::CreateFile(L"CONOUT$", GENERIC_WRITE, 
                    0, nullptr, OPEN_EXISTING, 0, nullptr);
				if (hConsole == INVALID_HANDLE_VALUE)
					return FALSE;
				::SetStdHandle(STD_OUTPUT_HANDLE, hConsole);
			}
			break;
	}
	return TRUE;
}

The injector process (WinHookInject project) first grabs a target process ID (if any):

int main(int argc, const char* argv[]) {
	DWORD pid = argc < 2 ? 0 : atoi(argv[1]);
	if (pid == 0) {
		printf("Warning: injecting to potentially processes with threads connected to the current desktop.\n");
		printf("Continue? (y/n) ");
		char ans[3];
		gets_s(ans);
		if (tolower(ans[0]) != 'y')
			return 0;
	}

The warning is shown of no PID is provided, because creating consoles for certain processes could wreak havoc. If you do want to inject a DLL to all processes on the desktop, avoid creating consoles.

Once we have a target process (or not), we need to load the DLL (hardcoded for simplicity) and grab the exported event handler function:

auto hLib = ::LoadLibrary(L"Injected.Dll");
if (!hLib) {
	printf("DLL not found!\n");
	return 1;
}
auto OnEvent = (WINEVENTPROC)::GetProcAddress(hLib, "OnEvent");
if (!OnEvent) {
	printf("Event handler not found!\n");
	return 1;
}

The final step is to register the handler. If you’re targetting all processes, you’re better off limiting the events you’re interested in, especially the noisy ones. If you just want a DLL injected and you don’t care about any events, select a range that has no events and then call a relevant function to force the DLL to be loaded into the target process(es). I’ll let the interested reader figure these things out.

auto hHook = ::SetWinEventHook(EVENT_MIN, EVENT_MAX, 
	hLib, OnEvent, pid, 0, WINEVENT_INCONTEXT);
::GetMessage(nullptr, nullptr, 0, 0);

Note the arguments include the DLL module, the handler address, and the flag WINEVENT_INCONTEXT. Here is some output when using this DLL on a Notepad instance. A console is created the first time Notepad causes an event to be raised:

Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 34756 Time: 70717718 Name: Edit
Event: 0x800C (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 34756 Time: 70717718 Name: Horizontal size
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 34756 Time: 70717718 Name: Horizontal size
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717734 Name: Horizontal size
Event: 0x800C (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717734 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717734 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717734 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717750 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717765 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717765 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717781 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717781 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717796 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717796 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717812 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717812 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717828 Name: Edit
Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 29516 Time: 70717843 Name: Edit
Event: 0x8 (Capture Start) HWND: 0x0000000000091CAC, ID: 0x0 Child: 0x0 TID: 29516 Time: 70717843 Name: (null)
Event: 0x3 (Foreground) HWND: 0x00000000000A1D50, ID: 0x0 Child: 0x0 TID: 34756 Time: 70717843 Name: Untitled - Notepad
Event: 0x8004 () HWND: 0x0000000000010010, ID: 0xFFFFFFFC Child: 0x0 TID: 29516 Time: 70717859 Name: Desktop 1
Event: 0x800B (Name Change) HWND: 0x00000000000A1D50, ID: 0x0 Child: 0x0 TID: 34756 Time: 70717859 Name: Untitled - Notepad
...

The full code is at zodiacon/WinEventHooks: SetWinEventHook Sample (github.com)