March 17, 2022

关于逆向工程核心原理-Windows消息钩取

(简称逆向工程核心原理第二十一章)

HookMain

冷静分析(好久没碰typedef都快忘了)

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
#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)(); // 定义一个没有返回值没有参数(为了对应KeyHook.dll的导出函数) 名为PEN_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;
}

// 获取导出函数的地址 前面的(PFN_HOOKSTART)是强制转换类型成对应类型的函数指针
// GetProcAddres是获取地址
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

DllMain

动态链接库的可选入口点,都进程启动或者终止进程或线程,它会使用进程的第一个线程,为每个加载的DLL调用入口点函数

(说白了就是加载卸载dll都要先执行DllMain)

DllMain 入口点

1
2
3
4
5
BOOL WINAPI DllMain(
_In_ HINSTANCE hinstDLL, // 该值是DLL的基地址
_In_ DWORD fdwReason, // 详见链接
_In_ LPVOID lpvReserved
);

于是乎在KeyHook.dll是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 加载卸载dll都要先执行DllMain
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
switch( dwReason )
{
// 加载时
case DLL_PROCESS_ATTACH:
// 该值是DLL的基地址
g_hInstance = hinstDLL;
break;

// 卸载时
case DLL_PROCESS_DETACH:
break;
}

return TRUE;
}

SetWindowsHookEx

SetWindowsHookEx

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

PS: 若dwThreadId参数被设置为0,则安装的钩子是全局钩子,它会影响到运行中的所有进程

像这样使用SetWindowsHookEx设置钩子,操作系统会将相关DLL文件强行注入进程,非常方便

1
2
3
4
5
6
HHOOK SetWindowsHookExA(
[in] int idHook, // hook type 钩子类型
[in] HOOKPROC lpfn, // hook procedure 指向挂钩过程的指针
[in] HINSTANCE hmod, // hook procedure所属的DLL句柄
[in] DWORD dwThreadId // 想要挂钩的线程ID
);

于是在KeyHook.dll里是这样的,注意declspec代表是导出函数的关键字

也是从这调用了SetWindowsHookEx

1
2
3
4
5
// declspec为导出函数关键字
__declspec(dllexport) void HookStart()
{
g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
}

KeyboardProc 回调函数

调用导出函数HookStart时,SetWindowsHookEx就会将KeyboardProc放入钩链!

  1. 在调用HookStart时,SetWindowsHookEx就会将KeyboardProc添加到钩链,安装好钩子,无论哪个进程,只要发生键盘输入事件,OS就强制把KeyHook.dll注入相应进程
  2. 发生键盘事件,就会首先调用执行KeyHook.KeyboardProc()
  3. KeyboardProc函数中发生输入事件,就会比较当前进程是否是Notepad.exe
  4. 如果是就直接返回1,终于该函数,这意味着截获且删除消息(这样键盘消息就不会传递到notepad.exe程序的相关队列)
  5. 如果不是notepad就执行CallNextHookEx,这意味着消息会被传递到另一个程序或者钩链的另一个钩子
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
// 1. 在调用HookStart时,SetWindowsHookEx就会将KeyboardProc添加到钩链,安装好钩子,无论哪个进程,只要发生键盘输入事件,OS就强制把KeyHook.dll注入相应进程
// 2. 发生键盘事件,就会首先调用执行KeyHook.KeyboardProc()
// 3. KeyboardProc函数中发生输入事件,就会比较当前进程是否是Notepad.exe
// 4. 如果是就直接返回1,终于该函数,这意味着截获且删除消息(这样键盘消息就不会传递到notepad.exe程序的相关队列)
// 5. 如果不是notepad就执行CallNextHookEx,这意味着消息会被传递到另一个程序或者钩链的另一个钩子
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, '\\');

// p + 1代表找到 \\后的第一位
if( !_stricmp(p + 1, DEF_PROCESS_NAME) )
return 1;
}
}

return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

逐个分析完,再看看这简单的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
#include "stdio.h"
#include "windows.h"

#define DEF_PROCESS_NAME "notepad.exe"

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

// 加载卸载dll都要先执行DllMain
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
switch( dwReason )
{
// 加载时
case DLL_PROCESS_ATTACH:
// 该值是DLL的基地址
g_hInstance = hinstDLL;
break;

// 卸载时
case DLL_PROCESS_DETACH:
break;
}

return TRUE;
}

// 1. 在调用HookStart时,SetWindowsHookEx就会将KeyboardProc添加到钩链,安装好钩子,无论哪个进程,只要发生键盘输入事件,OS就强制把KeyHook.dll注入相应进程
// 2. 发生键盘事件,就会首先调用执行KeyHook.KeyboardProc()
// 3. KeyboardProc函数中发生输入事件,就会比较当前进程是否是Notepad.exe
// 4. 如果是就直接返回1,终于该函数,这意味着截获且删除消息(这样键盘消息就不会传递到notepad.exe程序的相关队列)
// 5. 如果不是notepad就执行CallNextHookEx,这意味着消息会被传递到另一个程序或者钩链的另一个钩子
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, '\\');

// p + 1代表找到 \\后的第一位
if( !_stricmp(p + 1, DEF_PROCESS_NAME) )
return 1;
}
}

return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

#ifdef __cplusplus
extern "C" {
#endif
// declspec为导出函数关键字
__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

调试HookMain.exe

通过搜索字符串找到关键函数处

image-20220318105507378

现在是去找KeyHook.HookStart函数

进入到HookStart函数,可以发现在KeyboardProc的地址为10001020

image-20220318105643584

调试Notepad.exe内的KeyHook.dll

1. Attch Notepad.exe

image-20220318113240658

2. 设置Break on new moudle

开启该选项后,每当有新的DLL装入,就会弹出加载的DLL

image-20220318113317601

3. 开启钩子

已经挂起了钩子,现在只要触发键盘事件,就会强制注入KeyHook.dll

image-20220318113500525

4. DLL强制注入

在OD按F9让notepad处于可输入状态,在notepad输入字符,OD会自动弹出,可以发现KeyHook.dll已经注入

image-20220318113729704

5. 调试KeyboardProc函数

在调试HookMain的时候知道的KeyHook.KeyboardProc的地址,于是断点在这按个F9即可跑到这

image-20220318114046905

逐条调试,即可理解大部分意思,不过arg.1和local.66我还不太清楚具体是什么意思,应该是全局和局部的

image-20220318114342536

再附上一张作者写的流程

image-20220318114940904

DASCTF X SU
🍬
HFCTF2022
🍪

About this Post

This post is written by P.Z, licensed under CC BY-NC 4.0.