VC++深入详解:钩子函数
什么是HOOK编程?
这得从Windows消息机制说起:当在应用程序窗口内点击鼠标左键时,操作系统会感知这一事件,然后把消息放到应用程序的消息响应队列中,应用程序通过GetMessage读取消息,然后通过DispatchMessage将消息调度给操作系统,操作系统会调用在设计窗口类时指定的应用程序窗口过程函数对消息进行处理。
假如我们希望对某个特殊的消息进行屏蔽,比如希望这个应用程序不响应回车和空格消息,就需要截获所有消息,然后进行判断,如果是这两种消息,则将它们屏蔽掉。为了实现这个功能,我们可以安装一个HOOK过程,在此过程中检查,再决定是否放行该消息。
其中g_hMouse是用来存放钩子过程句柄的全局变量函数,而MouseProc则是与鼠标消息相对应的钩子函数,如果这个函数返回非0值,则表示以对其进行了处理,系统将不会把这消息传递给窗口过程函数了。我们这里只是简单地让其返回1:
我们运行一下程序,发现当鼠标移动到程序上时,鼠标变成了等待状态。无法点击确定或者取消按钮,只能通过按键来退出程序。
下面我们屏蔽键盘的空格键消息:首先设置一个钩子
然后在钩子函数中对于空格键直接返回1
其中VK_SPACE是空格键对应的虚拟按键码。在很久很久以前,各个公司生产的键盘是不一样的,为了能让Windows兼容不同的键盘,微软搞了一个虚拟按键码,使得同一个按键(比如字母A),不论它在各种类型的键盘上的位置如何,操作系统都能通过驱动辨认出来。虚拟按键是以VK开头的,我们也可以通过查看VK_SPACE的定义来看到其他的按键码。如果你想在钩子函数中处理这个消息,那么调用CallNextHookEx作为钩子函数的返回值。
假如我们想屏蔽Alt+F4键,那么可以
如果lParam 的第29位为1,那么表明Alt键按下。我们可以将lParam 右移29位,然后与1相与即可。
前面介绍的钩子函数都只能屏蔽当前进程的主线程上鼠标和键盘消息,我们可以利用全局钩子来屏蔽正在运行的所有线程的鼠标和键盘消息。但这个钩子函数必须在动态链接库中。我们先建一个动态链接库:
并为其指定模块定义文件,将导出函数名设为SetHook,序号为2
然后,我们新建一个MFC对话框工程,在OnInitDialog中调用SetHook()函数(当然,不要忘记对其声明:_declspec(dllimport) void SetHook();并在链接选项中增加对应的.lb)。然后build程序,运行,就会发现所有进程的鼠标消息都被屏蔽了。
下面我们在动态链接库中增加一个钩子函数,屏蔽键盘消息。首先,我们不能屏蔽所有的键盘消息,否则我们将无法退出程序,只能强者重新启动了。我们可以为键盘消息留一个F2键,当按下F2键时,程序将自动退出。程序的退出可以通过发送WM_CLOSE消息来实现,但是发送消息时需要获得当前窗口的句柄,这可如何是好呢?我们可以把窗口的句柄当做参数传递进去,然后在dll中用一个全局变量接收,然后在钩子函数中就能获得这个句柄了:
当运行程序时,在窗口激活时按下F2,程序就能退出,而当在别的窗口下按下F2,却不能退出。这与我们之前对dll的理解有一些出入:之前提到,当dll被多个进程使用时,这些进程可以共享DLL的代码和数据。因此dll中的全局变量g_hWnd应该是被所有进程所共享的。在其他进程下按下F2,也能够退出。可事实上并不是这样,这是为什么呢?这是因为Windows采用了写入时复制机制,当DLL被两个进程共享时,如果第二个进程想要修改其中的数据,那么他将会复制一份新的页面。这样做是为了安全着想,因为假设DLL中的变量时一个指针,第一个调用dll的进程修改了它的话,当第二个进程调用它时,指针可能已经指向了某个其他的地方。
如果非要让全局句柄g_hWnd可以被共享的话,可以将它放到一个共享的节中:
注意,共享的变量必须要初始化次时,用dumpbin -headers查看,可以看到:
这里的属性就必须使用全称了。
这得从Windows消息机制说起:当在应用程序窗口内点击鼠标左键时,操作系统会感知这一事件,然后把消息放到应用程序的消息响应队列中,应用程序通过GetMessage读取消息,然后通过DispatchMessage将消息调度给操作系统,操作系统会调用在设计窗口类时指定的应用程序窗口过程函数对消息进行处理。
假如我们希望对某个特殊的消息进行屏蔽,比如希望这个应用程序不响应回车和空格消息,就需要截获所有消息,然后进行判断,如果是这两种消息,则将它们屏蔽掉。为了实现这个功能,我们可以安装一个HOOK过程,在此过程中检查,再决定是否放行该消息。
我们先看代码。在对话框应用程序的OnInitDialog中设置一个钩子过程:
- g_hMouse = SetWindowsHookEx(WH_MOUSE, //截获鼠标消息
- MouseProc, //钩子函数
- NULL, //指定的线程
- GetCurrentThreadId()) //当前线程
其中g_hMouse是用来存放钩子过程句柄的全局变量函数,而MouseProc则是与鼠标消息相对应的钩子函数,如果这个函数返回非0值,则表示以对其进行了处理,系统将不会把这消息传递给窗口过程函数了。我们这里只是简单地让其返回1:
- LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
- {
- return 1;
- }
下面我们屏蔽键盘的空格键消息:首先设置一个钩子
- g_hKeyboard = SetWindowsHookEx(WH_KEYBOARD,
- KeyboardProc,
- NULL,
- GetCurrentThreadId());
- LRESULT CALLBACK KeyboardProc( int code, WPARAM wParam, LPARAM lParam )
- {
- if(VK_SPACE == wParam)
- {
- return 1;
- }
- else
- {
- return CallNextHookEx(g_hKeyboard,code,wParam,lParam);
- }
- }
假如我们想屏蔽Alt+F4键,那么可以
- if(VK_F4 == wParam && (1 == (lParam>>29 &1)))
假如我们想实现屏蔽其他键,但是按F2键程序退出,那么我们应该在按键响应中判断是否为F2键,如果是就该发送退出消息。可是发送消息时,需要获取窗口的句柄,这可怎么办好呢?无奈之下,我们只能使用一个全局句柄,并在OnInitDialog将其设为m_hWnd。
钩子函数的内容改为
- if(VK_F2 == wParam)
- {
- ::SendMessage(g_hWnd,WM_CLOSE,0,0);
- UnhookWindowsHookEx(g_hKeyboard);
- UnhookWindowsHookEx(g_hMouse);
- }
- return 1;
- #include <windows.h>
- HHOOK g_hMouse = NULL;
- LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
- {
- return 1;
- }
- void SetHook()
- {
- g_hMouse = SetWindowsHookEx(WH_MOUSE, //鼠标消息
- MouseProc, //钩子函数
- GetModuleHandle("Hook"), //动态链接库句柄
- 0); //与所有线程相关联
- }
并为其指定模块定义文件,将导出函数名设为SetHook,序号为2
- LIBRARY CH_20_HOOk
- EXPORTS
- SetHook @2
下面我们在动态链接库中增加一个钩子函数,屏蔽键盘消息。首先,我们不能屏蔽所有的键盘消息,否则我们将无法退出程序,只能强者重新启动了。我们可以为键盘消息留一个F2键,当按下F2键时,程序将自动退出。程序的退出可以通过发送WM_CLOSE消息来实现,但是发送消息时需要获得当前窗口的句柄,这可如何是好呢?我们可以把窗口的句柄当做参数传递进去,然后在dll中用一个全局变量接收,然后在钩子函数中就能获得这个句柄了:
- #include <windows.h>
- HHOOK g_hMouse = NULL;
- HHOOK g_hKeyboard = NULL;
- HWND g_hWnd;
- LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
- {
- return 1;
- }
- LRESULT CALLBACK KeyboardProc(int code,WPARAM wParam, LPARAM lParam )
- {
- if(VK_F2 == wParam)
- {
- SendMessage(g_hWnd,WM_CLOSE,0,0);
- UnhookWindowsHookEx(g_hMouse);
- UnhookWindowsHookEx(g_hKeyboard);
- }
- return 1;
- }
- void SetHook(HWND hwnd)
- {
- g_hWnd = hwnd;
- g_hMouse = SetWindowsHookEx(WH_MOUSE, //鼠标消息
- MouseProc, //钩子函数
- GetModuleHandle("CH_20_Hook"), //动态链接库句柄
- 0); //与所有线程相关联
- g_hKeyboard = SetWindowsHookEx(WH_KEYBOARD,
- KeyboardProc,
- GetModuleHandle("CH_20_Hook"),
- 0);
- }
当运行程序时,在窗口激活时按下F2,程序就能退出,而当在别的窗口下按下F2,却不能退出。这与我们之前对dll的理解有一些出入:之前提到,当dll被多个进程使用时,这些进程可以共享DLL的代码和数据。因此dll中的全局变量g_hWnd应该是被所有进程所共享的。在其他进程下按下F2,也能够退出。可事实上并不是这样,这是为什么呢?这是因为Windows采用了写入时复制机制,当DLL被两个进程共享时,如果第二个进程想要修改其中的数据,那么他将会复制一份新的页面。这样做是为了安全着想,因为假设DLL中的变量时一个指针,第一个调用dll的进程修改了它的话,当第二个进程调用它时,指针可能已经指向了某个其他的地方。
如果非要让全局句柄g_hWnd可以被共享的话,可以将它放到一个共享的节中:
- #pragma data_seg("MySec")
- HWND g_hWnd = NULL;
- #pragma data_seg()
- #pragma comment(linker,"/section:MySec,RWS")
SECTION HEADER #5
MySec name
104 virtual size
37000 virtual address
1000 size of raw data
35000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
D0000040 flags
Initialized Data
Shared
Read Write
此时,在任意窗口下按F2,都能关闭程序了。
除了使用#pragma comment来指定节的属性外,我们也可以在模块定义文件中设定共享节的属性:
- SEGMENTS
- MySec READ WRITE SHARED