几种简单的反调试方法

这里面没有什么自己的东西, 都是从看雪的OD从零系列教程里看来的. 最近看到的几章都是讲反调试的, 虽然对其本质还没有去深入了解, 还是觉得应该先把这些记下来.

下面的一些信息是基于自己现有的知识推测出来的, 不一定对. 关于函数的说明, 都是基于MSDN的粗浅翻译, 要想了解更准确的信息, 请查阅MSDN.

0x0 利用IsDebuggerParent()

0x0.0 介绍

该函数检测程序是否正在被调试, 是的话返回1,否则返回0, 该函数位于Kernel32.dll中, 其代码如下:

mov eax, dword ptr fs:[0x18]
mov eax, dword ptr fs:[eax + 0x30]
movzx eax, byte ptr ds:[eax + 0x2]

fs寄存器指示了(并不是储存了)PEB(Process Environment Block)的地址, 因为GDT的关系,fs寄存器中储存的只是选择子而不是地址, 因此要从fs的0x18偏移处取一个指向自己的self指针(这一步实际上是可以省略的).

接下来从PEB的0x30偏移处取得NT_TIB结构的首地址, 该结构的0x2偏移处是BeingDebugged字段, 表示当前进程是否被调试, 因此通过这个函数可以检测调试器.

你可以在代码中直接使用IsDebuggerParent或者嵌入等价的汇编代码. 动态载入函数比直接引入好些.

0x0.1 栗子

#include <stdio.h>
#include <windows.h>

/* 内联汇编 */
int foo(){
    /* 这里的 movl %fs:30, %ebx 就相当于
     * mov eax, dword ptr fs:[0x18]
     * mov eax, dword ptr fs:[eax + 0x30]
     */
    asm("movl %fs:0x30, %ebx; movzx 2(%ebx), %eax");
}
/* 动态载入 */
int bar(){
 int result = 0;
 HINSTANCE kern_lib = LoadLibraryEx("kernel32.dll", NULL, 0);
    if(kern_lib){
        FARPROC lIsDebuggerPresent = GetProcAddress(kern_lib, "IsDebuggerPresent");
        if(lIsDebuggerPresent && lIsDebuggerPresent()){
            result = 1;
        }
        FreeLibrary(kern_lib);
    }
    return result;
}

/* 测试的时候记得关掉OD的插件, 或者直接用原版 */
int main(){
    printf("foo = %d\n",foo());
    printf("bar = %d\n",bar());
    return 0;
}

0x0.2 绕过

  • 如果能定位到函数的话, 修改他的流程.

  • 可以在载入程序后, 把那个BeingDebugged位置0, 当然, HideDebugger插件已经替我们做了这件事.

0x1 检测进程名I

0x1.0 介绍

通过检测特定调试器(常常是OD)的进程是否存在来防止被调试.

用到了下面几个API:

  • EnumProcesses

BOOL WINAPI EnumProcesses(
  _Out_ DWORD *pProcessIds,
  _In_  DWORD cb,
  _Out_ DWORD *pBytesReturned
);

EnumProcesses 枚举所有的进程PID, 第一个参数是缓冲区, 储存所有进程PID的列表, 参数二是以byte计数的数组长度, 参数三是阶收到的数组长度, 同样以byte计数. 函数执行成功返回非零值.

  • GetModuleBaseNameA

DWORD WINAPI GetModuleBaseName(
  _In_     HANDLE  hProcess,
  _In_opt_ HMODULE hModule,
  _Out_    LPTSTR  lpBaseName,
  _In_     DWORD   nSize
);

该函数取得某个模块的名称, 参数一是线程句柄, 参数二是模块句柄, 参数三是储存返回模块名的缓冲区, 最后是缓冲区的长度, 以char计数. 函数执行成功则返回接收到的模块名的长度

  • OpenProcess

HANDLE WINAPI OpenProcess(
  _In_ DWORD dwDesiredAccess,
  _In_ BOOL  bInheritHandle,
  _In_ DWORD dwProcessId
);

该函数通过PID(参数4)获得进程句柄失败则返回NULL. (获得句柄后可以在OD的H窗口看到该句柄).

  • EnumProcessModules

BOOL WINAPI EnumProcessModules(
  _In_  HANDLE  hProcess,
  _Out_ HMODULE *lphModule,
  _In_  DWORD   cb,
  _Out_ LPDWORD lpcbNeeded
);

函数枚举指定进程里的所有Modules, 取回句柄. 参数一指定了进程句柄, 参数二是返回的模块句柄缓冲区, 参数三是以byte计数的缓冲区大小, 四是最终取回句柄的大小, byte计数. 函数执行成功返回非零值.

利用这些函数检测调试器的经典过程是这样的:

  1. 首先用GetProcAddress动态载入上面的其他函数

  2. 调用EnumProcesses对所有进程进行枚举, 实际上是获得一个储存了所有进程PID的列表

  3. 以获取到的PID为参数调用OpenProcess, 取得进程句柄

  4. 用获取到的句柄执行EnumProcessModules枚举进程的模块, 只取第一个模块

  5. 使用进程句柄和模块句柄为参数调用GetModuleBaseNameA得到进程名

  6. 和要检测的进程名作比较, 这决定了程序的流程

  7. 如果是待检测进程的话, 选择自行退出或者是结束调试器, 可能用到TerminatePorcess

  8. 调用CloseHandle关闭句柄

0x1.1 栗子

//TODO

0x1.2 绕过

  • 令OpenProcess始终返回NULL, 打不开任何进程.

  • 改动OpenProcess后的程序流程

  • 更改OD的名字, 进程名也会同时被更改;(最简单的做法了)

0x2 检测进程名II

0x2.0 介绍

使用的API:

  • CreateToolhelp32Snapshot

HANDLE WINAPI CreateToolhelp32Snapshot(
  _In_ DWORD dwFlags,
  _In_ DWORD th32ProcessID
);

该函数对指定的进程做快照, dwFlags参数决定进程的那一部分会被包含在快照中. 参数二为PID, 返回快照句柄. 指定参数 CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) 则对系统中所有的进程进行快照, 可以被Process32First进行枚举.

  • Process32First

BOOL WINAPI Process32First(
  _In_    HANDLE           hSnapshot,
  _Inout_ LPPROCESSENTRY32 lppe
);

在快照中取得第一个进程的相关信息. 参数一: 由CreateToolhelp32Snapshot返回的快照句柄. 参数二: 指向PORCESSENTRY32结构体的指针, 包含可执行文件名, PID,和父进程PID等. 执行成功返回true.

  • Process32Next

BOOL WINAPI Process32Next(
  _In_  HANDLE           hSnapshot,
  _Out_ LPPROCESSENTRY32 lppe
);

取回快照中下一个进程的信息(然而你必须先用Process32First取第一个), 参数和Process32First基本相同.

Process32FirstProcess32Next中涉及到的PPROCESSENTRY32结构体如下:

PROCESSENTRY32 structure
typedef struct tagPROCESSENTRY32 {
  DWORD     dwSize;
  DWORD     cntUsage;
  DWORD     th32ProcessID;
  ULONG_PTR th32DefaultHeapID;
  DWORD     th32ModuleID;
  DWORD     cntThreads;
  DWORD     th32ParentProcessID;
  LONG      pcPriClassBase;
  DWORD     dwFlags;
  TCHAR     szExeFile[MAX_PATH];
} PROCESSENTRY32, *PPRO

最后一个参数就是进程名了好像.

利用该方法检测进程的基本流程是:

  • 调用CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)获得所有进程快照

  • Process32First取得第一个进程的信息, 判断是否是要检测的进程

  • Process32Next循环检测其他进程

0x2.1 栗子

//TODO

0x2.2 绕过

绕过的做法基本同I.

检测窗口类名

0x3.0 介绍

又是API…

  • FindWindowA

HWND WINAPI FindWindow(
  _In_opt_ LPCTSTR lpClassName,
  _In_opt_ LPCTSTR lpWindowName
);

该函数取回和参数匹配的顶级窗口的句柄, 大小写不敏感.

参数一: 窗口类名 参数二: 窗口名 参数可选, 至少一个, 另一个可置NULL. 执行成功返回句柄.

因为OD的窗口名常常不确定, 利用窗口类名往往比较靠谱; 将窗口名置NULL, 检测OD的顶级窗体类名即可, 该类名可以通过Spy++得到.

0x3.1 栗子

//TODO

0x3.2 绕过

  • HideDebugger插件有绕过 FindWindowA/EnumWindows 的选项;

  • 使用RE-Pair为OD主程序打补丁, 可更改其类名

0x3 UnhandledExcepiton和ZwQueryInformationProcess

这种反调试方法比前面的方法更具技巧性一些, 利用了Windows的异常处理机制, 但是我还不了解这些异常处理, 不敢胡说,暂时略过.

* SetUnhandledExceptionFilter ```c LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter( _In_ LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter ); ``` 该函数让应用程序可以取代该进程中所有线程的系统异常处理函数.(大概是吧...) > Enables an application to supersede the top-level exception handler of each > thread of a process. 调用该函数后, 如果有异常发生, 且该进程当前没有被调试, 则该异常会被 `Unhandled Exception Filter`处理 , Filter会调用异常筛选(?)函数, 该函数由参数一指定. > After calling this function, if an exception occurs in a process that is not being debugged, > and the exception makes it to the unhandled exception filter, > that filter will call the exception filter function specified by the > lpTopLevelExceptionFilter parameter. * UnhandledExceptionFilter ```c LONG WINAPI UnhandledExceptionFilter( _In_ struct _EXCEPTION_POINTERS *ExceptionInfo ); ``` 如果当前进程被调试的话, 程序定义的函数(?)会将未处理的异常传递给调试器. 否则, 它将可选地显示一个应用程序错误的消息框, 并使得异常处理函数执行. 该函数只能在异常处理例程中的Filter Expression中被调用. > An application-defined function that passes unhandled exceptions to the debugger, > if the process is being debugged. Otherwise, > it optionally displays an Application Error message box and causes the exception handler to be executed. > This function can be called only from within the filter expression of an exception handler. 该函数唯一的参数是一个`EXCEPTION_POINTERS`指针, 指定了对此异常的描述和发生异常时的上下文. > A pointer to an `EXCEPTION_POINTERS` structure that specifies a description > of the exception and the processor context at the time of the exception. > 发生异常时系统的处理顺序(by Jeremy Gordon, Hume): > 1. 系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送, > 并且目标程序正在被调试,则系统挂起程序并向调试器发送`EXCEPTION_DEBUG_EVENT`消息. > 1. 如果你的程序没有被调试或者调试器未能处理异常, > 系统就会继续查找你是否安装了线程相关的异常处理例程, > 如果你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程, > 交由其处理. > 1. 每个线程相关的异常处理例程可以处理或者不处理这个异常, > 如果他不处理并且安装了多个线程相关的异常处理例程, 可交由链起来的其他例程处理. > 1. 如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger. > 1. **如果程序未处于被调试状态或者debugger没有能够处理, > 并且你调用SetUnhandledExceptionFilter安装了最后异常处理例程的话,系统转向对它的调用.** > 1. **如果你没有安装最后异常处理例程或者他没有处理这个异常, > 系统会调用默认的系统处理程序(UnhandledExceptionFilter),通常显示一个对话框, > 你可以选择关闭或者最后将其附加到调试器上的调试按钮. > 如果没有调试器能被附加于其上或者调试器也处理不了,系统就调用ExitProcess终结程序.** > 1. 不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开, > 这是线程异常处理例程最后清理的机会. 利用这两个函数的流程可能是: 1. 当点击CM中的check按钮时, 程序抛出不可忽略的异常, 因为程序正在被调试, 所以系统将异常传递给调试器(EXCEPITON_DEBUG_EVENT), `SetUnhandledExceptionFilter`指定的异常处理函数并没有被执行 (实际上这个函数里放置的应该是程序的真正流程). 2. 然而OD并不能处理这个异常, 因此最终将调用`UnhandledExceptionFilter`处理异常. 在`UnhandledExceptionFilter`中有函数`ZwQueryInfomationProcess`, 可以用来判断程序是否被调试, 它是随着`UnhandledExceptionFilter`被调用(在系统领空中), 但是这个函数也可以单独抽取出来被调用. ```c NTSTATUS WINAPI ZwQueryInformationProcess( _In_ HANDLE ProcessHandle, _In_ PROCESSINFOCLASS ProcessInformationClass, _Out_ PVOID ProcessInformation, _In_ ULONG ProcessInformationLength, _Out_opt_ PULONG ReturnLength ); ``` 取得特定进程的信息. 在这里只需要知道使ProcessInformationClass = ProcessDebugPort (7), 就可以从ProcessInformation缓冲区中取得ProcessInformationLength长度的信息, 返回FFFFFFFF的话表示正在被调试, 返回0反之. 对应上面步骤f的: 如果没有调试器能被附加于其上或者调试器也处理不了,系统就调用ExitProcess终结程序. 如果正在调试(返回FFFFFFF)的话->异常传递给调试器->调试器处理不了->程序退出. 按教程的说法和实际测试的得到: 如果返回0的话跳转到SetUnhandledExceptionFilter指定的函数, 利用异常实现了反调试. 可是执行SetUnhandledExceptionFilter指定的函数不是在步骤c吗, UnhandledExceptionFilter可是步骤6才执行的? ### 绕过 * 手动修改ZwQueryInformationProcess返回值 * HideDebugger插件的UnhandledExceptionTricks选项可以绕过此反调试. * HideOD插件可以单独绕过ZwQueryInformationProcess(记住勾选AutoRun)

0x4 NtGlobalFlag,ProcessHeap,OutputDebugStringA

这几个都比较简单, 从略.

NtGlobalFlag

该标志在PEB中,对于x86, 在0x68处 对于x64, 在 0xbc 处.

定位到PEB:

  • 在EIP入口点定位到EBP的值;

  • 或者定位到FS:[0x18];

NtGlobalFlag 默认总是0, 除非它被一个调试器所附加. 当调试器创建一个进程时, NtGlobalFlag会有如下的值:

> FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
> FLG_HEAP_ENABLE_FREE_CHECK (0x20)
> FLG_HEAP_VALIDATE_PARAMETERS (0x40)

因此, 如果NtGlobalFlag == 0x10 + 0x20 + 0x40 =  0x70时, 程序正在被调试.

ProcessHeap

在PEB的 0x10 偏移处的一个 DWORD, 不为0则表示正在被调试.

OutputDebugStringA

OutputDebugStringA是个函数, 该函数向调试器输出一个字符串, 它能用于反调试是因为OD的一个bug, 当用这个函数输出一长串的%s字串时, OD会崩溃.

0x4.1 栗子

0x4.2 绕过

  • 修改对应的值

  • HideOD 插件的 HideNtDebugBit选项, 以及 OutDebugStringA 选项或 Hide Debugger插件的OutputDebugString exploit选项