Windows消息钩子实现原理

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

一般来说,如果我们使用全局消息钩子,一般过程如下:

  1. 写一个Dll,作为消息钩子的回调函数。
  2. 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这个函数中实现的,这个函数的流程可以总结为如下:

  1. ptiCurrent = PtiCurrent(); 获取Win32线程结构。
  2. 使用phkNew = (PHOOK)HMAllocObject(ptiCurrent, ptiCurrent->rpdesk, TYPE_HOOK, sizeof(HOOK)); 创建一个钩子的结构,这个结构类型为tagHOOK
  3. 然后获取到消息钩子结构保存的位置,这里分为两种情况:
    1. 如果是当前线程的消息钩子,那么就使用pphkStart = &ptiThread->aphkStart[nFilterType + 1];
    2. 如果是全局的消息钩子,那么使用pphkStart = &ptiCurrent->pDeskInfo->aphkStart[nFilterType + 1];,并将钩子设置全局属性phkNew->flags |= HF_GLOBAL;.
  4. 使用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. 总结

有了上述的知识之后,我们就可以做到两件事情了:

  1. 消息钩子的枚举。
  2. 消息钩子的摘除。

很多的ARK工具都包含了上述两个功能,大致来说也是枚举和摘除相关链表。

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值