[原创]抛砖引玉—硬件断点的检测和反检测
硬件断点的检测和反检测
1、硬件断点设置的方法
有矛必有盾,有盾必有矛,说硬件断点的检测和反检测之前我们先来谈谈硬件断点设置的常规方法
方法1:
采用SetThreadContext直接设置指定线程的调试寄存器,由于这种方法简单实用,深受广大人民群众喜爱,具体怎么搞,附件代码中的SetHwBreakPoint函数有详细的code。
方法2:
采用SEH或者VEH等异常处理技术设置硬件断点,原理是自己搞个SEH或者VEH,然后自己触发一个异常(比如除0),跳到自己的异常处理函数中,这里只说SEH的异常处理函数结构:SEHHandler proc C _pstExceptionRecord:DWORD, _pstSEH:DWORD, _pstContext:DWORD, _pDispatcherContext:DWORD。第3个参数_pstContext就是异常线程的所有调试器结构指针,聪明的朋友肯定就知道该怎么搞了。不清楚的朋友可以去看看SEH实现的过程。
2、硬件断点的检测方法
方法1的检测方法:
GetThreadContext就是SetThreadContext的盾,把调试寄存器取过来送纪委检查检查就知道有没有人在干坏事了。
方法2的检测方法:
其实同样是用方法2设置硬件断点的手段,只不过在SEH的异常处理函数中用_pstContext取调试寄存器送纪委就是了,当然用SEH只能检测当前线程的,要检测整个程序的还是用VEH好,方法类似。我附件里的SEHCheckHardPoint和SEHHandler就是一个简单的例子。
;名称:SEHCheckHardPoint
;功能:构建一个SEH,检测硬件断点
;参数:无
;返回:无
SEHCheckHardPoint proc
assume fs:nothing
push offset SEHTest_Ret ;压入安全代码的地址
push ebp ;保存ebp
push SEHHandler ;压入SEH回调函数
push fs:[0] ;保存原SEH
mov fs:[0], esp ;设置当前SEH
xor eax, eax
mov DWORD ptr [eax], 1 ;人为的产生一个异常
SEHTest_Ret:
pop fs:[0] ;恢复原SEH
add esp, 12 ;平衡堆栈
ret
SEHCheckHardPoint endp
;名称:SEHHandler
;功能:SEH的异常处理函数,为cdecl调用
;参数:_pstExceptionRecord = 异常信息结构指针
;参数:_pstSEH = SEH结构指针
;参数:_pstContext = 发生异常线程的寄存器结构指针
;参数:_pDispatcherContext
;返回:无
SEHHandler proc C _pstExceptionRecord:DWORD, _pstSEH:DWORD, _pstContext:DWORD, _pDispatcherContext:DWORD
pushad
mov ebx, _pstSEH
mov eax, [ebx + 8]
mov esi, _pstContext
assume esi:ptr CONTEXT
mov [esi].regEbp, eax ;恢复ebp
mov eax, [ebx + 12]
mov [esi].regEip, eax
invoke VK_PrintfMsg, CTXT("Dr0 = 0x%X"), [esi].iDr0 ;显示Dr0的值,如果不是0,说明被设硬件断点了
assume esi:nothing
popad
mov eax, ExceptionContinueExecution
ret
SEHHandler endp
这个例子只简单的检查Dr0是否被设置了硬件断点,当然你也可以检查多个。
3、硬件断点的反检测方法
方法1的反检测方法:
方法1的反检测方法也很简单,估计很多朋友都会,就是在内核中Hook NtGetThreadContext,检测是否是我们的目标进程,如果是则将调试寄存器全部清0后返回。当然也有针对这种方法的检测方法,就是用SetThreadContext自己设置一下调试寄存器,然后调用GetThreadContext检查是不是和自己设置的相符,如果不相符或者为0说明有人动过手脚了。这种反检测方法ring3下也可以搞Hook 目标进程的GetThreadContext,只是有些程序也会检测。
方法2的反检测方法:
方法2的检测方法没怎么找到资料,个人探索了下。开始的方法是逆向检测进程,用个人写的工具“APIMonitor”里就有检测异常的功能,可以找到产生异常的地址,找到异常后就可以找到异常处理函数,然后把异常处理函数干掉就可以了。当然这种方法通用性差,说不定人家还把异常处理函数给加花或者VMP什么的,还不定好逆向。这里给大家分享一种个人通用方法:
Hook KiUserExceptionDispatcher:
KiUserExceptionDispatcher的函数定义:
VOID KiUserExceptionDispatcher(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT Context);
这个是我电脑上反汇编的KiUserExceptionDispatcher头部的一些代码:
7C92EAEC > 8B4C24 04 mov ecx,dword ptr ss:[esp+0x4] 7C92EAF0 8B1C24 mov ebx,dword ptr ss:[esp] 7C92EAF3 51 push ecx ; pContext 7C92EAF4 53 push ebx ; 压入pExceptionRecord 7C92EAF5 E8 C78C0200 call ntdll.7C9577C1 ; 调用RtlDispatchException 7C92EAFA 0AC0 or al,al ; 判断返回是否为0,即是否调用成功 7C92EAFC 74 0C je short ntdll.7C92EB0A ;失败则跳 7C92EAFE 5B pop ebx 7C92EAFF 59 pop ecx 7C92EB00 6A 00 push 0x0 7C92EB02 51 push ecx 7C92EB03 E8 11EBFFFF call ntdll.ZwContinue ; 调用RtlDispatchException成功则继续ZwContinue执行异常原来的代码
KiUserExceptionDispatcher是ring3下的异常分发函数,首先他会调用RtlDispatchException,对VEH,SEH的异常处理函数所传递的Context结构指针就是这个函数干的,所以我们在调用此函数之前把调试寄存器清0,但在KiUserExceptionDispatcher将调试寄存器清0会将我们设置的硬件断点清除,所以我们在调用完RtlDispatchException后要再次设置我们的硬件断点。
;名称:HookKiUserExceptionDispatcher
;功能:Hook KiUserExceptionDispatcher的函数头,跳至Hooked_KiUserExceptionDispatcher去执行
;参数:无
;返回:失败返回0,成功返回1
HookKiUserExceptionDispatcher proc
local @hNtdll:DWORD
local @dwOldProtect:DWORD
invoke GetModuleHandle, CTXT("ntdll.dll")
mov @hNtdll, eax
invoke GetProcAddress, @hNtdll, CTXT("KiUserExceptionDispatcher")
test eax, eax
jz HookKiUserExceptionDispatcher_Ret
mov pKiUserExceptionDispatcher, eax
invoke VirtualProtect, pKiUserExceptionDispatcher, 7, PAGE_EXECUTE_READWRITE, addr @dwOldProtect
test eax, eax
jz HookKiUserExceptionDispatcher_Ret
mov eax, offset Hooked_KiUserExceptionDispatcher ;hook KiUserExceptionDispatcher跳转至Hooked_KiUserExceptionDispatcher执行
sub eax, pKiUserExceptionDispatcher
sub eax, 5
mov edx, pKiUserExceptionDispatcher
mov BYTE ptr [edx], 0E9h
inc edx
mov [edx], eax
mov eax, 1
HookKiUserExceptionDispatcher_Ret:
ret
HookKiUserExceptionDispatcher endp
;名称:Hooked_KiUserExceptionDispatcher
;功能:将调试寄存器清0,然后调用RtlDispatchException,调用完后再次设置硬件断点,返回原函数继续执行
Hooked_KiUserExceptionDispatcher:
mov eax, [esp + 4] ;取pContext,并将调试寄存器置0
assume eax:ptr CONTEXT
mov [eax].iDr0, 0
mov [eax].iDr1, 0
mov [eax].iDr2, 0
mov [eax].iDr3, 0
mov [eax].iDr6, 0
mov [eax].iDr7, 0
assume eax:nothing
mov ecx, DWORD ptr [esp + 4] ;调用原代码跳回执行KiUserExceptionDispatcher
mov ebx, DWORD ptr [esp]
push ecx
push ebx
mov eax, cAddrRtlDispatchException
call eax ;调用RtlDispatchException
mov ecx, DWORD ptr [esp + 4] ;重新设置硬件断点
assume ecx:ptr CONTEXT
push HardPointAddr
pop [ecx].iDr0
mov [ecx].iDr7, 405h
assume ecx:nothing
push edx
mov edx, pKiUserExceptionDispatcher
add edx, 14
mov [esp], edx ;跳过11 Hook的call,执行下一条指令
ret
注:RtlDispatchException这里我用的是硬编码,不同的电脑上可能会有不同,测试代码的朋友注意修正
4、测试代码:
InitDialog proc _hWnd:DWORD invoke GetDlgItem, _hWnd, IDC_BTN1 mov hButton1, eax invoke GetDlgItem, _hWnd, IDC_BTN2 mov hButton2, eax mov eax, _hWnd mov hDlgmain, eax ;...................此处添加一些需要初始化的代码........................... mov HardPointAddr, offset ProcButton2Click invoke SetHwBreakPoint, HardPointAddr invoke HookKiUserExceptionDispatcher ;...................此处添加一些需要初始化的代码........................... ret InitDialog endp

这里设置硬件断点的位置就是程序“测试断点”按钮的处理函数首地址,我们点击“检测断点”按钮会弹出如下消息框:

可以看出DR0寄存器为0,说明没检测到
但我们点击“测试断点”按钮,就华丽丽的弹出微软的错误框了,错误码是0x80000004,就是硬件断点异常

我们点“取消”,来到OD中就可以看到0x004012B2就是我们“测试断点”按钮的处理函数首地址了,F9一下,就会弹出“如果硬件断点继续执行,就会看到此消息”的消息了

这里有朋友会说,谁知道是不是你的反检测成功了还是你的检测方法垃圾啊,别急,我们把invoke HookKiUserExceptionDispatcher注释掉,再次编译后点击“测试断点”弹出如下消息框:

这是DR0就不等于0了,说明检测方法是没有问题的。
有兴趣的朋友可以下载附件中的代码拿去编译一下,我是用MASM Plus编译的。
代码: 硬件断点检测与反检测.zip
