逆向工程核心原理第二十一章
Windows消息钩取
钩子(Hook)
英文Hook一词,翻成中文是"钩子"、"鱼钩"的意思,泛指勾取所需东西而使用的一切工具。"钩子"这一基本含义被延伸发展为"偷看或截取信息时所用的手段或工具"
消息钩子
Windows操作系统向用户提供GUI(Graphic User Interface,图形用户界面),它事件驱动(Event Driven)方式工作。在操作系统中借助键盘、鼠标,选择菜单、按钮,以及移动鼠标、改遍窗口大小与位置等都是事件(Event)。发生这样的事件时,OS会把事先定义好的消息发送给相应的应用程序,应用程序分析收到的信息后执行相应的动作。也就是说,敲击键盘时,消息会从OS移动到应用程序。所谓的"消息钩子"就在此间偷看这些信息。
常规Windows消息流
- 发生键盘输入事件时,WM_KEYDOWN消息被添加到[OS message queue]
- OS判断哪个应用程序中发生了事件,然后从[OS message queue]取出消息,添加到相应应用程序的[application message queue]中
- 应用程序(如记事本)监视自身的[application message queue],发现新添加的WM_KEYDOWN消息后,调用相应的事件处理程序处理
如下图所示,OS消息队列与应用程序消息队列之间存在一条"钩链"(Hook Chain),设置好键盘消息钩子之后,处于"钩链"中的键盘消息钩子会比应用程序先看到相应信息。在键盘消息钩子函数内部,除了可以查看消息之外,还可以修改消息本身,而且还能对消息实施拦截,阻止消息传递。
像这样的消息钩子功能是Windows操作系统提供的基本功能,其中最具代表性的是MS Visual Studio中提供的SPY++,它是一个功能十分强大的消息钩起程序,能够查看操作系统中来往的所有消息。
SetWindowsHookEx()
使用SetWindowsHookEx() API可轻松实现消息钩子,SetWindowsHookEx() API的定义如下
1
2
3
4
5
6HHOOK SetWindowsHookEx(
int idHook; // hook type
HOOKPROC lpfn; // hook procedure
HINSTANCE hMod; // hook procedure所属的DLL句柄
DWORD dwThreadID // 想要挂钩的线程ID
);钩子过程(hook procedure)是由操作系统调用的回调函数。安装消息"钩子"时,"钩子"过程需要存在于某个DLL内部,且该DLL的实例句柄(instance handle)即是hMod。
若dwThreadID参数被设置为0,则安装的钩子为"全局钩子"(Global Hook),它会影响到运行中的(以及以后要运行的)所有进程
键盘消息钩取练习
下面做一个简单的键盘消息钩取练习,以进一步加深对前面内容的理解
KeyHook.dll文件是一个含有钩子的过程(KeyboardProc)的DLL文件。HooMain.exe是最先加载KeyHook.dll并安装键盘钩子的程序。HookMain.exe加载KeyHook.dll文件后使用SetWindowsHookEx()安装键盘钩子(KeyboardProc)。若其他进程(explore.exe、iexplore.exe、notepad.exe等)中发生键盘输入事件,OS就会强制将KeyHook.dll加载到相应进程的内存,然后调用KeyboardProc()函数。
练习示例HooKMain.exe
从《逆向工程核心原理》书的官方githubhttps://github.com/reversecore/book.git,下载HookMain的源代码(有可执行程序,但是64位系统的不能使用,64位系统使用32位的Hook程序会直接卡死,所以需要自己编译测试)
一开始直接用32位的编译程序在64位的win10上运行,发现一直卡死。搜索后,在这篇文章找到了原因windows10 记事本进程 键盘消息钩子 dll注入,原因是:SetWindowsHookEx的官方文档提到了这个API只能用于32位程序注入32位程序或者64位注入64位程序,否则会直接卡死。
下载源代码后,用visual studio打开HookMain和KeyHook项目后,新建一个x64平台的活动解决方案,然后生成即可
编译生成好HookMain.exe和KeyHook.dll后,将两者放在一个文件夹下,运行HookMain.exe,安装键盘钩子
接着运行notepad.exe,使用Process Explore查看notepad.exe进程,在Process Explore选项卡中选择View DLLs(Ctrl + D)即可查看进程加载了哪些DLL文件,如下图所示,可以看到KeyHook.dll已经注入到notepad.exe进程中
在Process Explorer中检索(Find Handle or DLL ... Ctrl + Shift + F)注入KeyHook.dll的所有进程,如下图所示,一个进程开始运行并发生键盘事件时,KeyHook.dll就会注入其中(但忽视键盘事件的仅有notepad.exe进程,其他进程会正常处理键盘事件)
按"q",拆除键盘钩子,在notepad.exe中使用键盘输入,可以发现又能正常输入了。在Process Explorer中检索KeyHook会发现,没有进程加载KeyHook.dll
拆除键盘钩子后,相关进程就会将KeyHook.dll文件全部卸载
分析源代码
下载书中的源代码,首先看一下HookMain.cpp的源代码
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
typedef void (*PFN_HOOKSTART)();
typedef void (*PFN_HOOKSTOP)();
void main()
{
HMODULE hDll = NULL;
PFN_HOOKSTART HookStart = NULL;
PFN_HOOKSTOP HookStop = NULL;
char ch = 0;
// 加载 KeyHook.dll
hDll = LoadLibraryA(DEF_DLL_NAME);
if( hDll == NULL )
{
printf("LoadLibrary(%s) failed!!! [%d]", DEF_DLL_NAME, GetLastError());
return;
}
// 获取导出函数地址
HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);
// 启动Hook
HookStart();
// 等待用户输入"q"
printf("press 'q' to quit!\n");
while( _getch() != 'q' );
// 停止Hook
HookStop();
// 卸载KeyHook.dll
FreeLibrary(hDll);
}HookMain.exe的源代码非常简单,先加载KeyHook.dll文件,然后调用HookStart()函数开始钩取,用户输入"q"时,调用HookStop()函数终止钩取。
接下来看看KeyHook.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
HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
HWND g_hWnd = NULL;
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
switch( dwReason )
{
case DLL_PROCESS_ATTACH:
g_hInstance = hinstDLL;
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
char szPath[MAX_PATH] = {0,};
char *p = NULL;
if( nCode >= 0 )
{
// bit 31 : 0 => press, 1 => release
if( !(lParam & 0x80000000) ) // 释放键盘按键时
{
GetModuleFileNameA(NULL, szPath, MAX_PATH);
p = strrchr(szPath, '\\');
// 比较当前进程名称,若为notepad.exe则消息不会传递给应用程序(或下一个"钩子")
if( !_stricmp(p + 1, DEF_PROCESS_NAME) )
return 1;
}
}
// 若非notepad.exe,则调用CallNextHooxEx()函数,将消息传递给应用程序(或下一个"钩子")
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
extern "C" {
__declspec(dllexport) void HookStart()
{
g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
}
__declspec(dllexport) void HookStop()
{
if( g_hHook )
{
UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
}
}DLL代码部分,调用导出函数HookStart()时,SetWindowsHookEx()函数就会将KeyboardProc()添加到键盘钩链
MSDN中对KeyboardProc函数的定义如下:
1
2
3
4
5LRESULT CALLBACK KeyboardProc(
int code, // HC_ACTION(0), HC_NOREMOVE(3)
WPARAM wParam, // virtual-key code
LPARAM lParam // extra information
)安装好键盘"钩子"后,无论哪个进程,只要发生键盘输入事件,OS就会强制将KeyHook.dll注入相应进程。加载了KeyHook.dll的进程中,发生键盘事件时,就会首先调用执行KeyHook.KeyboardProc()
KeyboardProc()函数作用,判断当前发生键盘事件的进程是否为notepad.exe,如果是notepad.exe,那么返回1(拦截键盘事件,不往下传递),否则将键盘事件原封不动地传递给应用程序(或者下一个钩链)
调试练习
因为上面编译的HookMain.exe和KeyHook.dll都是64位的,Ollydbg不支持64可执行文件,所以这里使用x64dbg进行调试
因为HookMain.exe有字符串"press 'q' to quit!",字符串,所以这里可以使用搜索字符串定位到核心代码,如下图所示,图中代码就是HookMain.exe到main()函数
可以看到64位程序的寄存器名称都变成了R开头,但跟32位的寄存器大同小异。在"140069910"地址处下断点,然后运行程序,到断点处停下来,开始调试。从断点处开始依次跟踪调试代码,可以了解main()中的主要代码流。
先在"14006994E"地址处调用LoadLibrary("KeyHook.dll"),然后在"1400699A7"地址处调用HookStart()函数,该处执行后notepad.exe的键盘事件就会被拦截
F7步入查看HookStart()函数内容,可以看到在"1800698F1"调用了SetWindowsHookExW()函数
调试notepad.exe进程内的KeyHook.dll
先启动HookMain.exe
用x64dbg调试notepad.exe,F9让notepad.exe正常运行,然后在x64dbg的选项中勾选DLL入口,这样当新的DLL装入时,调试器会在DLL入口处停止(调试完后再取消勾选)。
在notepad.exe界面中,键盘输入触发KeyHook.dll装载。x64dbg中选择内存布局选项卡,可以看到keyhook.dll被装载在180000000地址处
点击CPU选项卡,回到主界面,可以看到,此时调试器停在KeyHook.dll的EP,
如下图所示,F7跟进"18006A41C"处的调用dllmain_dispatch,从函数名来看,这个函数就是DLLMain函数的转发的函数
继续调试,可以找到DLLMain函数调用的地方,如下图所示,在"18006A243"处调用了KeyHook.dll的DLLMain函数
F7步入"18006A243"该处的调用,就可以看到DLLMain函数,如下图所示
KeyHook.dll的KeyboardProc函数则可以通过搜索字符串"notepad.exe"来定位