> 技术文档 > 2507C++,窗口勾挂事件

2507C++,窗口勾挂事件

原文
许多人熟悉SetWindowsHookEx,API,它提供了拦截与用户界面相关的某些操作的方法,如针对窗口的消息,这里.

可在特定线程上或在当前桌面的所有线程上,设置大多数这些勾挂.

或将DLL注入到内联调用的目标进程以处理相关事件.

它还提供了与UI相关的同样可通过回调处理的各种事件.可在特定线程进程附加它,也可在有连接到当前桌面的线程所有进程上附加它.

有问题的APISetWinEventHook,这里:

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

该函数允许在触发事件时调用回调(pfnWinEventProc).eventMineventMax提供了一个过滤事件简单方法.如果需要所有事件,可用EVENT_MINEVENT_MAX覆盖一切事件.

如果函数在DLL内,则需要该模块,因此hmodWinEventProc是,加载进调用过程中的模块句柄.与SetWindowsHookEx类似,按需自动在目标进程中加载DLL.

如果两个ID均为零,idProcessidThread允许针对当前桌面中的特定线程,特定进程或所有进程.即使没有DLL,也可针对所有进程.

此时,事件信息将混杂回调用者的进程,并在那里调用.这确实需要传递WINEVENT_OUTOFCONTEXT标志来指示此要求.

以下示例显示如何为当前桌面中的所有进程/线程安装此类事件监听:

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

最后两个标志指示,不应报告调用者进程的事件.注意很奇怪取消息调用,它对要调用的事件处理器是必需的.奇怪的是,与需要非无效指针的函数的SAL相反,不需要消息(MSG)结构.这里

事件处理器自身可执行任何操作,但是,提供的信息与SetWindowsHookEx回调根本不同.如,无法\"改变\"任何东西–它只是通知已发生的事情.

这些事件与访问相关,与窗口消息没有直接关系.下面是事件处理器原型:

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

事件正在报告的事件.WinUser.h中定义了各种此类事件,且第三方和OEM可使用许多值.检查头文件是值得的,因为每个微软定义的事件都有有关何时触发此类事件的细节,及该事件的idObject,idChild窗柄的含义.

eventTid是事件发起的线程ID.hwnd一般是与事件(如果有)关联的窗口或控件的句柄,某些事件足够通用,因此没有提供窗柄.

可通过利用访问,API来取有关与事件关联的对象更多信息.访问对象至少实现IAccessibleCOM接口,但也可实现其他接口,这里.

要从事件处理器IAccesible指针,可用AccessibleObjectFromEvent,这里:

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

我包含了来取得ATL客户支持(灵针和COM类型包装器).在可引入IAccessible其他环境中的其他API,包括AccessibleObjectFromPointAccessibleObjectFromWindow.

注意,必须包含并链接进oleacc.lib.
IAccessible有很多方法和属性,其中最简单的是实现者必须提供的名字(Name):

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

IAccessible其他成员的文档.还可通过窗口句柄或线程ID,来取与事件关联的进程的细节,并取可执行文件名.下面是一个窗口句柄的示例:

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取与窗口句柄关联的进程ID(和线程ID).可用给定的线程ID,调用OpenThread,然后调用GetProcessIdOfThread,这里.这里

以下是转储所有使用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是一个按名转换某些事件ID的小助手.如果运行此代码(SimpleWinEventHook项目),你将看到大量输出,因为其中一个报告的事件是当鼠标光标位置更改时触发(除其他原因外)的EVENT_OBJECT_LOCATIONCHANGE:

Event: 0x800C (Name Change) HWND: 0x00000000000216F6, ID: 0xFFFFFFFC Child: 0x1DC TID: 39060 PID: 64932 (Taskmgr.exe) Time: 78492375 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: 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

不是在SetWinEventHook调用者的线程上取事件,相反,可注入DLL.此类DLL必须导出事件处理器,这样安装处理器的进程,可用GetProcAddress找到函数.

如,我创建了实现与前例类似(没有进程名)的事件处理器的简单DLL,如下:

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);}

注意,已导出该函数.代码使用printf,但不能保证目标进程有要使用的控制台.DllMain函数创建此控制台,并给它附加了标准输出句柄(否则printf将没有输出句柄,因为该进程不是用控制台引导的):

HANDLE hConsole;BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, PVOID lpReserved) { switch (reason) { case DLL_PROCESS_DETACH: if (hConsole) //很好 ::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;}

注入器进程(WinHookInject项目),如果有,首先取目标进程ID:

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; }

显示未提供PID的警告,因为为某些创建进程控制台可能会造成严重析构.如果确实想将DLL注入进桌面上的所有进程,请避免创建控制台.

一旦有了目标进程,需要加载DLL(为了简单而硬编码),并取导出的事件处理器函数:

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;}

最后一步是注册处理器.如果针对所有进程,最好限制你感兴趣的事件,尤其是嘈杂的事件.
如果只想注入DLL而不关心任何事件,请选择一个没有事件的范围,然后调用相关函数以强制在目标进程中加载DLL.剩下交给你.

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

注意,参数包括DLL模块,处理器地址和WINEVENT_INCONTEXT标志.
以下是在记事本实例上使用此DLL时的一些输出.记事本触发事件时,首次创建控制台:

Event: 0x800B (Name Change) HWND: 0x0000000000000000, ID: 0xFFFFFFF7 Child: 0x0 TID: 34756 Time: 70717718 Name: Edit......Event: 0x8004 () HWND: 0x0000000000010010, ID: 0xFFFFFFFC Child: 0x0 TID: 29516 Time: 70717859 Name: Desktop 1Event: 0x800B (Name Change) HWND: 0x00000000000A1D50, ID: 0x0 Child: 0x0 TID: 34756 Time: 70717859 Name: Untitled - Notepad...

完整代码在zodiacon/WinEventHooks:SetWinEventHook示例,这里.