常见反调试技术分析与绕过方法详解
本文系统梳理静态与动态反调试技术,涵盖PEB检测、API调用、异常处理及时间差分析等手段,并提供针对性绕过方案。
1.静态反调试
1.跟PEB有关
1.isdebuggerpresent

对于这种最简单说的调用windows API的反调试,可以看到这里有两个分支,一个是直接退出(在检测到调试的情况下),一边是正常运行的代码,这里其实可以现在反调试前下断点,然后运行到判断前面和判断那里,修改零标志位ZF为1,就可以绕过反调试。
如果遇到发现不了调试函数的情况的话,也许可以尝试在退出函数那里下断点
比如一般的反调试的退出手段都是调用exit
void AntiDebug(){ //反调试函数
bool IsBeingDebugged=checkIsDebug();//通过某种方式判断当前程序是否正在被调试
if (IsBeingDebugged){ //如果正在被调试
exit(); //退出程序
}
}
可以尝试在这个退出函数下断点,详情见https://www.52pojie.cn/thread-1432590-1-1.html
2.LDR(位于PEB 0x0c)
调试的时候,未使用的堆内存会被全部填充为0xEEFEEEFE,PEB.LDR成员指向_PEB_LDR_DATA结构体,这个结构体在堆里创造的,可以扫描区域就能判断是不是在调试
可以直接把被改变的堆内存直接修改成NULL
3.ProcessHeap(0x18)
PEB.ProcessHeap成员是指向HEAP结构体的指针
进程处于调试的状态的时候,这个结构体里面的Heap.Flags和Heap.ForceFlags会被改变
可以把这两个值分别设定为0x2和0x0(即原始状态)
方法
(1) 查找调用堆操作的函数
- 搜索堆操作函数: 在 IDA 中,通过 Shift+F12 或直接跳转,定位以下函数:
- 高级 API 函数:
HeapAlloc、HeapFree、HeapCreate。 - 底层实现函数:
RtlAllocateHeap、RtlFreeHeap、RtlCreateHeap。
- 定位传入的堆指针:
- 堆操作函数的第一个参数通常是堆的指针(
HEAP*)。 - IDA 的伪代码中可能显示为:
void *allocated = RtlAllocateHeap(heap_pointer, flags, size);- 通过交叉引用(
X快捷键)检查heap_pointer的来源,确定堆的地址。
4.NTGlobalFlag(0x68)
调试的时候,同样是PEB的成员会被设置为0x70,所以这里只需要修改为零即可。
2.NTqueryInformationProcess
NTqueryInformationProcess()API探测调试器技术通过NTQueryInformationProcess()API获取各种与进程调试相关的信息
__kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,//检索的进程信息类型
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);第二个PEOCESSINFOCLASS类型的参数反调试选用的结构体成员:
ProcessDebugPort,//0x7
ProcessDebugObjectHandle,//0x1E
PrecessDebugFlags, //0x1F
ProcessDebugPort
DWORD dwDebugPort = 0;
// 调用 NtQueryInformationProcess 查询调试端口
NTSTATUS status = NtQueryInformationProcess(
GetCurrentProcess(),
(PROCESSINFOCLASS)7, // 7 对应 ProcessDebugPort
&dwDebugPort,
sizeof(dwDebugPort),
NULL
);
if (status == 0) {
printf("调试端口值: 0x%X\n", dwDebugPort);
if (dwDebugPort != 0) {
printf("检测到调试器连接\n");
} else {
printf("未检测到调试器连接\n");
}
} else {
printf("NtQueryInformationProcess 调用失败,状态码: 0x%X\n", status);
}CheckRemoteDebuggerPresent
这个函数不仅可以检测进程是不是在调试状态,而且还可以用来检测其他进程是不是在调试状态
ProcessDebugObjectHandle(0x1E)
调试时会出现调试对象,进程处于调试状态时候,调试对象句柄的值就存在,处于非调试状态的时候,则为NULL
HANDLE hDebugObject=Null;
pNtQuerInformationProcess(GetCurrentProcess(),ProcessDebugObjectHandle,&hDebugObject,sizeof(hDebugObject),NUll)
if(hDebugObject!=0x0)
printf("Debugging")
ProcessDebugFlags(0x1F)
调试标志DebugFlags的值也可以判断进程是不是处于被调试状态,函数的第二个参数为ProcessDebugFlags的时候,调用函数的时候通过第三个参数(&DebugFlags)即可获得值。如果为0就是在调试状态
3.SystemKernelDebuggerInformation

第一个参数SystemInformationClass传入SystemKernelDebuggerInformation, 第二个参数SystemInformation为结构体SYSTEM_KERNEL_DEBUGGER_INFORMATION的地址, SYSTEM_KERNEL_DEBUGGER_INFORMATION.DebuggerEnabled == 1则表明系统处于调试状态.
4.NtQueryObject
调用NtQueryObject函数的时候。会向第二个参数OBJECT_INFORMATION_CLASS ObjectInformationClass赋予某个值,调用API后了包含相关信息的结构体指针会被返回到第三个参数PVOID objectInformation里面,
__kernel_entry NTSYSCALLAPI NTSTATUS NtQueryObject(
[in, optional] HANDLE Handle,
[in] OBJECT_INFORMATION_CLASS ObjectInformationClass,
[out, optional] PVOID ObjectInformation,
[in] ULONG ObjectInformationLength,
[out, optional] PULONG ReturnLength
);破解之法就是对第二个参数的值修改为0就可以了
5.ZwSetInformationThread
这个函数是强制的把被调试者和调试者分离出来,破解方法是在执行之前把第二个参数的值修改为0继续运行
6.TLS回调函数
1.TLS
TLS是线程局部存储,解决多线程中变量同步的问题
也就是说,在程序中,多个线程的变量是同步变化的,一个线程里面的变量被赋予了值,另外一个线程也会被赋予同样的值,而TLS技术可以让其他线程无法访问变量
TLS变量的定义是
__declspec(thread) int num = 100;//(这里必须在全局变量的位置)2.TLS回调函数
#include <windows.h>
#include <iostream>
#pragma comment(linker,"/INCLUDE:__tls_used")
HANDLE Event;
HANDLE Thread1, Thread2;
__declspec(thread) int num = 100;//TLS变量
/*
我们定义的TLS回调函数体
*/
VOID NTAPI Tls_CallBack(PVOID DllHandle,DWORD Reason,PVOID Reserved)
{
if (DLL_PROCESS_ATTACH == Reason)
{
printf("进程创建!\n");
}
if (DLL_PROCESS_DETACH == Reason)
{
printf("进程销毁!\n");
}
if (DLL_THREAD_ATTACH == Reason)
{
printf("线程创建!\n");
}
if (DLL_THREAD_DETACH == Reason)
{
printf("线程销毁!\n");
}
}
//首先注册TLS函数,用PIMAGE_TLS_CALLBACK 类型的数组来接受,最后一个参数必须是0
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTlsHeader[] = { Tls_CallBack,0 };
//上面是TLS回调函数的结构体,存储的回调函数的地址
#pragma data_seg()
DWORD WINAPI ThreadProc1(
LPVOID lpParam
)
{
num = 200;
printf("1. num= %d\n",num);
SetEvent(Event);
return 1;
}
DWORD WINAPI ThreadProc2(
LPVOID lpParam
)
{
WaitForSingleObject(Event, INFINITE);
printf("2. num= %d\n", num);
SetEvent(Event);
return 1;
}
int main()
{
Event = CreateEvent(NULL, FALSE, FALSE, NULL); //手动复位,无信号
Thread1 = CreateThread(NULL,NULL,ThreadProc1,NULL,NULL,NULL);
Thread2 = CreateThread( NULL, NULL, ThreadProc2,NULL, NULL, NULL);
WaitForSingleObject(Thread1, INFINITE);
WaitForSingleObject(Thread2, INFINITE);
system("pause");
return 0;
}TLS回调函数的程序中,在程序载入内存时候,首先执行TLS回调函数,再进入IMageBase+EntryPoint进入OEP
既然TLS回调函数会先于程序执行,那么在TLS回调函数之中加入反调试的话,就可以直接退出程序了
#pragma comment(linker,"/INCLUDE:__tls_used")
VOID NTAPI tls_callback(
PVOID DllHandle,
DWORD Reason,
PVOID Reserved
)
{
BOOL qqql = FALSE;
HANDLE Process = GetCurrentProcess();
//如果处于调试,则直接退出
CheckRemoteDebuggerPresent(Process, &qqql);
if (qqql)
{
MessageBoxA(NULL, "禁止调试!", "警告", MB_OK);
TerminateProcess(Process,NULL);
}
return;
}
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTlsfun[] = { tls_callback ,0 };
#pragma data_seg()
int main()
{
printf("你好!\n");
system("pause");
return 0;
}3.解决方法

然后去010中找到1B0的位置

![]()
这一段都填充为零

然后找到TLS在数据段的位置,并在010里面填充为0既可以了

7.ETC
这个就是判断当前系统是不是常规系统,就是从系统中获取各种信息
比如1.检测窗口
2.检测进程
3.检测计算机名称
4.检测程序运行路径
5.检测虚拟即是不是在运行状态
这样的话就可以判断出当前是不是在调试状态
破解方法就是在调试中去找到这些检测的API然后可以有NULL覆盖字符串,这样就无法检测是不是在调试,或者是各种修改跳转的寄存器等等
2.动态反调试
1.异常
异常用于反调试,程序发生异常的时候,SEH机制之下,os会接受到异常,然后调用进程中注册的SEH处理,但是如果在调试运行的时候发生异常,调试器就会接受处理,就无法去处理异常,甚至跳到非正常代码干扰用户。可以这样来判断是不是在调试。
一些常见异常
1.
- EXCEPTION_ACCESS_VIOLATION ( 0xC0000005 )
即非法访问,试图访问不存在或不具有访问权限的内存区域
2.
- EXCEPTION_BREAKPOINT ( 0x80000003 )
就是遇到了断点
3.
- EXCEPTION_ILLEGAL_INSTRUCTION ( 0xC000001D )
就是遇到了无法解析的指令
4.
- EXCEPTION_INT_DIVIDE_BY_ZERO ( 0xC0000094 )
除法的时候分母为零
5.
- EXCEPTION_SINGLE_STEP ( 0x80000004 )
执行一条指令然后暂停,就是单步模式
__try
{
//可能出现的异常
}
__except(/*异常代码*/)
{
//异常处理代码
}SetUnhandledExceptionFilter
如果程序发生异常的时候,但是没有注册SEH或则和没有处理异常的话,就会调用kernel32!UnhandledExceptionFilter()API,这个函数内部会运行系统的最后一个异常处理器,也就是弹出错误提示并终止运行(异常处理器是一个结构体,第一个元素的指针,指向下一个异常处理器,第二个元素是异常处理函数,如果不能处理异常就给下一个异常处理器,直到最后一个终止运行的异常处理器)
kernel32!UnhandledExceptionFilter()内部调用了ntdll!NtQueryInformationProcess判断进程是不是在调试
如果不是调试就正常处理,是调试的话就讲异常给调试器SetUnhandledExceptionFilter()API会修改最后的异常处理器
这个函数的参数是新的最后的异常处理函数的指针
综上:就可以在新注册的最后一个异常处理器中判断是不是在调试,然后根据结果可以改变EIP。总之,这个反调试的手段就是添加一个最后的异常处理里面加反调试,调试的时候遇到未注册的SEH什么的就给这个SetUnhandledExceptionFilter函数然后交给添加的异常处理函数
破解方法:我认为可以让NTQueryInformationProcess失效,不过要跟踪运行到这个注册的异常处理函数,然后跳转到正常的运行代码。
2.Timing Check
就是判断时间差,因为正常运行的代码时间差很短,而调试的话时间差很大,操作方法是直接操作获取时间信息的语句
clock()
time(NULL)获取当前时间
QueryPerformanceCounter() 计时数据,在实际应用中,程序通过测量微小的时间差来检测调试器的干预。
RDTSC
x86CPU里面一个64位的寄存器,CPU对每个时钟周期计数然后保存在TSC中,RDTSC是一条汇编指令,读取TSC的值到EDX:EAX寄存器里面,两次调用RDTSC指令会存在时间间隔,通过时间差值课判断进程是不是在调试状态,
破解方法
1.不使用跟踪命令,直接run过相关代码
2.操作第二个RDTSC的值
3.操作jz/jnz
#include <stdio.h>
#include <windows.h>
int main() {
int a = 10;
__try {
// 触发一个异常中断,模拟非法操作
__asm {
int 3; // 触发调试异常
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
// 异常处理部分
printf("Exception caught! Code: %x\n", GetExceptionCode());
}
printf("Program continues normally.\n");
return 0;
}
上面这个实验说明了当调试的时候,程序不会走异常处理部分,不调试的时候会走异常处理部分
3.陷阱标志
这个就是在EFLAGS寄存器的第8位,如果TF值被修改为1,那么CPU进入单步执行模式,单步执行中,CPU执行一条指令之后就会触发异常,然后陷阱标志被自动清零,可以这样和SEH结合,不调试的时候遇到异常进入SEH,调试的时候就会继续执行,陷入陷阱,可能花很久去调试一些无用的代码。
破解方法就是在注册SEH处设置一个断点,再在去新的EIP地址设置一个断点,然后到正常运行的代码
4.INT 2D
这是一个中断指令,作用是让下一个字节被忽略,后一个字节会被识别出新的指令继续执行。
在OLlydbg里面,执行到INT 2D的时候程序不会暂停,而是一直运行,知道遇到断点。
如果在调试的时候,执行INT 2D不会触发异常进入SEH,而是继续执行(跳过一个字节)
如果没有调试的时候,就会触发异常进入SEH
破解方法就是修改判断代码,跟踪进入SEH程序,或者运行到SEH程序,也可以设置OLLYDBG调试器选项,使之忽视EXCEPTION_SINGLE_STEP异常
5.0xcc
其实就是检测你是不是下了断点
还有一个就是API反调试,其实就是在API前设置断点来进行反反调试
6.比较校验和
这个反调试就是在这个区域设置了断点之后,这个地方的校验和肯定不一样,这样可以判断是不是在调试
破解方法就是修改CRC比较语句,让校验失效。或者不在这个校验的地方下断点。
本文系统梳理静态与动态反调试技术,涵盖PEB检测、API调用、异常处理及时间差分析等手段,并提供针对性绕过方案。
由 AI 根据全文自动生成,仅供参考,以原文为准
