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)