前言
前两篇介绍了软件断点与内存断点,这两种类型的断点都会留下明显的痕迹,也有相应的应对措施,对于软件断点,可以用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中记录的调试寄存器的值,线程间是隔离的,因为设置硬件断点不会影响到别的线程。
硬件断点执行流程(被调试进程角度)
尽管硬件断点的下断借助了调试寄存器,但是从执行流程上来看,仍然是通过触发异常来实现调试,所以调试的本质就是异常的分发。硬件断点的执行流程也可以完全参考软件断点的执行流程,仅有开始的异常处理函数不同:
- CPU执行时检测到当前的地址与其中一个调试寄存器(Dr0~Dr3)中存的地址相同。
- 查IDT表找到对应的中断处理函数(nt!_KiTrap01)
- CommonDispatchException
- KiDispatchException
- DbgkForwardException
- DbgkpSendApiMessage(x, x)
硬件断点的处理(调试器角度)
下断处
硬件断点测试时的下断处与软件断点或内存断点不同,软件断点与内存断点都可以断在OEP处,但是硬件断点不可以断在OEP处,因为此时主线程还未创建出来(参考CreateProcess创建调试关系时产生的调试事件)
而硬件断点又是基于线程的。没有线程硬件断点自然是无法触发的。因此可以换一种方式,在OEP处下一个软件断点,当触发软件断点后,会进入软件断点处理函数,这样也就有线程了,就可以触发硬件断点了(可以断在OEP+1处)。硬件断点实现如下(下断处的代码省略):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| VOID SetHardBreakPoint(PVOID pAddress) { CONTEXT Context; Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; GetThreadContext(hDebugeeThread, &Context); Context.Dr0 = (DWORD)pAddress; Context.Dr7 |= 1; Context.Dr7 &= 0xfff0ffff; SetThreadContext(hDebugeeThread, &Context); }
|
处理函数
由于单步异常有两种情况,因此在处理函数内部需要判断一下是否为硬件断点导致的异常,这里的代码逻辑也相对清晰,不作具体分析。
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; Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; GetThreadContext(hDebugeeThread,&Context); if(Context.Dr6&0xF) { 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; } while(bRet == FALSE) { bRet = WaitForUserCommand(); } return bRet; }
|
硬件Hook过检测
学到这里,我们已经掌握了异常以及调试相关的一些基础知识,这里介绍一种可以过检测的Hook手法,利用硬件断点不会修改机器码的特性,以及异常处理函数的机制实现。下面看代码:
DLL入口函数
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核心逻辑
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; VirtualProtect(text, length, PAGE_EXECUTE_READWRITE, &oldprotect); _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; 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); 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++; 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); }
}
|
这里简单说明一下这个代码的逻辑:
首先在SetSehHook函数中获取到想要Hook的地址
然后注册一个顶层异常处理函数ExceptionFilter(在没有调试器,且没有其它SEH的情况下,发生异常一定会调用这),ExceptionFilter调用了ModifytheText以及GetInformation,这两个函数通过Hook实现修改被Hook函数的参数,并打印相关寄存器中的状态。这些信息均可以通过Context获取到。
对要进行Hook的地址设置硬件断点,由于硬件断点就是异常,触发异常时便会调用注册的顶层异常处理函数ExceptionFilter,发生异常时,相应的环境信息也可以通过异常调试事件结构体ExceptionInfo->ContextRecord来获取。但是这个结构里似乎没有ContextRecord,参考下图。
(由于这是海哥的代码,所以理论上是跑的通的。这个点需要保留思考一下,二刷时得跑一遍这个代码,看看这个值是否存在)总之获取了Context保存的信息后,也就获取了寄存器以及堆栈的情况,就可以修改或者打印被Hook函数的参数了,从而达到Hook的目的。
这种依赖硬件断点的Hook,具有一点的隐蔽性,因为它不会修改被Hook的代码本身,但仍然有反制的手段,这里就不再展开。
参考资料
参考笔记:
参考教程: