windows钩子


windows应用程序是基于消息驱动的。各种应用程序对各种消息作出响应从而实现各种功能。

      windows钩子是windows消息处理机制的一个监视点,通过安装钩子可以达到监视指定窗口某种类型的消息的功能。所谓的指定窗口并不局限于当前进程的窗口,也可以是其他进程的窗口。当监视的某一消息到达指定的窗口时,在指定的窗口处理消息之前,钩子函数将截获此消息,钩子函数既可以加工处理该消息,也可以不作任何处理继续传递该消息。使用钩子是实现dll注入的方法之一。其他常用的方法有:注册表注入,远程线程注入。

       钩子函数是一个处理消息的程序段。是在安装钩子的时候向系统注册的

关于windows钩子要清楚以下三点:

     1:钩子是用来截获系统中的消息流的。利用钩子可以处理任何我们感兴趣的消息,当然包括其他进程的消息。

    2:截获该消息后,用于处理该消息的程序叫做钩子函数。它是自定义的函数,在安装钩子时将此函数的地址告诉windows。

3:系统同一时间可能有多个进程安装钩子,多个钩子构成钩子链。所以截获消息并处理后,应该将此消息继续传递下去,以便其他钩子处理这一消息。

      注意:使用钩子会使系统变慢,因为它增加了系统对每个消息的处理量。所以要仅在必要的时候才安装钩子。不需要时要及时卸载。

  安装钩子:

  1:

  1. SetWindowsHookEx(  
  2.   
  3.          int idHook,                  //要安装的钩子的类型。  
  4.   
  5.          HOOKPROC lpfn,                  //钩子函数的地址。  
  6.   
  7.          HINSTANCE hMode,               //钩子函数所在DLL在进程内的地址。  
  8.   
  9.          DWORD     dwThread,            //要安装钩子的线程。如为0,则为所有线程安装钩子。  
  10.   
  11.          );  
SetWindowsHookEx(

         int idHook,                  //要安装的钩子的类型。

         HOOKPROC lpfn,                  //钩子函数的地址。

         HINSTANCE hMode,               //钩子函数所在DLL在进程内的地址。

         DWORD     dwThread,            //要安装钩子的线程。如为0,则为所有线程安装钩子。

         );


idHook指定要安装钩子的类型,他可以是下面的值:

     WH_CALLWNDPROC           //目标线程调用SendMessage发送消息时,钩子函数被调用。

     WH_CALLWNDPROCRET   //当SendMessage返回时,钩子函数被调用。

     WH_KEYBOARD                 //从消息队列中查询WM_KEYUP或WM_KEYDOWN时。

     WH_GETMESSAGE           //目标线程调用GetMessage或PeekMessage时

     WH_MOUSE                      //查询消息队列中鼠标事件消息时。

     WH_MSGFILTER              //以下请参考MSDN。

     WH_SYSMSGFILTER

     WH_JORNALRECORD

     WHJORNALPLAYBACK

     WH_SHELL

     WH_CBT

     WH_FOREGROUNDIDLE

     WH_DEBUG

    2 :

     lpfn是钩子函数的地址。钩子安装后如果有相应的消息发生,windows将调用此参数指向的函数。一般钩子函数都是位于一个DLL中。当为其他进程内的线程安装钩子时,如果钩子函数在DLL中,系统会把DLL映射到那个进程内,使他能在该进程内被调用。

     注意:钩子函数多是被其他进程内的线程调用,而不一定是安装钩子的线程。

钩子函数被调用的过程

      当进程A一个线程准备向一个窗口发送一个消息,系统检查该线程是否被安装了钩子,如果该线程被安装了钩子且该消息与钩子要截获的消息类型一致,此消息将被截获。系统检查该钩子的钩子函数所在的DLL是否已经被映射进程A的地址空间中。如果尚未映射,系统会强制将该DLL映射到进程A的地址空间。然后获得钩子函数在进程A的虚拟地址,并调用钩子函数。我们可以在钩子函数内定义我们对该消息处理的过程。

      注意:当系统把钩子函数所在的DLL映射到某个进程地址空间时,会映射整个DLL,而不仅仅是钩子函数,这也就说我们可以使用该DLL中的所有导出函数。

     3:hmod参数是钩子函数所在dll的实例句柄,也是该dll在进程内的虚拟地址。如果钩子函数在当前进程中,此参数应被指定为NULL.

    4:dwThreadid指定要被安装钩子的线程的ID号。如果被设为0,就会为系统内的所有GUI线程安装钩子。

   5:钩子函数

   钩子被安装后,如果有相应的消息发生,windows将调用钩子函数。以下为钩子函数的原型:

  

  1. LRESULT CALLBACK HookProc(int nCode,WPARAM wParam,LPARAM lParam)  
  2.   
  3.  {  
  4.   
  5.    //处理消息的代码。  
  6.   
  7.     return CallNextHookEx(hHook,nCode,wParam,lParam);  
  8.   
  9.  }  
  LRESULT CALLBACK HookProc(int nCode,WPARAM wParam,LPARAM lParam)

   {

     //处理消息的代码。

      return CallNextHookEx(hHook,nCode,wParam,lParam);

   }


 

    HookProc为钩子函数的名称。

    nCode指定是否必须处理该消息。如果它为HC_ACTION,那么钩子函数就必须处理该消息。如果小于0,钩子函数就必须将该消息传递给CallNextHookEx,不对该消息进行处理,并返回CallNextHookEx的返回值。

    CallNextHookEx用于把消息传递到钩子链中下一个钩子函数。

    wParam和lParam的值依赖于具体的钩子类型。请参考MSDN。 

    卸载钩子

     BOOL UnhookWindowsHookEx(HHOOK hhk);

     hhk为要卸载的钩子句柄。

 

     下面将要实现一个例子,实现对键盘按键的监控。一旦有键盘被按下,就在主程序窗口显示一条信息指示哪一个键被按下。

    程序外观:

 

    首先要实现DLL:

     在dll内实现钩子函数这是毫无疑问的。而安装钩子和卸载钩子的函数既可以写在主程序内,也可以写在DLL内。写在主程序内时只可以在主程序内安装钩子。而在dll内实现则可以让所有载入该dll的程序安装钩子。如当某进程将该DLL载入的时候,可以在DllMain中创建一个线程,让他调用安装钩子的函数,实现为此进程内的线程安装钩子的目的。为了拓展程序的功能,实现代码重用,最好是将钩子函数写在DLL内。另外这也可以实现模块化。一旦需求发生更改可以只修改DLL内的代码,而不需要改变主程序。

      当钩子函数被调用的时候,也就是我们被拦截的消息已被触发,如何让主程序得到这个通知呢 ?

      我们可以在其他进程内的钩子函数内给主程序的窗口发送消息。但如何发送呢?

      PostMessage可以实现这个功能。

      看原型:

  1. BOOL WINAPI PostMessage(HWND hWnd,UINT Msg,WPARAM wparam,LPARAM lParam);  
    BOOL WINAPI PostMessage(HWND hWnd,UINT Msg,WPARAM wparam,LPARAM lParam);


     hWnd即为要接受消息的窗口句柄。

     Msg为要发送的消息。

    wParam和lParam为消息的附加参数。

    虽然可以使用PostMessage实现向主程序的窗口发送消息,但是我们如何获得主程序的窗口句柄呢?我们知道钩子函数是在DLL内实现的,而DLL会被加载到各个进程内。在其他进程要想得到主程序的窗口句柄这是一个问题。

    在《windows核心编程系列》谈谈内存映射文件中,我们谈到了在可执行文件内使用共享段,可以实现同一个可执行文件的多个实例共享共享段内的数据的目的。那么在DLL使用共享段呢?哈哈,或许你已经猜出来了,由于DLL被映射到了各个进程,将数据放在DLL的共享段,可以实现在各个进程内共享DLL内共享段数据的目的。

     我们的解决方法就是:在DLL内建立共享段,将主程序的窗口句柄放在共享段中。在主程序调用安装钩子的函数时可以将共享段内的窗口句柄赋为主程序的窗口句柄。从而达到在各个进程内共享数据的目的。到此,我们又学习一种在进程间共享数据的方法,另一种方法是利用内存映射文件。

建立和设置共享段的代码:可以参考《windows核心编程》谈谈内存映射文件。


 

  1. <span style="font-size: 18px;"> #pragma data_seg("shared")  
  2.   HWND hWnd=NULL;  
  3.   HHOOK hHook=NULL;  
  4.   
  5.  #pragma data_seg()  
  6.   
  7. #pragma comment(linker,"/SECTION:shared,RWS")  
  8.   
  9. </span>  
<span style="font-size: 18px;"> #pragma data_seg("shared")
  HWND hWnd=NULL;
  HHOOK hHook=NULL;

 #pragma data_seg()

#pragma comment(linker,"/SECTION:shared,RWS")

</span>


 

     怎么多了个hHook,hHook是创建的钩子的句柄。由于在钩子函数中会调用CallNextHookEx将消息传给钩子链的下一结点。二者都是在其他进程调用的,因此我们也必须把钩子的句柄设为共享。

 

     DLL内创建钩子的代码:

  1. <span style="font-size: 18px;">    KEYHOOKDLL_API bool SetHook(</span>  
<span style="font-size: 18px;">    KEYHOOKDLL_API bool SetHook(</span>
  1. <span style="font-size: 18px;">                            bool IsInstall,//true表示安装钩子,false表示卸载钩子。</span>  
<span style="font-size: 18px;">                            bool IsInstall,//true表示安装钩子,false表示卸载钩子。</span>
  1. <span style="font-size: 18px;">                            HWND hWnd,     //主程序窗口句柄,用于在主程序内传入设置。</span>  
<span style="font-size: 18px;">                            HWND hWnd,     //主程序窗口句柄,用于在主程序内传入设置。</span>
  1. <span style="font-size: 18px;">                              int ThreadId)//要安装钩子的线程。  
  2.    {  
  3.     ::hWnd=hWnd;//将当前窗口句柄赋给DLL共线段内的窗口句柄。  
  4.     if(IsInstall)  
  5.     {  
  6.         hHook=SetWindowsHookEx( WH_KEYBOARD,KeyHookProc,GetModuleHandle  </span>  
<span style="font-size: 18px;">                              int ThreadId)//要安装钩子的线程。
   {
	::hWnd=hWnd;//将当前窗口句柄赋给DLL共线段内的窗口句柄。
	if(IsInstall)
	{
		hHook=SetWindowsHookEx( WH_KEYBOARD,KeyHookProc,GetModuleHandle  </span>
  1. <span style="font-size: 18px;">                                           ("keyhookdll"),ThreadId);  
  2.         return true;  
  3.   
  4.     }  
  5.     else  
  6.     {  
  7.         UnhookWindowsHookEx(hHook);  
  8.         return true;  
  9.   
  10.     }  
  11.   
  12. }</span>  
<span style="font-size: 18px;">                                           ("keyhookdll"),ThreadId);
		return true;

	}
	else
	{
		UnhookWindowsHookEx(hHook);
		return true;

	}

}</span>

      创建的钩子类型为WH_KEYBOARD,他可以拦截WM_KEYDOWNWM_KEYUP 消息。具体请参考MSDN.

创建钩子函数功能很简单,仅仅安装钩子和设置共享段内的数据。Thread为要安装钩子的线程。主程序在调用时传入0,表示为所有线程安装钩子。

 

   再看钩子函数:

  1. LRESULT CALLBACK KeyHookProc(int nCode ,WPARAM wParam,LPARAM lParam)  
  2. {  
  3.     if(nCode<0||nCode==HC_NOREMOVE)  
  4.     {  
  5.         return CallNextHookEx(hHook,nCode,wParam,lParam);  
  6.     }  
  7.     if(lParam&0x40000000)//只对WM_DOWN进行响应。  
  8.       {  
  9.         PostMessage(hWnd,WM_KEYDOWN,wParam,lParam);  
  10.       }      
LRESULT CALLBACK KeyHookProc(int nCode ,WPARAM wParam,LPARAM lParam)
{
	if(nCode<0||nCode==HC_NOREMOVE)
	{
		return CallNextHookEx(hHook,nCode,wParam,lParam);
	}
	if(lParam&0x40000000)//只对WM_DOWN进行响应。
      {
        PostMessage(hWnd,WM_KEYDOWN,wParam,lParam);
      }	
  1. return CallNextHookEx(hHook,nCode,wParam,lParam);  
        return CallNextHookEx(hHook,nCode,wParam,lParam);

}

   在钩子函数中首先判断nCode的值,当他小于零时应该直调用CallNextHookEx,除此之外它也可以有以下取值:

    ACTION:说明wParam和lParam包含按键消息的信息,可以处理。

    HC_NOREMOVE:说明wParam和lParam包含按键消息的信息,但该消息没有被从消息队列中移除。即程序是调用PeekMessage来查询消息队列内的消息的。

     ( 与GetMessage的区别与联系:他们都从消息队列内查询消息,有消息时将此消息发送出去,GetMessage在消息队列没有消息时会一直等待,直到有消息到达时才返回。而PeekMessage无论消息队列中是否有消息都立即返回。)

     因此当检测到nCode小于0或者为WH_NOREMOVE时不能对消息进行处理而要直接调用CallNextHookEx。lParam的第30位为1时说明此时键被按下,为零时说明键被弹起。此处进行了判断,仅在键被按下时向窗口发送消息。防止消息每次击键发送两次消息。

 

     当某消息到达时我们给主程序窗口发送的消息为用户自定义消息:WM_KEY
     他被定义为#define WM_KEY  WM_USER+1

     在主程序内我们必须自己实现相应此消息的消息处理函数。

     原型为:

  1. afx_msg LRESULT OnKey(WPARAM wParam,LPARAM lParam);  
    afx_msg LRESULT OnKey(WPARAM wParam,LPARAM lParam);

   实现

  1.      char keyname[100];  
  2. ::GetKeyNameText(lParam,keyname,100);//获得按键的键名。  
  3. CString a;  
  4. a.Format("用户按键:%s\r\n",keyname);  
  5. m_output+=a;  
  6. UpdateData(false);  
  7. ::MessageBeep(MB_OK);  
  8. CEdit *edit=(CEdit*)GetDlgItem(IDC_EDIT_OUTPUT);  
  9. edit->LineScroll(edit->GetLineCount());  
  10. return 0;  
      char keyname[100];
	::GetKeyNameText(lParam,keyname,100);//获得按键的键名。
	CString a;
	a.Format("用户按键:%s\r\n",keyname);
	m_output+=a;
	UpdateData(false);
	::MessageBeep(MB_OK);
	CEdit *edit=(CEdit*)GetDlgItem(IDC_EDIT_OUTPUT);
	edit->LineScroll(edit->GetLineCount());
	return 0;

    到此为止各主要函数都介绍完毕,剩下都是如何创建dll。此处不再介绍。例子程序2011年12月2日下午实现。

 

总结:以上程序花了近三个小时实现,此程序看似容易但一旦自己动手实现各种问题接踵而至。所以以后要经常动手实现一些看似容易的程序,不要眼高手低。打这些字的时候键盘监控程序仍在工作,显示着我按下的每一个键。有明显的电脑感觉速度比平常慢了不少,看来使用钩子,尤其是系统范围内的钩子会导致很大的overhead。

    windows核心编程中谈到注入dll的几种方式。其中介绍了使用windows钩子,但是介绍的很简单。以上内容参考自《windows核心编程》第五版,第四部分和《windows程序设计》第二版,王艳平著。如有错误,请指正。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
二、API Hook的原理 这里的API既包括传统的Win32 APIs,也包括任何Module输出的函数调用。熟悉PE文件格 式的朋友都知道,PE文件将对外部Module输出函数的调用信息保存在输入表中,即.idata段。 下面首先介绍本段的结构。 输入表首先以一个IMAGE_IMPORT_DESCRIPTOR(简称IID)数组开始。每个被PE文件隐式链接 进来的DLL都有一个IID.在这个数组中的最后一个单元是NULL,可以由此计算出该数组的项数。 例如,某个PE文件从两个DLL中引入函数,就存在两个IID结构来描述这些DLL文件,并在两个 IID结构的最后由一个内容全为0的IID结构作为结束。几个结构定义如下: IMAGE_IMPORT_DESCRIPTOR struct union{ DWORD Characteristics; ;00h DWORD OriginalFirstThunk; }; TimeDateStamp DWORD ;04h ForwarderChain DWORD ;08h Name DWORD ;0Ch FirstThunk DWORD ;10h IMAGE_IMPROT_DESCRIPTOR ends typedef struct _IMAGE_THUNK_DATA{ union{ PBYTE ForwarderString; PDWORD Functions; DWORD Ordinal; PIMAGE_IMPORT_BY_NAME AddressOfData; }u1; } IMAGE_IMPORT_BY_NAME结构保存一个输入函数的相关信息: IMAGE_IMPORT_BY_NAME struct Hint WORD ? ;本函数在其所驻留DLL的输出表中的序号 Name BYTE ? ;输入函数的函数名,以NULL结尾的ASCII字符串 IMAGE_IMPORT_BY_NAME ends OriginalFirstThunk(Characteristics):这是一个IMAGE_THUNK_DATA数组的RVA(相对于PE文件 起始处)。其中每个指针都指向IMAGE_IMPORT_BY_NAME结构。 TimeDateStamp:一个32位的时间标志,可以忽略。 ForwarderChain:正向链接索引,一般为0。当程序引用一个DLL中的API,而这个API又引用别的 DLL的API时使用。 NameLL名字的指针。是个以00结尾的ASCII字符的RVA地址,如"KERNEL32.DLL"。 FirstThunk:通常也是一个IMAGE_THUNK_DATA数组的RVA。如果不是一个指针,它就是该功能在 DLL中的序号。 OriginalFirstThunk与FirstThunk指向两个本质相同的数组IMAGE_THUNK_DATA,但名称不同, 分别是输入名称表(Import Name Table,INT)和输入地址表(Import Address Table,IAT)。 IMAGE_THUNK_DATA结构是个双字,在不同时刻有不同的含义,当双字最高位为1时,表示函数以 序号输入,低位就是函数序号。当双字最高位为0时,表示函数以字符串类型的函数名 方式输入,这时它是指向IMAGE_IMPORT_BY_NAME结构的RVA。 三个结构关系如下图: IMAGE_IMPORT_DESCRIPTOR INT IMAGE_IMPORT_BY_NAME IAT -------------------- /-->---------------- ---------- ---------------- |01| 函数1 ||02| 函数2 || n| ... |"USER32.dll" | |--------------------| | | FirstThunk |---------------------------------------------------------------/ -------------------- 在PE文件中对DLL输出函数的调用,主要以这种形式出现: call dword ptr[xxxxxxxx] 或 jmp [xxxxxxxx] 其中地址xxxxxxxx就是IAT中一个IMAGE_THUNK_DATA结构的地址,[xxxxxxxx]取值为IMAGE_THUNK_DATA 的值,即IMAGE_IMPORT_BY_NAME的地址。在操作系统加载PE文件的过程中,通过IID中的Name加载相应 的DLL,然后根据INT或IAT所指向的IMAGE_IMPORT_BY_NAME中的输入函数信息,在DLL中确定函数地址, 然后将函数地址写到IAT中,此时IAT将不再指向IMAGE_IMPORT_BY_NAME数组。这样[xxxxxxxx]取到的 就是真正的API地址。 从以上分析可以看出,要拦截API的调用,可以通过改写IAT来实现,将自己函数的地址写到IAT中, 达到拦截目的。 另外一种方法的原理更简单,也更直接。我们不是要拦截吗,先在内存中定位要拦截的API的地址, 然后改写代码的前几个字节为 jmp xxxxxxxx,其中xxxxxxxx为我们的API的地址。这样对欲拦截API的 调用实际上就跳转到了咱们的API调用去了,完成了拦截。不拦截时,再改写回来就是了。 这都是自己从网上辛辛苦苦找来的,真的很好啊

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值