Drunkmars's Blog

hook技术详解

字数统计: 6.1k阅读时长: 25 min
2021/09/12

本文对两种常见的hook技术进行分析及实现。

基础知识

​ 对于Windows系统,它是建立在事件驱动机制上的,说白了就是整个系统都是通过消息传递实现的。hook(钩子)是一种特殊的消息处理机制,它可以监视系统或者进程中的各种事件消息,截获发往目标窗口的消息并进行处理。所以说,我们可以在系统中自定义钩子,用来监视系统中特定事件的发生,完成特定功能,如屏幕取词,监视日志,截获键盘、鼠标输入等等。

​ 钩子的种类很多,每种钩子可以截获相应的消息,如键盘钩子可以截获键盘消息,外壳钩子可以截取、启动和关闭应用程序的消息等。钩子可以分为线程钩子和系统钩子,线程钩子可以监视指定线程的事件消息,系统钩子监视系统中的所有线程的事件消息。因为系统钩子会影响系统中所有的应用程序,所以钩子函数必须放在独立的动态链接库(DLL) 中。

​ 所以说,hook(钩子)就是一个Windows消息的拦截机制,可以拦截单个进程的消息(线程钩子),也可以拦截所有进程的消息(系统钩子),也可以对拦截的消息进行自定义的处理。Windows消息带了一些程序有用的信息,比如Mouse类信息,就带有鼠标所在窗体句柄、鼠标位置等信息,拦截了这些消息,就可以做出例如金山词霸一类的屏幕取词功能。

hook原理

​ 在正确使用钩子函数前,我们先讲解钩子函数的工作原理。当创建一个钩子时,WINDOWS会先在内存中创建一个数据结构,该数据结构包含了钩子的相关信息,然后把该结构体加到已经存在的钩子链表中去。新的钩子将加到老的前面。当一个事件发生时,如果您安装的是一个线程钩子,您进程中的钩子函数将被调用。如果是一个系统钩子,系统就必须把钩子函数插入到其它进程的地址空间,要做到这一点要求钩子函数必须在一个动态链接库中,所以如果您想要使用系统钩子,就必须把该钩子函数放到动态链接库中去。

当然有两个例外:工作日志钩子和工作日志回放钩子。这两个钩子的钩子函数必须在安装钩子的线程中。原因是:这两个钩子是用来监控比较底层的硬件事件的,既然是记录和回放,所有的事件就当然都是有先后次序的。所以如果把回调函数放在DLL中,输入的事件被放在几个线程中记录,所以我们无法保证得到正确的次序。故解决的办法是:把钩子函数放到单个的线程中,譬如安装钩子的线程。

​ 几点需要说明的地方:

  (1) 如果对于同一事件(如鼠标消息)既安装了线程钩子又安装了系统钩子,那么系统会自动先调用线程钩子,然后调用系统钩子。
  (2) 对同一事件消息可安装多个钩子处理过程,这些钩子处理过程形成了钩子链。当前钩子处理结束后应把钩子信息传递给下一个钩子函数。而且最近安装的钩子放在链的开始,而最早安装的钩子放在最后,也就是后加入的先获得控制权。
  (3) 钩子特别是系统钩子会消耗消息处理时间,降低系统性能。只有在必要的时候才安装钩子,在使用完毕后要及时卸载。

应用场景

我们知道杀软有一个检测的点就是一些敏感的api,如OpenProcess,VirtualAllocEx,WriteProcessMemory 等等。最常用的一个方式就是加一个jmp,跳转到一个地址,这个地址一般就是杀软写代码进行行为检测的函数,那么jmp这个过程就是一个hook过程。当然我们如果想要杀软不分析我们的木马,那么unhook回来即可,这块就涉及到硬编码的知识,这里就不做延伸了。

IAThook

在实现IAThook之前就需要很多前置知识,主要要对PE结构有一定的了解才行。

PE结构

PE文件大致可以分为两部分,即数据管理结构及数据部分。数据管理结构包含:DOS头、PE头、节表。数据部分包括节表数据(节表数据是包含着代码、数据等内容)。详情见下图:

image-20211008200258406

1.DOS头

DOS头分为两个部分,分别是MZ头及DOS存根,MZ头是真正的DOS头部,它的结构被定义为IMAGE_DOS_HEADER。DOS存根是一段简单程序,主要是用于兼容DOS程序,当不兼容DOS程序时,输出:”this program cannot be run in DOS mode”。

2.PE头

PE头分为三个部分,分别是PE标识(IMAGE_NT_SIGNATRUE)、文件头(/images/hook技术/image_FILE_HEADER)、可选头(IMAHE_OPTION_HEADER)。PE头是固定不变的,位于DOS头部中e_ifanew字段指出位置。

3.节表

程序中组织按照不同属性存在不同的节中,如果PE中文件头的NumberOfSections值中有N个节,那么节表就是由N个节表(IMAGE_SECTION_HEADER)组成。节表主要是存储了何种借的属性、文件位置、内存位置等。位置紧跟PE头后。

4.节表数据

PE文件真正程序部分的存储位置,有几个节表就有几个节表数据,根据节表的属性、地址等信息,程序的程序就分布在节表的指定位置。位置紧跟节表后。

在了解IAT表之前,需要知道PE数据目录项的第二个结构 – 导入表

image-20211008180650945

由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL 中。当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成,其中导入地址表就指示函数实际地址。

导入表是一个结构体,如下所示

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

这里VirtualAddress为导入表的RVA(PE文件在内存中会拉伸,拉伸后的文件偏移地址称为RVA,原来的文件偏移地址称为FOA,计算公式为FOA = 导入RVA表地址 - 虚拟偏移 + 实际偏移),Size为导入表的大小。但是上面的解雇姿势说明导入表在哪里、有多大,并不是真正的导入表。VirtualAddress中存储的是RVA,如果要在FileBuffer中定位,需要将RVA转换成FOA,即内存偏移->文件偏移,通过转换过后才能得到真正的导入表,结构如下

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;

到真正的导入表这个地方,又涉及到两个表,即INT表(Import Name Table)和IAT(Import Address Table),很明显这里一个表是存储名称,一个表是存储地址的。这里又有一个注意的地方,就是在加载之前INT、IAT表里面存放的都是函数的名称并指向IMAGE_IMPORT_BY_NAME结构,如下图所示

image-20211008181630045

在PE文件加载到内存后,INT表的内容和指向的结构都不变,但是IAT表存放的就是函数的地址,也不指向IMAGE_IMPORT_BY_NAME结构了,如下所示

image-20211008183023615

hook原理

若我们找到了想要 HOOK 函数在 IAT 表中的具体位置,我们就可以通过修改该位置(该位置存放的是指针)指针的值为我们自己编写的函数的地址(在此之前肯定要把这个函数先加载到进程空间),但该函数的参数必须与被 HOOK 的函数完全一致

那么我们了解了导入表、INT表、IAT表之后,就来说说为什么要修改IAT表呢?

在调用api的时候,只要是通过LoadLibrary加载的dll,都会在IAT表里面,我们通上面了解到IAT表里面存放的地址,那么我们可以将IAT表里面的地址修改成我们自己写的函数的地址来执行我们函数的功能,这就是IAThook想要达到的目的。但是这里有很多步骤和需要用到很多api,下面就说一下IAThook的实现过程。

这里我们选择hookuser32.dll里面的MessageBoxW函数,我们这里首先定义一个自己的函数MyMessageBox,实现的功能就是获取参数和返回值

首先定义一个指针

1
typedef int (WINAPI* PFNMESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT);

然后打印指针的参数

1
printf("Argument: hwnd-%x lpText-%ws lpCaption-%ws uType-%x\n\n", hwnd, lpText, lpCaption, uType);

因为我们将IAT表里面的地址改为了我们函数的地址,但是原来IAT表里面的函数我们还是要执行才可以,所以这里执行真正的函数,这里pOldFuncAddr就是原来IAT表指向函数的地址,使用GetProcess得到原MessageBoxW的地址

1
2
3
int ret = ((PFNMESSAGEBOX)pOldFuncAddr)(hwnd, lpText, lpCaption, uType);

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

我们再获取返回值即可实现hookMessageBoxWMyMessageBox的完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int WINAPI MyMessageBox(
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;
}

再就是修改IAT表的函数编写

首先定位导入表,位于数据目录项的第二个,使用指针pImport指向导入表

1
2
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pOptionHeader->DataDirectory[1].VirtualAddress + dwImageBase);

然后使用GetModuleHandle获取进程基址

1
dwImageBase = (DWORD)::GetModuleHandle(NULL);

然后使用指针pIAT指向IAT表

1
pIAT = (PDWORD)(pImport->FirstThunk + dwImageBase);

这里作一个判断,因为我们之前通过GetProcess得到了原来MessageBoxW的地址,这里pIAT指向的也应该是原MessageBoxW的地址,所以正常情况下是相等的,使用*pIAT取值

1
if (*pIAT == pOldFuncAddr)

注意到这里有一个需要注意的地方,IAT表在默认的情况下是不能够进行写入的,如果这个地方直接修改IAT表的数据就会报错0xc0000005,所以我们需要修改IAT表为可写属性,这里用到VirtualProtect这个api

1
2
3
4
5
6
BOOL VirtualProtect(
LPVOID lpAddress, //要更改访问保护属性的页面区域的起始页面地址
SIZE_T dwSize, //要更改访问保护属性的区域的大小,以字节为单位
DWORD flNewProtect, //内存保护选项
PDWORD lpflOldProtect //指向一个变量的指针,该变量接收指定页面区域中第一页的先前访问保护值。如果此参数为NULL或未指向有效变量,则函数失败
);

image-20211008200033630

这里修改第三个参数为PAGE_EXECUTE_READWRITE,即可读可写即可

1
VirtualProtect(pIAT, 0x2000, PAGE_EXECUTE_READWRITE, &oldProtected);

然后把IAT表的地址改向我们自己定义函数的地址即可实现IAThook

1
*pIAT = dwNewAddr;

那么我们实现IAThook之后如果想把原函数的地址还原回去,就可以写一个UnSetIATHook,只要把*IAT指向旧的地址即可

在写一个TestIATHook调用一下这两个函数

1
2
3
4
5
6
7
8
int TestIATHook()
{
SetIATHook(pOldFuncAddr, (DWORD)MyMessageBox);
MessageBox(NULL, L"IAT HOOK", L"IATHOOK success!", MB_OK);
UnSetIATHook(pOldFuncAddr, (DWORD)MyMessageBox);

return 1;
}

完整代码如下

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// IAT hook.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#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 MyMessageBox(
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 TestIATHook()
{
SetIATHook(pOldFuncAddr, (DWORD)MyMessageBox);
MessageBox(NULL, L"IAT HOOK", L"IATHOOK success!", MB_OK);
UnSetIATHook(pOldFuncAddr, (DWORD)MyMessageBox);

return 1;
}

int main()
{
TestIATHook();
}

注意这里我写的时候有两个坑点,第一个地方就是打印的时候需要用tchar.h即宽字符,否则显示不完全

image-20211008190240810

再就是我一开始hook的是MessageBoxA这个api,但是获取不到返回值,改成MessageBoxW即可

image-20211008190335357

实现效果如下所示

IAThook

Inlinehook

API函数都保存在操作系统提供的DLL文件中,当在程序中使用某个API函数时,在运行程序后,程序会隐式地将API所在的DLL加载入进程中。这样,程序就会像调用自己的函数一样调用API。

在进程中当EXE模块调用CreateFile()函数的时候,会去调用kernel32.dll模块中的CreateFile()函数,因为真正的CreateFile()函数的实现在kernel32.dll模块中。

CreateFile()是API函数,API函数也是由人编写的代码再编译而成的,也有其对应的二进制代码。既然是代码,那么就可以被修改。通过一种“野蛮”的方法来直接修改API函数在内存中的映像,从而对API函数进行HOOK。使用的方法是,直接使用汇编指令的jmp指令将其代码执行流程改变,进而执行我们的代码,这样就使原来的函数的流程改变了。执行完我们的流程以后,可以选择性地执行原来的函数,也可以不继续执行原来的函数。

假设要对某进程的kernel32.dll的CreateFile()函数进行HOOK,首先需要在指定进程中的内存中找到CreateFile()函数的地址,然后修改CreateFile()函数的首地址的代码为jmp MyProc的指令。这样,当指定的进程调用CreateFile()函数时,就会首先跳转到我们的函数当中去执行流程,这样就完成了我们的HOOK了。

那么既然有了IAThook,我们为什么还要用Inlinehook呢,直接用IAThook不是更方便吗?看硬编码多麻烦。

我们思考一个问题,如果函数不是以LoadLibrary方式加载,那么肯定在导入表里就不会出现,那么IAThook就不能使用了,这就是Inlinehook诞生的条件。

硬编码

何为硬编码?

这里我就不生搬概念性的东西来解释了,说说我自己的理解。硬编码可以说就是用十六进制的字符组成的,他是给cpu读的语言,我们知道在计算机里面只有0和1,如果你要让他去读c语言的那些字符他是读不懂的,他只会读0和1,这就是硬编码。

硬编码的结构如下,有定长指令、变长指令等等一系列指令,还跟各种寄存器相关联起来,确实如果我们去读硬编码的话太痛苦了

image-20211008191650271

这里就不过多延伸了,我们在Inlinehook里面只会用到一个硬编码就是E9,对应的汇编代码就是jmp

我们的思路还是跟之前的IAThook一样,通过修改jmp跳转的地址跳转到我们的函数执行功能之后再跳转到原函数的地址执行原函数

那么这里我们首先定义一个Add函数,定义三个变量,函数的功能就是实现三个数的相加

1
2
3
4
DWORD Add(int x, int y, int z)
{
return x + y + z;
}

我们在vs里面写入Add函数并跟进反汇编进行查看,注意这里汇编代码对应的硬编码的字节数是确定的,例如55对应的就是push ebp,54对应的就是push esp,这是一个定长指令,也就是说push这个汇编代码在硬编码里面就是一字节

image-20211008192206988

那么我们要实现jmp跳转,执行的命令为jmp 0x123454678 ,对应的字节数为5(jmp也为定长指令),也就是说至少要有5个字节的空间才能够写入jmp跳转的硬编码。

image-20211008192443064

这里我们可以先将这几行代码移到一块空白的缓冲区里,再使用jmp即E9call跳转到我们想执行的函数的地址,执行完成过后再执行这几行代码之后跳转回来,如下所示。这里说一下E9的计算,E9后面要填的硬编码计算公式为 要跳转的地址 - (E9地址 + 5)

image-20211008192736083

先编写钩子函数,首先定义一个裸函数,由我们自己来平衡堆栈,因为C语言默认为stdcall,是自动平衡堆栈的,即内平栈

1
extern "C" _declspec(naked) void Hook()

我们在进行函数的hook过程中,要保证寄存器和标志寄存器的数值不能改变,否则程序可能会报错,所以这里先把标志寄存器的值压入堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_asm
{
pushad; //保留寄存器
pushfd; //保留标志寄存器
}

//读取寄存器的值
_asm
{
mov reg.EAX, eax
mov reg.EBX, ebx
mov reg.ECX, ecx
mov reg.EDX, edx
mov reg.EDI, edi
mov reg.ESI, esi
mov reg.ESP, esp
mov reg.EBP, ebp
}

然后把我们函数的三个参数压入堆栈,这里前面已经有10个值了,所以压栈的地址为esp + 28

1
2
3
4
5
6
7
8
9
_asm
{
mov eax, DWORD PTR ss : [esp + 0x28]
mov x, eax
mov eax, DWORD PTR ss : [esp + 0x2c]
mov y, eax
mov eax, DWORD PTR ss : [esp + 0x30]
mov z, eax
}

然后我们实现的功能就是把寄存器的值打印出来,当然这里是实验,实战中师傅们自行发挥

1
2
3
printf("EAX:%x EBX:%x ECX:%x EDX:%x EDI:%x ESI:%x ESP:%x EBP:%x\n\n", reg.EAX, reg.EBX, reg.ECX, reg.EDX, reg.EDI, reg.ESI, reg.ESP, reg.EBP);

printf("x:%d y:%d z:%d\n\n", x, y, z);

然后还原寄存器和标志寄存器

1
2
3
4
5
_asm
{
popfd; //还原标志寄存器
popad; //还原寄存器
}

到这里我们想要执行的函数功能就已经执行完成了,那么我们还需要将之前覆盖的汇编代码给还原回去

1
2
3
4
5
6
7
//执行之前覆盖的代码
_asm
{
push ebp
mov ebp, esp
sub esp, 0C0h
}

完成后我们再跳转回之前执行hook的地址

1
2
3
4
5
//执行完后跳转回hook地址
_asm
{
jmp RetWriteHookAddr;
}

钩子的完整代码如下

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
extern "C" _declspec(naked) void Hook()
{
_asm
{
pushad; //保留寄存器
pushfd; //保留标志寄存器
}

//读取寄存器的值
_asm
{
mov reg.EAX, eax
mov reg.EBX, ebx
mov reg.ECX, ecx
mov reg.EDX, edx
mov reg.EDI, edi
mov reg.ESI, esi
mov reg.ESP, esp
mov reg.EBP, ebp
}

//将参数压入堆栈
_asm
{
mov eax, DWORD PTR ss : [esp + 0x28]
mov x, eax
mov eax, DWORD PTR ss : [esp + 0x2c]
mov y, eax
mov eax, DWORD PTR ss : [esp + 0x30]
mov z, eax
}

printf("EAX:%x EBX:%x ECX:%x EDX:%x EDI:%x ESI:%x ESP:%x EBP:%x\n\n", reg.EAX, reg.EBX, reg.ECX, reg.EDX, reg.EDI, reg.ESI, reg.ESP, reg.EBP);

printf("x:%d y:%d z:%d\n\n", x, y, z);

_asm
{
popfd; //还原标志寄存器
popad; //还原寄存器
}

//执行之前覆盖的代码
_asm
{
push ebp
mov ebp, esp
sub esp, 0C0h
}

//执行完后跳转回hook地址
_asm
{
jmp RetWriteHookAddr;
}

}

然后我们再写SetInlineHook函数,首先判断传入的钩子函数的地址和钩子函数是否存在再往下执行

1
2
3
4
5
if (HookAddr == NULL || HookProc == NULL)
{
printf("The address is error,please try again!\n\n");
return FALSE;
}

判断是否大于5字节,小于5字节则报错空间不够写不进去

1
2
3
4
5
if (dwLength < 5)
{
printf("The alloc is too small,please try adgin!\n\n");
return FALSE;
}

然后使用之前IAThook里面的VirtualProtect修改为可读写属性

1
ReAdd = VirtualProtect((LPBYTE)HookAddr, dwLength, PAGE_EXECUTE_READWRITE, &OldProtect);

申请空间

1
szBuffer = malloc(dwLength * sizeof(char));

将内存全部首先置nop

1
2
memcpy(szBuffer, HookAddr, dwLength);
memset(HookAddr, 0x90, dwLength);

然后计算E9后面跟的硬编码,使用要跳转的地址 - E9地址 - 5

1
DWORD JmpAddr = (DWORD)HookProc - (DWORD)HookAddr - 5;

执行跳转即可

1
2
3
4
5
6
*(LPBYTE)HookAddr = 0xE9;
*(PDWORD)((LPBYTE)HookAddr + 1) = JmpAddr;

WriteHookAddr = (DWORD)HookAddr;
RetWriteHookAddr = (DWORD)HookAddr + dwLength;
dwHookFlag = 1;

再写一个解钩函数,成功后把dwHookFlag置0,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DWORD UnInlineHook(DWORD dwLength)
{
if (!dwHookFlag)
{
printf("UnInlineHook!\n\n");
return FALSE;
}

memcpy((LPVOID)WriteHookAddr, szBuffer, dwLength);
szBuffer = NULL;
dwHookFlag = 0;

return 1;
}

完整代码如下

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// InlineHook.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <windows.h>

//保留硬编码

BOOL ReAdd;
BOOL dwHookFlag;
LPVOID szBuffer;
LPBYTE HookAddr;
LPVOID HookProc;
DWORD dwLength;
DWORD WriteHookAddr, RetWriteHookAddr;
DWORD x, y, z;
DWORD Add(int x, int y, int z);
DWORD Sub(int i, int j, int k);

typedef struct _regeist
{
DWORD EAX;
DWORD EBX;
DWORD ECX;
DWORD EDX;
DWORD EBP;
DWORD ESP;
DWORD ESI;
DWORD EDI;
}regeist;

regeist reg = { 0 };


extern "C" _declspec(naked) void Hook()
{
_asm
{
pushad; //保留寄存器
pushfd; //保留标志寄存器
}

_asm
{
mov reg.EAX, eax
mov reg.EBX, ebx
mov reg.ECX, ecx
mov reg.EDX, edx
mov reg.EDI, edi
mov reg.ESI, esi
mov reg.ESP, esp
mov reg.EBP, ebp
}

_asm
{
mov eax, DWORD PTR ss : [esp + 0x28]
mov x, eax
mov eax, DWORD PTR ss : [esp + 0x2c]
mov y, eax
mov eax, DWORD PTR ss : [esp + 0x30]
mov z, eax
}

printf("EAX:%x EBX:%x ECX:%x EDX:%x EDI:%x ESI:%x ESP:%x EBP:%x\n\n", reg.EAX, reg.EBX, reg.ECX, reg.EDX, reg.EDI, reg.ESI, reg.ESP, reg.EBP);

printf("x:%d y:%d z:%d\n\n", x, y, z);

_asm
{
popfd; //还原标志寄存器
popad; //还原寄存器
}

//执行之前覆盖的代码
_asm
{
push ebp
mov ebp, esp
sub esp, 0C0h


}

//执行完后跳转回hook地址
_asm
{
jmp RetWriteHookAddr;
}

}

//HookAddr为钩子地址,HookProc为钩子函数,dwLength为要修改的硬编码长度
DWORD SetInlineHook(LPBYTE HookAddr, LPVOID HookProc, DWORD dwLength)
{

if (HookAddr == NULL || HookProc == NULL)
{
printf("The address is error,please try again!\n\n");
return FALSE;
}

if (dwLength < 5)
{
printf("The alloc is too small,please try adgin!\n\n");
return FALSE;
}

DWORD OldProtect;
ReAdd = VirtualProtect((LPBYTE)HookAddr, dwLength, PAGE_EXECUTE_READWRITE, &OldProtect);

if (!ReAdd)
{
printf("SetInlineHook_VirtualProtect - Error!\n\n");
return FALSE;
}

szBuffer = malloc(dwLength * sizeof(char));
memcpy(szBuffer, HookAddr, dwLength);
memset(HookAddr, 0x90, dwLength); //将Hook的内存全部初始化为nop(dwLength > 5)

//E9后面的值 = 要跳转的地址 - E9 - 5
DWORD JmpAddr = (DWORD)HookProc - (DWORD)HookAddr - 5;

*(LPBYTE)HookAddr = 0xE9;
*(PDWORD)((LPBYTE)HookAddr + 1) = JmpAddr;

WriteHookAddr = (DWORD)HookAddr;
RetWriteHookAddr = (DWORD)HookAddr + dwLength;
dwHookFlag = 1;

}

DWORD UnInlineHook(DWORD dwLength)
{
if (!dwHookFlag)
{
printf("UnInlineHook!\n\n");
return FALSE;
}

memcpy((LPVOID)WriteHookAddr, szBuffer, dwLength);
szBuffer = NULL;
dwHookFlag = 0;

return 1;
}

DWORD Add(int x, int y, int z)
{
return x + y + z;
}

DWORD TestInlineHook()
{

PBYTE Addr = (BYTE*)Add + 1;
Addr += *(DWORD*)Addr + 4;

SetInlineHook((LPBYTE)Addr, Hook, 9);

Add(8, 1, 5);

UnInlineHook(9);

Add(8, 1, 5);

return 0;
}


int main()
{
//Add(8, 1, 5);

TestInlineHook();

return 0;
}

实现效果如下,因为在TestInlineHook调用了两个Add函数,在第二个Add函数之前调用了UnInlineHook,最后的结果只显示了一次,所以UnInlineHook也执行成功了

Inlinehook

CATALOG
  1. 1. 基础知识
  2. 2. IAThook
  3. 3. Inlinehook