Windows反调试技术

病毒会通过各种方法获取当前运行的进程状态,看病毒进程是否处于被调试状态,如果处在被调试状态,则执行破坏模块或者执行其他的反调试逻辑。其中有很多技术已经失效,但是原理要掌握。
·
一、二、三 :探测调试器进程的存在与否
四 :识别调试器的行为
五 :干扰调试器的功能
六 :调试器漏洞


·

一、利用 WindowsAPI 检测调试器

1、IsDebuggerPresent

IsDebuggerPresent 查询进程环境块(PEB)中的 IsDebugged 标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。

BOOL ret = IsDebuggerPresent();
printf("ret = %d\n", ret);

汇编调用如下:

call                  [IsDebuggerPresent]
test                  eax,eax
jnz                   .debugger_found

2、CheckRemoteDebuggerPresent

CheckRemoteDebuggerPresent 同 IsDebuggerPresent 几乎一致。它不仅可以探测系统其他进程是否被调试,通过传递自身进程句柄还可以探测自身是否被调试。

BOOL CheckDebug()  
{  
    BOOL ret;  
    CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);  
    return ret;  
} 

汇编调用如下:

lea                eax,[.bDebuggerPresent]
push               eax                              ;pbDebuggerPresent
push 0xffffffff                                    ;hProcess
call                 [CheckRemoteDebuggerPresent]
cmp               dword [.bDebuggerPresent],0
jne               .debugger_found

3、NtQueryInformationProcess (未公开的API)

它用来检索一个指定进程的信息,主要参数是第一、二个。第一个参数可以指定病毒自身进程句柄;第二个参数设为 ProcessDebugPort (0x7),如果进程正在被调试,则返回调试端口,否则返回0。

__kernel_entry NTSTATUS NtQueryInformationProcess(
IN HANDLE ProcessHandle,// 指定进程句柄
IN PROCESSINFOCLASS ProcessInformationClass,// 想获取的信息类型
OUT PVOID ProcessInformation,// 指向保存提取信息的缓冲区
IN ULONG ProcessInformationLength,// 缓冲区大小
OUT PULONG ReturnLength // 返回提取信息的大小
);

汇编调用如下:

lea                eax,[.dwReturnLen]
push               eax                              ;ReturnLength
push               4                                 ;ProcessInformationLength
lea                eax,[.dwDebugPort]
push               eax                              ;ProcessInformation
push               ProcessDebugPort         ;ProcessInformationClass(7)
push                 0xffffffff                      ;ProcessHandle
call                  [NtQueryInformationProcess]
cmp               dword [.dwDebugPort],0
jne                  .debugger_found

4、GetLastError

编写应用程序时,经常需要涉及到错误处理问题。许多函数调用只用 TRUE 和 FALSE 来表明函数的运行结果。一旦出现错误,MSDN 中往往会指出请用 GetLastError() 函数来获得错误原因。恶意代码可以使用异常来破坏或者探测调试器。多数调试器默认的设置是捕获异常后不将异常传递给应用程序。如果调试器不能将异常结果正确返回到被调试进程,那么这种异常失效可以被进程内部的异常处理机制探测。
(1)OutputDebugString
它的作用是在调试器中显示一个字符串,同时它也可以用来探测调试器的存在。使用SetLastError函数,将当前的错误码设置为一个任意值。如果进程没有被调试器附加,调用OutputDebugString函数会失败,错误码会重新设置,因此GetLastError获取的错误码应该不是我们设置的任意值。但如果进程被调试器附加,调用OutputDebugString函数会成功,这时GetLastError获取的错误码应该没改变。

BOOL CheckDebug()  
{  
    DWORD errorValue = 12345;  
    SetLastError(errorValue);  
    OutputDebugString("Test for debugger!");  
    if (GetLastError() == errorValue)  
    {  
        return TRUE;  
    }  
    else  
    {  
        return FALSE;  
    }  
} 

(2)DeleteFiber
如果给次函数传递一个无效的参数的话会抛出 ERROR_INVALID_PARAMETER 异常。如果进程正在被调试,异常会被调试器捕获。因此可以通过 GetLastError 值来检测调试器的存在。如代码所示,0x57就是指ERROR_INVALID_PARAMETER。

BOOL CheckDebug()  
{  
    char fib[1024] = {0};  
    DeleteFiber(fib);  
    return (GetLastError() != 0x57);  
} 

(3)CloseHandle

BOOL CheckDebug()  
{  
    DWORD ret = CloseHandle((HANDLE)0x1234);  
    if (ret != 0 || GetLastError() != ERROR_INVALID_HANDLE)  
    {  
        return TRUE;  
    }  
    else  
    {  
        return FALSE;  
    }  
}

(4)CloseWindow

BOOL CheckDebug()  
{  
    DWORD ret = CloseWindow((HWND)0x1234);  
    if (ret != 0 || GetLastError() != ERROR_INVALID_WINDOW_HANDLE)  
    {  
        return TRUE;  
    }  
    else  
    {  
        return FALSE;  
    }  
}

5、ZwSetInformationThread

该函数用来设置一个线程的优先级(priority),也可以用来将线程隐藏,从而使调试器接收不到信息。

NTSYSAPI NTSTATUS ZwSetInformationThread(
  HANDLE          ThreadHandle,
  THREADINFOCLASS ThreadInformationClass,
  PVOID           ThreadInformation,
  ULONG           ThreadInformationLength
);

隐藏线程的调用形式:ZwSetInformationThread(GetCurrentThread( ), ThreadHideFromDebugger, NULL, 0);
调用上述形式的 ZwSetInformationThread 后,被调试进程就会被分离出来。该函数不会对正常运行的程序产生任何影响,但若运行的是调试器程序,因为该函数隐藏了当前线程,调试器无法再收到该线程的调试事件,最终会停止调试。

6、DebugActiveProcessStop

还有一个函数 DebugActiveProcessStop 用来分离调试器和被调试进程,从而停止调试。
·

二、手动检测 PEB 成员属性

虽然使用 Windows API 是探测调试器存在的最简单办法,但手动检查数据结构是恶意代码编写者最常使用的办法。这是因为很多时候通过 Windows API 实现的反调试技术无效,例如这些 API 函数被 rootkit 挂钩,并返回错误信息。因此,恶意代码编写者经常手动执行与这些API功能相同的操作。在手动检测中,PEB 结构中的一些标志暴露了调试器存在的信息。

1、检测 PEB.BeingDebugged

最基本的调试器检测技术就是检测进程环境块(PEB)中的 BeingDebugged 标志。IsDebuggerPresent() 就是检查这个标志以确定进程是否正在被用户模式的调试器调试。
在这里插入图片描述
汇编代码如下(比较简单,因此病毒多会用花指令来进行隐蔽,这个思路要有),反反调试思路是将 BeingDebugged 属性值修改为 0:

Mov                 eax,dword [fs:0x30]      ;EAX =  TEB.ProcessEnvironmentBlock
movzx              eax,byte [eax+0x02]      ;AL =  PEB.BeingDebugged
test                  eax,eax
jnz                   .debugger_found

2、检测 PEB.ProcessHeap

ProcessHeap 是一个未公开的位置,它被设置为加载器为进程分配的第一个堆的位置。ProcessHeap 位于PEB结构的 0x18 处。第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建。这些属性叫作 ForceFlags 和 Flags。
在这里插入图片描述
① 检测 ForceFlags 属性
在Windows XP系统中,ForceFlags属性位于堆头部偏移量 0x10 处;
在Windows 7系统中,对于32位的应用程序来说ForceFlags属性位于堆头部偏移量 0x44 处。

BOOL CheckDebug()  
{  
    int result = 0;  
    DWORD dwVersion = GetVersion();  
    DWORD dwWindowsMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));  
    //for xp  
    if (dwWindowsMajorVersion == 5)  
    {  
        __asm  
        {  
            mov eax, fs:[30h]  
            mov eax, [eax + 18h]  
            mov eax, [eax + 10h]  
            mov result, eax  
        }  
    }  
    else  
    {  
        __asm  
        {  
            mov eax, fs:[30h]  
            mov eax, [eax + 18h]  
            mov eax, [eax + 44h]  
            mov result, eax  
        }  
    }  
    return result != 0;  
}  

② 检测 Flags 属性
同样可以检查Windows XP系统中偏移量 0x0C 处,或者Windows 7系统中偏移量0x40处的Flags属性。这个属性总与ForceFlags属性大致相同,但通常情况下Flags与值2进行比较。

BOOL CheckDebug()  
{  
    int result = 0;  
    DWORD dwVersion = GetVersion();  
    DWORD dwWindowsMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));  
    //for xp  
    if (dwWindowsMajorVersion == 5)  
    {  
        __asm  
        {  
            mov eax, fs:[30h]  
            mov eax, [eax + 18h]  
            mov eax, [eax + 0ch]  
            mov result, eax  
        }  
    }  
    else  
    {  
        __asm  
        {  
            mov eax, fs:[30h]  
            mov eax, [eax + 18h]  
            mov eax, [eax + 40h]  
            mov result, eax  
        }  
    }  
    return result != 2;  
}

3、 检测 PEB.NTGlobalFlag

由于调试器中启动进程与正常模式下启动进程有些不同,所以它们创建内存堆的方式也不同。系统使用PEB结构偏移量 0x68 处的一个未公开位置,来决定如何创建堆结构。如果这个位置的值为 0x70,我们就知道进程正运行在调试器中。

BOOL CheckDebug()  
{  
    int result = 0;  
    __asm  
    {  
        mov eax, fs:[30h]  
        mov eax, [eax + 68h]  
        and eax, 0x70  
        mov result, eax  
    }  
    return result != 0;  
} 

操作系统创建堆时,值0x70是下列标志的一个组合。如果进程从调试器启动,那么进程的这些标志将被设置。
( FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS )
在这里插入图片描述

三、检测系统痕迹

通常,我们使用调试工具来分析恶意代码,但这些工具会在系统中驻留一些痕迹。恶意代码通过搜索这种系统痕迹,来确定你是否试图分析它。

1、查找调试器引用的注册表项

下面是调试器在注册表中的一个常用位置。
SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug(32位系统)
SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug(64位系统)
该注册表项指定当应用程序发生错误时,触发哪一个调试器。默认情况下,它被设置为Dr.Watson。如果该这册表的键值被修改为OllyDbg,则恶意代码就可能确定它正在被调试。

BOOL CheckDebug()  
{  
    BOOL is_64;  
    IsWow64Process(GetCurrentProcess(), &is_64);  
    HKEY hkey = NULL;  
    char key[] = "Debugger";  
    char reg_dir_32bit[] = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug";  
    char reg_dir_64bit[] = "SOFTWARE\\Wow6432Node\\Microsoft\\WindowsNT\\CurrentVersion\\AeDebug";  
    DWORD ret = 0;  
    if (is_64)  
    {  
        ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_64bit, &hkey);  
    }  
    else  
    {  
        ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_32bit, &hkey);  
    }  
    if (ret != ERROR_SUCCESS)  
    {  
        return FALSE;  
    }  
    char tmp[256];  
    DWORD len = 256;  
    DWORD type;  
    ret = RegQueryValueExA(hkey, key, NULL, &type, (LPBYTE)tmp, &len);  
    if (strstr(tmp, "OllyIce")!=NULL || strstr(tmp, "OllyDBG")!=NULL || strstr(tmp, "WinDbg")!=NULL || strstr(tmp, "x64dbg")!=NULL || strstr(tmp, "Immunity")!=NULL)  
    {  
        return TRUE;  
    }  
    else  
    {  
        return FALSE;  
    }  
} 

2、查找窗体信息

(1)FindWindow
检索处理顶级窗口的类名和窗口名称匹配指定的字符串。

BOOL CheckDebug()  
{  
    if (FindWindowA("OLLYDBG", NULL)!=NULL || FindWindowA("WinDbgFrameClass", NULL)!=NULL || FindWindowA("QWidget", NULL)!=NULL)  
    {  
        return TRUE;  
    }  
    else  
    {  
        return FALSE;  
    }  
} 

(2)EnumWindows
枚举所有屏幕上的顶层窗口,并将窗口句柄传送给应用程序定义的回调函数。

BOOL CALLBACK EnumWndProc(HWND hwnd, LPARAM lParam)    
{    
    char cur_window[1024];  
    GetWindowTextA(hwnd, cur_window, 1023);  
    if (strstr(cur_window, "WinDbg")!=NULL || strstr(cur_window, "x64_dbg")!=NULL || strstr(cur_window, "OllyICE")!=NULL || strstr(cur_window, "OllyDBG")!=NULL || strstr(cur_window, "Immunity")!=NULL)  
    {  
        *((BOOL*)lParam) = TRUE;  
    }  
    return TRUE;  
}
BOOL CheckDebug()  
{  
    BOOL ret = FALSE;  
    EnumWindows(EnumWndProc, (LPARAM)&ret);   
    return ret;  
} 

(3)GetForegroundWindow
获取一个前台窗口的句柄。

BOOL CheckDebug()  
{  
    char fore_window[1024];  
    GetWindowTextA(GetForegroundWindow(), fore_window, 1023);  
    if (strstr(fore_window, "WinDbg")!=NULL || strstr(fore_window, "x64_dbg")!=NULL || strstr(fore_window, "OllyICE")!=NULL || strstr(fore_window, "OllyDBG")!=NULL || strstr(fore_window, "Immunity")!=NULL)  
    {  
        return TRUE;  
    }  
    else  
    {  
        return FALSE;  
    }  
}

3、查找进程信息

BOOL CheckDebug()  
{  
    DWORD ID;  
    DWORD ret = 0;  
    PROCESSENTRY32 pe32;  
    pe32.dwSize = sizeof(pe32);   
    HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);   
    if(hProcessSnap == INVALID_HANDLE_VALUE)   
    {   
        return FALSE;   
    }  
    BOOL bMore = Process32First(hProcessSnap, &pe32);   
    while(bMore)  
    {  
        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)  
        {  
            return TRUE;  
        }  
        bMore = Process32Next(hProcessSnap, &pe32);   
    }  
    CloseHandle(hProcessSnap);   
    return FALSE;  
}

·

四、识别调试器行为

在逆向工程中,为了帮助恶意代码分析人员进行分析,可以使用调试器设置一个断点,或是单步执行一个进程。然而,在调试器中执行这些操作时,它们会修改进程中的代码。因此,恶意代码常使用几种反调试技术探测软件/硬件断点、完整性校验、时钟检测等几种类型的调试器行为。直接运行恶意代码与在调试器中运行恶意代码也会在一些细节上不同,如父进程信息、STARTUPINFO信息、SeDebugPrivilege权限等。

1、软件断点检查

调试器设置断点的基本机制是用软件中断指令INT 3临时替换运行程序中的一条指令,然后当程序运行到这条指令时,调用调试异常处理例程。INT 3指令的机器码是0xCC,因此无论何时,使用调试器设置一个断点,它都会插入一个0xCC来修改代码。恶意代码常用的一种反调试技术是在它的代码中查找机器码0xCC,来扫描调试器对它代码的INT 3修改。repne scasb指令用于在一段数据缓冲区中搜索一个字节。EDI需指向缓冲区地址,AL则包含要找的字节,ECX设为缓冲区的长度。当ECX=0或找到该字节时,比较停止。

BOOL CheckDebug()  
{  
    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  
        mov     ecx,dwCodeSize  
        mov     al,0CCH   
        repne   scasb  
        jnz     NotFound  
        mov Found,1  
NotFound:           
    }  
    return Found;  
}

2、硬件断点检查

在OllyDbg的寄存器窗口按下右键,点击View debug registers可以看到DR0、DR1、DR2、DR3、DR6和DR7这几个寄存器。DR0、Dr1、Dr2、Dr3用于设置硬件断点,由于只有4个硬件断点寄存器,所以同时最多只能设置4个硬件断点。DR4、DR5由系统保留。 DR6、DR7用于记录Dr0-Dr3中断点的相关属性。如果没有硬件断点,那么DR0、DR1、DR2、DR3这4个寄存器的值都为0。
在这里插入图片描述

BOOL CheckDebug()  
{  
    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 TRUE;    
    }    
    return FALSE;    
}  

3、执行代码校验和检查

恶意代码可以计算代码段的校验并实现与扫描中断相同的目的。与扫描0xCC不同,这种检查仅执行恶意代码中机器码CRC或者MD5校验和检查。

BOOL CheckDebug()  
{  
    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;      
    DWORD checksum = 0;  
    __asm  
    {  
        cld  
        mov     esi, dwAddr  
        mov     ecx, dwCodeSize  
        xor eax, eax  
    checksum_loop :  
        movzx    ebx, byte ptr[esi]  
        add        eax, ebx  
        rol eax, 1  
        inc esi  
        loop       checksum_loop  
        mov checksum, eax  
    }  
    if (checksum != 0x46ea24)  
    {  
        return FALSE;  
    }  
    else  
    {  
        return TRUE;  
    }  
}

4、时钟检测

被调试时,进程的运行速度大大降低,例如,单步调试大幅降低恶意代码的运行速度,所以时钟检测是恶意代码探测调试器存在的最常用方式之一。有如下两种用时钟检测来探测调试器存在的方法。
记录一段操作前后的时间戳,然后比较这两个时间戳,如果存在滞后,则可以认为存在调试器。
记录触发一个异常前后的时间戳。如果不调试进程,可以很快处理完异常,因为调试器处理异常的速度非常慢。默认情况下,调试器处理异常时需要人为干预,这导致大量延迟。虽然很多调试器允许我们忽略异常,将异常直接返回程序,但这样操作仍然存在不小的延迟。
(1)使用 rdtsc 指令(操作码0x0F31)
它返回至系统重新启动以来的时钟数,并且将其作为一个64位的值存入EDX:EAX中。恶意代码运行两次rdtsc指令,然后比较两次读取之间的差值。

BOOL CheckDebug()  
{  
    DWORD time1, time2;  
    __asm  
    {  
        rdtsc  
        mov time1, eax  
        rdtsc  
        mov time2, eax  
    }  
    if (time2 - time1 < 0xff)  
    {  
        return FALSE;  
    }  
    else  
    {  
        return TRUE;  
    }  
}

(2)QueryPerformanceCounter 和 GetTickCount
这两个Windows API函数也被用来执行一个反调试的时钟检测。使用这种方法的前提是处理器有高分辨率能力的计数器-寄存器,它能存储处理器活跃的时钟数。为了获取比较的时间差,调用两次QueryPerformanceCounter函数查询这个计数器。若两次调用之间花费的时间过于长,则可以认为正在使用调试器。GetTickCount函数返回最近系统重启时间与当前时间的相差毫秒数(由于时钟计数器的大小原因,计数器每49.7天就被重置一次)。

BOOL CheckDebug()  
{  
    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;  
    }  
}

5、判断父进程是否是 explorer.exe

一般双击运行的进程的父进程都是 explorer.exe,但是如果进程被调试的话,则父进程是调试器进程。也就是说如果父进程不是explorer.exe则可以认为程序正在被调试。

BOOL CheckDebug()  
{  
    LONG                      status;    
    DWORD                     dwParentPID = 0;    
    HANDLE                    hProcess;    
    PROCESS_BASIC_INFORMATION pbi;    
    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);  
    PROCESSENTRY32 pe32;  
    pe32.dwSize = sizeof(pe32);   
    HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);   
    if(hProcessSnap == INVALID_HANDLE_VALUE)   
    {   
        return FALSE;   
    }  
    BOOL bMore = Process32First(hProcessSnap, &pe32);   
    while(bMore)  
    {  
        if (pbi.InheritedFromUniqueProcessId == pe32.th32ProcessID)  
        {  
            if (stricmp(pe32.szExeFile, "explorer.exe")==0)  
            {  
                CloseHandle(hProcessSnap);  
                return FALSE;  
            }  
            else  
            {  
                CloseHandle(hProcessSnap);  
                return TRUE;  
            }  
        }  
        bMore = Process32Next(hProcessSnap, &pe32);   
    }  
    CloseHandle(hProcessSnap);  
} 

6、判断 STARTUPINFO 信息

explorer.exe创建进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0。所以可以利用STARTUPINFO来判断程序是否在被调试。

BOOL CheckDebug()  
{  
    STARTUPINFO si;  
    GetStartupInfo(&si);  
    if (si.dwX!=0 || si.dwY!=0 || si.dwFillAttribute!=0 || si.dwXSize!=0 || si.dwYSize!=0 || si.dwXCountChars!=0 || si.dwYCountChars!=0)  
    {  
        return TRUE;  
    }  
    else  
    {  
        return FALSE;  
    }  
}  

7、判断是否具有 SeDebugPrivilege 权限

默认情况下进程是没有SeDebugPrivilege权限的,但是当进程通过调试器启动时,由于调试器本身启动了SeDebugPrivilege权限,当调试进程被加载时SeDebugPrivilege也就被继承了。所以我们可以检测进程的SeDebugPrivilege权限来间接判断是否存在调试器,而对 SeDebugPrivilege 权限的判断可以用能否打开 csrss.exe 进程来判断

BOOL CheckDebug()  
{  
    DWORD ID;  
    DWORD ret = 0;  
    PROCESSENTRY32 pe32;  
    pe32.dwSize = sizeof(pe32);   
    HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);   
    if(hProcessSnap == INVALID_HANDLE_VALUE)   
    {   
        return FALSE;   
    }  
    BOOL bMore = Process32First(hProcessSnap, &pe32);   
    while(bMore)  
    {  
        if (strcmp(pe32.szExeFile, "csrss.exe")==0)  
        {  
            ID = pe32.th32ProcessID;  
            break;  
        }  
        bMore = Process32Next(hProcessSnap, &pe32);   
    }  
    CloseHandle(hProcessSnap);   
    if (OpenProcess(PROCESS_QUERY_INFORMATION, NULL, ID) != NULL)  
    {  
        return TRUE;  
    }  
    else  
    {  
        return FALSE;  
    }  
}

·

五、干扰调试器的功能

恶意代码可以用一些技术来干扰调试器的正常运行,这些技术当且仅当程序处于调试器控制之下时才试图扰乱程序的运行。

1、使用 TLS 回调

Thread Local Storage(TLS),即线程本地存储,是Windows为解决一个进程中多个线程同时访问全局变量而提供的机制。TLS可以简单地由操作系统代为完成整个互斥过程,也可以由用户自己编写控制信号量的函数。当进程中的线程访问预先制定的内存空间时,操作系统会调用系统默认的或用户自定义的信号量函数,保证数据的完整性与正确性。下面是一个简单的TLS回调的例子,TLS_CALLBACK1函数在main函数执行前调用IsDebuggerPresent函数检查它是否正在被调试。

#include "stdafx.h"  
#include <stdio.h>  
#include <windows.h>  
   
void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD dwReason, PVOID Reserved);  
   
#ifdef _M_IX86  
#pragma comment (linker, "/INCLUDE:__tls_used")  
#pragma comment (linker, "/INCLUDE:__tls_callback")  
#else  
#pragma comment (linker, "/INCLUDE:_tls_used")  
#pragma comment (linker, "/INCLUDE:_tls_callback")  
#endif  
EXTERN_C  
#ifdef _M_X64  
#pragma const_seg (".CRT$XLB")  
const  
#else  
#pragma data_seg (".CRT$XLB")  
#endif  
   
PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK1,0};  
#pragma data_seg ()  
#pragma const_seg ()  
   
#include <iostream>  
   
void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)  
{  
    if (IsDebuggerPresent())  
    {  
        printf("TLS_CALLBACK: Debugger Detected!\n");  
    }  
    else  
    {  
        printf("TLS_CALLBACK: No Debugger Present!\n");  
    }  
}  
   
int main(int argc, char* argv[])  
{  
    printf("233\n");  
    return 0;  
} 

要在程序中使用TLS,必须为TLS数据单独建一个数据段,用相关数据填充此段,并通知链接器为TLS数据在PE文件头中添加数据。_tls_callback[]数组中保存了所有的TLS回调函数指针。数组必须以NULL指针结束,且数组中的每一个回调函数在程序初始化时都会被调用,程序员可按需要添加。但程序员不应当假设操作系统已何种顺序调用回调函数。如此则要求在TLS回调函数中进行反调试操作需要一定的独立性。
正常运行这个程序会打印下面的内容。
TLS_CALLBACK: No Debugger Present!
233
如果把此程序在OllyDbg中运行,在OllyDbg暂停之前会打印下面的内容。
TLS_CALLBACK: Debugger Detected!
使用PEview查看.tls段,可以发现TLS回调函数。通常情况下,正常程序不使用.tls段,如果在可执行程序中看到.tls段,应该立即怀疑它使用了反调试技术。
在这里插入图片描述
由于TLS回调已广为人知,因此同过去相比,恶意代码使用它的次数已经明显减少。为数不多的合法程序使用TLS回调,所以可执行程序中的.tls段特别突出(IDA中 Ctrl+E 可查看所有入口点,有 tls 的话也会显示)。

2、主动设置断点干扰分析

因为调试器使用INT 3来设置软件断点,所以一种反调试技术就是在合法代码段中插入0xCC(INT 3)欺骗调试器,使其认为这些0xCC机器码是自己设置的断点。

BOOL CheckDebug()  
{  
      __try  
      {  
            __asm int 3  
      }  
      __except(1)  
      {  
            return FALSE;  
      }  
      return TRUE;  
} 

除了使用_try和_except以外还可以直接使用汇编代码安装SEH。在下面的代码中如果进程没有处于调试中,则正常终止;如果进程处于调试中,则跳转到非法地址0xFFFFFFFF处,无法继续调试。

#include "stdio.h"  
#include "windows.h"  
#include "tchar.h"  
   
void AD_BreakPoint()  
{  
    printf("SEH : BreakPoint\n");  
   
    __asm {  
        // install SEH  
        push handler  
        push DWORD ptr fs:[0]  
        mov DWORD ptr fs:[0], esp  
           
        // generating exception  
        int 3  
   
        // 1) debugging  
        //    go to terminating code  
        mov eax, 0xFFFFFFFF  
        jmp eax                 // process terminating!!!  
   
        // 2) not debugging  
        //    go to normal code  
handler:  
        mov eax, dword ptr ss:[esp+0xc]  
        mov ebx, normal_code  
        mov dword ptr ds:[eax+0xb8], ebx  
        xor eax, eax  
        retn  
   
normal_code:  
        //   remove SEH  
        pop dword ptr fs:[0]  
        add esp, 4  
    }  
   
    printf("  => Not debugging...\n\n");  
}  
   
int _tmain(int argc, TCHAR* argv[])  
{  
    AD_BreakPoint();  
   
    return 0;  
}

双字节操作码0xCD03也可以产生INT 3中断,这是恶意代码干扰WinDbg调试器的有效方法。在调试器外,0xCD03指令产生一个STATUS_BREAKPOINT异常。然而在WinDbg调试器内,由于断点通常是单字节机器码0xCC,因此WinDbg会捕获这个断点然后将EIP加1字节。这可能导致程序在被正常运行的WinDbg调试时,执行不同的指令集(OllyDbg可以避免双字节INT 3的攻击)。

BOOL CheckDebug()  
{  
      __try  
      {  
            __asm  
            {  
                  __emit 0xCD  
                  __emit 0x03  
            }  
      }  
      __except(1)  
      {  
            return FALSE;  
      }  
      return TRUE;   
} 

INT 2D原为内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常。但程序调试运行时不会触发异常,只是忽略。INT 2D指令在ollydbg中有两个有趣的特性。在调试模式中执行INT 2D指令,下一条指令的第一个字节将被忽略。使用StepInto(F7)或者StepOver(F8)命令跟踪INT 2D指令,程序不会停在下一条指令开始的地方,而是一直运行,就像RUN(F9)一样。在下面的代码中,程序调试运行时,执行INT 2D之后不会运行SEH,而是跳过NOP,把bDebugging标志设置为1,跳转到normal_code;程序正常运行时,执行INT 2D之后触发SEH,在异常处理器中设置EIP并把bDebugging标志设置为0。

BOOL CheckDebug()  
{  
    BOOL bDebugging = FALSE;  
   
    __asm {  
        // install SEH  
        push handler  
        push DWORD ptr fs:[0]  
        mov DWORD ptr fs:[0], esp  
           
        int 0x2d  
   
        nop  
        mov bDebugging, 1  
        jmp normal_code  
   
handler:  
        mov eax, dword ptr ss:[esp+0xc]  
        mov dword ptr ds:[eax+0xb8], offset normal_code  
        mov bDebugging, 0  
        xor eax, eax  
        retn  
   
normal_code:  
        //   remove SEH  
        pop dword ptr fs:[0]  
        add esp, 4  
    }  
   
    printf("Trap Flag (INT 2D)\n");  
    if( bDebugging )  return 1;  
    else              return 0;  
}

片内仿真器(ICE)断点指令ICEBP(操作码0xF1)是Intel未公开的指令之一。由于使用ICE难以在任意位置设置断点,因此ICEBP指令被设计用来降低使用ICE设置断点的难度。运行ICEBP指令将会产生一个单步异常,如果通过单步调试跟踪程序,调试器会认为这是单步调试产生的异常,从而不执行先前设置的异常处理例程。利用这一点,恶意代码使用异常处理例程作为它的正常执行流程。为了防止这种反调试技术,执行ICEBP指令时不要使用单步。

BOOL CheckDebug()  
{  
      __try  
      {  
            __asm __emit 0xF1  
      }  
      __except(1)  
      {  
            return FALSE;  
      }  
      return TRUE;  
} 

3、设置陷阱标志位

EFLAGS寄存器的第八个比特位是陷阱标志位。如果设置了,就会产生一个单步异常。

BOOL CheckDebug()  
{  
      __try  
      {  
            __asm  
            {  
                  pushfd       
                  or word ptr[esp], 0x100              
                  popfd  
                  nop          
            }  
      }  
      __except(1)  
      {  
            return FALSE;  
      }  
      return TRUE;  
} 

4、利用异常机制

(1)RaiseException
该函数可以产生若干不同类型的异常被调试器捕获。

BOOL TestExceptionCode(DWORD dwCode)  
{  
      __try  
      {  
            RaiseException(dwCode, 0, 0, 0);  
      }  
      __except(1)  
      {  
            return FALSE;  
      }  
      return TRUE;  
}  
    
BOOL CheckDebug()  
{  
      return TestExceptionCode(DBG_RIPEXCEPTION);      
}

(2)SetUnhandledExceptionFilter
进程中发生异常时若SEH未处理或注册的SEH不存在,会调用UnhandledExceptionFilter,它会运行系统最后的异常处理器。UnhandledExceptionFilter内部调用了前面提到过的NtQueryInformationProcess以判断是否正在调试进程。若进程正常运行,则运行最后的异常处理器;若进程处于调试,则将异常派送给调试器。SetUnhandledExceptionFilter函数可以修改系统最后的异常处理器。下面的代码先触发异常,然后在新注册的最后的异常处理器内部判断进程正常运行还是调试运行。进程正常运行时pExcept->ContextRecord->Eip+=4;将发生异常的代码地址加4使得其能够继续运行;进程调试运行时产生无效的内存访问异常,从而无法继续调试。

#include "stdio.h"  
#include "windows.h"  
#include "tchar.h"  
   
LPVOID g_pOrgFilter = 0;  
   
LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS pExcept)  
{  
    SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)g_pOrgFilter);  
   
    // 8900    MOV DWORD PTR DS:[EAX], EAX  
    // FFE0    JMP EAX  
    pExcept->ContextRecord->Eip += 4;  
   
    return EXCEPTION_CONTINUE_EXECUTION;  
}  
   
void AD_SetUnhandledExceptionFilter()  
{  
    printf("SEH : SetUnhandledExceptionFilter()\n");  
   
    g_pOrgFilter = (LPVOID)SetUnhandledExceptionFilter(  
                                (LPTOP_LEVEL_EXCEPTION_FILTER)ExceptionFilter);  
   
    __asm {  
        xor eax, eax;  
        mov dword ptr [eax], eax  
        jmp eax                       
    }  
       
    printf("  => Not debugging...\n\n");  
}  
   
int _tmain(int argc, TCHAR* argv[])  
{  
    AD_SetUnhandledExceptionFilter();  
   
    return 0;  
}

·

六、调试器漏洞

与所有软件一样,调试器也存在漏洞,有时恶意代码编写者为了防止被调试,会攻击这些漏洞。这里我们展示几种 OllyDbg 调试器处理PE格式文件时的漏洞

1、PE头漏洞

OllyDbg 非常严格地遵循了微软对 PE 文件头部的规定。在 PE 文件的头部,通常存在一个叫作 IMAGE_OPTIONAL_HEADER 的结构。
在这里插入图片描述
需要特别注意这个结构中的最后几个元素。NumberOfRvaAndSizes 标识了后面 DataDirectory 数组中的元素个数。DataDirectory 数组表示在这个可执行文件中的什么地方可找到其他导入可执行模块的位置,它位于可选头部结构的末尾,是一个比 IMAGE_DATA_DIRECTORY 略大一些的数组。数组中每个结构目录都指明了目录的相对虚拟地址和大小。DataDirectory 数组的大小被设置为 IMAGE_NUMBEROF_DIRECTORY_ENTRIES,它等于0x10。因为 DataDirectory 数组不足以容纳超过 0x10 个目录项,所以当 NumberOfRvaAndSizes 大于 0x10 时,Windows 加载器将会忽略 NumberOfRvaAndSizes。
OllyDbg 遵循了这个标准,然而无论 NumberOfRvaAndSizes 是什么值,OllyDbg 都使用它。因此,设置 NumberOfRvaAndSizes 为一个超过 0x10 的值,会导致在程序退出前,OllyDbg 对用户弹出一个窗口。如图所示,使用 LordPE 打开可执行文件,修改 RVA 数及大小并保存,再用 OllyDbg 打开,会提示错误 Bad or unknown format of 32-bit executable file。
在这里插入图片描述
另一种 PE 头的欺骗与节头部有关。文件内容中包含的节包括代码节、数据节、资源节,以及一些其他信息节。每个节都拥有一个 IMAGE_SECTION_HEADER 结构的头部。
在这里插入图片描述
在这里插入图片描述
VirtualSize 和 SizeOfRawData 是其中两个比较重要的属性。根据微软对PE的规定,VirtualSize 应该包含载入到内存的节大小,SizeOfRawData 应该包含节在硬盘中的大小。Windows 加载器使用 VirtualSize 和 SizeOfRawData 中的最小值将节数据映射到内存。如果 SizeOfRawData 大于 VirtualSize,则仅将 VirtualSize 大小的数据复制入内存,忽略其余数据。因为 OllyDbg 仅使用 SizeOfRawData,所以设置 SizeOfRawData 为一个类似 0x77777777 的大数值时,会导致 OllyDbg 崩溃。如图所示,使用 LordPE 打开可执行文件,点击区段,在区段表上右击,点击编辑区段,修改物理大小并保存,再用 OllyDbg打开,会提示同样的错误。
在这里插入图片描述
对抗这种反调试技术的最简单方法是用类似的编辑器手动修改PE头部。OllyDbg2.0和WinDbg不存在这种漏洞。

2、OutputDebugString漏洞

恶意代码常尝试利用 OllyDbg1.1 的格式化字符串漏洞,为 OutputDebugString 函数提供一个 %s 字符串的参数,让 OllyDbg 崩溃。因此,需要注意程序中可疑的 OutputDebugString 调用,例如 OutputDebugString("%s%s%s%s%s%s%s%s%s")。如果执行了这个调用,OllyDbg 将会崩溃。

·

七、参考资料

1、反调试技术总结——https://bbs.pediy.com/thread-225740.htm
2、八种反调试技术——https://blog.csdn.net/qq_42577465/article/details/91380966
3、脱壳的艺术(Anti-debug),调试器检测技术——https://blog.csdn.net/zhoujiaxq/article/details/23172281
4、《恶意代码分析实战》第16章反调试技术(本文的主体框架)
5、《逆向工程核心原理》第51章静态反调试技术&第52章动态反调试技术
6、技术分享:利用异常实现反调试——https://www.freebuf.com/articles/terminal/99454.html
7、https://github.com/LordNoteworthy/al-khaser

本文只是整理工作,收集整理自参考文章,持续更新,侵删。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值