反调试合集

反调试合集反调试是一种用于阻碍程序动态调试的技术 首先大致说明一下反调试的工作原理

反调试

  前文写过,花指令通常干扰静态分析,而反调试与之相反,主要为了干扰动态调试。

1.反调试简介

  反调试是一种用于阻碍程序动态调试的技术,首先大致说明一下反调试的工作原理。

  在操作系统内部提供了一些API,用于调试器调试。当调试器调用这些API时系统就会在被调试的进程内存中留下与调试器相关的信息。一部分信息是可以被抹除的,也有一部分信息是难以抹除的。

  当调试器附加到目标程序后,用户的很多行为将优先被调试器捕捉和处理。其中大部分是通过异常捕获通信的,包括断点的本质就是异常。如果调试器遇到不想处理的信息,一种方式是忽略,另一种方式是交给操作系统处理。

  那么目前为止,程序就有两种方式检测自己是否被调试:

  • 检测内存中是否有调试器的信息。
  • 通过特定的指令或触发特定异常,检测返回结果。

  通常来说,存在反调试的程序,当检测到自身处于调试状态时,就会控制程序绕过关键代码,防止关键代码被调试,或者干脆直接退出程序。



2.API反调试

  Windows内部提供了一些用于检测调试器的API

  其中一个APIIsDebuggerPresent,原型为:

BOOL IsDebuggerPresent(); 

  返回值为1表示当前进程被调试的状态,反之为0.

  另一个常用的APICheckRemoteDebuggerPresent,原型为:

BOOL CheckRemoteDebuggerPresent(HANDLE hProcess, PBOOL pbDebuggerPresent); 

  返回值为1表示当前进程被调试的状态,反之为0.


3.PEB反调试

  当程序处于3环(低权限)时, FS:[0] 寄存器指向TEB(Thread Environment Block),即线程环境块结构体,TEB向后偏移0x30字节的位置保存的是PEB(Process Environment Block ),即进程环境块的结构体地址。PEB中的部分成员是与调息相关的成员,当调试器通过 Windows提供的API调试目标程序时,Windows会将一部分调试信息写人这个结构体中。

kd>dt_TEB nt! _TEB ... +0x030 ProcessEnvironmentBlock :Ptr32_PEB ... kd>dt_TEB ... +0x002 BeingDebugged :UChar ... +Ox018 ProcessHeap :Ptr32 Void ... +0x068 NtGlobalF1ag :Uint4B ... 

  本处只介绍这两个结构体中几个重要的成员,若是想在实际调试时查看其他成员的具体内容,其中一种方法是使用WinDbg调试内核。

  在PEB结构体中中,BeingDebuggedProcessHeapNtGlobalFlag是与调试信息相关的三个重要成员。

  • BeingDebugged:当进程处于被调试状态时,值为1,否则为0。
  • ProcessHeap:指向Heap结构体,偏移0xC处为Flags成员,偏移0x10处为ForceFlags成员。通常情况下,Flags的值为2.ForceFlags的值为0,当进程被调试时会发生改变
  • NGlobalFlag:占四个字节,默认值为0。当进程处于被调试状态时,第一个字节会被置为0x70。

  通过FS.Base能够定位到TEB,再通过TEB+0x30能够定位PEB。通过在内存中检测或修改相关成员的值,便可达到反试、反反调试的效果。


4.TLS反调试

  TLS (Thread Local Storage),即线程局部存储是Windows提供的一种处理机制,每进行一次线程切换,便会调用一次TLS回调。它本意是想给每个线程都提供访问全局变量的机会。例如,需要统计当前程序进行了多少次线程切换,但并不想让其他线程访问到这个计数变量,使用TLS进行计数,便能够解决这个问题,一个程序能设置多个TLS.
  由于进程在启动时至少需要创建一个线程来运行,因此在调用main函数前就会调用一次 TLS 回调。利用这个特点,在TLS回调中写入与反调试相关的代码,便可悄无声息地令调试器失效。

#include <windows.h> #include <iostream> // TLS回调函数定义 void NTAPI TLS_Callback(PVOID DllHandle, DWORD Reason, PVOID Reserved) { if (Reason == DLL_PROCESS_ATTACH) { // 检测是否存在调试器 if (IsDebuggerPresent()) { std::cout<<"Debugger detected!\n"; ExitProcess(1); // 如果检测到调试器则退出 } else { std::cout<<"No debugger detected.\n"; } } } // 声明TLS回调函数 #ifdef _MSC_VER #pragma const_seg(".CRT$XLB") EXTERN_C const PIMAGE_TLS_CALLBACK pTLS_CALLBACK = TLS_Callback; #pragma const_seg() #else __attribute__((section(".CRT$XLB"))) PIMAGE_TLS_CALLBACK pTLS_CALLBACK = TLS_Callback; #endif int main() { std::cout<<"Program started.\n"; return 0; } 
  • TLS_Callback:TLS的回调函数,每当一个新线程创建时,或者进程加载时,它都会被调用。
  • IsDebuggerPresent:这是Windows提供的API,用来检查当前进程是否被调试。
  • #pragma const_seg:用于指定TLS回调函数的位置,它被放在.CRT$XLB节中,这样Windows在加载时会自动执行。

5.进程名反调试

#include <windows.h> #include <tlhelp32.h> #include <iostream> #include <string> // 检查是否存在指定的调试器进程 bool IsDebuggerProcessRunning() { const char* debuggerNames[] = { "ollydbg.exe", "x64dbg.exe", "ida.exe", "windbg.exe" }; // 创建进程快照 HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapshot == INVALID_HANDLE_VALUE) { return false; } PROCESSENTRY32 pe32; pe32.dwSize = sizeof(PROCESSENTRY32); // 遍历进程列表 if (Process32First(hSnapshot, &pe32)) { do { // 遍历已知的调试器名称 for (const auto& debuggerName : debuggerNames) { // 将进程名转换为小写以进行匹配 std::string processName = pe32.szExeFile; for (auto& c : processName) c = tolower(c); if (processName == debuggerName) { CloseHandle(hSnapshot); return true; // 找到匹配的调试器进程 } } } while (Process32Next(hSnapshot, &pe32)); } CloseHandle(hSnapshot); return false; // 未找到调试器进程 } int main() { if (IsDebuggerProcessRunning()) { std::cout << "Debugger process detected! Exiting...\n"; ExitProcess(1); // 检测到调试器,退出程序 } else { std::cout << "No debugger detected.\n"; } // 正常程序逻辑 std::cout << "Program is running.\n"; return 0; } 
  • 调试器进程名列表:在debuggerNames数组中列出了常见的调试器进程名(如ollydbg.exe, x64dbg.exe等)。可以根据需要添加更多的调试器进程名。
  • CreateToolhelp32Snapshot:这是一个Windows API,用于创建系统中所有进程的快照,以便遍历这些进程。
  • Process32First 和 Process32Next:这些函数用于遍历进程快照中的每一个进程。
  • tolower:为了匹配时忽略大小写,将进程名全部转换为小写进行比较。
  • ExitProcess:如果发现调试器进程,程序直接退出。

  代码在执行时遍历系统中所有正在运行的进程,并检查是否有已知的调试器进程在运行。如果发现某个调试器进程(如ollydbg.exe),则程序会直接退出,否则会继续运行。这种方法可以用来检测外部调试器是否正在运行,但它不是百分之百可靠,因为高级调试器可能会通过修改进程名或隐藏自己来规避检测。


6.窗口名反调试

  检测已打开窗口的窗口也是一种较为常用的反调试手段。示例代码如下

#include <windows.h> #include <iostream> // 定义要检查的调试器窗口名称 const char* debuggerWindowNames[] = { "OllyDbg", "x64dbg", "IDA", "WinDbg" }; // 枚举系统中所有窗口,查找是否有调试器窗口存在 BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) { char windowTitle[256]; GetWindowTextA(hwnd, windowTitle, sizeof(windowTitle)); // 遍历已知的调试器窗口名称 for (const auto& debuggerWindowName : debuggerWindowNames) { if (strstr(windowTitle, debuggerWindowName)) { std::cout << "Debugger window detected: " << windowTitle << "\n"; return FALSE; // 找到调试器窗口,停止枚举 } } return TRUE; // 继续枚举其他窗口 } // 检查是否存在调试器窗口 bool IsDebuggerWindowOpen() { return !EnumWindows(EnumWindowsProc, 0); } int main() { if (IsDebuggerWindowOpen()) { std::cout << "Debugger window detected! Exiting...\n"; ExitProcess(1); // 检测到调试器窗口,退出程序 } else { std::cout << "No debugger window detected.\n"; } std::cout << "Program is running.\n"; return 0; } 
  • EnumWindows:这是Windows API,允许遍历系统中所有的顶层窗口。每找到一个窗口,就会调用回调函数EnumWindowsProc
  • EnumWindowsProc:这是枚举窗口的回调函数。通过GetWindowTextA函数获取窗口标题,然后使用strstr来判断窗口名是否包含已知调试器窗口名称。
  • ExitProcess:如果检测到调试器窗口,程序直接退出。

  有些调试器允许用户自定义窗口名,因此该方法并非完全可靠。高级调试器同样可能会通过修改窗口名或隐藏窗口来规避检测。例如,OD在刚启动时的窗口名为“OllyDbg - [CPU]”,而加载程序后会有所改变,但前几个字节仍然为“OllyDbg”,对于这类窗口,规定字符串的检测长度往往能取得不错的效果。


7.时间戳反调试

  • rdtsc: 汇编指令,能够以纳秒级记录系统启动以来的时间戳,返回值保存在EDX:EAX(高位保存到EDX,低位保存到EAX)中。
  • QueryPerformanceCounter:能够以微秒为单位高精度计时。
  • GetTickCount:返回值为自系统启动以来所经过的毫秒数。

  例如:

#include <windows.h> #include <iostream> int main() { DWORD time1 = GetTickCount(); int result, a = 1, b = 2; __asm( "movl %1, %%ebx\n\t" "addl %%ebx, %0" : "=r" (result) : "r" (b), "0" (a) ); DWORD time2 = GetTickCount(); if(time2-time1>0x10) ExitProcess(0); std::cout << "Program started.\n"; std::cout << result; return 0; } 

  程序执行完内联汇编后,会计算前后时间差。如果时间差超过16毫秒(0x10),程序会调用ExitProcess强制退出。这可以用于反调试:调试器往往会减缓程序的执行速度,因此通过这种时间检测方法可以检测到调试行为。

8.硬件断点检测反调试

  硬件断点是调试器常用的手段之一,它通过CPU的调试寄存器(如DR0-DR7)设置断点。可以通过检查这些寄存器是否有断点设置来检测调试器。使用GetThreadContext API来获取当前线程的上下文,检查调试寄存器(DR0DR3)是否设置断点。

CONTEXT ctx = { 
   }; ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; GetThreadContext(GetCurrentThread(), &ctx); if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) { 
    ExitProcess(0); } 

9.异常处理反调试

  调试器通常会捕获异常,反调试技术可以通过故意引发异常并检查其处理方式来检测调试器。通过引发INT 3指令(断点中断)或其他异常(如除零异常),查看是否有异常处理程序被插入,然后使用SetUnhandledExceptionFilter设置自定义的异常处理程序。

__try { __asm { int 3 } // 触发断点异常 } __except (EXCEPTION_EXECUTE_HANDLER) { // 如果捕获了异常,则说明没有调试器 std::cout << "No debugger detected." << std::endl; } 

10.单步检测反调试

  单步检测反调试是一种通过检测CPU的单步执行(Trap Flag, TF)来判断是否有调试器介入的技术。当调试器单步执行目标程序时,CPU的TF标志会被设置为1,这会导致在每条指令执行完后触发一个调试中断。因此,程序可以通过监控TF标志的变化来检测调试行为。

  Trap Flag(TF):当EFLAGS寄存器中的TF标志被置为1时,CPU会进入单步模式,每执行一条指令后都会产生一个调试中断(INT 1)。通过检查和控制TF标志,可以判断程序是否被调试器单步执行。如果调试器处于单步调试模式,TF标志会被置1,程序可以利用这一特性检测调试行为。

例如:

#include <iostream> #include <windows.h> int main() { // 保存原来的EFLAGS寄存器值 unsigned int eflags; __asm { pushfd // 将EFLAGS压入栈中 pop eax // 将栈顶的EFLAGS值弹出到EAX寄存器 mov eflags, eax // 保存EFLAGS寄存器到eflags变量 or eax, 0x100 // 设置TF(Trap Flag)位为1,启用单步调试模式 push eax // 将修改后的EFLAGS值压回栈 popfd // 恢复EFLAGS寄存器,使Trap Flag生效 } // 执行单步调试后检测 __asm { nop // 一个空操作,用于单步执行检测 pushfd // 将当前的EFLAGS寄存器值压入栈 pop eax // 弹出EFLAGS到EAX mov eflags, eax // 保存当前的EFLAGS值 } // 检测Trap Flag是否被清除(如果有调试器在调试,该标志可能被复位) if (eflags & 0x100) { std::cout << "No debugger detected (TF still set)." << std::endl; } else { std::cout << "Debugger detected (TF cleared)." << std::endl; ExitProcess(0); // 检测到调试器,退出程序 } std::cout << "Program continues running..." << std::endl; return 0; } 

  如果程序在执行nop指令后,TF标志依然保持为1,则说明程序未被调试,输出“No debugger detected (TF still set)”。如果程序发现TF标志被清除(调试器可能重置了该标志),则输出“Debugger detected (TF cleared)”,并终止程序执行。

今天的文章 反调试合集分享到此就结束了,感谢您的阅读。
编程小号
上一篇 2025-01-01 21:01
下一篇 2025-01-01 20:57

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ji-chu/98487.html