全局钩子注入介绍背景
Windows下的应用程序大部分都是基于消息机制的,它们都会有一个消息过程函数,根据不同的消息完成不同的功能。Windows操作系统提供的钩子机制就是用来截获和监视这些系统中的消息。
按照钩子作用的范围不同,又可以分为局部钩子和全局钩子。局部钩子是针对某个线程的;而全局钩子则是作用于整个系统中基于消息的应用。全局钩子需要使用DLL文件,在DLL中实现相应的钩子函数。
接下来,本文将介绍利用全局钩子来进行DLL注入。
主要步骤
1.在DLL中设置钩子回调函数、设置全局钩子和卸载钩子三个模块
(1)设置全局钩子
既然叫全局钩子那么就要作用于整个系统的基于消息的应用,我们知道进程的地址空间都是相互独立的,之间互不影响,那么发生对应事件的进程不能调用其他进程地址空间的钩子函数,所以钩子函数必须在一个DLL中,这样当成功设置全局钩子之后,只有进程产生设置对应的事件的时候系统才会将指定的DLL模块加载到该进程中,实现DLL注入。
为了能够让DLL注入到所有的进程中,程序设置WH_GETMESSAGE消息的全局钩子,这是因为所有进程都会有一个消息队列,当与系统有消息交互的时候,都会加载WH_GETMESSAGE类型的全局钩子DLL。
将程序定义的钩子函数安装到挂钩链中需要用到SetWindowsHookEx函数
函数声明
HHOOK SetWindowsHookExA(
int idHook,
HOOKPROC lpfn,
HINSTANCE hmod,
DWORD dwThreadId
);
idHook [in]
要安装的钩子程序的类型。 该参数可以是以下值之一。
VALUE | MEANING |
---|---|
WH_CALLWNDPROC | 安装钩子程序,在系统将消息发送到目标窗口过程之前监视消息。 |
WH_CALLWNDPROCRET | 安装钩子程序,在目标窗口过程处理消息后监视消息。 |
WH_CBT | 安装接收对CBT应用程序有用的通知的挂钩程序。 |
WH_DEBUG | 安装可用于调试其他挂钩程序的挂钩程序。 |
WH_FOREGROUNDIDLE | 安装将在应用程序的前台线程即将变为空闲时调用的钩子过程。该挂钩对于在空闲时执行低优先级任务很有用。 |
WH_GETMESSAGE | 安装一个监视发送到消息队列的消息的挂钩过程。 |
WH_JOURNALPLAYBACK | 安装一个挂钩过程,用于发布先前由WH_JOURNALRECORD挂钩过程记录的消息。 |
WH_JOURNALRECORD | 安装一个挂钩过程,记录发布到系统消息队列的输入消息。这个钩子对于录制宏很有用。 |
WH_KEYBOARD | 安装监视按键消息的挂钩过程。 |
WH_KEYBOARD_LL | 安装监视低级别键盘输入事件的挂钩过程。 |
WH_MOUSE | 安装监视鼠标消息的挂钩过程。 |
WH_MOUSE_LL | 安装监视低级别鼠标输入事件的挂钩过程。 |
WH_MSGFILTER | 安装钩子程序,用于监视在对话框,消息框,菜单或滚动条中由于输入事件而生成的消息。 |
WH_SHELL | 安装接收对shell应用程序有用的通知的挂钩程序。 |
WH_SYSMSGFILTER | 安装钩子程序,用于监视在对话框,消息框,菜单或滚动条中由于输入事件而生成的消息。钩子程序监视与调用线程相同的桌面中的所有应用程序的这些消息。 |
设置WH_GETMESSAGE全局钩子具体实现的代码如下:
//设置全局钩子
BOOL SetGlobalHook()
{
g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0);
if (NULL == g_hHook)
return FALSE;
return FALSE;
}
第一个参数设置为WH_GETMESSAGE,它可以监视发送到消息队列的消息,第二个参数表示钩子回调函数,第三个参数表示包含钩子回调函数的DLL模块句柄,即我们需要DLL注入的那个DLL;第四个参数直接设为0表示为全局钩子关联所有线程,返回值是钩子句柄HHOOK类型。
(2)设置回调函数
回调函数即是钩到进程时需要注入的函数,当成功设置全局钩子后,只有进程有消息发送到消息队列中时,系统才会自动将指定的DLL模块加载到进程中实现DLL注入。
// 钩子回调函数
LRESULT GetMsgProc(
int code,
WPARAM wParam,
LPARAM lParam)
{
return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}
上述回调函数中,参数和返回值的数据类型是固定的。其中,CallNextHookEx函数表示将当前钩子传递给钩子链中的下一个钩子,第一个参数要指定当前钩子的句柄。如果直接返回0,则表示中断钩子的传递,对钩子进行拦截。
注意一定要用CallNextHookEx函数让钩子传递,不然在一个钩子响应之后在钩子链中位于它之前的钩子们无法相应,这样就做不到全局注入。
当钩子不再使用,可以对钩子进行卸载操作。卸载全局钩子,那么已经加载包含钩子回调函数的DLL模块的进程,将会释放DLL模块。那么,卸载全局钩子的具体实现代码如下所示。
(3)卸载全局钩子
// 卸载钩子
BOOL UnsetGlobalHook()
{
if (g_hHook)
{
::UnhookWindowsHookEx(g_hHook);
}
return TRUE;
}
上述代码中,UnsetGlobalHook函数用来卸载指定钩子,参数便是卸载钩子的句柄。卸载成功后,所有加载了全局钩子DLL模块的进程,都会释放该DLL模块。
上述介绍了全局钩子的设置、钩子回调函数的实现以及全局钩子的卸载,这些操作都需要用到全局钩子的句柄作为参数。而全局钩子是以DLL的形式加载到其他进程空间中,而且进程都是独立的,任意修改其中一个内存里的数据是不影响另一个进程的。那么,如何将钩子句柄传递给其他进程呢?为了解决这个问题,本文采用的方法是在DLL中创建共享内存。
共享内存,则是突破进程独立性,多个进程共享同一段内存。在DLL中创建共享内存,就是在DLL中创建一个变量,然后DLL被加载到多个进程空间,只要其中一个进程修改了该变量的值,其他进程DLL中的这个值也会改变,就相当于多个进程共享一块内存。
共享实现起来内存的原理比较简单,首先为DLL创建一个数据段,然后再对程序的链接器进行设置,把指定的数据段链接为共享数据段。这样,就可以成功创建共享内存了。具体实现代码如下所示。
// 共享内存
#pragma data_seg("mydata")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS")
在上面的代码中,使用#pragma data_seg创建了一个名为“mydata”的数据段,然后使用/section:mydata ,RWS把mydata数据段设置为可读、可写、可共享的共享数据段。
2.创建进程控制DLL注入
#include<Windows.h>
int main()
{
typedef BOOL(*typedef_SetGlobalHook)();
typedef BOOL(*typedef_UnsetGlobalHook)();
HMODULE hDll = NULL;
typedef_SetGlobalHook SetGlobalHook = NULL;
typedef_UnsetGlobalHook UnsetGlobalHook = NULL;
BOOL bRet = FALSE;
do
{
hDll = ::LoadLibrary("Global_Dll.dll");
if (NULL == hDll)
{
printf("LoadLibrary Error[%d]\n", ::GetLastError());
break;
}
SetGlobalHook = (typedef_SetGlobalHook)::GetProcAddress(hDll, "SetGlobalHook");
if (NULL == SetGlobalHook)
{
printf("GetProcAddress Error[%d]\n", ::GetLastError());
break;
}
bRet = SetGlobalHook();
if (bRet)
{
printf("SetGlobalHook OK.\n");
}
else
{
printf("SetGlobalHook ERROR.\n");
}
system("pause");
UnsetGlobalHook = (typedef_UnsetGlobalHook)::GetProcAddress(hDll, "UnsetGlobalHook");
if (NULL == UnsetGlobalHook)
{
printf("GetProcAddress Error[%d]\n", ::GetLastError());
break;
}
UnsetGlobalHook();
printf("UnsetGlobalHook OK.\n");
} while (FALSE);
system("pause");
return 0;
}
测试
本文创建一个名为GlobalHook_Test的DLL工程项目,将上述的设置钩子、钩子回调函数以及卸载钩子的代码均编写在同一DLL中,并对设置钩子以及卸载钩子部分的函数进行导出,方便外部程序的调用。然后,创建名为Test的控制台项目,对GlobalHook_Test.dll进行加载调用,测试全局钩子注入。测试流程如下所示。
首先,运行Test.exe程序加载GlobalHook_Test.dll并调用导出函数设置全局钩子,提示钩子创建成功后,使用进程查看器ProcessExplorer.exe查看进程520.exe进程的加载模块,发现加载了GlobalHook_Test.dll模块,说明DLL注入成功。
继续执行Test.exe程序卸载全局钩子,提示钩子卸载成功后,再使用进程查看器ProcessExplorer.exe查看进程520.exe进程的加载模块,发现GlobalHook_Test.dll模块已经不存在了,说明DLL释放成功。
小结
主要通过调用SetWindowsHookEx函数设置全局钩子,完成DLL注入。通过调用CallNextHookEx函数传递钩子,让进程继续运行。通过调用UnhookWindowsHookEx函数卸载钩子,实现DLL释放。
在调用SetWindowsHookEx函数设置全局钩子的时候,一定要将钩子回调函数编写在DLL模块当中,并指定该DLL模块的句柄。
通过在DLL中通过#pragma data_seg()指令创建共享内存,那么,加载了该DLL的进程,共享一块内存,只要其中一个进程修改了内存区域的数据,其他进程对应内存区域的数据也会改变。