第51章 静态反调试技术

本文最后更新于:2022年5月27日 下午

本章我们将学习静态组别中的反调试技术,了解各技术的工作原理,并学习相应的破解之法。

51.1 静态反调试的目的

被调试进程用静态反调试技术来侦测自身是否处于被调试状态,若侦测到处于被调试状态,则执行非常规代码(主要是终止代码)来阻止。具体的实现方法包括调试器检测方法、调试环境检测方法、强制隔离调试器的方法等等。反调试破解方法主要用来从探测代码获取信息,然后修改信息本身使反调试技术失效。

许多静态反调试技术对OS有较强依赖性。这意味着静态反调试技术在Windows XP系统下可以正常使用,而在Windows Vista/7操作系统中可能失效。本章所有练习示例在Windows XP中顺利通过测试。

51.2 PEB

利用PEB结构体信息可以判断当前进程是否处于被调试状态。这些信息值得信赖、使用方便,所以广泛应用于反调试技术。

PEB结构体的成员中与反调试技术密切相关的成员如代码51-2所示。

1
2
3
4
5
+0x002 BeingDebugged    : UChar
+0x008 ImageBaseAddress : Ptr32 Void
+0x00c Ldr : Ptr32 _PEB_LDR_DATA
+0x018 ProcessHeap : Ptr32 Void
+0x068 NtGlobalFlag : Uint4B

BeingDebugged成员是一个标志(Flag),用来表示进程是否处于被调试状态。Ldr、ProcessHeap、NtGlobalFlag成员与被调试进程的堆内存特性相关。

接下来分别讲解以上4个PEB成员。

借助FS段寄存器所指的TEB结构体可轻松获取进程的PEB结构体地址。TEB.ProcessEnvironmentBlock成员(偏移为+0x30)指向PEB结构体地址,有以下2种方法可以获取PEB结构体的地址。

(1)直接获取PEB的地址

MOV EAX, DWORD PTR FS: [0x30]; FS[0x30] = address of PEB

(2) 先获取TEB地址,再通过ProcessEnvironmentBlock成员(偏移为+0x30 )获取PEB地址

MOV EAX, DWORD PTR FS: [0x18] ; FS[0x18] = address of TEB

MOV EAX, DWORD PTR DS: [EAX+0x30]; DS[EAX+0x30] = address of PEB

第二种方法其实是第一种方法的展开形式,二者都通过TEB.ProcessEnviromnentBlock成员的值来获取PEB结构体的地址。更详细的说明请参考第46、47章。

51.2.1 BeingDebugged(+0x2)

进程处于调试状态时,PEB.BeingDebugged成员(+0x2)的值被设置为1 (TRUE);进程在非调试状态下运行时,其值被设置为0 ( FALSE )。

lsDebuggerPresent()

IsDebuggerPresent() AIM获取PEB.BeingDebugged的值来判断进程是否处于被调试状态。直 接查看其代码可以更清楚地理解它(我的系统环境中,PEB的起始地址为7FFD9000 ),如图51-1所示。

IsDebuggerPresent() API代码非常简单,先获取TEB结构体的地址(FS:[18]),再通过TEB.ProcessEnvironmentBlock成员(+0x30)获取PEB的地址,然后访问PEB.BeingDebugged成员

(+0x2)。如图51-1所示,PEB的地址为7FFD7000, PEB.BeingDebugged成员的地址为7FFDF002。因当前正用OllyDbg调试进程,故BeingDebugged的值为1 (TRUE)。

-
破解之法

只要借助OllyDbg调试器的编辑功能,将PEB.BeingDebugged的值修改为0 ( FALSE )即可。

51.2.2 Ldr(+0xC)

该方法仅适用于Windows XP系统,而在Windows Vista以后的系统中则无法使用。另外,利用附加功能将运行中的进程附加到调试器时,堆内存中并不出现上述标识。

调试进程时,其堆内存区域中就会岀现一些特殊标识,表示它正处于被调试状态。其中最醒目的是,未使用的堆内存区域全部填充着0xFEEEFEEE,这证明正在调试进程。利用这一特征即可判断进程是否处于被调试状态。

PEB.Ldr成员是一个指结构体的指针,结构体恰好是在堆内存区域中创建的,所以扫描该区域即可轻松查找是否存在0xFEEEFEEE区域(我的系统环境中,PEB的起始地址为7FFD9000 ),如图51-2所示。

进入PEB.Ldr地址(241EA0),向下拖动滑动条,查找0xFEEEFEEE区域,如图51-3所示。

在堆内存中可以看到填充着0xFEEEFEEE的区域。

-
破解之法

只要将填充着0xFEEEFEEE值的区域全部覆写为NULL即可。

51.2.3 Process Heap(+0x18)

该方法仅在WindowsXP系统中有效,Windows7系统不存在以上特征。此外,将运行中的进程附加到调试器时,也不会出现上述特征

PEB.ProcessHeap成员(+0x18)是指向HEAP结构体的指针。

1
2
3
4
5
6
7
8
lkd> dt ntdll!_HEAP
+0x000 Entry : _HEAP_ENTRY
+0x008 SegmentSignature : Uint4B
+0x00c SegmentFlags : Uint4B
+0x010 SegmentListEntry : _LIST_ENTRY
+0x018 Heap : Ptr32 _HEAP
+0x01c BaseAddress : Ptr32 Void
+0x020 NumberOfPages : Uint4B

以上列岀了HEAP结构体的部分成员,进程处于被调试状态时,Flags(+0xC)与Force Flags成员(+0x10)被设置为特定值。

GetProcessHeap()

PEB.ProcessHeap成员(+0x18)既可以从PEB结构体直接获取,也可以通过GetProcessHeap()API获取。下面看看GetProcessHeap() API的代码(我的系统环境中,PEB的起始地址为7FFDF000)。

GetProcessHeap() API 的代码基本类似于 IsDebuggerPresent(),按照 TEB → PEB →

PEB.ProcessHeap顺序依次访问。

图51-4中进程HEAP结构体的地址为PEB.ProcessHeap=140000。

Flags(+0xC) & Force Flags(+0x10)

进程正常运行(非调试运行)时,Heap.Flags成员(+0xC)的值为0x2,Heap.ForceFlags成员 (+0x10)的值为0x0。进程处于被调试状态时,这些值也会随之改变(参考图51-5.)。

所以,比较这些值就可以判断进程是否处于被调试状态。

-
破解之法

只要将HEAP.Flags与HEAP.ForceFlags的值重新设置为2与0即可(HEAP.Flags=2,HEAP.ForceFlags=0)。

51.2.4 NtGlobalFlag(+0x68)

调试进程时,PEB.NtGlobalFlag成员(+0x68)的值会被设置为0x70。所以,检测该成员的值即可判断进程是否处于被调试状态(我的系统环境中,PEB的起始地址为7FFD7000),如图51-6所示。

NtGlobalFlag 0x70是下列Flags值进行bit OR (位或)运算的结果。

1
2
3
FLG_HEAP_ENABLE_TAIL_CHECK		(0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)

被调试进程的堆内存中存在(不同于非调试运行进程的)特别标识,因此在PEB.NtGlobalFlag成员中添加了上述标志。

-
破解之法

重设PEB.NtGlobalFlag值为0即可(PEB.NtGlobalFlag=0 )。

将运行中的进程附加到调试器时,NtGlobalFlag的值不变。

51.2.5 练习:StaAD_PEB.exe

下面调试示例文件StaAD_PEB.exe来学习基于PEB的反调试技术,以及相应的破解方法。在OllyDbg中按F9键运行StaAD_PEB.exe文件,如图51-7所示,所有项都显示当前进程处于调试之中,基于PEB的反调试功能工作正常。

51.2.6 破解之法

下面介绍在OllyDbg中破解PEB反调试技术的方法。按Ctrl+F2重启OllyDbg后,直接到main()函数的起始地址处(401000)。

PEB.BeingDebugged

跟踪调试代码,在401036地址处遇到调用IsDebuggerPresent()API的代码,如图51-8所示。使用StepInto(F7)命令跟踪进入API,出现图51-1所示的代码。只要将PEB.BeingDebugged值修改为0,

即可破解基于BeingDebugged检测的反调试技术。

PEB.Ldr

继续调试会遇到PEB.Ldr反调试代码,如图51-9所示。

接下来简单讲解代码。40107B地址处的CALL EAX指令用来调用ntdll.NtCurrentTeb() API,40107D地址处的MOV指令用来将PEB地址保存到EBX寄存器。地址40109040109E间的指令用来将局部变量([EBP-20][EBP-2C])初始化为EEFEEEFE值。而4010A1地址处的MOV指令用来将PEB.Ldr地址存储到ESI寄存器。继续跟踪到4010C7地址处,如图51-10所示。

地址4010B0~4010DA间的代码由循环构成。下面看看4010C7地址处的CMP EDI,DWORD PTR DS:[ECX]指令。EDI寄存器中存储着从PEB.Ldr地址读取的4个字节值,[ECX]中的值为EEFEEEFE (ECX寄存器中存储着初始化为EEFEEEFE的数组的起始地址)。也就是说,图51-10中的代码用来查找PEB.Ldr中初始化为EEFEEEFE的区域。

该调试探测技术的破解之法是:先转到(4010C5地址的)EDX寄存器所指的PEB.Ldr,然后查找EEFEEEFE区域并用NULL值覆盖。

选中PEB.Ldr下的整个EEFEEEFE区域,在OllyDbg菜单栏中依次选择“Binary - Fill with 00’s”菜单填充(如图51-11所示)。按F2键在4010FB地址处设置断点,然后按F9运行即可安全跳出循环。

PEB.ProcessHeap

继续调试,遇到图51-12所示的代码。

以上代码通过检测 PEB.ProcessHeap.Flags 与 PEB.ProcessHeap.ForceFlags 的值来反调试。401112地址处的MOV指令用来将PEB.ProcessHeap结构体的首地址转移到EDI寄存器(图51 -12内存窗口显示的地址为150000 )。地址401115处的[EDI+C]是PEB.ProcessHeap.Flags值,将该值修改为2。地址40113F处的[EDI+10]是PEB.ProcessHeap.ForceFlags值,将该值修改为0。这样修改就能破解基于PEB.ProcessHeap的反调试代码。

PEB.NtGlobalFlag

继续调试,遇到基于PEB.NtGlobalFlag的反调试代码,如图51-13所示。

地址401168处的[EBX+68]即为PEB.NtGlobalFlag,将其值修改为0即可破解反调试代码

请注意:在Windows XP中使用OllyDbg开始调试程序时,EBX寄存器中存储的是PEB的地址。

51.3 NtQuerylnformationProcess()

下面介绍另外一种利用NtQueryInformationProcess() API探测调试器的技术。通过NtQueryInformationProcess() API可以获取各种与进程调试相关的信息,该函数定义如代码51-4所示。

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

为 NtQueryInformationProcess()函数的第二个参数 PROCESSINFOCLASS ProcesslnformationClass指定特定值并调用该函数,相关信息就会设置到其第三个参数PVOID Processlnformation。PROCESSINFOCLASS是枚举类型,拥有的值如代码51-5所示。

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
enum PROCESSINFOCLASS
{
ProcessBasicInformation = 0x00,
ProcessQuotaLimits = 0x01,
ProcessIoCounters = 0x02,
ProcessVmCounters = 0x03,
ProcessTimes = 0x04,
ProcessBasePriority = 0x05,
ProcessRaisePriority = 0x06,
ProcessDebugPort = 0x07,
ProcessExceptionPort = 0x08,
ProcessAccessToken = 0x09,
ProcessLdtInformation = 0x0A,
ProcessLdtSize = 0x0B,
ProcessDefaultHardErrorMode = 0x0C,
ProcessIoPortHandlers = 0x0D,
ProcessPooledUsageAndLimits = 0x0E,
ProcessWorkingSetWatch = 0x0F,
ProcessUserModeIOPL = 0x10,
ProcessEnableAlignmentFaultFixup = 0x11,
ProcessPriorityClass = 0x12,
ProcessWx86Information = 0x13,
ProcessHandleCount = 0x14,
ProcessAffinityMask = 0x15,
ProcessPriorityBoost = 0x16,
ProcessDeviceMap = 0x17,
ProcessSessionInformation = 0x18,
ProcessForegroundInformation = 0x19,
ProcessWow64Information = 0x1A,
ProcessImageFileName = 0x1B,
ProcessLUIDDeviceMapsEnabled = 0x1C,
ProcessBreakOnTermination = 0x1D,
ProcessDebugObjectHandle = 0x1E,
ProcessDebugFlags = 0x1F,
ProcessHandleTracing = 0x20,
ProcessIoPriority = 0x21,
ProcessExecuteFlags = 0x22,
ProcessResourceManagement = 0x23,
ProcessCookie = 0x24,
ProcessImageInformation = 0x25,
ProcessCycleTime = 0x26,
ProcessPagePriority = 0x27,
ProcessInstrumentationCallback = 0x28,
ProcessThreadStackAllocation = 0x29,
ProcessWorkingSetWatchEx = 0x2A,
ProcessImageFileNameWin32 = 0x2B,
ProcessImageFileMapping = 0x2C,
ProcessAffinityUpdateMode = 0x2D,
ProcessMemoryAllocationMode = 0x2E,
ProcessGroupInformation = 0x2F,
ProcessTokenVirtualizationEnabled = 0x30,
ProcessConsoleHostProcess = 0x31,
ProcessWindowInformation = 0x32,
ProcessHandleInformation = 0x33,
ProcessMitigationPolicy = 0x34,
ProcessDynamicFunctionTableInformation = 0x35,
ProcessHandleCheckingMode = 0x36,
ProcessKeepAliveCount = 0x37,
ProcessRevokeFileHandles = 0x38,
ProcessWorkingSetControl = 0x39,
ProcessHandleTable = 0x3A,
ProcessCheckStackExtentsMode = 0x3B,
ProcessCommandLineInformation = 0x3C,
ProcessProtectionInformation = 0x3D,
ProcessMemoryExhaustion = 0x3E,
ProcessFaultInformation = 0x3F,
ProcessTelemetryIdInformation = 0x40,
ProcessCommitReleaseInformation = 0x41,
ProcessDefaultCpuSetsInformation = 0x42,
ProcessAllowedCpuSetsInformation = 0x43,
ProcessSubsystemProcess = 0x44,
ProcessJobMemoryInformation = 0x45,
ProcessInPrivate = 0x46,
ProcessRaiseUMExceptionOnInvalidHandleClose = 0x47,
ProcessIumChallengeResponse = 0x48,
ProcessChildProcessInformation = 0x49,
ProcessHighGraphicsPriorityInformation = 0x4A,
ProcessSubsystemInformation = 0x4B,
ProcessEnergyValues = 0x4C,
ProcessActivityThrottleState = 0x4D,
ProcessActivityThrottlePolicy = 0x4E,
ProcessWin32kSyscallFilterInformation = 0x4F,
ProcessDisableSystemAllowedCpuSets = 0x50,
ProcessWakeInformation = 0x51,
ProcessEnergyTrackingState = 0x52,
ProcessManageWritesToExecutableMemory = 0x53,
ProcessCaptureTrustletLiveDump = 0x54,
ProcessTelemetryCoverage = 0x55,
ProcessEnclaveInformation = 0x56,
ProcessEnableReadWriteVmLogging = 0x57,
ProcessUptimeInformation = 0x58,
ProcessImageSection = 0x59,
ProcessDebugAuthInformation = 0x5A,
ProcessSystemResourceManagement = 0x5B,
ProcessSequenceNumber = 0x5C,
ProcessLoaderDetour = 0x5D,
ProcessSecurityDomainInformation = 0x5E,
ProcessCombineSecurityDomainsInformation = 0x5F,
ProcessEnableLogging = 0x60,
ProcessLeapSecondInformation = 0x61,
ProcessFiberShadowStackAllocation = 0x62,
ProcessFreeFiberShadowStackAllocation = 0x63,
MaxProcessInfoClass = 0x64
};

以上代码中与调试器探测有关的成员为ProcessDebugPort(0x7)、ProcessDebugObject-Handle(0x1E)、ProcessDebugFlags(0x1F)。

51.3.1 ProcessDebugPort(0x7)

进程处于调试状态时,系统就会为它分配1个调试端口(Debug Port )。 ProcessInformationClass参数的值设置为ProcessDebugPort(0x7)时,调用NtQueryInformationProcess()函数就能获取调试端口。若进程处于非调试状态,则变量dwDebugPort的值设置为0;若进程处于调试状态,则变量dwDebugPort的值设置为0xFFFFFFFF (参考代码51-6 )。

1
2
3
4
5
6
7
8
9
10
// ProcessDebugPort (0x7)
DWORD dwDebugPort = 0;
pNtQuerylnformationProcess(GetCurrentProcess(),
ProcessDebugPort,
&dwDebugPort,
sizeof(dwDebugPort),
NULL);
printf("NtQueryInformationProcess(ProcessDebugPort) = 0x%X\n",dwDebugPort);
if( dwDebugPort != 0x0 ) printf(" => Debugging!!!\n\n");
else printf(" => Not Debugging...\n\n");

CheckRemoteDebuggerPresent()

CheckRemoteDebuggerPresent() API与IsDebuggerPresent() API类似,用来检测进程是否处于调试状态。CheckRemoteDebuggerPresent()函数不仅可以用来检测当前进程,还可以用来检测其他进程是否处于被调试状态。进入CheckRemoteDebuggerPresent() API查看代码,可以看到其调用了NtQuerylnformationProcess(ProcessDebugPort) API (参见图51-14 )。

51.3.2 ProcessDebugObjectHandle(0x1E)

调试进程时会生成调试对象(Debug Object )。函数的第二个参数值为ProcessDebugObjectHandle(0x1E)时,调用函数后通过第三个参数就能获取调试对象句柄。进程处于调试状态时,调试对象句柄的值就存在;若进程处于非调试状态,则调试对象句柄值为NULL。

1
2
3
4
5
6
7
8
9
10
// ProcessDebugObjectHandle(0x1E)
DWORD hDebugObject = 0;
pNtQuerylnformationProcess(GetCurrentProcess(),
ProcessDebugObjectHandle,
&hDebugObject,
sizeof(dwDebugPort),
NULL);
printf("NtQueryInformationProcess(ProcessDebugObjectHandle) = 0x%X\n",hDebugObject);
if( hDebugObject != 0x0 ) printf(" => Debugging!!!\n\n");
else printf(" => Not Debugging...\n\n");

51.3.3 ProcessDebugFlags(0x1F)

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

1
2
3
4
5
6
7
8
9
10
// ProcessDebugFlags(0x1F)
BOOL bDebugFlag = 0;
pNtQuerylnformationProcess(GetCurrentProcess(),
ProcessDebugFlags,
&bDebugFlag,
sizeof(dwDebugPort),
NULL);
printf("NtQueryInformationProcess(ProcessDebugFlags) = 0x%X\n",bDebugFlag);
if( bDebugFlag == 0x0 ) printf(" => Debugging!!!\n\n");
else printf(" => Not Debugging...\n\n");

51.3.4 练习:StaAD_NtQIP.exe

下面通过StaAD_NtQIP.exe示例程序练习基于NtQueryInformationProcess()函数的反调试。在OllyDbg调试器中运行示例程序后,借助NtQueryInformationProcess()反调试技术显示“探测到调试器”的信息,如图51-15所示。

51.3.5 破解之法

要想破解使用NtQueryInformationProcess() API探测调试器的技术,应当对该函数在特定参数值(ProcessInformationClass )下输出的值(返回-Processlnformation )进行操作(参考代码51-4 )。

特定参数值是前面提过的 ProcessDebugPort ( 0x7 )、ProcessDebugObjectHandle ( 0x1E )、

ProcessDebugFlags ( 0x1F )。

若只是调用几次API,则可以在调试器中手动操作输岀值。相反,若函数被反复调用,则需要使用API钩取技术。在练习中我们将使用OllyDbg的汇编命令手动设置钩取代码。

此处介绍的使用API钩取破解反调试的方法只是为了说明相关概念与原理。实际操作中直接使用相应的调试器插件(如:advanced olly)即可解决问题。每次在插件中启动调试时,都会自动钩取API。

首先重新运行OllyDbg调试器。

确定钩取函数的位置

使用DLL注入技术钩取API时,钩取函数一般位于要注入的DLL文件内部。为了操作方便,我们将钩取代码设置在代码节区中的最后一个NULL Padding区域——407E00地址处,如图51-16所示。

修改原API代码

进入原NtQueryInformationProcess() API代码,如图51 -17所示。

在该处设置一条JMP指令,用来跳转到钩取函数地址处(407E00)。利用OllyDbg的汇编功能将7C93D7EA地址处的代码修改为JMP 00407E00指令,如图51 -18所示。

该JMP指令为5个字节,可以准确覆写原代码中的CALL DWORD PTR DS:[EDX] & RETN 14 指令(位于地址77F06092~77F06097)。

钩取API时,一般要在原API起始地址处设置JMP指令。以上面这种情形为例,JMP指令要设置在77F06088地址处,但是我却将JMP命令设置在略微偏下的地址处(7C93D7EA),这是为了回避某些PE保护器的API钩取探测功能。这些PE保护器会检测NtQueryInformationProcess() API起始地址的第一个字节,若非“B8”,则认为该API被钩取,就会执行某些非正常运行的行为(也算是一种调试器探测技术)。当然,如果采用更精巧的API钩取探测技术,那么上面这种回避方法就会失效,必须采用其他更好的方法。

编写钩取函数

在407E00地址处编写钩取函数,如图51-19所示。

地址407E00处的CALL DWORD PTR DS:[EDX]指令与地址407E3B处的RETN 14指令都是原NtQuerylnformationProcess() API中的代码,钩取代码就设置在这2条指令之间。

407C03地址之后的CMP/JNZ指令组合类似于C语言中的switch/case多分支选择语句。ProcessInformationClass参数(DWORD PTR SS:[ESP+C])值为0x7、0x1E、0x1F之一时,则将Processlnformation参数(DWORD PTR SS:[ESP+10])地址所指的返回值分别修改为0、0、1。在该状态下(在调试器中)运行进程,即可破解基于NtQueryInformationProcess() API的反调试技术,如图51-20所示。

51.4 NtQuerySystemlnformation()

下面介绍基于调试环境检测的反调试技术。

前面介绍的反调试技术中,我们通过探测调试器来判断自己的进程是否处于被调试状态,这是一种非常直接的调试器探测方法。除此之外,还有间接探测调试器的方法,借助该方法可以检测调试环境,若显露出调试器的端倪,则立刻停止执行程序。运用这种反调试技术可以检测当前OS是否在调试模式下运行

为了使用WinDbg工具调试系统内核(Kernel Debugging ),需要先准备2个系统(Host、Target)并连接(Serial、1394、USB、Direct Cable)。其中,Target 的OS 以调试模式运行,连接到Host系统的WinDbg上后即可调试。

设置调试模式的方法

(1) Windows XP:编辑 “C:\boot.ini” 后重启

1
2
3
4
5
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect /debug /debugport=com1 /baudrate=115200

(2) Windows 7:使用 bcdedit.exe 实用程序

1
bcdedit /debug on

ntdll!NtQuerySystemInformation()API是一个系统函数,用来获取当前运行的多种OS信息。

1
2
3
4
5
6
NTSTATUS WINAPI NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);

SYSTEM_INFORMATION_CLASS SystemlnformationClass参数中指定需要的系统信息类型,将某结构体的地址传递给PVOID Systemlnformation参数,API返回时,该结构体中就填充着相关信息。

SYSTEM_INFORMATION_CLASS是枚举类型,拥有的值如代码51-10所示。

1
2
3
4
5
6
7
8
9
10
11
12
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation = 0,
SystemPerformanceInformation = 2,
SystemTimeOfDayInformation = 3,
SystemProcessesAndThreadsInformation = 5,
SystemProcessorTimes = 8,
SystemProcessorStatistics = 23,
SystemExceptionInformation = 33,
SystemKernelDebuggerInformation = 35,
SystemRegistryQuotaInformation = 37,
SystemLookasideInformation = 45
} SYSTEM_INFORMATION_CLASS;

向SystemlnformationClass参数传入SystemKernelDebuggerInformation( 0x23 ),即可判断岀

当前OS是否在调试模式下运行。

51.4.1 SystemKernelDebuggerlnformation(0x23)

查看实际的反调试源代码即可轻松掌握其工作原理。

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
void MyNtQuerySystemInformation()
{
typedef NTSTATUS (WINAPI *NTQUERYSYSTEMINFORMATION)(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);

typedef struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION
{
BOOLEAN DebuggerEnabled;
BOOLEAN DebuggerNotPresent;
} SYSTEM_KERNEL_DEBUGGER_INFORMATION, *PSYSTEM_KERNEL_DEBUGGER_INFORMATION;

NTQUERYSYSTEMINFORMATION NtQuerySystemInformation;

NtQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)
GetProcAddress(GetModuleHandle(L"ntdll"),
"NtQuerySystemInformation");

ULONG SystemKernelDebuggerInformation = 0x23;
ULONG ulReturnedLength = 0;
SYSTEM_KERNEL_DEBUGGER_INFORMATION DebuggerInfo = {0,};

NtQuerySystemInformation(SystemKernelDebuggerInformation,
(PVOID) &DebuggerInfo,
sizeof(DebuggerInfo), // 2 bytes
&ulReturnedLength);

printf("NtQuerySystemInformation(SystemKernelDebuggerInformation) = 0x%X 0x%X\n",
DebuggerInfo.DebuggerEnabled, DebuggerInfo.DebuggerNotPresent);
if( DebuggerInfo.DebuggerEnabled ) printf(" => Debugging!!!\n\n");
else printf(" => Not debugging...\n\n");
}

在上述代码中调用NtQuerySystemInformation() API时,第一个参数(SystemlnformationClass)的值设置为 SystemKernelDebuggerInformation(0x23),第二个参数(Systemlnformation)为SYSTEM_KERNEL_DEBUGGER_INFORMATION结构体的地址。当API返回时,若系统处在调试模式下,则 SYSTEM_KERNEL_DEBUGGER_INFORMATION.DebuggerEnabled的值设置为 1)SYSTEM_KERNEL_DEBUGGER_INFORMATION.DebuggerNotPresent的值恒为1)。

51.4.2 练习:StaAD_NtQSI.exe

运行练习示例StaAD_NtQSI.exe,如图51 -21所示。

我的测试环境启动时默认处于调试模式,所以运行示例程序后显示“探测到调试环境”的信息。

51.4.3破解之法

在Windows XP系统中编辑boot.ini文件,删除 /debugport=com1 /baudrate=115200 /Debug值。在Windows 7系统的命令行窗口执行bcdedit/debug off命令即可。并且,若重启系统则要以正常模式(Normal Mode)启动。

问题:可以Hook吗?

51.5 NtQueryObject()

系统中的某个调试器调试进程时,会创建1个调试对象类型的内核对象。检测该对象是否存在即可判断是否有进程正在被调试。

ntdll!NtQueryObject() API用来获取系统各种内核对象的信息,NtQueryObject()函数的定义如下:

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

调用 NtQueryObject()函数时,先向第二个参数OBJECT_NFORMATION_CLASS ObjectlnformationClass赋予某个特定值,调用API后,包含相关信息的结构体指针就被返回第三

个参数PVOID Objectlnformation。

OBJECT_INFORMATION_CLASS是枚举类型,其拥有的值如代码51-13所示。

1
2
3
4
5
6
7
typedef enum _OBJECT_INFORMATION_CLASS {
ObjectBasicInformation,
ObjectNameInformation,
ObjectTypeInformation,
ObjectAllTypesInformation, // 3
ObjectHandleInformation
} OBJECT_INFORMATION_CLASS, *POBJECT_INFORMATION_CLASS;

首先使用ObjectAllTypesInformation值获取系统所有对象信息,然后从中检测是否存在调试对象。NtQueryObject() API使用方法略为复杂。

NtQueryObject() API使用方法

(1) 获取内核对象信息链表的大小

1
2
ULONG lSize = 0;
pNtQueryObject(NULL, ObjectAllTypesInformation, &lSize, sizeof(lSize), &lSize);

(2) 分配内存

1
2
void *pBuf = NULL;
pBuf = VirtualAlloc(NULL, lSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

(3) 获取内核对象信息链表

1
2
3
4
5
6
7
8
9
10
11
12
13
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;

pNtQueryObject((HANDLE)0xFFFFFFFF, ObjectAllTypesInformation, pBuf, lSize, NULL);
pObjectAllInfo = (POBJECT_ALL_INFORMATION)pBuf;

调用NtQueryObject()函数后,系统所有对象的信息代码就被存入pBuf,然后将pBuf转换(casting)为POBJECT_ALL_INFORMATION类型。OBJECT_ALL_INFORMATION结构体由OBJECT_TYPE_INFORMATION结构体数组构成。实际内核对象类型的信息就被存储在OBJECT_TYPE_INFORMATION结构体数组中,通过循环检索即可查看是否存在“调试对象”对象类型。

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

为便于理解,请先看下面一段代码。

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include "stdio.h"
#include "windows.h"
#include "tchar.h"

typedef enum _OBJECT_INFORMATION_CLASS {
ObjectBasicInformation,
ObjectNameInformation,
ObjectTypeInformation,
ObjectAllTypesInformation,
ObjectHandleInformation
} OBJECT_INFORMATION_CLASS, *POBJECT_INFORMATION_CLASS;

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

int _tmain(int argc, TCHAR* argv[])
{
MyNtQueryObject();

printf("\npress any key to quit...\n");
_gettch();

return 0;
}

练习:StaAD_NtQO.exe

在OllyDbg调试器中运行示例程序StaAD_NtQO.exe,显示“程序处于调试中”的信息,如图51-22所示。这是因为在NtQueryObject()API中探测到了调试对象。

  • 破解之法

按Ctrl+F2键重新运行OllyDbg调试器,在401059地址处按F2键设置断点,然后按F9键运行程序。

位于401059地址处的CALLESI指令用来调用ntdll.ZwQueryObject()API,如图51-23所示。此时查看栈可以发现,第二个参数的值为ObjectAllTypesInformation(3),将该值修改为0后再执行401059地址处的指令,这样就无法探测到调试器的存在了。

当然,直接钩取ntdll.ZwQueryObject() API,输入ObjectAllTypesInformation(3)值或操作结果值,也能不被探测到。

51.6 ZwSetlnformationThread()

下面介绍强制分离(Detach )被调试者和调试器的技术。利用ZwSetInformationThread() API, 被调试者可将自身从调试器中分离出来。

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
typedef enum _THREAD_INFORMATION_CLASS {
ThreadBasicInformation,
ThreadTimes,
ThreadPriority,
ThreadBasePriority,
ThreadAffinityMask,
ThreadImpersonationToken,
ThreadDescriptorTableEntry,
ThreadEnableAlignmentFaultFixup,
ThreadEventPair,
ThreadQuerySetWin32StartAddress,
ThreadZeroTlsCell,
ThreadPerformanceCount,
ThreadAmILastThread,
ThreadIdealProcessor,
ThreadPriorityBoost,
ThreadSetTlsArrayAddress,
ThreadIsIoPending,
ThreadHideFromDebugger // 17(0x11)
} THREAD_INFORMATION_CLASS, *PTHREAD_INFORMATION_CLASS;

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

ZwSetInformationThread()函数是一个系统原生API(System Native API),顾名思义,它是用来为线程设置信息的。该函数拥有2个参数,第一个参数ThreadHandle用来接收当前线程的句柄,第二个参数ThreadlnformationClass表示线程信息类型,若其值设置为ThreadHideFromDebugger(0x11),调用该函数后,调试进程就会被分离出来。ZwSetInformationThread() API不会对正常运行的程序(非调试运行)产生任何影响,但若运行的是调试器程序,调用该API将使调试器终止运行,同时终止自身进程。

51.6.1 练习:StaAD_ZwSIT.exe

首先在OllyDbg调试器中打开示例程序StaAD_ZwSIT.exe,然后分别在401027与401029地址处按F2键设置断点,按F9运行程序。

如图51-24所示,调试器在401027地址的断点处暂停,位于该地址处(401027)的CALL ESI指令用来调用ntdll.ZwSetlnformationThread() API。按F9键继续执行401027地址处的指令,这样就会分离出被调试进程并终止运行。而且,OllyDbg调试器将无法正常调试401029地址处的指令,出现运行错误。

51.6.2破解之法

简单的破解思路是:调用401027地址处的ZwSetInformationThread()API前,查找存储在栈中的第二个参数ThreadlnformationClass值,若其值为ThreadHideFromDebugger(0xl 1),则修改为0后继续运行即可。

当然也可以钩取ZwSetInformationThread()API,并以同样方式操作函数的参数。

利用ZwSetInformationThread()进行反调试的工作原理是:将线程隐藏起来,调试器就接收不到信息,从而无法调试。另外,Windows XP以后新增了 DebugActiveProcessStop() API。

1
2
3
BOOL DebugActiveProcessStop(
[in] DWORD dwProcessId
);

DebugActiveProcessStop() API用来分离调试器和被调试进程,从而停止调试。而前面介绍的ZwSetlnformationThread() API则用来隐藏当前线程,使调试器无法再收到该线程的调试事件,最终停止调试(2个API易混淆,需牢记)。

51.7 TLS回调函数

TLS回调函数是反调试技术中常用的函数,像前面介绍的技术一样,如果不明白其工作原理,使用时就会束手无策。

其实,我们并不能将TLS回调本身看作一种反调试技术,但是由于回调函数会先于EP代码执行,所以反调试技术中经常使用它。在TLS回调函数内部使用IsDebuggerPresent() 等函数判断调试与否,然后再决定是否继续运行程序。

第45章中对反调试相关内容与破解之法做了详细讲解,请各位参考。

51.8 ETC

首先,要明白我们应用反调试技术的目的在于防止程序遭受逆向分析。不必非得为此费力判断自身进程是否处于被调试状态。一个更简单、更好的方法是,判断当前系统是否为逆向分析专用系统(非常规系统),若是,则直接停止程序。这样就出现了各种各样的反调试技术,这些技术都能从系统中轻松获取各种信息(进程、文件、窗口、注册表、主机名、计算机名、用户名、环境变量等)。这些反调试技术通常借助Win32 API获取系统信息来具体实现。下面简单介绍几个例子。

(1) 检测OllyDbg窗口 ← FindWindow()。

(2) 检测OllyDbg进程 ← CreateToolhelp32Snapshot()。

(3) 检查计算机名称是否为 “TEST”、“ANALYSIS” 等 ← GetComputerName()。

(4) 检查程序运行路径中是否存在“TEST”、“SAMPLE”等名称 ← GetCommandLine()。

(5) 检测虚拟机是否处于运行状态(查看虚拟机特有的进程名称—VMWareService.exe、VMWareTray.exe、VMWareUser.exe等)。

上述这些反调试技术的破解之法并不难,所以著名的保护器中并不会使用它们(更棒的反调试技术多得是)。但偶尔有一些不怎么出名的保护器/压缩器会使用,恶意代 码中也经常用到。如果平时不在意这些,那么很有可能会被它们“套住”,白白浪费许多时间。

51.8.1 练习:StaAD_FindWindow.exe

首先启动OllyDbg调试器,然后双击运行StaAD_FindWindow.exe程序,命令行窗口中就会显示“探测到调试器”的信息,如图51-25所示。

StaAD_FindWindow.exe代码中调用了FindWindow()与GetWindowText() API,探测是否存在指定名称(OllyDbg、IDA Pro、WinDbg等)的调试器窗口。

51.8.2破解之法

首先在OllyDbg调试器中打开练习文件,然后在401023地址处设置好断点并运行程序,如图51-26所示。

图51-26的代码中共有3处调用FindWindow() API。40101E地址处的PUSH409D10指令中,地 址409D10指向Window Class名称字符串,它是FindWindow() API的第一个参数。转到409D10地址处,使用NULL覆盖Window Class名称字符串缓冲区,那么FindWindow() API将无法探测到相应调试器。

接下来要使GetWindowText() API失效。在401093地址处设置好断点并运行程序,如图51-27所示。

调用GetWindowTextW() API的代码在4010B4地址处。若想正常调用GetWindowTextW() API,就不能执行4010AD地址处的条件跳转指令。要实现这一点,可以直接操作条件跳转语句,也可 以将其上GetDesktopWindow()与GetWindow() API的返回值(EAX寄存器)修改为NULL值。当然,钩取FindWindow() API与GetWindowText() API也是非常棒的方法。

51.9小结

本章讲解了静态反调试的方法。其实,除了本章介绍的方法外,还有很多其他方法,而且调试过程中还会遇到更多,这些反调试方法你可能之前从未见过,只要认真分析、查找相关资料,一般都能找到好的破解之道,这是积累经验、不断进步的必经之路。本章还说明了静态反调试技术的破解之法,这些方法虽然不太难,但若完全不了解,调试时可能遭受很大困难。

反调试技术对OS有很强的依赖性,所以应用某个反调试技术时要事先确认:它是否可以应用到目标操作系统。实际调试中会使用多种调试器插件,借助这些插件可以有效回避反调试技术,使用起来非常方便。但调试器的插件也不是万能的,它们无法破解某些反调试技术。此时,了解这些插件的工作原理、学习基本的破解之法就显得非常有用了。


第51章 静态反调试技术
https://m0ck1ng-b1rd.github.io/1999/05/01/逆向工程核心原理/第51章 静态反调试技术/
作者
何语灵
发布于
1999年5月1日
许可协议