网络安全编程:Windows钩子函数

安全
Windows下的窗口应用程序是基于消息驱动的,但是在某些情况下需要捕获或者修改消息,从而完成一些特殊的功能。

[[401215]]

 Windows下的窗口应用程序是基于消息驱动的,但是在某些情况下需要捕获或者修改消息,从而完成一些特殊的功能。对于捕获消息而言,无法使用IAT或Inline Hook之类的方式去进行捕获,不过Windows提供了专门用于处理消息的钩子函数。

01 钩子原理

Windows下的应用程序大部分是基于消息模式机制的,一些CUI的程序不是基于消息的。Windows下的应用程序都有一个消息过程函数,根据不同的消息来完成不同的功能。Windows操作系统提供的钩子机制的作用是用来截获、监视系统中的消息。Windows操作系统提供了很多不同种类的钩子,可以处理不同的消息。

Windows系统提供的钩子按照挂钩范围分为局部钩子和全局钩子。局部钩子是针对一个线程的,而全局钩子则是针对整个操作系统内基于消息机制的应用程序的。全局钩子需要使用DLL文件,DLL文件里存放了钩子函数的代码。

在操作系统中安装全局钩子以后,只要进程接收到可以发出钩子的消息后,全局钩子的DLL文件会被操作系统自动或强行地加载到该进程中。由此可见,设置消息钩子也是一种可以进行DLL注入的方法。

02 钩子函数

钩子函数主要有3个,分别是SetWindowsHookEx()、CallNextHookEx()和UnhookWindowsHookEx()。下面介绍这些函数的使用方法。

SetWindowsHookEx()函数的定义如下: 

  1. HHOOK SetWindowsHookEx(  
  2.  int idHook,  
  3.  HOOKPROC lpfn,  
  4.  HINSTANCE hMod,  
  5.  DWORD dwThreadId  
  6. ); 

该函数的返回值为一个钩子句柄。这个函数有4个参数,下面分别进行介绍。

lpfn:该参数指定为 Hook 函数的地址。如果 dwThreadId 参数被赋值为 0,或者被设置为一个其他进程中的线程 ID,那么 lpfn 则属于 DLL 中的函数过程。如果 dwThreadId 为当前进程中的线程 ID,那么 lpfn 可以是指向当前进程中的函数过程,也可以是属于 DLL 中的函数过程。

hMod:该参数指定钩子函数所在模块的模块句柄。该模块句柄就是 lpfn 所在的模块的句柄。如果 dwThreadId 为当前进程中的线程 ID,而且 lpfn 所指向的函数在当前进程中,那么 hMod 将被设置为 NULL。

dwThreadId:该参数设置为需要被挂钩的线程的 ID 号。如果设置为 0,表示在所有的线程中挂钩(这里的“所有的线程”表示基于消息机制的所有的线程)。如果指定为具体的线程的 ID 号,表示要在指定的线程中进行挂钩。该参数影响上面两个参数的取值。该参数的取值决定了该钩子属于全局钩子,还是局部钩子。

idHook:该参数表示钩子的类型。由于钩子的类型非常多,因此放在所有的参数后面进行介绍。下面介绍几个常用到的钩子,也可能是大家比较关心的几个类型。

1. WH_GETMESSAGE

安装该钩子的作用是监视被投递到消息队列中的消息。也就是当调用GetMessage()或PeekMessage()函数时,函数从程序的消息队列中获取一个消息后调用该钩子。

WH_GETMESSAGE钩子函数的定义如下: 

  1. LRESULT CALLBACK GetMsgProc(  
  2.  int code, // 钩子编码  
  3.  WPARAM wParam, // 移除选项  
  4.  LPARAM lParam // 消息  
  5. ); 

2. WH_MOUSE

安装该钩子的作用是监视鼠标消息。该钩子函数的定义如下: 

  1. LRESULT CALLBACK MouseProc(  
  2.  int nCode, // 钩子编码  
  3.  WPARAM wParam, // 消息标识符  
  4.  LPARAM lParam // 鼠标的坐标  
  5. ); 

3. WH_KEYBOARD

安装该钩子的作用是监视键盘消息。该钩子函数的定义如下: 

  1. LRESULT CALLBACK KeyboardProc(  
  2.  int code, // 钩子编码  
  3.  WPARAM wParam, // 虚拟键编码  
  4.  LPARAM lParam // 按键信息  
  5. ); 

4. WH_DEBUG

安装该钩子的作用是调试其他钩子的钩子函数。该钩子函数的定义如下: 

  1. LRESULT CALLBACK DebugProc(  
  2.  int nCode, // 钩子编码  
  3.  WPARAM wParam, // 钩子类型  
  4.  LPARAM lParam // 调试信息  
  5. ); 

从上面的这些钩子函数定义可以看出,每个钩子函数的定义都是一样的。每种类型的钩子监视、截获的消息不同。虽然它们的定义都是相同的,但是函数参数的意义是不同的。

接着介绍跟钩子有关的另外一个函数:UnhookWindowsHookEx(),其定义如下: 

  1. BOOL UnhookWindowsHookEx(  
  2.  HHOOK hhk // 钩子函数的句柄  
  3. ); 

这个函数是用来移除先前用SetWindowsHookEx()安装的钩子。该函数只有一个参数,是钩子句柄,也就是调用该函数通过指定的钩子句柄来移除与其相应的钩子。

在操作系统中,可以多次反复地使用SetWindowsHookEx()函数来安装钩子,而且可以安装多个同样类型的钩子。这样,钩子就会形成一条钩子链,最后安装的钩子会首先截获到消息。当该钩子对消息处理完毕以后,会选择返回,或者选择把消息继续传递下去。在通常情况下,如果为了屏蔽消息,则直接在钩子函数中返回一个非零值。比如要在自己程序中屏蔽鼠标消息,则在安装的鼠标钩子函数中直接返回非零值即可。如果为了消息在经过钩子函数后可以继续传达到目标窗口,必须选择将消息继续传递。使消息能继续传递的函数的定义如下: 

  1. LRESULT CallNextHookEx(  
  2.  HHOOK hhk, // 当前钩子的句柄  
  3.  int nCode, // 传递给钓子函数的钓子编码  
  4.  WPARAM wParam, // 传递给钓子函数的值  
  5.  LPARAM lParam // 传递给钓子函数的值  
  6. ); 

该函数有4个参数。第一个参数是钩子句柄,就是调用SetWindowsHookEx()函数的返回值;后面3个参数是钩子函数的参数,直接依次抄过来即可。例如: 

  1. HHOOK g_Hook = SetWindowsHook(……);  
  2. LRESULT CALLBACK GetMsgProc(  
  3.  int code, // 钩子编码  
  4.  WPARAM wParam, // 移除选项  
  5.  LPARAM lParam // 消息  
  6.  
  7.  
  8.  return CallNextHookEx(g_Hook, code, wParam, lParam); 

03 钩子实例

Windows钩子的应用比较广。无论是安全产品还是恶意软件,甚至是常规软件,都会用到Windows提供的钩子功能。

1. 全局键盘钩子

下面来写一个可以截获键盘消息的钩子程序,其功能非常简单,就是把按下的键对应的字符显示出来。既然要截获键盘消息,那么肯定是截获系统范围内的键盘消息,因此需要安装全局钩子,这样就需要DLL文件的支持。先来新建一个DLL文件,在该DLL文件中需要定义两个导出函数和两个全局变量,定义如下: 

  1. extern "C" __declspec(dllexport) VOID SetHookOn();  
  2. extern "C" __declspec(dllexport) VOID SetHookOff();  
  3. // 钩子句柄  
  4. HHOOK g_Hook = NULL 
  5. // DLL 模块句柄  
  6. HINSTANCE g_Inst = NULL

在DllMain()函数中,需要保存该DLL模块的句柄,以方便安装全局钩子。代码如下: 

  1. BOOL APIENTRY DllMain( HANDLE hModule,  
  2.  DWORD ul_reason_for_call, 
  3.  LPVOID lpReserved  
  4.  )  
  5.  
  6.  // 保存 DLL 的模块句柄  
  7.  g_Inst = (HINSTANCE)hModule;  
  8.  return TRUE;  

安装与卸载钩子的函数如下: 

  1. VOID SetHookOn()  
  2.  
  3.  // 安装钩子  
  4.  g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_Inst, 0);  
  5. VOID SetHookOff()  
  6.  
  7.  // 卸载钩子  
  8.  UnhookWindowsHookEx(g_Hook);  

对于Windows钩子来说,上面的这些步骤基本上都是必需的,或者是差别不大,关键在于钩子函数的实现。这里是为了获取键盘按下的键,钩子函数如下: 

  1. // 钩子函数  
  2. LRESULT CALLBACK KeyboardProc(  
  3.   int code, // 钩子编码  
  4.   WPARAM wParam, // 虚拟键编码  
  5.   LPARAM lParam // 按建信息  
  6.  
  7.  
  8.   if ( code < 0 )  
  9.   {  
  10.     return CallNextHookEx(g_Hook, code, wParam, lParam);  
  11.   }  
  12.   if ( code == HC_ACTION && lParam > 0 )  
  13.   {  
  14.     char szBuf[MAXBYTE] = { 0 };  
  15.     GetKeyNameText(lParam, szBuf, MAXBYTE);  
  16.     MessageBox(NULL, szBuf, NULL, MB_OK);  
  17.   } 
  18.   return CallNextHookEx(g_Hook, code, wParam, lParam);  

关于钩子函数,这里简单地解释一下,首先是进入钩子函数的第一个判断。 

  1. if ( code < 0 )  
  2.  
  3.  return CallNextHookEx(g_Hook, code, wParam, lParam);  

如果code的值小于0,则必须调用CallNextHookEx(),将消息继续传递下去,不对该消息进行处理,并返回CallNextHookEx()函数的返回值。这一点是MSDN上要求这么做的。 

  1. if ( code == HC_ACTION && lParam > 0 )  
  2.  
  3.  char szBuf[MAXBYTE] = { 0 };  
  4.  GetKeyNameText(lParam, szBuf, MAXBYTE); 
  5.   MessageBox(NULL, szBuf, NULL, MB_OK); 

如果code等于HC_ACTION,表示消息中包含按键消息;如果为WM_KEYDOWN,则显示按键对应的文本。

将该DLL文件编译连接。为了测试该DLL文件,新建一个MFC的Dialog工程,添加两个按钮,如图1所示。

图1  键盘钩子的测试程序

分别对这两个按钮添加代码,具体如下: 

  1. void CHookTestDlg::OnButton1()  
  2.  
  3.  // 在这里添加控制通知的处理程序  
  4.  SetHookOn();  
  5.  
  6. void CHookTestDlg::OnButton2()  
  7.  
  8.  // 在这里添加控制通知的处理程序  
  9.  SetHookOff();  

直接调用DLL文件导出这两个函数,不过在使用之前要先对这两个函数进行声明,否则编译器因无法找到这两个函数的原型而导致连接失败。定义如下: 

  1. extern "C" VOID SetHookOn();  
  2. extern "C" VOID SetHookOff(); 

进行编译连接,提示出错,内容如下: 

  1. Linking...  
  2. HookTestDlg.obj : error LNK2001: unresolved external symbol _SetHookOn  
  3. HookTestDlg.obj : error LNK2001: unresolved external symbol _SetHookOff  
  4. Debug/HookTest.exe : fatal error LNK1120: 2 unresolved externals  
  5. Error executing link.exe.  
  6. HookTest.exe - 3 error(s), 0 warning(s) 

从给出的提示可以看出是连接错误,找不到外部的符号。将DLL编译连接后生成的DLL文件和LIB文件都复制到测试工程的目录下,并将LIB文件添加到工程中。在代码中添加如下语句: 

  1. #pragma comment (lib, KeyBoradHookTest) 

再次连接,成功!

运行测试程序,并单击“HookOn”按钮,随便按下键盘上的任意一个键,会出现提示对话框,如图2所示。

图2  截获到的键盘输入

从图2中可以看出,当按下键盘上的按键时,程序将捕获到按键。到此,键盘钩子的例子程序就完成了。

2. 低级键盘钩子

数据防泄露软件通常会禁止PrintScreen键,防止通过截屏将数据保存为图片而导致泄密。这类软件想要实现是比较简单的,但是想要将功能做得强大些,还是需要下功夫的。数据防泄露的软件除了要有兼容性好的底层驱动的设计,还要有完善的规则设置。此外就是需要软件安全人员对其进行各种各样的攻击,避免数据防泄露软件因为各种各样的原因而被心怀恶意者突破。

这里介绍如何禁止PrintScreen键。其实很简单,只要安装低级键盘钩子(WH_KEYBO ARD_LL)就可以搞定。普通的键盘钩子(WH_KEYBOARD)是无法过滤一些系统按键的。在低级键盘钩子的回调函数中,判断是否为PrintScreen键,如果是,则直接返回TRUE(前面提到,如果想屏蔽某个消息的话,那么在钩子函数中对该消息进行处理后,直接返回一个非零值),如果不是,则传递给钩子链的下一处。

代码如下: 

  1. extern "C" __declspec(dllexport) BOOL SetHookOn()  
  2.  
  3.   if ( g_hHook != NULL ) 
  4.   {  
  5.     return FALSE;  
  6.   }  
  7.   g_hHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, g_hIns, NULL);  
  8.   if ( NULL == g_hHook )  
  9.   {  
  10.     MessageBox(NULL, "安装钩子出错 !", "error", MB_ICONSTOP);  
  11.     return FALSE; 
  12.   }  
  13.   return TRUE;  
  14.  
  15. extern "C" __declspec(dllexport) BOOL SetHookOff()  
  16.  
  17.   if ( g_hHook == NULL )  
  18.   {  
  19.     return FALSE;  
  20.   }  
  21.   UnhookWindowsHookEx(g_hHook);  
  22.   g_hHook = NULL 
  23.   return TRUE;  
  24.  
  25. LRESULT CALLBACK LowLevelKeyboardProc(  
  26.   int nCode, WPARAM wParam, LPARAM lParam)  
  27.  
  28.   KBDLLHOOKSTRUCT *Key_Info = (KBDLLHOOKSTRUCT*)lParam; 
  29.   if ( HC_ACTION == nCode )  
  30.   {  
  31.     if ( WM_KEYDOWN == wParam || WM_SYSKEYDOWN == wParam )  
  32.     {  
  33.       if ( Key_Info->vkCode == VK_SNAPSHOT )  
  34.       {  
  35.         return TRUE;  
  36.       }  
  37.     }  
  38.   }  
  39.   return CallNextHookEx(g_hHook, nCode, wParam, lParam);  

代码量非常短,然而,就是这短短的代码阻止了数据的泄露。当然,对于一个攻击者来说,这个代码无法保护数据,这种保护也就很脆弱了。任何的保护都有突破的办法,攻击无处不在,攻击者会尝试任何手段突破所有的保护。

3. 使用钩子进行DLL注入

Windows提供的钩子类型非常多,其中一种类型的钩子非常实用,那就是WH_GETME SSAGE钩子。它可以很方便地将DLL文件注入到所有的基于消息机制的程序中。

在有些情况下,需要DLL文件完成一些功能,但是完成功能时需要DLL在目标进程的空间中。这时,就需要使用WH_GETMESSAGE消息把DLL注入到目标的进程中。代码非常简单,这里直接给出DLL文件的代码,具体如下: 

  1. #include <windows.h>  
  2. extern "C" __declspec(dllexport) VOID SetHookOn();  
  3. extern "C" __declspec(dllexport) VOID SetHookOff();  
  4. HHOOK g_HHook = NULL 
  5. HINSTANCE g_hInst = NULL 
  6. VOID DoSomeThing()  
  7.  
  8.   /*  
  9.   ……  
  10.   自己要实现功能的代码  
  11.   ……  
  12.   */  
  13.  
  14. BOOL WINAPI DllMain(  
  15.   HINSTANCE hinstDLL, // DLL 模块的句柄  
  16.   DWORD fdwReason, // 调用函数的原因  
  17.   LPVOID lpvReserved // 保留的  
  18.  
  19.  
  20.   switch ( fdwReason )  
  21.   {  
  22.   case DLL_PROCESS_ATTACH:  
  23.     {  
  24.       g_hInst = hinstDLL 
  25.       DoSomeThing();  
  26.       break;  
  27.     }  
  28.   }  
  29.   return TRUE;  
  30.  
  31. LRESULT CALLBACK GetMsgProc(  
  32.   int code, // 钩子编码  
  33.   WPARAM wParam, // 移除选项  
  34.   LPARAM lParam // 消息  
  35.  
  36.  
  37.   return CallNextHookEx(g_HHook, code, wParam, lParam);  
  38.  
  39. VOID SetHookOn()  
  40.  
  41.   g_HHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_hInst, 0); 
  42.  
  43. VOID SetHookOff()  
  44.  
  45.   UnhookWindowsHookEx(g_HHook);  

整个代码就是这样。只要知道,在需要DLL大范围地注入到基于消息的进程中时,可以使用这种方法。 

 

责任编辑:庞桂玉 来源: 计算机与网络安全
相关推荐

2021-01-18 10:35:18

网络安全Windows代码

2023-06-19 08:23:28

kubernetes容器

2021-03-03 12:20:42

网络安全DLL编程

2021-02-07 10:55:01

网络安全文件API

2021-03-05 13:46:56

网络安全远程线程

2021-01-26 13:45:03

网络安全Winsock编程

2011-05-20 17:59:06

回调函数

2021-05-08 11:50:59

网络安全API函数代码

2021-03-19 10:23:45

网络安全内核文件

2021-04-14 15:53:58

网络安全C语言wcslen

2021-02-21 18:19:43

网络安全网络安全编程创建进程

2021-02-23 10:20:07

网络安全进程代码

2016-10-10 00:18:27

2021-04-06 11:04:54

网络安全C语言代码

2021-02-15 15:23:03

网络安全注册表API

2021-06-18 09:55:09

网络安全目录监控

2011-03-17 13:32:45

2021-03-01 11:20:13

网络安全多线程代码

2021-06-24 08:37:34

网络安全内核代码

2021-06-15 11:16:24

网络安全U盘软件
点赞
收藏

51CTO技术栈公众号