Hook初探

基本概念

Windows系统是建立在事件驱动机制上的,整个系统都是通过消息传递实现的。Hook,中文里常常被译作“钩子”或者“挂钩”,是一种特殊的消息处理机制,它可以监视系统或者进程中的各种事件消息,截获发往目标窗口的消息并进行处理。即,Hook可以改变程序执行流程,将程序原有的执行流程拦截,更改程序流向,并可以执行自己的自定义代码。Hook可以分为线程钩子系统钩子,线程钩子可以监视指定线程的事件消息,系统钩子监视系统中的所有线程的事件消息。

Hook技术被广泛应用于安全的多个领域,例如杀毒软件的主动防御功能,涉及到对一些敏感API的监控,就需要对这些API进行Hook;窃取密码的木马病毒,为了接收键盘的输入,需要Hook键盘消息;甚至是Windows系统及一些应用程序,在打补丁时也需要用到Hook技术。

基本原理

在真正执行原始API之前,对程序流进行拦截,使其先执行自定义的代码后,再执行原始API调用流程。简而言之就是篡改程序的运行路径,实现执行自定义程序的目的。

例如,在calc.exe和kernel32.dll之间挂上一个钩子,将它们之间要使用的CreateFile函数替换成自定义的EvilFunc函数,即可实现想要实现的自定义功能。

分类

Hook大体分为应用层Hook内核层Hook

  1. 应用层Hook:在Ring3层的Hook机制,主要涉及到用户态的程序和接口。这包括了对API函数的拦截和处理,例如通过IAT钩子(Import Address Table Hook)来拦截DLL的导入函数调用,或者通过Inline Hook直接在内存中修改函数的指令来实现拦截。
    1. 消息Hook
    2. 注入Hook
      1. IAT Hook(导入地址表钩子):通过修改PE文件结构中的导入表来实现对特定函数调用的拦截。这种方法通常用于DLL注入,通过修改目标进程的导入表来劫持函数调用。
      2. Inline Hook(内联钩子):直接在内存中修改函数的指令来实现拦截。这种方法通过在目标函数的指令流中插入跳转指令(如,jmp),使得函数调用被重定向到自定义的代码中。
      3. HotFix Hook(热修复钩子):通过在函数的头部分寻找可替换的“无用”指令(例如 mov edi, edi),并将这些指令替换为跳转指令(如,EB F9)来实现拦截。这种方法不需要频繁地挂载和卸载Hook,从而避免了资源浪费,提高了效率。当需要调用原始函数时,只需跳过这个短跳转指令即可,而无需还原被替换的指令,使得Hook过程更加高效和稳定。
      4. VEH Hook(向量化异常处理钩子):通过注册一个异常处理函数到操作系统的异常处理链表中,来实现对特定函数的拦截。当目标函数被调用时,会触发一个软件断点(如将指令修改为int 3),从而引发一个异常。VEH钩子作为异常处理程序,可以捕获这个异常,并在异常处理函数中执行自定义的代码。在处理完异常后,可以恢复原始函数的执行,或者修改寄存器和栈来改变程序的执行流程。VEH钩子的优点是它不需要修改原始函数的代码,因此可以实现无痕Hook,难以被检测工具发现。VEH钩子通常用于高级恶意软件技术,以绕过操作系统和安全软件的检测。
      5. ……
    3. 调试Hook
  2. 内核层Hook:在Ring0层的Hook机制,通常涉及到更底层的系统调用和内核API的拦截。例如SSDT钩子(System Service Descriptor Table Hook),它通过修改系统服务描述符表来拦截内核API调用。
    1. SSDT Hook(系统服务描述符表钩子):这是一种内核层的Hook技术,通过修改SSDT表中的函数地址来拦截系统服务调用。这种方法可以用于监控和过滤系统级别的活动,如文件操作、网络通信等。
    2. ……

常见Hook技术

IAT Hook

基本概念

可移植可执行文件( PE):可移植可执行文件格式是一种文件格式,用于32位和64位版本的Windows操作系统中的可执行文件、目标代码、DLL、FON字体文件等。PE格式是一种数据结构,它封装了Windows操作系统加载器管理封装可执行代码所需的信息。

导入地址表(**Import Address Table,IAT**地址表在应用程序调用不同模块中的函数时用作查找表,它可以采用按序号导入和按名称导入两种形式。由于编译后的程序无法知道其所依赖的库的内存位置,因此每次调用API时都需要间接跳转。动态链接器在加载模块并将它们连接在一起时,会将实际地址写入IAT插槽,使它们指向相应库函数的内存位置。每个进程都有IAT表,当PE加载到内存中,系统会将被导入函数的地址写到对应的函数指针位置,通过IAT表,可以直接调用导入函数。

导入名称表(Import Name Table,INT):用于储存被导入函数的名称。加载PE时,系统会根据INT表的函数名,查找填充IAT的函数地址。

实现方式

IAT HOOK可以解释为操纵导入地址表,将API函数重定向到所需的内存地址,该地址可以是另一个API函数、恶意shellcode或程序代码的另一部分。要覆盖IAT中的地址,第一步是找到进程内存中IAT表的地址。查找PE文件中的任何表都需要大量的结构解析,但是查找IAT地址比大多数情况都要容易,因为它可以在PE文件可选头文件中的数据目录中找到。

但是,仅仅找到导入地址表还不足以HOOK API函数。该表只包含API地址,为了替换API函数地址,还需要知道哪个条目属于将要HOOK的API函数。深入研究PE格式后可以发现,导入地址表中的地址顺序与导入名称表相同。因此,可以通过解析导入名称表来找到所需的API函数的条目编号。

在导入名称表中查找函数名需要解析PE文件的导入表中的_IMAGE_IMPORT_DESCRIPTOR结构,在解析了必要的结构并在IAT中找到API函数索引之后,在覆盖函数地址之前还需要执行另一个步骤。通常导入地址表位于内存中,只有读权限,为了覆盖表内的条目,需要将内存保护属性修改为PAGE_READWRITE。借助VirtualProtect函数,可以更改IAT的内存保护属性(或者只是需要覆盖的条目)。

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // RVA 指向IMAGE_THUNK_DATA结构数组(即INT表)
};
DWORD TimeDateStamp; // 时间戳
DWORD ForwarderChain;
DWORD Name; // RVA,指向dll名字,该名字已0结尾
DWORD FirstThunk; // RVA,指向IMAGE_THUNK_DATA结构数组(即IAT表)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

示例

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
105
106
107
108
109
110
111
#include <iostream>
#include <Windows.h>

DWORD pOldFuncAddr = (DWORD)::GetProcAddress(LoadLibrary(L"USER32.dll"), "MessageBoxW");

BOOL SetIATHook(DWORD dwOldAddr, DWORD dwNewAddr) {
DWORD dwImageBase = 0;
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
PIMAGE_IMPORT_DESCRIPTOR pImport = NULL;
PDWORD pIAT = NULL;
DWORD oldProtected = 0;
bool Flag = FALSE;

dwImageBase = (DWORD)::GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwImageBase;
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4);
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pOptionHeader->DataDirectory[1].VirtualAddress + dwImageBase);

//定位IAT表
while (pImport->FirstThunk != 0 && Flag == FALSE) {
pIAT = (PDWORD)(pImport->FirstThunk + dwImageBase);

while (*pIAT) {
if (*pIAT == pOldFuncAddr) {
VirtualProtect(pIAT, 0x4096, PAGE_EXECUTE_READWRITE, &oldProtected);
*pIAT = dwNewAddr;
Flag = TRUE;
printf("Hook Success!\n\n");
break;
}
pIAT++;
}
pImport++;
}

return Flag;
}

DWORD UnSetIATHook(DWORD dwOldAddr, DWORD dwNewAddr) {
DWORD dwImageBase = 0;
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
PIMAGE_IMPORT_DESCRIPTOR pImport = NULL;
PDWORD pIAT = NULL;
DWORD oldProtected = 0;
bool Flag = FALSE;

dwImageBase = (DWORD)::GetModuleHandle(NULL); // 获取进程基址
pDosHeader = (PIMAGE_DOS_HEADER)dwImageBase;
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4);
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pOptionHeader->DataDirectory[1].VirtualAddress + dwImageBase);

while (pImport->FirstThunk != 0 && Flag == FALSE) {
pIAT = (PDWORD)(pImport->FirstThunk + dwImageBase);
while (*pIAT) {
if (*pIAT == dwNewAddr) {
*pIAT = dwOldAddr;
Flag = TRUE;
break;
}
pIAT;
}
pImport;
}

return Flag;
}

int WINAPI EvilMessageBox(
HWND hwnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType) {
//定义MyMessageBox的指针
typedef int (WINAPI* PFNMESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT);

//获取参数
printf("Argument: hwnd-%x lpText-%ws lpCaption-%ws uType-%x\n\n", hwnd, lpText, lpCaption, uType);

//执行真正的函数
int ret = ((PFNMESSAGEBOX)pOldFuncAddr)(hwnd, lpText, lpCaption, uType);

//获取返回值
printf("The return value is: %x\n\n", ret);

return ret;
}

int EvilIATHook() {
MessageBox(NULL, L"Before IAT HOOK", L"HOOK!", MB_OK);
SetIATHook(pOldFuncAddr, (DWORD)EvilMessageBox);
MessageBox(NULL, L"IAT HOOK", L"IATHOOK Success!", MB_OK);
UnSetIATHook(pOldFuncAddr, (DWORD)EvilMessageBox);
MessageBox(NULL, L"After IAT HOOK", L"HOOK!", MB_OK);

return 1;
}
int main() {
EvilIATHook();
}

Inline Hook

基本概念

Inline Hook又被称为内联Hook,主要思想是直接修改目标API函数的代码

实现方式

  1. 使用E9(JMP)进行InlineHook

E9是JMP指令的操作码,用于进行近跳转。它后面跟随一个相对偏移,表示从当前指令后的下一个指令开始计算的跳转目标。

当使用E9进行InlineHook时,直接在目标函数的入口插入一个跳转指令,使其跳转到Hook函数。例如,E9 [offset],其中[offset]是从JMP指令后的下一个指令到Hook函数的相对偏移(相对偏移 = 目的地址 - 源地址 - 5)。其中,5是jmp指令的字节数。

  1. 使用B8(MOV)和FF E0(JMP EAX)进行InlineHook

B8是MOV指令的操作码,用于将一个立即数值移动到EAX寄存器。FF E0是JMP EAX指令的操作码,表示跳转到EAX寄存器中的地址。

当使用这种方法进行InlineHook时,首先将Hook函数的地址移动到EAX寄存器,然后使用JMP EAX跳转到该地址。例如,B8 [hook function address] FF E0,其中[hook function address]是Hook函数的绝对地址(即目标地址)。

示例

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

#if defined(_WIN64)
#define ORIG_BYTES_SIZE 14
#else
#define ORIG_BYTES_SIZE 7
#endif

// Save some of the original code bytes of MessageBoxA
BYTE OriginalBytes[ORIG_BYTES_SIZE]{};
// Construct jump instruction
BYTE PatchBytes[ORIG_BYTES_SIZE]{};

using MessageBoxAT = int (WINAPI*)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
MessageBoxAT OriginalMessageBox = nullptr;

int WINAPI HookMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
// Execute custom code
SIZE_T bytesOut = 0;

MessageBoxW(0, L"HookedMessageBox() called", L"Inline Hook", 0);
// unpatch MessageBoxA
WriteProcessMemory(GetCurrentProcess(), (LPVOID)OriginalMessageBox, OriginalBytes, sizeof(OriginalBytes), &bytesOut);
// call MessageBoxA
int result = MessageBoxA(NULL, lpText, lpCaption, uType);
// patch MessageBoxA
WriteProcessMemory(GetCurrentProcess(), OriginalMessageBox, PatchBytes, sizeof(PatchBytes), &bytesOut);

return result;
}

bool SetHook(std::string dllName, std::string origFunc, FARPROC hookingFunc) {
SIZE_T bytesIn = 0;
SIZE_T bytesOut = 0;

// Save MessageBoxA original address
OriginalMessageBox = (MessageBoxAT)GetProcAddress(GetModuleHandleA(dllName.c_str()), origFunc.c_str());

// Save some of the original code bytes of MessageBoxA
ReadProcessMemory(GetCurrentProcess(), OriginalMessageBox, OriginalBytes, ORIG_BYTES_SIZE, &bytesIn);

memset(PatchBytes, 0, sizeof(PatchBytes));
#if defined(_WIN64)
/*
JMP [RIP+0];
\xFF\x25\x00\x00\x00\x00
\x00\x11\x22\x33\x44\0x55\x66\x77
*/
memcpy(PatchBytes, "\xFF\x25", 2);
memcpy(PatchBytes + 6, &hookingFunc, 8);
#else
/*
mov eax, &hookingFunc
jmp eax
*/
memcpy(PatchBytes, "\xB8", 1);
memcpy(PatchBytes + 1, &hookingFunc, sizeof(ULONG_PTR));
memcpy(PatchBytes + 5, "\xFF\xE0", 2);
#endif

// patch the MessageBoxA
WriteProcessMemory(GetCurrentProcess(), OriginalMessageBox, PatchBytes, sizeof(PatchBytes), &bytesOut);

return true;
}

int main() {
// Before Hook
MessageBoxA(0, "Before Hooking", "Inline Hook", 0);

// Inline Hook
SetHook("user32.dll", "MessageBoxA", (FARPROC)HookMessageBoxA);

// After Hook
MessageBoxA(0, "After Hooking", "Inline Hook", 0);

return 0;
}

Reference

Offensive IAT Hooking

安全开发之应用层Hook技术