PatchGuard 工作原理

前言

之前找资料的时候发现国内关于这块基本是空白的,参阅多篇论文后有的此文

PatchGuard 介绍

PatchGuard 在 64 位 Windows 操作系统中的安全措施,它阻止了内核级 rootkit 和其他恶意软件操纵关键系统代码和结构。 它的运作方式是通过定期监控内核来识别任何非法修改并立即阻止。 PatchGuard 的目的是维护操作系统的完整性并确保其以最佳水平运行,从而提高系统的整体安全性和稳定性。 PatchGuard 验证内核代码和数据结构并强制执行数字签名。 在 PatchGuard 的限制范围内,开发者必须确保合规性和安全设计,以便与内核无缝运行。

初始化

KiFilterFiberContext

PatchGuard 的初始化过程主要由 KiFilterFiberContext 函数执行,该函数初始化 PatchGuard 的上下文和验证机制。 KiFilterFiberContext 在启动过程中被调用两次,其中一种方法涉及称为 KiAmd64SpecificState 的异常处理程序。 为了触发异常处理程序并执行 KiFilterFiberContext,在启动过程开始时会故意触发,除法错误。 用于计算除法的两个值是已知符号,KdDebuggerNotPresent 和 KdPitchDebugger ,用于确定是否附加了调试器。 如果存在调试器,则不会初始化 PatchGuard。 在正常情况下,这两个符号的值设置为 1,这会导致 idiv 指令计算值 rax=0x80000000、rdx=0x80000000 和 r8d=0xffffffff。 此除法的计算导致除法错误,从而触发 KiDivideErrorFault 函数执行 KiFilterFiberContext 函数。 KiFilterFiberContext 负责调用创建 PatchGuard 上下文的初始化过程,并使用特定参数。值得注意的是,其中一个参数被硬编码为 0,这表明它可能从其他地方调用。但实际上,另一个初始化过程已经开始,它指向函数。

ExpLicenseWatchInitWorker

在启动过程中在 KeInitAmd64SpecificState 之前调用。 它是 Microsoft PatchGuard 的一部分,用于验证 Microsoft 许可证的真实性。 调用堆栈显示 ExInitSystemPhase2 调用与许可证验证相关的ExpGetNtProductTypeFromLicenseValue
ExpLicenseWatchInitWorker 随后调用 KiFilterFiberContext,但概率只有 4%,使用 rdtsc 指令生成的随机值。
ExpLicenseWatchInitWorker 包括一些检查,用于检查是否存在调试器和安全启动模式,这些都是常见的安全措施。
该函数的返回值是 rdtsc 指令生成的随机值乘以常数值 0x51eb851f。如果 InitIsWinPEMode 为真,则随机返回的值稍后将用作索引。
KiFilterFiberContext 的调用使用从 PRCB(进程寄存器控制块)获取的值构建的结构,特别是 HalReserved 字段,以及指向 KiFilterFiberContext 的指针,然后立即清除这些字段。
ExpLicenseWatchInitWorker 分别将 KiFilterParampKiFilterFiberContext 指针设置为 Prcb.HalReserved[6]Prcb.HalReserved[5]
如果 InitSafeBootMode 不为 0 或 KUSER_SHARED_DATA.KdDebuggerEnabled >> 1,则返回 rand_stuff。否则,如果 random(0,100) ≤ 4,则调用 KiFilterFiberContext(pKiFilterFiberParam)。
KiFilterParam 和 pKiFilterFiberContext 这两个指针在启动开始时在函数 KiLockServiceTable 中设置。
KiLockServiceTable 填充 HalReserved 字段,第一个要填充的 HalReserved 字段是第 6 个。 KiServiceTablesLocked 持有的结构名为 KI_FILTER_FIBER_PARAM,是 KiFilterFiberContext 函数的一个参数。
KI_FILTER_FIBER_PARAM 结构包括 code_prefetch_rcx_retn、padding、pPsCreateSystemThread 和 pKiBalanceSetManagerPeriodicDpc。

PatchGuard Context
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
struct PatchGuardContext
{
constexpr auto CmpAppendDllSection[];
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto nt!ExAcquireResourceSharedLite
constexpr auto nt!ExAcquireResourceExclusiveLite
constexpr auto nt!ExAllocatePoolWithTag
constexpr auto nt!ExFreePool
constexpr auto nt!ExMapHandleToPointer
constexpr auto nt!ExQueueWorkItem
constexpr auto nt!ExReleaseResourceLite
constexpr auto nt!ExUnlockHandleTableEntry
constexpr auto nt!ExAcquirePushLockExclusiveEx
constexpr auto nt!ExReleasePushLockExclusiveEx
constexpr auto nt!ExAcquirePushLockSharedEx
constexpr auto nt!ExReleasePushLockSharedEx
constexpr auto nt!KeAcquireInStackQueuedSpinLockAtDpcLevel
constexpr auto nt!ExAcquireSpinLockSharedAtDpcLevel
constexpr auto nt!KeBugCheckEx
constexpr auto nt!KeDelayExecutionThread
constexpr auto nt!KeEnterCriticalRegionThread
constexpr auto nt!KeLeaveCriticalRegion
constexpr auto nt!KeEnterGuardedRegion
constexpr auto nt!KeLeaveGuardedRegion
constexpr auto nt!KxReleaseQueuedSpinLock
constexpr auto nt!ExReleaseSpinLockSharedFromDpcLevel
constexpr auto nt!KeRevertToUserGroupAffinityThread
constexpr auto nt!KeProcessorGroupAffinity
constexpr auto nt!KeInitializeEnumerationContext
constexpr auto nt!KeEnumerateNextProcessor
constexpr auto nt!KeCountSetBitsAffinityEx
constexpr auto nt!KeQueryAffinityProcess
constexpr auto nt!KeQueryAffinityThread
constexpr auto nt!KeSetSystemGroupAffinityThread
constexpr auto nt!KeSetCoalescableTimer
constexpr auto nt!ObfDereferenceObject
constexpr auto nt!ObReferenceObjectByName
constexpr auto nt!RtlImageDirectoryEntryToData
constexpr auto nt!RtlImageNtHeader
constexpr auto nt!RtlLookupFunctionTable
constexpr auto nt!RtlPcToFileHeader
constexpr auto nt!RtlSectionTableFromVirtualAddress
constexpr auto nt!DbgPrint
constexpr auto nt!MmAllocateIndependentPages
constexpr auto nt!MmFreeIndependentPages
constexpr auto nt!MmSetPageProtection
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto nt!RtlLookupFunctionEntry
constexpr auto nt!KeAcquireSpinLockRaiseToDpc
constexpr auto nt!KeReleaseSpinLock
constexpr auto nt!MmGetSessionById
constexpr auto nt!MmGetNextSession
constexpr auto nt!MmQuitNextSession
constexpr auto nt!MmAttachSession
constexpr auto nt!MmDetachSession
constexpr auto nt!MmGetSessionIdEx
constexpr auto nt!MmIsSessionAddress
constexpr auto nt!MmIsAddressValid
constexpr auto nt!MmSessionGetWin32Callouts
constexpr auto nt!KeInsertQueueApc
constexpr auto nt!KeWaitForSingleObject
constexpr auto unknown;
constexpr auto nt!ExReferenceCallBackBlock
constexpr auto nt!ExGetCallBackBlockRoutine
constexpr auto nt!ExDereferenceCallBackBlock
constexpr auto nt!KiMarkBugCheckRegions+0x3c0
constexpr auto nt!PspEnumerateCallback
constexpr auto nt!CmpEnumerateCallback
constexpr auto nt!DbgEnumerateCallback
constexpr auto nt!ExpEnumerateCallback
constexpr auto nt!ExpGetNextCallback
constexpr auto nt!EmpCheckErrataList
constexpr auto nt!KiSchedulerApcTerminate
constexpr auto nt!KiSchedulerApc
constexpr auto nt!EmpCheckErrataList
constexpr auto nt!KiSwInterruptDispatch+0xfd0
constexpr auto nt!MmAllocatePagesForMdlEx
constexpr auto nt!MmAllocateMappingAddress
constexpr auto nt!MmMapLockedPagesWithReservedMapping
constexpr auto nt!MmUnmapReservedMapping
constexpr auto nt!KiSwInterruptDispatch+0xd220
constexpr auto nt!KiSwInterruptDispatch+0xd290
constexpr auto nt!MmAcquireLoadLock
constexpr auto nt!MmReleaseLoadLock
constexpr auto nt!KeEnumerateQueueApc
constexpr auto nt!KeIsApcRunningThread
constexpr auto nt!KiSwInterruptDispatch+0xeb0
constexpr auto nt!PsAcquireProcessExitSynchronization
constexpr auto nt!ObDereferenceProcessHandleTable
constexpr auto nt!PsGetNextProcess
constexpr auto nt!PsQuitNextProcess
constexpr auto nt!PsGetNextProcessEx
constexpr auto nt!MmIsSessionLeaderProcess
constexpr auto nt!PsInvokeWin32Callout
constexpr auto nt!MmEnumerateAddressSpaceAndReferenceImages
constexpr auto nt!PsGetProcessProtection
constexpr auto nt!PsGetProcessSignatureLevel
constexpr auto nt!PsGetProcessSectionBaseAddress
constexpr auto nt!SeCompareSigningLevels
constexpr auto nt!KeComputeSha256
constexpr auto nt!KeComputeParallelSha256
constexpr auto nt!KeSetEvent
constexpr auto nt!RtlpConvertFunctionEntry
constexpr auto nt!RtlpLookupPrimaryFunctionEntry
constexpr auto nt!RtlIsMultiSessionSku
constexpr auto nt!KiEnumerateCallback
constexpr auto nt!KeStackAttachProcess
constexpr auto nt!KeUnstackDetachProcess
constexpr auto nt!KeIpiGenericCall
constexpr auto nt!KiSwInterruptDispatch+0xd070
constexpr auto nt!MmGetPhysicalAddress
constexpr auto nt!MmUnlockPages
constexpr auto nt!VslVerifyPage
constexpr auto nt!KiGetInterruptObjectAddress
constexpr auto unknown;
constexpr auto nt!PsLookupProcessByProcessId
constexpr auto nt!PsGetProcessId
constexpr auto nt!MmCheckProcessShadow
constexpr auto nt!MmGetImageRetpolineCodePage
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto nt!matherr_flag+0x8
constexpr auto nt!TriageImagePageSize+0xa4
constexpr auto nt!TriageImagePageSize+0xac
constexpr auto nt!TriageImagePageSize+0xb4
constexpr auto unknown;
constexpr auto unknown;
constexpr auto nt!KiEntropyTimingRoutine
constexpr auto nt!KiProcessListHead
constexpr auto nt!KiProcessListLock
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto nt!PsActiveProcessHead
constexpr auto nt!PsInvertedFunctionTable
constexpr auto nt!PsLoadedModuleList
constexpr auto nt!PsLoadedModuleResource
constexpr auto nt!PsLoadedModuleSpinLock
constexpr auto nt!PspActiveProcessLock
constexpr auto nt!PspCidTable
constexpr auto nt!ExpUuidLock
constexpr auto nt!AlpcpPortListLock
constexpr auto nt!KeServiceDescriptorTable
constexpr auto nt!KeServiceDescriptorTableShadow
constexpr auto nt!KeServiceDescriptorTableFilter
constexpr auto nt!VfThunksExtended
constexpr auto nt!PsWin32CallBack
constexpr auto nt!TriageImagePageSize+0x84
constexpr auto nt!KiTableInformation
constexpr auto nt!HandleTableListHead
constexpr auto nt!HandleTableListLock
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto nt!SeProtectedMapping
constexpr auto unknown;
constexpr auto nt!KiStackProtectNotifyEvent
constexpr auto unknown;
constexpr auto nt!MmFreeIndependentPages (nt+0x0)
constexpr auto hal!EmonQueryInformation (hal+0x0)
constexpr auto nt!KeNumberProcessors
constexpr auto unknown;
constexpr auto unknown;
constexpr auto nt!RtlpInvertedFunctionTable
constexpr auto nt!KiIsrThunkShadow
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
constexpr auto unknown;
// INCOMPL:FOR REF
};

PatchGuard 上下文结构分为三个部分。 第一部分包含 PatchGuard 机制的核心内容。第二部分是数据接收者,用于保存原始数据,而第三部分则包含有关要检查的数据的信息。
第一部分 结构的第一部分包括函数 CmpAppendDllSection 的代码,该代码直接复制到结构中,并在 PatchGuard 触发完整性检查时使用。
此代码的主要功能是使用随机生成的密钥解密 PatchGuard 上下文结构的其余部分。该部分还包含来自 ntoskrnl API 的许多函数指针,这些指针以这种方式保存,以便 PatchGuard 例程可以独立于重定位去使用它们。

第一部分

结构的第一部分包含函数 CmpAppendDllSection 的代码,该代码直接复制到结构中,并在 PatchGuard 触发完整性检查时使用。此代码的主要功能是使用随机生成的密钥解密 PatchGuard 上下文结构的其余部分。该部分还包含来自 ntoskrnl API 的许多函数指针,这些指针以这种方式保存,以便 PatchGuard 例程可以独立于重定位使用它们。

结构的第二部分包含许多对全局变量的引用,例如 KiWaitAlways 和 KiWaitNever。这些值在启动时随机初始化,用于编码和解码 PatchGuard DPC 指针。此外,它还包含指向另一个 PatchGuard 上下文结构的指针,该指针多次用作结构的干净备份。它也是此全局指向的结构,在发生 KeBugCheck 时发送,如 KiMarkBugCheckRegion 中所示。

最后,该结构的第三部分包括公共变量,例如 Ntoskrnl 和 Hal 基地址、当前 PRCB 和最大虚拟寻址大小。它还包括运行时变量,例如为“检查会话”检查的数据总量。关键结构校验和所需的数据在每次校验和之后都会增加其大小,并与最大值进行比较。此外,使用 rdtsc 随机初始化用于关键结构校验和的初始化向量以及用于在每个块迭代中导出初始化向量的移位值。最后,PatchGuard 上下文的校验和存储在其自身中,用于检测任何损坏。

第二部分

该结构的第二部分包含稍后将使用的数据。在 Windows 中,条目保存在结构的这一部分中以防止绕过。这些条目包括 PTE(在触发 KeBugCheck 之前恢复)以及关键内核例程,
例如 Hal、Ntoskrnl 和 RtlpBreakWithStatusInstruction/DbgBreakPointWithStatus 等。每个例程都由其在结构中的相应偏移量标识,并附带其大小,以便恢复例程知道要重写多少。

第三部分

PatchGuard 上下文结构的第三部分包含一个结构数组,其中包含需要执行的每个检查的信息。每个结构都有一个 KeBugCheckType 字段,用于区分正在检查的结构类型,例如 IDT 或 GDT。该结构还包含指向要检查的数据的指针、其大小以及在 PatchGuard 初始化期间计算的校验和,作为完整性检查的参考。此外,结构中还有针对每种检查类型的特定条目,例如 IDT 检查的目标处理器。结构数组存储在 PatchGuard 上下文结构中,结构第一部分中的几个条目提供了重要信息。这些条目包括数组中关键结构的总数、要进行校验和的下一个关键结构的偏移量、第一个关键结构数据的偏移量以及当前已检查的结构数量。PatchGuard 在其检查算法中使用这些信息。

初始化阶段

上下文的初始化主要通过名为 KiInitPatchGuardContext 的函数进行,该函数虽然未命名,但在现有文献中已有记载。但是,还有其他初始化 PatchGuard 上下文的方法,并且在某些情况下,会为系统检查目的建立单独的机制。

不同的初始化方法

如前所述,KiInitPatchGuardContext 函数负责初始化大多数 PatchGuard 上下文,方法的选择由函数参数决定。这些参数通常是随机选择的,如 KiFilterFiberContext 概述中所述。在本节中,我们将研究传递给此函数的参数以及它们如何确定 PatchGuard 检查的初始化和触发。

函数参数如下:

  • Arg 1:DPC 方法的索引
  • Arg 2:调度方法
  • Arg 3:用于确定要检查的最大大小的随机值
  • Arg 4:指向来自 ExpLicenseWatchInitWorker 的结构的指针(可能性低)
  • Arg 5:布尔值,用于决定是否必须检查 NT 例程的完整性

在这些参数中,第 2 个参数(调度方法)和第 4 个参数(允许使用其他调度方法)是最重要的。在 KiFilterFiberContext 中,随机值用作第二个参数的索引,该索引决定要使用的方法。在下一节中,我们将描述 KiInitPatchGuardContext 可以与第 4 个参数结合使用的各种方法。

已知方法

KiInitPatchGuardContext 可以使用不同的方法来初始化 PatchGuard 检查。

第一种方法涉及插入与 DPC 链接的计时器。 PatchGuard 初始化一个 PatchGuard Context 结构和一个 DPC 结构并将其设置在计时器结构中。计时器与 KeSetCoalescableTimer 一起排队,并将在调用后 2 到 2’10” 之间从第一个参数触发 DPC。TolerableDelay 参数是 0 到 0.001 秒之间的随机值。此计时器不是周期性的,需要在检查例程结束时恢复。

第二种和第三种方法涉及将 DPC 隐藏在内核结构 PRCB 中,而不是使用计时器。如果 KiInitPatchGuardContext 的第二个参数为 1 或 2,PatchGuard 将初始化上下文结构和 DPC 结构并将其隐藏在 PRCB 中。系统中的合法函数负责排队 DPC。

对于方法 1,指向 DPC 的指针隐藏在 PRCB 的 AcpiReserved 字段中。它在 HalpTimerDpcRoutine 中排队,并检查每次检查之间是否至少经过了两分钟。全局变量 HalpTimerLastDpc 跟踪上次排队的时间,其值取自全局变量,与正常运行时间有关。当发生某个 ACPI 事件(例如转换为空闲状态)时,将调用 HalpTimerDpcRoutine。

对于方法 2,指向 DPC 的指针隐藏在 PRCB 的 HalReserved 字段中。它由 HalpMcaQueueDpc 排队,最短周期也是 2 分钟,并在发生 HAL 定时器时钟中断时进行检查,
请参阅 HalpTimerClockInterrupt/HalpTimerAlwaysOnClockInterrupt。
有趣的是当从 ExpLicenseWatchInitWorker 调用 KiFilterFiberContext 时,此字段还用于保留指向结构 KI_FILTER_FIBER_PARAM 的指针。

对于方法 3,它涉及使用指向 KI_FILTER_FIBER_PARAM 结构的指针创建一个新的系统线程,这种情况发生的概率仅为 4%。 KI_FILTER_FIBER_PARAM 结构包含指向 PsCreateSystemThread 函数的指针,该函数用于创建新的系统线程。新创建的线程的 StartAddress 设置为指向 PatchGuard 验证例程的存根函数。此线程创建直接在 KiInitPatchGuardContext 函数中启动。使用了一种有趣的混淆技术,PatchGuard 在线程创建后立即将相应 ETHREAD 结构的 StartAddress 和 Win32StartAddress 字段修改为通用函数指针,以避免被检测到。为此,PatchGuard 获取 0 到 7 之间的随机值,并从内存中特定偏移量的函数名称数组中获取函数指针。在数组中的七个可能函数中,只有最后一个是设置 StartAddress 和 Win32StartAddress 字段的正确函数。

对于方法 4,初始化 PatchGuard Context 结构和 APC 结构,然后将其插入现有系统线程,并将 NormalRoutine 参数设置为 xHalTimerWatchdogStop,这只是一个“ret 0”指令。将 KernelRoutine 设置为 KiDispatchCallout,它将调用验证例程,RundownRoutine 为 NULL。使用带有回调的 PsEnumProcessThreads 选择要附加的系统线程,该回调会查询线程起始地址并将结果与​​ PopIrpWorkerControl 进行比较。如果找到匹配的线程,则

对于方法 5,需要有效的 KI_FILTER_FIBER_PARAM 结构,否则 KiInitPatchGuardContext 将回退到方法 0。结构的最后一项将被使用,它是指向全局变量 KiBalanceSetManagerPeriodicDpc 的指针。此变量包含 KDPC 结构,其 DPC 例程在函数 KiInitSystem 中初始化。此方法涉及挂接系统每秒左右由 KeClockInterruptNotify 排队的合法 DPC。PatchGuard 挂接此合法 DPC,以便每 120 个队列(或 120 到 130 之间的随机值),PatchGuard DPC 都会排队。如果 PatchGuard DPC 排队,它首先会清除全局 DPC 的副本,并让验证例程在检查结束时将其重新设置。

实际上还有更多方法,但本文只介绍到此。

全局上下文初始化

当使用索引 7 调用 KiInitPatchGuardContext 时,将初始化全局 PatchGuard 上下文结构,并可通过全局指针访问。虽然某些机制(例如校验和)是使用与 SHA256 相关的算法而不是通常的算法执行的,但我们没有专门分析这些机制,因为想法保持不变。值得注意的是,使用索引 7 调用 KiInitPatchGuardContext 始终会发生,并且与 Windows 8.1 相比,其他新方法会使用全局 PatchGuard 上下文。这结束了对 PatchGuard 可用于初始化上下文的各种方法的描述。此外,还可以描述提供给 KiInitPatchGuardContext 的其他参数。

继续讨论 KiInitPatchGuardContext 函数的其他参数,我们已经讨论了第二和第四个参数的重要性。现在,让我们看看第一个参数,它是指向 DPC 例程的指针。由于多种 PatchGuard 方法使用 DPC 结构来隐藏 PatchGuard 并在某个点将其排队,因此必须注意,验证例程并非直接设置为 DPC 的例程。相反,DPC 将包含一个指向已知函数的指针,该函数会在 DPC 为 PatchGuard 时取消 DPC 的排队并执行特定操作。

第一个参数随机选择一个索引来选择一个例程,该例程将被设置为以下函数之一:

  1. CmpEnableLazyFlushDpcRoutine
  2. ExpCenturyDpcRoutine
  3. ExpTimeZoneDpcRoutine
  4. ExpTimeRefreshDpcRoutine
  5. CmpLazyFlushDpcRoutine
  6. ExpTimerDpcRoutine
  7. IopTimerDispatch
  8. IopIrpStackProfilerDpcRoutine
  9. KiBalanceSetManagerDeferredRoutine
  10. PopThermalZoneDpc
  11. KiTimerDispatch/KiDpcDispatch
  12. KiTimerDispatch/KiDpcDispatch
  13. KiTimerDispatch/KiDpcDispatch

对于后三个例程 KiTimerDispatch 和 KiDpcDispatch,两者之间的选择取决于第二个参数是否小于 3。如果小于 3,则使用 KiTimerDispatch;否则,使用 KiDpcDispatch。

在 KiFilterFiberContext 函数的先前伪代码中,第一个参数是随机选择的,除了最后一次调用 KiInitPatchGuardContext 时,它被设置为 0(CmpEnableLazyFlushDpcRoutine)。但是,在这种情况下,它不用于初始化例程。

KiInitPatchGuardContext 的第三个参数是一个随机值,用于确定要检查的数据的总大小。该值除以硬编码值 0x140000,结果值立即设置到偏移量 0x6cc 处的 PatchGuard 上下文结构中。每次 PatchGuard 检查时要进行校验和的数据的最大大小(以字节为单位)由该值决定。PatchGuard 维护一个要检查完整性的结构列表,每次校验和之后,计数器都会增加数据的大小。当检查的数据总量小于先前定义的最大值时,PatchGuard 将继续检查其列表中的下一个结构。

KiInitPatchGuardContext 的第五个参数是一个布尔值,用于确定是否应执行 ntoskrnl 函数的校验和。此检查完成后,结果将存储在 PatchGuard 上下文中,以及 PatchGuard 检查的其他 Windows 内核结构中。在 KiFilterFiberParam 中,仅在第一次调用 KiInitPatchGuardContext 时,此参数才设置为 True。

以上就是可能来自 KiInitPatchGuardContext 的初始化方法的描述。其他方法直接初始化,或者根本不使用任何上下文结构。

PatchGuardTVCallback 又名 542875F90F9B47F497B64BA219CACF69 回调

KiFilterFiberContext 函数包含一个小的回调函数,该函数在 ntoskrnl 中找不到,并以名为 PatchGuardTVCallback 的函数指针作为参数。 此指针在二进制文件 mssecflt.sys 中的函数 SecInitializeKernelIntegrityCheck 中初始化。此函数直接从 mssecflt.sys 的驱动程序入口例程调用,该例程在启动过程中调用。 回调函数 SecKernelIntegrityCallback 只是将函数指针分配给全局变量,并将另一个全局变量设置为 SecProtectedRanges。 PatchGuardTVCallback 函数是 PatchGuard 检查例程之一,类似于 FsRtlMdlReadCompleteDevEx 函数,但调度方法不同。 此方法没有其他特定的初始化,因为它使用全局 PatchGuard 上下文结构。
如果想了解有关此回调的更多信息,请查看 542875F90F9B47F497B64BA219CACF69

附加检查 PspProcessDelete

可以在特定函数(如 PspProcessDelete)中找到附加完整性检查,在删除进程期间对 KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow 执行完整性检查。 此检查不需要任何 PatchGuard 上下文结构或专用线程,只是系统代码中存在的一小段验证。两个表的原始校验和以及计算校验和所需的初始化向量和移位值都保存在全局变量中,这使得攻击者可以修补描述符表(无论是否为 Shadow)的条目并替换原始校验和。
使用 KiQueryUnbiaisedInterruptTime 生成的随机值计算校验和。如果这些校验和中的任何一个失败,则通过插入 KiSchedulerDpc 的 DPC 触发 KeBugCheck。这些校验和的初始化在 CmpInitDelayRefKCBEngine 中执行。
要禁用此方法,可以将计时器修补为无限长或再次计算受 PatchGuard 保护的修改表的校验和。

KiInitializeUserApc

与 PspProcessDelete 类似,还有另一个函数,其中包含对中断描述符表 (IDT) 的独立完整性验证。
如果检测到修改,则使用 KiSchedulerDpc 注入 DPC,然后触发 KeBugCheck。要禁用此方法,可以将计时器设置为无穷大。

CcInitializeBcbProfiler

PatchGuard 使用隐藏方法通过 CcInitializeBcbProfiler 函数执行检查。此函数首先计算 ntoskrnl 中随机例程的校验和。 然后,它使用 CcBcbProfiler 例程和一些附加数据设置 DPC。作为参数传递的结构包含计算随机例程校验和所需的所有内容,包括函数入口指针、图像的基址、函数大小、校验和以及用作校验和种子的随机值。
DPC 使用 KeSetCoalescableTimer 排队,并将 DueTime 设置在 2’ 到 2’10” 之间。CcBcbProfiler 例程要么将参数中的工作项作为 WorkerRoutine 排队,要么继续执行。
CcBcbProfiler 例程的主要目标是执行随机 ntoskrnl 函数的完整性检查,并将结果与​​存储在结构中的结果进行比较。之后,这两个函数都使用 KeSetCoalescableTimer 再次设置计时器。

CcInitializeBcbProfiler

PatchGuard 使用隐藏方法通过 CcInitializeBcbProfiler 函数执行检查。此函数首先计算 ntoskrnl 中随机例程的校验和。然后,它使用 CcBcbProfiler 例程和一些附加数据设置 DPC。 作为参数传递的结构包含计算随机例程校验和所需的所有内容,包括函数入口指针、图像的基址、函数大小、校验和以及用作校验和种子的随机值。 DPC 使用 KeSetCoalescableTimer 排队,并将 DueTime 设置在 2’ 到 2’10” 之间。CcBcbProfiler 例程要么将参数中的工作项作为 WorkerRoutine 排队,要么继续执行。CcBcbProfiler 例程的主要目标是执行随机 ntoskrnl 函数的完整性检查,并将结果与​​存储在结构中的结果进行比较。 之后,这两个函数都使用 KeSetCoalescableTimer 再次设置计时器。

触发上下文

本节重点介绍如何激活这些上下文结构,具体方法可能因使用的方法而异。

通过 DPC 执行

PatchGuard 用于启动检查的一种常见方法是通过 DPC。触发检查的 DeferredRoutine 函数是从十个函数池中选择出来的。编号为 0 到 9 的函数使用异常处理程序来激活检查(前面提到的随机函数),而 KiTimerDispatch 和 KiDpcDispatch 则直接调用 DPC,而不使用此异常技巧。此外,方法 5 始终使用 KiBalanceSetManagerDeferredRoutine 函数。调用 PatchGuard DPC 函数之一时,第一步是确定 DPC 是 PatchGuard DPC 还是普通 DPC。为此,该函数将 DPC 结构指针作为参数,用于检查 DPC 是否来自 PatchGuard。检查是根据参数 KDPC.DeferredContext 执行的,通过验证它是否具有规范地址。一个简单的代码片段用于检查 DeferredContext 是否具有规范地址。如果 DeferredContext 参数具有非规范地址,则该函数调用 KiCustomAccessRoutineN(N 根据调用的函数而变化)并执行所谓的“俄罗斯轮盘赌博技巧”。

触发异常处理程序

在检查 DeferredContext 参数的规范地址后,如果发现它是非规范的,PatchGuard 会调用函数 KiCustomAccessRoutineX。然后,此函数使用两个参数调用 KiCustomRecurseRoutineX:一个计数器和非规范 DeferredContext。计数器来自 DeferredContext 参数的最后两位加一。KiCustomRecurseRoutineX 函数由 10 个循环函数组成,这些函数会减少计数器并调用下一个函数,直到计数器达到零。该机制如图所示。其目的是不断减少计数器,直到无效指针被取消引用。根据原始函数,使用 try/except/finally 处理程序的组合来解密 PatchGuard 上下文结构。该机制类似于玩俄罗斯轮盘赌,玩家不断扣动枪的扳机,直到枪开火。

解密上下文

解密 PatchGuard 上下文结构第一层的责任在于异常处理程序。解密过程涉及两层和一个小技巧。第一层解密整个结构,然后其中的“一半”用硬编码值覆盖标头。第二层涉及自修改代码,该代码解密其余结构。

解密第一层

解密的初始步骤针对整个 PatchGuard 上下文结构。有各种代码可用于完成此任务,总结如下:
CmpEnableLazyFlushDpcRoutine | 索引:0 | 方法 1
ExpCenturyDpcRoutine | 索引:1 | 方法 1
ExpTimeZoneDpcRoutine | 索引:2 | 方法 1
ExpTimeRefreshDpcRoutine | 索引:3 | 方法 2
CmpEnableLazyFlushDpcRoutine | 索引:4 | 方法 1
ExpTimerDpcRoutine | 索引:5 |方法 2
IopTimerDispatch | 索引:6 | 方法 2
IopIrpStackProfilerDpcRoutine | 索引:7 | 方法 1
KiBalanceSetManagerDeferredRoutine | 索引:8 | 方法 1
PopThermalZoneDpc | 索引:9 | 方法 2
KiTimerDispatch | 索引:10-12 | 使用全局变量的方法 1
KiDpcDispatch | 索引:10-12 | 未使用

PatchGuard 中使用的加密和解密例程依赖于存储在名为 KiWaitNever 和 KiWaitAlways 的全局变量中的随机值。这些变量在启动时保存随机生成的值,并由 KiInitPatchGuardContext 用来加密 PatchGuard 上下文结构。这意味着试图访问该结构的攻击者必须知道这些全局变量的位置,这需要访问 ntoskrnl 版本和相应的符号信息。

解密第一层和“一半”

在应用第二层解密之前,PatchGuard 会用硬编码值修改 PatchGuard 结构的前四个字节,这些硬编码值表示用于通过第三层解密(CmdAppendDllSection)解密上下文的代码。这种重写使用不同的方法,例如逐个重写每个字节或使用两个硬编码值的 XOR。目前不清楚为什么这样做,但这可能是作为即时代码优化引入的。另一种可能性是这样做是为了防止某些值在代码中被轻易搜索到,但这似乎并不是一个难以克服的障碍。

解密第二层和最后一层

第二层和最后一层解密涉及第二层的代码存在于 PatchGuard 上下文结构的第一部分中。在调用上一层解密后直接调用此代码。第二层解密以多个用于解密自身的 XOR 指令开始。解密过程可以分为两部分,第一部分是改写自己的指令,解密下一条指令,第二部分是解密循环,解密整个上下文结构。

验证例程

解密 PatchGuard 上下文结构后,将依次调用两个函数。第一个函数有两个主要目的。首先,它验证 PatchGuard 上下文结构和与 PatchGuard 相关的 47 个关键例程或部分例程的完整性。这是通过检查这些例程的代码并将其与预期值进行比较来完成的。如果检测到任何修改,PatchGuard 将触发 KeBugCheck 进程。其次,该函数初始化 WORK_QUEUE_ITEM 结构并选择一个存根以将验证例程作为 WorkItem 调用。存根可以是 KiMachineCheckControl 数组中的随机存根,即 PatchGuard 上下文结构中 FsRtlUninitializeSmallMcb 的副本。然后将 WORK_QUEUE_ITEM 结构作为参数传递给 ExQueueWorkItem,一旦 Worker 线程处理新项目,它就会启动验证例程。对于 DPC 方法,此机制用于将控制权传递给验证例程。

通过系统线程

PatchGuard 使用的第三种方法涉及创建系统线程,此函数在 KiInitPatchGuardContext 中调用。

触发异常处理程序

该错误是由取消引用被认为是“随机”的寄存器引起的。

新线程创建

此方法涉及创建新的系统线程。使用 KI_FILTER_FIBER_PARAM 结构中包含的指向 PsCreateSystemThread 的指针创建该线程。传递给 PsCreateSystemThread 的 StartContext 参数是指向名为 PG_StartContext 的新结构的指针,该结构包含指向同一结构中的事件的指针、布尔值、未知字段和 KEVENT 对象。事件在函数中初始化,新创建的线程等待使用存根函数中的 KeWaitForSingleObject 发出此事件的信号。事件在 KiInitPatchGuardContext 结束时发出信号,然后开始解密和检查过程。步骤 3 涉及解密过程,该过程与 DPC 使用的解密过程相同,使用两阶段解密并附加硬编码序言。第一阶段使用 KiWaitNever 和 KiWaitAlways,第二阶段由 CmpAppendDllSection 的副本执行,然后进入验证例程。验证例程结束后,使用 KeDelayExecutionThread 或 KeWaitForSingleObject 将上下文恢复为等待状态,超时时间设置在 2 到 2 分 10 秒之间,这是在禁用驱动程序中搜索 PatchGuard 线程的重要位置。

通过 APC 插入

如前所述,第四种方法涉及将 APC 插入系统线程队列,其中 StartAddress 指向 PopIrpWorkerControl,KernelRoutine 参数为 KiDispatchCallout。与 DPC 和系统线程方法类似,它采用两阶段解密例程,并使用硬编码 XOR 值覆盖上下文的第一部分。由于 APC 的交付速度很快,因此这种方法相对较快。但是,与以前的方法类似,需要进行验证等待以确保经过最少的时间,等待时间为 2 到 2.10 秒。需要注意的是,在禁用驱动程序中搜索 PatchGuard 线程时,也应考虑此方法。

通过全局变量调用

在 KiFilterFiberContext 方法中,会通知一个回调函数,即我们之前讨论过的 TV 回调 (PatchGuardTVCallback),它将指向检查例程的指针放在 mssecflt.sys 的全局变量中。此方法利用全局 PatchGuard 上下文结构,当第二个参数为 7 时,该结构由 KiInitPatchGuardContext 初始化。与其他方法不同,此方法不需要解密过程,因为全局 PatchGuard 结构在内存中以明文形式存在。检查例程最多可调用五次,直到返回的状态不同于 STATUS_MORE_PROCESSING_REQUIRED。交叉引用此函数表明它可以从不同的路径调用,其中最有趣的是 SetGetProcessContextWithAssertion,它可以从多个回调函数调用,例如 SecPreCleanup、SecSendFileDeleteEvent、SecSendFileModifyEvent 等。对检查例程的调用直接进入检查,并且此版本的检查例程中不存在负责修改 PatchGuard 与该方法的行为的代码。

通过 KiSwInterruptDispatch

与全局变量方法类似,此技术还利用了内存中以明文形式存在的 PatchGuard 上下文结构。因此,不需要解密过程,验证例程在 KiSwInterrupt 的某个阶段直接调用,这是一个 IDT 函数。

验证阶段

本节讨论 PatchGuard 中使用的不同验证例程,包括 FsRtlMdlReadCompleteDevEx,它是主例程。该函数可分为几个部分,包括序言,它涉及对 pg_ctx 部分进行校验和并重新加密第 1 部分,然后对第 2 部分和第 3 部分进行校验和,然后等待。然后,该函数解密第 1 部分,对第 2 部分和第 3 部分进行校验和,并将它们与保存的校验和进行比较。它还对第 1 部分进行校验和并设置亲和性线程。

序言

  1. PatchGuard 首先检查整个 PatchGuard 上下文结构的完整性,该结构现在以纯文本形式存储在内存中。它将校验和结果与上下文解密之前存储的结果进行比较,该结果在 KiInitPatchGuardContext 中初始化。在执行校验和之前,变量数据会保存在堆栈上并从结构中清除,以确保校验和保持不变。这包括上下文校验和之类的值以及 WorkItem 之类的结构。
  2. PatchGuard 继续重新加密 PatchGuard 上下文结构的第一部分。尚不清楚为什么结构的其余部分未加密。
  3. PatchGuard 对上下文中的第 2 部分和第 3 部分执行另一次校验和,其中包含一些 NT 例程的完整代码,以及一个包含每个关键结构的信息的数组,这些结构稍后将进行验证。在等待之前,PatchGuard 不会重新加密这些部分。
  4. 等待(休眠)可确保两次检查之间至少间隔两分钟。它可以通过以下三种不同的方法之一来执行:未命名函数、KeWaitForSingleObject 或 KeDelayExecutionThread。
  5. 在主函数中,上下文的第一部分无需任何额外步骤即可解密。
  6. 对上下文的第 2 部分和第 3 部分执行校验和,以确保在等待期间没有发生任何修改。原始校验和先前存储在寄存器中,并由等待例程推送/弹出到堆栈上,因此很难找到和修改。
  7. 对上下文的前 0x618 个字节进行校验和,其中包含函数指针,但不包含哈希值或变量。将结果与 KiInitPatchGuardContext 中上下文初始化期间计算的原始校验和进行比较,后者存储在结构中的偏移量 0x8b8 处。
  8. 为了确定将在其上运行检查的处理器,PatchGuard 检索先前在 KiInitPatchGuardContext 中设置的 SessionId,并生成 0 到系统上进程总数之间的随机值。 PatchGuard 不会选择随机 PID,而是循环获取第 n 个进程,其中 n 是随机值。接下来,PatchGuard 附加到此进程并检索其组亲和性。通过对表示亲和性的位图执行汉明权重,可获得 0 到可运行此线程的处理器数量之间的随机值。使用随机值 n,PatchGuard 选择使用 KeEnumerateNextProcessor 循环获得的第 n 个处理器,并将新亲和性设置为此处理器。例如,如果线程可以在处理器 1、2 和 6 上运行,PatchGuard 将选择一个随机值 0 <= n < 3,并使用 KeSetSystemGroupAffinityThread 将其系统亲和性设置为 n。

内核结构的完整性检查

PatchGuard 算法对 PatchGuard 上下文结构中包含的各种数据结构进行操作。此结构包括指向要检查的数据的指针、数据大小、类型以及初始化期间计算的校验和等信息。算法首先分派要检查的数据类型,以确定当前结构之后要检查的下一个结构。这很重要,因为某些结构可能需要初步检查或操作,然后才能使用校验和进行完整性验证。验证所选结构的完整性后,PatchGuard 会增加检查的数据总量,并将其与 KiInitPatchGuardContext 的第三个参数中定义的最大值进行比较。如果总量尚未达到,PatchGuard 将继续处理关键数据结构数组中的下一个条目。对于第二部分,PatchGuard 计算中断描述符表 (IDT) 寄存器指向的表的校验和。在哈希计算之后,PatchGuard 恢复之前的处理器亲和性,并将获得的哈希与存储在内存中的哈希进行比较。

检测到修改时

在 PatchGuard 中完成 IDT 案例的校验和后,将恢复当前线程的原始关联性,并将计算出的哈希值与初始化阶段的哈希值进行比较。如果检测到修改,PatchGuard 会在执行某些特定操作后触发 BSOD。

首先,PatchGuard 将结构置于通用状态,通过在堆栈上保存值并从上下文中清除它们来计算校验和。这包括完整上下文结构的校验和、已检查数据的总大小以及工作项,工作项保存在堆栈中并从上下文中清零。然后,执行完整结构的校验和。此后,从堆栈中恢复上下文中的工作项,并将校验和结果存储在特定偏移量处。虽然这个校验和没有与前一个校验和进行比较,但它似乎并不那么重要。接下来,PatchGuard 继续重新加密 PatchGuard 上下文开头的 CmpAppendDllSection 代码。目前尚不清楚为什么需要进行这种加密,特别是因为结构的其余部分目前仍为明文。在重新加密过程中,新加密的数据是选定的部分,其余部分是随后立即加密的数据。

检测到潜在攻击后,该过程的下一步是恢复敏感数据。在调用 KeBugCheck 时,PatchGuard 倾向于重写 PTE 和 Windows 关键例程,而不是检查完整性。首先使用 KeAquireSpinLockForDpc 从上下文中获取 PTE 以安全地操作它们。使用了一个“技巧”,其中“mov cr4”指令通过修改第 7 位(即 PGE)来刷新 TLB(包括全局条目)。重写的下一部分涉及关键例程,例如 KeBugCheckEx、KeBugCheck 或 KeIsEmptyAffinityEx,它们被重写并存储为 PatchGuard 上下文中的对数组(pFunction、size_of_routine)。 DbgPrint 例程也被重写为 0xC3,这是一个“ret”指令,作为一种反调试措施。PatchGuard 清除了上下文结构中位于 0x610 和 0x690 处的两个偏移量,尽管原因未知。最后,PatchGuard 调用 KeGuardCheckICall 并以 KeBugCheckEx 作为参数,但如果使用的调度方法不是 7,则调用 SdpbCheckDll 而不是 KeBugCheckEx。****

总结

PatchGuard 检查例程分为两部分:一部分用于检测到修改,另一部分用于一切正常。当完成结构的最终哈希比较时,如果检查的数据总量低于 KiInitPatchGuardContext 中定义的最大值,则 PatchGuard 将继续处理数组中的下一个结构。如果超过最大值,它将重新装备 PatchGuard 上下文以供以后使用,这与初始化过程类似。对于方法 0、1、2、4 和 5,代码与初始化过程几乎相同。这些方法涉及调用 KeSetCoalescableTimer、将 DPC 存储在 KPRCB.AcpiReserved 和 KPRCB.HalReserved 中、使用 KeInsertQueueApc 插入 APC 或在全局变量中设置 DPC。对于创建系统线程的第三种方法,它会重新装备,但不在同一个主函数中。验证例程完成后,一个小调度程序会在 KeDelayExecutionThread 或 KeWaitForSingleObject 之间进行选择。如果选择 KeDelayExecutionThread,则设置 2’ 到 2’10” 之间的超时。如果使用 KeWaitForSingleObject,则这次设置相同的 2’ 超时。在这种情况下,在第七个方法的 KiInitPatchGuardContext 末尾通过 KeSetEvent 通知的事件被重置,并且有 50% 的可能性永远不会再次设置它。