调试
- 简 介
- 下 载
- 留 言
- 说 明
被调试时,进程的运行速度大大降低,例如,单步调试大幅降低恶意代码的运行速度,所以时钟检测是恶意代码探测调试器存在的最常用方式之一。有如下两种用时钟检测来探测调试器存在的方法。
记录一段操作前后的时间戳,然后比较这两个时间戳,如果存在滞后,则可以认为存在调试器。
记录触发一个异常前后的时间戳。如果不调试进程,可以很快处理完异常,因为调试器处理异常的速度非常慢。默认情况下,调试器处理异常时需要人为干预,这导致大量延迟。虽然很多调试器允许我们忽略异常,将异常直接返回程序,但这样操作仍然存在不小的延迟。
执行代码校验和检查
恶意代码可以计算代码段的校验并实现与扫描中断相同的目的。与扫描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; } }
SetUnhandledExceptionFilter
EXCEPTION_EXECUTE_HANDLER equ 1 表示我已经处理了异常,可以优雅地结束了
EXCEPTION_CONTINUE_SEARCH equ 0 表示我不处理,其他人来吧,于是windows调用默认的处理程序显示一个错误框,并结束
EXCEPTION_CONTINUE_EXECUTION equ -1 表示错误已经被修复,请从异常发生处继续执行
IsDebuggerPresent
调用IsDebuggerPresent,判断返回值eax==1被调试,eax==0没有调试。
CheckRemoteDebuggerPresent
CheckRemoteDebuggerPresent,判断buffer返回值是0还是1,1被调试,0没有调试。
NtQueryInformationProcess
NtQueryInformationProcess,ProcessInfoClass=0x1E来获取调试句柄,判断是否被调试,这个函数是Ntdll.dll中一个API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。
BOOL CheckDebug() { int debugPort = 0; HMODULE hModule = LoadLibrary("Ntdll.dll"); NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess"); NtQueryInformationProcess(GetCurrentProcess(), 0x7, &debugPort, sizeof(debugPort), NULL); return debugPort != 0; } BOOL CheckDebug() { HANDLE hdebugObject = NULL; HMODULE hModule = LoadLibrary("Ntdll.dll"); NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess"); NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &hdebugObject, sizeof(hdebugObject), NULL); return hdebugObject != NULL; } BOOL CheckDebug() { BOOL bdebugFlag = TRUE; HMODULE hModule = LoadLibrary("Ntdll.dll"); NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess"); NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &bdebugFlag, sizeof(bdebugFlag), NULL); return bdebugFlag != TRUE; }
NtSetInformationThread
NtSetInformationThread,ThreadInfoClass=0x11,这个不是检测调试,而是设置不把调试信息发送到调试器,可以直接把0x11修改为0x3或者其它数值就可以了。
ZwQuerySystemInformation
ZwQuerySystemInformation,SystemInfoClass=0x23(MACRO:SystemKernelDebuggerInformation),返回值是2字节的bool值,设置为0即可。
ZwQuerySystemInformation
ZwQuerySystemInformation,SystemInfoClass=0xB(MACRO:SystemModuleInformation),会去遍历内核模块,然后进行判断,第一次是获取需要存储的buffer长度,第二次才是真正获取信息,只要把buffer都置为0,就检测不到调试了,至于buffer的位置会在第一次调用后使用LocalAlloc申请空间来存储
CloseHandle
CloseHandle反调试,如果被调试了,那么KiRaiseUserExceptionDispatcher函数会被调用,走异常处理流程;如果是不被调试的状态,不会走向异常流程
检测硬件断点
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; }
检测硬件断点,是通过SEH异常来处理的
首先在SEH链中对当前模块的SEH头下一个软件断点
反虚拟机
CPUID判断ECX最高位31位是否为1,如果是1那么就是在虚拟机中,如果是0那就是在宿主机上。我自己加壳的这个样本有两个CPUID检测点,等它执行后修改ecx最高位为0就行。
使用GetSystemFirewareTable获取系统固件信息,判断其中有无下列字符串:VMware、VirtualBox、Parallels。注意大小写,只需要等GetSystemFirewareTable执行完成后对内存搜索VMware字符串(因为我用的VMware虚拟机),然后填充为0就可以过虚拟机检测了。主要就是填充VMware字符串。
IAT解密
进入解密函数后单步f7,当看到类似:lea register,dword ptr[register+imm](register:寄存器,imm:立即数)的语句,在执行完成后register中就是函数,再跟踪几步通过ret一类的就去执行真正的函数了
使用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回调函数中进行反调试操作需要一定的独立性。
设置System break-point作为第一个暂停的位置,这样就可以让OllyDbg在TLS回调执行前暂停
在IDA Pro中按Ctrl+E快捷键看到二进制的入口点,该组合键的作用是显示应用程序所有的入口点,其中包括TLS回调。双击函数名可以浏览回调函数
软件断点检查
调试器设置断点的基本机制是用软件中断指令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; }