2507C++,窗口勾挂事件
原文
许多人熟悉SetWindowsHookEx,API
,它提供了拦截与用户界面相关
的某些操作的方法,如针对窗口的消息,这里.
可在特定线程
上或在当前桌面的所有线程
上,设置大多数
这些勾挂.
或将DLL
注入到内联调用
的目标进程以处理相关事件
.
它还提供了与UI
相关的同样可通过回调处理
的各种事件.可在特定线程
或进程
附加它,也可在有连接到当前桌面的线程
的所有进程
上附加它.
有问题的API
是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);
该函数
允许在触发事件
时调用回调(pfnWinEventProc
).eventMin
和eventMax
提供了一个过滤事件
的简单方法
.如果需要所有事件
,可用EVENT_MIN
和EVENT_MAX
来覆盖一切事件
.
如果函数在DLL
内,则需要该模块,因此hmodWinEventProc
是,加载进调用过程
中的模块句柄
.与SetWindowsHookEx
类似,按需自动在目标进程
中加载DLL
.
如果两个ID
均为零,idProcess
和idThread
允许针对当前桌面
中的特定线程
,特定进程或所有进程
.即使没有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
来取有关与事件关联的对象
的更多信息
.访问
对象至少实现IAccessible
的COM
接口,但也可实现其他接口
,这里.
要从事件处理器
取IAccesible
指针,可用AccessibleObjectFromEvent
,这里:
CComPtr<IAccessible> spAcc;
CComVariant child;
::AccessibleObjectFromEvent(hwnd, idObject, idChild, &spAcc, &child);
我包含了<atlbase.h>
来取得ATL
客户支持(灵针和COM
类型包装器).在可引入IAccessible
的其他环境
中的其他API
,包括AccessibleObjectFromPoint
和AccessibleObjectFromWindow
.
注意,必须包含<oleacc.h>
并链接进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: DesktopEvent: 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 1
Event: 0x800B (Name Change) HWND: 0x00000000000A1D50, ID: 0x0 Child: 0x0 TID: 34756 Time: 70717859 Name: Untitled - Notepad
...
完整代码在zodiacon/WinEventHooks:SetWinEventHook
示例,这里.