Last updated on January 30, 2024 am
记录一些常用基础反调试技术
一、 API级别
1.IsDebuggerPresent
IsDebuggerPresent查询进程环境块PEB中的IsDebugged
标志,如果没有运行在调试环境中,返回0,否则返回一个非零值
1 2 3
| BOOL CheckDebug() { return IsDebuggerPresent(); }
|
2.ChechRemoteDebuggerPresent
与IsDebuggerPresent
使用几乎一致,可以探测系统其他进程是否被调试,通过传递自身进程句柄还可以探测自身是否被调试
1 2 3 4 5
| BOOL CheckDebug() { BOOL ret; CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret); return ret; }
|
函数原型
1 2 3 4 5 6 7
| __kernel_entry NTSTATUS NtQueryInformationProcess( [in] HANDLE ProcessHandle, [in] PROCESSINFOCLASS ProcessInformationClass, [out] PVOID ProcessInformation, [in] ULONG ProcessInformationLength, [out, optional] PULONG ReturnLength );
|
参数
要为其检索信息的进程句柄。
1
| [in] ProcessInformationClass
|
值 |
含义 |
ProcessBasicInformation 0 |
检索指向 PEB 结构的指针,该结构可用于确定是否正在调试指定的进程,以及系统用于标识指定进程的唯一值。使用 CheckRemoteDebuggerPresent 和 GetProcessId 函数获取此信息。 |
ProcessDebugPort 7 |
检索一个 DWORD_PTR 值,该值是进程的调试器的端口号。 非零值指示进程正在环 3 调试器的控制下运行。 |
ProcessDebugFlag 0x1f |
检测是否在调试器环境下运行,零值表示正在被调试 |
1
| [out] ProcessInformation
|
指向调用应用程序提供的缓冲区的指针,函数将请求的信息写入其中。 写入的信息的大小因 ProcessInformationClass 参数的数据类型而异
1
| [in] ProcessInformationLength
|
ProcessInformation 参数指向的缓冲区的大小(以字节为单位)。
1
| [out, optional] ReturnLength
|
指向变量的指针,其中函数返回所请求信息的大小。 如果函数成功,则这是 由 ProcessInformation 参数指向的缓冲区中写入的信息的大小 (如果缓冲区太小,则为成功接收信息) 所需的最小缓冲区大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| BOOL CheckDebug() {
int debugFlag = 0; HMODULE hModule = LoadLibrary(TEXT("Ntdll.dll")); NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess"); NtQueryInformationProcess(GetCurrentProcess(), (PROCESSINFOCLASS)0x1f, &debugFlag, sizeof(debugFlag), NULL); cout << debugFlag << endl; return debugFlag == 0; } int main() { if (CheckDebug()) { printf("HACK\n"); } else { printf("HELLO WORLD\n"); } getchar(); return 0; }
|
4.GetLastError
在程序出现错误时,MSDN会指出使用GetLastError()
函数来获得错误原因。而调试器捕获异常后,并不会立即讲处理权交给进程处理,而是自己先接管。多数调试器默认的设置是捕获异常后不将异常传递给应用程序。如果调试器不能将异常结果正确返回到被调试进程,那么这种异常失效可以被进程内部的异常处理机制探测,据此就可以利用异常来实现反调试技术。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include<Windows.h> #include <iostream> using namespace std;
BOOL CheckDebug() { DWORD ret = CloseHandle((HANDLE)0x1234); if (ret != 0 || GetLastError() != ERROR_INVALID_HANDLE) { return TRUE; } else { return FALSE; } }
int main() {
if (CheckDebug()) { cout << "True" << endl; } else { cout << "没有调试" << endl; }
getchar(); return 0; }
|
二、手动检测数据结构
除了使用API检测,也可以是手动检测,也就是自己去检查线程环境块等相关信息
1.检测BeingDebugged属性
Windows操作系统维护着每个正在运行的进程的PEB结构,它包含与这个进程相关的所有用户态参数。这些参数包括进程环境数据,环境数据包括环境变量、加载的模块列表、内存地址,以及调试器状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| typedef struct _PEB { BYTE Reserved1[2]; BYTE BeingDebugged; BYTE Reserved2[1]; PVOID Reserved3[2]; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; PVOID Reserved4[3]; PVOID AtlThunkSListPtr; PVOID Reserved5; ULONG Reserved6; PVOID Reserved7; ULONG Reserved8; ULONG AtlThunkSListPtr32; PVOID Reserved9[45]; BYTE Reserved10[96]; PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine; BYTE Reserved11[128]; PVOID Reserved12[1]; ULONG SessionId; } PEB, *PPEB;
|
运行时,fs:[0x30]
始终指向PEB基址,为了实现反调试技术,恶意代码可以通过这个位置检查BeingDebugged
标志,来判断是否被调试
1 2 3 4 5 6 7 8 9 10 11
| BOOL CheckDebug() { int result = 0; __asm { mov eax, fs:[30h] mov al, BYTE PTR [eax + 2] mov result, al } return result != 0; }
|
2.检测ProcessHeap属性
Reserved数组中有一个未公开的位置叫做ProcessHeap,它被设置为加载器为进程分配的第一个堆的位置
这个ProcessHeap位于PEB结构的0x18处,第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建,这些属性叫做ForeceFlags
和Flags
,在Windows XP系统中,ForeceFlags
属性位于堆头部偏移0x10
处,Flags
位于偏移0x0C
处,在Win7以上系统中,ForeceFlags
位于堆头部偏移0x44
处,Flags
位于偏移0x40
处
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| BOOL CheckDebug() { int result = 0; __asm { mov eax, fs:[30h] mov eax, [eax + 18h] mov eax, [eax + 44h] mov result, eax } return result != 0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| BOOL CheckDebug() { int result = 0; __asm { mov eax, fs:[30h] mov eax, [eax + 18h] mov eax, [eax + 40h] mov result, eax } return result != 2; }
|
3.检测NTGlobalFlag属性
由于调试器中启动进程与正常模式下启动进程有些不同,所以它们创建内存堆的方式也不同。系统使用PEB结构偏移量0x68处的一个未公开位置,来决定如何创建堆结构。如果这个位置的值为0x70,我们就知道进程正运行在调试器中。
1 2 3 4 5 6 7 8 9 10 11 12
| 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.软件断点检查
调试器设置断点的基本机制是用软件中断指令INT 3临时替换运行程序中的一条指令,然后当程序运行到这条指令时,调用调试异常处理例程。
INT 3指令的机器码是0xCC,因此无论何时,使用调试器设置一个断点,它都会插入一个0xCC来修改代码。
恶意代码常用的一种反调试技术是在它的代码中查找机器码0xCC,来扫描调试器对它代码的INT 3修改。
repne scasb指令用于在一段数据缓冲区中搜索一个字节。EDI需指向缓冲区地址,AL则包含要找的字节,ECX设为缓冲区的长度。当ECX=0或找到该字节时,比较停止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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.硬件断点检查
DR0、Dr1、Dr2、Dr3用于设置硬件断点,由于只有4个硬件断点寄存器,所以同时最多只能设置4个硬件断点。DR4、DR5由系统保留。 DR6、DR7用于记录Dr0-Dr3中断点的相关属性。如果没有硬件断点,那么DR0、DR1、DR2、DR3这4个寄存器的值都为0。
1 2 3 4 5 6 7 8 9 10 11 12
| 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. 执行代码校验和检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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. 时钟检测
被调试时,进程的运行速度大大降低,例如,单步调试大幅降低恶意代码的运行速度,所以时钟检测是恶意代码探测调试器存在的最常用方式之一。有如下两种用时钟检测来探测调试器存在的方法。
记录一段操作前后的时间戳,然后比较这两个时间戳,如果存在滞后,则可以认为存在调试器。
记录触发一个异常前后的时间戳。如果不调试进程,可以很快处理完异常,因为调试器处理异常的速度非常慢。默认情况下,调试器处理异常时需要人为干预,这导致大量延迟。虽然很多调试器允许我们忽略异常,将异常直接返回程序,但这样操作仍然存在不小的延迟。