逆向工程核心原理-21

逆向工程核心原理-第二十一章-Windows信息钩取

突然发现SYJ学长之前做的DLL注入的博客, 非常详细, 后面关于钩取代码的分析和SetWindowsHookEx函数的参数分析都有参考他的博客, 看雪的博客地址: https://bbs.pediy.com/thread-266862.htm

21.1 钩子(Hook)

在计算机中”钩子”的意思是: 偷看或截取信息时所用的手段或工具.

21.2 消息钩子

计算机与用户之间的交互是通过消息(比如点击一个窗口最大化, 就是一个消息, 向程序输入一个字母, 也是一个消息)来互相传送的, 而媒介就是钩子, 所以我们就通过钩子(hook)来获取或者改变这些消息.

下面是常规的Windows消息流:

  • 发生键盘输入事件时, WM_KEYDOWN(按键)消息被添加到OS message queue
  • OS判断哪个程序中发生了事件, 从[OS message queue]中取出消息, 添加到相应程序的[application message queue]中.
  • 应用程序监视自身的[application message queue], 发现了新添加的WM_KEYDOWN消息, 调用相应的事件处理程序处理

OS消息队列应用程序消息队列之间存在一条 ” 钩链 ” (Hook Chain), 处于钩链中的键盘消息钩子会比应用程序先看到相应的消息, 可以查看或者修改消息, 也可以拦截消息.

Untitled

注意链表调用钩子是有顺序的

21.3 SetWindowsHookEx()

使用SetWindowsHookEx()可以实现消息钩子, API定义:

1
2
3
4
5
6
7
HHOOK SetWindowsHookEx(
int idHook, //钩子类型
HOOKPROC Lpfn, //钩子过程
HINSTANCE hMod, //钩子过程所属的DLL句柄
DWORD dwThreadId //想要挂钩的线程ID(0 就是全局钩子, 所有进程满足条件都会被挂钩;
//非0, 只针对一个线程的钩子)
)

各个参数, 返回值(看了SYJ学长的博客, 写的真的非常详细, 下面对钩子函数的注释也是参考了他在看雪中的博客: https://bbs.pediy.com/thread-266862.htm):

  • HHOOK: 返回值, 钩子句柄, 需要保留, 不需要的时候通过UnhookWindWindowsHookEx来卸载掉
  • idHook: 钩子拦截消息的类型, 比如这道题的类型是WH_KEYBOARD
  • Lpfn: 信息的回调函数地址, 一般填函数名
  • hMod: 钩子函数所在的实例的句柄, 对于线程钩子, 该参数为NULL;, 对于系统钩子, 该参数为钩子函数所在的DLL句柄.
  • dwThreadld: 钩子所监视的线程线程号, 可通过GetCurrentThreadld()获取线程号, 对于全局钩子, 该参数为NULL(或0)

钩子过程(hook procedure)是由操作系统调用的回调函数. (回调函数: 某个事件发生时被指定调用的函数).

1
2
3
4
这是StackOverflow某位大神对回调函数间接的表述:  也就是说,函数 F1 调用函数 F2 的时候,
函数 F1 通过参数给 函数 F2 传递了另外一个函数 F3 的指针,在函数 F2 执行的过程中,函数F2
调用了函数 F3,这个动作就叫做回调(Callback),而先被当做指针传入、后面又被回调的函数 F3
就是回调函数。

安装钩子的时候, 钩子需要存在于某个DLL内部, 该DLL的实例句柄即是参数hMod

使用SetWindowsHookEx()设置好钩子后, 某个进程生成指定消息时, 操作系统(注意主语)会将相关的DLL文件强制注入相应进程, 然后调用注册的”钩子”过程(就是使用钩子).

注入过程我们什么都不用干, 由操作系统完成.

下面是设置钩子的流程:

  • 把钩子放到一个DLL中
  • 设置一个程序使用SetWindowsHookEx()设置钩子
  • 当有另一个进程发生一个符合触发钩子的事件, DLL被操作系统注入到该进程中, 然后使用钩子

21.4 键盘消息钩取练习

在这里我们要知道钩子安装跟DLL注入之间的练习和先后顺序:

  • 首先是HookMain.exe运行后先加载了KeyHook.dll后安装了钩子, 同时要注意, 其他程序并没有发生键盘事件, 所以也就是没被强制注入KeyHook.dll
  • 随后explore.exe发生了键盘事件, 先被强制注入了KeyHook.dll, 然后安装了键盘钩子.

可以看到, 是先发生了DLL注入, 才安装了键盘钩子, 因为SetWindowsHookEx()是被包含在DLL中的.

Untitled

21.4.1 练习实例HookMain.exe

运行HookMain.exe

Untitled

按下q即可退出程序, 但是在Win10环境可以运行, 但是退出时会卡住. 所以这里用的是xp虚拟机

无法在notepad中输入任何东西

Untitled

使用ProcessExplorer的搜索DLL功能找出我们钩子所在的DLL

Untitled

在HookMain中键入q即可退出

再次查找keyhook.dll, 发现没有进程中由keyhook.dll.

Untitled

21.4.2 分析源代码.

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一个名叫HOOKSTART函数指针
typedef void (*PFN_HOOKSTOP)();//同上

void main()
{
HMODULE hDll = NULL;//类型是一个句柄(整数)
PFN_HOOKSTART HookStart = NULL;//声明变量HookStart类型在上面
PFN_HOOKSTOP HookStop = NULL;
char ch = 0;

// 加载KeyHook.dll
hDll = LoadLibraryA(DEF_DLL_NAME);//这里就是载入KeyHook.dll, 成功返回模块句柄, 失败返回NULL
if( hDll == NULL )//跟打开文件类似, 载入失败执行的语句
{
printf("LoadLibrary(%s) failed!!! [%d]", DEF_DLL_NAME, GetLastError());
return;
}

// 获取导出函数地址
HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);//获取HOOKSTART函数地址
HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);//获取HOOKSTOP函数地址

// 开始钩取
HookStart();

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

// 终止钩子
HookStop();

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

流程:

  • 加载KeyHook.dll文件, 并获取其成员函数HookStart()跟HookStop()
  • 然后调用HookStart()开始钩取
  • 输入”q”时退出

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include "stdio.h"
#include "windows.h"

#define DEF_PROCESS_NAME "notepad.exe"//定义进程名为notepad.exe

HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
HWND g_hWnd = NULL;

//定义的第一个函数, 这个函数在每个DLL载入的时候都会运行
/*
hinstDLL: 指向自身的句柄
dwReason: 调用原因
lpvReserved: 隐式加载和显式加载
*/
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
//WINAPI代表调用方式: 被调用者处理栈(不重要)
{
switch( dwReason )//根据下面的宏定义名可以知道表示的是附加到进程还是拆离进程
{
case DLL_PROCESS_ATTACH:
g_hInstance = hinstDLL;
break;

case DLL_PROCESS_DETACH:
break;
}

return TRUE;
}

//这个函数就是钩子过程: 也就是钩子内容具体实现的地方
/*
nCode: 根据这个数值决定怎样处理消息
wParam: 按键的虚拟键值
lParam: 不同的位数具有多种不同含义(比如a和A, 就靠这个区分)
*/
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, '\\');//从路径名中获取文件名
//strrchr: 搜索在szPath中最后一次出现//的地方

//比较当前进程名称是否为notepad.exe,成立则消息不传递给应用程
if( !_stricmp(p + 1, DEF_PROCESS_NAME) )//比较文件名是否为目标程序, 这里是notepad
return 1;//如果是目标程序, 则直接返回, 不将消息传递给下一个钩子
}
}

//如果不是notepad.exe,则调用CallNextHookEx()函数,将消息传递给应用程序
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

#ifdef __cplusplus//如果这是cpp代码, 就加入extern "C"
extern "C" {//extern "C"的作用是在C++代码中有了这个标识,编译器会按照C语言来编译代码
#endif
__declspec(dllexport) void HookStart()//__declspec(dllexport)显式声明这是一个导出函数
{
g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
}//当调用HookStart函数的时候, SetWindowsHookEx函数就KeyboardProc添加至链表

__declspec(dllexport) void HookStop()
{
if( g_hHook )
{
UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
}
#ifdef __cplusplus
}//这个括号是extern "C"的, 根据__cplusplus与上面的ifdef一同编译或不便宜
#endif

流程:

  • HookMain调用HookStart时, SetWindowsHookEx()设置钩子
  • 当发生键入消息的时候, keyhook.dll会注入到该进程
  • 然后运行dll中的keyboardproc(), 通过进程名拦截消息

关键在标红框的地方, 这里会检查进程名, 如果是notepad就会截断消息, 如果不是就会把消息传入链表中下一个钩子

注意这里截断的实现方法是: 不将消息传入下一个钩子(也可能下一个就是程序), 只是return 1, 这样消息消失了.

21.5 调试HookMain.exe

使用OD打开HookMian.exe

Untitled

通过字符串查找”q to quit”

Untitled

在起始位置401000下断点, 调试到这里.

然后可以看到401001和401006处加载了KeyHook.dll

然后是40104B处调用了CALL EBX指令, 就是调用了HookStart()

Untitled

我们跟进HookStart()

Untitled

可以看到调用了100010EF处调用了SetWindowsHookExW函数,

前面的两个指令逆序压入了前两个指令

  • 上面的第二个参数就是钩子过程的函数(钩子过程就是钩子具体执行处理的函数), 我们要记下其起始地址: 10001020
  • 第一个参数为2 , 代表WH_KEYBOARD键盘的钩子类型

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

首先调试notepad程序, 按下F9使其正常运行

Untitled

然后设置OD, 使得在载入新的DLL时调试中断

Untitled

然后再运行HookMain程序

Untitled

然后再在notepad中输入字符, 马上就会暂停并弹出模块界面, 可以看到第二条就是我们新载入的keyhook.dll

Untitled

我们双击keyhook.dll, 跳转到了其EP, 这里跟书中的不太一样, 钩子过程地址在59F1020, 但是因为偏移是一样的所以问题不大.

Untitled

我们在59F1020地址处打下断点. F9继续运行到59F1020处即是我们的钩子

流程:

  • 使用OD运行notepad
  • 打开在选项”在导入新模块时中断”
  • 运行KeyLogger.exe, 安装钩子
  • 在notepad中使用键盘输入 → 发生键盘事件
  • KeyLogger.dll被注入到notepad中
  • 在钩子进程中设置断点
  • 调试

我的一些理解

钩子的顺序

  • 使用了HookMain使用了SetWindowsHookEx()设置了钩子(全局钩子)到钩链中, 但是传入钩链的只是钩子KeyboardProc()的地址或者句柄, 实际内存中并没有这个钩子的存在, 所以后面才需要DLL强制注入. 同时需要注意的是只有HookMain调用了HookStart()和HookStop(), 也只有HookStart()调用了SetWindowsHookEx(), 我之前认为加载了DLL就会执行DLLMain也会调用SetWindowsHookEx()是错误的.
  • 当一个进程发生键盘消息的时候, 这时候消息被钩子捕获, 但是发现当前虚拟内存空间没有KeyboardProc()回调函数来处理(这是我个人的理解, 我认为钩子所导致的DLL注入是一个被动的过程), 所以强制DLL注入. 接上面我的错误认识, 这里的DLL注入只是为了提供KeyboardProc()给当前进程.
  • 使用KeyboardProc回调函数对键盘消息进行处理(这个时候才开始区分是否是notepad.exe)
  • 按下’q’键后对钩子进行卸载.

问题

  • 具体实现过程中HookStart()跟HookStop()它们干了什么
  • 全局钩子跟局部钩子的区别是什么, 是当发生相应消息的时候会先检查进程的PID, 然后决定是否进行DLL注入吗, 链表那里是不是会直接跳过这个钩子
文章作者: LamのCrow
文章链接: http://example.com/2022/03/31/逆向核心工程原理-第 55802/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LamのCrow