windows 反调试
API 反调试
IsDebuggerPresent
一个很简单的函数,用于检测当前进程是否被调试
- 函数原型
1 |
|
如果当前进程运行在调试器的上下文中,返回值为非零值,否则返回值为零
工作原理
每个 Windows 进程中都有一个叫进程环境块(Process Environment Block, PEB)的内核数据结构,PEB 中包含了很多关于进程状态的信息
其中有一个很重要的字段叫 BeingDebugged(偏移量 0x02),当调试器附加到一个进程时,操作系统会自动将这个标志位设置为 1,IsDebuggerPresent() 函数内部所作的就是读取当前进程的 PEB 中的 BeingDebugged 字段,并返回其值绕过方法
- 在调试器中,直接在 IsDebuggerPresent 函数返回后( ret 指令处)将寄存器(如 RAX/EAX) 的值从 1 改为 0
- 直接 patch 掉 调用 IsDebuggerPresent 的相关汇编
- 修改 PEB:在调试器中手动找到 PEB 的地址,并将其 BeingDebugged 字段置为 0
CheckRemoteDebuggerPresent
比 IsDebuggerPresent 更强大一些,不仅可以检测当前进程,还可以检测另一个进程是否在被调试
- 函数原型
1 |
|
hProcess 是目标进程的句柄,需要具有 PROCESS_QUERY_INFORMATION 或 PROCESS_QUERY_LIMITED_INFORMATION 访问权限;
pbDebuggerPresent 是指向一个布尔值的指针,函数会将检测结果通过这个指针返回
工作原理
同样基于 PEB,但它查询的是指定远程进程的 PEB 中 BeingDebugged 标志,除此之外,它底层还会与内层中的调试端口进行交互,当一个调试器附加到进程时,内核会为该进程创建一个调试对象和一个调试端口,CheckRemoteDebuggerPresent 会查询这个端口的存在,即使用户态的 PEB 被修改,内核中的调试端口依然存在绕过方法:
- 同上,修改返回值
- 用内核调试器隐藏调试端口
ZwSetInformationThread
接操纵线程以干扰调试器正常工作,利用了 Windows 操作系统内核提供的原生 API 来隐藏线程,从而剥夺调试器接收和处理该线程事件的能力
1 |
|
PEB 反调试
检查 BeingDebugged
字段
一个字节,为 1 表示被调试;上文提到的 IsDebuggerPresent() 的内部实现就是检查这个字段
检查 NtGlobalFlag
字段
它检测的是调试器创建进程时所留下的痕迹
4 字节字段,默认值为 0;在 x86 系统中,通常位于 fs:[30h] + 0x68 的偏移处;当进程由调试器创建(而不是先运行再附加),NtGlobalFlag 字段会被设置为一个特定的值(通常为 0x70),这个值是多个标志位(FLG_HEAP_ENABLE_TAIL_CHECK = 0x10,FLG_HEAP_ENABLE_FREE_CHECK = 0x20, FLG_HEAP_VALIDATE_PARAMETERS = 0x40)的组合
检查堆标志 Heap Flags
与 NtGlobalFlag 相关,调试器创建的进程,其堆内存也会包含一些调试标志
PEB.ProcessHeap 指向进程的主堆
ProcessHeap->Flags 这个字段包含多个标志,中其中 HEAP_GROWABLE (0x02) 是最基本的、通常都会存在的标志,如果调试器启用了额外的检查,HEAP_TAIL_CHECKING_ENABLED (0x20) 等其他标志会被添加进去
ProcessHeap->ForceFlags,在非调试环境下是 0,但如果调试器想要强制启用某些严格的堆检查,就会在这个字段中设置一个非零值
检查 ProcessDebugPort
和 ProcessDebugObjectHandle
ProcessDebugPort:每个进程在内核中都有一个 EPROCESS
结构,当被调试时,其中的 DebugPort 字段会指向一个非空的值(与调试器通讯的端口),CheckRemoteDebuggerPresent 就是查询这个值
ProcessDebugObjectHandle:当一个进程被调试时,系统会为其创建一个调试对象,并在 EPROCESS 中保存其句柄,检查这个句柄是否存在即可判断是否被调试
当程序处于 3 环(低权限)时,FS:[0] 寄存器指向 TEB(Thread Environment Block, 线程环境块),通过 FS.Base 能够定位到 TEB,TEB 的 0x30 偏移处存放 PEB 的地址
OutputDebugString 反调试
OutputDebugStringA/W:向调试器发送一段字符串,当没有调试器时,OutputDebugString 的调用会失败,因为没有接受者,为了报告这个错误,Windows 内核会设置一个错误代码(通过 通过 SetLastError 内部机制),这个错误码通常是 0x50(ERROR_FILE_NOT_FOUND),表示找不到调试客户端;当有调试器时,调用成功,因此,内核不会修改最后的错误代码,它保持原样
这个技术就是通过故意设置一个错误码,然后调用 OutputDebugString,再检查错误码是否被改变,来推断调试器的存在
典型的流程就是
1 |
|
TLS 反调试
TLS (Thread Local Storage)线程本地存储,每个线程独立的一块内存区域,用来存放线程私有的数据,使得一个线程在修改自己的 TLS 数据时不会影响到其他线程的数据
TLS 回调(TLS Callback):PE 文件的 TLS 目录中,可以指定一组函数指针,这些函数会在以下时机由系统调用,反调试代码就利用 DLL_PROCESS_ATTACH
这个时机
1 |
|
由于进程在启动时至少需要创建一个线程来运行,所以 TLS 回调函数的执行先于入口点(main 或 DllMain),而在 TLS 回调函数写入反调试相关代码就能使调试器失效
异常处理反调试
调试器通常会捕获异常,通过故意引发异常并检测处理方式来检测调试器CloseHandle
异常 => 系统调用触发异常或返回值异常
CloseHandle 用于关闭一个打开的对象句柄,其函数原型为 BOOL CloseHandle(HANDLE hObject);
,如果传入了一个无效的句柄(如 0xDEADBEEF 或 NULL)
正常情况下:
1 |
|
调试器存在时:将引发EXCEPTION_INVALID_HANDLE(0xC0000008)异常,这个异常可以被一个异常处理程序缓存起来;如果控制被传递给异常处理程序,就表明有一个调试器存在
1 |
|
int 3 或者除零异常 => CPU 异常触发
int 3:
断点分为三种:软件断点、硬件断点和内存断点
软件断点的本质是把下断点的指令的第一个字节替换成了 0xCC,即汇编 int 3 指令,当 eip 指令到到该位置时,发现 int3 指令,将会触发一个异常中断(EXCEPTION_BREAKPOINT 异常(异常码 0x80000003)),这个异常就被调试器捕捉到,程序也因此中断;断点中断时,0xCC 恢复为原来的指令,中断运行之后,又恢复为 0xCC
xdbg 中可以让调试器直接忽略 int3 异常;逆向绕过的话直接把 int 3 nop 掉即可
硬件断点:通过 CPU 内部的调试寄存器来实现, DR0-DR3 用于存放断点地址,Dr4,Dr5一般不使用, DR6 用于记录断点状态,DR7 是调试控制寄存器,设置断点类型、长度和启用状态;反调试可以通过访问相关寄存器来判断是否有硬件断点被设置,如果发现 DR0-DR3 中有非零值,则可能有调试器存在
1 |
|
除零:
CPU 执行 DIV reg, 0
时,会触发 EXCEPTION_INT_DIVIDE_BY_ZERO 异常(异常码 0xC0000094)
调试器同样会先拦截 CPU 异常,程序可以利用 SEH / VEH 捕获异常,检查调试器是否存在
SEH(线程级结构化异常处理)
1 |
|
VEH(向量化异常处理)
1 |
|
调试器不存在时,触发异常之后异常进入 SEH/VEH,程序捕获,判断正常,程序继续执行
调试器存在时,异常被调试器捕获,程序中断或者篡改异常,程序捕获不到异常,判断被调试
进程名反调试
程序在运行时枚举系统进程列表,查找已知调试器的进程名(比如 ida.exe、x64dbg.exe 等)。如果发现匹配项,就认为程序正被外部调试器调试,进而采取相应措施(退出、限制功能、触发误导逻辑等)
1 |
|
窗口名反调试
大多数调试器在运行时会创建一个主窗口,且这个窗口会带有比较固定的标题文本(窗口名),例如:x64dbg 窗口标题含有 “x64dbg”,
IDA 窗口标题含有 “IDA”,利用这一点,程序可以调用 Windows API(例如 EnumWindows + GetWindowText)来遍历系统中所有顶层窗口,检查是否存在这些关键词,如果找到,就说明调试器可能正在运行,从而触发反调试行为
1 |
|
时间检测反调试
程序在正常环境下运行的速度与在调试器或模拟器下运行的速度有显著差异,调试器逐行执行程序时,每条指令之间会有额外延迟,模拟器的指令执行速度也远低于真实 CPU,因此,可以通过测量运行时间是否异常来判断是否被调试
程序测量时间间隔主要有两种方法:
1.通过 CPU 或系统高精度计数器测量时间差
函数名 | 说明 |
---|---|
RDTSC | CPU 内部时间戳计数器,直接读取 CPU 周期数,精度最高 |
NtQueryPerformanceCounter / QueryPerformanceCounter | Windows 高精度性能计数器,基于硬件计时器 |
GetTickCount | 返回自系统启动以来的毫秒数,精度较低 |
2.通过系统提供的时间 API 测量时间间隔
函数名 | 说明 |
---|---|
timeGetTime() | 返回自系统启动以来的毫秒数,类似 GetTickCount,但精度略高 |
_ftime() | 返回当前时间(精确到毫秒),主要用于 C 语言计时 |
单步执行反调试
在 x86/x64 CPU 中,有一个陷阱标志 Trap Flag (TF) 位(在 EFLAGS/RFLAGS 寄存器中),如果 Trap Flag = 1 ,CPU 就进入单步模式
在单步模式下,每执行一条指令,CPU 会触发一次 Debug Exception (#DB, 0x1),#DB 异常被封装为 EXCEPTION_SINGLE_STEP ,调试器会捕获这个异常,再把这个异常与 SEH 结合即可实现反调试
1 |
|
参考文章
https://www.cnblogs.com/PaperPlaneFly/p/18474056
https://bbs.kanxue.com/thread-259098.htm
https://whitebird0.github.io/post/%E5%8F%8D%E8%B0%83%E8%AF%95%E6%8A%80%E6%9C%AF
https://sgsgsama.github.io/ctf/re-hints/%E5%8F%8D%E8%B0%83%E8%AF%95/#more