avatar

Catalog
硬件断点

前言

前两篇介绍了软件断点与内存断点,这两种类型的断点都会留下明显的痕迹,也有相应的应对措施,对于软件断点,可以用CRC校验来检测;对于内存断点,可以起一个线程不断刷新PTE的属性,防止其被修改。而本篇要介绍的硬件断点就比较难防了,所以这也是硬件断点值得学习的地方。

硬件断点的原理

硬件断点的实现需要借助调试寄存器DR0~DR7,其结构如下:

DR0~DR3

这四个寄存器用于设置硬件断点,由于只有4个断点寄存器,所以最多只能设置4个硬件调试断点

DR7

Dr7是最重要的寄存器,它控制着断点的各类属性:

  • L0/G0-L3/G3:控制Dr0-Dr3是否有效,是局部还是全局的。每次异常后,Lx都被清零,Gx不清零。
  • LENx(断点长度):00(1字节),01(2字节),11(4字节)
  • R/Wx(断点类型):00(执行断点),01(写入断点),11(访问断点)

DR6

  • 硬件调试断点产生的异常是 STATUS_SINGLE_STEP(单步异常)
  • B0~B3:哪个寄存器触发的异常。

但是还有一种情况也会产生单步异常

当Eflags的TF位置1时,产生的异常也是单步异常。DR6的作用就是用来确定产生的是哪一种单步异常。当B0-B3中有值时,则可以确定是某一个硬件断点触发产生的单步异常。若B0-B3的值均为空,说明是Eflags的TF值置1产生的单步异常。

下断过程

下断时将需要下断的线性地址写入Dr0~Dr3中任意一个寄存器中,当CPU执行到该线性地址时,发现与调试寄存器中的值相同,便会断下,触发异常。注意一点,这里下断是修改当前线程Context中记录的调试寄存器的值,线程间是隔离的,因为设置硬件断点不会影响到别的线程。

硬件断点执行流程(被调试进程角度)

尽管硬件断点的下断借助了调试寄存器,但是从执行流程上来看,仍然是通过触发异常来实现调试,所以调试的本质就是异常的分发。硬件断点的执行流程也可以完全参考软件断点的执行流程,仅有开始的异常处理函数不同:

  1. CPU执行时检测到当前的地址与其中一个调试寄存器(Dr0~Dr3)中存的地址相同。
  2. 查IDT表找到对应的中断处理函数(nt!_KiTrap01)
  3. CommonDispatchException
  4. KiDispatchException
  5. DbgkForwardException
    • DbgkpSendApiMessage(x, x)

硬件断点的处理(调试器角度)

下断处

硬件断点测试时的下断处与软件断点或内存断点不同,软件断点与内存断点都可以断在OEP处,但是硬件断点不可以断在OEP处,因为此时主线程还未创建出来(参考CreateProcess创建调试关系时产生的调试事件)

而硬件断点又是基于线程的。没有线程硬件断点自然是无法触发的。因此可以换一种方式,在OEP处下一个软件断点,当触发软件断点后,会进入软件断点处理函数,这样也就有线程了,就可以触发硬件断点了(可以断在OEP+1处)。硬件断点实现如下(下断处的代码省略):

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
VOID SetHardBreakPoint(PVOID pAddress)
{
CONTEXT Context;
//1.获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebugeeThread, &Context);

//2.设置断点位置(这里选Dr0作为断点寄存器)
Context.Dr0 = (DWORD)pAddress;
Context.Dr7 |= 1;

//3.设置断点长度
Context.Dr7 &= 0xfff0ffff;

//4.设置线程上下文
SetThreadContext(hDebugeeThread, &Context);
}

处理函数

由于单步异常有两种情况,因此在处理函数内部需要判断一下是否为硬件断点导致的异常,这里的代码逻辑也相对清晰,不作具体分析。

c
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
BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO * pExceptionInfo)
{
CONTEXT Context;
BOOL bRet = FALSE;

//1.获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebugeeThread,&Context);

//2.判断是否是硬件断点导致的异常
if(Context.Dr6&0xF) //B0~B3不为空
{
//显示断点信息
printf("硬件断点 %d 0x%p\n",Context.Dr7 & 0x00030000,Context.Dr0);

//将断点去除
Context.Dr0 = 0;
Context.Dr7 &= 0xfffffffe;
}
else
{
//显示断点信息
printf("单步异常 0x%p\n",Context.Eip);

//去除单步异常
Context.EFlags &= 0xfffffeff;
}

//3.等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}
return bRet;
}

硬件Hook过检测

学到这里,我们已经掌握了异常以及调试相关的一些基础知识,这里介绍一种可以过检测的Hook手法,利用硬件断点不会修改机器码的特性,以及异常处理函数的机制实现。下面看代码:

DLL入口函数

c
1
2
3
4
5
6
7
8
int APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved)
{
if (reason == DLL_PROCESS_ATTACH)
{
SetSehHook();
}
return TRUE;
}

DLL核心逻辑

c
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
#include "windows.h"
#include "TlHelp32.h"
#include "stdio.h"
#include "limits.h"

typedef HANDLE(WINAPI *OPENTHREAD) (DWORD dwFlag, BOOL bUnknow, DWORD dwThreadId);
OPENTHREAD g_lpfnOpenThread = NULL;

DWORD g_HookAddr;
DWORD g_HookAddrOffset;

void GetInformation(PCONTEXT context)
{
printf("EAX: %X \nEBX: %X\nECX: %X\nEDX: %X\nESP: %X\nEBP: %X\nESI: %X\nEDI: %X\n",
context->Eax,
context->Ebx,
context->Ecx,
context->Edx,
context->Esp,
context->Ebp,
context->Esi,
context->Edi
);

printf("参数 \n"
"参数1: %X\n"
"参数2: %s\n"
"参数3: %s\n"
"参数4: %s\n",
(HWND) (*(DWORD*)(context->Esp + 0x4)),
(char*)(*(DWORD*)(context->Esp + 0x8)),
(char*)(*(DWORD*)(context->Esp + 0xC)),
(UINT) (*(DWORD*)(context->Esp + 0x10))
);

}

void ModifytheText(PCONTEXT debug_context)
{
char* text = (char*)(*(DWORD*)(debug_context->Esp + 0x8));
int length = strlen(text);

DWORD oldprotect = 0;
//修改PTE.P=1 PTE.R/W=0
VirtualProtect(text, length, PAGE_EXECUTE_READWRITE, &oldprotect);
//修改messagebox的信息
_snprintf(text, length, "Hook 成功");
VirtualProtect(text, length, oldprotect, &oldprotect);
}

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

//异常处理函数
LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
{

if ((DWORD)ExceptionInfo->ExceptionRecord->ExceptionAddress == g_HookAddr)
{
PCONTEXT pcontext = ExceptionInfo->ContextRecord;
//修改messagebox信息
ModifytheText(pcontext);
//获取寄存器,堆栈信息
GetInformation(pcontext);
pcontext->Eip = (DWORD)&OriginalFunc;
return EXCEPTION_CONTINUE_EXECUTION;
}

}
return EXCEPTION_CONTINUE_SEARCH;
}

void SetSehHook()
{

g_lpfnOpenThread = (OPENTHREAD)GetProcAddress(LoadLibrary(L"kernel32.dll"),"OpenThread");
g_HookAddr = (DWORD)GetProcAddress(GetModuleHandle(L"user32.dll"), "MessageBoxA");
g_HookAddrOffset = g_HookAddr + 2;
printf("MessageBoxA:%X\n", g_HookAddr);

//遍历线程 找到要Hook的地址
HANDLE hTool32 = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hTool32 != INVALID_HANDLE_VALUE)
{
THREADENTRY32 thread_entry32;
thread_entry32.dwSize = sizeof(THREADENTRY32);
HANDLE hHookThrad = NULL;
DWORD dwCount = 0;
if (Thread32First(hTool32, &thread_entry32))
{
do
{
if (thread_entry32.th32OwnerProcessID == GetCurrentProcessId())
{
dwCount++;
//Hook第一条线程
if (dwCount == 1)
{

hHookThrad = g_lpfnOpenThread(
THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION,
FALSE,
thread_entry32.th32ThreadID);

}
}
thread_entry32.dwSize = sizeof(THREADENTRY32);

} while (Thread32Next(hTool32,&thread_entry32));


//注册顶层异常处理函数
SetUnhandledExceptionFilter(ExceptionFilter);

//设置硬件断点
CONTEXT thread_context = { CONTEXT_DEBUG_REGISTERS };
thread_context.Dr0 = g_HookAddr ;
thread_context.Dr7 = 1;
SetThreadContext(hHookThrad, &thread_context);
CloseHandle(hHookThrad);

}
CloseHandle(hTool32);
}

}

这里简单说明一下这个代码的逻辑:

  1. 首先在SetSehHook函数中获取到想要Hook的地址

  2. 然后注册一个顶层异常处理函数ExceptionFilter(在没有调试器,且没有其它SEH的情况下,发生异常一定会调用这),ExceptionFilter调用了ModifytheText以及GetInformation,这两个函数通过Hook实现修改被Hook函数的参数,并打印相关寄存器中的状态。这些信息均可以通过Context获取到。

  3. 对要进行Hook的地址设置硬件断点,由于硬件断点就是异常,触发异常时便会调用注册的顶层异常处理函数ExceptionFilter,发生异常时,相应的环境信息也可以通过异常调试事件结构体ExceptionInfo->ContextRecord来获取。但是这个结构里似乎没有ContextRecord,参考下图

    (由于这是海哥的代码,所以理论上是跑的通的。这个点需要保留思考一下,二刷时得跑一遍这个代码,看看这个值是否存在)总之获取了Context保存的信息后,也就获取了寄存器以及堆栈的情况,就可以修改或者打印被Hook函数的参数了,从而达到Hook的目的。

这种依赖硬件断点的Hook,具有一点的隐蔽性,因为它不会修改被Hook的代码本身,但仍然有反制的手段,这里就不再展开。

参考资料

参考笔记:

参考教程:

  • 海哥逆向中级预习班
Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/09/19/%E7%A1%AC%E4%BB%B6%E6%96%AD%E7%82%B9/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶