21 HOOK编程
21.1.1基本知识
通过SetWindowsHookEx安装钩子:
HHOOK SetWindowsHookEx(
int idHook,//将要安装的钩子过程的类型
HOOKPROC lpfn,//指向相应的钩子过程
HINSTANCE hMod,//指定lpfn指向的钩子过程所在的DLL的句柄
DWORD dwThreadId//指定与钩子过程相关的线程标识
);
SetWindowsHookEx函数的作用是安装一个应用程序定义的钩子过程,并将其放到钩子链中。
最后安装的钩子过程总是排在该链的前面。
21.1.2 进程内钩子
1.安装鼠标钩子
- 在CinnerHookDlg::OnInitDialog()函数之前添加鼠标钩子过程代码
HHOOK g_hMouse = NULL;
LRESULT CALLBACK MouseProc(int nCode,
WPARAM wParam,
LPARAM lParam
)
{
return 1;
}
- 在CinnerHookDlg::OnInitDialog()函数return之前添加安装鼠标钩子过程代码
g_hMouse = SetWindowsHookEx(WH_MOUSE,MouseProc,NULL,GetCurrentThreadId());//GetCurrentThreadId():得到当前线程ID
安装鼠标钩子过程中,SetWindowsHookEx函数的入参:安装的钩子过程类型是WH_MOUSE,相应的钩子过程是MouseProc,钩子过程在当前进程中定义并且dwThreadId是当前进程的线程,所以这里hMod是NULL,dwThreadId设置为当前线程ID,该ID通过GetCurrentThreadId()函数获取。
2.安装键盘钩子
(1)屏蔽所有键盘按钮消息
类似于安装鼠标钩子的过程。
- 在CinnerHookDlg::OnInitDialog()函数之前添加键盘钩子过程代码
HHOOK g_hKeyboard = NULL;
LRESULT CALLBACK KeyboardProc(int code,
WPARAM wParam,
LPARAM lParam
)
{
return 1;
}
- 在CinnerHookDlg::OnInitDialog()函数return之前添加安装键盘钩子过程代码
g_hKeyboard = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, NULL, GetCurrentThreadId());
(2)仅屏蔽键盘的空格键和回车键消息
- 在CinnerHookDlg::OnInitDialog()函数之前添加键盘钩子过程代码
HHOOK g_hKeyboard = NULL;
LRESULT CALLBACK KeyboardProc(int code,
WPARAM wParam,
LPARAM lParam
)
{
//return 1;
if (VK_SPACE == wParam || VK_RETURN == wParam)
{
return 1;
}
else
{
return CallNextHookEx(g_hKeyboard,code,wParam,lParam);//将按键消息传递给下一个钩子过程
}
}
- 在CinnerHookDlg::OnInitDialog()函数return之前添加安装键盘钩子过程代码
g_hKeyboard = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, NULL, GetCurrentThreadId());得到当前线程ID
钩子过程通过wParam参数得到当前按下的是哪个按键。VK_SPACE宏是空格键的虚拟键码。VK_RETURN是回车键的虚拟键码。
(3)仅屏蔽键盘的组合键“Alt+F4”消息
- 在CinnerHookDlg::OnInitDialog()函数之前添加键盘钩子过程代码
LRESULT CALLBACK KeyboardProc(int code,
WPARAM wParam,
LPARAM lParam
)
{
//return 1;
//if (VK_SPACE == wParam || VK_RETURN == wParam)
if (VK_F4 == wParam && (1 == (lParam>>29 & 1)))
{
return 1;
}
else
{
return CallNextHookEx(g_hKeyboard,code,wParam,lParam);//将键盘消息传递给下一个钩子过程
}
}
- 在CinnerHookDlg::OnInitDialog()函数return之前添加安装键盘钩子过程代码
g_hKeyboard = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, NULL, GetCurrentThreadId());
键盘按键为F4且Alt时,return 1,其余情况将键盘消息传递给下一个钩子过程。
这里按键F4用wParam 为VK_F4 宏来判断。
按键Alt用lParam第29位,值为1则是Alt键,值为0非Alt键。lParam右移29位,将第29位移动到最右侧,和1相与,值为1说明是Alt键,值为0说明非Alt键。
(4)只能按照某个特定键退出
- 在CinnerHookDlg::OnInitDialog()函数之前添加键盘钩子过程代码
HHOOK g_hKeyboard = NULL;
HWND g_hWnd = NULL;
LRESULT CALLBACK KeyboardProc(int code,
WPARAM wParam,
LPARAM lParam
)
{
//return 1;
//if (VK_SPACE == wParam || VK_RETURN == wParam)
//if (VK_F4 == wParam && (1 == (lParam>>29 & 1)))
//{
// return 1;
//}
//else
//{
// return CallNextHookEx(g_hKeyboard,code,wParam,lParam);//将消息传递给下一个钩子过程
//}
if (VK_F2 == wParam)
{
::SendMessage(g_hWnd,WM_CLOSE,0,0);
UnhookWindowsHookEx(g_hMouse);//移走鼠标钩子过程
UnhookWindowsHookEx(g_hKeyboard);//移走键盘钩子过程
}
return 1;
}
- 在CinnerHookDlg::OnInitDialog()函数return之前添加安装键盘钩子过程代码
g_hWnd = m_hWnd;//获取目标窗口的句柄
g_hMouse = SetWindowsHookEx(WH_MOUSE,MouseProc,NULL,GetCurrentThreadId());//GetCurrentThreadId():得到当前线程ID
g_hKeyboard = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, NULL, GetCurrentThreadId());
这里的键盘钩子过程是一个全局函数,因此该函数内部只能用一个全局的SendMessage函数。SendMessage的第一个参数是目标窗口的句柄,该句柄由CinnerHookDlg类的成员变量m_hWnd获得。
UnhookWindowsHookEx(HHOOK hhk);从钩子链中移走一个已经安装的钩子
21.1.3 全局钩子
(1)动态链接库实现全局钩子过程
要屏蔽当前正在运行的所有进程的鼠标和键盘消息,安装钩子过程的代码放到动态链接库中实现。
新建-项目-win32项目-应用程序类型选择Dll,空项目。项目名称为Hook
添加Hook.cpp,添加代码如下
#include <Windows.h>
HHOOK g_hMouse = NULL;
HHOOK g_hKeyboard = NULL;
HWND g_hwnd;
HINSTANCE g_hInst;
//鼠标钩子过程,屏蔽所有鼠标消息
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;
}
_declspec(dllexport) void SetHook(HWND hwnd);//如果接口函数没有导出,不会生成lib文件,只有dll文件生成
//安装鼠标钩子过程的函数
void SetHook(HWND hwnd)
{
g_hwnd = hwnd;
//g_hMouse = (WH_MOUSE, MouseProc,GetModuleHandle("Hook"),0);//第四个参数dwThreadId=0,说明安装的钩子过程与运行在同一桌面上的所有线程相关了。
//第三个参数是安装钩子过程所在的DLL模块句柄
//两种方式得到该句柄,1.DllMain函数,加载DLL时,系统调用DllMain并传递当前的DLL模块的句柄。2.GetModuleHandle
//动态链接库不可单独运行,必须被调用。编写一个客户端程序,加载这个DLL。
如果接口函数没有导出,不会生成lib文件,只有dll文件生成.所以给Hook.dll代码中添加_declspec(dllexport) void SetHook();
//要调试dll,将dll复制到运行程序同目录下。
//留后门,传递HookTest的窗口句柄给键盘钩子过程
g_hMouse = SetWindowsHookEx(WH_MOUSE, MouseProc, g_hInst, 0);//用g_hInst或者GetModuleHandle获取安装钩子过程所在的dll句柄都可以
g_hKeyboard = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, GetModuleHandle(("Hook")), 0);
}
1.SetWindowsHookEx第三个参数是安装钩子过程所在的DLL模块句柄
有两种方式得到该句柄:方法(1).DllMain函数,加载DLL时,系统调用DllMain并传递当前的DLL模块的句柄。如代码中设置鼠标钩子。方法(2)用模块定义文件及GetModuleHandle,如代码中设置键盘钩子。
模块定义文件Hook.def内容:
LIBRARY Hook
EXPORTS
SetHook @2
记得一定要配置模块定义文件到dll的属性中:
工程属性–>linker—>input---->Module Definition File
2.如果接口函数没有导出,不会生成lib文件,只有dll文件生成.所以给Hook.dll代码中添加_declspec(dllexport) void SetHook();
3.键盘钩子过程留后门F2,防止将所有进程的鼠标和键盘消息都屏蔽后,卡死的情况。当键盘按下F2,时,发送WM_CLOSE消息给调用进程的主窗口,SendMessage需要主窗口的句柄,由SetHook函数传递m_hWnd给g_hwnd 。
上述货物安装钩子过程所在的DLL模块句柄要用的.DllMain函数代码如下:
dllmain.cpp
#include<Windows.h>
extern HINSTANCE g_hInst;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
g_hInst = hModule;
return TRUE;
}
动态链接库不可单独运行,必须被调用。编写一个客户端程序,加载这个DLL。
新建-项目-基于对话框的MFC项目-项目名称为HookTest
配置上面动态链接库项目Hook生成的Hook.lib文件:HookTest属性-链接器-输入-值设置为:…\Debug\Hook.lib
HookTestDlg.cpp文件中添加安装钩子代码:
_declspec(dllimport) void SetHook(HWND hwnd);
// CHookTestDlg 消息处理程序
BOOL CHookTestDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
...
// TODO: 在此添加额外的初始化代码
int cxScreen, cyScreen;
cxScreen = GetSystemMetrics(SM_CXSCREEN);
cyScreen = GetSystemMetrics(SM_CYSCREEN);
SetWindowPos(&wndTopMost, 0, 0, cxScreen, cyScreen, SWP_SHOWWINDOW);//置顶HookTest的对话框窗口
SetHook(m_hWnd);
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
其中,窗口置顶并占据整个屏幕用SetWindowPos实现。
(2)共享节实现全局变量在多个进程间共享
上面的全局钩子程序,在不切换进程时按下F2键HookTest程序退出,在切换进程之后,按下F2键HookTest程序没有退出。原因是WINDOWS采用了**写入时复制时机制。**即就是DLL有个数据被两个进程共享,当第2个进程想修改DLL数据页面2的数据,操作系统会将数据2页面的数据复制一份,新建一个页面,断开数据2页面与第2个进程之间的映射关系,将新页面与第2个进程映射,这样修改第2个进程的页面数据相当于修改新页面的额数据。而第1个进程仍然用DLL的原始数据,第一个进程和第二个进程在修改之前共享同一份数据。修改后不是同一个页面上的数据。
所以上面程序HookTest,在调用SetHook函数时,全局窗口变量g_hWnd发生变化,操作系统采用了写入时复制时机制,为HoolTest进程 重新分配一个页面,随后对g_hWnd变量的设置值就是在新页面中进行的,在切换到其他进程之后,g_hWnd仍然是原始数据,所以按下F2后HookTest不能退出。
想要切换进程后,按下F2,HookTest仍能退出,需要采用共享节的方法。给Hook.dll中新建并共享一个节,将全局窗口句柄变量g_hWnd放入该节中,该全局变量在多个进程之间共享。
定义共享结点有两种方式:
1.节设置为R(读)W(写)S(共享)类型,例如
#pragma data_seg(".MySec")
HWND g_hwnd = NULL;
#pragma data_seg()
#pragma comment(linker,"/section:.MySec,RWS");//将.MySec这个节设置为共享节
2.用SECTIONS关键字实现,例如
Hook.def文件中添加
LIBRARY Hook
EXPORTS
SetHook @2
SECTIONS
MySec READ WRITE SHARED
显示动态链接库导出函数的命令:dumpbin -exports Hook.dll
显示动态链接库的节,命令:dumpbin -headers Hook.dll
通过共享节方式,所有进程都共享g_hwnd的同一份数据。当运行HookTest程序后,g_hwnd为HookTest程序的主窗口句柄,切换到其他窗口,g_hwnd值未变,仍然为HookTest程序的主窗口句柄。按下F2,发送WM_CLOSE消息,HookTest退出。
总结
1.Hook编程实现进程内钩子和全局钩子安装