【反调试】常用反调试方法汇总

前言

常用反调试(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_检测

// 如果被调试,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            
}
View Code

Heap_检测

// 使用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
}
View Code

API_检测

// 内部原理同PEB->BeingDebugged一样
if (IsDebuggerPresent())
{
    // 调试
}
else
{
    // 未调试
}
View Code
// 检测指定进程是否被调试,内部会调用NtQueryInformationProcess()
BOOL    result;
CheckRemoteDebuggerPresent(NtCurrentProcess, &result);
if (result)
{
    // 调试
}
else
{
    // 未调试
}
View Code
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))
    {
        //调试
    }
}
View Code

时间_检测

//利用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  
    {  
        //未调试 
    }  
} 
View Code
//该方法适合放在关键代码处检测,如果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
{
    //未调试
}
View Code

硬件断点_检测

//获取线程上下文,检测硬件断点
CONTEXT        ctx;
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(NtCurrentThread,&ctx);

if (ctx.Dr0 || ctx.Dr1 || ctx.Dr3 || ctx.Dr2)
{
    //调试
}
else
{
    //未调试
}
View Code

异常_检测

//======设置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);
}
View Code

内存CRC_检测

//======内存检测除了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次计算结果,如果不相同程序既有可能被调试或补丁
}
View Code

分离调试器

//该函数不会对正常运行的程序产生任何影响,但若运行的是调试器程序,
//因为该函数隐藏了当前线程,调试器无法再收到该线程的调试事件,最终停止调试
void _ThreadHideFromDebugger()
{
    NtSetInformationThread(
        NtCurrentThread,
        ThreadHideFromDebugger, //隐藏调试器线程
        NULL,
        NULL);
}
View Code

参考

网上有很多文章总结的都比较详细,这里就不在做重复劳动了,具体可以参考以下文章。

//看雪
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
posted @ 2020-01-11 22:11  SunsetR  阅读(1607)  评论(0编辑  收藏  举报