霍雅
反调试总结
参考文献:https://www.bilibili.com/video/BV1gZ2CBxERj/?spm_id_from=333.1387.upload.video_card.click
进程创建的时候就以调试模式创建
进程正常创建但是后面附加调试
进程调试模式创建会修改比附加多修改一些标志位
反调试方法全面总结
以下是所有反调试方法的部分总结
一、基于系统标志的检测
1.PEB BeingDebugged标志检测
检香PEB结构的BeingDebugged字段是否被设置
数据检测是指程序通过测试一些与调试相关的关键位置的数据来判断是否处于调试状态。比如 PEB 中 的 BeingDebugged 参数。数据检测就是直接定位到这些数据地址并测试其中的数据,从而避免调用 函数,使程序的行为更加隐蔽。 示例:
BOOL CheckDebug()
{
int BeingDebug = 0;
__asm
{
mov eax, dword ptr fs:[30h] ; 指向 PEB 基地址
movzx eax, byte ptr [eax+2]
mov BeingDebug, eax
}
return BeingDebug != 0;
}2.NtGlobalFlag标志检测
检香PEB中的NtGlobalFlag字段是否包含调试标志
由于调试器中启动的进程与正常启动的进程创建堆的方式有些不同,系统使用 PEB 结构偏移量 0x68 处的一个未公开的位置 NTGlobalFlag,来决定如果创建堆结构。如果这个位置上的值为 0x70,则 进程处于调试器中。
BOOL CheckDebug()
{
int BeingDbg = 0;
__asm
{ mov eax, dword ptr fs:[30h]
mov eax, dword ptr [eax + 68h]
and eax, 0x70
mov BeingDbg, eax
}
return BeingDbg != 0;
}3.调试端口检测
使用NtQueryInformationProcess查询ProcessDebugPort
NtQueryInformationProcess 用于获取给定进程的信息:
NTSTATUS WINAPI NtQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);第二个参数 ProcessInformationClass 给定了需要查询的进程信息类型。当给定值为 0(ProcessBasicInformation)或 7(ProcessDebugPort)时,就能得到相关调试信息,返回 信息会写到第三个参数 ProcessInformation 指向的缓冲区中。
BOOL CheckDebug() {
DWORD dbgport = 0;
HMODULE hModule = LoadLibrary("Ntdll.dll");
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessP tr)GetProcAddress(hModule, "NtQueryInformationProcess");
NtQueryInformationProcess(GetCurrentProcess(), 7, &dbgPort, sizeof(dbgPort), NULL); return dbgPort != 0;
}4:调试对象句柄检测
使用NtQueryInformationProcess香询ProcessDebugobjectHandle
5.调试标志检测
使用NtQueryInformationProcess查询ProcessDebugFlags
6.内核调试器检测
使用NtQuerySystemInformation查询系统内核调试状态
函数检测
最简单的调试器检测函数是 IsDebuggerPresent():
BOOL WINAPI IsDebuggerPresent(void);该函数查询进程环境块(PEB)中的 BeingDebugged 标志,如果进程处在调试上下文中,则返回一 个非零值,否则返回零。
BOOL CheckDebug()
{
return IsDebuggerPresent();
}二、异常行为检测
7.无效句柄关闭检测
bool check_CloseHandle()
{
//
// 注:只要是无效的句柄就可以,这里使用 0xDEADC0DE。
// 如果存在调试器,调用 CloseHandle 时
// 会产生一个 EXCEPTION_INVALID_HANDLE (0xC0000008) 异常,并被 __except 捕获。
// 如果没有调试器,CloseHandle 会执行失败并返回 false。
//
__try
{
if (CloseHandle(HANDLE(INT_PTR(0xDEADC0DE))))
{
return true;
}
cout << "CloseHandle failed to execute." << endl;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return true;
}
return false;
}
8.单步异常检测
bool check_writeeflags()
{
size_t drx = 0;
CONTEXT* ctx;
__try
{
__writeeflags(__readeflags() | 0x100); // 触发单步异常
// 下一条指令会立即触发异常
__nop(); // 若指令继续执行,说明调试器捕获了异常并继续
// 能执行到这里,说明调试器已接管异常
return true; // 返回 true 表示检测到调试器
}
__except (
ctx = (GetExceptionInformation())->ContextRecord,
drx = (ctx->ContextFlags & CONTEXT_DEBUG_REGISTERS)
? (ctx->Dr0 | ctx->Dr1 | ctx->Dr2 | ctx->Dr3)
: 0,
EXCEPTION_EXECUTE_HANDLER
)
{
// 硬件断点检测:drx 非零表示存在调试器
return (drx != 0);
}
}
9.硬件断点检测
// 增强版检测:使用内联汇编直接读取寄存器
bool check_HardwareBreakpoints()
{
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if (!GetThreadContext(
GetCurrentThread(),
&ctx))
{
return false;
}
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3)
{
return true;
}
return (ctx.Dr7 & 0xFF) != 0;
}
10.调试器异常行为检测
// 方法5:除零异常检测
BOOL DetectDebuggerByDivideZero()
{
BOOL debuggerDetected = FALSE;
__try
{
// 故意触发除零异常
int a = 10;
int b = 0;
int result = a / b; // 这里会触发异常
// 如果有调试器且设置为“忽略除零异常”,会执行到这里
printf("[+] 检测到调试器 - 除零异常被忽略\n");
debuggerDetected = TRUE;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
DWORD exceptionCode = GetExceptionCode();
if (exceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO)
{
printf("[-] 正常:除零异常被捕获\n");
debuggerDetected = FALSE;
}
else
{
printf("[+] 检测到调试器 - 异常代码: 0x%08X\n", exceptionCode);
debuggerDetected = TRUE;
}
}
return debuggerDetected;
}
// 方法6:访问非法内存地址
BOOL DetectDebuggerByInvalidMemoryAccess()
{
BOOL debuggerDetected = FALSE;
__try
{
// 尝试访问 NULL 指针
int* pNull = NULL;
*pNull = 0xDEADBEEF; // 这里会触发访问违规
// 如果调试器忽略异常,会执行到这里
printf("[+] 检测到调试器 - 访问违规被忽略\n");
debuggerDetected = TRUE;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
// 正常情况下异常会被捕获
}
return debuggerDetected;
}
三、调试器行为干扰
11.线程隐藏
使用NtSetInformationThread隐藏线程
12.调试器于扰
插入无效指令或特殊序列干扰调试器
花指令. jmp eax等等
四、环境特征检测
13.调试器窗口检测
14.父进程检测
检查父进程是否为常见调试器调试器
一般双击运行的进程的父进程都是 explorer.exe,但是如果进程被调试父进程则是调试器进程。也就是 说如果父进程不是 explorer.exe 则可以认为程序正在被调试。
BOOL CheckDebug(void)
{
LONG status;
HANDLE hProcess;
DWORD dwParentPID = 0;
PROCESS_BASIC_INFORMATION pbi;
PROCESSENTRY32 pe32;
int pid = getpid();
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if (!hProcess)
{
return -1;
}
PNTQUERYINFORMATIONPROCESS NtQueryInformationProcess =
(PNTQUERYINFORMATIONPROCESS)GetProcAddress(
GetModuleHandleA("ntdll"),
"NtQueryInformationProcess"
);
status = NtQueryInformationProcess(
hProcess,
SystemBasicInformation,
(PVOID)&pbi,
sizeof(PROCESS_BASIC_INFORMATION),
NULL
);
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(
TH32CS_SNAPPROCESS,
0
);
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
CloseHandle(hProcess);
return FALSE;
}
BOOL bMore = Process32First(hProcessSnap, &pe32);
while (bMore)
{
if (pbi.InheritedFromUniqueProcessId == pe32.th32ProcessID)
{
if (stricmp(pe32.szExeFile, "explorer.exe") == 0)
{
CloseHandle(hProcessSnap);
CloseHandle(hProcess);
return FALSE; // 父进程是 explorer
}
else
{
CloseHandle(hProcessSnap);
CloseHandle(hProcess);
return TRUE; // 父进程异常(可能被调试)
}
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
CloseHandle(hProcess);
return FALSE;
}
15.进程检测
枚举系统进程查找调试器进程
进程检测通过检测当前桌面中是否存在特定的调试进程来判断是否存在调试器,但不能判断该调试器 是否正在调试该程序。
// 检查程序是否被调试。
// 通过查找特定调试器的窗口名来实现。
BOOL CheckDebug()
{
// 尝试查找标题为 "x32dbg" 的窗口。
// 如果窗口句柄非 NULL,则表示找到了调试器窗口。
if (FindWindowA("x32dbg", 0))
{
// 找到了调试器窗口,返回 0 (可能表示处于调试状态)。
return 0;
}
// 未找到调试器窗口。
return 1; // 返回 1 (可能表示未处于调试状态)。
}
// 检查程序是否被调试。
// 通过遍历进程列表,检查是否存在已知的调试器进程文件名来实现。
BOOL CheckDebug()
{
// 定义局部变量
DWORD ID; // 进程 ID (在此实现中未使用)
DWORD ret = 0; // 返回值 (在此实现中未使用)
PROCESSENTRY32 pe32;
// 设置 PROCESSENTRY32 结构体的大小,这是使用 Toolhelp32 函数的必需步骤
pe32.dwSize = sizeof(pe32);
// 创建系统所有进程的快照
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
// 检查快照句柄是否有效
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE; // 快照创建失败,返回 FALSE
}
// 获取快照中的第一个进程信息
BOOL bMore = Process32First(hProcessSnap, &pe32);
// 开始循环遍历快照中的所有进程
while (bMore)
{
// 检查当前进程的执行文件名 (szExeFile) 是否与已知的调试器文件名匹配
// 使用 stricmp 进行不区分大小写的字符串比较
if (stricmp(pe32.szExeFile, "OllyDBG.EXE") == 0 ||
stricmp(pe32.szExeFile, "OllyICE.exe") == 0 ||
stricmp(pe32.szExeFile, "x64_dbg.exe") == 0 ||
stricmp(pe32.szExeFile, "windbg.exe") == 0 ||
stricmp(pe32.szExeFile, "ImmunityDebugger.exe") == 0)
{
// 找到调试器进程,直接返回 TRUE (表示处于调试状态)
return TRUE;
}
// 获取下一个进程信息
bMore = Process32Next(hProcessSnap, &pe32);
}
// 遍历结束,关闭进程快照句柄
CloseHandle(hProcessSnap);
// 循环结束仍未找到任何调试器,返回 FALSE (表示未处于调试状态)
return FALSE;
}16.可凝DLL检测
检香加载的模块是否有调试相关DLL
五、时间差异检测
17.RDTSC计时检测
->QueryPerformanceCounter的实现使用时间截计数器检测热行时间差异
时间检测是指在程序中通过代码感知程序处于调试时与未处于调试时的各种运行时间差异来判断程序 是否处于调试状态。
例如我们在调试时步过两条指令所花费的时间远远超过 CPU 正常执行花费的时间,于是就可以通过 rdtsc 指令或 GetTickCount 函数来进行测试。
注:rdtsc 指令用于将时间标签计数器读入 EDX:EAX 寄存器。GetTickCount 返回从操作系统启动 所经过的毫秒数。
BOOL CheckDebug_RDTSC(void)
{
int BeingDbg = 0;
__asm
{
rdtsc // 第一次读取时间戳
mov ecx, edx
rdtsc // 第二次读取时间戳
sub edx, ecx // 计算时间差
mov BeingDbg, edx
}
if (BeingDbg > 2)
{
return 1; // 可能存在调试
}
return 0; // 正常执行
}18.GetTickCount计时检测
->可以自己实现这个函数
使用系统时间API检测执行时间
BOOL CheckDebug_TickCount(void)
{
DWORD time1 = GetTickCount();
__asm
{
mov ecx, 10
mov edx, 6
mov ecx, 10
}
DWORD time2 = GetTickCount();
if ((time2 - time1) > 0x1A)
{
return TRUE; // 执行时间异常(可能被调试)
}
else
{
return FALSE; // 正常
}
}
六、内存完整性检测
19.代码断点检测
扫描内存香找INT3断点指令(0xCC)
断点检测是根据调试器设置断点的原理来检测软件代码中是否设置了断点。调试器一般使用两者方法 设置代码断点:
通过修改代码指令为 INT3(机器码为0xCC)触发软件异常
通过硬件调试寄存器设置硬件断点 针对软件断点,检测系统会扫描比较重要的代码区域,看是否存在多余的 INT3 指令。
BOOL CheckDebug(void)
{
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS32 pNtHeaders;
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
pNtHeaders = (PIMAGE_NT_HEADERS32)(
(DWORD)pDosHeader + pDosHeader->e_lfanew
);
pSectionHeader = (PIMAGE_SECTION_HEADER)(
(DWORD)pNtHeaders +
sizeof(pNtHeaders->Signature) +
sizeof(IMAGE_FILE_HEADER) +
(WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader
);
DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
DWORD dwCodeSize = pSectionHeader->SizeOfRawData;
BOOL Found = FALSE;
__asm
{
cld // 清方向标志
mov edi, dwAddr // EDI 指向代码段起始
mov ecx, dwCodeSize // ECX = 代码段大小
mov al, 0CCh // INT3 指令(断点)
repne scasb // 搜索 0xCC
jnz NotFound // 未找到
mov Found, 1 // 找到断点字节
NotFound:
}
return Found;
}
20.硬件断点检测
直接读取DR寄存器值
而对于硬件断点,由于程序工作在保护模式下,无法访问硬件调试断点,所以一般需要构建异常程序 来获取 DR 寄存器的值。
BOOL CheckDebug(void)
{
CONTEXT context;
HANDLE hThread = GetCurrentThread();
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread, &context);
if (context.Dr0 != 0 ||
context.Dr1 != 0 ||
context.Dr2 != 0 ||
context.Dr3 != 0)
{
return 1; // 存在硬件断点
}
return 0; // 未检测到
}
21.内存保护检测
检查内存页的保护属性
e是可执行
r是可读
如果不符合er就可能是有调试

22.CRC校验检测
计算代码段CRC与预期值比较
23.内存断点检测
检测内存页是否被设置为特殊保护
下断点就是把你要下断点的内存页的权限去掉
比如读写断点,就是把读写权限去掉,然后访问就是出现异常
通过异常判断是否为你断点下的位置
七、字符串扫描检测
24:内存符串扫描
扫描进程内存查找调试器特征字符串
特征码检测枚举当前正在运行的进程,并在进程的内存空间中搜索特定调试器的代码片段。 例如 OllyDbg 有这样一段特征码:
0x41, 0x00, 0x62, 0x00, 0x6f, 0x00, 0x75, 0x00, 0x74, 0x00, 0x20, 0x00, 0x4f, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x79, 0x00, 0x44, 0x00, 0x62, 0x00, 0x67, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x4b, 0x00, 0x00, 0x00BOOL CheckDebug(void)
{
// OllyDbg 内存中的特征字符串(Unicode)
BYTE sign[] = {
0x41, 0x00, 0x62, 0x00, 0x6F, 0x00, 0x75, 0x00,
0x74, 0x00, 0x20, 0x00, 0x4F, 0x00, 0x6C, 0x00,
0x6C, 0x00, 0x79, 0x00, 0x44, 0x00, 0x62, 0x00,
0x67, 0x00, 0x00, 0x00,
0x4F, 0x00, 0x4B, 0x00, 0x00, 0x00
};
PROCESSENTRY32 sentry32 = {0};
sentry32.dwSize = sizeof(sentry32);
// 创建系统进程快照
HANDLE phsnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (phsnap == INVALID_HANDLE_VALUE)
return 1;
// 获取第一个进程
if (!Process32First(phsnap, &sentry32))
{
CloseHandle(phsnap);
return 1;
}
do
{
// 尝试打开目标进程(部分进程会失败)
HANDLE hps = OpenProcess(
MAXIMUM_ALLOWED,
FALSE,
sentry32.th32ProcessID
);
if (hps)
{
BYTE signRemote[sizeof(sign)] = {0};
DWORD szReaded = 0;
// 从固定地址读取内存(版本/ASLR 敏感)
ReadProcessMemory(
hps,
(LPCVOID)0x4F632A,
signRemote,
sizeof(signRemote),
&szReaded
);
CloseHandle(hps);
// 比对调试器特征
if (szReaded == sizeof(sign) &&
memcmp(sign, signRemote, sizeof(sign)) == 0)
{
CloseHandle(phsnap);
return 0; // 检测到调试器
}
}
sentry32.dwSize = sizeof(sentry32);
} while (Process32Next(phsnap, &sentry32));
CloseHandle(phsnap);
return 1; // 未检测到
}
25:模块字符串扫描
扫描加载模块的名称和路径
八、堆栈检测
26:堆栈指针检测
检查ESP寄存器值是否在合理范围
27.堆标志检测
检查堆分配标志是否包含调试特征
九、其他检测方法
28.OutputDebugString检测
使用OutputDebugstring并检查错误码
编写应用程序时,经常需要涉及到错误处理问题。许多函数调用只用 TRUE 和 FALSE 来表明函数的运 行结果。一旦出现错误,MSDN 中往往会指出请用 GetLastError 函数来获得错误原因。
恶意代码可以使用异常来破坏或者探测调试器。调试器捕获异常后,并不会立即将处理权返回被调试 进程处理,大多数利用异常的反调试技术往往据此来检测调试器。
多数调试器默认的设置是捕获异常后不将异常传递给应用程序。如果调试器不能将异常结果正确返回 到被调试进程,那么这种异常失效可以被进程内部的异常处理机制探测。
对于 OutputDebugString 函数,它的作用是在调试器中显示一个字符串,同时它也可以用来探测调 试器的存在。使用 SetLastError 函数,将当前的错误码设置为一个任意值。
如果进程没有被调试器附加,调用 OutputDebugString 函数会失败,错误码会重新设置,因此 GetLastError 获取的错误码应该不是我们设置的任意值。
但如果进程被调试器附加,调用 OutputDebugString 函数会成功,这时 GetLastError 获取的错误 码应该没改变
BOOL CheckDebug()
{
DWORD errorValue = 12345; SetLastError(errorValue);
OutputDebugStringA("Test for debugger!");
if (GetLastError() == errorValue)
{
return TRUE;
}
else
{
return FALSE;
}
}
同样还可以使用 CloseHandle、CloseWindow 产生异常,使得错误码改变。
BOOL CheckDebug()
{
DWORD ret = CloseHandle((HANDLE)0x1234);
if (ret != 0 || GetLastError() != ERROR_INVALID_HANDLE)
{
return TRUE;
}
else
{
return FALSE;
}
}
BOOL CheckDebug()
{
DWORD ret = CloseWindow((HWND)0x1234);
if (ret != 0 || GetLastError() != ERROR_INVALID_WINDOW_HANDLE)
{
return TRUE;
}
else
{
return FALSE;
}
}29.CheckRemoteDebuggerPresent
使用WindowsAPI直接检测调试器
CheckRemoteDebuggerPresent() 用于检测一个远程进程是否处于调试状态:
BOOL WINAPI CheckRemoteDebuggerPresent( _In_ HANDLE hProcess, _Inout_ PBOOL pbDebuggerPresent );如果 hProcess 句柄表示的进程处于调试上下文,则设置 pbDebuggerPresent 变量被设置为 TRUE,否则被设置为 FALSE。
BOOL CheckDebug() { BOOL ret; CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret); return ret; }30.TEB检测
检查TEB结构中的调试相关字段
31.指令计数检测
使用性能计数器统计指令数量
32.端口扫描检测
扫描常见的调试器通信端口
41.测试 STARTUPINFO
在使用 CreateProcess 创建进程时,需要传递 STARTUPINFO 的结构体指针,而常常我们并不会一个 一个设置其结构的值,连把其他不用的值清 0 都会忽略。 故可以使用 GetStartupInfo 检查启动信息,如果很多值不为 0,那么就说明自己的父进程不是 explorer。(explorer.exe 使用 shell32 中 ShellExecute 来运行程序,ShellExecute 会清掉不用的 值)
所以可以利用 STARTUPINFO 结构体中不用的字段来判断程序是否在被调试。
42.基于异常的反调试
进程中发生异常时若 SEH 未处理或注册的 SEH 不存在,会调用 UnhandledExceptionFilter,它会 运行系统最后的异常处理器。UnhandledExceptionFilter 内部调用了前面提到过的 NtQueryInformationProcess 以判断进程是否正在被调试。
若进程未被调试,则运行最后的异常处理器。若进程处于调试状态,则将异常派送给调试器。
SetUnhandledExceptionFilter 函数可以修改系统最后的异常处理器。
43.Debug Block
Debug Block 是指在需要保护的程序中,程序自身将一些只能同时有 1 个实例的功能占为己用。比如 一般情况下,一个进程只能同时被 1 个调试器调试,那么就可以设计一种模式,将程序以调试方式启 动,然后利用系统的调试机制防止被其他调试器调试。
十、组合防御策略
33.多层级防御
组合多种检测方法提高检测准确性
34.随机化检测
在程序不同位置随机执行检测
35.时间延识检测
在程序运行一段时间后执行检测
36.虚假检测
添加无害检测方法作为于扰
十一、代码保护技术
38.代码混淆
对反调试代码进行混清处理
39.动态代码生成
在运行时动态生成检测代码
40.加密保护
加密关键检测代码段