逆核 _07_ 反调试技术

0x00 反调试技术

反调试技术大致分为静态与动态两组。静态反调试的目的大部分是检测调试器, 而动态反调试大部分是为了隐藏内部代码和数据。
注: 因为本书年代稍有久远, 且反调试技术也是日新月异, 所以我只总结一部分内容, 更多的还要靠实践后得到。

0x01 静态反调试技术

00. 目的

被调试进程用静态反调试技术来侦测自身是否处于被调试状态, 若侦测到处于被调试状态,则执行非常规代码来阻止。

01. PEB

  1. BeingDebugged(+0x2)
    被调试时该成员值被设为0x01。
    通过IsDebuggerPresent()API获取此值, 来判断进程是否处于被调试的状态。

  2. Ldr(+0xC)
    调试进程时, 其堆内存区域中就回出现一些特殊标识, 表示它处于被调试的状态, 其中一个醒目的标志是: 未使用的堆内存区域全部填充着0xFEEEFEEE。
    Ldr成员指向_PEB_LDR_DATA结构体的指针, 而_PEB_LDR_DATA结构体恰好是在堆内存区域中创建的。

  3. Process Heap(+0x18)
    ProcessHeap成员是指向HEAP结构体的指针。
    HEAP结构体中的Flags(+0xC)成员和Force Flags(+0x10)成员, 当进程处于正常运行时, 前者值为0x2, 后者值为0x0。
    通过GetProcessHeap()API或者从PEB结构体直接获取此值, 来判断进程是否处于被调试的状态。

  4. NtGlobalFlag(+0x68)
    调试进程时, 该成员值被设为0x70。
    通过PEB结构体直接获取此值, 来判断进程是否处于被调试的状态。

02. NtQueryInformationProcess()

通过NtQueryInformationProcess()API可以获取各种与进程调试相关的信息。
NtQueryInformationProcess()API定义:

1
2
3
4
5
6
7
NTSTATUS WINAPI NtQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_ PVOID ProcessInformationClass,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);

此函数的第2个参数: PROCESSINFOCLASS ProcessInformationClass指定特定值并调用该函数, 相关信息就会设置到第3个参数PVOID ProcessInformationClass。

PROCESSINFOCLASS是枚举类型, 其中有3个成员与调试器探测有关。

  1. ProcessDebugPort(0x7)
    进程处于调试状态时,系统会为它分配一个调试端口(Debug Port)。
    第2个参数值设为ProcessDebugPort(0x7)时, 调用NtQueryInformationProcess()就能获取调试端口。
    若进程处于调试状态, 则第3个参数设置为0xFFFFFFFF; 反之,设置为0。
    注: 通过CheckRemoteDebuggerPresent()API来检测进程是否处于被调试的状态, 此API内部调用了NtQueryInformationProcess(ProcessDebuggerPort)API。

  2. ProcessDebugObjectHandle(0x1E)
    调试进程时会生成调试对象(Debug Object)。
    第2个参数值设置为ProcessDebugObjectHandle(0x1E)时, 调用函数后通过第3个参数获取调试对象句柄。
    若进程处于调试状态, 句柄值存在; 反之, 句柄值为NULL。

  3. ProcessDebugFlags(0x1F)
    检测调试标志(Debug Flags)的值也可以判断进程是否处于被调试的状态。
    第2个参数值设为ProcessDebugFlags(0x1F)时, 调用函数后通过第3个参数可以获取调试标志的值。
    若进程处于调试状态, 值为0; 反之,值为1。

03. NtQuerySystemInformation()

通过NtQuerySystemInformation()API可以获取当前运行的多种OS信息。这种反调试技术基于调试环境检测: 检测当前OS是否在调试模式下运行。
NtQuerySystemInformation()API定义:

1
2
3
4
5
6
NTSTATUS WINAPI NtQuerySystemInformation(
_In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
_Inout_ PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength
);

此函数的第1个参数: SYSTEM_INFORMATION_CLASS SystemInformationClass指定特定值并调用该函数, 相关信息就会设置到第2个参数PVOID SystemInformation。

PROCESSINFOCLASS是枚举类型, 其中有1个成员可以判断出当前OS是否在调试模式下运行。

  1. SystemKernelDebuggerInformation(0x23)
    调用此API, 第1个参数为0x23时, 第2个参数为SYSTEM_KERNEL_DEBUGGER_INFORMATION结构体的地址。
    当API返回时, 若系统处于调试模式下, SYSTEM_KERNEL_DEBUGGER_INFORMATION.DebuggerEnabled值设为1。

04. NtQueryObject()

系统中的某个调试器调试进程时, 会创建1个调试对象类型的内核对象。

通过NtQueryObject()API可以获取系统各种内核对象的信息。
NtQueryObject()API定义:

1
2
3
4
5
6
7
NTSTATUS NtQueryObject(
_In_opt_ HANDLE Handle,
_In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
_Out_opt_ PVOID ObjectInformation,
_In_ ULONG ObjectInformationLength,
_Out_opt_ PULONG ReturnLength
);

此函数的第2个参数: OBJECT_INFORMATION_CLASS ObjectInformationClass指定特定值并调用该函数, 相关信息就会设置到第3个参数PVOID ObjectInformation。

OBJECT_INFORMATION_CLASS是枚举类型, 其中有1个成员的值获取系统所有对象的信息, 然后从中检测是否存在调试对象。

04.0. ObjectAllTypesInformation(0x03)
基本步骤:
(1)获取内核对象信息链表的大小

(2)分配内存

(3)获取内核对象信息链表
调用NtQueryObject()API后, 系统所有对象的信息代码就会被存入之前分配的内存处(pBuf), 然后将pBuf转换成POBJECT_ALL_INFORMATION类型。
OBJECT_ALL_INFORMATION结构体由OBJECT_TYPE_INFORMATION结构体数组组成。
实际内核对象类型的信息就存储在此结构体数组中, 通过循环检索即可查看是否存在”调试对象”对象类型。

(4)确定”调试对象”对象类型

detail:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void MyNtQueryObject()
{
typedef struct _LSA_UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} LSA_UNICODE_STRING, *PLSA_UNICODE_STRING, UNICODE_STRING, *PUNICODE_STRING;
typedef NTSTATUS (WINAPI *NTQUERYOBJECT)(
HANDLE Handle,
OBJECT_INFORMATION_CLASS ObjectInformationClass,
PVOID ObjectInformation,
ULONG ObjectInformationLength,
PULONG ReturnLength
);
#pragma pack(1)
typedef struct _OBJECT_TYPE_INFORMATION {
UNICODE_STRING TypeName;
ULONG TotalNumberOfHandles;
ULONG TotalNumberOfObjects;
}OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
typedef struct _OBJECT_ALL_INFORMATION {
ULONG NumberOfObjectsTypes;
OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];
} OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION;
#pragma pack()
POBJECT_ALL_INFORMATION pObjectAllInfo = NULL;
void *pBuf = NULL;
ULONG lSize = 0;
BOOL bDebugging = FALSE;
NTQUERYOBJECT pNtQueryObject = (NTQUERYOBJECT)
GetProcAddress(GetModuleHandle(L"ntdll.dll"),
"NtQueryObject");
// Get the size of the list
pNtQueryObject(NULL, ObjectAllTypesInformation, &lSize, sizeof(lSize), &lSize);
// Allocate list buffer
pBuf = VirtualAlloc(NULL, lSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
// Get the actual list
pNtQueryObject((HANDLE)0xFFFFFFFF, ObjectAllTypesInformation, pBuf, lSize, NULL);
pObjectAllInfo = (POBJECT_ALL_INFORMATION)pBuf;
UCHAR *pObjInfoLocation = (UCHAR *)pObjectAllInfo->ObjectTypeInformation;
POBJECT_TYPE_INFORMATION pObjectTypeInfo = NULL;
for( UINT i = 0; i < pObjectAllInfo->NumberOfObjectsTypes; i++ )
{
pObjectTypeInfo = (POBJECT_TYPE_INFORMATION)pObjInfoLocation;
if( wcscmp(L"DebugObject", pObjectTypeInfo->TypeName.Buffer) == 0 )
{
bDebugging = (pObjectTypeInfo->TotalNumberOfObjects > 0) ? TRUE : FALSE;
break;
}
// calculate next struct
pObjInfoLocation = (UCHAR*)pObjectTypeInfo->TypeName.Buffer;
pObjInfoLocation += pObjectTypeInfo->TypeName.Length;
pObjInfoLocation = (UCHAR*)(((ULONG)pObjInfoLocation & 0xFFFFFFFC) + sizeof(ULONG));
}
if( pBuf )
VirtualFree(pBuf, 0, MEM_RELEASE);
printf("NtQueryObject(ObjectAllTypesInformation)\n");
if( bDebugging ) printf(" => Debugging!!!\n\n");
else printf(" => Not debugging...\n\n");
}

05. ZwSetInformationThread()

通过ZwSetInformationThread()API可以强制将被调试者和调试者分离(Detach)。
ZwSetInformationThread()API定义:

1
2
3
4
5
6
NTSTATUS ZwSetInformationThread(
_In_ HANDLE ThreadHandle,
_In_ THREADINFOCLASS ThreadInformationClass,
_In_ PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength
);

ZwSetInformationThread()API是为线程设置信息的。

该函数拥有2个参数: 第1个参数ThreadHandle用来接收当前线程的句柄; 第2个参数ThreadInformationClass表示线程信息。

若第2个参数设置为ThreadHideFromDebugger(0x11), 调用该函数后, 调试进程就会被分离出来, 这会导致调试器终止运行,同时终止自身进程。

通过DebugActiveProcessStop()API可以分离调试器与被调试进程, 从而停止调试。
与上文的API不同的是, 上文中的API隐藏当前线程, 使调试器无法再收到线程的调试事件, 最终停止调试。

06. TLS回调函数

实际上TLS回调函数本身并非是一种反调试技术, 但TLS回调函数会优先EP代码执行, 所以反调试技术中常使用到它。

07. ETC

各种各样的反调试技术会根据系统中获取的进程、文件、窗口、注册表、主机名、计算机名、用户名、环境变量等, 来判断是否处于被调试的状态。

0x02 动态反调试技术

00. 目的

反调试技术的目的就是隐藏和保护程序代码和数据。PE保护器中一般会大量应用动态反调试技术, 以保护源程序的核心算法。动态反调试技术会干扰调试器, 使之无法正常跟踪查找源程序的核心代码(OEP)。

01. 异常

根据正常运行进程和被调试进程遇到异常时, 接受异常的对象不同来进行判断, 然后根据不同的结果执行不同的操作。

02. Timing Check

在调试器中逐行跟踪代码比程序正常运行耗费的时间要多的多, 利用此技术计算运行时间来判断是否处于被调试

03. 陷阱标志

EFLAGS的第9个为陷阱标志。当TF设为1是, CPU进入单步执行模式, 执行1指令后出发一个单步异常, 然后TF自动清0。

0x03 高级反调试技术

垃圾代码、扰乱代码对齐、加密/解密、Stolen Bytes(Remove OEP)、API重定向、Debug Block.ETC。