首页
社区
课程
招聘
[翻译]MS Windows 10 RS4 v1.00 上的 PatchGuard 更新分析(01)
2022-7-14 14:46 9576

[翻译]MS Windows 10 RS4 v1.00 上的 PatchGuard 更新分析(01)

2022-7-14 14:46
9576

文章太长了,只能分几部分传了,论坛上的文章格式有些问题,有点懒不太想改,稍后会放出完整的 导出pdf

原文见附件

Updated Analysis of PatchGuard on Microsoft Windows 10 RS4

自 Windows 64b 以来,PatchGuard 一直对 Windows 安全研究者有浓厚的吸引力。

 

在其开发的大多数迭代中,有几个人分析了它的主要机制和内部结构,很多时候这导致了功能绕过。

 

研究人员似乎同意一件事:

 

绕过 PatchGuard 在理论上总是可行的,因为它与驱动程序运行在同一级别。

 

从理论上讲,这似乎是正确的。

 

也就是说,就像漏洞利用不再是关于 NOP-sled (nop和滑块指令)一样,绕过 PatchGuard 也不再是挂钩 KeBugCheck。

 

本文将全面介绍 PatchGuard 机制,从初始化到蓝屏死机,以及我们如何实现能够禁用它的驱动程序的见解。

 

特别是,这项研究是使用 Tetrane 的工具 REVEN 进行的timeless分析。

 

在整个分析过程中没有使用任何调试器

 

I - 简介

 

本文将全面介绍 PatchGuard 机制,从初始化到蓝屏死机,以及我们如何实现能够禁用它的驱动程序的见解。

 

在本介绍中,我们将首先介绍一些关于永恒分析的内容,然后我们将了解 PatchGuard 是什么以及它是如何工作的概述,以及我们分析它的方法。

 

A - 关于 REVEN timeless分析的几句话

 

对于这项研究,我们使用了timeless分析。

 

由于大多数人不知道它是什么,我想用几句话来介绍它是个好东西。

 

经典调试器可以为您提供特定指令的状态并且只能继续执行,

 

而 Timeless Analysis 是一种机制,允许您在整个系统的执行中进行时间旅行并立即检索系统的完整状态(完整内存、用户和内核、硬件事件、任何进程/线程)。

 

Timeless Analysis 工作流程包括几个步骤:

 

• 记录虚拟机的完整执行(超过 100 亿条指令是可以的)

 

• 在模拟 CPU 上重放记录的场景

 

• 像在任何调试器中一样分析生成的跟踪,但时间旅行

 

PatchGuard,这允许我们只记录一次初始化和蓝屏死机,并在整个研究过程中使用它。

 

使用经典的调试器,必须设置很多断点才能绕过反调试检查,还要设置更多的断点来观察系统的特定状态。

 

此外,由于 PatchGuard 在不运行时基本上会自行加密,因此我们可以轻松检索它的完整解密状态。

 

在本文的 VII - B 中查看有关 REVEN 出色功能的更多信息,并访问我们的网站和博客 www.tetrane.com 和blog.tetrane.com。

 

不要犹豫与我们联系并享受阅读!

 

B - 什么是 PatchGuard

 

PatchGuard,最初命名为“内核补丁保护”,是一种 Windows 机制,旨在保护内核免受补丁的影响。

 

以下是 Microsoft 常见问题解答中的一份声明:

 

« 由于修补程序会用未知、未经测试的代码替换内核代码,因此无法评估第三方代码的质量或影响……

 

对 Microsoft 在线崩溃分析 (OCA) 数据的检查显示该系统崩溃通常是由修补内核的恶意和非恶意软件引起的。 »

 

Microsoft 从未支持修补内核,因为它会导致许多负面影响。

 

从供应商的角度来看,PatchGuard 迫使他们停止使用未记录的结构来继续他们的检测机制

 

从恶意软件编写者的角度来看,PatchGuard 可防止 Rootkit 持久存在且难以检测或删除

 

因此,从攻击者的角度来看,PatchGuard 非常有趣。

 

C - 它是如何工作的?

 

PatchGuard 将检查内核中的许多结构和代码区域,攻击者/供应商可以使用这些结构和代码区域来执行敏感操作。

 

如前所述,攻击者可以挂钩一些结构,例如中断描述符表 (IDT) 或其他结构,PatchGuard 将通过执行检查来防止这种情况。

 

例如,检查结构的非详尽列表包括:

 

• IDT/GDT

 

• 调试例程

 

• 加载的模块列表

 

• PatchGuard 代码和结构本身

 

• 等等

 

MSDN 上的BugCheck 0x109 页面也提供了一个不详尽的列表。

 

PatchGuard 背后的基本思想是,

 

它会在系统执行期间定期计算敏感结构的校验和,并将其与启动时获得的校验和进行比较,然后再加载任何用户驱动程序。

 

如果检测到修改,则 PatchGuard 将触发带有错误检查代码 0x109 (CRITICAL_STRUCTURE_CORRUPTION) 的蓝屏死机 (BSOD),考虑到系统已受到威胁。

 

现在,由于 PatchGuard 运行在与任何驱动程序相同的级别,因此始终可以禁用它,只要您能找到它。

 

这就是 PatchGuard 复杂的地方。

 

因为它必须对攻击者隐藏自己,所以 PatchGuard 使用了许多将在本文中描述的机制。

 

这很重要,因为它还定义了我们如何通过查找 PatchGuard 上下文可能存在的每个位置来成功禁用它(有一些与 PatchGuard 无关的限制)。

 

D - 我们的方法:timeless调试

 

为了分析 PatchGuard,我们首先开发了一个驱动程序来修补 IDT。

 

然后使用 Tetrane 的 Timeless Analysis 工具 REVEN,我们记录了 PatchGuard 的初始化和触发 BSOD 的过程。

 

例如,下面是我们如何使用修补的 IDT 上的内存历史记录来获取对该区域的内存访问列表,显示负责检查的指令
图片描述

 

通过使用此内存历史功能,这使我们能够快速找到校验和算法以及用于随机化它的加密密钥。

 

然后,我们发现了解密的内存中 PatchGuard 上下文结构,PatchGuard 使用它来保存信息并执行检查。

 

在分析了许多条目后,我们很好地了解了 PatchGuard 的主要机制是如何工作的,我们能够通过静态分析和 Timeless Debugging 继续进行分析,以观察执行工作流程。

 

II - 初始化

 

在这一部分我们将描述 PatchGuard 如何初始化它的上下文和验证机制

 

它主要由 KiFilterFiberContext 完成。

 

KiFilterFiberContext 最初以这种方式命名是为了误导逆向分析人员,但它现在是一个众所周知的功能。

 

A - 调用 KiFilterFiberContext

 

PatchGuard 的初始化主要由 KiFilterFiberContext 执行。

 

此函数在引导开始时调用,在任何用户驱动程序加载之前。

 

KiFilterFiberContext 有两种调用方式,下文详述。

 

1 - KiAmd64SpecificState 中触发异常

 

PatchGuard 的初始化使用异常处理程序作为混淆方法。

 

自动触发除法错误,执行异常处理程序并调用 patchguard 初始化函数。

 

此机制在引导过程开始时可见。

 

以下是我们可以通过 REVEN 看到的错误说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0xfffff803c98dabd1 movzx edx, byte ptr [rip – 0x4f3255] *; KdDebuggerNotPresent*
 
0xfffff803c98dabd8 movzx eax, byte ptr [rip – 0x51ee66] *; KdPitchDebugger*
 
0xfffff803c98dabdf or edx, eax
 
0xfffff803c98dabe1 mov ecx, edx
 
0xfffff803c98dabe3 neg ecx
 
0xfffff803c98dabe5 sbb r8d, r8d
 
0xfffff803c98dabe8 and r8d, 0xffffffee
 
0xfffff803c98dabec add r8d, 0x11
 
0xfffff803c98dabf0 ror edx, 1
 
0xfffff803c98dabf2 mov eax, edx
 
0xfffff803c98dabf4 cdq
 
0xfffff803c98dabf5 divide error while executing idiv r8d

这里有趣的是,用于计算除法的两个值实际上是已知符号:

 

KdDebuggerNotPresent

 

和 KdPitchDebugger。

 

这两个值用于确定是否附加了调试器。

 

因此,如果存在调试器,则不会初始化 PatchGuard。

 

在正常情况下,这两个变量设置为 1,

 

这在 idiv 指令中给出值 rax=0x80000000、rdx=0x80000000 和 r8d=0xffffffff。

 

idiv 指令计算如下:

1
2
3
[edx:eax] / r8d
 
i.e. 0x8000000080000000 / 0xffffffff

如 AMD64 文档中所定义,

 

* 如果正结果大于 7FFFFFFFH 或负结果小于 80000000H *,

 

则触发除法错误。

 

在这种情况下,两个操作数都是负数,应该给出正数结果,但是这个除法的结果是 0x80000001,因此除法错误。

 

一旦触发除法错误,就会执行 KiDivideErrorFault 函数,该函数会继续将异常分派给正确的处理程序。

 

在这种情况下,处理程序只是 KiFilterFiberContext 函数的一个存根:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
*; Exception handler for KiInitAmd64SpecificState*
 
0xfffff803c98f1d1c push rbp
 
0xfffff803c98f1d1e sub rsp, 0x20
 
0xfffff803c98f1d22 mov rbp, rdx
 
0xfffff803c98f1d25 xor ecx, ecx
 
0xfffff803c98f1d27 call 0xfffff803c98a0bb0
 
*; KiFilterFiberContext - ntoskrnl.exe*
 
0xfffff803c98a0bb0 mov qword ptr [rsp + 8], rbx
 
[...]

我们从 REVEN 得到的调用栈如下:

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
KiFilterFiberContext
 
KiInitAmd64SpecificState_ExceptionHandler
 
__C_Specific_Handler
 
RtlpExecuteHandlerForException
 
RtlDispatchException
 
KiDispatchException
 
KiExceptionDispatch
 
KiDivideErrorFault
 
KeInitAmd64SpecificState // Triggers a page fault
 
PipInitializeCoreDriversAndElam
 
IopInitializeBootDrivers
 
IoInitSystemPreDrivers
 
IoInitSystem

众所周知,KiFilterFiberContext 负责使用特定参数调用初始化过程以创建 Patchguard 上下文。

 

这里要注意的一件事是,其中一个参数被硬编码为 0,这暗示了它可能在其他地方被调用的事实。

 

事实上,另一个初始化已经记录在案并指向函数 ExpLicenseWatchInitWorker。

 

2 - ExpLicenseWatchInitWorker

 

该函数在启动过程中在 KeInitAmd64SpecificState 之前调用。

 

这是调用堆栈

1
2
3
4
5
6
7
8
9
KiFilterFiberContext
 
**ExpLicenseWatchInitWorker**
 
ExInitSystemPhase2
 
Phase1InitializationDiscard
 
Phase1Initialization

ExInitSystemPhase2还负责调用函数ExpGetNtProductTypeFromLicenseValue,这显然与微软的license验证有关。

 

在这种情况下,有趣的是 ExpLicenseWatchInitWorker 将调用 KiFilterFiberContext,但概率很低。

 

PatchGuard 的许多机制使用随机值(使用指令 rdtsc)来决定事情,在这种情况下,它用于决定是否应该调用 KiFilterFiberContext,概率为 4%。

 

在这个函数中需要注意几点,特别是一个。

 

• 首先要注意的是,此功能包括一些检查是否存在调试器和安全启动模式

 

• 第二件事,与 PatchGuard 无关的是,该函数的返回值是 rdtsc 指令生成的随机值乘以常数值 0x51eb851f

 

(这实际上是优化除法的常数)。

 

如果我们只假设该函数是由 ExInitSystemPhase2 调用的,那么如果 InitIsWinPEMode 为 true,这个随机返回的值稍后将用作索引

1
2
3
4
5
6
7
8
9
mov al, r15b *; eax is NOT zero extended here*
 
loc_1408EAFBB
 
inc rax
 
cmp [rcx + rax*2], di *;RAX is the following: [0000.0000][RAND][r15b]*
 
jnz loc_1408EAFBB

a - 传递给 KiFilterFiberContext 的结构

 

这次是用一个结构体来调用KiFilterFiberContext的。

 

这个结构是根据从 PRCB(进程寄存器控制块)、HalReserved 字段中获取的值以及指向 KiFilterFiberContext 的指针构建的

1
2
3
4
5
6
7
8
9
0xfffff803c98dedda mov rax, qword ptr [rip – 0x46d3a1] *; KPRCB*
 
0xfffff803c98dede1 mov r11, qword ptr [rax + 0x78] *; HalReserved[6]*
 
0xfffff803c98dede5 mov rbx, qword ptr [rax + 0x70] *; HalReserved[5]*
 
0xfffff803c98dede9 and qword ptr [rax + 0x78], 0
 
0xfffff803c98dedee and qword ptr [rax + 0x70], 0

可以看到,这些字段在之后就被清理了。

 

这是 ExpLicenseWatchInitWorker 的伪代码

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
DWORD64 ExpLicenseWatchInitWorker()
 
{
 
 KiFilterParam = Prcb.HalReserved[6]; *// &KiServiceTablesLocked*
 
 pKiFilterFiberContext = Prcb.HalReserved[5]; *// &KiFilterFiberContext*
 
 Prcb.HalReserved[6] = 0;
 
 Prcb.HalReserved[5] = 0;
 
 if (InitSafeBootMode != 0 | KUSER_SHARED_DATA.KdDebuggerEnabled >> 1)
 
 {
 
 return rand_stuff
 
 }
 
 if(random(0,100) ≤ 4)
 
 KiFilterFiberContext(pKiFilterFiberParam);
 
}

这两个指针是在启动的最开始设置的,在函数 KiLockServiceTable 中,它来自以下调用堆栈:

1
2
3
4
5
6
7
KiLockServiceTable
 
KeCompactServiceTable
 
KiInitializeKernel
 
KiSystemStartup

这个函数需要解释两件事。

 

第一个是它如何将两个指针放在 HalReserved 字段中

 

第二个是它在其开头调用的函数

 

i - KiLockServiceTable:填充 HalReserved[] 字段

 

为了“混淆”其控制流,KiLockServiceTable 再次使用异常处理程序,但不是触发故障,

 

而是通过使用 RtlLookupExceptionHandler 获取指向它的指针直接调用处理程序。

 

处理程序本身只是函数 KiFatalExceptionFilter 的一个存根,我们对其进行了分析:

 

第一个要填充的 HalReserved 字段是第 6 个:

1
2
3
4
5
lea rbx, KiServiceTablesLocked
 
[...]
 
mov [rsi+(_KPRCB_.HalReserved[6])], rbx

这个函数 KiServiceTablesLocked 是一个误导性的名称,因为它拥有一个结构。

 

这个结构是给 KiFilterFiberContext 函数的一个参数。

 

因此,它已经在文献中命名为 KI_FILTER_FIBER_PARAM。

 

该结构的原型如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _KI_FILTER_FIBER_PARAM
 
{
 
 CHAR code_prefetch_rcx_retn[4]; *// prefetchw byte ptr [rcx]; retn;*
 
 CHAR padding[4]; *// Align*
 
 PVOID pPsCreateSystemThread;
 
 PVOID Pg_Method3StubToCheckRoutine_sub_1402CD680;
 
 PVOID pKiBalanceSetManagerPeriodicDpc;
 
}KI_FILTER_FIBER_PARAM, *PKI_FILTER_FIBER_PARAM;

稍后将给出有关此结构的详细信息,因为它涉及对用于触发检查例程的机制的深入解释。

 

ii - KiLockServiceTable:校验和初始化

 

KiLockServiceTable 在函数 KiLockExtendedServiceTable 的开头调用,这也是一个 PatchGuard 相关函数。

 

它用于执行几个部分的校验和或函数表条目的校验和。

 

两个结果都设置在两个全局变量(qword_1403AD4B8 和 qword_1403AD4C8)中,稍后将在上下文初始化过程中使用。

 

这些校验和机制本身将在本文后面解释。

 

B - KiFilterFiberContext

 

如前所述,可以使用参数(KI_FILTER_FIBER_PARAM 结构指针)或 NULL(大多数情况下,来自 KiAmd64SpecificState)调用 KiFilterFiberContext

 

它的主要工作是使用特定参数调用上下文初始化例程。

 

这些参数将主要确定使用哪种方法来触发 PatchGuard 检查

 

由于这个主要的初始化函数是已知的,所以通用名称是 KiInitializePatchGuardContext(来自文献)。

 

1 - 快速概览

 

这是 KiFilterFiberContext 的伪代码

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
KiFilterFiberContext(PKI_FILTER_FIBER_PARAM pKiFilterFiberParam)
 
{
 
​    AntiDebug();
 
​    rand1_10 = __rdtsc() % 10;
 
​    rand2_1 = rand1_10 > 6;
 
​    rand3_6 = __rdtsc() % 6;
 
​    rand4_13 = __rdtsc() % 13;
 
​    *// First initialize a global in memory, this will be explained*
 
​    if (!g_pGlobalCtx &&
 
​      !pKiFilterFiberParam &&
 
​      !KpgApiRegistered)
 
​      if (PsIntegrityCheckEnabled)
 
​      {
 
​        Notify_Callback("TV", Pg_TVCallback_CheckRoutine_sub_1401825A0, & KpgApiConsumerRanges)
 
​        if (KpgApiConsumerRanges)
 
​          KpgApiRegistered = 1;
 
​      }
 
​    *// Now initialize a first context*
 
​    result = KiInitPatchGuardContext(
 
​      rand3_13,
 
​      rand2_6,
 
​      rand2_1 + 1,
 
​      pKiFilterFiberParam,
 
​      1)
 
​    if (result)
 
​    {
 
​      if (rand1_10 < 6)
 
​      {
 
​        rand5_13 = __rdtsc() % 13;
 
​        *// Get a random value < 6 but different from rand3_6*
 
​        rand6_6 = __rdtsc() % 6;
 
​        while (rand6_6 == rand3_6)
 
​        {
 
​          rand6_6 = __rdtsc() % 6;
 
​        }
 
​        *// Initialize a second context*
 
​        result = KiInitPatchGuardContext(
 
​          rand5_13,
 
​          rand6_6,
 
​          rand2_1 + 1,
 
​          pKiFilterFiberParam,
 
​          0);
 
​      }
 
​      if (result) {
 
​        if (!g_pGlobalCtx &&
 
​          !pKiFilterFiberParam &&
 
​          (KiSwInterruptPresent() >= 0) &&
 
​          KpgApiRegistered) {
 
​          localvar = 8;
 
​          if (KiSwInterruptPresent() >= 0)
 
​          {
 
​            localvar = 0;
 
​          }
 
​          *// Initialize a Third context*
 
​          result = KiInitPatchGuardContext(0, 7, 1, 0, localvar);
 
​        }
 
​        if (result && !pKiFilterFiberParam)
 
​        {
 
​          *// Zero stuff*
 
​          memset( & KpgKernelExtents, 0, 24);
 
​          KpgProtectedFunctionExtentsSupported = 0;
 
​          KpgDisabledTimerMethods = 0;
 
​          KpgProcessListOverflowLock = 0;
 
​          dword_1403AD510 = 0;
 
​          qword_140904080 = 0;
 
​        }
 
​      }
 
​    }
 
​    AntiDebug();
 
​    return result;

这个函数比 Windows 8.1 的以前版本稍微复杂一些,

 

但主要思想仍然相同:

 

使用大部分随机值作为参数,KiInitPatchGuardContext 最多被调用 3 次;

 

第一次无论如何都会发生,

 

第二次只有50%的几率,

 

第三次,用新的方法,比较特殊,出现的次数最多,本文会介绍。

 

另一个新事物是名为“TV”的回调通知,它来自另一个二进制文件。

 

C - PatchGuard 上下文的初始化

 

大多数初始化方法都依赖于 KiInitPatchGuardContext,哪些参数决定如何触发检查,但存在其他机制。

 

在本节中,我们将描述什么是 PatchGuard 上下文,并描述 PatchGuard 用于将自身隐藏在系统中的多种方法。

 

如果其中许多方法是已知的,但不是全部,我们将尝试仔细描述它们,因为这是我们开发的用于完全禁用 PatchGuard 的代码的基础。

 

1 - PatchGuard 上下文:定义

 

在文献中,PatchGuard 上下文用于描述 PatchGuard 用于执行检查的巨大结构。

 

但随着时间的推移,我们可以看到,当研究人员说“我找到了一个 PatchGuard 上下文”时,

 

他们并没有谈论结构,而是更多地谈论 PatchGuard 的“实例”,这基本上意味着方法和结构的结合;

 

方法是如何初始化和触发检查

 

结构是 PatchGuard 用于执行检查的全部数据量。

 

a - 结构

 

为了分析其内容和初始化,我们分析了对其字段的大部分访问,并将其与 KiInitPatchGuardContext 函数相关联。

 

以下是对该结构中一些有趣领域的一些解释。

 

这并不详尽且非常详细,但它暗示了可以在此结构中找到的内容。

 

它主要分为三个部分

 

第一个,大小为 0x928,是 PatchGuard 机制的核心内容。

 

第二个更多的是数据接收者,它将保留原始数据以供以后使用。

 

第三部分包含有关要检查的数据的信息。

 

i - 第一部分

 

CmpAppendDllSection

 

PatchGuard 上下文结构的最开头包含函数 CmdAppendDllSection 的代码。

 

这段代码直接复制到结构体0x1408929CC处,稍后在PatchGuard触发完整性检查时使用。

 

它的主要工作是使用随机生成的密钥**解密**(异或)PatchGuard 上下文结构的其余部分

 

通过访问的内存历史和时间旅行调试,我们很容易发现密钥是在 0x1408A8291 处生成的。

 

对于使用 DPC 的方法,此键作为 DeferredContext 参数传递。

 

如果我们以函数 PopThermalZoneDpc 为例,

 

KiProcessExpiredTimerList 将使用 rdx 中的 DeferredContext 调用它。

 

• Nt API 指针

 

结构的下一部分包含来自 ntoskrnl API 的许多函数指针(超过 100 个)。

 

这些指针以这种方式保存,以便 PatchGuard 例程可以独立于重定位使用它们,并且其中一些能够复制它们(就像 CmdAppendDllSection 一样)。

 

这是有道理的,因为主验证例程实际上并不直接使用 ntos 函数,而是将其完整副本复制到可执行内存中。

 

这些指针大部分都在 0x140892AC4 附近初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sti
 
 lea rax, ExAcquireResourceSharedLite
 
 mov [r14+pg_ctx_rs4.ntoskrnl_ExAcquireResourceSharedLite_0xe8], rax
 
 lea rax, ExAcquireResourceExclusiveLite
 
 mov [r14+pg_ctx_rs4.ntoskrnl_ExAcquireResourceExclusiveLite_0xf0], rax
 
 lea rax, ExAllocatePoolWithTag
 
 mov [r14+pg_ctx_rs4.ntoskrnl_ExAllocatePoolWithTag_0xf8], rax
 
 [...]

这些函数中的大多数具有已知符号并且是常见的 Windows 内核例程,

 

但其中一些是与 PatchGuard 直接相关的未命名例程。

 

例如在 0x1401812E0 处,该函数只是在这里直接调用 PatchGuard 在某些时候使用的 DPC 的延迟例程条目。

 

• 指向全局变量其值的指针

 

许多对全局变量的引用被存储和使用。

 

例如,它保存了最初由全局 KiWaitAlways 和 KiWaitNever 在偏移量 0x4e0 和 0x5b8 处保存的两个值。

 

这些值在启动时随机初始化,稍后我们将看到这些每次启动的随机值用于编码和解码 PatchGuard DPC 指针。

 

另一个有趣的全局示例是在偏移量 0x5f8 处保存指向另一个 PatchGuard 上下文结构的指针。

 

该指针被多次用作结构的干净备份。

 

在 KeBugCheck 的情况下发送的也是这个全局指向的结构,正如在 KiMarkBugCheckRegion 中可以看到的那样:

1
2
3
4
5
6
7
8
9
mov rcx, cs:Pg_GlobalCtx_qword_14045E208
 
test rcx, rcx
 
jz short loc_1401812BD
 
mov edx, 928h // Size of the PatchGuard structure
 
call IoAddTriageDumpDataBlock

• 公共变量

 

系统相关变量:

 

在这个类别中,我们可以找到诸如Ntoskrnl 和Hal 基地址、当前PRCB、最大虚拟寻址大小等变量。

 

我们还可以找到与关键结构的校验和一起使用的初始化向量,或用于在每次块迭代中导出初始化向量的移位值。

 

这两个值都在 0x1408937A0 处使用 rdtsc 随机初始化。

 

同样,PatchGuard 上下文的校验和存储在其自身中。

 

为了检测任何损坏,首先在初始化期间计算它,并在每个检查例程开始时与运行时计算的校验和进行比较。

 

• 运行时变量

 

一些字段也用作运行时变量以跟踪检查例程状态。

 

例如,我们可以找到可以称为“检查会话”的检查数据总量。

 

正如前面对 KiInitPatchGuardContext 的第三个参数所解释的,它在每个关键结构校验和之后增加它的大小,并与最大值进行比较。

 

调度方法所需的数据临时存储在上下文结构中,例如 DPC 结构、ETHREAD 指针,以便它可以调用 KiInsertQueueApc 之类的函数。

 

还可以找到在检测到损坏时传递给 KeBugCheck 的参数,或作为参数传递给 KiInitPatchGuardContext 的调度方法。

 

• 标志

 

主要标志之一是位于偏移0x828 的标志

 

它用作表示布尔值的位图,例如(非详尽列表):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BIT 6  0x40      Only one processor
 
BIT 8  0x100      Use of KiDpcDispatch
 
BIT 9  0x200      Use of KiTimerDispatch
 
BIT 15 0x8000     Use of KeSetEvent
 
BIT 18 0x40000    Related to the ntoskrnl routines checksum
 
BIT 20 0x100000   Should DR7 be cleared
 
BIT 24 0x1000000  loc_1402F4907
 
BIT 27 0x8000000  Should PTE be restored loc_1402F117F
 
BIT 28 0x10000000 Scheduling method 7, use of KiInterruptThunk
 
BIT 30 0x40000000 loc_1408A836F Again, scheduling method 7
 
BIT 31 0x80000000 Result of KiSwInterruptPresent

存在其他标志,但我们没有分析所有标志。

 

ii - 第二部分

 

该结构的第二部分保存将保留以供以后使用的数据。

 

• PTE 的保存

 

在 Windows 10 RS4 中,结构中恰好保存了 20 个条目。

 

这些条目被保存,因为它缓解了绕过。

 

稍后我们将看到这些 PTE 在触发 KeBugCheck 之前被恢复。

 

• 关键内核例程保存

 

出于同样的原因,PTE 被保存,关键内核的整个代码被立即保存。

 

对于 Windows 10 RS4,以下是在结构中具有各自偏移量的例程:

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
Hal HaliHaltSystem_0x930
 
Ntosrknl KeBugCheckEx_0x940
 
Ntoskrnl KeBugCheck2_0x950
 
Ntoskrnl KiBugCheckDebugBreak_0x960
 
Ntoskrnl KiDebugTrapOrFault_0x970
 
Ntoskrnl RtlpBreakWithStatusInstruction_OR_DbgBreakPointWithStatus_0x980
 
Ntoskrnl RtlCaptureContext_0x990
 
Ntoskrnl StartOfChunckFor_KeQueryCurrentStackInformation_0x9a0
 
Ntoskrnl KeQueryCurrentStackInformation_0x9b0
 
Ntoskrnl KiSaveProcessorControlState_0x9c0
 
Ntoskrnl memcpy_OR_memmove_0x9d0
 
Ntoskrnl IoSaveBugCheckProgress_0x9e0
 
Ntoskrnl KeIsEmptyAffinityEx_0x9f0
 
Ntoskrnl VfNotifyVerifierOfEvent_0xa00
 
Ntoskrnl _guard_check_icall_0xa10
 
Ntoskrnl KeGuardDispatchICall_0xa20
 
Ntoskrnl g_pxHalHaltSystem_0xa30

稍后我们将再次看到这些函数在触发 KeBugCheck 之前恢复。

 

所有这些函数都有各自的大小,因此恢复例程知道要重写多少。

 

代码本身稍后存储在结构中。

 

有趣的是,最后一个“函数”实际上只是一个指向 xHalHaltSystem 的指针。

 

iii - 第三部分

 

为了跟踪需要检查的结构,PatchGuard 使用一个结构数组来保存每次检查的必要信息。

 

• 检查的关键结构

 

这是一个结构的原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct pg_crit_struct_check_data
 
 {
 
 ULONG64 KeBugCheckType_0x0; *// 0x2 for IDT, 0x3 for GDT, etc.*
 
 ULONG64 pData_0x8;
 
 ULONG32 szData_0x10;
 
 ULONG32 hash_0x14;
 
 **ULONG64 specific[3];**
 
 };

KeBugCheckType 用于区分结构类型。

 

MSDN 文档中提供了非详尽列表,

 

因为此信息与 PatchGuard 发布的 KeBugCheck 一起提供

 

(请参阅 BugCheck 0x109:CRITICAL_STRUCTURE_CORRUPTION 的文档)。

 

接下来有一个指向要检查的数据的指针以及要检查的大小。

 

重要的值是校验和结果。

 

此校验和在 PatchGuard 初始化期间计算,并在 PatchGuard 检查相应结构的完整性时用作参考。

 

最后,此结构中的最后一项特定于必须检查的数据。

 

例如,对于 IDT 检查用例,此特定值将保存已用于执行的目标处理器。

 

一般来说,这意味着该结构在检查结构方面可能有所不同,并表明检查代码对于所有结构并不完全相同。

 

• PatchGuard 上下文结构中的相对条目

 

这些结构存储在 PatchGuard 上下文结构中的数组中。

 

PatchGuard 上下文结构的第一部分中存在几个条目以使用此数组:

1
2
3
4
5
6
7
0x680: Total count of critical structure in the array
 
0x684: Offset to next critical structure data to checksum
 
0x6a8: Offset to the first critical structure data
 
0x6ac: Current count of checked structure

这些信息很重要,PatchGuard 在其检查算法中使用这些信息。

 

2 - PatchGuard 上下文:初始化

 

PatchGuard 上下文主要由 KiInitPatchGuardContex**t** 初始化。

 

这个函数实际上是未命名的,但在文献中是已知的。

 

我们将在本节中看到存在其他方法来初始化 PatchGuard 上下文,

 

并且在某些情况下,设置了一些独立的系统检查方式。

 

a - KiInitPatchGuardContext: Method 0, 1, 2, 3, 4, 5, 7

 

如前所述,该函数负责大多数 PatchGuard 上下文的初始化。

 

使用哪种方法的选择取决于赋予此函数的参数。

 

正如我们在 KiFilterFiberContext 概述中所描述的,这些参数大多是随机选择的。

 

在本节中,我们将介绍给此函数的参数,该参数将描述 PatchGuard 检查是如何初始化和触发的。

 

以下是此函数的参数:

 

• - Arg 1:DPC 方法的索引

 

• - Arg 2:调度方法

 

• - Arg 3:用于确定要检查的最大大小的随机值

 

• - Arg 4:指向来自 ExpLicenseWatchInitWorker 的结构的指针(只有 4 % 的机会)

 

• - Arg 5:布尔值,用于确定 nt 例程的完整性是否具有待检查

 

在我们的例子中,最重要的参数是第二个(用于安排检查的方法)和第四个(允许更多的调度方法)。

 

KiFilterFiberContext 中,随机值作为第二个参数的索引,它将决定应该使用什么方法。

 

在本节中,我们将首先描述 KiInitPatchGuardContext 可能初始化的不同方法以及关于该方法的第四个参数。

 

然后我们将快速浏览其他参数。

 

i - 方法 0 - 插入与 DPC 链接的计时器

 

该方法的主要思想是 PatchGuard 将初始化 PatchGuard 上下文结构和 DPC(延迟过程调用),并将其设置在计时器结构中。

 

然后计时器在 0x1408A8920 附近与 KeSetCoalescableTimer 一起排队。

 

计时器在调用后将用 2' 到 2'10" 之间的第一个参数触发 DPC。

 

此计时器不是周期性的,必须在检查例程结束时恢复,我们将在本文后面看到

 

TolerableDelay 参数是 0 到 0.001 秒之间的随机值

 

ii - 方法 1 和 2 - 隐藏 DPC

 

当 KiInitPatchGuardContext 的第二个参数为 1 或 2 时

 

PatchGuard 初始化上下文结构和 DPC 结构,但不使用计时器, 将其隐藏在内核结构PRCB(进程寄存器控制块)中

 

这种方法的有趣之处在于系统中的合法函数实际上负责对DPC进行排队。

 

• AcpiReserved

 

对于方法1,指向DPC的指针隐藏在来自PRCB的AcpiReserved字段:

1
2
3
4
5
6
7
loc_1408A890C:
 
mov rax, [rsp+2238h+KPRCB_var_308]
 
mov [rax+_KPRCB_.AcpiReserved], r8 ; DPC initialized by PatchGuard
 
jmp loc_1408A89CE

它在 HalpTimerDpcRoutine 中排队,并检查每次检查之间是否至少经过了两分钟。

 

为了计算最后一个队列发生的时间,它使用全局变量 HalpTimerLastDpc。

 

这个全局变量是在 HalpTimerSchedulePeriodicQueries 中初始化的,它的值取自全局变量 0xFFFFF78000000014,这与正常运行时间有关(我认为是机器的,但我不确定)。

 

当某个 ACPI 事件发生时调用 HalpTimerDpcRoutine,例如过渡到空闲状态。

 

• HalReserved

 

对于方法 2**,指向 DPC 的指针隐藏在来自 PRCB HalReserved 字段中:**

1
2
3
4
5
6
7
loc_1408A88F8:
 
mov rax, [rsp+2238h+KPRCB_var_308]
 
mov [rax+(_KPRCB_.HalReserved+38h)], r8 *; DPC initialized by PatchGuard*
 
jmp loc_1408A89CE

旁注:

 

回想一下,当从 ExpLicenseWatchInitWorker 调用 KiFilterFiberContext 时,该字段(但该数组的条目)也用于保存指向结构 KI_FILTER_FIBER_PARAM 的指针。

 

它由 HalpMcaQueueDpc 排队,最短周期也为 2 分钟,并在发生 HAL 定时器时钟中断时完成检查(请参阅 HalpTimerClockInterrupt/HalpTimerAlwaysOnClockInterrupt)

 

iii - 方法 3 – 系统线程

 

这种情况需要一个指向 KI_FILTER_FIBER_PARAM 结构的指针,它只有 4% 的机会发生(来自函数 ExpLicenseWatchInitWorker,在 II - A - 2 - a 中解释)。

 

前面已经展示了这个结构的概述,但请记住,它包含一个指向 PsCreateSystemThread 函数的指针

 

该指针用于在函数 sub_1408A9518(我们方便地将其命名为 Pg_InitMethod3SystemThread)中创建一个新的系统线程,

 

使用函数 sub_1402CD680(在 KI_FILTER_FIBER_PARAM 结构中偏移 0x10,

 

它是验证例程的存根,因此我们方便地将其命名为 Pg_Method3StubToCheckRoutine_sub_1402CD680)作为起始地址。

 

Pg_InitMethod3SystemThread 直接在 KiInitPatchGuardContext 的 0x1408A5B88 处调用。

 

需要注意的一件有趣的事情是添加了优雅的混淆。

 

这个想法是一些绕过用于从 ETHREAD 结构中定位条目 StartAddress 和 Win32StartAddress 以识别 PatchGuard 线程,因此在 Windows 10 中,他们使用通用函数指针修改了这些条目:

 

在线程创建后,PatchGuard 立即获取指向相应的指针ETHREAD(只是说没有锁)并修改 StartAddress 和 Win32StartAddress 两个字段:

 

lea rcx, Pg_FuncArray_off_1408F71E0

 

mov rcx, [rcx+rax8] ; rax is a random value*

 

mov rax, [rsp+0A8h+var_68]

 

mov [rax+ETHREAD_.anonymous_1.anonymous_0.StartAddress], rcx

 

mov [rax+ETHREAD_.Win32StartAddress], rcx

 

为此,它首先获取 0 到 7 之间的随机值,

 

并在内存中偏移 Pg_FuncArray_off_1408F71E0 的数组中获取函数指针。

 

这是这个数组的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
index Function name
 
0   KeBalanceSetManager
 
1   KeSwapProcessOrStack
 
2   ExpWorkerThread
 
3   PopIrpWorker
 
4   FsRtlWorkerThread
 
5   EtwpLogger
 
6   **Pg_Method3StubToCheckRoutine_sub_1402CD680**

只有最后一项是正确的,这意味着 ETHREAD 结构中的 StartAddress Win32StartAddress 文件只有七分之一的机会是正确的。

 

iv - 方法 4 - 异步过程调用

 

第四种方法初始化一个**PatchGuard Context结构和一个APC**结构,并直接将其插入到现有系统线程中。

 

NormalRoutine 参数设置为 xHalTimerWatchdogStop,它实际上只是一个 «ret 0» 指令。

 

KernelRoutine 设置为 KiDispatchCallout,它将以某种方式调用验证例程,而 RundownRoutine 为 NULL。

 

这些参数设置为 0x14089555B(上下文结构中函数指针的初始化)和 0x1408A8734(为 KiInsertQueueApc 调用准备参数)

 

它选择附加到哪个线程的方式是使用带有回调 Pg_IsStartAddressPopIrpWorkerControl_sub_1408A9B70 的 PsEnumProcessThreads 完成的,该任务是查询线程起始地址并将结果与 PopIrpWorkerControl 进行比较。

 

如果要找到此类线程,则将指向 ETHREAD 结构的指针存储在 PatchGuard 上下文结构的偏移量 0x830 处,然后将其复制到提供给 KeInsertQueueApc 的 KAPC 结构中。

 

v - 方法 5 – 挂钩常规 DPC

 

与方法 3(使用系统线程)一样,此方法需要有效的 KI_FILTER_FIBER_PARAM 结构,否则 KiInitPatchGuardContext 将回退到方法 0。

 

对于此方法,使用结构的最后一个条目,即是指向全局变量 KiBalanceSetManagerPeriodicDpc 的指针。

 

该变量包含一个 KDPC 结构,其 DPC 例程在函数 KiInitSystem 中初始化。

 

这种方法的优雅之处在于它实际上是一个合法的 DPC,

 

它由系统每秒左右通过 KeClockInterruptNotify 在 0x1400619b6 处排队;

 

并且 PatchGuard 钩住这个合法的 DPC,

 

以便每 120 个队列(实际上,像许多其他方法一样,120 到 130 次之间的随机值),PatchGuard DPC 排队而不是合法的一个。

 

这是简化此机制代码的图表

 

图片描述

 

如果要对 PatchGuard DPC 进行排队,那么它首先会清除全局 DPC 的副本,并让验证例程在检查结束时将其设置回来。

 

vi - 方法 7 - 新的怪异方法。

 

(不,没有方法6,我对此没有任何解释。)

 

乍一看,这种方法……什么都没有。

 

嗯,几乎没有。

 

它实际上做了两件事。

 

第一件事是它初始化一个要排队的 DPC,但在之后立即清除它,所以永远不要排队。

 

第二个是它初始化一个全局 PatchGuard 上下文结构,该结构将通过系统的全局指针可用。

 

这个全局 PatchGuard 上下文结构实际上在内存中是明文的,并且保留在初始化函数的末尾。

 

在这一部分中,我们将描述我们的发现,特别是未使用的 DPC 的内容。

 

• 未使用的 DPC

 

当索引 7 被赋予 KiInitPatchGuardContext 时,会采用许多特定的分支

 

特别是,初始化 DPC 并将例程定义为 KiInterruptThunk 函数之一或 KiMachineCheckControl 函数之一。

 

KiInterruptThunk 和 KiMachineCheckControl 都是一组 16 个存根,

 

分别对应于函数 FsRtlTruncateSmallMcb 和 KiDecodeMcaFault,

 

它们依次调用检查例程 FsRtlMdlReadCompleteDevEx。

 

在初始化函数 KiInitPatchGuardContext 中,使用的是 KiInterruptThunk 函数,

 

但我们稍后会看到其他 PatchGuard 例程中存在对 KiMachineCheckControl 的一些引用。

 

要使用这个函数数组,会生成一个从 0 到 0xf 的随机值(rdtsc & 0xf),然后用作这些存根中的索引。

 

尽管每个函数有 16 个存根可用,但只有两种不同类型的存根:

 

一种在调用检查例程之前清除 DR7(调试寄存器),

 

另一种则不清除。

 

这是 KiInterruptThunk 函数的两个不同存根

1
2
3
4
5
6
7
8
9
10
11
33 C0 xor eax, eax
 
90 nop
 
90 nop
 
90 nop
 
E9 F6 AF 12 00 jmp FsRtlTruncateSmallMcb
 
66 0F 1F 44 00 00 align 10h
1
2
3
4
5
6
7
33 C0 xor eax, eax
 
0F 23 F8 mov dr7, rax
 
E9 E6 AF 12 00 jmp FsRtlTruncateSmallMcb
 
66 0F 1F 44 00 00 align 10h

由于 NOP 说明,两者或完全相同的尺寸。

 

这两个存根重复 8 次,随机值用于选择其中一个。

 

对于 KiMachineCheckControl 函数,存根几乎相同,

 

不同之处在于调用 KiDecodeMcaFault 而不是 FsRtlTruncateSmallMcb

 

现在,正如我们之前所说,这种方法的问题在于它似乎没有做更多的事情。

 

其他方法通过将 DPC 与计时器耦合或将其放在内存中的某个位置来使用 D**PC**,以便系统可以在某个时间点对其进行排队,但这个没有。

 

这是详细说明我们发现的技术分析。

 

尽管不能证明没有任何路径可以让这个 DPC 排队,但它会展示我们对这种方法的一些研究。

 

技术分析:使用 Reven 作为时间旅行调试器,我们跟踪了这个初始化的执行,找到了为什么没有这个方法的处理程序

 

• 第一次检查:使用 0x10000000 测试标志

 

从随机选择 KiInterruptThunk 存根的块开始,我们在 1408A8308 处发现对标志的检查:

1
test [rsp+2238h+flag_828_on_stack_var_140], 10000000h

让我们分析一下这个标志是从哪里来的。

 

这个标志来自 PatchGuard 上下文,我们可以使用内存历史来找出它来自哪里。

 

通过 reven 的 Memory History 功能检查了几个 memcpy,我们发现该标志的设置在 0x140891B60

 

以下是如何使用内存历史记录来查找此内容的屏幕截图:

 

多次使用相同的方法,这里是标志 0x10000000 的结果摘要。

 

如屏幕截图所示,这是负责设置标志的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
mov ecx, [r14+pg_ctx_rs4.multiple_flag_0x828]
 
mov eax, r12d
 
btr ecx, 1Ch
 
shl eax, 1Ch
 
or ecx, eax
 
mov edx, 2000h
 
mov [r14+pg_ctx_rs4.multiple_flag_0x828], ecx

是根据 r12d 的值来设置的。

 

再次进行时间旅行调试,发现这个寄存器设置为0x140891707:

1
mov r12d, dword ptr [rsp+2238h+var_bIsMethod7_2158]*;*

同样,使用此堆栈内存位置的 Memory History 功能,我们发现它已设置为 140890A13:

1
2
3
4
5
6
7
cmp esi, 7
 
mov rdi, 0CCCCCCCCCCCCCCCDh
 
cmovz ebx, r12d *; r12 = 1*
 
mov dword ptr [rsp+2238h+var_bIsMethod7_2158], ebx

这里 esi 包含调度方法,即方法 7。

 

最后一条数据是 r12,但静态我们很容易发现它被硬编码为 1,与控制流无关。

 

• 第二次检查:使用 0x40000000 测试标志

 

接下来进行另一次检查以确定是否应采用方法调度程序

1
test [rsp+2238h+flag_828_on_stack_var_140], 40000000h

在初始化的记录中,这个标志被设置并且方法分派器没有被执行。

 

使用与 0x10000000 标志相同的机制,我们发现标志 40000000h 的设置在 0x140893BC9

1
2
3
4
5
6
7
8
9
10
11
cmp esi, 7
 
jnz short loc_140893BEC
 
mov eax, [r14+pg_ctx_rs4.multiple_flag_0x828]
 
and eax, 0FBFFFFF7h
 
bts eax, 1Eh
 
mov [r14+pg_ctx_rs4.multiple_flag_0x828], eax

同样,esi包含调度方法,为7。

 

标志直接设置,之后没有修改。

 

• 第三次检查:来自堆栈变量,与方法 7 无关

 

通过跟踪执行跟踪,我们在 0x1408A8C81 处找到另一个最后决定性检查,该检查将决定是否应使用特定参数调用函数 KeSetEvent:

1
2
3
4
5
mov rax, [rsp+2238h+var_21E8]
 
test rax, rax
 
jz short loc_1408A8CAF

进行此跳转并且不调用 KeSetEvent。

 

再次使用 Memory History,我们在 0x1408A5B9F 处找到该堆栈区域的起源:

1
mov [rsp+2238h+ var_bIsMethod7_21E8], r11

如果调度方法为 3 (PsCreateSystemThread) 并且新线程的设置成功,则此内存区域可能不为 NULL .

 

如果是这样,这个堆栈变量包含一个指向给 PsCreateSystemThread 的 StartContext 参数的指针,我们将在后面描述,但基本思想是新线程将等待这个对象,KeSetEvent 会通知它。

 

• 快速结论

 

整个 PatchGuard 上下文在完全归零之后(包括先前选择的 DPC 例程)并且执行正确退出函数。

 

我们展示了这两个第一次检查直接与作为第二个参数传递给 KiInitPatchGuardContext 的调度方法相关联,尽管它不能证明没有路径可以导致方法的真正设置,但它表明没有明显的标志或随机值这样做。

 

• 全局PatchGuard 上下文初始化

 

正如我们之前提到的,当索引7 被赋予KiInitPatchGuardContext 时,全局PatchGuard 上下文结构也被初始化。

 

可以通过位于 0x14045E208 的全局指针访问此全局 PatchGuard 上下文。

 

许多机制是不同的,例如不使用通常的算法而是使用一些 SHA256 相关算法执行的校验和。

 

我们没有具体分析这些机制,因为想法保持不变。

 

对索引为 7 的 KiInitPatchGuardContext 的调用始终发生这一事实很重要,因为这也意味着此初始化很重要,而且事实是此全局 PatchGuard 上下文实际上被其他新方法使用(与 Windows 8.1 相比)。

 

这结束了对 PatchGuard 可以用来初始化上下文的不同方法的描述。

 

在这一点上,我们可以描述给 KiInitPatchGuardContext 的其他参数。

 

vii - KiInitPatchGuardContext:其他参数

 

我们之前说过 KiInitPatchGuardContext 最重要的参数,

 

其中第二个(用作方法的索引)和第四个(指向 KI_FILTER_FIBER_PARAM 的指针,来自 4% 机会函数 ExpLicenseWatchInitWorker)。

 

这小部分是用来描述其他论点的。

 

参数 1DPC 例程指针

 

正如我们所描述的,有几种方法使用 DPC 结构来隐藏 PatchGuard 并在某些时候对其进行排队,重要的是要注意验证例程不是在 DPC 中设置的。

 

DPC 实际上将包含一个指向已知取消 DPC 排队的函数的指针,并且当 DPC 实际上是 PatchGuard 时将执行特定操作。

 

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

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
Index Routine
 
0   CmpEnableLazyFlushDpcRoutine
 
1   ExpCenturyDpcRoutine
 
2   ExpTimeZoneDpcRoutine
 
3   ExpTimeRefreshDpcRoutine
 
4   CmpLazyFlushDpcRoutine
 
5   ExpTimerDpcRoutine
 
6   IopTimerDispatch
 
7   IopIrpStackProfilerDpcRoutine
 
8   KiBalanceSetManagerDeferredRoutine
 
9   PopThermalZoneDpc
 
10   KiTimerDispatch OR KiDpcDispatch
 
11   KiTimerDispatch OR KiDpcDispatch
 
12   KiTimerDispatch OR KiDpcDispatch

对于最后的例程 KiTimerDispatch 和 KiDpcDispatch,如果第二个参数小于 3,则使用 KiTimerDispatch,否则(大于或等于 3)使用 KiDpcDispatch。

 

此选择在 0x1408A50CA 处进行。正如在前面的 KiFilterFiberContext 伪代码中可以看到的那样,

 

第一个参数是随机选择的,除了最后一次调用 KiInitPatchGuardContext 时它是 0 - CmpEnableLazyFlushDpcRoutine,但我们会看到在这种情况下它没有被初始化例程使用。

 

在 0x1408A5AA9 附近可以看到这 12 个例程之间的切换。

 

• 参数3:确定要检查的数据的总大小的随机值

 

该随机值可以是1 或2(如在KiFilterFiberContext 中所见)。

 

它用于划分硬编码值 0x140000,并将结果立即设置到偏移量 0x6cc 处的 PatchGuard 上下文结构中。

 

此值用于确定每次 PatchGuard 检查时校验和的最大数据大小(以字节为单位)。

 

主要思想是 PatchGuard 使用一个结构列表来检查完整性,并且在每次校验和之后,计数器会根据数据的大小递增。

 

虽然检查的数据总量小于先前定义的最大值,PatchGuard 继续下一个结构以检查其列表。

 

该机制将在验证例程部分中更详细地解释。

 

• 参数5:用于ntosrknl 函数完整性检查的布尔值

 

此参数是一个布尔值,用于决定是否应执行ntoskrnl 函数校验和。

 

检查在 140894183 完成:

1
2
3
4
5
6
7
mov eax, [rsp+2238h+arg_20_var_2140]
 
and eax, r13d*; r13 is hardcoded to 1*
 
mov dword ptr [rsp+2238h+arg20_copy_var_2170], eax
 
jz loc_1408943C4

校验和结果随后存储在 PatchGuard 上下文中,与 PatchGuard 要检查的每个其他 Windows 内核结构一样。

 

在 KiFilterFiberParam 中,可以看到该参数仅在第一次调用 KiInitPatchGuardContext 时为 True。

 

到此为止可能来自 KiInitPatchGuardContext 的初始化方法,现在我们将描述直接初始化的其他方法,或者根本不使用任何上下文结构。

 

b - « TV » 回调,第一次将 PatchGuard 链接到 mssecflt.sys

 

KiFilterFiberContext 是一个相当小的函数,我们可以很容易地看到回调的通知。

 

在 ntoskrnl 中找不到这个回调,

 

但我们可以看到它以函数指针(sub_1401825A0,重命名为 Pg_TVCallback_CheckRoutine_sub_1401825A0)作为参数。

 

可能很难找到它的来源。

 

从 KiFilterFiberContext 函数中,我们注意到没有调用 ExRegisterCallback,这意味着对象回调已经存在并且之前在引导期间已创建

 

通过timeless分析,我们立即发现这个回调是在函数 SecInitializeKernelIntegrityCheck 的二进制 mssecflt.sys 中初始化的

 

回调函数名为 SecKernelIntegrityCallback。

 

它在 SecInitializeKernelIntegrityCheck 中初始化,直接从 mssecflt.sys 的驱动程序入口例程调用。

 

这是 SecInitializeKernelIntegrityCheck 的调用堆栈(您也可以在上面的屏幕截图中看到),

 

这表明它来自 IoInitSystem 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SecInitializeKernelIntegrityCheck
 
mssecflt.sys DriverEntry
 
_guard_dispatch_icall
 
IopInitializeBuiltinDriver
 
PnpInitializeBootStartDriver
 
PipInitializeCoreDriversByGroup
 
PipInitializeCoreDriversAndElam
 
IopInitializeBootDrivers
 
IoInitSystemPreDrivers
 
IoInitSystem

回调函数本身就是 SecKernelIntegrityCallback。

 

这是一个非常小的例程,只需将函数指针放入全局变量:

1
2
3
4
5
6
7
[...] *// Tracing and Logging related actions*
 
 g_qword_1C0013428 = &Pg_TVCallback_CheckRoutine_sub_1401825A0; *// Pointer from the notification*
 
 *// function argument*
 
 *KpgApiConsumerRanges = SecProtectedRanges;

我们还可以看到,它将全局变量 KpgApiConsumerRanges(作为参数传递)的值设置为 SecProtectedRanges。

 

快速查看 Pg_TVCallback_CheckRoutine_sub_1401825A0 表明它是 PatchGuard 检查例程之一,

 

因为它看起来非常像 FsRtlMdlReadCompleteDevEx。

 

但是可以注意到一个区别:调度方法不会在例程结束时重置。

 

对于这个方法,除了这个回调之外没有特定的初始化,正如我们前面提到的,它使用全局 PatchGuard 上下文结构。

 

本文稍后将详细介绍如何调用此函数。

 

c - KiSwInterruptDispatch

 

和回调方法一样,这个方法本身并没有初始化,因为它使用了方法 7 中的全局 PatchGuard 上下文结构。

 

它也是一个新方法,由 IDT 函数 KiSwInterrupt 函数调用。

 

我们将在本文后面描述它的触发机制。

 

我们可以在 KiFilterFiberContext 中看到一些对 KiSwInterrupt 的引用,它们是相关的。

 

d - 拾遗:**CcInitializeBcbProfiler**

 

PatchGuard 使用隐藏的方式 CcInitializeBcbProfiler 执行检查。

 

该函数首先计算随机 ntoskrnl 例程的校验和。

 

然后它使用例程 CcBcbProfiler 设置 DPC,并在 DPC 中添加一些额外数据。

 

这是作为参数传递的结构:

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
struct pg_CcInitializeBcbProfiler
 
{
 
 KDPC_ kdpc;
 
 KTIMER timer;
 
 ULONG64 res_RtlpLookupPrimaryFunctionEntry_0x80; *// 0D1B71759*
 
 ULONG64 hardcoded_140000000h_0x88;
 
 ULONG32 func_size_0x90;
 
 ULONG32 padding_0x94;
 
 ULONG64 checksum_function_0x98;
 
 ULONG64 random_1_0xa0;
 
 ULONG32 random_2_0xa8;
 
 ULONG32 bool_CcBcbProfiler_or_sub_140499010_0xac;
 
 ULONG64 bKiAreCodePatchesAllowed_0xb0;
 
 struct _LIST_ENTRY_ workitem_List_0xb8;
 
 void* workitem_WorkerRoutine_psub_140499010_0xc8 */\* function \*/*;
 
 void* workitem_Parameter_pCurrentStruct_0xd0;
 
};

请注意,此结构包含再次计算随机例程校验和的所有内容

 

• 指向函数条目的指针

 

• 映像的基地址(添加到 RVA 以获得 VA)

 

• 函数的大小

 

• 校验和

 

• 使用的随机值校验和的种子

 

DPC 与 KeSetCoalescableTimer 一起排队,就像在初始化函数中一样,DueTime 设置在 2' 和 2'10" 之间。

 

接下来,例程 CcBcbProfiler 或者将带有 sub_1404099010 的参数中的工作项排队(我们为方便起见为它重命名 Pg_CcBcbProfilerTwin_sub_140499010)为WorkerRoutine,或者继续执行。

 

除了 WorkItem 部分,例程 Pg_CcBcbProfilerTwin_sub_1404099010 和 CcBcbProfiler 几乎相同,主要目的是执行随机 ntoskrnl 函数的完整性检查,并将结果与存储在结构中的结果进行比较。

 

这两个函数之后再次使用 KeSetCoalescableTimer 设置计时器。

 

e - 拾遗:PspProcessDelete

 

某些完整性验证也可以在特定的地方找到,例如 PspProcessDelete。

 

这个函数不仅仅是删除中间的进程,还会对 KeServiceDescriptorTable 及其影子孪生 KeServiceDescriptorTableShadow 执行完整性检查。

 

这种完整性检查是独立的,因为它不需要任何 PatchGuard 上下文结构或专用线程。

 

这只是可以在系统代码中间找到的一小部分验证。

 

请注意,两个表的原始校验和以及初始化向量和计算校验和所需的移位值都在全局变量中可用,如果攻击者想要修补描述符表的条目(阴影与否) ),然后再次计算校验和并替换原来的校验和是完全可行的。

 

此校验和发生在 0x1401ecd55 生成的随机值,使用 KiQueryUnbiaisedInterruptTime,因此它不会启动太多次(间隔尚未反转,但我们可以看到结果是通过添加 288e9 和随机值计算得出的)。

 

该定时器存储在 0x1403DB100。这些结构的校验和结果存储在 0x1403DB108、0x1403DB110 和 0x1403DB118。

 

IV存储在0x1403DB0F0,移位值存储在0x1403DB0F8。

 

如果其中一个校验和失败,则通过插入 KiSchedulerDpc 的 Dpc 触发 KeBugCheck。

 

这些校验和的初始化在 CmpInitDelayRefKCBEngine 中执行。

 

要禁用此方法,只需将计时器修补为无穷大或再次计算修改表的校验和(并使其挂钩由 PatchGuard 保护,这很好)。

 

f - 拾遗: KiInitializeUserApc

 

就像 PspProcessDelete 一样,这个函数隐藏了一段自主代码来检查 IDT 的完整性

 

定义是否应执行检查的定时器存储在 0x1403DB1C0,

 

IV 存储在 0x1403DB1B0,

 

位移值存储在 0x1403DB1B0。

 

原始校验和存储在 0x1403DB1B8。

 

同样,如果检测到修改,代码会使用 KiSchedulerDpc 注入一个 DPC,该 DPC 将调用 KeBugCheck。

 

就像 PspProcessDelete 的情况一样,要禁用此方法,只需将计时器设置为无穷大或再次计算修改后的 IDT 的校验和(并使其挂钩由 PatchGuard 保护,这很好)。

 

g - KiInitPatchGuardContext 的其他调用

 

通过交叉引用,可以从 KiVerifyXcpt15 的异常处理程序中看到对 KiInitPatchGuardContext 的其他调用。

 

该例程属于一个名为 KiVerifyXcptRoutines 的函数指针数组,它在 KiVerifyScopesExecute 中被多次调用(由常量 KiVerifyPass,0xA 定义)。

 

这个方法还没分析太多,关键是KiInitPatchGuardContext是用方法0来创建上下文的(用KeSetCoalescableTimer注入的定时器),所以没有新的方法去禁用


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2022-8-19 21:28 被kanxue编辑 ,原因:
上传的附件:
收藏
点赞12
打赏
分享
最新回复 (8)
雪    币: 6
活跃值: (2895)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
咖啡_741298 2022-7-14 17:35
2
0
谢谢分享,很详细
雪    币: 12837
活跃值: (8993)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
hzqst 3 2022-7-14 20:14
3
0
怎么看的这么难受呢,机翻的?
雪    币: 285
活跃值: (3054)
能力值: ( LV5,RANK:75 )
在线值:
发帖
回帖
粉丝
囧囧 2022-7-15 10:40
4
0
感谢分享
雪    币: 2251
活跃值: (2148)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
LexSafe 2022-7-18 09:14
5
0
Timeless 3900刀 
雪    币: 108
活跃值: (863)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
yangya 2022-7-18 09:59
6
0
hzqst 怎么看的这么难受呢,机翻的?
人翻的,过多直译了,比机翻还难看。
雪    币: 3350
活跃值: (3372)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 1 2022-7-18 10:30
7
0
感谢分享!
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_vmkgkbrm 2022-7-27 22:18
8
0
hzqst 怎么看的这么难受呢,机翻的?
大表哥牛逼
雪    币: 1414
活跃值: (4180)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Oxygen1a1 2022-8-17 19:31
9
0
这附件也没翻译啊
游客
登录 | 注册 方可回帖
返回