Drunkmars's Blog

浅谈hook攻防

字数统计: 5.6k阅读时长: 21 min
2022/04/07

攻与防都是相对的,只有掌握细节才能更好的对抗。

基础知识

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

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

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

hook原理

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

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

几点需要说明的地方:

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

hook分类

hook从总体来说可以分成两类:

  • 修改函数代码:Inline hook
  • 修改函数地址:IAT hook、SSDT hook、IRP hook、IDT hook等

局限性

如果修改函数地址,其实是很容易被检测到的,例如IAT hook可以通过自己的代码去找到在真正内存中的IAT表,再去对比函数的真正地址,SSDT hook可以通过内核重载比对函数地址来检测到,另外一个局限性就是只能hook对应表里面的函数,IAT表只能hook IAT表导出的函数

所以在实际的应用中一般Inline hook会使用得比较多,虽然相比于修改函数地址更不易检测到,但是还是有技术手段能够进行修改函数代码的检测,本文就基于Inline hook来从防守方制定hook检测的策略和攻击方如何绕过hook检测两方面来浅谈hook技术的攻防

Inline hook

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

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

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

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

hook攻防

这里我选择使用MessageBoxA函数来进行hook的检测,因为MessageBoxA在hook之后能够比较清晰的看到结果

这里我首先使用win32资源文件来创建一个图形窗口,功能是点击开始就会弹窗(不要问我为啥不用MFC写窗口,问就是不会 /狗头),代码如下

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
// MessageBoxA.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include <windows.h>
#include "resource.h"

BOOL CALLBACK DialogProc(
HWND hwndDlg, // handle to dialog box
UINT uMsg, // message
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
)
{

switch(uMsg)
{
case WM_INITDIALOG :

return TRUE ;

case WM_COMMAND :

switch (LOWORD (wParam))
{
case IDC_BUTTON_BEGIN :

MessageBox(NULL,TEXT("使用hook来更改此界面"),TEXT("文本框"),MB_OK);

return TRUE;

}

break ;

case WM_CLOSE:
{
EndDialog(hwndDlg,0);
break;
}

}

return FALSE ;
}

int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// TODO: Place code here.

DialogBox(hInstance, MAKEINTRESOURCE (IDD_DIALOG_MAIN), NULL, DialogProc);

return 0;
}

这里生成的是一个没有任何检测代码的MessageBox测试程序

image-20220403194929538

点击开始就会弹出文本框,我们hook要达到的目的就是修改文本框里面显示的文字

image-20220403194940007

第一层

这里就不细说Inline hook的细节了,跟到关键代码

定义我们要修改文本框内的值存放到szNewText里面

image-20220403222525406

看一下MessageBox的函数结构

1
2
3
4
5
6
int MessageBox(
[in, optional] HWND hWnd,
[in, optional] LPCTSTR lpText,
[in, optional] LPCTSTR lpCaption,
[in] UINT uType
);

这里我们直接选择在函数的起始位置进行hook

image-20220403224835457

szNewText赋给eax,再压到ESP + 0x24 + 0x8的位置,0x24是pushadpushfd压入堆栈寄存器占用的内存,因为我们要修改MessageBox的第二个值,位于0x8偏移,修改值之后将寄存器还原并执行之前被覆盖的代码,然后通过jmp跳转回原函数

image-20220403213127316

然后通过HookMessageBox进行Inline 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
BOOL HookMessageBox(BOOL bOpen)
{
BOOL bRet = FALSE;
BYTE byJmpCode[PATCH_LENGTH] = {0xE9};
DWORD dwOldProtect = 0;

static BYTE byOriginalCode[PATCH_LENGTH] = {0};
static BOOL bHookFlag = FALSE;

memset(&byJmpCode[1],0x90,PATCH_LENGTH-1);

*(DWORD*)&byJmpCode[1] = (DWORD)NewMessageBox - (DWORD)dwHookAddress - 5;

memcpy(byOriginalCode, (LPVOID)dwHookAddress, PATCH_LENGTH);

if (bOpen)
{
if (!bHookFlag)
{
VirtualProtect((LPVOID)dwHookAddress, PATCH_LENGTH, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy((LPVOID)dwHookAddress, byJmpCode, PATCH_LENGTH);
::VirtualProtect((LPVOID)dwHookAddress, PATCH_LENGTH, dwOldProtect, 0);
bHookFlag = TRUE;
bRet = TRUE;
}
else
{
if (bHookFlag)
{
VirtualProtect((LPVOID)dwHookAddress, PATCH_LENGTH, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy((LPVOID)dwHookAddress, byOriginalCode, PATCH_LENGTH);
::VirtualProtect((LPVOID)dwHookAddress, PATCH_LENGTH, dwOldProtect, 0);
bHookFlag = FALSE;
bRet = TRUE;
}
}
return bRet;
}
}

然后打包成dll,通过远程线程注入来hook

image-20220403212854650

首先看一下没有hook的效果

image-20220403212927661

通过远程线程注入,可以看到已经注入成功

image-20220403212955911

然后这里再点击开始,可以看到文本框的内容已经是我们自己的内容

image-20220403213012794

这里到汇编层面去看一下hook前后的变化,首先在MessageBoxA断点

image-20220403225358352

image-20220403225410006

77D507EA的位置的汇编语句是mov edi,edi

image-20220403225425944

注入dll之后再断MessageBoxA

image-20220403225557987

77D507EA的位置已经变成了jmp Hook.1000100F,即我们自己写的jmp语句

image-20220403225618667

这里F7跟进去看看

image-20220403225654345

可以发现这里正是我们通过_asm传入的语句

image-20220403225717131

我们通过阅读MessageBox的源码可以发现,汇编语句里面是没有E9jmp指令的,也就是说我们如果在MessageBox这一块内存里面检测到了有E9jmp指令就可以认为MessageBox被hook

image-20220403231023501

这里创建一个新线程检测在MessageBox的内存空间里是否有E9,这里因为MessageBox这里只有0x48字节的空间,这里就只循环判断80字节的空间里面有没有E9即可,如果发现E9则弹窗并直接退出进程

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
DWORD WINAPI Hook_E9_Thread(LPVOID lpParameter)
{
char MessageBoxAddrString[0x10] = {0};

while(1)
{
Sleep(500);
sprintf(MessageBoxAddrString,"%x",*(char*)MessageBox);
//MessageBox(0,0,MessageBoxAddrString,0);

for (int i = 0; i < 80 ; i++)
{
if (*(unsigned char*)MessageBox + i == 0xE9)
{
MessageBoxA(NULL,TEXT("MessageBoxA is been hooked"),TEXT("MessageBoxA is been hooked"),MB_OK);
::ExitProcess(0);
}
}

}


return 0;

}


DWORD Hook_E9()
{
HANDLE hThread;
DWORD threadId;

hThread = CreateThread(NULL, 0, Hook_E9_Thread, 0, 0, &threadId);


return 0;
}

这里首先启动程序

image-20220405180054523

可以看到有两个线程

image-20220405182912324

然后通过远程线程注入dll,这里显示dll已经注入成功,但是程序会退出,证明反调试成功

image-20220405180133814

我们知道在汇编里面进行跳转一般有两个硬编码,分别为E8和E9,E8即为call,E9即为jmp

  • 短跳转:机器码为2个字节EB XX,E8是call的硬编码,XX是跳转范围-128~127
  • 长跳转:机器码为5个字节E9 XX XX XX XX,E9是jmp的硬编码,剩下4个字节表示转移偏移量

我们知道如果要对E9进行监控肯定只会在MessageBoxA这块内存空间进行监控,那么我们就可以通过E8短跳到其他地址,再通过E9长跳到我们自己的函数

第二层

在第一层的hook攻防中,我们首先用常规方式被hook,然后使用E8指令代替E9来实现了绕过hook的效果,作为蓝队人员也对检测机制进行了升级,在第二层hook攻防中,蓝方成员选择的是CRC/全代码校验。通俗来说,就是把某个API的硬编码给抠出来,单独起一个线程一直循环比对,无论是使用E8还是E9都会修改内存中的硬编码,这时候就会被检测到。CRC校验的定义如下:

image-20220405200201631

因为CRC校验的实现难度较大,这里我就使用抠出MessageBox硬编码的方式,这里MessageBox的内存范围就是77D507EA77D50832这一块

image-20220405201007436

这里有一个坑,如果对着上面的硬编码抠的话有些硬编码是没有显示出来的,这里最好是对着下面的内存窗口抠

image-20220405201125139

然后将抠出来的硬编码存放到szAPICode里面,使用memcmp函数进行比较,并用VirtualProtect来改变内存的读写情况,如果不相等则使用ExitProcess()结束进程

1
2
3
4
5
6
7
8
9
10
11
12
13
if (memcmp((LPVOID)MessageBoxAddr, szAPICode, 0x30) != 0)
{
BOOL bRet = VirtualProtect((LPVOID)MessageBoxAddr, 0x10, PAGE_READWRITE, &dwOldProtect);

if (bRet)
{
::memcpy((LPVOID)MessageBoxAddr, szAPICode, 0x30);
::VirtualProtect((LPVOID)MessageBoxAddr,0x10, dwOldProtect, &dwOldProtect);
}

MessageBoxA(NULL,TEXT("MessageBoxA is been hooked"),TEXT("MessageBoxA is been hooked by CRC"),MB_OK);
::ExitProcess(0);
}

然后检验一下,首先启动exe

image-20220405195542755

然后还是使用之前的dll进行远程线程注入,这里可以看到被检测到hook

image-20220405195732084

当检测程序使用了全代码校验的情况下,就不能使用常规的方式去进行hook,这里我们去逆向分析一下代码

硬件断点

首先定位到MessageBox,尝试直接软件断点,这里软件断点的原理就是将8B这个硬编码改为CC,也就是说在全代码校验的情况下肯定是会被拦截的

image-20220405202613273

果然不出所料拦截了

image-20220405202633018

程序直接退出

image-20220405202641558

我们在内存里面找到77D507EA

image-20220405202941203

右键下一个硬件访问断点,我们知道硬件断点的原理是通过控制dr0-dr3寄存器的值来实现异常,也就是说我们不会去修改内存里面的值

image-20220405203103871

可以看到断到了全代码的检测函数

image-20220405204919682

这里单步往下跟,到call MessageB.memcmp是比较的关键

image-20220405210205117

可以看到memcmp这个函数就是将edi和esi所在地址里面存的值存入cl和dl,如果相等继续往下走,取下一个地址里面存的值放入,不相等则直接跳转到0040114A

image-20220405210238291

00401118这个地址,如果上面都没有跳转,则又继续回到004010FD这个地址,可以发现这就是一个循环比较的过程,这里当eax的值减为0的时候结束循环

image-20220405210301403

那么我们就可以将比较的jcc语句直接置为nop,然后让最后跳转会函数起始地址的jcc语句直接改为jmp,即恒执行循环

image-20220405213048565

修改完成后我们删除硬件断点

image-20220405210449414

再次执行hook,注入成功

image-20220405213638288

线程挂起

这里因为我们的程序比较简单,通过线程很容易看出来哪个线程是检测线程,这里我们直接将检测线程挂起

image-20220405213732396

然后进行注入也可以注入成功

image-20220405213830080

第三层

我们从第二层的hook攻防可以得出两种思路,因为我们程序的逻辑比较简单,那么我们在进行逆向分析的过程中很容易找到程序运行的逻辑点,对于相对复杂的程序,这种方法是行不通的,第二种思路当然也有一定的局限性,但是总体来说实现也还是比较容易

那么作为防守方,又想出了相应的对策:

对于硬件断点修改逻辑,防守方选择写多个call嵌套,在其中的一个call里面retn,但是不是retn到初始call的下一行,而是跳转到其他地址。那么当调试者看到这个call的时候,因为逻辑很复杂,会选择直接步过,如果直接步过,因为retn的地址并不是下一行,这样就会跟丢

对于挂起线程,同时起多个线程互相检测是否都处于活动状态,例如A检测B,B检测C,C检测D,D再检测防止hook的线程,这样就会使攻击方的工作量大大提升

对于防守方的策略可以概括为:首先对API进行全代码校验,然后通过多个线程互相监控线程状态,对VirtualProtectExitProcess进行挂钩处理。那么针对防守方的策略我们可以想到两种方法进行绕过,一种是通过逆向分析检测线程的代码,通过寻找代码的漏洞进行绕过,另一种方法就是通过CPU层面的DR0-DR7寄存器实现硬件断点来触发异常。

瞬时钩子

假设这里我们已经通过逆向分析找到了主监测线程的代码,这里可以发现是先监测E9,再监测ExitProcess有没有被hook,然后再检测MessageBoxA有没有被hook,这里我们的一个想法是直接hook ExitProcess函数,但是这里因为有另外一个线程监控着主线程,如果直接hook程序也会直接退出,那么我们可以发现主检测线程的一个漏洞点,就是先检测ExitProcess有没有被hook,再继续往下走,那么我们就可以在1这个地方给ExitProcess挂钩,这里要注意VirtualProtect这个函数并不是只有我们一个线程在使用,所以这里需要写一个判断确认当前的地址,然后在2这个地方ExitProcess因为被hook的关系不会正常退出,在即将进入下一个循环的时候将钩子摘除,即可达到瞬时钩子的效果

image-20220406153008174

那么我们首先hookVirtualProtect,这里我就直接用E9来hook,如果有E9的检测就可以用E8来跳

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL bRet = FALSE;
BYTE byJmpCode[PATCH_LENGTH_VP] = { 0xE9 };
DWORD dwOldProtect = 0;

static BYTE byOriginalCode[PATCH_LENGTH_VP] = { 0 };
static BOOL bHookFlag = FALSE;

// 初始化 byJmpCode
memset(&byJmpCode[1], 0x90, PATCH_LENGTH_VP - 1);
// 存储跳转地址
*(DWORD*)&byJmpCode[1] = (DWORD)NewVirtualProtect - (DWORD)dwHookAddressVP - 5;
// 备份被覆盖的 Code
memcpy(byOriginalCode, (LPVOID)dwHookAddressVP, PATCH_LENGTH_VP);

然后把要hook的地址存到数组里面

1
2
3
VirtualProtect((LPVOID)dwHookAddressVP, PATCH_LENGTH_VP, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy((LPVOID)dwHookAddressVP, byJmpCode, PATCH_LENGTH_VP);
VirtualProtect((LPVOID)dwHookAddressVP, PATCH_LENGTH_VP, dwOldProtect, 0);

这里VirtualProtect在内存中的地址可以通过GetProcAddress配合LoadLibrary获取

1
dwHookAddressVP = (DWORD)GetProcAddress(LoadLibrary("kernel32.dll"), "VirtualProtect");

NewVirtualProtect使用asm调用VirtualProtectProcExitProcess挂钩

1
2
3
4
5
void VirtualProtectProc() {
dwHookAddressVP = (DWORD)GetProcAddress(LoadLibrary("kernel32.dll"), "ExitProcess");
OutputDebugString("VirtualProtect 开始挂钩 , hook ExitProcess successfully");
HookExitProcess(TRUE);
}

再就是hook ExitProcess,跟hook VirtualProtect的方法相同

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL bRet = FALSE;
BYTE byJmpCode[PATCH_LENGTH_EP] = { 0xE9 };
DWORD dwOldProtect = 0;

static BYTE byOriginalCode[PATCH_LENGTH_EP] = { 0 };
static BOOL bHookFlag = FALSE;

// 初始化 byJmpCode
memset(&byJmpCode[1], 0x90, PATCH_LENGTH_EP - 1);
// 存储跳转地址
*(DWORD*)&byJmpCode[1] = (DWORD)NewExitProcess - (DWORD)dwHookAddressEP - 5;
// 备份被覆盖的 Code
memcpy(byOriginalCode, (LPVOID)dwHookAddressEP, PATCH_LENGTH_EP);

同样将地址存入数组

1
2
3
VirtualProtect((LPVOID)dwHookAddressEP, PATCH_LENGTH_EP, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy((LPVOID)dwHookAddressEP, byJmpCode, PATCH_LENGTH_EP);
VirtualProtect((LPVOID)dwHookAddressEP, PATCH_LENGTH_EP, dwOldProtect, 0);

然后同样在NewExitProcess里面将钩子摘除,避免在进入下一个循环的时候被检测到

1
2
3
4
5
6
void ExitProcessProc() 
{
// 卸载瞬时 HOOK 避免被检测到
OutputDebugString("ExitProcess 开始卸载 , unhook ExitProcess successfully");
HookExitProcess(FALSE);
}

再就是hook MessageBox的函数,这里直接用之前写好的Inline hook挂钩到NewMessageBox即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void __declspec(naked) NewMessageBox() {
__asm {
pushad
pushfd
lea eax, dword ptr ds : [szNewText]
mov dword ptr ss : [esp + 0x24 + 8] , eax
popfd
popad
mov edi, edi
push ebp
mov ebp, esp
jmp dwRetAddress
}
}

然后加载dll

image-20220407155346515

首先在VirtualProtect挂钩ExitProcess,再对MessageBox进行hook,再将ExitProcess的钩子摘除,如此循环往复

image-20220407155558320

image-20220407155614350

看一下实现的效果,这里还是执行一下hook之前的程序

image-20220407151346906

然后远程线程注入瞬时钩子的dll,修改文本框内容成功,可以看到windbg里面挂钩函数跟卸载函数是不断的交替执行的,hook成功

image-20220407151625819

硬件钩子

我们知道硬件断点是基于线程的,因为每个线程的CONTEXT结构是不同的,这里首先就需要找到我们要修改dr寄存器的线程,也就是我们要hook的检测线程,找到线程之后我们通过OpenThread去获得线程的句柄,然后通过SetUnhandledExceptionFilter去注册一个异常处理函数,注册完成之后就可以更改dr寄存器的值来触发访问/写入/执行断点,然后再通过SetThreadContext放到CONTEXT结构里面即可

那么这里先找到OpenThreadMessageBoxA在内存中的地址

1
2
g_fnOpenThread = (FNOPENTHREAD)::GetProcAddress(LoadLibrary("kernel32.dll"), "OpenThread");
g_dwHookAddr = (DWORD)GetProcAddress(GetModuleHandle("user32.dll"),"MessageBoxA");

然后拍摄快照遍历线程

1
HANDLE hTool32 = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);

定位到我们要hook的线程

1
2
3
4
5
6
7
8
if (Thread32First(hTool32, &thread_entry32))
{
do
{
if (thread_entry32.th32OwnerProcessID == GetCurrentProcessId())
{
dwCount++;
if (dwCount == 1)

这里定位到线程之后我们把THREADENTRY32里面的进程ID和线程ID打印出来

1
2
3
4
char szBuffer[0x100];
ZeroMemory(szBuffer,0x100);
sprintf(szBuffer, "PID:%x - TID:%x\n", thread_entry32.th32OwnerProcessID, thread_entry32.th32ThreadID);
OutputDebugString(szBuffer);

然后通过内存中定位的OpenThread得到线程的句柄

1
hHookThread = g_fnOpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, thread_entry32.th32ThreadID);

拿到线程句柄之后我们通过SetUnhandledExceptionFilter注册一个异常处理函数MyExceptionFilter

1
SetUnhandledExceptionFilter(MyExceptionFilter);

通过ExceptionRecord里面的ExceptionCode判断错误码是否为EXCEPTION_SINGLE_STEP即单步异常以及ExceptionAddress判断是否到我们设置hook的地址,然后通过ChangeContext修改CONTEXT,再修改EIP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LONG WINAPI MyExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo)
{
if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
{
if((DWORD)pExceptionInfo->ExceptionRecord->ExceptionAddress == g_dwHookAddr)
{
PCONTEXT pContext = pExceptionInfo->ContextRecord;
ChangeContext(pContext);
pContext->Eip = (DWORD)&OriginalFunc;
return EXCEPTION_CONTINUE_EXECUTION;
}
}

return EXCEPTION_CONTINUE_SEARCH;
}

这里ChangeContext要实现的功能就是修改文本框中的内容,esp指向的是MessageBox,那么esp+8即为MessageBox的第二个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ChangeContext(PCONTEXT pContext)
{
char szBuffer[0x100];
DWORD dwOldProtect = 0;
DWORD dwLength = 0;
LPSTR lpOldText = NULL;

char szNewText[] = "SEH Hook successfully";

lpOldText = (LPSTR)(*(DWORD*)(pContext->Esp + 0x8));
dwLength = strlen(lpOldText);

VirtualProtect(lpOldText, dwLength, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy(lpOldText, szNewText, dwLength);
VirtualProtect(lpOldText, dwLength, dwOldProtect, 0);
}

然后就是Eip修改到hook+2的位置,我们知道一般API起始的位置都是mov edi,edi,不能从这个起始位置执行,否则会死循环

1
2
3
4
5
6
7
8
9
10
g_dwHookAddrOffset = g_dwHookAddr + 2;

void __declspec(naked) OriginalFunc(void)
{
__asm
{
mov edi,edi
jmp [g_dwHookAddrOffset]
}
}

然后将hook的地址放到dr0寄存器里面,设置dr7的L0位为1即局部有效,断点长度设置为1即18、19位设置为0即可,断点类型设置为访问断点对应的值为0(20、21位设置为0),这样dr7寄存器的1-31位都为0,32位为1,所以将dr7寄存器的值设置为1。然后通过SetThreadContext存入CONTEXT结构

1
2
3
4
5
threadContext.Dr0 = g_dwHookAddr;
threadContext.Dr7 = 1;

SetThreadContext(hHookThread, &threadContext);
CloseHandle(hHookThread);

首先看一下没有hook之前

image-20220406155042156

这里在ChangeContext里面将CONTEXT结构里面的EAXECX寄存器打印出来,修改文本框的内容为SEH Hook successfully

image-20220406155248686

然后注入dll,hook成功

image-20220406155110037

打印出了EAXECX

image-20220406155311624

这里再打印出PID跟TID,证明hook成功

image-20220406161724983

CATALOG
  1. 1. 基础知识
    1. 1.1. hook分类
    2. 1.2. 局限性
    3. 1.3. Inline hook
  2. 2. hook攻防
    1. 2.1. 第一层
      1. 2.1.1.
      2. 2.1.2.
      3. 2.1.3.
    2. 2.2. 第二层
      1. 2.2.1.
      2. 2.2.2.
        1. 2.2.2.1. 硬件断点
        2. 2.2.2.2. 线程挂起
    3. 2.3. 第三层
      1. 2.3.1.
      2. 2.3.2.
        1. 2.3.2.1. 瞬时钩子
        2. 2.3.2.2. 硬件钩子