反调试技术初探

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;
}

3. NtQueryInformationProcess

函数原型

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] ProcessHandle

要为其检索信息的进程句柄。

1
[in] ProcessInformationClass
含义
ProcessBasicInformation
0
检索指向 PEB 结构的指针,该结构可用于确定是否正在调试指定的进程,以及系统用于标识指定进程的唯一值。使用 CheckRemoteDebuggerPresentGetProcessId 函数获取此信息。
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处,第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建,这些属性叫做ForeceFlagsFlags,在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] //检测ForeceFlags属性
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] //检测Flags
mov result, eax
}
return result != 2;//与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. 时钟检测

被调试时,进程的运行速度大大降低,例如,单步调试大幅降低恶意代码的运行速度,所以时钟检测是恶意代码探测调试器存在的最常用方式之一。有如下两种用时钟检测来探测调试器存在的方法。
记录一段操作前后的时间戳,然后比较这两个时间戳,如果存在滞后,则可以认为存在调试器。
记录触发一个异常前后的时间戳。如果不调试进程,可以很快处理完异常,因为调试器处理异常的速度非常慢。默认情况下,调试器处理异常时需要人为干预,这导致大量延迟。虽然很多调试器允许我们忽略异常,将异常直接返回程序,但这样操作仍然存在不小的延迟。


反调试技术初探
http://example.com/2023/10/21/anti_base/
Author
yring
Posted on
October 21, 2023
Licensed under