几种简单的反调试方法#
提示
这是一篇迁移自 Jekyll 的文章,如有格式问题,可到 ⛺ SilverRainZ/bullet 反馈
这里面没有什么自己的东西, 都是从看雪的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计数. 函数执行成功返回非零值.
利用这些函数检测调试器的经典过程是这样的:
首先用
GetProcAddress
动态载入上面的其他函数调用
EnumProcesses
对所有进程进行枚举, 实际上是获得一个储存了所有进程PID的列表以获取到的PID为参数调用
OpenProcess
, 取得进程句柄用获取到的句柄执行
EnumProcessModules
枚举进程的模块, 只取第一个模块使用进程句柄和模块句柄为参数调用
GetModuleBaseNameA
得到进程名和要检测的进程名作比较, 这决定了程序的流程
如果是待检测进程的话, 选择自行退出或者是结束调试器, 可能用到
TerminatePorcess
调用
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基本相同.
Process32First
和Process32Next
中涉及到的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的异常处理机制, 但是我还不了解这些异常处理, 不敢胡说,暂时略过.
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选项
如果你有任何意见,请在此评论。 如果你留下了电子邮箱,我可能会通过 回复你。