【反调试】常用反调试方法汇总
前言
常用反调试(Anti-Debug)检测思路:
检测PEB结构特定标志位,例如:"BeingDebugged"。
使用系统API,例如:"IsDebuggerPresent"等。
检测指定调试器特征,例如:检测进程,窗口标题等。
索引
// PEB_检测 BeingDebugged NtGlobalFlag // Heap_检测 HeapFlags ForceFlags // API_检测 IsDebuggerPresent CheckRemoteDebuggerPresent NtQueryInformationProcess NtQuerySystemInformation // 时间_检测 rdtsc GetTickCount // 硬件断点_检测 GetThreadContext // 异常_检测 SEH // 内存CRC_检测 --- // 分离调试器 NtSetInformationThread
示例
示例中的检测代码,仅用于效果演示,实际分析时检测代码并不会与示例代码完全相同(作用或原理相同),另外某些数据结构在32/64系统中的偏移也不相同,需要读者灵活应用后自行识别反调试!
PEB_检测
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 如果被调试,BeingDebugged标志位 == 1 __asm { mov eax, fs:18h // TEB mov eax, [eax + 30h] // PEB movzx eax, [eax + 2] // PEB->BeingDebugged } // PEB偏移0x68,默认值==0,如果被调试系统会设置特定值(0x70) __asm { mov eax, fs:18h // TEB mov eax, [eax + 30h] // PEB movzx eax, [eax + 68h] // PEB->NtGlobalFlags and eax, 0x70 }
Heap_检测
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 使用HeapFlags和ForceFlags检测调试器并不可靠,但却很常用 __asm { mov eax, fs:[30h] mov eax, [eax + 18h] // PEB.ProcessHeap mov eax, [eax + 0ch] // PEB.ProcessHeap.Flags cmp eax, 2 // 正常情况下,该值==2 } __asm { mov eax, fs:[30h] mov eax, [eax + 18h] // PEB.ProcessHeap mov eax, [eax + 10h] // PEB.ProcessHeap.ForceFlags test eax, eax // 正常情况下,该值 == 0 }
API_检测
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 内部原理同PEB->BeingDebugged一样 if (IsDebuggerPresent()) { // 调试 } else { // 未调试 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 检测指定进程是否被调试,内部会调用NtQueryInformationProcess() BOOL result; CheckRemoteDebuggerPresent(NtCurrentProcess, &result); if (result) { // 调试 } else { // 未调试 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
PVOID pInfo; NTSTATUS status; ULONG returnlen; //如果进程被调试,其返回值应为0xffffffff status = NtQueryInformationProcess(NtCurrentProcess, ProcessDebugPort, &pInfo, sizeof(pInfo), &returnlen); if (NT_SUCCESS(status)) { if(pInfo != NULL) { //调试 } } // status = NtQueryInformationProcess(NtCurrentProcess, ProcessDebugObjectHandle, &pInfo, sizeof(pInfo), &returnlen); if (status == 0xC0000353) { //未调试 } else { //调试 } // 当调试器存在时,返回0 status = NtQueryInformationProcess(NtCurrentProcess, ProcessDebugFlags, &pInfo, sizeof(pInfo), &returnlen); if (NT_SUCCESS(status)) { if((ULONG_PTR)pInfo == 0)) { //调试 } }
时间_检测
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//利用rdtsc指令(操作码0x0F31),它返回至系统重新启动以来的时钟数,并且将其作为一个64位的值存入EDX:EAX中 //恶意代码运行两次rdtsc指令,然后比较两次读取之间的差值 BOOL CheckDebug() { DWORD time1, time2; __asm { rdtsc mov time1, eax rdtsc mov time2, eax } if (time2 - time1 < 0xff) { //调试 } else { //未调试 } }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//该方法适合放在关键代码处检测,如果F9运行一般检测不到 DWORD t1, t2; t1 = GetTickCount(); for (int i = 0; i < 0x100000; i++) { __asm{ nop; nop; nop; nop; } } t2 = GetTickCount(); if (t2 - t1 > 1000) { //调试 } else { //未调试 }
硬件断点_检测
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//获取线程上下文,检测硬件断点 CONTEXT ctx; ctx.ContextFlags = CONTEXT_ALL; GetThreadContext(NtCurrentThread,&ctx); if (ctx.Dr0 || ctx.Dr1 || ctx.Dr3 || ctx.Dr2) { //调试 } else { //未调试 }
异常_检测
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//======设置veh异常回调 LONG NTAPI VEHandle(PEXCEPTION_POINTERS ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == 0xC0000005) { //检测硬件断点 if (ExceptionInfo->ContextRecord->Dr0 || ExceptionInfo->ContextRecord->Dr1 || ExceptionInfo->ContextRecord->Dr2 || ExceptionInfo->ContextRecord->Dr3) { //调试 } else { //未调试 } ExceptionInfo->ContextRecord->Eip += 3; return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; } //======手动构造异常,在VEH中检测硬件断点调试 void SetVeh() { AddVectoredExceptionHandler(0, VEHandle); PUCHAR p = NULL; *p = NULL; RemoveVectoredExceptionHandler(VEHandle); }
内存CRC_检测
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//======内存检测除了CRC也可以使用其它HASH函数,只要保证内存区段数据不被修改即可 void _CRCCheck() { DWORD start, size; //1.更早的 计算一遍页面CRC PVOID imagebase; PIMAGE_DOS_HEADER pDosHead; PIMAGE_NT_HEADERS pNtHead; PIMAGE_SECTION_HEADER pSec; CRCPAGE_HASH ctx; //获取DOS头 imagebase = GetModuleHandle(NULL); pDosHead = (PIMAGE_DOS_HEADER)imagebase; if (pDosHead->e_magic != IMAGE_DOS_SIGNATURE) { return; } //获取NT头 pNtHead = (PIMAGE_NT_HEADERS)((ULONG_PTR)imagebase + pDosHead->e_lfanew); if (pNtHead->Signature != IMAGE_NT_SIGNATURE) { return; } //获取区段头 pSec = IMAGE_FIRST_SECTION(pNtHead); //遍历所有区段,计算CRC g_crcPage.clear(); for (int i = 0; i < pNtHead->FileHeader.NumberOfSections; i++) { if (pSec->Characteristics & IMAGE_SCN_MEM_EXECUTE) { start = (ULONG_PTR)imagebase + pSec->VirtualAddress; size = (ULONG_PTR)pSec->Misc.VirtualSize; ctx.m_startAddr = (PVOID)start; ctx.m_size = size; ctx.m_hash = crc32((const void*)start, size); g_crcPage.push_back(ctx); } pSec++; } //计算完CRC后,在后面的某段代码中再次计算,比较2次计算结果,如果不相同程序既有可能被调试或补丁 }
分离调试器
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//该函数不会对正常运行的程序产生任何影响,但若运行的是调试器程序, //因为该函数隐藏了当前线程,调试器无法再收到该线程的调试事件,最终停止调试 void _ThreadHideFromDebugger() { NtSetInformationThread( NtCurrentThread, ThreadHideFromDebugger, //隐藏调试器线程 NULL, NULL); }
参考
网上有很多文章总结的都比较详细,这里就不在做重复劳动了,具体可以参考以下文章。
//看雪 https://bbs.pediy.com/thread-70470.htm https://bbs.pediy.com/thread-225740.htm //比较全面,有反调试和虚拟机 https://github.com/LordNoteworthy/al-khaser //转载的前面的文章,格式比较好,方便查看 https://www.cnblogs.com/huhu0013/archive/2011/07/05/2098358.html