反调试主要利用程序在调试状态下和非调试状态下的区别
PS:以下内容均来源于各大安全论坛与博客
检查调试器
PEB
1: kd> dt _PEB
nt!_PEB
...
+0x002 BeingDebugged : UChar
...
+0x030 ProcessHeap : Ptr64 Void
...
+0x0bc NtGlobalFlag : Uint4B
...
BeingDebugged
:表示是否在调试状态- 相关API:
IsDebuggerPresent
- 相关API:
NtGlobalFlag
:如果在调试状态下,此值为0x70
NtQueryInformationProcess
利用NtQueryInformationProcess
获取进程相关调试信息
__kernel_entry NTSTATUS NtQueryInformationProcess(
IN HANDLE ProcessHandle, // 进程句柄
IN PROCESSINFOCLASS ProcessInformationClass, // 进程信息类型
OUT PVOID ProcessInformation, // 进程信息,与类型相关
IN ULONG ProcessInformationLength, // 进程信息块长度
OUT PULONG ReturnLength // 返回长度
);
进程信息类型填写为:ProcessDebugPort (0x7)
或ProcessDebugObjectHandle (0x1e)
- 当传入
ProcessDebugPort
时,返回一个调试端口值,在调试状态下此值为0xffffffff
,非调试状态下为0,等价API:CheckRemoteDebuggerPresent
- 当传入
ProcessDebugObjectHandle
时,返回一个调试对象句柄,在调试状态下为非0,非调试状态下为0
STARTUPINFO
程序正常启动时,CreateProcess
的STARTUPINFO结构中的成员一般会被填为0,而调试启动时不会
故反调试检测可以检查STARTUPINFO结构中的成员是否为0,可以多检查几个成员,GetStartupInfo
可以获取STARTUPINFO结构
SedebugPrivilege
一般在未提权情况下,程序正常启动不会有调试权限,但调试器启动程序时,应用程序会继承调试器的调试权限,因此可以检查调试权限来进行反调试
在管理员启动+调试权限下,可以通过OpenProcess
来打开csrss.exe进程句柄,可以通过检查是否能拿到此句柄来判断是否有调试器存在
检查调试环境
注册表Just-in-time调试器
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug
中的Debugger键值对,可以检查此键值对
窗口检查
通过查找窗口来检查是否开启了调试器,常用API有:FindWindow
、EnumWindows
等
扩展:
- 也可以检查进程的父进程是否是调试器
- 也可以枚举遍历进程查找调试器
查找调试内核对象
NtQueryObject
查询内核对象,当调试器调试进程时,会创建一个DebugObject对象,可以遍历内核对象列表查找有没有相应的调试对象
NTSTATUS NtQueryObject(
_In_opt_ HANDLE Handle,
_In_ OBJECT_INFORMATION_CLASS objectInformationClass, // 此值要传入0x3
_Out_opt_ PVOID ObjectInformation, // 传出参数,通过其可遍历内核对象
_In_ ULONG ObjectInformationLength,
_Out_opt_ PULONG ReturnLength
);
NtQuerySystemInformation
同NtQueryInformationProcess
,当传入类型为systemKernelDebuggerInfromation (0x32)
时,若在调试状态下返回的system_kernel_Debug_Information
的结构中的Debugedable
为1
ZwSetInformationThread
传入参数ThreadHideFromDebugger (0x11)
当程序正常运行,该函数相当于空函数,当程序处于在调试状态下,则该函数可以使当前线程脱离调试器,即调试器无法获得此线程的调试事件
动态反调试
时钟检测
在调试状态下,跟踪代码比正常运行代码的时间要长。可以通过比较运行代码之间的时间间隔来判断是否被调试了
- TSC寄存器保存着CPU的时钟周期计数,
RDTSC
指令将TSC寄存器的值装入EDX:EAX
中 - 也可利用相关API:
QueryPerformanceCounter
、GetTickCount
、GetSystemTime
、GetLocalTime
等
异常处理
- SEH
在程序中主动触发异常
RaiseException
,如果此时存在调试器,那么异常会被调试器接收(没有调试器时,此异常会给注册的SEH,一般会在SEH中执行敏感操作),调试器接收了异常后无法按程序原定的流程继续执行,起到了反调试的作用 - SetUnhandledExceptionFilter
当SEH不存在时,则会调用
UnhandledExceptionFilter
,该函数检查是否存在调试器,存在就给调试器,不存在就调用默认的异常处理:弹出错误对话框结束进程SetUnhandledExceptionFilter
可以设置异常处理函数来替换默认的异常处理,可以在此异常处理中做敏感操作,如果有调试器存在,此敏感操作不会被执行也可导致无法按程序原定流程执行 - int 2d
int 2d
是一个特殊的指令,原为内核模式中用来触发断点异常的,也可以在用户模式下正常运行时触发异常,调试器不会触发此异常,只是忽略,如果遇到Int 2d
指令,调试器无法执行单步指令,直到遇到断点
0xCC探测
0xCC即int 3
指令,若在关键代码位置检测到有0xCC,即可判断进程处于调试状态(0xCC也可以为其他指令的值)
- API断点:检查API首地址处是否存在断点
- 校验和:对敏感区域进行校验和比较,如果存在调试器且正在调试当前敏感位置,校验和会不一样
- 硬件断点检测:Dr0~Dr3保存硬件断点的地址,检查这几个寄存器的值是否为0就知道有没有硬件断点(可利用
GetThreadContext
或者主动触发异常来检查)
自调试
同一个进程不允许同时被两个调试器调试,可以自己调试自己防止被其他调试器调试
程序第一次打开创建同步对象,并且在以调试方式创建自己,使得第一次打开的程序成为第二次打开的程序的调试器
相关API:CreateProcess
或DebugActiveProcess
检查VMware
检测VMware也是检测一些特征. 根据检测的结果进行判断.
__indword(0x5658)
使用in
指令通过0x5658
端口读取数据,如果程序在 VMware内运行,则ebx
寄存器的值就会变为0x564D5868
#include <iostream>
#include <string>
bool check_vmware()
{
bool is_in_vmware = false;
__try {
__asm {
// 保存寄存器环境
push ebx
push edx
push ecx
xor ebx, ebx
mov ecx, 0xa
mov edx, 0x5658 // dx = "VX"
mov eax, 0x564D5868
in eax, dx // 若不在VMware内,此处会抛出异常
// 比较ebx是否是0x564D5868,即"VMXh"
cmp ebx, 0x564D5868
sete is_in_vmware
// 恢复环境
pop ecx
pop edx
pop ebx
}
} __except (1)
{
}
return is_in_vmware;
}
int main()
{
if (check_vmware()) {
std::cout << "Is in VMware." << std::endl;
} else {
std::cout << "Is not in VMware." << std::endl;
}
return 0;
}