反调试笔记
反调试是逆向工程中的常见对抗手段,这篇笔记总结了一些各种平台常见的调试检测手段,随机更新中
IsDebuggerPresent()
特征最明显的反调试,因为是windows系统api所以也没法隐藏,直接读NtCurrentPeb上的beingdebugged标志,如果被调试就返回1
*(NtCurrentPeb+0x2) ( beingdebugged Flag )
是上面这条的变体,实际上beingdebugged标志就在NtCurrentPeb+0x2的位置上
*(NtCurrentPeb+0x68) ( NtGlobalFlag )
当进程被调试器启动时这个标志会被设置为0x70 (实际上是三个堆相关的标志被设置为111)
NtCurrentPeb()->ProcessParameters->DebugFlags
进程启动参数,这个标志为1时说明在被调试
NtCurrentPeb()->ProcessParameters->DebugPort
调试对象的句柄,如果这个值不为NULL说明有对象在调试该进程
NtCurrentPeb()->ProcessHeap
这个位置有两个标志Flags和ForceFlags用于告诉内核这个堆是否是在调试器中被创建的,正常情况下Flag=2,ForceFlags=0
32位windows中 Flags位于ProcessHeap+0xC ,ForceFlags位于ProcessHeap+0x10
64位windows中 Flags位于ProcessHeap+0x70 ,ForceFlags位于ProcessHeap+0x74
OutputDebugString()
这个函数会向调试器中输出一段字符串,如果当前程序没有被调试就会更新当前程序的错误码
可以配合SetLastError先将错误码设置为一个预设值,然后调用该函数更新错误码,再用GetLastError获取新的错误码,如果新的错误码和旧的一样说明这个函数没有报错,就说明调试器存在
ZwSetInformationThread(ThreadHideFromDebugger)
这个函数可以设置当前线程的一些信息,主要是优先级之类的,他的参数是一个枚举类,其中ThreadHideFromDebugger这个选项会把当前线程从调试器中抹掉,具体实现就是把DebugPort置为NULL(疑似有点草台)
扫描0xCC (int 3 指令)
软件断点的工作原理是将断点位置的指令替换为 int 3 指令(编码为0xCC),当调试器遇到 int 3 时就调用调试异常中断执行
在程序执行时扫描全程序是否有0xCC就可以检测到是否存在软件断点
该方法无法检测硬件断点,因为硬件断点并不修改程序指令
注意到软件断点需要修改指令,所以烧录在rom中的程序无法下软件断点
PS1. 硬件断点
硬件断点是用寄存器实现的断点,直接在调试寄存器中保存断点的地址,当PC跑到对应位置时中断执行,断点的数量受限于调试寄存器的数量
GetThreadContext(hThread,lpContext)
这个函数可以获取对应线程的上下文信息,如果lpContext中读取的DRx系列寄存器(调试寄存器)的值不为0说明存在硬件断点,可以据此检测硬件断点
哈希校验
直接计算整个程序机器码的哈希值,与正常情况下的预设值进行校验,因为软件断点会插入int 3,所以调试状态下哈希值会有不同
rdstc
rdstc 可以获取cpu开机起经历的时钟周期,调用两次该函数就可以获取一段操作经历的时间,如果时间过长可以猜测存在调试器,因为调试器会极大影响程序性能
父进程检测
一般正常用户态运行的程序只可能是由几个系统服务或者已知的程序启动的(cmd,explorer.exe…..),可以通过th32ParentProcessID获取父进程的PID,在与系统中所有进程的PID进行比对,如果父进程不在上述的范围内则可以推断进程在被调试
基于VEH,SEH的反调试
这个反调试其实还是利用了上述反调试的逻辑,只不过把检测的代码藏在异常处理的逻辑里,然后再手动触发一个异常来进入这段逻辑,一般直接搜索VEH就可以搜到设置veh的地方,进而找到用户自定义的handler,SEH可以去反汇编软件中寻找except包裹的部分。
因为异常处理逻辑通常要跳过垃圾代码,所以这种异常通常不会选择传递给调试器(就算传给调试器调试器也会卡在垃圾代码里),所以也可能会把一些加密的逻辑藏在异常处理中(说白了还是增加翻代码的难度,并没有什么新鲜的)
SetUnhandledExceptionFilter
SetUnhandledExceptionFilter()函数接受一个winapi的异常过滤器函数作为参数用来设置自定义的异常过滤器,当正常运行时,windows让异常过滤器先接管未处理的异常,此时过滤器内可以做加密相关的操作,也可以做处理异常的操作,但是调试模式下所有的异常就会传递给调试器(现代调试器已经有选择由app处理异常的选项),此时我们又没有写处理异常的逻辑,调试器就会直接卡住
1 | SetUnhandledExceptionFilter(CustomUnhandledExceptionFilter); |