本文转自梦织未来(www.mengwuji.net)
地址:http://www.mengwuji.net/forum.php?mod=viewthread&tid=1404
作者:mengwuji
检测硬件断点,是一件很有趣的事儿,至少这玩意儿可以让xx工具威力丧失一半儿。不过对于这方面的探讨是比较少的,主要在于一般检测硬件断点的方法,不外乎用NtGetContextThread函数进行获取。
NtGetContextThread内部是向目标线程插入一个apc,然后在目标线程环境中进行获取工作。这个apc呀,实现是比较底层的。一般我们可以通过系统提供的api进行相关插入apc的相关操作。
但是鲜有某工具字节写apc插入来检测硬件断点,原因我想可能考虑到兼容性是个很大的问题......实际apc相关的底层实现,也不是很麻烦,主要是对数据结构的各种操作,自己实现一个apc也不费事儿,但还是兼容性的问题导致了一些商业工具没这样做。
那么,既然是通过NtGetContextThread获取线程上下文来检测硬件断点的话,我们在内核层拦截到这样的操作,然后处理之~~,这样就可对基本市面上大部分的商业保护进行反检测了。所以对硬件断点的处理在反反调试上就不是那么被津津乐道的话题。
有没有不用什么硬编码或者用很少硬编码就能检测硬件断点的呢?答案是存在的。
我以windows7 32位为例,进行探讨一下。
首先看看我们内核线程对象:
NtGetContextThread内部是向目标线程插入一个apc,然后在目标线程环境中进行获取工作。这个apc呀,实现是比较底层的。一般我们可以通过系统提供的api进行相关插入apc的相关操作。
但是鲜有某工具字节写apc插入来检测硬件断点,原因我想可能考虑到兼容性是个很大的问题......实际apc相关的底层实现,也不是很麻烦,主要是对数据结构的各种操作,自己实现一个apc也不费事儿,但还是兼容性的问题导致了一些商业工具没这样做。
那么,既然是通过NtGetContextThread获取线程上下文来检测硬件断点的话,我们在内核层拦截到这样的操作,然后处理之~~,这样就可对基本市面上大部分的商业保护进行反检测了。所以对硬件断点的处理在反反调试上就不是那么被津津乐道的话题。
有没有不用什么硬编码或者用很少硬编码就能检测硬件断点的呢?答案是存在的。
我以windows7 32位为例,进行探讨一下。
首先看看我们内核线程对象:
kd> dt nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 CycleTime : Uint8B
+0x018 HighCycleTime : Uint4B
+0x020 QuantumTarget : Uint8B
+0x028 InitialStack : Ptr32 Void
+0x02c StackLimit : Ptr32 Void
+0x030 KernelStack : Ptr32 Void
+0x034 ThreadLock : Uint4B
+0x038 WaitRegister : _KWAIT_STATUS_REGISTER
+0x039 Running : UChar
+0x03a Alerted : [2] UChar
+0x03c KernelStackResident : Pos 0, 1 Bit
+0x03c ReadyTransition : Pos 1, 1 Bit
+0x03c ProcessReadyQueue : Pos 2, 1 Bit
+0x03c WaitNext : Pos 3, 1 Bit
+0x03c SystemAffinityActive : Pos 4, 1 Bit
+0x03c Alertable : Pos 5, 1 Bit
+0x03c GdiFlushActive : Pos 6, 1 Bit
+0x03c UserStackWalkActive : Pos 7, 1 Bit
+0x03c ApcInterruptRequest : Pos 8, 1 Bit
+0x03c ForceDeferSchedule : Pos 9, 1 Bit
+0x03c QuantumEndMigrate : Pos 10, 1 Bit
+0x03c UmsDirectedSwitchEnable : Pos 11, 1 Bit
+0x03c TimerActive : Pos 12, 1 Bit
+0x03c SystemThread : Pos 13, 1 Bit
+0x03c Reserved : Pos 14, 18 Bits
+0x03c MiscFlags : Int4B
+0x040 ApcState : _KAPC_STATE
+0x040 ApcStateFill : [23] UChar
+0x057 Priority : Char
+0x058 NextProcessor : Uint4B
+0x05c DeferredProcessor : Uint4B
+0x060 ApcQueueLock : Uint4B
+0x064 ContextSwitches : Uint4B
+0x068 State : UChar
+0x069 NpxState : Char
+0x06a WaitIrql : UChar
+0x06b WaitMode : Char
+0x06c WaitStatus : Int4B
+0x070 WaitBlockList : Ptr32 _KWAIT_BLOCK
+0x074 WaitListEntry : _LIST_ENTRY
+0x074 SwapListEntry : _SINGLE_LIST_ENTRY
+0x07c Queue : Ptr32 _KQUEUE
+0x080 WaitTime : Uint4B
+0x084 KernelApcDisable : Int2B
+0x086 SpecialApcDisable : Int2B
+0x084 CombinedApcDisable : Uint4B
+0x088 Teb : Ptr32 Void
+0x090 Timer : _KTIMER
+0x0b8 AutoAlignment : Pos 0, 1 Bit
+0x0b8 DisableBoost : Pos 1, 1 Bit
+0x0b8 EtwStackTraceApc1Inserted : Pos 2, 1 Bit
+0x0b8 EtwStackTraceApc2Inserted : Pos 3, 1 Bit
+0x0b8 CalloutActive : Pos 4, 1 Bit
+0x0b8 ApcQueueable : Pos 5, 1 Bit
+0x0b8 EnableStackSwap : Pos 6, 1 Bit
+0x0b8 GuiThread : Pos 7, 1 Bit
+0x0b8 UmsPerformingSyscall : Pos 8, 1 Bit
+0x0b8 VdmSafe : Pos 9, 1 Bit
+0x0b8 UmsDispatched : Pos 10, 1 Bit
+0x0b8 ReservedFlags : Pos 11, 21 Bits
+0x0b8 ThreadFlags : Int4B
+0x0bc ServiceTable : Ptr32 Void
+0x0c0 WaitBlock : [4] _KWAIT_BLOCK
+0x120 QueueListEntry : _LIST_ENTRY
+0x128 TrapFrame : Ptr32 _KTRAP_FRAME
+0x12c FirstArgument : Ptr32 Void
+0x130 CallbackStack : Ptr32 Void
+0x130 CallbackDepth : Uint4B
+0x134 ApcStateIndex : UChar
+0x135 BasePriority : Char
+0x136 PriorityDecrement : Char
+0x136 ForegroundBoost : Pos 0, 4 Bits
+0x136 UnusualBoost : Pos 4, 4 Bits
+0x137 Preempted : UChar
+0x138 AdjustReason : UChar
+0x139 AdjustIncrement : Char
+0x13a PreviousMode : Char
+0x13b Saturation : Char
+0x13c SystemCallNumber : Uint4B
+0x140 FreezeCount : Uint4B
+0x144 UserAffinity : _GROUP_AFFINITY
+0x150 Process : Ptr32 _KPROCESS
+0x154 Affinity : _GROUP_AFFINITY
+0x160 IdealProcessor : Uint4B
+0x164 UserIdealProcessor : Uint4B
+0x168 ApcStatePointer : [2] Ptr32 _KAPC_STATE
+0x170 SavedApcState : _KAPC_STATE
+0x170 SavedApcStateFill : [23] UChar
+0x187 WaitReason : UChar
+0x188 SuspendCount : Char
+0x189 Spare1 : Char
+0x18a OtherPlatformFill : UChar
+0x18c Win32Thread : Ptr32 Void
+0x190 StackBase : Ptr32 Void
+0x194 SuspendApc : _KAPC
+0x194 SuspendApcFill0 : [1] UChar
+0x195 ResourceIndex : UChar
+0x194 SuspendApcFill1 : [3] UChar
+0x197 QuantumReset : UChar
+0x194 SuspendApcFill2 : [4] UChar
+0x198 KernelTime : Uint4B
+0x194 SuspendApcFill3 : [36] UChar
+0x1b8 WaitPrcb : Ptr32 _KPRCB
+0x194 SuspendApcFill4 : [40] UChar
+0x1bc LegoData : Ptr32 Void
+0x194 SuspendApcFill5 : [47] UChar
+0x1c3 LargeStack : UChar
+0x1c4 UserTime : Uint4B
+0x1c8 SuspendSemaphore : _KSEMAPHORE
+0x1c8 SuspendSemaphorefill : [20] UChar
+0x1dc SListFaultCount : Uint4B
+0x1e0 ThreadListEntry : _LIST_ENTRY
+0x1e8 MutantListHead : _LIST_ENTRY
+0x1f0 SListFaultAddress : Ptr32 Void
+0x1f4 ThreadCounters : Ptr32 _KTHREAD_COUNTERS
+0x1f8 XStateSave : Ptr32 _XSTATE_SAVE
成员很多,但是与我们本主题相关的只有一个:
+0x000 Header : _DISPATCHER_HEADER
这个是内核线程的开头成员,是个分发器对象,这个分发器呢,是实现线程同步的基础,当然与我们本题目也没什么关系,我们看看这个_DISPATCHER_HEADER数据结构。
+0x000 Header : _DISPATCHER_HEADER
这个是内核线程的开头成员,是个分发器对象,这个分发器呢,是实现线程同步的基础,当然与我们本题目也没什么关系,我们看看这个_DISPATCHER_HEADER数据结构。
kd> dt _DISPATCHER_HEADER
hal!_DISPATCHER_HEADER
+0x000 Type : UChar
+0x001 TimerControlFlags : UChar
+0x001 Absolute : Pos 0, 1 Bit
+0x001 Coalescable : Pos 1, 1 Bit
+0x001 KeepShifting : Pos 2, 1 Bit
+0x001 EncodedTolerableDelay : Pos 3, 5 Bits
+0x001 Abandoned : UChar
+0x001 Signalling : UChar
+0x002 ThreadControlFlags : UChar
+0x002 CpuThrottled : Pos 0, 1 Bit
+0x002 CycleProfiling : Pos 1, 1 Bit
+0x002 CounterProfiling : Pos 2, 1 Bit
+0x002 Reserved : Pos 3, 5 Bits
+0x002 Hand : UChar
+0x002 Size : UChar
+0x003 TimerMiscFlags : UChar
+0x003 Index : Pos 0, 1 Bit
+0x003 Processor : Pos 1, 5 Bits
+0x003 Inserted : Pos 6, 1 Bit
+0x003 Expired : Pos 7, 1 Bit
+0x003 DebugActive : UChar
+0x003 ActiveDR7 : Pos 0, 1 Bit
+0x003 Instrumented : Pos 1, 1 Bit
+0x003 Reserved2 : Pos 2, 4 Bits
+0x003 UmsScheduled : Pos 6, 1 Bit
+0x003 UmsPrimary : Pos 7, 1 Bit
+0x003 DpcActive : UChar
+0x000 Lock : Int4B
+0x004 SignalState : Int4B
+0x008 WaitListHead : _LIST_ENTRY
结构也挺多,我们主要看看:
+0x003 DebugActive : UChar
这个成员名字是不是很敏感?
好,我们现在知道了这个成员,那么我们转回NtSetContextThread函数看看。这个函数是设置硬件断点的,我们的调试器就是通过这个函数设置硬件断点,它的执行流程如下:
NtSetContextThread-->PsSetContextThread-->PspSetContextThreadInternal-->PspGetSetContextSpecialApc;
最终是执行了PspGetSetContextSpecialApc函数后,设置了线程的Context。PspGetSetContextSpecialApc函数不仅可以设置Context,也可以获取Context。
用户层设置Context的时候,它内部实际通过一个KeContextToKframes实现的,KeContextToKframes通过给定的Context设置线程的对象的内核栈的一个陷阱帧达到目的。
KeContextToKframes函数,内部会设置线程的内核栈的陷阱帧后,这个函数会判断当前设置的各个调试寄存器,从DR0~DR7,其中dr4和dr5忽略。这8个调试寄存器刚好对应一个1字节变量的8位。通过判断是dr0~dr7来设置一个BYTE类型变量。此变量设置好后,就会给赋值给我们的线程Header的DebugActive。所以我们当前线程分发器的DebugActive就是代表了哪些调试寄存器是有值的。
现在就知道了,实际我们可以通过Thread->Header.DebugActive成员判断当前是否存在硬件断点。
那么,既然有这种方式进行判断,我们就可以用一个定时器去不断的改写目标进程所有线程的Thread->Header.DebugActive为零就行了。
这貌似看起来是一种解决手段,但是如果写程序实现可能会有问题,那么我们更加深入一点儿的研究下这个Thread->Header.DebugActive;
我们拿之前的int 3断点的陷阱处理器来说吧。
KiTrap03开头会做一点儿事情:
+0x003 DebugActive : UChar
这个成员名字是不是很敏感?
好,我们现在知道了这个成员,那么我们转回NtSetContextThread函数看看。这个函数是设置硬件断点的,我们的调试器就是通过这个函数设置硬件断点,它的执行流程如下:
NtSetContextThread-->PsSetContextThread-->PspSetContextThreadInternal-->PspGetSetContextSpecialApc;
最终是执行了PspGetSetContextSpecialApc函数后,设置了线程的Context。PspGetSetContextSpecialApc函数不仅可以设置Context,也可以获取Context。
用户层设置Context的时候,它内部实际通过一个KeContextToKframes实现的,KeContextToKframes通过给定的Context设置线程的对象的内核栈的一个陷阱帧达到目的。
KeContextToKframes函数,内部会设置线程的内核栈的陷阱帧后,这个函数会判断当前设置的各个调试寄存器,从DR0~DR7,其中dr4和dr5忽略。这8个调试寄存器刚好对应一个1字节变量的8位。通过判断是dr0~dr7来设置一个BYTE类型变量。此变量设置好后,就会给赋值给我们的线程Header的DebugActive。所以我们当前线程分发器的DebugActive就是代表了哪些调试寄存器是有值的。
现在就知道了,实际我们可以通过Thread->Header.DebugActive成员判断当前是否存在硬件断点。
那么,既然有这种方式进行判断,我们就可以用一个定时器去不断的改写目标进程所有线程的Thread->Header.DebugActive为零就行了。
这貌似看起来是一种解决手段,但是如果写程序实现可能会有问题,那么我们更加深入一点儿的研究下这个Thread->Header.DebugActive;
我们拿之前的int 3断点的陷阱处理器来说吧。
KiTrap03开头会做一点儿事情:
nt!KiTrap03+0x40:
8407dc60 7596 jne nt!V86_kit3_a (8407dbf8)
8407dc62 648b0d24010000 mov ecx,dword ptr fs:[124h]
8407dc69 fc cld
8407dc6a 83652c00 and dword ptr [ebp+2Ch],0
8407dc6e f64103df test byte ptr [ecx+3],0DFh //这里ecx就是当前线程,+0x3的值实际就是Thread->Header.DebugActive
8407dc72 0f8500ffffff jne nt!Dr_kit3_a (8407db78)
上面代码实际就是Thread->Header.DebugActive & DFh,这里的df转换成2进制就是11011111,这里进行的操作就是只要DR0、DR1、DR2、DR3、DR6、DR7中有超过1位存在,那就跳转。其中第五位不为零,这里不是代表DR4是否存在,而是代表DR4和DR5是否被遗弃.....
那么这个跳转处我们看看:
那么这个跳转处我们看看:
nt!Dr_kit3_a:
8407db78 f7457000000200 test dword ptr [ebp+70h],20000h
8407db7f 750a jne nt!Dr_kit3_a+0x13 (8407db8b)
8407db81 f6456c01 test byte ptr [ebp+6Ch],1
8407db85 0f84ed000000 je nt!KiTrap03+0x58 (8407dc78)
8407db8b 0f21c3 mov ebx,dr0
8407db8e 0f21c9 mov ecx,dr1
8407db91 0f21d7 mov edi,dr2
8407db94 895d18 mov dword ptr [ebp+18h],ebx
8407db97 894d1c mov dword ptr [ebp+1Ch],ecx
8407db9a 897d20 mov dword ptr [ebp+20h],edi
8407db9d 0f21db mov ebx,dr3
8407dba0 0f21f1 mov ecx,dr6
8407dba3 0f21ff mov edi,dr7
8407dba6 895d24 mov dword ptr [ebp+24h],ebx
8407dba9 894d28 mov dword ptr [ebp+28h],ecx
8407dbac 33db xor ebx,ebx
8407dbae 897d2c mov dword ptr [ebp+2Ch],edi
8407dbb1 0f23fb mov dr7,ebx
8407dbb4 648b3d20000000 mov edi,dword ptr fs:[20h]
8407dbbb 8b9ff4020000 mov ebx,dword ptr [edi+2F4h]
8407dbc1 8b8ff8020000 mov ecx,dword ptr [edi+2F8h]
8407dbc7 0f23c3 mov dr0,ebx
8407dbca 0f23c9 mov dr1,ecx
8407dbcd 8b9ffc020000 mov ebx,dword ptr [edi+2FCh]
8407dbd3 8b8f00030000 mov ecx,dword ptr [edi+300h]
8407dbd9 0f23d3 mov dr2,ebx
8407dbdc 0f23d9 mov dr3,ecx
8407dbdf 8b9f04030000 mov ebx,dword ptr [edi+304h]
8407dbe5 8b8f08030000 mov ecx,dword ptr [edi+308h]
8407dbeb 0f23f3 mov dr6,ebx
8407dbee 0f23f9 mov dr7,ecx
8407dbf1 e982000000 jmp nt!KiTrap03+0x58 (8407dc78)
这里所做的操作主要是把各个调试寄存器保存到KiTrap03所构造的陷阱帧中。注意,这里很重要,基本KiTrap00---KiTrap0F这几个异常大致都会如此做。
我们在看看KiTrap03就去分发异常了,分发异常我们可以忽略,虽然内部也也操作了Thread->Header.DebugActive......
那么KiTrap03的最后清理工作呢,是执行了KiExceptionExit,我们看看重要的一点儿代码:
我们在看看KiTrap03就去分发异常了,分发异常我们可以忽略,虽然内部也也操作了Thread->Header.DebugActive......
那么KiTrap03的最后清理工作呢,是执行了KiExceptionExit,我们看看重要的一点儿代码:
nt!KiExceptionExit+0x4a:
8407d2a2 fa cli
8407d2a3 ebc0 jmp nt!KiExceptionExit+0xd (8407d265)
8407d2a5 8d4900 lea ecx,[ecx]
8407d2a8 8b54244c mov edx,dword ptr [esp+4Ch]
8407d2ac 64891500000000 mov dword ptr fs:[0],edx
8407d2b3 f744242cff23ffff test dword ptr [esp+2Ch],0FFFF23FFh
8407d2bb 754f jne nt!KiExceptionExit+0xb4 (8407d30c)
8407d2bd f744247000000200 test dword ptr [esp+70h],20000h
上面的test dword ptr [esp+2Ch],0FFFF23FFh就是测试我们的调试寄存器DR7其中某些值是否存在,存在的话,我们进行跳转。跳过去执行的工作就是刚好和上面保存的工作相反,把保存在陷阱帧中的各个调试寄存器恢复给我们实际的调试寄存器。
上面大致介绍了下流程,我们回过头来看看如果对Thread->Header.DebugActive清零后的影响。
如果Thread->Header.DebugActive执行了清零操作,那么进入陷阱处理器KiTrap03后,线程的各个调试寄存器不会被保存到陷阱帧中。而在退出的恢复工作中,由于陷阱帧中的DR7没有值,所以也就不会恢复各种调试寄存器。这里这个恢复工作是很重要的,调试器设置硬件断点可以用此种方式进行设置,如果是通过这种方式设置硬件断点的调试器,你所下的硬件断点会让你淹没掉之前的硬件断点...
Thread->Header.DebugActive这玩意儿也不是只会被设置一次,而是在会在你每次设置硬件断点的时候都会更新,更新的函数主要是KeContextFromKframes。
于是,我们得到了一种比较郁闷的检测硬件断点的方法。对于反检测,直接清零会不会带来什么不可预知的问题,我没试验。我想会带来问题,由于处理器控制块的调试寄存器我没找到是哪儿给赋予的,所以暂时不清楚此清零带来的问题,更多细节在以后的试验中再说吧。
上面大致介绍了下流程,我们回过头来看看如果对Thread->Header.DebugActive清零后的影响。
如果Thread->Header.DebugActive执行了清零操作,那么进入陷阱处理器KiTrap03后,线程的各个调试寄存器不会被保存到陷阱帧中。而在退出的恢复工作中,由于陷阱帧中的DR7没有值,所以也就不会恢复各种调试寄存器。这里这个恢复工作是很重要的,调试器设置硬件断点可以用此种方式进行设置,如果是通过这种方式设置硬件断点的调试器,你所下的硬件断点会让你淹没掉之前的硬件断点...
Thread->Header.DebugActive这玩意儿也不是只会被设置一次,而是在会在你每次设置硬件断点的时候都会更新,更新的函数主要是KeContextFromKframes。
于是,我们得到了一种比较郁闷的检测硬件断点的方法。对于反检测,直接清零会不会带来什么不可预知的问题,我没试验。我想会带来问题,由于处理器控制块的调试寄存器我没找到是哪儿给赋予的,所以暂时不清楚此清零带来的问题,更多细节在以后的试验中再说吧。
更多内容请关注梦织未来(www.mengwuji.net)