今天学一学 hook 专题最后一章,也就是 无痕 hook。

学习笔记

这个无痕 hook 是相对无痕,而不是没有方法检测。因为前面的五种 hook 方法可以在完整性检查中直接被检测出来,但是无痕 hook 是通过硬断+VEH的方式 hook 的。内存完整性检查能完美 bypass。

硬件断点在之前的 kxbook 中讲过,是通过 DR 寄存器来实现的。

DR寄存器的用途

DR0-DR3

DR0到DR3被称为“调试地址寄存器”或“地址断点寄存器”,它们非常简单,其中仅包含断点的线性地址。当该地址与指令或数据引用匹配时,将发生中断。调试寄存器DR7可用于对每个断点的条件进行更细粒度的控制。因为寄存器需要填充线性地址,所以即使关闭分页,它们也可以正常工作。在这种情况下,线性地址将与物理地址相同。

由于这些寄存器中只有4个是可用的,因此每个线程最多只能同时具有4个断点。

DR4-DR5

DR4和DR5被称为“保留的调试寄存器”。尽管它们的名称中有“保留”字样,但实际上却不总是保留的,仍然可以使用。它们的功能取决于控制寄存器CR4中DE字段的值。在启用此位后,将启用I/O断点,如果尝试访问其中一个寄存器将会导致#UD异常。但是,如果未启用DE位时,调试寄存器DR4和DR5分别映射到DR6和DR7.这样做的目的是为了与旧版本处理器的软件相兼容。

DR6

在触发硬件断点时,调试状态存储在调试寄存器DR6中。也正因如此,该寄存器被称为“调试状态寄存器”。其中包含用于快速检查某些事件是否被触发的位。
第0-3位是根据触发的硬件断点而进行设置,可以用于快速检查触发了哪个断点。

第13位称为BD,如果由于访问调试寄存器而触发当前异常,则会将其置为1。必须在DR7中启用GD位,才能触发此类异常。

第14位称为BS,如果由于单个步骤而触发当前异常,则会设置这一位。必须在EFLAGS寄存器中启用TF标志,才能触发此类异常。

第15位称为TS,如果由于当前任务切换到了启用调试陷阱标志的任务而触发了当前异常,则会设置这一位。

DR7

DR7被称为“调试控制寄存器”,允许对每个硬件断点进行精细控制。其中,前8位控制是否启用了特定的硬件断点。偶数位(0、2、4、6)称为L0-L3,在本地启用了断点,这意味着仅在当前任务中检测到断点异常时才会触发。奇数位(1、3、5、7)称为G0-G3,在全局启用了断点,这意味着在任何任务中检测到断点异常时都会触发。如果在本地启用了断点,则在发生硬件任务切换时会删除相应的位,以避免新任务中出现不必要的断点。在全局启用时不会清除这些位。
第8位和第9位分别称为LEGE,是沿用的传统功能,在现代处理器上无法执行任何操作。这些位用于指示处理器检测断点发生的确切指令。在现代处理器上,所有断点条件都是精确的。为了与旧硬件兼容,建议始终将这两个位都设置为1。

第13位被称为GD,这一位非常值得关注。如果这一位被启用,则当每一条指令尝试访问调试寄存器时,都会生成调试异常。为了将这种类型的异常与普通的硬件断点异常区分开来,在调试寄存器DR6中设置了BD标志。这一位通常用于阻止程序干扰调试寄存器。关键点在于,异常发生在指令执行之前,并且当进入调试异常处理程序时,该标志会被处理器自动删除。但是,这样的解决方案并不完美,因为它只能使用MOV指令来访问调试寄存器。这些在用户模式下是不可访问的,并且根据我的测试,GetThreadContextSetThreadContext函数不会触发该事件。这样一来,这种检测就无法在用户模式下使用。

第16-31位用于控制每个硬件断点的条件和大小。每个寄存器有4位,分为4个2位字段。前2位用于确定硬件断点的类型。仅能在指令执行、数据写入、I/O读写、数据读写时才能生成调试异常。仅有在启用了控制寄存器CR4的DE字段时,才启用I/O读写功能,否则这种情况是不确定的。大小可以使用后2位来控制,并用于指定特定地址处内存位置的大小。可用的大小有1字节、2字节、4字节和8字节。

HW Hook

跟 VEH hook的一个区别就是我们不使用 int 3 断,用DR寄存器去断,但是要注意,程序可能有多个线程,因此要对每个线程设置这样的断点。

我们在写代码的时候就注意一下,获取 该进程每一个线程的 context 然后改 DR 寄存器再设置回去。如果发现断点地址非hook地址我们可以加强hook,再次设置一下 DR 寄存器。最后还是老生常谈的问题就是 eip+=2。

然后每次增加线程都给线程设置上 hook就好了。

dllmain

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
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>

size_t MessageBoxAddr = (size_t)GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");


LONG NTAPI Handler(
struct _EXCEPTION_POINTERS* ExceptionInfo
) {
if ((size_t)ExceptionInfo->ExceptionRecord->ExceptionAddress == MessageBoxAddr)
{
const char* szSrt = "xia0ji233";
*(DWORD*)(ExceptionInfo->ContextRecord->Esp + 0x8) = (DWORD)szSrt;
ExceptionInfo->ContextRecord->Eip += 2;
return EXCEPTION_CONTINUE_EXECUTION;
}
else
{
ExceptionInfo->ContextRecord->Dr0 = MessageBoxAddr;
ExceptionInfo->ContextRecord->Dr7 = 0x405;
return EXCEPTION_CONTINUE_SEARCH;
}
}


VOID SetThreadHook(HANDLE hThread)
{
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(hThread, &ctx);
ctx.Dr0 = MessageBoxAddr;
ctx.Dr7 = 0x1;
SetThreadContext(hThread, &ctx);
}

VOID SetHook()
{
HANDLE hThreadShot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, GetCurrentProcessId());
THREADENTRY32* ThreadInfo = new THREADENTRY32;
ThreadInfo->dwSize = sizeof(THREADENTRY32);
HANDLE hThread = NULL;
while (Thread32Next(hThreadShot, ThreadInfo))
{
if (GetCurrentProcessId() == ThreadInfo->th32OwnerProcessID)
{
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, ThreadInfo->th32ThreadID);
SetThreadHook(hThread);
CloseHandle(hThread);
}
}
CloseHandle(hThread);

}


BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
AddVectoredExceptionHandler(1, Handler);
SetHook();
break;
case DLL_THREAD_ATTACH:
SetThreadHook(GetCurrentThread());
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

运行结果