HOOK技术学习笔记

0x00 前言

再来系统学习一下之前学的不扎实的HOOK技术:cry:

HOOK技术,用好了能有很奇妙的效果哟~

可以对程序的执行流进行监控、拦截。举个例子:键盘钩子,可以监控用户通过键盘输入了什么:happy:

本文重点去讨论HOOK过程的原子性

0x01 HOOK的几种分类

HOOK技术可以分为Inline HookAddress Hook基于异常处理的Hook不是HOOK的HOOK

1. Inline Hook

Inline Hook是指直接修改指令的Hook,其关键是转移程序的执行流程,一般用jmp、call、retn等转移指令

几种常见形式

  1. jmp xxxxxxxx(5字节)
  2. push xxxxxxxx / retn(6字节)
  3. mov eax,xxxxxxxx / jmp eax(7字节)
  4. call Hook
  5. HotPatch Hook

2. Address Hook

Address Hook是指通过修改数据来进行Hook,这些数据往往是一些函数的地址或者偏移量,它们通常存放在某类数据结构中或是指定位置处,也可能是存在于寄存器中。它们有一个共同点,就是会在某时刻成为执行过程中的eip,因此将这数据替换成我们想要的函数地址就实现了Hook

几种常见形式

  1. IAT(Import Address Table,输入表)
  2. EAT(Export Address Table,输出表)
  3. user32.dll的回调函数表
  4. IDT(Interrupt Descriptor Table,系统的中断描述符表)
  5. SSDT和Shadow SSDT
  6. C++类的虚函数表
  7. COM接口的功能函数表
  8. 特殊寄存器中的地址
  9. 特定的函数指针

3. 基于异常处理的Hook

在程序中自行安装SEH,向被HOOK的位置写入一条会引发异常的指令,只要程序执行到这里就会触发异常,从而跳转到事先安装的异常处理程序处。

4. 不是HOOK的HOOK

回顾病毒和操作系统的一些行为,都是在某些时候取得程序的控制权,并进行适当的处理。

几种常见的形式

  1. PE被感染,修改EnterPoint

    会在执行完病毒代码后跳回正常的程序入口,使其不易被察觉。

  2. 系统回调机制

  3. 分层服务和过滤驱动模型

0x02 几个细节

1. 裸函数 naked

__declspec(naked) 告诉编译器,以下的汇编不需要编译器再优化什么指令。

2. 系统位数引起的细节问题

指针

指针在x86上4字节,在x64上8字节。

可以使用ULONG_PTR来自适应于编译器,类似的还有SIZE_T

内存地址对齐

x64最好按照16字节来对齐,尽管x64对应8字节

PE格式

影响依照PE结构的HOOK

调用约定

x64仅有__fastcall调用方式

用寄存器来传参

第1~4个整型参数依次按照RCX、RDX、R8、R9来传参

第1~4个浮点参数依次按照XMM0、XMM1、XMM2、XMM3来传参

跳转指令

对于直接跳转指令E9,跳转范围以当前位置为界,前后各2GB,在X86平台完全够用,但是在X64平台就不够了。

所以在64位的情况下,jmp必须直接包含目标地址

可以用

mov rax,addr / jmp rax

push ret

jmp [addr]其中此jmp为FF25Address = eip + 偏移量 + 6(指令长度)

PatchGuard

3. 一个经典的问题

我们该如何保证HOOK的原子性?

即,我们如何保证负责HOOK的线程在进行HOOK的时候,其他线程不会访问被HOOK但还没完成HOOK的代码段?

这里提出几种解决方案,这几种解决方案均是受启发于《Windows核心编程》这本经典著作。

方案一

我们可以在执行HOOK的时候,挂起(Suspend)其他一切无关的线程,在HOOK完毕后再恢复(Resume)其他线程。

不过,这个方案相比方案二,耗时会较多一些

相关代码如下:

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
BOOL Solution1(BOOL isSuspend) {
HANDLE hThread = NULL;
DWORD dwPid = GetCurrentProcessId();
DWORD dwTid = GetCurrentThreadId();
THREADENTRY32 stThreadEntry32;
stThreadEntry32.dwSize = sizeof(THREADENTRY32);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwPid);
if (hSnapshot == INVALID_HANDLE_VALUE) {
return FALSE;
}
BOOL anymore = Thread32First(hSnapshot, &stThreadEntry32);
while (anymore) {
if (stThreadEntry32.th32OwnerProcessID == dwPid && stThreadEntry32.th32ThreadID != dwTid) {
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, stThreadEntry32.th32ThreadID);
if (!hThread) {
return FALSE;
}
if (isSuspend) {
SuspendThread(hThread);
}
else {
ResumeThread(hThread);
}
CloseHandle(hThread);
}
anymore = Thread32Next(hSnapshot, &stThreadEntry32);
}
CloseHandle(hSnapshot);
return TRUE;
}

方案二

我们可以借助Windows提供的具有原子性的API,例如

InterlockedExchange系列与InterlockedCompareExchange系列

但是值得注意的是,这两个系列均仅支持修改1/2/4/8字节,并不支持修改其他字节,是的,我并没有找到直接修改HOOK所需的5或者7字节的API,但是这并不妨碍我们进行修改,例如我们的Inline Hook往往是需要修改5或者7字节,这都小于8字节,因此我们可以调用InterlockedExchange64来直接修改8字节!

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

BOOL Solution2_Install(DWORD originalCodeAddr, DWORD originalSize, DWORD newCodeAddr) {

if (originalCodeAddr == 0 || originalSize < 5 || originalSize > 8 || newCodeAddr == 0)
{
return FALSE;
}
// 设置内存写权限
DWORD dwOldProtectFlag;
BOOL bRet = VirtualProtect((LPVOID)originalCodeAddr, 8, PAGE_EXECUTE_READWRITE, &dwOldProtectFlag);
if (!bRet)
{
return FALSE;
}
// 计算E9 JMP后面的4字节 = 要跳转的地址 - CALL的下一条指令的地址
DWORD dwJmpCode = newCodeAddr - (originalCodeAddr + 5);
// 由于要使用InterlockedExchange64来修改8字节,故构造替换的8字节
BYTE bReplace[8] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };//全部用NOP替换
bReplace[0] = 0xE9; // JMP

*(PDWORD)(&(bReplace[1])) = dwJmpCode;//☆体会这种语句的好处

//这里以及下面的memcpy无伤大雅,这块只是准备工作,即便不能够线程安全也无所谓
memcpy( (BYTE*)(bReplace + originalSize),
(LPVOID)(originalCodeAddr + originalSize), 8 - originalSize);//把后面字节copy过来

LONG64 llReplace;
memcpy(&llReplace, bReplace, 8);//全都copy给一个64位的变量

// 原子操作hook
InterlockedExchange64((LONG64 volatile*)originalCodeAddr, llReplace);
// 恢复内存属性
VirtualProtect((LPVOID)originalCodeAddr, 8, dwOldProtectFlag, &dwOldProtectFlag);

return TRUE;
}

BOOL Solution2_Uninstall(DWORD originalCodeAddr, DWORD originalSize, BYTE * oldCodeAddr) {

if (originalCodeAddr == 0 || originalSize < 5 || originalSize > 8 || oldCodeAddr == 0)
{
return FALSE;
}

// 设置内存写权限
DWORD dwOldProtectFlag;
BOOL bRet = VirtualProtect((LPVOID)originalCodeAddr, 8, PAGE_EXECUTE_READWRITE, &dwOldProtectFlag);
if (!bRet)
{
return FALSE;
}

// 由于要使用InterlockedExchange64来修改8字节,故构造替换的8字节
BYTE bReplace[8] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };//全部用NOP替换

//这里以及下面的memcpy无伤大雅,这块只是准备工作,即便不能够线程安全也无所谓
memcpy((BYTE*)bReplace, (LPVOID)(oldCodeAddr), originalSize);//还原原始的

memcpy((BYTE*)(bReplace + originalSize),
(LPVOID)(originalCodeAddr + originalSize), 8 - originalSize);//把后面字节copy过来

LONG64 llReplace;
memcpy(&llReplace, bReplace, 8);//全都copy给一个64位的变量
// 原子操作hook
InterlockedExchange64((LONG64 volatile*)originalCodeAddr, llReplace);
// 恢复内存属性
VirtualProtect((LPVOID)originalCodeAddr, 8, dwOldProtectFlag, &dwOldProtectFlag);
return TRUE;
}

方案三

借助信号量机制。

首先,我们进行修改内存的时候极有可能使用memcpy这个函数,而这个函数并不是线程安全函数,引用自StackOverflow

memcpy is typically coded for raw speed. It will not be thread safe. If you require this, you need to perform the memcpy call inside of a critical section or use some other semaphor mechanism.

1
2
3
4
> take_mutex(&mutex);
> memcpy(dst, src, count);
> yield_mutex(&mutex);
>

0x03 HOOK实例

来写一个DLL来稍微练习一下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
//InlineHook.h
#pragma once
#include <windows.h>
#include <Tlhelp32.h>
#include <stdio.h>

//自定义函数
int WINAPI HookMessageBox(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);

//安装钩子
BOOL InstallHook();

//卸载钩子
BOOL UnInstallHook();

//初始化函数
BOOL InitHook();

//方案一,依据BOOL型参数来决定是挂起还是恢复
BOOL Solution1(BOOL isSuspend);

BOOL Solution2_Install(DWORD originalCodeAddr, DWORD originalSize, DWORD newCodeAddr);
BOOL Solution2_Uninstall(DWORD originalCodeAddr, DWORD originalSize, BYTE* oldCodeAddr);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "InlineHook.h"

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
InitHook();
InstallHook();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
UnInstallHook();
break;
}
return TRUE;
}

方案一

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
//InlineHook.cpp
#include "InlineHook.h"

DWORD g_unHook = NULL;//保存函数地址
BYTE g_oldCode[5] = { 0 };//原始的5个字节
BYTE g_newCode[5] = { 0xE9 };//改变后的5个字节


//自定义函数
int WINAPI HookMessageBox(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
) {
UnInstallHook();//先卸载钩子,防止死循环
int result = MessageBox(NULL, TEXT("HOOK Success"), TEXT("test4fun"), MB_OK);
InstallHook();
return result;
}

BOOL InitHook() {
HMODULE hMoudle = LoadLibrary("user32.dll");
if (!hMoudle) {
return FALSE;
}
g_unHook = (DWORD)GetProcAddress(hMoudle, "MessageBoxA");
memcpy_s(g_oldCode, 5, (BYTE*)g_unHook, 5);

DWORD offset = (DWORD)HookMessageBox - g_unHook - 5;
memcpy_s(g_newCode + 1, 4, &offset, 4);
return TRUE;
}

BOOL InstallHook() {
if (!g_unHook) {//初始化未成功
return FALSE;
}
Solution1(TRUE);//挂起全部其他线程
DWORD OldProtect = 0;//原来的内存属性
VirtualProtect((DWORD*)g_unHook, 5, PAGE_EXECUTE_READWRITE, &OldProtect);//修改内存地址可读可写可执行
memcpy_s((DWORD*)g_unHook, 5, g_newCode, 5);//把要改变的5个字节写到函数地址里
VirtualProtect((DWORD*)g_unHook, 5, OldProtect, &OldProtect);//再将内存属性改回去
Solution1(FALSE);//恢复全部其他线程

return TRUE;
}

BOOL UnInstallHook() {
if (!g_unHook) {
return FALSE;
}
Solution1(TRUE);//挂起其他全部线程
DWORD OldProtect = 0;//原来的内存属性
VirtualProtect((DWORD*)g_unHook, 5, PAGE_EXECUTE_READWRITE, &OldProtect);//修改内存地址可读可写可执行
memcpy_s((DWORD*)g_unHook, 5, g_oldCode, 5);//把原来的5个字节写到函数地址里
VirtualProtect((DWORD*)g_unHook, 5, OldProtect, &OldProtect);//再将内存属性改回去
Solution1(FALSE);//恢复其他全部线程

return TRUE;
}

BOOL Solution1(BOOL isSuspend) {
HANDLE hThread = NULL;
DWORD dwPid = GetCurrentProcessId();
DWORD dwTid = GetCurrentThreadId();
THREADENTRY32 stThreadEntry32;
stThreadEntry32.dwSize = sizeof(THREADENTRY32);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwPid);
if (hSnapshot == INVALID_HANDLE_VALUE) {
return FALSE;
}
BOOL anymore = Thread32First(hSnapshot, &stThreadEntry32);
while (anymore) {
if (stThreadEntry32.th32OwnerProcessID == dwPid && stThreadEntry32.th32ThreadID != dwTid) {
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, stThreadEntry32.th32ThreadID);
if (!hThread) {
return FALSE;
}
if (isSuspend) {
SuspendThread(hThread);
}
else {
ResumeThread(hThread);
}
CloseHandle(hThread);
}
anymore = Thread32Next(hSnapshot, &stThreadEntry32);
}
CloseHandle(hSnapshot);
return TRUE;
}

方案二

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
112
113
//InlineHook.cpp
#include "InlineHook.h"

DWORD g_unHook = NULL;//保存函数地址
BYTE g_oldCode[5] = { 0 };//原始的5个字节
BYTE g_newCode[5] = { 0xE9 };//改变后的5个字节

//自定义函数
int WINAPI HookMessageBox(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
) {
UnInstallHook();//先卸载钩子,防止死循环
int result = MessageBox(NULL, TEXT("HOOK Success!"), TEXT("Test4fun"), MB_OK);
InstallHook();
return result;
}

BOOL InitHook() {
HMODULE hMoudle = LoadLibrary("user32.dll");
if (!hMoudle) {
return FALSE;
}
g_unHook = (DWORD)GetProcAddress(hMoudle, "MessageBoxA");
memcpy_s(g_oldCode, 5, (BYTE*)g_unHook, 5);
return TRUE;
}

BOOL InstallHook() {
if (!g_unHook) {//初始化未成功
return FALSE;
}
Solution2_Install(g_unHook, 5, (DWORD)HookMessageBox);
return TRUE;
}

BOOL UnInstallHook() {
if (!g_unHook) {
return FALSE;
}
Solution2_Uninstall(g_unHook, 5, g_oldCode);
return TRUE;
}

BOOL Solution2_Install(DWORD originalCodeAddr, DWORD originalSize, DWORD newCodeAddr) {
if (originalCodeAddr == 0 || originalSize < 5 || originalSize > 8 || newCodeAddr == 0)
{
return FALSE;
}
// 设置内存写权限
DWORD dwOldProtectFlag;
BOOL bRet = VirtualProtect((LPVOID)originalCodeAddr, 8, PAGE_EXECUTE_READWRITE, &dwOldProtectFlag);
if (!bRet)
{
return FALSE;
}
// 计算E9 JMP后面的4字节 = 要跳转的地址 - CALL的下一条指令的地址
DWORD dwJmpCode = newCodeAddr - (originalCodeAddr + 5);
// 由于要使用InterlockedExchange64来修改8字节,故构造替换的8字节
BYTE bReplace[8] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };//全部用NOP替换
bReplace[0] = 0xE9; // JMP

*(PDWORD)(&(bReplace[1])) = dwJmpCode;//☆体会这种语句的好处

//这里以及下面的memcpy无伤大雅,这块只是准备工作,即便不能够线程安全也无所谓
memcpy( (BYTE*)(bReplace + originalSize),
(LPVOID)(originalCodeAddr + originalSize), 8 - originalSize);//把后面字节copy过来

LONG64 llReplace;
memcpy(&llReplace, bReplace, 8);//全都copy给一个64位的变量

// 原子操作hook
InterlockedExchange64((LONG64 volatile*)originalCodeAddr, llReplace);
// 恢复内存属性
VirtualProtect((LPVOID)originalCodeAddr, 8, dwOldProtectFlag, &dwOldProtectFlag);

return TRUE;
}

BOOL Solution2_Uninstall(DWORD originalCodeAddr, DWORD originalSize, BYTE * oldCodeAddr) {

if (originalCodeAddr == 0 || originalSize < 5 || originalSize > 8 || oldCodeAddr == 0)
{
return FALSE;
}

// 设置内存写权限
DWORD dwOldProtectFlag;
BOOL bRet = VirtualProtect((LPVOID)originalCodeAddr, 8, PAGE_EXECUTE_READWRITE, &dwOldProtectFlag);
if (!bRet)
{
return FALSE;
}

// 由于要使用InterlockedExchange64来修改8字节,故构造替换的8字节
BYTE bReplace[8] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };//全部用NOP替换

//这里以及下面的memcpy无伤大雅,这块只是准备工作,即便不能够线程安全也无所谓
memcpy((BYTE*)bReplace, (LPVOID)(oldCodeAddr), originalSize);//还原原始的

memcpy((BYTE*)(bReplace + originalSize),
(LPVOID)(originalCodeAddr + originalSize), 8 - originalSize);//把后面字节copy过来

LONG64 llReplace;
memcpy(&llReplace, bReplace, 8);//全都copy给一个64位的变量
// 原子操作hook
InterlockedExchange64((LONG64 volatile*)originalCodeAddr, llReplace);
// 恢复内存属性
VirtualProtect((LPVOID)originalCodeAddr, 8, dwOldProtectFlag, &dwOldProtectFlag);
return TRUE;
}

一个用于测试的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <Windows.h>
#include <iostream>
using namespace std;

int main()
{
HMODULE hMoudle = LoadLibrary("InlineHookTest.dll");

if (hMoudle)
{
MessageBox(NULL, TEXT("HOOK Failed"), TEXT("test4fun"), MB_OK);//调用MessageBox来检测
}
//如果HOOK成功以后,弹出的MessageBox应该是HOOK Success
//否则就会是这个HOOK Failed
return 0;
}

0x04 参考

《加密与解密》第四版

《逆向工程核心原理》

多线程inline hook

多线程inline hook

inline hook需要注意的问题

inline hook需要注意的问题这一篇里面关于挂起线程和恢复线程的代码是存在问题的,它挂起的是所有进程的线程而非目标进程的线程,本文已经对其代码进行了修改完善,不过仍要感谢文章作者提供的思路!

memcpy是否线程安全

-------------至此本文结束感谢您的阅读-------------
如果觉得这篇文章对您有用,请随意打赏。 (๑•⌄•๑) 您的支持将鼓励我继续创作!