0x00 前言
最近突然看到以前的一些知识 想到一些反调试的东西 于是想总结一些关于这方面的知识
0x01 方法
IsDebuggerPresent
这可能是接触到的第一个反调试技术 最早接触在<<使用OllyDbg从零开始Cracking>>这本书就接触过了 这个函数会检查用户模式的调试器是否正在调试该进程
首先看看PEB结构体的内容
1 | ntdll!_PEB |
32位的IsDebuggerPresent函数代码 我们可以看出这个函数的作用是检测PEB结构体的第三个字段的内容 在OD中可以看到这个值的 不过现在优化过的OD已经可以自动绕过这个检测了
1 | .text:0DD01E2E mov eax, large fs:18h |
64位的IsDebuggerPresent函数也有异曲同工之妙
1 | .text:000007FF3888CAA0 mov rax, gs:30h |
绕过的原理是 在调用IsDebuggerPresent函数之前 将BeingDebugged设置为0
对于32位进程
1 | mov eax, dword ptr fs:[0x30] |
64位进程
1 | DWORD64 dwpeb = __readgsqword(0x60); |
TLS回调
检查main函数中是否存在调试器并不是最好的办法 并且在分析反汇编代码时首先会观察到这个位置 在main中设置的检查机制可以通过nop指令擦除 从而接触防护机制 如果使用了CRT库 mian线程在将控制权交给main函数之前已经有个调用栈 可以在TLS回调中检查是否存在调试器 这是在可执行模块入口点调用之前会调用的函数
NtGlobalFlag
在Windows NT中 NtGlobalFlag全局变量中存在一组标志 整个系统都可以使用这个变量 为了判断进程是否由调试器启动 我们可以检查PEB结构中的NtGlobalFlag字段 32位的偏移为0x70 64位的为0xbc
绕过方法是在检查前将被调试进程的PEB的NtGlobalFlag字段设置为0
堆标志以及ForceFlags
PEB结构中包含一个指向进程堆(_HEAP结构)的一个指针
32位系统
1 | kd> dt _PEB ProcessHeap |
64位系统
1 | kd> dt _PEB ProcessHeap |
如果正在被调试 那么Flags和ForceFlags字段都会被设置成与调试相关的值:
1、如果Flags字段没有设置HEAP_GROWABLE(0x00000002标志) 那么该进程正在被调试
2、如果ForceFlags的值不为0 那么该进程正在被调试
绕过方法是设置为0
Trap标志
TF标志位 设置为1就会产生单步异常 即就是我们经常使用的单步执行
CheckRemoteDebuggerPresent以及NtQueryInformationProcess
CheckRemoteDebuggerPresent检查进程是否被另外一个进程调试 其中会调用NtQueryInformationProcess函数
当NtQueryInformationProcess函数的第二个参数为7时 CheckRemoteDebuggerPresent函数会被赋予DebugPort值
绕过的方法是 替换NtQueryInformationProcess函数的返回值
基于NtQueryInformationProcess的其他反调试技术
ProcessDebugObjectHandle
从xp开始 系统会为被调试进程创建一个“调试对象” 如果存在 就证明正在被调试
绕过方法 将ProcessDebugObjectHandle设置为0
ProcessDebugFlags
检查该标志时 会返回EPROCESS内核结构中NoDebugInherit位的取反值 如果NtQueryInformationProcess函数的返回值为0 那么该进程正在被调试
绕过方法 将ProcessDebugFlags设置为1
ProcessBasicInformation
当使用ProcessBasicInformation标志来调用NtQueryInformationProcess函数时 就会返回PROCESS_BASIC_INFORMATION结构体 这个结构体获取了父进程的名称 将其与常用调试器进行对比
1 | typedef struct _PROCESS_BASIC_INFORMATION { |
绕过方法 对于ProcessBasicInformation 将InheritedFromUniqueProcessId的值修改为其他进程的ID
断点
在调试中我们经常使用断点来调试程序 我们可以通过检测断点来检测是否被调试
软件断点
软件断点即int 3指令 向断点位置写入0xcc 当cpu执行指令时 产生中断 为了检测断点是否存在 可以计算函数的校验和
绕过方法 找到计算校验的代码 将其的返回值替换为存储校验和的所有变量的值
硬件断点
设置好断点之后 调试器将我们的地址放入4个(DR0-DR3)中的一个 其中包含断点的线性地址 系统在转换为物理地址之前比较这个地址 DR6描述调试状态 DR7通过访问模式来定义断点激活模式
绕过方法 将Dr0-Dr4寄存器的值设置为0 使用SetThreadContext函数来重设硬件断点
SEH
异常处理机制在程序内部处理异常 无需操作系统的接入 当操作系统创建新进程时 系统会将SEH帧加入其中 如果程序正在被调试 产生int 3中断后 调试器会拦截控制权 否则将会交给SEH
绕过方法 没有。。。只能通过绕过这些函数来 假如多个函数中都加入了SEH机制 那么将会很麻烦
VEH
与SEH机制相似 区别是系统使用已公开的函数来设置并删除VEH处理函数 当调试时产生中断时 出现异常 交给调试器 否则将会交给VEH 不过VEH时跟硬件断点相关的
绕过方法 通过函数hook来绕过硬件断点检测
NtSetInformationThread:从调试器中隐藏线程
NtSetInformationThread函数的一个线程信息类:ThreadHideFromDebugger 如果某个线程设置了该标志 那么该线程会停止发生关于调试事件的通知
win7 32位下
1 | kd> dt _ETHREAD HideFromDebugger |
绕过方法 使用hook完成 需要hook NtSetInformationThread函数调用 在被hook的函数中 如果正确调用的话就会返回STATUS_SUCCESS 并且不会将控制权交给原始的NtSetInformationThread函数
NtCreateThreadEx
函数原型:
1 | NTSTATUS NTAPI NtCreateThreadEx ( |
其中的CreateFlags标志位有以下参数 当线程设置flag位为4时 即为调试
1 | #define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001 |
绕过方法 使用hook在函数中重置THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER的值
句柄跟踪
xp开始具备内核对象句柄跟踪机制 当跟踪模式启动后 与句柄所有操作都会被保存到循环缓冲区中 并且当使用不存在的句柄时 就会出现EXCEPTION_INVALID_HANDLE异常 如果没有调试器 CloseHandle函数会返回FALSE
篡改堆栈段
当修改ss堆栈段(stack segment register)寄存器时 调试器会跳过指令跟踪
调试信息
win10开始 修改了OutputDebugstring函数的实现 改成带有特定参数的RaiseExcprtion调用 因此现在调试输出异常必须由调试器来处理
使用两种异常类型来检测是否存在调试器 分别为DBG_PRINTEXCEPTION_C(0x40010006)以及DBG_PRINTEXCEPTION_W(0x4001000A)
0x02 后记
基本介绍了反调试的方法和简单说了下绕过的方法 在调试的时候需要了解到这些道理