我们知道Windows中的窗口程序是基于消息,由事件驱动的,在某些情况下可能需要捕获或者修改消息,从而完成一些特殊的功能(MFC框架就利用Windows钩子对消息进行引导)。对于捕获消息而言,无法使用IAT或Inline Hook之类的方式去进行捕获,这就要用到接下来要介绍的Windows提供的专门用于处理消息的钩子函数。
Windows下的应用程序大部分都是基于消息机制的,它们都会有一个消息过程函数,根据不同的消息完成不同的功能。Windows操作系统提供的钩子机制的作用就是用来截获和监视这些系统中的消息。Windows钩子琳琅满目,可以用来应对各种不同的消息。
按照钩子作用的范围不同,又可以分为局部钩子和全局钩子。局部钩子是针对某个线程的;而全局钩子则是作用于整个系统中基于消息的应用。全局钩子需要使用DLL文件,在DLL中实现相应的钩子函数。在操作系统中安装全局钩子后,只要进程接收到可以发出钩子的消息,全局钩子的DLL文件就会被操作系统自动或强行地加载到该进程中。因此,设置消息钩子,也可以达到DLL注入的目的。
HHOOK SetWindowsHookEx(
int idHook,
HOOKPROC lpfn,
HINSTANCE hMod,
DWORD dwThreadId
);
该函数的返回值是一个钩子句柄。参数介绍如下:
lpfn:指定Hook函数的地址。如果dwThreadId参数被赋值为0,或者被设置为一个其他进程中的线程ID,那么lpfn属于DLL中的函数过程。如果dwThreadId为当前进程中的一个线程ID,那么lpfn可以使指向当前进程模块中的函数,当然,也可以使DLL模块中的函数。
hMod:该参数指定钩子函数所在模块的模块句柄。即lpfn所在的模块句柄。如果dwThreadId为当前进程中的线程ID,且lpfn所指函数在当前进程中,则该参数被设置为NULL。
dwThreadId:指定需要被挂钩的线程ID号。如果设置为0,表示在所有基于消息的线程中挂钩;如果设置了具体的线程ID,表示在指定线程中挂钩。该参数影响上面两个参数的取值,同时也决定了该钩子是全局钩子还是局部钩子。
idHook:该参数表示钩子的类型。常用的几种如下:
※ WH_GETMESSAGE
按照该钩子的作用是监视被投递到消息队列中的消息。也就是当调用GetMessage或PeekMessage函数时,函数从程序的消息队列中获取一个消息后调用该钩子。
WH_GETMESSAGEG的钩子函数如下:
LRESULT CALLBACK GetMsgProc(
int code, //hook code
WPARAM wParam, //removal option
LPARAM lParam //message
);
※ WH_MOUSE
该钩子用于监视鼠标消息。钩子函数如下:
LRESULT CALLBACK MouseProc(
int nCode, //hook code
WPARAM wParam, //message identifier
LPARAM lParam //mouse coordinates
);
※ WH_KEYBOARD
该钩子用于监视键盘消息。钩子函数如下:
LRESULT CALLBACK KeyboardProc(
int code, //hook code
WPARAM wParam, //virtual-key code
LPARAM lParam //keystroke-message information
);
※ WH_DEBUG
用于调试其它钩子。钩子函数如下:
LRESULT CALLBACK DebugProc(
int nCode, //hook code
WPARAM wParam, //hook type
LPARAM lParam //debugging information
);
对于以上钩子函数的详情还请各位看客老爷们自行挪步到MSDN了。
移除先前用SetWindowsHookEx安装的钩子:
BOOL UnhookWindowsHookEx(
HHOOK hhk
);
唯一的参数是待移除的钩子句柄。
在实际应用中,可以多次调用SetWindowsHookEx函数来安装钩子,而且可以安装多个同样类型的钩子。这样,钩子就会形成一条钩子链,最后安装的钩子会首先截获到消息。当该钩子对消息处理完毕后,可以选择返回或者把消息继续传递下去。如果是为了屏蔽某消息,可以在安装的钩子函数中直接返回非零值。如果希望我们的钩子函数处理完消息后可以继续传递给目标窗口,则必须选择将消息继续传递。继续传递消息的函数定义如下:
LRESULT CallNextHookEx(
HHOOK hhk, //handle to current hook
int nCode, //hook code passed to hook procedure
WPARAM wParam, //value passed to hook procedure
LPARAM lParam //value passed to hook procedure
);
第一个参数是钩子句柄,就是调用SetWindowsHookEx函数的返回值;后面3个参数是钩子的参数,直接一次copy即可。例如:
HHOOK g_Hook = SetWindowsHookEx(…);
LRESULT CALLBACK GetMsgProc(
int code, //hook code
WPARAM wParam, //removal option
LPARAM lParam //message
)
{
return CallNextHookEx(g_Hook, code, wParam, lParam);
}
Windows钩子的使用场景比较广泛,我们就几种比较常见的情况做一个应用示例。
先新建一个DLL程序(这个不会可以看我以前的博客,这里就不重复了),我们在头文件中增加两个导出函数和两个全局。
#define MY_API __declspec(dllexport)
extern "C" MY_API VOID SetHookOn();
extern "C" MY_API VOID SetHookOff();
HHOOK g_Hook = NULL; //钩子句柄
HINSTANCE g_Inst = NULL; //DLL模块句柄
在DllMain中保存该DLL模块的句柄,以方便安装全局钩子。
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
//保存DLL模块句柄
g_Inst = (HINSTANCE)hModule;
return TRUE;
}
安装与卸载钩子的函数如下:
VOID SetHookOn()
{
//安装钩子
g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_Inst, 0);
}
VOID SetHookOff()
{
//卸载钩子
UnhookWindowsHookEx(g_Hook);
}
钩子函数的实现如下:
//钩子函数
LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
if(code < 0)
{
//如果code小于0,必须调用CallNextHookEx传递消息,不处理该消息,并返回CallNextHookEx的返回值。
return CallNextHookEx(g_Hook, code, wParam, lParam);
}
if(code == HC_ACTION && lParam > 0)
{
//code等于HC_ACTION,表示消息中包含按键消息
//如果为WM_KEYDOWN,则显示按键对应的文本
char szBuf[MAXBYTE] = {0};
GetKeyNameText(lParam, szBuf, MAXBYTE);
MessageBox(NULL, szBuf, "提示", MB_OK);
}
return CallNextHookEx(g_Hook, code, wParam, lParam);
}
编译链接后产生我们需要的.dll和.lib文件,然后新建一个项目来导入动态库内容调用相关函数。
新建项目如下:
首先导入库:
#pragma comment (lib, "全局钩子.lib")
声明将要调用的函数(不声明链接时将报错):
extern "C" VOID SetHookOn();
extern "C" VOID SetHookOff();
在按钮事件中调用导出函数:
void CHookDebugDlg::OnHookon()
{
SetHookOn();
}
void CHookDebugDlg::OnHookoff()
{
SetHookOff();
}
执行结果如下:
数据防泄漏软件通常会精致PrintScreen键,防止通过截屏将数据保存为图片而导致数据泄密。下面我们也可以模仿一下,简单的实现该功能。这里需要注意的是,普通的键盘钩子(WH_KEYBOARD)是无法过滤一些系统按键的,得通过安装低级键盘钩子(WH_KEYBOARD_LL)来达到目的。
在低级键盘钩子的回调函数中,判断是否为PrintScreen键,如果是,则直接返回TRUE;如果不是,则传递给下一个钩子处理。
具体DLL中的实现代码如下:
BOOL SetHookOn()
{
if(g_Hook != NULL)
{
return FALSE;
}
//安装钩子
g_Hook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, g_Inst, 0);
if(NULL == g_Hook)
{
MessageBox(NULL, "安装钩子出错!", "ERROR", MB_ICONSTOP);
return FALSE;
}
return TRUE;
}
BOOL SetHookOff()
{
if(g_Hook == NULL)
{
return FALSE;
}
//卸载钩子
UnhookWindowsHookEx(g_Hook);
g_Hook = NULL;
return TRUE;
}
//钩子函数
LRESULT CALLBACK LowLevelKeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
KBDLLHOOKSTRUCT *Key_Info = (KBDLLHOOKSTRUCT *)lParam;
if(HC_ACTION == code)
{
if(WM_KEYDOWN == wParam || WM_SYSKEYDOWN == wParam)
{
if(Key_Info->vkCode == VK_SNAPSHOT)
{
MessageBox(NULL, "该键已禁用!", "ERROR", MB_ICONSTOP);
return TRUE;
}
}
}
return CallNextHookEx(g_Hook, code, wParam, lParam);
}
依然利用前面的小程序,执行后按下PrintScreen键,效果如下:
可能在编译时会报错,说WH_KEYBOARD_LL和KBDLLHOOKSTRUCT未定义,此时可以在文件开头加上如下代码:
#define WH_KEYBOARD_LL 13
typedef struct tagKBDLLHOOKSTRUCT {
DWORD vkCode;
DWORD scanCode;
DWORD flags;
DWORD time;
DWORD dwExtraInfo;
} KBDLLHOOKSTRUCT, FAR *LPKBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;
其实在winuser.h中已有定义,但是可能是兼容的缘故用不了。
利用WH_GETMESSAGE钩子,可以方便地将DLL文件注入到所有基于消息机制的程序中。因为有时候可能需要DLL文件完成一些工作,但是工作时需要DLL在目标进程的空间中。这个时候,就可以将DLL注入目标进程来完成相关的功能。
主要的代码如下:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
//保存DLL模块句柄
g_Inst = (HINSTANCE)hModule;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
DoSomething();
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
VOID SetHookOn()
{
g_Hook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_Inst, 0);
}
VOID SetHookOff()
{
UnhookWindowsHookEx(g_Hook);
}
LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(g_Hook, code, wParam, lParam);
}
VOID DoSomething()
{
MessageBox(NULL, "Hello,我被执行了!", "提示", MB_OK);
}
执行效果图:
需要注意的是,此处执行的DoSomething并不是导出函数哦。