# 逆向常见反调试合集
# 1、反调试简介
反调试是一种用于阻碍程序动态调试的技术,这里大致说明一下反调试的工作原理:
在操作系统内部提供了一些 API (即应用程序编程接口,可以允许两个完全不同的应用程序相互 “交谈” 并交换数据,而不需要知道彼此内部是如何工作的),用于调试器调试。当调试器调用这些 API 时系统就会在被调试的进程内存中留下与调试器相关的信息。一部分信息是可以被抹除的,也有一部分信息是难以抹除的。
当调试器附加到目标程序后,用户的很多行为将优先被调试器捕捉和处理。其中大部分是通过异常捕获通信的,包括断点的本质就是异常。如果调试器遇到不想处理的信息,一种方式是忽略,另一种方式是交给操作系统处理。
到目前为止,程序有两种方式检测自己是否被调试:
(1)检测内存中是否有调试器的信息。
(2)通过特定的指令或触发特定异常,检测返回结果。
通常来说,存在反调试的程序,当检测到自身处于调试状态时,就会控制程序绕过关键代码,防止关键代码被调试,或者干脆直接退出程序。
# 2、反调试的类型
# (1)API 反调试
Windows 内部提供了一些用于检测调试器的 API。
其中一个 API 是 IsDebuggerPresent,原型为:
1 | BOOL IsDebuggerPresent(); |
返回值为 1 表示当前进程被调试的状态,反之为 0。
另一个常用 API 是 CheckRemoteDebuggerPresent,原型为:
1 | BOOL CheckRemoteDebuggerPresent(HANDLE hProcess, PBOOL pbDebuggerPresent); |
返回值为 1 表示当前进程被调试的状态,反之为 0.
# (2)PEB 反调试
当程序处于 3 环 (低权限) 时, FS:[0] 寄存器指向 TEB (Thread Environment Block),即线程环境块结构体,TEB 向后偏移 0x30 字节的位置保存的是 PEB (Process Environment Block ), 即进程环境块的结构体地址。PEB 中的部分成员是与调息相关的成员,当调试器通过 Windows 提供的 API 调试目标程序时,Windows 会将一部分调试信息写人这个结构体中。
1 | kd>dt_TEB |
如 NtGlobalFlag
在 32 位机器上,NtGlobalFlag 字段位于 PEB (进程环境块)0x68 的偏移处,64 位机器则是在偏移 0xBC 位置。该字段的默认值为 0. 当调试器正在运行时,该字段会被设置为一个特定的值。尽管该值并不能十分可信地表明某个调试器真的有在运行,但该字段常出于该目的而被使用.
1 | mov eax, fs:[30h] → [eax+2] (BeingDebugged);[eax+68h] (NtGlobalFlag) |
该字段包含有一系列的标志位。由调试器创建的进程会设置以下标志位:
1 | FLG_HEAP_ENABLE_TAIL_CHECK (0x10) |
因此,可以检查这几个标志位来检测调试器是否存在。比如用形如以下的 32 位的代码在 32 位机器上进行检测:
1 | mov eax, fs:[30h] ;Process Environment Block |
以下是 64 位的代码在 64 位机器上的检测代码:
1 | push 60h |
要注意的是,如果是一个 32 位程序在 64 位机器上运行,那么实际上会存在两个 PEB: 一个是 32 位部分的而另一个是 64 位. 64 位的 PEB 的对应字段也会像在 32 位的那样而改变.
于是我们就还有以下的,用 32 位的代码检测 64 位机器环境:
1 | mov eax, fs:[30h] ; Process Environment Block |
有以下 2 种方法来绕过 NtGlobalFlag 的检测:
手动修改标志位的值 (FLG_HEAP_ENABLE_TAIL_CHECK, FLG_HEAP_ENABLE_FREE_CHECK, FLG_HEAP_VALIDATE_PARAMETERS);
在 Windbg 禁用调试堆的方式启动程序 (windbg -hd program.exe)
在 PEB 结构体中,BeingDebugged、ProcessHeap、NtGlobalFlag 是与调试信息相关的三个重要成员。
BeingDebugged:当进程处于被调试状态时,值为1,否则为0。
ProcessHeap:指向Heap结构体,偏移0xC处为Flags成员,偏移0x10处为ForceFlags成员。通常情况下,Flags的值为2.ForceFlags的值为0,当进程被调试时会发生改变。
NGlobalFlag:占四个字节,默认值为0。当进程处于被调试状态时,第一个字节会被置为0x70。
通过 FS.Base 能够定位到 TEB,再通过 TEB+0x30 能够定位 PEB。通过在内存中检测或修改相关成员的值,便可达到反试、反反调试的效果。(如 fs:[30h] 指令的作用就是从 TEB 中拿到了 PEB 的钥匙,然后进去看了看 BeingDebugged 是 0 还是 1)
# (3)TLS 反调试
TLS (Thread Local Storage),即线程局部存储是 Windows 提供的一种处理机制,每进行一次线程切换,便会调用一次 TLS 回调。它本意是想给每个线程都提供访问全局变量的机会。例如,需要统计当前程序进行了多少次线程切换,但并不想让其他线程访问到这个计数变量,使用 TLS 进行计数,便能够解决这个问题,一个程序能设置多个 TLS.
由于进程在启动时至少需要创建一个线程来运行,因此在调用 main 函数前就会调用一次 TLS 回调。利用这个特点,在 TLS 回调中写入与反调试相关的代码,便可悄无声息地令调试器失效。
1 |
|
TLS_Callback:TLS 的回调函数,每当一个新线程创建时,或者进程加载时,它都会被调用。
IsDebuggerPresent:这是 Windows 提供的 API,用来检查当前进程是否被调试。
#pragma const_seg:用于指定 TLS 回调函数的位置,它被放在.CRT$XLB 节中,这样 Windows 在加载时会自动执行。
# (4)进程名反调试
当使用调试器调试程序时,调试器是一个独立的进程,运行在内存中。若在程序执行到某一反调试方法。阶段时遍历当前系统中的进程列表,检测是否存在与调试器相关的进程名,也是一种可行的方法。
示例如下:
1 |
|
调试器进程名列表:在 debuggerNames 数组中列出了常见的调试器进程名(如 ollydbg.exe, x64dbg.exe 等)。可以根据需要添加更多的调试器进程名。
CreateToolhelp32Snapshot:这是一个 Windows API,用于创建系统中所有进程的快照,以便遍历这些进程。
Process32First 和 Process32Next:这些函数用于遍历进程快照中的每一个进程。
tolower 为了匹配时忽略大小写,将进程名全部转换为小写进行比较。
ExitProcess:如果发现调试器进程,程序直接退出。
代码在执行时遍历系统中所有正在运行的进程,并检查是否有已知的调试器进程在运行。如果发现某个调试器进程(如 ollydbg.exe),则程序会直接退出,否则会继续运行。这种方法可以用来检测外部调试器是否正在运行,但它不是百分之百可靠,因为高级调试器可能会通过修改进程名或隐藏自己来规避检测。
# (5)窗口名反调试
检测已打开窗口的窗口也是一种较为常用的反调试手段。示例代码如下:
1 |
|
EnumWindows:这是 Windows API,允许遍历系统中所有的顶层窗口。每找到一个窗口,就会调用回调函数 EnumWindowsProc。
EnumWindowsProc:这是枚举窗口的回调函数。通过 GetWindowTextA 函数获取窗口标题,然后使用 strstr 来判断窗口名是否包含已知调试器窗口名称。
ExitProcess:如果检测到调试器窗口,程序直接退出。
有些调试器允许用户自定义窗口名,因此该方法并非完全可靠。高级调试器同样可能会通过修改窗口名或隐藏窗口来规避检测。例如,OD 在刚启动时的窗口名为 “OllyDbg - [CPU]”,而加载程序后会有所改变,但前几个字节仍然为 “OllyDbg”,对于这类窗口,规定字符串的检测长度往往能取得不错的效果。
# (6)时间戳反调试
正常情况下,CPU 的执行速度是非常快的,每秒能执行数条指令,每条指令的执行时间非常短。而在调试状态下,由于软件中断、单步调试等因素,可能会造成指令间的执行间隔远大于正常时间,分别记录两条指令执行前后的时间戳,利用时间戳的差值便能够判断当前进程是否处于被调试状态。 时间戳反调试有三种常用手段:
rdtsc: 汇编指令,能够以纳秒级记录系统启动以来的时间戳,返回值保存在EDX:EAX(高位保存到EDX,低位保存到EAX)中。
QueryPerformanceCounter:能够以微秒为单位高精度计时。
GetTickCount:返回值为自系统启动以来所经过的毫秒数。
例如:
1 |
|
程序执行完内联汇编后,会计算前后时间差。如果时间差超过 16 毫秒(0x10),程序会调用 ExitProcess 强制退出。这可以用于反调试:调试器往往会减缓程序的执行速度,因此通过这种时间检测方法可以检测到调试行为。
# (7)硬件断点检测反调试
硬件断点是调试器常用的手段之一,它通过 CPU 的调试寄存器(如 DR0-DR7)设置断点。可以通过检查这些寄存器是否有断点设置来检测调试器。使用 GetThreadContext API 来获取当前线程的上下文,检查调试寄存器(DR0 到 DR3)是否设置断点。
1 | CONTEXT ctx = {}; |
# (8)异常处理反调试
调试器通常会捕获异常,反调试技术可以通过故意引发异常并检查其处理方式来检测调试器。通过引发 INT 3 指令(断点中断)或其他异常(如除零异常),查看是否有异常处理程序被插入,然后使用 SetUnhandledExceptionFilter 设置自定义的异常处理程序。
1 | __try { |
# (9)单步检测反调试
单步检测反调试是一种通过检测 CPU 的单步执行(Trap Flag, TF)来判断是否有调试器介入的技术。当调试器单步执行目标程序时,CPU 的 TF 标志会被设置为 1,这会导致在每条指令执行完后触发一个调试中断。因此,程序可以通过监控 TF 标志的变化来检测调试行为。
Trap Flag(TF):当 EFLAGS 寄存器中的 TF 标志被置为 1 时,CPU 会进入单步模式,每执行一条指令后都会产生一个调试中断(INT 1)。通过检查和控制 TF 标志,可以判断程序是否被调试器单步执行。如果调试器处于单步调试模式,TF 标志会被置 1,程序可以利用这一特性检测调试行为。
例如:
1 |
|
如果程序在执行 nop 指令后,TF 标志依然保持为 1,则说明程序未被调试,输出 “No debugger detected (TF still set)”。如果程序发现 TF 标志被清除(调试器可能重置了该标志),则输出 “Debugger detected (TF cleared)”,并终止程序执行。