第7章 栈帧

本文最后更新于:2022年5月19日 晚上

第21章Windows消息钩取

21.1 钩子

英文Hook—词,翻成中文是“钩子”、“鱼钩”的意思,泛指钓取所需东西而使用的一切工具。“钩子”这一基本含义延伸发展为“偷看或截取信息时所用的手段或工具”。下面举例向各位进一步说明“钩子”这一概念。

“钩子”的概念
假设有一个非常重要的军事设施,其外围设置了3层岗哨以进行保护。外部人员若想进入,需要经过3层岗哨复杂的检查程序(身份确认、随身物品查验、访问事由说明等)。若间谍在通往该军事设施的道路上私设一个岗哨,经过该岗哨的人员未起疑心,通 过时履行同样的检查程序,那么间谍就可以坐享其成,轻松获取(甚至可以操纵)来往该岗哨的所有信息。

像这样,为了偷看或截取来往信息而在中间设置岗哨的行为称为“挂钩”(或“安装钩子”),实际上,偷看或操作信息的行为就是人们常说的“钩取”(Hooking)。

“钩取”技术广泛应用于计算机领域。其实,我们不仅可以查看来往于 “OS-应用程序-用户”之间的全部信息,也可以操作它们,并且神不知鬼不觉。具体方法有很多,其中最基本的是“消 息钩子”(Message Hook ),下面会详细介绍。

“钓取”是代码逆向分析中非常重要且有趣的主题,后面会逐一介绍各种“钩取”方法。

21.2 消息钩子

Windows操作系统向用户提供GUI (Graphic User Interface,图形用户界面),它以事件驱动(Event Driven)方式工作。在操作系统中借助键盘、鼠标,选择菜单、按钮,以及移动鼠标、改变窗口大小与位置等都是事件(Event)。发生这样的事件时,OS会把事先定义好的消息发送给相应的应用程序,应用程序分析收到的信息后执行相应动作(上述过程在《Windows程序设计》一书中有详尽说明)。也就是说,敲击键盘时,消息会从OS移动到应用程序。所谓的“消息钩子”就在此间偷看这些信息。为了帮助各位进一步理解它,下面以键盘消息为例说明。请看图21-1。

先讲解常规Windows消息流。

□发生键盘输入事件时,WM_KEYDOWN消息被添加到[OS message queue]。

□ OS判断哪个应用程序中发生了事件,然后从[OS message queue]取出消息,添加到相应应用程序[application message queue]中。

□应用程序(如记事本)监视自身的[application message queue],发现新添加的WM_KEYDOWN消息后,调用相应的事件处理程序处理。

图21-1 消息钩取工作原理

正如在图21-1中看到的一样,OS消息队列与应用程序消息队列之间存在一条“钩链”(Hook Chain),设置好键盘消息钩子之后,处于“钩链”中的键盘消息钩子会比应用程序先看到相应信息。在键盘消息钩子函数的内部,除了可以查看消息之外,还可以修改消息本身,而且还能对消息实施拦截,阻止消息传递。

可以同时设置多个相同的键盘消息钩子。按照设置顺序依次调用这些钩子,它们 组成的链条称为“钩链”。

像这样的消息钩子功能是Windows操作系统提供的基本功能,其中最具代表性的是MS Visual Studio中提供的SPY++,它是一个功能十分强大的消息钩取程序,能够查看操作系统中来往的所有消息。

21.3 SetWindowsHookEx()

使用SetWindowsHookEx() API可轻松实现消息钩子,SetWindowsHookEx() API的定义如下所示。

1
2
3
4
5
6
7
8
9
10
HHOOK SetWindowsHookEx(
int idHook,
// type of hook to install
HOOKPROC lpfn,
// address of hook procedure
HINSTANCE hMod,
// handle of application instance
DWORD dwThreadId
// identity of thread to install hook for
);

钩子过程(hook procedure)是由操作系统调用的回调函数。安装消息“钩子“时,“钩子”过程需要存在于某个DLL内部,且该DLL的示例句柄(instance handle )即是hMod。

若dwThreadID参数被设置为0,则安装的钓子为“全局钓子”(Global Hook ),它会影响到运行中的(以及以后要运行的)所有进程。

像这样,使用SetWindowsHookExO设置好钩子之后,在某个进程中生成指定消息时,操作系统会将相关的DLL文件强制注入(injection)相应进程,然后调用注册的“钩子”过程。注入 进程时用户几乎不需要做什么,非常方便。

21.4 键盘消息钩取练习

本节将做一个简单的键盘消息钩取练习,以进一步加深各位对前面内容的理解。请看图21-2。

图21-2 键盘消息钩取

KeyHook.dll文件是一个含有钩子过程(KeyboardProc )的DLL文件。HookMain.exe是最先加载KeyHook.dll并安装键盘钩子的程序。HookMain.exe加载KeyHook.dll文件后使用SetWindowsHookEx()安装键盘钩子(KeyboardProc)。若其他进程(explorer.exe、iexplore.exe、notepad.exe等)中发生键盘输入事件,OS就会强制将KeyHook.dll加载到相应进程的内存,然后调用KeyboardProc()函数。

这里需要注意的一点是,OS会将KeyHooLdll强制加载到发生键盘输入事件的所有进程。换言之,消息钩取技术常常被用作一种DLL注入技术(后面会单独讲解DLL注入的相关内容)

21.4.1 练习示例 HookMain.exe

本节通过示例来练习一下键盘钩取技术,拦截notepad.exe进程的键盘消息,使之无法显示在记事本中。

运行HookMain.exe - 安装键盘钩子

首先运行HookMain.exe程序,如图21-3所示。

图21-3 运行HookMain.exe

运行HookMain.exe程序后,输岀”press ‘q’ to quit!”信息,提示在HookMain.exe程序中输入”q”即可停止键盘钩取。

运行Notepad.exe程序

当前系统中已安装好键盘钩子。运行Notepad.exe,用键盘输入。

图2-14 Notepad.exe进程忽视键盘输人

如图所示,Notepad.exe进程忽视了用户的键盘c。使用Process Explorer查看notepad.exe进程,可以看到KeyHook.dll已经加载其中(参考图2-14)。

在Process Explorer中检索注入KeyHook.dll的所有进程,如图21-5所示。一个进程开始运行并发生键盘事件时,KeyHook.dll就会注入其中(但其实忽视键盘事件的仅有notepad.exe进程,其他进程会正常处理键盘事件)。

图21-5 注入KeyHook.dll的所有进程

HookMain.exe终止-拆除键盘钩子

在HookMain.exe程序中输入“q”命令,HookMain.exe将拆除键盘钩子,并终止运行。

拆除键盘钩子后,在notepad.exe (记事本)中使用键盘输入,可以发现记事本又能正常接收了。在Process Explorer中检索KeyHook.dll会发现,无任何一个进程加载KeyHook.dll。

拆除键盘钩子后,相关进程就会将KeyHook.dll文件全部卸载(Unloading )。

21.4.2 分析源代码

下面分析一下示例的源代码。

示例是使用 MS Visual C++2010 Express Edition 编写的,已在 Windows XP/7 (32 位)环境中通过测试。为便于讲解,我已经去除了示例代码中的返回值/错误处理语句。

HookMain.cpp
首先看一下HookMain.exe文件的源代码(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
#include "stdio.h"
#include "conio.h"
#include "windows.h"

#define DEF_DLL_NAME "KeyHook.dll"
#define DEF_HOOKSTART "HookStart"
#define DEF_HOOKSTOP "HookStop"

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);

// 开始钩取
HookStart();

// 等待直到用户输入"q"
printf("press 'q' to quit!\n");
while( _getch() != 'q' ) ;

// 终止钩取
HookStop();

// 卸载KeyHook.dll
FreeLibrary(hDll);
}

源代码非常简单。先加载KeyHook.dll文件,然后调用HookStart()函数开始钩取,用户输入“q” 时,调用HookStop()函数终止钩取。重要代码处添加了注释,认真查看就能轻松理解,不会遇到什么困难。

KeyHook.dll.cpp
接下来继续看KeyHook.dll文件的源代码(KeyHook.dll.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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include "stdio.h"
#include "windows.h"

#define DEF_PROCESS_NAME "notepad.exe"

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,则调用CallNextHookEx()函数,将消息传递给应用程序(或下一个”钩子”)。
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

#ifdef __cplusplus
extern "C" {
#endif
__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;
}
}
#ifdef __cplusplus
}
#endif

DLL代码也非常简单。调用导出函数HookStart()时,SetWindowsHookEx()函数就会将KeyboardProc()添加到键盘钩链。

MSDN中对KeyboardProc函数的定义如下:

1
2
3
4
5
6
7
8
LRESULT CALLBACK KeyboardProc(
int code,
// hook code
WPARAM wParam,
// virtual-key code
LPARAM lParam
// keystroke-message information
);

上面3个参数中,wParam指用户按下的键盘按键的虚拟键值(virtual key code)。对键盘这一硬件而言,英文字母“A”与“a”具有完全相同的虚拟键值。参数IParam根据不同的位具有多种不同的含义(repeat count、scan code、extended-key flag、context code、previous key - state flag、transition-state flag )。使用 ToAscii() API 函数可以获得实际按下的键盘的ASCII值。

安装好键盘“钩子”后,无论哪个进程,只要发生键盘输入事件,OS就会强制将KeyHook.dll注入相应进程。加载了 KeyHook.dll的进程中,发生键盘事件时会首先调用执行KeyHook.KeyboardProc()。

函数中发生键盘输入事件时,就会比较当前进程的名称与“notepad.exe”字符串,若相同,则返回1,终止KeyboardProc()函数,这意味着截获且删除消息。这样,键盘消息就不会传递到notepad.exe程序的消息队列。

因notepad.exe未能接收到任何键盘消息,故无法输出。

除此之外(即当前进程名称非 “notepad.exe” 时),执行return CallNextHookEx(g_hHook,nCode,wParam,lParam);语句,消息会被传递到另一个应用程序或钩链的另一个“钩子”函数。

监视或记录用户键盘输入的程序被称为“键盘记录器”(Key Logger)。有些键盘记录器本身是PC恶意代码,通过钩取键盘消息,在PC用户不知情的情况下盗走用户的键盘输入,其工作原理与KeyHook.dll的工作原理基本一致。

21.5 调试练习

本节将学习有关Windows消息钩取调试的技术。

21.5.1 调试 HookMain.exe

先调试用来安装键盘钩子的HookMain.exe。请使用OllyDbg打开HookMain.exe文件,如图21-8所示。

图21-8 HookMain.exe的EP代码

图21-8显示的是HookMain.exe的EP代码,它是典型的VC++启动函数,其中最受关注的是开始进行键盘钩取的部分。

查找核心代码

有几种方法可以帮助我们找到关注的核心代码:

□逐行跟踪。

□检索相关API。

□检索相关字符串。

第一种方法是程序无法正常运行或难以预测时使用的下策,此处略去不谈。这样就剩下后面2种方法(检索API或字符串)了。

由于已经运行过HookMain.exe程序,我们知道了该程序的功能(键盘钩取)与输岀的字符串,所以下面要使用检索字符串(图21-3的”press ‘q’ to quit!”)的方法。引用该字符串代码的前后就是我们关注的代码。在OllyDbg的代码窗口中,选择鼠标右键菜单中的Search for - All referenced text strings项。

弹出字符串窗口,如图21-10所示。

图21-10 Text strings referenced in HookMain

从图21-10中可以看到,40104D地址处的指令引用了要査找的字符串。双击字符串,转到相应地址处(40104D)。

图21-11中显示的代码其实就是HookMain.exe程序的main()函数(借助OllyDbg的字符串检索功能即可轻松找到)。

图21-11 main()函数

调试main()函数

在401000地址处设置断点,然后运行程序,到断点处停下来,开始调试。从断点开始依次跟踪调试代码,可以了解main()中的主要代码流。先在401006地址处调用LoadLibrary(KeyHook.dll),然后由40104B地址处的CALL EBX命令调用KeyHook.dll.HookStart()函数。跟踪40104B地址处的CALL EBX命令(StepInto(F7)),出现图21-12所示的代码。

DLL加载的地址本该是0x1000000,这里是被别的DLL占用了位置所以重定位到了0x290000

下面Notepad被加载时定位到了0x640000和0x940000,可以看偏移来确定函数地址,偏移是不会变得。

图21-12 KeyHook.HookStart()函数

图21-12中的代码是被加载到HookMain.exe进程中的KeyHook.dll的HookStart()函数(请确认一下图中的地址区域)。在2910DF地址处可以看到CALL SetWindowsHookExW()指令,其上方2910D8与2910DD地址处的2条PUSH指令用于把SetWindowsHookExW() API的第1、2两个参数压入栈。

SetWindowsHookExW()API的第一个参数(idHook )值为WH_KEYBOARD(2),第二个参数(lpfn)值为291020,该值即是钩子过程的地址。后面调试KeyHook.dll时再仔细看该地址。HookMain.exe的main()函数( 401000 )的其余代码接收到用户输入的“q”命令后终止钩取。以上内容非常简单,希望各位亲自调试。

21.5.2 调试 Notepad.exe 进程内的 KeyHook.dll

本小节将调试KeyHook.dll中的钩子过程,此时KeyHook.dll已被注入notepad.exe进程。首先使用OllyDbg打开Notepad.exe程序(也可以使用Attach命令打开运行中的notepad.exe进程)。通过OllyDbg中的“运行”(F9)命令使notepad.exe进程正常运行。

如图21-14所示,在OllyDbg的Debugging options(中文为选项菜单->调试设置)中,点选Break on new module(DLL)复选框。

图21 -14 更改OllyDbg选项:Break on new module(DLL)

开启该选项后,每当新的DLL装人被调试(Debuggee )进程时就会自动暂停调试(这在“从DLL注入时开始调试”的情况下非常有用)。此时运行HookMain.exe (参考图21-3)。然后在notepad.exe中使用键盘输入,此时OllyDbg暂停调试,并弹出Executable modules窗口。

如图21-15所示,KeyHook.dll被加载到640000地址处。

图21-15 Executable modules窗口

根据系统环境不同,有时不会先显示KeyHook.dll,而是先加载其他DLL库。此时按(F9)运行键,直到KeyHook.dll加栽完成。有些系统无法正常运行该功能,此时使用OllyDbg 2.0即可保证运行顺畅。

如图21-15所示,双击KeyHook.dll转到KeyHook.dll的EP地址处。由于我们已经知道钩子过程的地址为291020,下面直接转到该地址处(请先在OllyDbg中取消对Break on new module(DLL)项的选择,使其处于“未选中”状态)。

如图21-16所示,向“钩子”过程(641020)设置断点,每当notepad.exe中发生键盘输入事件时,调试就停在该处。

调到这里闪退了,重开后Base变成了0x940000

在栈中可以看到KeyboardProc()函数的参数。最后,将以上操作过程按顺序整理如下:

□用OllyDbg 运行 notepad.exe (或者 Attach 运行中的 notepad.exe );

□开启 Break on new module(DLL)选项;

□运行 KeyLogger.exe— 安装 global keyboard message hook;

□在notepad中使用键盘输入―发生键盘消息事件(按键a输入);

□ KeyLogger.dll 被注入 notepad.exe 进程;

□在OllyDbg中向KeyboardProc (钩子进程)设置断点。

关于KeyboardProc()函数(偏移为0x1020)可以参考前面源代码说明中的相关内容,请各位自己调试。

21.6小结

本章讲解了Windows消息钩取技术与DLL钩子过程调试方法。这些知识在代码逆向分析中起着非常重要的作用,希望各位认真学习并掌握。特别是要反复练习“从DLL的EP代码开始调试”的方法,直到完全掌握。

Q.回调函数(CALLBACK)是什么?

A.简言之,就是某个特定事件发生时被指定调用的函数。窗口Windows过程(WndProc)就是一个典型的回调函数(键盘、鼠标等事件发生时OS会调用注册的窗口过程)。

Q.我是超级菜鸟,几乎什么都不懂,应该从哪儿开始学啊?从C语言开始吗?

A.其实,只有具备一定的Win32编程知识,才能较好地理解示例中的代码(当然也要有一定的C语言知识)。初次接触代码逆向分析时会遇到大量术语,这些术语往往让人一头雾水、不知所措,“这些都是学习代码逆向分析技术的绊脚石”,我(直到几年)之前一直这样想。但是看到那些没有以上知识却依然能够将代码#逆向分析做得很棒的人,我的想法慢慢改变了。

我认识的代码逆向分析人员中,有几个人学习逆向分析技术时根本就不怎么懂C语言,他们学习时每当遇到C语言代码就直接敲一下,不断查找询问,后面就慢慢弄懂了。学习过 程中遇到不懂的术语就记下来(初次看到会觉得难,但见过10次以后就不会这样想了)。遇难而退不可取,反而应该谦虚谨慎、不骄不躁地去吸收更多新知识。他们现在都成为代码逆向分析技术的专家了呢。

Q. declspec函数是什么?

A. declspec是针对编译器的关键字,指出相应函数为导出函数。

Q. SetWindowsHookEx()API为什么在KeyHook.dll内部调用?您说它是安装钩子的API?

A.是的。SetWindowsHookEx() API用于将指定的“钩子”过程注册到钩链中。无论在DLL内部还是外部均可调用(编程时怎么方便怎么来)。

参考

《逆向工程核心原理》 第21章


第7章 栈帧
https://m0ck1ng-b1rd.github.io/1999/02/19/逆向工程核心原理/第21章 Windows消息钩取/
作者
何语灵
发布于
1999年2月19日
许可协议