Windows API HOOK钩子

本文讲下Windows API HOOK(钩子),主要和DLL注入联系在一起.
Windows系统把每个WindowsAPI函数功能封装到了每一个DLL库文件中,从而提高程序的编译运行效率,而在应用程序运行期间动态库时,必然存在程序获取DLL中的函数地址.于是通过注入DLL来截取信息.

消息HOOK

HOOK(挂钩)技术,可分为 内核层(R0)HOOK , 应用层(R3)HOOK ,这两种技术实现起来虽然略相似,但是 内核层HOOK 比起 应用层HOOK 难度要高得多,而且编写的代码也要保证正确(异常机制),不然万一来个赋值给一个空指针的代码,蓝屏就呵呵了…本文主要讲解 应用层hook

主要用到的WindowsAPI

头文件 Winuser.h (include Windows.h)
库文件 User32.lib
DLL文件 User32.dll

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

其中 idHook 可选的值有很多种, 更多选项可以到 MSDN https://msdn.microsoft.com/en-us/library/windows/desktop/ms644990(v=vs.85).aspx 查看
本文用到其中的 WH_GETMESSAGE 监视发送到消息队列的消息

GetMsgProc

_In_ HOOKPROC lpfn, 对应的回调函数如下

1
2
3
4
5
LRESULT CALLBACK GetMsgProc(
_In_ int code,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);

lParam 参数可以转换为 **MSG***结构体
MSG* pMsg = (MSG*)lParam;

CallNextHookEx

这个函数起到传递下一个钩子函数的作用,这样的话,其他钩子才可以继续执行

1
2
3
4
5
6
LRESULT WINAPI CallNextHookEx(
_In_opt_ HHOOK hhk,
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);

nCode,wParam,lParam 一般都是 GetMsgProc 中的参数
,而 hhk 则为 SetWindowsHookEx 返回的钩子句柄.

UnhookWindowsHookEx

卸载钩子,与 SetWindowsHookEx 成对使用

1
2
3
BOOL WINAPI UnhookWindowsHookEx(
_In_ HHOOK hhk
);

下面以一个简单例子说明(MFC)

Source.def文件,用于导出DLL函数

1
2
3
4
LIBRARY 	hookFun.dll	;dll名
EXPORTS ;导出函数
SetHook @1
UnsetHook @2

DLL模块文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<windows.h>
#define DLL_EXPORT extern "C" __declspec(dllexport)
//开始HOOK
DLL_EXPORT void _stdcall SetHook(DWORD pid);
//取消HOOK
DLL_EXPORT void _stdcall UnsetHook();
//全局 钩子句柄
HHOOK hHook_g = NULL;
//全局 DLL模块句柄
HINSTANCE hInstance_g = NULL;
//DLL 入口函数
BOOL APIENTRY DllMain(_In_ void* _DllHandle, _In_ unsigned long _Reason, _In_opt_ void* _Reserved)
{
if (_Reason==DLL_PROCESS_ATTACH)
{
//保存当前 DLL模块的句柄到全局,以后要用!
hInstance_g = (HINSTANCE)_DllHandle;
}
return TRUE;
}
//自定义消息,处理钩子消息函数
#define WM_HOOKMSG WM_USER+100
//过程消息处理函数
LRESULT CALLBACK GetMsgProc(_In_ int code,_In_ WPARAM wParam,_In_ LPARAM lParam)
{
//把 参数 lParam 转换为 MSG 结构体,获取 消息类型
MSG* pMsg = (MSG*)lParam;
if (pMsg->message== WM_HOOKMSG)
{
MessageBoxA(NULL, "HOOK MSG", NULL, 0);
}
//继续传递下一个钩子
return CallNextHookEx(hHook_g, code, wParam, lParam);
}
//开始HOOK,这里过于简单,没有进行其他处理...(-。-;)
//参数--> 线程ID
DLL_EXPORT void _stdcall SetHook(DWORD tid)
{
//安装钩子
//hInstance_g:标识 GetMsgProc 在 该模块内
//tid: 标识是 全局钩子( tid=0 ) 还是指定 线程钩子
hHook_g = SetWindowsHookExW(WH_GETMESSAGE, GetMsgProc, hInstance_g, tid);
}
//取消HOOK
DLL_EXPORT void _stdcall UnsetHook()
{
//卸载钩子
UnhookWindowsHookEx(hHook_g);
}

调用进程主要代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//自定义消息
#define WM_HOOKMSG WM_USER+100
//路径
#define DLL_PATH L"C:\\hookFun.dll"
//SetHook 函数地址
typedef void(_stdcall* LPFNSETHOOK)(DWORD);
//UnsetHook 函数地址
typedef void(_stdcall* LPFNUNSETHOOK)();
HMODULE hMo = NULL;
void ChookMfcDlg::OnBnClickedButton1()
{
//获取指定主窗口的句柄
HWND hwndMain = ::FindWindow(NULL, L"剪贴簿查看器 - [剪贴板]");
//保存 进程ID
DWORD PID;

char szMsg[50]={ 0 };
//返回 线程ID
DWORD TID= GetWindowThreadProcessId(hwndMain, &PID);
sprintf(szMsg, "Thread ID:%08X", TID);
MessageBoxA(NULL, szMsg, szMsg, MB_ICONINFORMATION);
//先动态载入 要注入的DLL,然后再把它卸载!!!
hMo= LoadLibrary(DLL_PATH);
//获取模块内 SetHook 函数地址
LPFNSETHOOK lpSethook = (LPFNSETHOOK)GetProcAddress(hMo, "SetHook");
//调用SetHook函数
lpSethook(TID);
//向 要注入的 进程的线程ID 投递 我自己消息
PostThreadMessageW(TID, WM_HOOKMSG, 0, 0);
//获取模块内 UnsetHook 函数地址
LPFNUNSETHOOK lpUnSethook = (LPFNUNSETHOOK)GetProcAddress(hMo, "UnsetHook");
}
void ChookMfcDlg::OnBnClickedButton2()
{
//获取模块内 UnsetHook 函数地址
LPFNUNSETHOOK lpUnSethook = (LPFNUNSETHOOK)GetProcAddress(hMo, "UnsetHook");
// 取消HOOK!
lpUnSethook();
//把 主调用进程 的 DLL 卸载,不然会一直存在主调用进程中!!
FreeLibrary(hMo);
}

一般来说,注入DLL这项技术确实很老了,注入时可能会失败.so

键盘HOOK

这个例子没有用到DLL,直接勾住当前程序

  • KeyboardProc —- WH_KEYBOARD(2)
  • LowLevelKeyboardProc —- WH_KEYBOARD_LL(13)

注意 上面的 KeyboardProcLowLevelKeyboardProc 函数,前者是普通的钩子函数,而后者是一个低级的钩子函数,这两个函数的参数不同,所以要获取的值也不同!!

wParam [in]
Type: WPARAM
The virtual-key code of the key thatgenerated the keystroke message.
lParam [in]
Type: LPARAM
The repeat count, scan code, extended-key flag, contextcode, previous key-state flag, and transition-state flag. For more informationabout thelParam parameter, seeKeystroke Message Flags. The following table describes the bits of this value.
由此可看出 ,wparam主要是键盘的虚拟键代码,lparam主要是:
The lParam parameter of a keystroke message containsadditional information about the keystroke that generated the message. Thisinformation includes therepeat count, the scan code, the extended-key flag, thecontext code, the previous key-state flag, and the transition-state flag.The following illustration shows the locations of these flags and values in thelParam parameter.

SetWindowsHookEx() 参数 idHOOKWH_KEYBORAD_LL 时,这个函数的参数中的 wparam键盘消息 ,如WM_KEYDOMN… 那么 lparam 就是一个 LPKBDLLHOOKSTRUCT结构体 了!看这个结构体的名字就知道这是一个用于 低级键盘钩子的,“LL”->”LowLevel“,呵呵~这个结构体中 包括了 虚拟键代码 和 扫描码!

别忘了还有那个 SetWindowsHookEx() 函数,现在在回忆一下那个函数原型:

1
2
3
4
5
6
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId
);
  • IdHook为WH_KEYBOARD时,lpfn为KeyboardProc(名字自定义),hMod为当前程序实例句柄,MFC 有多种方法获取,如: AfxGetInstanceHandle()
    最后一个 dwThreadId为0

  • 当idHook为WH_KEYBOARD_LL时,dwThreadid必须为当前模块的线程ID!
    如下表:

    idHook lpfn hMod dwThreadId
    WH_KEYBOARD KeyboardProc 当前程序实例句柄 0
    WH_KEYBOARD_LL LowLevelKeyboardProc 当前程序实例句柄 当前程序线程ID

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
HHOOK hHook2;
LRESULT CALLBACK KeyBoradProc(intcode,WPARAMwp,LPARAMlp)
{
if (code<0)
{
return CallNextHookEx(hHook2,code,wp, lp);
}
WCHAR szkeyValue[20]={ 0 };
//wsprintf(szkeyValue, L"%c", wp); //虚拟键代码
//获取按键的名称
GetKeyNameText(lp,szkeyValue, 50);
AfxGetMainWnd()->SetDlgItemText(IDC_EDIT1,szkeyValue);
return CallNextHookEx(hHook2,code,wp, lp);
}
//安装键盘钩子
hHook2 = SetWindowsHookEx(WH_KEYBOARD,KeyBoradProc,AfxGetInstanceHandle(),GetCurrentThreadId()); //注意这里的线程ID为当前程序的线程ID!

//卸载钩子
UnhookWindowsHookEx(hHook2);
下面是关于低级键盘钩子的代码;
//低级的键盘钩子
hHook= ::SetWindowsHookEx(WH_KEYBOARD_LL,myLowLevelKeyboardProc,AfxGetInstanceHandle(),0);//注意这里TID为 0

//卸载低级钩子
UnhookWindowsHookEx(hHook)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//低级钩子函数处理过程
LRESULT CALLBACK LowLevelKeyboardProc(_In_int code,_In_WPARAMwParam,_In_LPARAMlParam)
{
if (code < 0)
{
returnCallNextHookEx(hHook,code,wParam, lParam);
}

// 低级键盘钩子时,wparam参数为 WM键盘消息!!!

//按下的
if (code ==HC_ACTION&&wParam==WM_KEYDOWN)
{
LPKBDLLHOOKSTRUCTpKbs = (LPKBDLLHOOKSTRUCT)lParam;
WCHARszlMsg[100] = { 0 };
wsprintf(szMsg,L"vkCode:%c-scanCode:%02X",pKbs->vkCode,pKbs->scanCode);

//ESC键扫描码
if (pKbs->scanCode==0x01)
{
returnTRUE;
}
}
return CallNextHookEx(hHook,code,wParam, lParam);
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!