Abstract
Keywords Reprint  Windows Hook  技术笔记 
Citation Yao Qing-sheng.Hook_Windows_NT.FUTURE & CIVILIZATION Natural/Social Philosophy & Infomation Sciences,20191203. https://yaoqs.github.io/20191203/hook-windows-nt/

Reprint from https://blog.csdn.net/WinsenJiansbomber/article/details/16891189

Hook API

2013 年 11 月 16 日功能追溯

Windows 编程的最简单的程序结构,只需要一个消息环。以下展示一个基本的 Win32 程序,它在开发执行时,会播放 Windows 7 启动时的使用的音响。#pragma 是 VC 平台的专用指令,使用它来替代手动设置工程属性,免去手动添加链接所需的 LIB 库。和普通控制台程序

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#pragma comment(lib,"winmm.lib")
#include <windows.h>
#include <string.h>
using namespace std;

static TCHAR szTitle[] = TEXT("GUI App");
static TCHAR szSound[] = TEXT( "c:\\windows\\media\\Windows Logon Sound.wav" );

HWND hWin;
HINSTANCE hAPP;

LRESULT CALLBACK circle( HWND, UINT, WPARAM, LPARAM );

int WINAPI WinMain(HINSTANCE hApp, HINSTANCE hPre, LPSTR lpLine, int iCmd )
{
MSG msg;
WNDCLASS winClass;

hAPP = hApp;

winClass.style = CS_HREDRAW | CS_VREDRAW;
winClass.lpfnWndProc = circle;
winClass.hInstance= hApp;
winClass.hIcon= LoadIcon( NULL, IDI_APPLICATION );
winClass.hCursor= LoadCursor( NULL,IDC_CROSS );
winClass.hbrBackground = (HBRUSH) GetStockObject( WHITE_BRUSH );
winClass.lpszClassName = szTitle;
winClass.lpszMenuName = NULL;
winClass.cbClsExtra = 0;
winClass.cbWndExtra = 0;

if( !RegisterClass( &winClass ) ){
MessageBox( NULL, TEXT("I need a Window!"), szTitle, MB_ICONERROR );
return 0;
}

hWin = CreateWindow(
szTitle, szTitle, //window class and caption
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, // position x, y
CW_USEDEFAULT, CW_USEDEFAULT, // size width, height
NULL, // parent windows handle
NULL, // menu handle
hApp,
NULL); //creation parameters

ShowWindow( hWin, iCmd );
UpdateWindow( hWin );

while(GetMessage( &msg, NULL, 0, 0 ) ){
TranslateMessage( &msg );
DispatchMessage( &msg );
}

return msg.wParam;

}

LRESULT CALLBACK circle( HWND hWin, UINT iMsg, WPARAM wp, LPARAM lp ){
HDC hdc ;
PAINTSTRUCT ps ;
RECTrect ;

switch( iMsg ){
case WM_CREATE:
PlaySound( szSound, NULL, SND_FILENAME | SND_ASYNC );
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
hdc = BeginPaint( hWin, &ps);
GetClientRect( hWin, &rect );
string text = "Appication API HOOK";
DrawText( hdc, text.c_str(), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER );
EndPaint( hWin, &ps);
return 0;
}

return DefWindowProc( hWin, iMsg, wp, lp);
}

上面的程序结构和普通的 DOS 程序大体相同,只是加入了 Windows 平台的各种对象。为了新建一个 GUI 界面,首先设置了一个窗口类 winClass,然后通过 RegisterClass 来注册到系统内,最后通过 CreateWindow 完成窗口的创建并用 ShowWindow 将其显示出来程序的结束同样是以 WinMain 的结束而完成的。不同的地方就在 while 循环,这个就是 Windows 系统特有的消息环。Windows 通过消息机制来管理着系统设备的各种事件,如鼠标移动了,键盘被按下了,网络连接上了,或者是停电了等等。每一个事件有伴随着消息的传递,每条消息按先后缓急顺序被存储到一个称为消息队列 Queue 的地方。用户程序在消息环调用 GetMessage 方法时,就会进入一个不消耗 CUP 的等待状态,Windows 在消息队列中发现一个属于当前用户程序的消息时,就会发送给用户程序,这样 GetMessage 就取得消息并返回到用户程序的消息环。这时最重要的事情就是对消息进行向应,这就是 Windows 编程要做的事。在前面,创建窗口时,给 GreateWindow 传入了一个窗口类,这个类的 lpfnWndProc 成员设置了一个引用 circle 的函数指针,这就是程序用来响应消息的方法,称为窗口过程。当消息环中调用 DispatchMessage 方法时,就会将消息传递给窗口过程进行处理。而在窗口过程中,将对不同的消息进行选择性处理,如程序完成初始化时的 WM_CREATE 消息,程序将播放一段乐音来响应。当用户通过鼠标点击窗口右上角的关闭按钮时,将产生一个 WM_DESTORY 消息,这就是一个关闭程序的意图。窗口过程在接收到这个关闭程序的消息时,就以调用 PostQuitMesage 来响应,它就是用来告知系统,程序需要关闭,不需要再做处理其它消息了。然后系统给消息环发送一个空消息,以使用 while 终止循环,最后 WinMain 即,程序结束。

注意,和 PostQuitMessage 相似的 PostMessage 可以用来向程序发送任意消息,它只负责将消息放到消息队列中,然后直接返回,相应的 SendMessage 要等到受到消息处理的返回码后才继续。

WM_PAINT 是 Windows 窗口系统中一条重要的消息,应用程序通过处理该消息实现在窗口上的绘制工作。系统会在多个不同的时机发送 WM_PAINT 消息:当第一次创建一个窗口时,当改变窗口的大小时,当把窗口从另一个窗口背后移出时,当最大化或最小化窗口时,等等,这些动作都是由系统管理的,应用只是被动地接收该消息,在消息处理函数中进行绘制操作。大多数的时候应用也需要能够主动引发窗口中的绘制操作,比如当窗口显示的数据改变的时候,这一般是通过 InvalidateRect 和 InvalidateRgn 函数来完成的,前者把指定的区域加到窗口的待更新区域 Update Region 中,当应用的消息队列没有其他消息且待更新区域不为空时,系统就会自动产生 WM_PAINT 消息。待更新区域是用一个 RECT 结构表示的,如下定义:

1
2
3
4
5
6
typedef struct _RECT {
LONG left; // 窗口左边开始计算的像素位置
LONG top; // 窗口的顶部计算的像素位置
LONG right;
LONG bottom;
} RECT, *PRECT;

系统为什么不在调用 Invalidate 时发送 WM_PAINT 消息呢?又为什么非要等应用消息队列为空时才发送 WM_PAINT 消息呢?这是因为系统把在窗口中的绘制操作当作一种低优先级的操作,于是尽可能地推后做。待更新区域区域会被累加起来,然后在一个 WM_PAINT 消息中一次得到更新,不仅能避免多次重复地更新同一区域,也优化了应用的更新操作。这种通过 InvalidateRect 和 InvalidateRgn 来使窗口区域无效,依赖于系统在合适的时机发送 WM_PAINT 消息的机制实际上是一种异步工作方式,也就是说,在无效化窗口区域和发送 WM_PAINT 消息之间是有延迟的;有时候这种延迟并不是我们希望的,这时我们当然可以在无效化窗口区域后利用 SendMessage 发送一条 WM_PAINT 消息来强制立即重画,但不如使用 Windows GDI API:UpdateWindow 和 RedrawWindow 或者使用 WM_PRINT 、WM_PRINTCLIENT 消息。

BeginPaint 和 WM_PAINT 消息紧密相关。试一试在 WM_PAINT 处理函数中不写 BeginPaint 会怎样?程序会像进入了一个死循环一样达到惊人的 CPU 占用率,因为程序总有处理不完的 WM_PAINT 消息。其实 BeginPaint 的一个作用就是把待更新区域清空。BeginPaint 和 WM_ERASEBKGND 消息也有关系。当窗口的待更新区域被标志为需要擦除背景时,BeginPaint 会发送 WM_ERASEBKGND 消息来重画背景,同时在其返回信息里有一个标志表明窗口背景是否被重画过。当我们用 InvalidateRect 和 InvalidateRgn 来把指定区域加到待更新区域中时,可以设置该区域是否需要被擦除背景,这样下一个 BeginPaint 就知道是否需要发送 WM_ERASEBKGND 消息了。

要注意的是,BeginPaint 只能在 WM_PAINT 处理函数中使用,在其它消息下无法实现窗口重绘功能。例如可以使用以下方法来产生一个待更新区,以强制刷新窗口。

1
2
3
4
RECT rect;
GetClientRect( hWin, &rect );
InvalidateRect( hWin, &rect, TRUE);
UpdateWindow( hWin );

Windows 程序就是这样一个基本的工作过程,然而,从 DOS 开始,程序开发就有个传统:程序需求对系统功能的监视和响应以实现程序的功能。这也是程序开发的基本需求,Windows 3.x 的时代 HOOK 已经普遍应用。HOOK 根本上来讲就是一处提供给开发者嵌入自定义例程以实现程序功能的场所。目前普遍将 HOOK 翻译为钩子的做法其实不太恰当,如果按照我在做 Wordpress 二次开发的经验,我更愿意将 HOOK 称作过滤器 Filter,从本质上讲 Windows 的 HOOK 和 Wordpress 的 Filter 是一致的。它们所起的作用就像是水管中间接上的一个过滤器,把某些东西过滤出来。事实上,1993 年 Kyle Marsh 在 MSDN 上发表过一篇文章 Win32 Hooks,里就是将钩子回调过程为过滤器函数,这篇文章可以在 MSDN 的技术文章栏目中找到。

实现程序功能代码嵌入的方法有各种形式,如下:

使用注册表注入,将程序注册到:HKLM/Software/Microsoft/Windows NT/CurrentVersion/Windows/AppInit_DLLs。这 AppInit_DLLs 这个键记录了一个或一组逗号分隔的 DLL 文件,当一个使用 USER32.DLL 的程序载入时,就会透过 LoadLibrary () API 依次加载 AppInit_DLLs 指定的链接库。这种方法只在 NT 架构系统且 2K + 版本上才有效。使用 API 注入,通过 SetWindowsHookEx () 来注册 HOOK 处理程序,使用 CallNextHookEx () 来保持钩子链正常工作,退出时,使用 UnhookWindowsHookEx () 卸载钩子。通过修改 PE 文件注入,PE 程序文件中有一个导入地址表 IAT Import Address Table,它记录了程序要调用的外部函数的地址,改 PE 文件的 IAT,使之指向自己的代码,这样 EXE/DLL 在调用系统 API 的时候便会调用你自己的函数。要注意,Windows API 均有两个版本:Ansi 和 Unicode。例如获取程序标题的 GetWindowTextAPI 实际上只是一个宏,根据编译条件 UNICODE 来决定是调用 GetWindowTextA 还是 GetWindowTextW。在 NT 系统下所有 ANSI 版本 API 会转换成 UNICODE 版本。通过遥距线程注入,使用 CreateRemoteThread () 方法可以创建一个线程,将要注入的程序透过 LPTHREAD_START_ROUTINE 参数传递给创建的线程,但是程序要先使用 ThreadProc () API 包装。这种方法是 Jeffrey Ritcher 提出来的,他写的文档也很完善,不过也只在 NT 架构系统且 2K + 版本上才有效。透过 BHO 插件注入,BHO 是 Browser Helper Objects,只在 IE 浏览器中使用。IE 运行时会加载所有实现 IObjectWithSite 接口的 COM 组件。通过 Office 插件注入,和 BHO 方式相似,使用范围限制在 Office 内。

本文主要涉及 API 注入、PE 文件注入及遥距线程注入。
API 钩子程序结构

钩子,按习惯,大伙都叫它为钩了。在伴随 Windows 系统的发展中,钩子也发展出好多的分类,有系统层次的,有应用程序层次的,有核心层次的,有处理键盘消息的,有处理系统日志的,有监视 API 调用的,各式各样。对于一个钩子程序,按上面水管过滤的理解,首先就需要安装一个钩子,主钩子在系统中起作用;然后程序按功能逻辑进行处理,这需要一个钩子回调函数 hook procedure;完了,程序要退出,就要清场,把钩子回收。

而每条水管可以安装多个不同的过滤器,同理钩子不也可以有多个,因此组成了一条链,系统将按注册顺序来调用。先调用线程钩子,然后调用系统钩子,后注册的先调用。

对于用户层次的钩子,只需要一个可以安装和回收钩子的程序就可以了,连带程序功能都在一个程序内实现。当钩子需要处理 Windows 内核消息时,就需要钩子运行于内核模式,这时就需要可以开发内核程序的 DDK,它才是用来开发内核应用的,像驱动程序这类一样,而且必需将钩子程序编译到 DLL 程序中。只 Win16 程序才允许在程序内容注册一个系统钩子。先来看看 SetWindowsHookEx 原型:

1
2
3
4
5
6
HHOOK SetWindowsHookEx(
int idHook,// hook type
HOOKPROC lpfn, // hook procedure
HINSTANCE hMod,// handle to application instance
DWORD dwThreadId // thread identifier
);

hMod 指定钩子回调函数所在 DLL 的实例句柄。如果安装的是局部钩子的话,由于局部钩子的回调函数并不需要放在动态链接库中,这时这个参数就使用 NULL。

dwThreadID 是安装钩子后想监控的线程的 ID 号。该参数可以决定钩子是线程钩子局部范围的还是系统钩子全局范围。如果参数指定的是自己进程中的某个线程 ID 号,那么该钩子是一个局部钩子;如果指定的线程 ID 是另一个进程中某个线程的 ID,那么安装的钩子是一个局部的远程钩子;如果想要安装系统范围的全局钩子的话,可以将这个参数指定为 NULL,这样钩子就会被解释成系统范围的,可以用来监控所有的进程及它们的线程。

由于 32-bit 钩子不能注入到 64-bit 的进程,反之亦然,在 x64 平台上,需要准备 x64 版本的钩子程序,才能正常得到系统支持。使用 SetWindowsHookEx 注册钩子时,如果回 NULL,则表示注册失败,可以通过 GetLastError () 获取错误代码:返回代码 含义
ERROR_INVALID_HOOK_FILTER 钩子代码无效。
ERROR_INVALID_FILTER_PROC 钩子函数无效。
ERROR_HOOK_NEEDS_HMOD 注册系统钩子使用了空 hInstance 参数,或者注册线程钩子的线程不存在。
ERROR_GLOBAL_ONLY_HOOK 以系统钩子方式注册了线程钩子。
ERROR_INVALID_PARAMETER 线程 ID 无效。
ERROR_JOURNAL_HOOK_SET JOURNAL 钩子已经注册。
ERROR_MOD_NOT_FOUND hInstance 参数不是指向一个库。简而言之就是在模块列表中定位到不到指定模块。其它值 出于安全,不允许或系统内存不足。

下表展示了 WINUSER.H 定义的一些钩子的类型:钩子名称 作用层次 监视消息的类型和时机
WH_CALLWNDPROC 线程、系统 应用于 SendMessage 函数调用时。
WH_CALLWNDPROCRET 线程、系统 应用于 SendMessage 函数调用后。
WH_CBT 线程、系统 当基于计算机的训练(CBT)事件发生时调用钩子函数
WH_DEBUG 线程、系统 在系统调用其他钩子函数前执行的钩子,当然是除了 WH_DEBUG 了,不然会循环。
WH_FOREGROUNDIDLE 系统 系统空闲钩子,当系统空闲的时候调用钩子函数,这样就可以在这里安排一些优先级很低的任务
WH_GETMESSAGE 线程、系统 应用于 GetMessage 或 PeekMessage 函数执行后。
WH_HARDWARE 线程、系统 每当调用 GetMessage 或 PeekMessage 函数时,如果从消息队列中得到的是非鼠标和键盘消息,则调用钩子函数
WH_JOURNALRECORD 系统 日志记录钩子,用来记录发送给系统消息队列的所有消息
WH_JOURNALPLAYBACK 系统 日志回放钩子,用来回放日志记录钩子记录的系统事件
WH_KEYBOARD 线程、系统 每当调用 GetMessage 或 PeekMessage 函数时,如果从消息队列中得到的是 WM_KEYUP 或 WM_KEYDOWN 消息,则调用钩子函数
WH_KEYBOARD_LL 系统 像 Ctrl+alt+del 系统会先处理掉,WH_KEYBOARD 没法截获,而 WH_KEYBOARD_LL 可以,但很容易引起挂起之类的问题,不过操作系统通过 LowLevelHooksTimeout 限时操作,超时就直接被忽略。
WH_MOUSE 线程、系统 每当调用 GetMessage 或 PeekMessage 函数时,如果从消息队列中得到的是鼠标消息,则调用钩子函数
WH_MOUSE_LL 系统 截获整个系统的鼠标事件消息。
WH_MSGFILTER 线程、系统 应用于用户程序对对话框、菜单和滚动条的消息,先于程序行为。
WH_SYSMSGFILTER 系统 同 WH_MSGFILTER,应用于系统范围,影响更大。
WH_SHELL 线程、系统 当 Windows shell 程序准备接收一些通知事件前调用钩子函数,如 shell 被激活和重画等

日志记录钩子和日志回放钩子可以放在安装钩子的程序中,并不需要单独放在一个动态链接库中,因为它们是由 Windows 系统调用的钩子。程序内的线程级别钩子

在前面熟悉了 Win32 程序的基本结构和消息环的作用,在此基础上进行应用应用程序级别的钩子开发相对会比较容易入手。为了直观地显示注册钩子不成功的原因,下面使用 FormatMessage 增加了一个显示错误信息的方法,此方法通过查询 GetLastError 返回的错误代码返回文字版本的信息,然后通过对话框显示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void ShowErrorInfo(int Error){
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
Error,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
(LPTSTR) &lpMsgBuf,
0,
NULL
);
MessageBox( NULL, (LPCTSTR)lpMsgBuf, "Error", MB_OK | MB_ICONINFORMATION );
}

按照前面对钩子应用的基本结构,这里要添加注册钩子 HookStart 和清除钩子 HookDown 的函数,以及钩子过程 FilterFunc。现在就来实现最简单的当前程序级别的钩子,将文本最开始给出的 Win32 例子的窗口过程稍为修改一下,再添加上钩子功能得以下修改过的部分:

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
50
51
52
UINT COUNT;
HHOOK MyHook;

LRESULT CALLBACK FilterFunc( int nCode, WORD wp, DWORD lp) {
stringstream ss;
char text[MAXBYTE];
ss << szTitle << " - " << ++COUNT << std::hex << " MSG:" << nCode
<< std::hex << " wp:0x" << wp << " lp:0x" << lp << "\n";
ss.getline( text, MAXBYTE);
SetWindowText( hWin, text );
return CallNextHookEx( MyHook, nCode, wp, lp);
}

HHOOK HookStart(int idHook, HINSTANCE hd, DWORD td){
HOOKPROC hkp = reinterpret_cast( FilterFunc );
HHOOK hk = SetWindowsHookEx( idHook, hkp, hd, td );
if(hk==NULL){
UINT Error = GetLastError();
ShowErrorInfo(Error);
}
return hk;
}

void HookDown(HHOOK hook){
UnhookWindowsHookEx(hook);
}

LRESULT CALLBACK circle( HWND hWin, UINT iMsg, WPARAM wp, LPARAM lp ){
HDC hdc ;
PAINTSTRUCT ps ;
RECTrect ;

switch( iMsg ){
case WM_CREATE:
PlaySound( szSound, NULL, SND_FILENAME | SND_ASYNC );
MyHook = HookStart( WH_KEYBOARD, NULL,GetCurrentThreadId() );
return 0;
case WM_DESTROY:
PostQuitMessage(0);
HookDown( MyHook );
return 0;
case WM_PAINT:
hdc = BeginPaint( hWin, &ps);
GetClientRect( hWin, &rect );
string text = "Appication API HOOK";
DrawText( hdc, text.c_str(), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER );
EndPaint( hWin, &ps);
return 0;
}

return DefWindowProc( hWin, iMsg, wp, lp);
}

当按下键盘时,标题就会显示 WPARAM 和 LPARAM 参数,注意窗口过程中没有对按键消息进行处理,这是在钩子过程 FilterFunc 中处理的,如下图:

下一步将要实现系统级别的钩子,这个程序将需要获取其它进程的线程 ID 以将 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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
HWND SetCapture(  HWND hWnd );  // 捕捉鼠标事件
BOOL ReleaseCapture(VOID); // 释放鼠标
HWND WindowFromPoint( POINT Point ); // 通过鼠标位置来获取窗口句柄

HWND FindWindow( // 通过窗口类查找窗体
LPCTSTR lpClassName, // 窗口类名,NULL结束字符串
LPCTSTR lpWindowName // 窗口标题过滤
); // 例如记事本Notepad、写字板WordPadClass、控制台ConsoleWindowClass

HWND FindWindowEx( // 升级版FindWindow
HWND hwndParent, // handle to parent window
HWND hwndChildAfter, // handle to child window
LPCTSTR lpszClass,// class name
LPCTSTR lpszWindow// window name
);

BOOL EnumWindows( // 通过枚举查找窗口,回调函数原型随后
WNDENUMPROC lpEnumFunc, // callback function
LPARAM lParam// application-defined value
);
BOOL EnumChildWindows(
HWND hWndParent, // handle to parent window
WNDENUMPROC lpEnumFunc, // pointer to callback function
LPARAM lParam// application-defined value
);

BOOL CALLBACK EnumWindowsProc( // 返回False主动停止窗口枚举
HWND hwnd, // handle to parent window
LPARAM lParam // application-defined value
);

DWORD GetWindowThreadProcessId( // 通过窗口句柄来获取线程ID
HWND hWnd, // handle to window
LPDWORD lpdwProcessId // process identifier
);

HANDLE GetCurrentProcess(VOID) // 一组获取当前进程、线程及基ID的API
DWORD GetCurrentProcessId(VOID)
DWORD GetCurrentThreadId(VOID)
HANDLE GetCurrentThread(VOID)

int GetClassName( // 获取窗口类名
HWND hWnd, // handle to window
LPTSTR lpClassName, // class name
int nMaxCount// size of class name buffer
);

HANDLE OpenProcess( // 通过线程ID获取线程句柄
DWORD dwDesiredAccess, // 访问许可标志,见随后常数定义
BOOL bInheritHandle,// handle inheritance option
DWORD dwProcessId // process identifier
);

#define PROCESS_TERMINATE (0x0001)// 允许终止进程。
#define PROCESS_CREATE_THREAD (0x0002) // 允许创建远程线程。
#define PROCESS_VM_OPERATION (0x0008) // 许可WriteProcessMemory、VirtualProtectEx修改地址空间。
#define PROCESS_VM_READ (0x0010)// 允许对进程的地址空间进行读操作。
#define PROCESS_VM_WRITE (0x0020)// 允许对进程的地址空间进行写操作。
#define PROCESS_DUP_HANDLE(0x0040) // 允许进程句柄被复制。
#define PROCESS_CREATE_PROCESS(0x0080) //
#define PROCESS_SET_QUOTA (0x0100)//
#define PROCESS_SET_INFORMATION (0x0200) // 许可SetPriorityClass函数设置进程的优先级。
#define PROCESS_QUERY_INFORMATION (0x0400) //许可GetExitCodeProcess查询进程的退出码
// 或使用GetPriorityClass函数查询进程的优先级。
#define PROCESS_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFF)

FARPROC GetProcAddress( // 获取指定模块的函数地址
HMODULE hModule,// handle to DLL module
LPCSTR lpProcName // name of function
);
HINSTANCE LoadLibrary( // 手动装载程序模块
LPCTSTR lpLibFileName // address of filename of executable module
);
HINSTANCE LoadLibraryEx(
LPCTSTR lpLibFileName, // points to name of executable module
HANDLE hFile, // reserved, must be NULL
DWORD dwFlags // 入口点处理标志,见随后的常数定义
);
#define DONT_RESOLVE_DLL_REFERENCES 0x00000001
#define LOAD_LIBRARY_AS_DATAFILE0x00000002 // 不执行入口,相当LoadResource
#define LOAD_WITH_ALTERED_SEARCH_PATH 0x00000008

DWORD GetModuleFileName( // 获取模块文件位置
HMODULE hModule,// handle to module to find filename for
LPTSTR lpFilename, // pointer to buffer to receive module path
DWORD nSize // size of buffer, in characters
);

UINT_PTR SetTimer( // 设置定时器
HWND hWnd, // handle to window
UINT_PTR nIDEvent, // your timer identifier
UINT uElapse, // time-out value in milliseconds
TIMERPROC lpTimerFunc // timer procedure
);
BOOL KillTimer(
HWND hWnd, // handle to window
UINT_PTR uIDEvent // timer identifier
);
VOID CALLBACK TimerProc(
HWND hwnd, // handle to window
UINT uMsg, // WM_TIMER message
UINT_PTR idEvent, // timer identifier
DWORD dwTime // current system time
);

先来热身一下,这是一个通过枚举查找窗口类,标题、句柄等信息的控制台程序:

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
#include <windows.h>
#include <iostream>
#include <string>

using namespace std;

BOOL CALLBACK EnumProc( HWND hwnd, LPARAM lParam ){
char buf[MAXWORD];
char bufClass[MAXWORD];
DWORD thread;

GetWindowTextA( hwnd, buf, MAXWORD );
GetClassNameA( hwnd, bufClass, MAXWORD );
thread = GetWindowThreadProcessId( hwnd, NULL );

string title(buf);
if(
title==string("Default IME") ||
title==string("Default IME") ||
title==string("MSCTFIME UI")
) return true;
cout << "Found:0x" << hwnd << " THREAD:0x" << thread
<< " CLASS:" << bufClass << " TITLE:" << buf << endl;
return true;
}

void main(){
int (WINAPI *cb)() = reinterpret_cast(EnumProc);
BOOL isok = EnumWindows( cb, NULL);
cout << "exit:" << isok << endl;
}

这一个程序可以通过 ShowWindow 来让程序在任务栏显示或隐藏。

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
#include <windows.h>
#include <iostream>
#include <string>

using namespace std;

BOOL Toggle(const char *name, int state=SW_SHOW){
HWND hwin = FindWindowA( name, NULL );
return ShowWindow( hwin, state );
}

void main(){
cout << "Type exit to close." << endl;
for(;;){
string s;
cin >> s;
if(s=="show"){
string name;
cin >> name;
if( name.length()>0 ) Toggle( name.c_str(), SW_SHOW );
}else if( s=="hide"){
string name;
cin >> name;
if( name.length()>0 ) Toggle( name.c_str(), SW_HIDE );
}else if( s=="exit") break;
}
}

系统钩子

接下来开始正题,这次要建立系统级别的钩子。接下来需要深入一层使用系统钩子,这样就要新建一个 DLL 工程了,命名为 hooksrv,这样工程就会生成 hooksrv.dll。现在就来定义 hooksrv.h 头文件,注意前面的代码文件中 HookStart 和 HookDown 两个方法是要导出供程序调用的,这也是 DLL 程序的基本要求,那么导出标记 EXPORT 就在下面这个头文件中按格式定义。导出标记可以有三种方式,源代码中的 __declspec (dllexport) 关键字,.def 文件中的 EXPORTS 语句,LINK 命令中的 /EXPORT 规范。注意,使用 .def 文件从 DLL 中导出变量时,不需要在变量上指定导出标记。但是,在使用 DLL 的代码上,仍必须使用函数的导出声明,这个声明通常伴随在 DLL 的头文件内。头文件同时定义了一个 WM_HOOK 消息,这个消息将在消息环中使用。因为这是个自定义消息,所以只能使用 Windows 要求的比 WM_USER 大的值,比 WM_USER 小的消息值只供系统专用。

1
2
3
4
5
6
7
8
9
10
11
/*********** hooksrv.h **********/
#include <windows.h>

#define EXPORT __declspec(dllexport)
// #define IMPORT extern _declspec(dllimport)
// IMPORT int hookCode; // 此两行用于程序导入DLL变量

EXPORT HHOOK HookStart( int hookID, HWND hWindow, DWORD dwThreadId );
EXPORT BOOL HookDown( HHOOK );

#define WM_HOOK WM_USER + 1

这里使用了 Visual Studio 97,编译输出是 x86 架构的,如果要使用 x64 平台,请使用 Visual Studio 2005 的 x64 编译器。接下来要设置一下工程属性。在 Post-build step 新建一个命令动作,在 DLL 完成编译时用来将 DLL 拷贝到程序目录。假设我们的主程序在 hookcross 目录下,并且和 hooksrv 工作目录同级,那么对于 DEBUG 版本就可以使用以下命令,这样编译 DLL 时,就会自动拷贝到程序的工作目录下了:

1
copy debug\hooksrv.dll ..\hookcross\debug\

再来实现钩子的基本功能代码部分 hooksrv.cpp,其中 HookShared 是一个共享段,RWS 标记它是 Read、Write 和 Shared 的共享段,它是可选部分,这时用来展示如果实现 DLL 共享数据段。这样 DLL 注入到不同的程序后,可以透过共享段来进行数据共享。否则,在 Win32 受保护的程序内存空间下,程序间是不可以相互直接获取数据的。当然可以变通地使用传输只读数据的 WM_COPYDATA 消息,网络传输,或用 CreateFileMapping 内存映射文件实现共享内存,进程内存读写 ReadProcessMemory 及 WriteProcessMemory,又或者使用剪切板 SetClipboardData 等手段。DLL 共享数据时,一定要使用静态始化,否则编译时会因被放到未初始化数据段而失效,初始化为 0 的值也会当作未初始化数据而存放于.bss 段中。另外,DLL 本身有可能加载到每个进程的虚拟地址空间中的不同地址。因此具有指向 DLL 中的函数或指向其他共享变量的指针是不安全的。注意永远不要将特定于进程的信息存储在共享数据段中,这里只是展示如何使用 DLL 共享数据,在逻辑上来讲这样共享 hookCode 做是不正确的,因为它是属于被注入钩子的进程的。而前导的 EXPORT 是导出标记,定义在头文件中,导出 DLL 变量是为了方便在程序中访问,这些导出也可以通过模块定义文件 DEF 来标记。特别说明一下,导出变量和共享变量是两个概念,共享变量是所有进程加载 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
#include "hooksrv.h"

#pragma data_seg("HookShared")
HHOOK hHook = NULL;
HWND hWnd = NULL;
EXPORT int hookCode = 0xff;
#pragma data_seg()
#pragma comment(linker, "/SECTION:HookShared,RWS")

HINSTANCE hMod;

EXPORT BOOL APIENTRY DllMain( HANDLE hModule,
DWORD fdwreason, LPVOID lpReserved )
{
hMod = (HINSTANCE)hModule;
return TRUE;
}

void ShowErrorInfo(int Error){ } // 函数体在前面已经给出,这里省略。

LRESULT WINAPI HookProc(int code, WPARAM wParam, LPARAM lParam){
hookCode = code;
SendMessage( hWnd, WM_HOOK, wParam, lParam );
return CallNextHookEx(hHook, code, wParam, lParam);
}

EXPORT HHOOK HookStart(int hookID, HWND hWindow, DWORD dwThreadId){
hWnd = hWindow;
HOOKPROC cb = reinterpret_cast( HookProc ); // for VS97
hHook = SetWindowsHookEx( hookID, cb, hMod, dwThreadId);
if( !hHook ) ShowErrorInfo( GetLastError() );
return hHook;
}

EXPORT BOOL HookDown(HHOOK idHook){
hWnd = NULL;
return UnhookWindowsHookEx(hHook);
}

对于 DLL 的入口 DLLMain,这是自定义的入口点。如果没有指定它,系统会使用内置的入口点_DllMainCRTStartup,它会调用一个例程来初始化 C/C++ 的运行库,这个例程就是_CRT_INIT。事实上,DLLMain 也应该完成这些初始化的功能。入口参数 hModule 指载入 DLL 的进行句柄,参数 fdwreason 表示了载入 DLL 的进程是以什么方式进行的,如下:

1
2
3
4
#define DLL_PROCESS_ATTACH 1  // 正在映射到进程地址空间,执行初始化,出错则返回FALSE
#define DLL_THREAD_ATTACH 2 // 线程已经创建,执行初始化
#define DLL_THREAD_DETACH 3 // 线程终止,执行清理。测试时并没有执行!
#define DLL_PROCESS_DETACH 0 // 进程终止,从映射空间撤除,执行必要的清理

对于主程序,还是拿最开始的样板来使用,只需要修改一部分就可以了。新建一具 hookcross 工程,添加一个代码文件,文章开始将样板代码拷贝过来,修改窗口过程,并添加一个时钟调用。这样程序执行时会定时检查系统,看看有没有写字板程序在运行,如果发现它就将钩子注入。hooksrv.dll 在程序执行时将由系统自动调入,手动装入 DLL 可以参考 MSDN 的 Platform SDK > Windows Base Services > Excutables 部分。注意,注册钩子时,使用了 DLL 的进程 ID,而且使用了其它线程,这里指写字板线程,所以注册的是一个线程的系统钩子,而且只对 Wordpad 的消息环进行监测。如果,线程 ID 设置为 NULL,那么,这个钩子将对所有进程的消息进行监测,这就成了一个全局系统钩子。主程序不用修改,主要是修改窗口过程部分,这里将修改及添加部分的代码罗列出来:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include "../HookSrv/hooksrv.h"
#pragma comment(lib,"../HookSrv/Debug/hooksrv.lib")

extern __declspec(dllimport) int hookCode;

UINT COUNT;
HHOOK MyHook;
UINT TIMER_HOOK = 1;
string tipText = "Appication API HOOK Wating for a Wordpad.";

void CALLBACK TimerProc(HWND hwin, UINT uMsg, UINT id, DWORD t){
HWND hw = FindWindow( "WordPadClass",NULL );
if(!hw) return;
DWORD thread = GetWindowThreadProcessId( hw, NULL );
MyHook = HookStart( WH_SHELL, hWin, thread );
if(MyHook){
KillTimer( hWin,TIMER_HOOK );
tipText = "Hooked for Wordpad.";
RECT rect;
GetClientRect( hWin, &rect );
InvalidateRect( hWin, &rect, TRUE);
UpdateWindow( hWin );
}
}

void SendToPad(char * buf){ // 通过消息给程序发送字符及换行符
string txt(buf);
HWND hw = FindWindow( "WordPadClass",NULL );
HWND hc = FindWindowEx( hw, NULL,"RICHEDIT50W",NULL );
if(!hc) return;
for( int i=0; i<txt.length(); i++){
SendMessage( hc, WM_CHAR, (char) txt[i], 0 );
}
SendMessage( hc, WM_KEYDOWN, VK_RETURN, 0 ); // "\r\n" for Wordpad;
}

LRESULT CALLBACK circle( HWND hWin, UINT iMsg, WPARAM wp, LPARAM lp ){
HDC hdc ;
PAINTSTRUCT ps ;
RECT rect ;
string text = "Appication API HOOK Wating for a Wordpad.";
char buf[MAXBYTE];
stringstream ss;
TIMERPROC cb = reinterpret_cast(TimerProc); // for VS97

switch( iMsg ){
case WM_CREATE:
PlaySound( szSound, NULL, SND_FILENAME | SND_ASYNC );
SetTimer( hWin, TIMER_HOOK, 1000, cb);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
HookDown( MyHook );
return 0;
case WM_PAINT:
hdc = BeginPaint( hWin, &ps);
GetClientRect( hWin, &rect );
DrawText( hdc, tipText.c_str(), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER );
EndPaint( hWin, &ps);
return 0;
case WM_HOOK:
ss << szTitle << " - " << ++COUNT << std::hex << " HOOK:0x" << hookCode
<< " wp:0x" << wp << " lp:0x" << lp << "\n";
ss.getline( buf, MAXBYTE);
SetWindowText( hWin, buf );
SendToPad(buf);
}

return DefWindowProc( hWin, iMsg, wp, lp);
}

注意主程序使用__declspec (dllimport) 标记来导入 DLL 变量,hookCode 这个变量存储在 DLL 的共享数据段,所有使用此 DLL 的进程都可以修改它,并且所有进程都会得到修改后的数据,这种情况就是进程不安全的状态。

如果钩子不是 DLL 模块,那么在设置系统钩子时,就会出错:没有模块句柄无法设置非本机连接。这里尝试将钩子也编译到同一个程序文件来测试,通过 LocaLibrary 来加载程序,通过返回的模块句柄来操作,但 LoadLibrary 不能使其形成有效的模块,即使程序正常运行,但即收不到钩子回调动作。同时,每一个 DLL 钩子同时只能有个程序使用,如果多个程序使用注册 DLL 钩子,那么最后注册的才有效,即设置了不同的钩子类型,但是清除钩子却可以由不同的程序完成!钩子回调过程细节

钩子回调时,传回三个参数中,第一个为 ncode,它指一个钩子的代码,如果这个为负值,那么应该直接通过 CallNextHookEx 传回系统内部处理,然后回调函数直接返回系统给出的结果。但是从 Windows 3.1 开始,不再向回调函数传递负值了。后两个 wParam 和 lParam,与消息环的参数可以说十分一致。它们则是根据不同的钩子,用来传递不同的参数或数据。下面逐个钩子类型进行解释。这里特别要说明一点,因为 Windows 进程的内存空间是受保护的,所以注入 DLL 的进程和主程序是两个受保护的内存空间,要共享数据就要透过前面介绍的 DLL 共享数据的方法,或其它手段的实现,绝对不能通过指针来实现。

WH_CALLWNDPROC

得到一个钩子代码指示 SendMessage 已经发送消息,和一个指针指向真正的消息数据,如下:

1
2
3
4
5
6
typedef struct tagCWPSTRUCT {
LPARAM lParam;
WPARAM wParam;
DWORD message;
HWND hwnd;
} CWPSTRUCT, *PCWPSTRUCT, NEAR *NPCWPSTRUCT, FAR *LPCWPSTRUCT;

WH_DEBUG

当系统调用其它类型钩子过程之前会执行这个钩子,wParam 中得到下一个将被调用的钩子的 ID,例如可能是一个 WH_MOUSE。可以通过返回一个非 0 值来阻止系统,但不能修改钩子 ID。lParam 指向一个结构体,定义如下:

1
2
3
4
5
6
7
8
typedef struct tagDEBUGHOOKINFO
{
DWORD idThread; // The thread ID for the current thread
LPARAM reserved;
LPARAM lParam;// The lParam for the target filter function
WPARAM wParam;// The wParam for the target filter function
int code;
} DEBUGHOOKINFO, *PDEBUGHOOKINFO, NEAR *NPDEBUGHOOKINFO, FAR* LPDEBUGHOOKINFO;

WH_FOREGROUNDIDLE

当用户进程空闲无用户输入时调用的钩子,注意,只有钩子注入的线程是当前线程时才有效。这只是一个通知钩子,可以用来执行空闲任务,参数 wParam 和 lParam 都是 0。

WH_GETMESSAGE

在 GetMessage 和 PeekMessage 即将返回时消息时调用的钩子,lParam 指向一个结构体,包含了消息体数据,对消息的所有修改也会原样返回给程序。

1
2
3
4
5
6
7
8
typedef struct tagMSG { /* msg */
HWND hwnd; // The window whose Winproc will receive the message
UINT message; // The message number
WPARAM wParam;
LPARAM lParam;
DWORD time; // The time the message was posted
POINT pt;//The cursor position in screen coordinates of the message
} MSG;

WH_HARDWARE

欠缺资料。

WH_KEYBOARD

当 GetMessage 或 PeekMessage 即将返回键盘消息时调用,即一系列的 WM_KEYUP, WM_KEYDOWN, WM_SYSKEYUP, WM_SYSKEYDOWN, 和 。回调过程会收到键盘的虚拟按键代码和键盘状态,还可以让系统忽略掉这些消息。回调过程的钩子代码有两种,HC_ACTION 和 HC_NOREMOVE。前者表示事件将要从系统队列中清除,而后者则提示消息队列不会移除键盘消息。这是因为程序使用了带 PM_NOREMOVE 参数的 PeekMessage。wParam 存放虚拟键值如 VK_ESCAPE 之类,或在 WM_CHAR 消息中存放字符代码,lParam 存放按键数据,如重复次数,按键扫描码之类。虚拟键值定义在 WINRESRC.H,现罗列如下:

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
50
51
52
53
54
55
#define VK_LBUTTON0x01
#define VK_RBUTTON0x02
#define VK_CANCEL 0x03
#define VK_MBUTTON0x04/* NOT contiguous with L & RBUTTON */

#define VK_BACK 0x08 #define VK_SPACE 0x20
#define VK_TAB0x09 #define VK_PRIOR 0x21
#define VK_NEXT 0x22
#define VK_CLEAR 0x0C #define VK_END0x23
#define VK_RETURN 0x0D #define VK_HOME 0x24
#define VK_LEFT 0x25
#define VK_SHIFT 0x10 #define VK_UP 0x26
#define VK_CONTROL0x11 #define VK_RIGHT 0x27
#define VK_MENU 0x12 #define VK_DOWN 0x28
#define VK_PAUSE 0x13 #define VK_SELECT 0x29
#define VK_CAPITAL0x14 #define VK_PRINT 0x2A
#define VK_EXECUTE0x2B
#define VK_SNAPSHOT 0x2C
#define VK_ESCAPE 0x1B #define VK_INSERT 0x2D
#define VK_DELETE 0x2E
#define VK_HELP 0x2F

/* VK_0 thru VK_9 are the same as ASCII '0' thru '9' (0x30 - 0x39) */
/* VK_A thru VK_Z are the same as ASCII 'A' thru 'Z' (0x41 - 0x5A) */

#define VK_LWIN 0x5B
#define VK_RWIN 0x5C
#define VK_APPS 0x5D #define VK_SEPARATOR 0x6C
#define VK_SUBTRACT 0x6D
#define VK_NUMLOCK0x90 #define VK_DECIMAL0x6E
#define VK_SCROLL 0x91 #define VK_DIVIDE 0x6F
#define VK_NUMPAD00x60 #define VK_F1 0x70
#define VK_NUMPAD10x61 #define VK_F2 0x71
#define VK_NUMPAD20x62 #define VK_F3 0x72
#define VK_NUMPAD30x63 #define VK_F4 0x73
#define VK_NUMPAD40x64 #define VK_F5 0x74
#define VK_NUMPAD50x65 #define VK_F6 0x75
#define VK_NUMPAD60x66 #define VK_F7 0x76
#define VK_NUMPAD70x67 #define VK_F8 0x77
#define VK_NUMPAD80x68 #define VK_F9 0x78
#define VK_NUMPAD90x69 #define VK_F100x79
#define VK_MULTIPLY 0x6A #define VK_F110x7A
#define VK_ADD0x6B #define VK_F120x7B

/*
* VK_L* & VK_R* - left and right Alt, Ctrl and Shift virtual keys.
* Used only as parameters to GetAsyncKeyState() and GetKeyState().
* No other API or message will distinguish left and right keys in this way.
*/
#define VK_LSHIFT 0xA0
#define VK_RSHIFT 0xA1
#define VK_LCONTROL 0xA2
#define VK_RCONTROL 0xA3
#define VK_LMENU 0xA4
#define VK_RMENU 0xA5

WH_MOUSE

当 GetMessage 或 PeekMessage 即将返回鼠标消息时调用,和 WH_KEYBOARD 相似,也有钩子代码指示消息是否会从队列中移除。而鼠标事件消息有好多,如 WM_MOUSEMOVE,也有直接根据鼠标按键状态引发的事件消息,如 WM_LBUTTONDOWN、WM_LBUTTONUP、WM_LBUTTONDBLCLK,还有相应的中键和右键的消息。lParam 的高低两个字节分别存储了 Y、X 坐标,即 LOWORD (lParam) 表示 X 坐标。wParam 存储了按键状态数据,可以和按键掩码进行运算,如 MK_CONTROL、MK_LBUTTON、MK_MBUTTON、MK_RBUTTON、MK_SHIFT。

当鼠标在非显示区域时,会引发 WM_NCMOUSEMOVE 这类事件,有对应按下左键的 WM_NCLBUTTONDOWN、WM_NCLBUTTONUP、WM_NCLBUTTONDBLCLK,其它按键类似。此时 wParam 表示命中的区域,lParam 还是表示鼠标的屏幕坐标系。

鼠标命中测试消息 WM_NCHITTEST,Windows 根据它来产生与鼠标位置相关的所有其它鼠标消息。wParam 未使用,lParam 存储屏幕坐标。滑轮消息 WM_MOUSEWHEEL,此时 wParam 还保存按键状态数据,而且在其高两字节保存了滚动计数值 zDelta,即 HIWORD (wParam)。

但是在鼠标钩子回调过程中,wParam 存放的是鼠标消息 ID,lParam 指向一个结构体,如下罗列:

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
#define WM_MOUSEACTIVATE0x0021
#define WM_NCMOUSEMOVE 0x00A0
#define WM_NCMOUSEMOVE 0x00A0
#define WM_NCLBUTTONDOWN0x00A1
#define WM_NCLBUTTONUP 0x00A2
#define WM_NCLBUTTONDBLCLK 0x00A3
#define WM_NCRBUTTONDOWN0x00A4
#define WM_NCRBUTTONUP 0x00A5
#define WM_NCRBUTTONDBLCLK 0x00A6
#define WM_NCMBUTTONDOWN0x00A7
#define WM_NCMBUTTONUP 0x00A8
#define WM_NCMBUTTONDBLCLK 0x00A9
#define WM_MOUSEFIRST 0x0200
#define WM_MOUSEMOVE0x0200
#define WM_LBUTTONDOWN 0x0201
#define WM_LBUTTONUP0x0202
#define WM_LBUTTONDBLCLK0x0203
#define WM_RBUTTONDOWN 0x0204
#define WM_RBUTTONUP0x0205
#define WM_RBUTTONDBLCLK0x0206
#define WM_MBUTTONDOWN 0x0207
#define WM_MBUTTONUP0x0208
#define WM_MBUTTONDBLCLK0x0209

#if(_WIN32_WINNT >= 0x0400)
#define WM_MOUSEWHEEL 0x020A
#endif /* _WIN32_WINNT >= 0x0400 */
#if (_WIN32_WINNT < 0x0400)
#define WM_MOUSELAST0x0209
#else
#define WM_MOUSELAST0x020A
#endif /* if (_WIN32_WINNT < 0x0400) */

#if(_WIN32_WINNT >= 0x0400)
#define WM_MOUSEHOVER 0x02A1
#define WM_MOUSELEAVE 0x02A3
#endif /* _WIN32_WINNT >= 0x0400 */

typedef struct tagMOUSEHOOKSTRUCT {
POINT pt;
HWND hwnd;
UINT wHitTestCode;
DWORD dwExtraInfo;
} MOUSEHOOKSTRUCT, FAR *LPMOUSEHOOKSTRUCT, *PMOUSEHOOKSTRUCT;

typedef struct tagPOINT {
LONG x;
LONG y;
} POINT;

WH_MSGFILTER

这个钩子在对话框、消息框、滚动条、菜单条收到消息时,或 ALT+TAB、ALT+ESC 等组合键在钩子活动中被按下时调用,不过测试时发现组合键不会引用钩子调用。因为这个钩子是指定线程的,因此它可以在程序或 DLL 中运行良好。它的回调过程将会收到 lParam 指向的消息数据,还有以下几个钩子代码表示不同的状态,还有些未使用的就不罗列了:

1
2
3
4
5
6
#define MSGF_DIALOGBOX  0 // 消息框或对话框消息
#define MSGF_MENU 2 // 菜单条消息
#define MSGF_MOVE 3 // 移动窗口消息
#define MSGF_SIZE 4 // 调整窗口大小
#define MSGF_SCROLLBAR 5 // 滚动条消息
#define MSGF_NEXTWINDOW 6 // 即将替换为下一个窗口

WH_SYSMSGFILTER

相似 WH_MSGFILTER 钩子,它要更早执行,因为是系统级别的。因此通过返回 TRUE,可以忽略掉 WH_MSGFILTER 钩子。

WH_SHELL

外壳钩子发生在顶级窗口消息中,因此也是要指定线程 ID 的,钩子只在属于线程的窗口消息中引发。是一个通知钩子,因此不能更改事件消息,在 wParam 参数中包含了窗口的句柄。部分钩子代码有如下值定义:

1
2
3
4
5
6
7
8
9
#define HSHELL_WINDOWCREATED1 // 窗口已经创建消息
#define HSHELL_WINDOWDESTROYED 2 // 窗口即将解构
#define HSHELL_ACTIVATESHELLWINDOW 3 // 激活shell主窗口
#define HSHELL_WINDOWACTIVATED 4 // 窗口已切换
#define HSHELL_GETMINRECT 5 // 系统需要该窗口被最小化时的矩形坐标
#define HSHELL_REDRAW 6 // 任务条上的标题已被重画
#define HSHELL_TASKMAN 7 // 用户已选择其任务列表
#define HSHELL_LANGUAGE 8 // 键盘语言被改变或者一个新的键盘布局被加载
#define HSHELL_ACCESSIBILITYSTATE 11 // NT5.0或以上版本有效,指示可访问性已改变

记录钩子

JOURNAL,是日志记录的意思,相关的钩子也是和记录和回放事件有关的,因为它是系统全局的钩子,影响所有程序,因而负作用更多。为此系统提供了几种默认的的按键来清除这些钩子,有 CTRL+ESC、 ALT+ESC 和 CTRL+ALT+DEL。然后,系统通过一条 WM_CANCELJOURNAL 消息通知程序被挂了日志钩子。这个消息并没指定窗口句柄,因此没有窗口过程会收到这样的消息分配。一个好方法就是使用 WH_GETMESSAGE 钩子来截取这条消息。当然提供一个取消钩子的方法更重要,如通过 VK_CANCEL 即 CTRL+BREAK。下面是两个日志相关钩子。

WH_JOURNALRECORD

记录钩子,系统从队列中移除鼠标及键盘消息时引发,除了回放钩子的消息外。钩子可以处理,但不可以修改或丢弃消息,这是因为记录已经保存在磁盘或内存中。目前只实现 HC_ACTION 这个有效钩子代码。lParam 参数指向一个 ENVENTMSG 结构体,通常的做法是将这些数据存储起来,然后再通过回放钩子将这些数据形成回放动作。注意它只能是全局的系统钩子。

1
2
3
4
5
6
7
8
typedef struct tagEVENTMSG {
UINT message; // 消息ID,如WM_MOUSEMOVE
UINT paramL;
UINT paramH;
DWORD time; // 消息发生的系统时间,GetTickCount的返回值
HWND hwnd;
} EVENTMSG;
typedef struct tagEVENTMSG *PEVENTMSG, NEAR *NPEVENTMSG, FAR *LPEVENTMSG;

如果是键盘消息,paramL 高字节放有扫描码,低字节放有虚拟键值,paramH 则包含重复次数、Bit 15 指示扩展键是否等内容;如果是鼠标消息,paramL 和 paramH 则是 xy 坐标。

WH_JOURNALPLAYBACK

回放钩子在回放记录钩子的数据,或给其它程序发送事件消息时使用。当回放钩子一挂接,系统就会忽略鼠标的移动,其它的键盘和鼠标事件消息将会在回放钩子撤除后才会进入队列。钩子代码有以下两个:

HC_GETNEXT,在访问线程消息时引发,系统会使用相同的消息调用多次。lParam 指定一个 EVENTMSG 结构体,回调过程需要将已经记录的数据填回到这个结构体中,通常是直接拷贝已在记录钩子保存的数据。系统需要两个参数来处理钩子提供的回放数据,一个是等待处理这条消息的时间,另一个是在什么时候处理这条消息。通常等待时间可以使用两个相间的消息时间的差值,而指定处理时间则通过等待时间和当前 GetTickCount 返回值相加得到。如果想要加速回放,设置等待时间为 0 就可以了,等待时间通过钩子回调返回给系统。

HC_SKIP,当系统处理完一个回放记录时引发,此时应当准备下一个回放记录,当所胡记录都回放完了,就应该清除回放钩子,以使程序回到正常工作状态。

WH_CBT

这可以说是最最长篇的一个钩子,它有如下几种钩子代码:

1
2
3
4
5
6
7
8
9
10
# define HCBT_MOVESIZE 0
#define HCBT_MINMAX 1
#define HCBT_QS 2
#define HCBT_CREATEWND 3
#define HCBT_DESTROYWND 4
#define HCBT_ACTIVATE 5
#define HCBT_CLICKSKIPPED 6
#define HCBT_KEYSKIPPED 7
#define HCBT_SYSCOMMAND 8
#define HCBT_SETFOCUS 9

HCBT_ACTIVATE,激活窗口,返回 TRUE 阻止焦点以禁止激活。对应的参数,wParam 指向正在激活的窗口的句柄,lParam 指向一个结构体:

1
2
3
4
5
typedef struct tagCBTACTIVATESTRUCT
{
BOOL fMouse; // TRUE if activation results from a mouse click
HWND hWndActive; // the currently active window's handle.
} CBTACTIVATESTRUCT, *LPCBTACTIVATESTRUCT;

HCBT_CREATEWND,窗口创建中,但程序的 WM_GETMINMAXINFO, WM_NCCREATE, WM_CREATE 消息还没有发出,因此返回 TRUE 可以禁止窗体的创建。参数 wParam 是窗口的句柄,lParam 指向一个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct CBT_CREATEWND{
struct tagCREATESTRUCT *lpcs;
HWND hwndInsertAfter; // The window will be followed, in Z-order.
} CBT_CREATEWND, *LPCBT_CREATEWND;

typedef struct tagCREATESTRUCTA {
LPVOID lpCreateParams;
HINSTANCE hInstance;
HMENU hMenu;
HWND hwndParent;
int cy;
int cx;
int y;
int x;
LONG style;
LPCSTR lpszName;
LPCSTR lpszClass;
DWORD dwExStyle;
} CREATESTRUCTA, *LPCREATESTRUCTA;

HCBT_DESTROYWND,窗口解构中,但 WM_DESTROY 消息还没有发出,可以通过返回 TRUE 来阻止窗口解构。参数 wParam 是窗口的句柄,lParam 为 0L。

HCBT_MINMAX 最大最小化时引发,返回 TRUE 可以禁止动作。参数 wParam 是窗口的句柄,lParam 是一个 ShowWindow 常数 SW_* 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define SW_HIDE 0
#define SW_SHOWNORMAL 1
#define SW_NORMAL 1
#define SW_SHOWMINIMIZED2
#define SW_SHOWMAXIMIZED3
#define SW_MAXIMIZE 3
#define SW_SHOWNOACTIVATE 4
#define SW_SHOW 5
#define SW_MINIMIZE 6
#define SW_SHOWMINNOACTIVE 7
#define SW_SHOWNA 8
#define SW_RESTORE 9
#define SW_SHOWDEFAULT 10
#define SW_MAX 10

HCBT_MOVESIZE,移动或调整窗口大小时引发,返回 TRUE 可以禁止动作。参数 wParam 是窗口的句柄,lParam 是一个矩形 RECT 结构体指针,文章开始处已经出现过。

HCBT_SYSCOMMAND,系统菜单命令,即左上角的弹出菜单引发,返回 TRUE 可以禁止系统菜单弹出。WH_CBT 钩子是由 DefWindowsProc 过程调用的,如果没有发送 WH_SYSCOMMAND 过来,就不会有这个钩子的调用。参数 wParam 包含即将执行行的系统命令,即鼠标划过的菜单项,lParam 的低位字和高位字存放鼠标的 xy 坐标。如果 wParam 是 SC_HOTKEY,那么 lParam 就包含热键适用的窗口句柄。其它系统命令列表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define SC_SIZE 0xF000 // 调整窗口大小
#define SC_MOVE 0xF010 // 移动窗口位置
#define SC_MINIMIZE 0xF020 // 最小化窗口
#define SC_MAXIMIZE 0xF030 // 最大化窗口
#define SC_NEXTWINDOW 0xF040 // 下一个窗口
#define SC_PREVWINDOW 0xF050 // 上一个窗口
#define SC_CLOSE0xF060 // 关闭命令
#define SC_VSCROLL 0xF070 // 垂直滚动
#define SC_HSCROLL 0xF080 // 水平滚动
#define SC_MOUSEMENU0xF090 // 通过鼠标单击获取菜单
#define SC_KEYMENU 0xF100 // 通过按键获取菜单
#define SC_ARRANGE 0xF110
#define SC_RESTORE 0xF120 // 还原窗口位置状态
#define SC_TASKLIST 0xF130 // 执行或激活任务管理程序
#define SC_SCREENSAVE 0xF140 // 执行屏保程序
#define SC_HOTKEY 0xF150
#define SC_DEFAULT 0xF160
#define SC_MONITORPOWER 0xF170
#define SC_CONTEXTHELP 0xF180
#define SC_SEPARATOR0xF00F

HCBT_SETFOCUS,当窗口激活就要取得焦点时引起,可以通过返回 TRUE 来阻止窗口取得焦点。参数 wParam 有窗口的句柄,lParam 失去焦点的窗口句柄。

HCBT_QS,当移动或调整窗口大小过程中,一个 WM_QUEUESYNC 消息从系统队列移除时引起,其它任何情况不会发生。参数 wParam 和 lParam 都为 0。

HCBT_CLICKSKIPPED,当鼠标事件要从队列移除时引发,也就是说这个鼠标事件是无效的,通常是日志钩子回放时引起的。wParam 包含鼠标事的类型,如 WM_LBUTTONDOWN,lParam 包含一个结构体的指针:

1
2
3
4
5
6
typedef struct tagMOUSEHOOKSTRUCT {
POINT pt; // Location of mouse in screen coordinates
HWND hwnd; // Window that receives this message
UINT wHitTestCode; // The result of hit-testing (HT_*)
DWORD dwExtraInfo;// Extra info associated with the current message
} MOUSEHOOKSTRUCT, FAR *LPMOUSEHOOKSTRUCT, *PMOUSEHOOKSTRUCT;

HCBT_KEYSKIPPED,和前者一样,路过或从队列中移除消息都会引发。wParam 包含虚拟键值,lParam 包含其它属性,和消息环的键盘事件消息一样。

WM_QUEUESYNC,CBT 编程通常都要响应主程序的这些键盘和鼠标事件,例如,在确定一个对话框后,CBT 程序可能会要向主程序输入几个字符。通过鼠标钩子可以用来确定有没有按下 OK 按钮,根据结果来决定要输入哪些字符到主程序, 这样 CBT 程序就要等到按下 OK 按钮后的过程处理完成后。那么 CBT 程序就可以通过 WM_QUEUESYNC 消息来监视主程序,看看动作何时完成,下面是两个判断步骤:

CBT 程序等待直到收到 WH_CBT 钩子,带 HCBT_CLICKSKIPPED 或 HCBT_KEYSKIPPED 代码的钩子。这会在主程序把消息从系统队列中移除时发生。当 CBT 安装回放钩子时,直到收到 HCBT_CLICKSKIPPED 或 HCBT_KEYSKIPPED 代码才能安装成功。回放钩子发送 WM_QUEUESYNC 消息给 CBT 程序,CBT 程序可以响应这个事件,比如说输入一些字符到主程序。

参考资料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Visual Studio MSDN Library 6.0 SPY Code Sample
Win32 Hooks -MSDN 99, Kyle Marsh July 29, 1993
Using Hooks, MSDN: http://msdn.microsoft.com/en-us/library/ms644960%28v=vs.85%29.aspx
"Load Your 32-bit DLL into Another Process's Address Space Using INJLIB" Jeffrey Ritcher, MSJ May 1994
"An In-Depth Look into the Win32 PE file format" , part 1, Matt Pietrek, MSJ February 2002
"An In-Depth Look into the Win32 PE file format" , part 2, Matt Pietrek, MSJ March 2002
DLLs in Win32 by Randy Kath,MSDN September 15, 1992
DLLs for Beginners by Debabrata Sarma,MSDN 1996
Advanced Windows NT, The Developer's Guide to the Win32 Application Programming Interface by J. Richter
Detecting Windows NT/2K process execution: http://www.codeproject.com/Articles/2018/Detecting-Windows-NT-2K-process-execution
API hooking revealed: http://www.codeproject.com/Articles/2082/API-hooking-revealed
API Hook完全手册 : http://blog.csdn.net/ATField/article/details/1507122
让EXE导出函数,看雪论坛: http://bbs.pediy.com/showthread.php?t=56840
如何与应用程序或其他 DLL 共享自己 DLL 中的数据?: http://msdn.microsoft.com/zh-cn/library/h90dkhs0
DLL导出变量: http://blog.csdn.net/henry000/article/details/6852521

修改文件的 IMAGE_NT_HEADERS.FileHeader.Characteristics 为 IMAGE_FILE_DLL。

版权声明:自由转载 - 非商用 - 非衍生 - 保持署名 | Creative Commons BY-NC-ND 3.0
ByNo CommercialNo Derived
建档时间:2013 年 11 月 15 日修改时间:2013 年 11 月 22 日
————————————————
版权声明:本文为 CSDN 博主「Jimbo」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/winsenjiansbomber/article/details/16891189

References