文章目录
Windows消息钩子实现原理
在Windows开发中,如果要响应窗口各种鼠标,键盘和窗口消息,一般需要我们实现如下典型的窗口消息循环:
BOOL bRet;
while( (bRet = GetMessage( &msg, hWnd, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
但是在Windows消息响应的中间,可以通过一种方式来拦截窗口消息,这个就是Windows消息钩子。本文我们来探讨一下Windows消息钩子的实现原理。
1. 使用
消息钩子使用SetWindowsHookExW
就可以实现了,这个函数声明如下:
HHOOK SetWindowsHookExW(
int idHook,
HOOKPROC lpfn,
HINSTANCE hmod,
DWORD dwThreadId
);
一般来说,如果我们使用全局消息钩子,一般过程如下:
- 写一个Dll,作为消息钩子的回调函数。
SetWindowsHookExW
注册钩子(提供回调函数和模块信息)。
一般来说,我们的消息钩子函数可以实现如下:
LRESULT CALLBACK KeyboardProc(int code,WPARAM wParam,LPARAM lParam)
{
if (VK_F2==wParam && (lParam>>31 &1)==0)
{
return true;
}
else if (VK_F4==wParam && (lParam>>31 &1)==0)
{
SendMessage(g_hwnd,WM_CLOSE,0,0);
if (g_hook)
UnhookWindowsHookEx(g_hook);
return true;
}
else
return CallNextHookEx(g_hook,code,wParam,lParam);
assert(false);
return false;
}
2. SetWindowsHookExW
这里我们先通过SetWindowsHookExW
来看一下整个消息钩子的实现原理,先来看一下这个函数的调用堆栈:
0: kd> kb
# ChildEBP RetAddr Args to Child
00 a1f23bcc 94d38438 00000000 0112f57c fe8c6dd8 win32k!zzzSetWindowsHookEx
01 a1f23c14 83e751ea 00000000 0112f57c 00001564 win32k!NtUserSetWindowsHookEx+0x88
02 a1f23c14 774c70b4 00000000 0112f57c 00001564 nt!KiFastCallEntry+0x12a
03 0112f558 775de304 775de2ef 00000000 0112f57c ntdll!KiFastSystemCallRet
04 0112f55c 775de2ef 00000000 0112f57c 00001564 USER32!NtUserSetWindowsHookEx+0xc
05 0112f58c 775de2a7 00000000 00000000 00001564 USER32!_SetWindowsHookEx+0x33
06 0112f7c0 775de324 ffffffff 00ddaba7 00000000 USER32!SetWindowsHookExAW+0x57
07 0112f7dc 00dc8fff ffffffff 00ddaba7 00000000 USER32!SetWindowsHookExW+0x18
其中消息钩子的注册是在zzzSetWindowsHookEx
这个函数中实现的,这个函数的流程可以总结为如下:
ptiCurrent = PtiCurrent();
获取Win32线程结构。- 使用
phkNew = (PHOOK)HMAllocObject(ptiCurrent, ptiCurrent->rpdesk, TYPE_HOOK, sizeof(HOOK));
创建一个钩子的结构,这个结构类型为tagHOOK
。 - 然后获取到消息钩子结构保存的位置,这里分为两种情况:
- 如果是当前线程的消息钩子,那么就使用
pphkStart = &ptiThread->aphkStart[nFilterType + 1];
。 - 如果是全局的消息钩子,那么使用
pphkStart = &ptiCurrent->pDeskInfo->aphkStart[nFilterType + 1];
,并将钩子设置全局属性phkNew->flags |= HF_GLOBAL;
.
- 如果是当前线程的消息钩子,那么就使用
- 使用
phkNew->phkNext = *pphkStart;
将消息钩子插入到指定的链表中去。
在这里我们看一下这几个数据结构。
2.1 tagHOOK
这个表示着一个消息钩子的相关信息,整个声明如下:
typedef struct tagHOOK { /* hk */
THRDESKHEAD head;
struct tagHOOK *phkNext;
int iHook; // WH_xxx hook type
DWORD offPfn;
UINT flags; // HF_xxx flags
int ihmod;
PTHREADINFO ptiHooked; // Thread hooked.
PDESKTOP rpdesk; // Global hook pdesk. Only used when
// hook is locked and owner is destroyed
#ifdef HOOKBATCH
DWORD cEventMessages; // Number of events in the cache
DWORD iCurrentEvent; // Current cache event
DWORD CacheTimeOut; // Timeout between keys
PEVENTMSG aEventCache; // The array of Events
#endif // HOOKBATCH
} HOOK, *PHOOK;
我们可以看一下这个具体信息如下:
0: kd> dt win32k!tagHOOK ff691290
+0x000 head : _THRDESKHEAD
+0x014 phkNext : (null)
+0x018 iHook : 0n-1
+0x01c offPfn : 0xddaba7
+0x020 flags : 0
+0x024 ihmod : 0n-1
+0x028 ptiHooked : 0xfe8c6dd8 tagTHREADINFO
+0x02c rpdesk : (null)
+0x030 nTimeout : 0y0000000 (0)
+0x030 fLastHookHung : 0y0
我们从flags
知道,这个是局部消息钩子。
接下来我们看一下全局消息钩子的例子,我们看到,整个调用过程如下:
1: kd> kb 10
# ChildEBP RetAddr Args to Child
00 8835eb48 94d38438 64500000 002bee84 00000000 win32k!zzzSetWindowsHookEx
01 8835eb90 8f051ce2 64500000 002bee84 00000000 win32k!NtUserSetWindowsHookEx+0x88
1: kd> dd esp
8835eb4c 94d38438 64500000 002bee84 00000000
8835eb5c 00000005 64501470 00000000 1cd99fdb
8835eb6c 00000005 002bee84 00000000 8835eb68
8835eb7c 8f03247b 8835ebd4 94e40d90 000794b3
8835eb8c fffffffe 8835ebe4 8f051ce2 64500000
此时我们发现模块信息如下:
1: kd> dS 002bee84
002beebc "C:\Users\xxx\Desktop\win32\xxx.dll"
1: kd> !lmi xxx.dll
Loaded Module Info: [xxx]
Module: xxx
Base Address: 64500000
1: kd> u 64501470
xxx!xxxFun+0x80:
64501470 55 push ebp
64501471 8bec mov ebp,esp
64501473 8b4510 mov eax,dword ptr [ebp+10h]
64501476 8b4d0c mov ecx,dword ptr [ebp+0Ch]
64501479 8b5508 mov edx,dword ptr [ebp+8]
6450147c 50 push eax
此时我们可以看到我们创建的消息钩子的结构体信息如下:
2: kd> dt win32k!tagHOOK fea0c690
+0x000 head : _THRDESKHEAD
+0x014 phkNext : (null)
+0x018 iHook : 0n5
+0x01c offPfn : 0x1470
+0x020 flags : 1
+0x024 ihmod : 0n2
+0x028 ptiHooked : (null)
+0x02c rpdesk : (null)
+0x030 nTimeout : 0y0000000 (0)
+0x030 fLastHookHung : 0y0
从offPfn : 0x1470
这里我们可以发现,这个就是我们消息钩子函数相对模块的偏移值。那么模块怎么来呢?就是ihmod: 0n2
来表示,这个是一个原子表的值(相当一个句柄)。
因为我们线程消息钩子和全局消息钩子位置不同,保存的位置如下:
//线程钩子
typedef struct tagTHREADINFO {
W32THREAD;
//...
PHOOK aphkStart[CWINHOOKS]; // Hooks registered for this thread
//...
};
//全局桌面钩子
typedef struct tagDESKTOPINFO {
KERNEL_PVOID pvDesktopBase; // For handle validation
KERNEL_PVOID pvDesktopLimit; // ???
PWND spwnd; // Desktop window
DWORD fsHooks; // Deskop global hooks
PHOOK aphkStart[CWINHOOKS]; // List of hooks
PWND spwndShell; // Shell window
PPROCESSINFO ppiShellProcess; // Shell Process
PWND spwndBkGnd; // Shell background window
PWND spwndTaskman; // Task-Manager window
PWND spwndProgman; // Program-Manager window
PVWPL pvwplShellHook; // see (De)RegisterShellHookWindow
int cntMBox; // ???
} DESKTOPINFO;
2.2 从线程中查找钩子
从上面的分析,我们可以大致从线程中查找到相关的消息钩子,例如我们查一下全局消息钩子:
2: kd> dt nt!_KTHREAD -n Win32Thread 8ce2ba58
+0x18c Win32Thread : 0xfe429ac0 Void
2: kd> dt win32k!tagTHREADINFO -n pDeskInfo 0xfe429ac0
+0x0cc pDeskInfo : 0xfea00578 tagDESKTOPINFO
2: kd> dx -id 0,0,ffffffff8ce2bd40 -r1 ((win32k!tagDESKTOPINFO *)0xfea00578)
((win32k!tagDESKTOPINFO *)0xfea00578) : 0xfea00578 [Type: tagDESKTOPINFO *]
[+0x000] pvDesktopBase : 0xfea00000 [Type: void *]
[+0x004] pvDesktopLimit : 0xff600000 [Type: void *]
[+0x008] spwnd : 0xfea00618 [Type: tagWND *]
[+0x00c] fsHooks : 0x4040 [Type: unsigned long]
[+0x010] aphkStart [Type: tagHOOK * [16]]
[+0x050] spwndShell : 0xfea0b1a0 [Type: tagWND *]
[+0x054] ppiShellProcess : 0xffb53390 [Type: tagPROCESSINFO *]
[+0x058] spwndBkGnd : 0xfea0d758 [Type: tagWND *]
[+0x05c] spwndTaskman : 0xfea05768 [Type: tagWND *]
[+0x060] spwndProgman : 0x0 [Type: tagWND *]
[+0x064] pvwplShellHook : 0xfe6234b0 [Type: VWPL *]
[+0x068] cntMBox : 0 [Type: int]
[+0x06c] spwndGestureEngine : 0x0 [Type: tagWND *]
[+0x070] pvwplMessagePPHandler : 0x0 [Type: VWPL *]
[+0x074 ( 0: 0)] fComposited : 0x0 [Type: unsigned long]
[+0x074 ( 1: 1)] fIsDwmDesktop : 0x1 [Type: unsigned long]
2: kd> dx -id 0,0,ffffffff8ce2bd40 -r1 (*((win32k!tagHOOK * (*)[16])0xfea00588))
(*((win32k!tagHOOK * (*)[16])0xfea00588)) [Type: tagHOOK * [16]]
[0] : 0x0 [Type: tagHOOK *]
[1] : 0x0 [Type: tagHOOK *]
[2] : 0x0 [Type: tagHOOK *]
[3] : 0x0 [Type: tagHOOK *]
[4] : 0x0 [Type: tagHOOK *]
[5] : 0x0 [Type: tagHOOK *]
[6] : 0xfea0c690 [Type: tagHOOK *]
[7] : 0x0 [Type: tagHOOK *]
[8] : 0x0 [Type: tagHOOK *]
[9] : 0x0 [Type: tagHOOK *]
[10] : 0x0 [Type: tagHOOK *]
[11] : 0x0 [Type: tagHOOK *]
[12] : 0x0 [Type: tagHOOK *]
[13] : 0x0 [Type: tagHOOK *]
[14] : 0xfea164a8 [Type: tagHOOK *]
[15] : 0x0 [Type: tagHOOK *]
2: kd> dx -id 0,0,ffffffff8ce2bd40 -r1 ((win32k!tagHOOK *)0xfea0c690)
((win32k!tagHOOK *)0xfea0c690) : 0xfea0c690 [Type: tagHOOK *]
[+0x000] head [Type: _THRDESKHEAD]
[+0x014] phkNext : 0x0 [Type: tagHOOK *]
[+0x018] iHook : 5 [Type: int]
[+0x01c] offPfn : 0x1470 [Type: unsigned long]
[+0x020] flags : 0x1 [Type: unsigned int]
[+0x024] ihmod : 2 [Type: int]
[+0x028] ptiHooked : 0x0 [Type: tagTHREADINFO *]
[+0x02c] rpdesk : 0x0 [Type: tagDESKTOP *]
[+0x030 ( 6: 0)] nTimeout : 0x0 [Type: unsigned long]
[+0x030 ( 7: 7)] fLastHookHung : 0 [Type: int]
如上,我们就找到了上述注册的全局消息钩子了。
3. xxxCallHook
现在我们已经注册好了钩子了,那么钩子该什么时候被调用呢?我们看一下Windows消息的分发过程:
BOOL bRet;
while( (bRet = GetMessage( &msg, hWnd, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
这里猜测到是GetMessage
这个函数获取到消息的时候就会过滤一下钩子,因为这样的话对于程序来说这个消息就像没有发生一样,被HOOK拦截了。那么是否真是这样的呢?我们看一下这个调用堆栈:
1: kd> kb
# ChildEBP RetAddr Args to Child
00 a1fe5b04 94d43d65 00000000 00000001 a1fe5b30 win32k!xxxCallHook
01 a1fe5b68 94d862f0 fe8b7dd8 35122e73 018af85c win32k!xxxReceiveMessage+0x2dc
02 a1fe5bb8 94d89aa2 a1fe5be8 000025ff 00000000 win32k!xxxRealInternalGetMessage+0x252
03 a1fe5c1c 83e751ea 018af85c 00000000 00000000 win32k!NtUserGetMessage+0x3f
04 a1fe5c1c 774c70b4 018af85c 00000000 00000000 nt!KiFastCallEntry+0x12a
05 018af818 775ecde0 775ece13 018af85c 00000000 ntdll!KiFastSystemCallRet
06 018af81c 775ece13 018af85c 00000000 00000000 USER32!NtUserGetMessage+0xc
07 018af838 6c0014c1 018af85c 00000000 00000000 USER32!GetMessageW+0x33
这个函数如下:
int xxxCallHook(
int nCode,
WPARAM wParam,
LPARAM lParam,
int iHook)
{
BOOL bAnsiHook;
return (int)xxxCallHook2(PhkFirstValid(PtiCurrent(), iHook), nCode, wParam, lParam, &bAnsiHook);
}
其中PhkFirstValid
就是获取相关的消息钩子对象。看来Windows是在通过GetMessageW
获取到消息的时候,就会调用消息安装函数,然后对窗口消息进行过滤。
4. CallNextHookEx
从上面的分析我们知道xxxCallHook
只调用了一个其中一个钩子,那么钩子链怎么依次调用呢?这个就需要CallNextHookEx
这个函数了,这个函数的调用过程如下:
2: kd> kb
# ChildEBP RetAddr Args to Child
00 a248bb70 94dd69ff 00000003 00020238 a248bbdc win32k!xxxCallNextHookEx
01 a248bc00 94de86f8 00000003 00020238 001df008 win32k!NtUserfnHkINLPCBTCREATESTRUCT+0x10e
02 a248bc1c 83e751ea 00000003 00020238 001df008 win32k!NtUserCallNextHookEx+0x87
03 a248bc1c 774c70b4 00000003 00020238 001df008 nt!KiFastCallEntry+0x12a
04 001def44 7760313b 77603118 00000003 00020238 ntdll!KiFastSystemCallRet
05 001def48 77603118 00000003 00020238 001df008 USER32!NtUserCallNextHookEx+0xc
06 001def6c 64c6b71f 00120169 00000003 00020238 USER32!CallNextHookEx+0x71
xxxCallNextHookEx
过程如下:
LRESULT xxxCallNextHookEx(
int nCode,
WPARAM wParam,
LPARAM lParam)
{
BOOL bAnsiHook;
if (PtiCurrent()->sphkCurrent == NULL) {
return 0;
}
return xxxCallHook2(PhkNextValid(PtiCurrent()->sphkCurrent), nCode, wParam, lParam, &bAnsiHook);
}
PhkNextValid(PtiCurrent()->sphkCurrent)
这个就是获取下一个钩子了。
5. xxxLoadHmodIndex
上面我们知道,消息钩子被调用的过程如下:
int xxxCallHook(
int nCode,
WPARAM wParam,
LPARAM lParam,
int iHook)
{
BOOL bAnsiHook;
return (int)xxxCallHook2(PhkFirstValid(PtiCurrent(), iHook), nCode, wParam, lParam, &bAnsiHook);
}
这里有一个通过另外一个底层函数xxxCallHook2
来实现,这个函数中,会判断当前消息钩子所在的模块是否已经被加载,如果没有加载,那么将会加载模块:
LRESULT xxxCallHook2(
PHOOK phkCall,
int nCode,
WPARAM wParam,
LPARAM lParam,
LPBOOL lpbAnsiHook)
{
//...
fLoadSuccess = (xxxLoadHmodIndex(phkCall->ihmod) != NULL);
//...
}
xxxLoadHmodIndex
这个就是先通过原子表获取DLL所在路径,然后加载DLL,具体过程如下:
HANDLE xxxLoadHmodIndex(
int iatom)
{
//...
UserGetAtomName(aatomSysLoaded[iatom], pszLibName, sizeof(pszLibName)/sizeof(WCHAR));
RtlInitUnicodeString(&strLibrary, pszLibName);
hmod = ClientLoadLibrary(&strLibrary, (iatom == gihmodUserApiHook) ? goffPfnInitUserApiHook : 0);
//...
return hmod;
}
xxxLoadHmodIndex
这个机制在Windows下面造成一个很重要的功能,那就是Windows消息钩子带来的DLL注入功能,很多软件使用这个机制将自己的DLL注入到其他窗口进程,对其他进程做监控,拦截等操作。
6. 总结
有了上述的知识之后,我们就可以做到两件事情了:
- 消息钩子的枚举。
- 消息钩子的摘除。
很多的ARK工具都包含了上述两个功能,大致来说也是枚举和摘除相关链表。