早上好!在今天的博客中,我们将探讨 Windows 系统最强大的保护机制之一:PatchGuard,也被称为 KPP(内核补丁保护,Kernel Patch Protection)。
我会将这篇博客分为几个部分。第一部分将从理论角度阐述这项缓解技术的原理;第二部分将深入探讨其内部机制、它所带来的影响,以及为何逆向工程它如此困难;最后,我们将探讨可能的绕过方法。
注:本博客中的所有分析均基于 Windows 11 24H2 版本(操作系统版本号 26100)进行。
理论视角
基本概念
PatchGuard(又称 KPP,内核补丁保护)是 2005 年在 64 位版本的 Windows Vista 和 Server 2003 中引入的一项缓解技术。这是一种极其复杂却又极为有效的缓解手段。
本质上,它是内核的一个关键组成部分,和内核的其他部分一样运行在 Ring 0 权限级别。它并非那种拥有比 Ring 0 代码更高权限的 Ring -1 机制。它的主要目的是检查关键的内核结构和代码,确保它们未被篡改。如果检测到任何未经授权的修改,它将触发一个蓝屏死机(BSOD)错误,错误代码为 CRITICAL_STRUCTURE_CORRUPTION,错误检查代码为 0x109,不给任何出错或混淆的余地。它明确表明 Ring 0 权限级别中有本不应被修改的内容被修改了。
举例:PatchGuard 就像一个房间里极其不稳定的运动传感器。有时传感器会完全关闭,但时不时地,它会激活(而且你无法预知激活时间)。一旦激活,如果它检测到有超出预期的运动迹象,传感器就会“爆炸”,让整个房间化为乌有,并强制系统重启(即蓝屏死机)。
注意:本文不会讨论 Hyperguard,但你可以想象,这个传感器不再位于房间里。相反,它位于你上方一个玻璃墙的控制室中,从外部观察一切,并以 4K 分辨率进行记录。
PatchGuard 的有趣之处在于,它完全采用异步运行机制,这使得我们根本无法确定它何时会对关键结构和代码进行检查。
它所检查的内容大致可以总结为以下列表:
中断描述符表(IDT,Interrupt Descriptor Table)与全局描述符表(GDT,Global Descriptor Table)
GDT: 全局描述符表是 IA-32 和 x86-64 架构特有的二进制数据结构。它包含的条目用于向 CPU 说明内存段的相关信息。
IDT: 中断描述符表也是 IA-32 和 x86-64 架构特有的二进制数据结构。它是保护模式和长模式下与实模式中断向量表(IVT,Interrupt Vector Table)对应的结构,用于告知 CPU 中断服务例程(ISR,Interrupt Service Routines)的位置(每个中断向量对应一个)。
模型特定寄存器(MSR,Model Specific Registers):CPU 寄存器,用于控制高级行为,如功能特性、限制条件以及执行流程管理。 系统服务描述符表(SSDT,System Service Descriptor Table):该表包含指向实现系统调用(如 NtCreateFile、NtOpenProcess 等)的内核函数的指针。 内核栈(Kernel Stacks) 内核结构(Kernel Structures) 全局变量(Global Variables) KPP 引擎(KPP engine)(你无法对 KPP 的整个实现进行补丁修改)
初始化
每款软件都有其初始化过程,PatchGuard 也不例外。
正如佐藤丹达(Satoshi Tanda)在他的博客《分析 PatchGuard 的一些技巧》 中所恰当指出的那样,我们要在 ntoskrnl.exe 中寻找最大的函数。
sub_140BD3620():
我们可以看到,这个函数所做的第一件事就是检查是否已附加调试器,如果已附加,PatchGuard 将不会激活。
如果我们列出交叉引用(即调用该函数的其他位置),会发现它是由另一个函数调用的:
sub_140BFABF0():
如图所示,这个函数非常简短,但它使用了一个指针来获取传递给我们的主函数 sub_140BD3620() 的参数。
以下是伪代码:
void __fastcall sub_140BFABF0(_BYTE *Parameter)
{
Parameter[28] = sub_140BD3620(
*(_DWORD *)Parameter,
*((_DWORD *)Parameter + 1),
*((_DWORD *)Parameter + 2),
*((_QWORD *)Parameter + 2),
*((_DWORD *)Parameter + 6));
} 复制代码 为了简化表述,在本文接下来的内容中,我们将对以下函数进行重命名:将 sub_140BD3620() 重命名为 PgInitialization(),将 sub_140BFABF0() 重命名为 PgWrapper2PgInit 。
我们可以看到,从函数 KiFilterFiberContext() 中,我们找到了对负责初始化 PatchGuard 的包装函数的调用引用。这个函数(KiFilterFiberContext())在参与这项臭名昭著的缓解技术初始化方面是众所周知的。
(尽管我们也看到 KeCheckedKernelInitialize() 中存在对该函数的调用)
那么接下来,我们将对 KiFilterFiberContext() 函数进行分析。
KiFilterFiberContext()
_BOOL8 __fastcall KiFilterFiberContext(__int64 a1)
{
NTSTATUS v2; // r12d
unsigned __int64 v3; // rax
unsigned __int128 v4; // rax
unsigned __int64 v5; // rbx
unsigned __int64 v6; // rax
unsigned __int128 v7; // rax
__int64 v8; // r9
unsigned __int64 v9; // r10
unsigned __int128 v10; // rax
unsigned __int64 v11; // r15
NTSTATUS v12; // eax
char v13; // di
unsigned __int64 v14; // rax
unsigned __int128 v15; // rax
int v16; // r8d
unsigned __int64 v17; // rax
unsigned __int128 v18; // rax
NTSTATUS v19; // eax
char v20; // cl
int v21; // eax
NTSTATUS v22; // eax
char v23; // cl
int v24; // ecx
__int64 *v25; // rax
__int64 v26; // rdx
_DWORD Parameter[4]; // [rsp+40h] [rbp-89h] BYREF
__int64 v29; // [rsp+50h] [rbp-79h]
int v30; // [rsp+58h] [rbp-71h]
char v31; // [rsp+5Ch] [rbp-6Dh]
_DWORD v32[4]; // [rsp+60h] [rbp-69h] BYREF
__int64 v33; // [rsp+70h] [rbp-59h]
int v34; // [rsp+78h] [rbp-51h]
char v35; // [rsp+7Ch] [rbp-4Dh]
_DWORD v36[4]; // [rsp+80h] [rbp-49h] BYREF
__int64 v37; // [rsp+90h] [rbp-39h]
int v38; // [rsp+98h] [rbp-31h]
char v39; // [rsp+9Ch] [rbp-2Dh]
__int64 v40; // [rsp+A0h] [rbp-29h]
__int64 v41; // [rsp+A8h] [rbp-21h]
OBJECT_ATTRIBUTES ObjectAttributes; // [rsp+B0h] [rbp-19h] BYREF
PCALLBACK_OBJECT CallbackObject; // [rsp+130h] [rbp+67h] BYREF
__int64 v44; // [rsp+138h] [rbp+6Fh]
__int64 v45; // [rsp+140h] [rbp+77h]
__int64 v46; // [rsp+148h] [rbp+7Fh]
v2 = KdDisableDebugger();
KeKeepData(KiFilterFiberContext);
_disable();
if ( !(_BYTE)KdDebuggerNotPresent )
{
while ( 1 )
;
}
_enable();
v3 = __rdtsc();
v4 = (__ROR8__(v3, 3) ^ v3) * (unsigned __int128)0x7010008004002001uLL;
v44 = *((_QWORD *)&v4 + 1);
v5 = ((unsigned __int64)v4 ^ *((_QWORD *)&v4 + 1)) % 0xA;
if ( !*(_QWORD *)&MaxDataSize && !a1 && !__2c )
{
if ( PsIntegrityCheckEnabled )
{
ObjectAttributes.Length = 0x30;
ObjectAttributes.ObjectName = (PUNICODE_STRING)L"TV";
ObjectAttributes.RootDirectory = 0;
ObjectAttributes.Attributes = 0x40;
*(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0;
if ( ExCreateCallback(&CallbackObject, &ObjectAttributes, 0, 0) >= 0 )
{
ExNotifyCallback(CallbackObject, sub_140510650, &__24);
ObfDereferenceObject(CallbackObject);
if ( __24 )
__2c = 1;
ExInitializeNPagedLookasideList(&stru_140E0EF80, 0, 0, 0x200u, 0xB38u, 0x746E494Bu, 0);
}
}
}
v6 = __rdtsc();
v7 = (__ROR8__(v6, 3) ^ v6) * (unsigned __int128)0x7010008004002001uLL;
v45 = *((_QWORD *)&v7 + 1);
v8 = v7;
*(_QWORD *)&v7 = __rdtsc();
v9 = v8 ^ *((_QWORD *)&v7 + 1);
Parameter[2] = (v5 < 6) + 1;
v29 = a1;
v30 = 1;
v31 = 0;
v10 = (__ROR8__(v7, 3) ^ (unsigned __int64)v7) * (unsigned __int128)0x7010008004002001uLL;
v46 = *((_QWORD *)&v10 + 1);
v11 = ((unsigned __int64)v10 ^ *((_QWORD *)&v10 + 1)) % 6;
Parameter[1] = v11;
Parameter[0] = v9 % 0xD;
v12 = KeExpandKernelStackAndCallout((PEXPAND_STACK_CALLOUT)Wrapper2PgInit, Parameter, 0xC000u);
v13 = v31;
if ( v12 < 0 )
v13 = 0;
v31 = v13;
if ( v13 )
{
if ( v5 >= 6 )
goto LABEL_21;
v14 = __rdtsc();
v15 = (__ROR8__(v14, 3) ^ v14) * (unsigned __int128)0x7010008004002001uLL;
v40 = *((_QWORD *)&v15 + 1);
v16 = ((unsigned __int64)v15 ^ *((_QWORD *)&v15 + 1)) % 0xD;
do
{
v17 = __rdtsc();
v18 = (__ROR8__(v17, 3) ^ v17) * (unsigned __int128)0x7010008004002001uLL;
v41 = *((_QWORD *)&v18 + 1);
}
while ( (_DWORD)v11 && ((unsigned __int64)v18 ^ *((_QWORD *)&v18 + 1)) % 6 == (_DWORD)v11 );
v32[0] = v16;
v32[1] = ((unsigned __int64)v18 ^ *((_QWORD *)&v18 + 1)) % 6;
v32[2] = (v5 < 6) + 1;
v33 = a1;
v34 = 0;
v35 = 0;
v19 = KeExpandKernelStackAndCallout((PEXPAND_STACK_CALLOUT)Wrapper2PgInit, v32, 0xC000u);
v20 = v35;
if ( v19 < 0 )
v20 = 0;
v35 = v20;
v13 = v20;
if ( v20 )
{
LABEL_21:
if ( *(_QWORD *)&MaxDataSize )
goto LABEL_29;
if ( a1 )
goto LABEL_37;
if ( (int)KiSwInterruptPresent() < 0 && !__2c )
{
LABEL_30:
if ( qword_141006660 )
ExFreePool(qword_141006660);
v24 = 24;
v25 = &__25;
v26 = 3;
do
{
*v25 = 0;
v24 -= 8;
++v25;
--v26;
}
while ( v26 );
for ( ; v24; --v24 )
{
*(_BYTE *)v25 = 0;
v25 = (__int64 *)((char *)v25 + 1);
}
__2e = 0;
__26 = 0;
__27 = 0;
dword_140E0EEC0 = 0;
qword_141006080 = 0;
goto LABEL_37;
}
v36[0] = 0;
v36[1] = 7;
v36[2] = 1;
v37 = 0;
v21 = KiSwInterruptPresent();
v39 = 0;
v38 = (v21 >> 31) & 8;
v22 = KeExpandKernelStackAndCallout((PEXPAND_STACK_CALLOUT)Wrapper2PgInit, v36, 0xC000u);
v23 = v39;
if ( v22 < 0 )
v23 = 0;
v39 = v23;
v13 = v23;
}
if ( !v13 )
goto LABEL_37;
LABEL_29:
if ( a1 )
goto LABEL_37;
goto LABEL_30;
}
LABEL_37:
_disable();
if ( !(_BYTE)KdDebuggerNotPresent )
{
while ( 1 )
;
}
_enable();
_disable();
_enable();
if ( v2 >= 0 )
KdEnableDebugger();
return v13 != 0;
} 复制代码
进入这个函数后,我们看到如下伪代码:
__int64 KeInitAmd64SpecificState()
{
__int64 result; // rax
_mm_lfence();
if ( *(_QWORD *)&HvlpVsmVtlCallVa || !(_DWORD)InitSafeBootMode )
return (unsigned int)(__ROR4__((unsigned __int8)KdPitchDebugger | (unsigned __int8)KdDebuggerNotPresent, 1)
/ (((unsigned __int8)KdPitchDebugger | (unsigned __int8)KdDebuggerNotPresent) != 0 ? -1 : 17));
return result;
} 复制代码 乍一看,似乎没有直接引用到 KiFilterFiberContext() —— 但如果我们查看反汇编代码……
进入异常处理程序后,我们看到如下内容:
如图所示,该调用确实是在那个 __except() 块内完成的。
但让我们聚焦核心问题:KiFilterFiberContext() 是什么?
它是 PatchGuard 初始化过程中的一个关键函数,在 Windows 启动期间会被调用两次。其中一次调用是在 KeInitAmd64SpecificState() 内部的一个异常处理程序(__except())中完成的。 该函数的激活是通过在 KeInitAmd64SpecificState() 起始处强制触发一个错误来实现的,在此过程中会用到 KdDebuggerNotPresent() 和 KdPitchDebugger() 函数。
上下文信息
PatchGuard 上下文是一个大型内存结构,用于监控受 PatchGuard 保护的内核结构。一些研究人员将此定义扩展为,把检查方法也涵盖在内。因此,狭义的定义仅指该结构本身,而广义的定义则既包括该结构,也包括 PatchGuard 用于初始化和验证的方法。
第一部分
PatchGuard 会将 CmpAppendDllSection 函数的代码复制到自身的结构中,并利用该代码通过与随机密钥进行异或(XOR)操作来解密其余部分。具体可参见以下伪代码:
__int64 __fastcall CmpAppendDllSection(_QWORD *a1, __int64 a2)
{
_QWORD *v2; // rcx
__int64 v3; // rax
_QWORD *v4; // rdx
__int64 v5; // rcx
__int64 v6; // rax
__int64 v7; // rax
*a1 ^= a2;
a1[1] ^= a2;
a1[2] ^= a2;
a1[3] ^= a2;
a1[4] ^= a2;
a1[5] ^= a2;
a1[6] ^= a2;
a1[7] ^= a2;
a1[8] ^= a2;
a1[9] ^= a2;
a1[10] ^= a2;
a1[11] ^= a2;
a1[12] ^= a2;
a1[13] ^= a2;
a1[14] ^= a2;
a1[15] ^= a2;
v2 = a1 + 15;
v2[1] ^= a2;
v2[2] ^= a2;
v2[3] ^= a2;
v2[4] ^= a2;
v2[5] ^= a2;
v2[6] ^= a2;
v2[7] ^= a2;
v2[8] ^= a2;
v2[9] ^= a2;
v2 -= 15;
*(_DWORD *)v2 ^= a2;
v3 = a2;
v4 = v2;
v5 = *((unsigned int *)v2 + 49);
if ( v3 )
{
do
{
v4[v5 + 24] ^= v3;
v6 = __ROR8__(v3, v5);
v3 = v6 ^ (1LL << v6);
--v5;
}
while ( v5 );
}
v7 = ((__int64 (__fastcall *)(__int64))((char *)v4 + *((unsigned int *)v4 + 514)))(v5);
return (*(__int64 (__fastcall **)(__int64, __int64))(v7 + 288))(v7 + 1976, 1);
} 复制代码 存在对 KiWaitAlways 和 KiWaitNever 等全局变量的引用,这些变量用于在 PatchGuard 执行延迟过程调用(DPC,Deferred Procedure Call)期间对指针进行编码或解码。
在这里,我们看到对 KiWaitAlways 的引用:
继续向下滚动查看,我们还发现了来自 PgInit 的引用:
许多来自 ntoskrnl.exe 的指针也被复制到了 PatchGuard 上下文中,这使得 PatchGuard 能够在不依赖内核导出表的情况下调用函数。
第二部分
此阶段会收集后续将用到的数据,例如页表项(PTE entries)、来自 ntoskrnl 和硬件抽象层(HAL,Hardware Abstraction Layer)的例程,以及其他关键的内核结构。
第三部分
第三阶段包含一个结构体数组,其中每个结构体负责一项特定的验证工作,例如:
每个结构体包含以下内容:
一个 KeBugCheckType 字段,用于指示检查类型 一个指向待验证数据的指针 数据大小 一个参考校验和(在初始化期间计算得出)
上下文初始化
KiInitPatchGuardContext 使用多种方法来初始化 PatchGuard 检查。
(所有内容均参考自 《对微软 Windows 10 RS4 版 PatchGuard 的更新分析》 )
方法 1:使用与 DPC(延迟过程调用,Deferred Procedure Call)结构相关联的计时器。PatchGuard 会初始化上下文和 DPC,然后通过 KeSetCoalescableTimer 将它们集成在一起,该计时器会在设置后的 2 到 130 秒内触发,随机延迟容差在 0 到 0.001 秒之间。由于该计时器不是周期性的,因此必须在检查例程结束时重置它。
方法 2 和方法 3:通过将 DPC 直接隐藏在内核的处理器区域控制块(PRCB,Processor Region Control Block)结构中,从而避免使用计时器。如果传递给 KiInitPatchGuardContext 的第二个参数为 1 或 2,则会初始化一个上下文和 DPC,并将它们隐藏在特定的 PRCB 字段中,依靠合法的系统函数来将 DPC 加入队列。
方法 2(利用 AcpiReserved 字段):DPC 指针被隐藏在此处,并通过 HalpTimerDpcRoutine 加入执行队列,该例程至少每 2 分钟触发一次。它使用 HalpTimerLastDpc 基于全局系统运行时间变量来跟踪上一次事件。此事件通常由 ACPI 状态转换(例如,进入空闲状态)触发。 方法 3(利用 HalReserved 字段):与方法 2 类似,但将指针存储在 HalReserved 字段中。它由 HalpMcaQueueDpc 在 HAL 时钟中断期间(例如 HalpTimerClockInterrupt)加入执行队列。此字段还可能包含一个指向 KI_FILTER_FIBER_PARAM 结构的指针,该结构由 ExpLicenseWatchInitWorker 中的 KiFilterFiberContext 使用。
方法 4:以 4% 的概率创建一个新的系统线程,使用 KI_FILTER_FIBER_PARAM 结构。该结构包含一个指向 PsCreateSystemThread 的指针,用于生成该线程。StartAddress 指向一个执行验证操作的函数。作为一种混淆手段,一旦线程创建完成,线程环境块(ETHREAD)中的 StartAddress 和 Win32StartAddress 字段会被覆盖为常见的函数指针。这些指针会从包含八个元素的数组中随机选择,其中仅有一个是有效的。
方法 5:需要一个有效的 KI_FILTER_FIBER_PARAM 结构。如果该结构不可用,则回退到方法 0。此方法利用该结构中的最后一个条目——一个指向全局变量 KiBalanceSetManagerPeriodicDpc 的指针,该指针包含一个在 KiInitSystem 中初始化的 KDPC 结构。PatchGuard 会挂钩这个合法的 DPC,该 DPC 每秒通过 KeClockInterruptNotify 运行一次。每执行 120 到 130 次后,会改为排队执行 PatchGuard 的 DPC。它会清除全局副本,并在验证例程完成后允许其重置该副本。
KiFilterFiberContext() 中的“TV”回调机制
回到 KiFilterFiberContext() 函数,值得一提的是其中涉及一个在 ntoskrnl.exe 中并不存在的回调函数:
有趣的关键例程
为了更深入地理解 PatchGuard 那些不同寻常的内部运作机制,让我们来剖析一些除已讨论过的函数之外的其他关键函数。
KeBugCheck()
该函数是 KeBugCheckEx 的封装函数:
void __stdcall __noreturn KeBugCheck(ULONG BugCheckCode)
{
ULONG_PTR v1; // rdx
ULONG_PTR v2; // r8
ULONG_PTR v3; // r9
ULONG_PTR v4; // [rsp+20h] [rbp-8h]
KeBugCheckEx(BugCheckCode, v1, v2, v3, v4);
} 复制代码 KeBugCheckEx()
KeBugCheckEx 的目标是调用 KeBugCheck2,不过它并非一个简单的封装函数。它会对参数执行多项检查,并从上下文(Context)中提取相关值,但最终仍会调用 KeBugCheck2:
// local variable allocation has failed, the output may be wrong!
void __stdcall __noreturn KeBugCheckEx(
ULONG BugCheckCode,
ULONG_PTR BugCheckParameter1,
ULONG_PTR BugCheckParameter2,
ULONG_PTR BugCheckParameter3,
ULONG_PTR BugCheckParameter4)
{
_CONTEXT *Context; // r10
char **v6; // r8
void *v7; // r9
signed __int8 CurrentIrql; // al
__int64 v9; // [rsp+30h] [rbp-8h]
char *retaddr; // [rsp+38h] [rbp+0h] BYREF
unsigned __int64 var_BugCheckCode; // [rsp+40h] [rbp+8h]
int var_BugCheckParameter1; // [rsp+48h] [rbp+10h]
int var_BugCheckParameter2; // [rsp+50h] [rbp+18h]
int var_BugCheckParameter3; // [rsp+58h] [rbp+20h]
char v15; // [rsp+68h] [rbp+30h] BYREF
var_BugCheckCode = *(_QWORD *)&BugCheckCode;
var_BugCheckParameter1 = BugCheckParameter1;
var_BugCheckParameter2 = BugCheckParameter2;
var_BugCheckParameter3 = BugCheckParameter3;
_disable();
RtlCaptureContext(KeGetCurrentPrcb()->Context);
KiSaveProcessorControlState(&KeGetCurrentPrcb()->ProcessorState);
Context = KeGetCurrentPrcb()->Context;
Context->Rcx = var_BugCheckCode;
*(_QWORD *)&Context->EFlags = v9;
if ( &byte_1403FDFD9 == retaddr )
{
v6 = (char **)&v15;
v7 = KeBugCheck;
}
else
{
v6 = &retaddr;
v7 = KeBugCheckEx;
}
Context->Rsp = (unsigned __int64)v6;
Context->Rip = (unsigned __int64)v7;
CurrentIrql = KeGetCurrentIrql();
__writegsbyte(0x8018u, CurrentIrql);
if ( CurrentIrql < 2 )
__writecr8(2u);
if ( (v9 & 0x200) != 0 )
_enable();
_InterlockedIncrement(&KiHardwareTrigger);
if ( &byte_1403FDFD9 != retaddr )
KeBugCheck2(
var_BugCheckCode,
var_BugCheckParameter1,
var_BugCheckParameter2,
var_BugCheckParameter3,
BugCheckParameter4,
0);
KeBugCheck2(var_BugCheckCode, 0, 0, 0, 0, 0);
} 复制代码 这个函数在诸如 KiInitializeKernel 等关键例程中被引用:
KeBugCheck2()
这是该调用链中的最终函数。
没错,就是这个。鉴于其代码规模较大,我们参考一个研究 Windows 内部机制的最佳资料来源:ReactOS 项目。
以下是 KeBugCheckEx 的代码逻辑,它会调用 KeBugCheckWithTf:
DECLSPEC_NORETURN
VOID
NTAPI
KeBugCheckEx(IN ULONG BugCheckCode,
IN ULONG_PTR BugCheckParameter1,
IN ULONG_PTR BugCheckParameter2,
IN ULONG_PTR BugCheckParameter3,
IN ULONG_PTR BugCheckParameter4)
{
/* Call the internal API */
KeBugCheckWithTf(BugCheckCode,
BugCheckParameter1,
BugCheckParameter2,
BugCheckParameter3,
BugCheckParameter4,
NULL);
} 复制代码 这段代码显然已过时,且可能存在不准确之处,但它能让我们对这一关键的 PatchGuard 函数有扎实的理解:
DECLSPEC_NORETURN
VOID
NTAPI
KeBugCheckWithTf(IN ULONG BugCheckCode,
IN ULONG_PTR BugCheckParameter1,
IN ULONG_PTR BugCheckParameter2,
IN ULONG_PTR BugCheckParameter3,
IN ULONG_PTR BugCheckParameter4,
IN PKTRAP_FRAME TrapFrame)
{
PKPRCB Prcb = KeGetCurrentPrcb();
CONTEXT Context;
ULONG MessageId;
CHAR AnsiName[128];
BOOLEAN IsSystem, IsHardError = FALSE, Reboot = FALSE;
PCHAR HardErrCaption = NULL, HardErrMessage = NULL;
PVOID Pc = NULL, Memory;
PVOID DriverBase;
PLDR_DATA_TABLE_ENTRY LdrEntry;
PULONG_PTR HardErrorParameters;
KIRQL OldIrql;
/* Set active bugcheck */
KeBugCheckActive = TRUE;
KiBugCheckDriver = NULL;
/* Check if this is power failure simulation */
if (BugCheckCode == POWER_FAILURE_SIMULATE)
{
/* Call the Callbacks and reboot */
KiDoBugCheckCallbacks();
HalReturnToFirmware(HalRebootRoutine);
}
/* Save the IRQL and set hardware trigger */
Prcb->DebuggerSavedIRQL = KeGetCurrentIrql();
InterlockedIncrement((PLONG)&KiHardwareTrigger);
/* Capture the CPU Context */
RtlCaptureContext(&Prcb->ProcessorState.ContextFrame);
KiSaveProcessorControlState(&Prcb->ProcessorState);
Context = Prcb->ProcessorState.ContextFrame;
/* FIXME: Call the Watchdog if it's registered */
/* Check which bugcode this is */
switch (BugCheckCode)
{
/* These bug checks already have detailed messages, keep them */
case UNEXPECTED_KERNEL_MODE_TRAP:
case DRIVER_CORRUPTED_EXPOOL:
case ACPI_BIOS_ERROR:
case ACPI_BIOS_FATAL_ERROR:
case THREAD_STUCK_IN_DEVICE_DRIVER:
case DATA_BUS_ERROR:
case FAT_FILE_SYSTEM:
case NO_MORE_SYSTEM_PTES:
case INACCESSIBLE_BOOT_DEVICE:
/* Keep the same code */
MessageId = BugCheckCode;
break;
/* Check if this is a kernel-mode exception */
case KERNEL_MODE_EXCEPTION_NOT_HANDLED:
case SYSTEM_THREAD_EXCEPTION_NOT_HANDLED:
case KMODE_EXCEPTION_NOT_HANDLED:
/* Use the generic text message */
MessageId = KMODE_EXCEPTION_NOT_HANDLED;
break;
/* File-system errors */
case NTFS_FILE_SYSTEM:
/* Use the generic message for FAT */
MessageId = FAT_FILE_SYSTEM;
break;
/* Check if this is a coruption of the Mm's Pool */
case DRIVER_CORRUPTED_MMPOOL:
/* Use generic corruption message */
MessageId = DRIVER_CORRUPTED_EXPOOL;
break;
/* Check if this is a signature check failure */
case STATUS_SYSTEM_IMAGE_BAD_SIGNATURE:
/* Use the generic corruption message */
MessageId = BUGCODE_PSS_MESSAGE_SIGNATURE;
break;
/* All other codes */
default:
/* Use the default bugcheck message */
MessageId = BUGCODE_PSS_MESSAGE;
break;
}
/* Save bugcheck data */
KiBugCheckData[0] = BugCheckCode;
KiBugCheckData[1] = BugCheckParameter1;
KiBugCheckData[2] = BugCheckParameter2;
KiBugCheckData[3] = BugCheckParameter3;
KiBugCheckData[4] = BugCheckParameter4;
/* Now check what bugcheck this is */
switch (BugCheckCode)
{
/* Invalid access to R/O memory or Unhandled KM Exception */
case KERNEL_MODE_EXCEPTION_NOT_HANDLED:
case ATTEMPTED_WRITE_TO_READONLY_MEMORY:
case ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY:
{
/* Check if we have a trap frame */
if (!TrapFrame)
{
/* Use parameter 3 as a trap frame, if it exists */
if (BugCheckParameter3) TrapFrame = (PVOID)BugCheckParameter3;
}
/* Check if we got one now and if we need to get the Program Counter */
if ((TrapFrame) &&
(BugCheckCode != KERNEL_MODE_EXCEPTION_NOT_HANDLED))
{
/* Get the Program Counter */
Pc = (PVOID)KeGetTrapFramePc(TrapFrame);
}
break;
}
/* Wrong IRQL */
case IRQL_NOT_LESS_OR_EQUAL:
{
/*
* The NT kernel has 3 special sections:
* MISYSPTE, POOLMI and POOLCODE. The bug check code can
* determine in which of these sections this bugcode happened
* and provide a more detailed analysis. For now, we don't.
*/
/* Program Counter is in parameter 4 */
Pc = (PVOID)BugCheckParameter4;
/* Get the driver base */
DriverBase = KiPcToFileHeader(Pc,
&LdrEntry,
FALSE,
&IsSystem);
if (IsSystem)
{
/*
* The error happened inside the kernel or HAL.
* Get the memory address that was being referenced.
*/
Memory = (PVOID)BugCheckParameter1;
/* Find to which driver it belongs */
DriverBase = KiPcToFileHeader(Memory,
&LdrEntry,
TRUE,
&IsSystem);
if (DriverBase)
{
/* Get the driver name and update the bug code */
KiBugCheckDriver = &LdrEntry->BaseDllName;
KiBugCheckData[0] = DRIVER_PORTION_MUST_BE_NONPAGED;
}
else
{
/* Find the driver that unloaded at this address */
KiBugCheckDriver = NULL; // FIXME: ROS can't locate
/* Check if the cause was an unloaded driver */
if (KiBugCheckDriver)
{
/* Update bug check code */
KiBugCheckData[0] =
SYSTEM_SCAN_AT_RAISED_IRQL_CAUGHT_IMPROPER_DRIVER_UNLOAD;
}
}
}
else
{
/* Update the bug check code */
KiBugCheckData[0] = DRIVER_IRQL_NOT_LESS_OR_EQUAL;
}
/* Clear Pc so we don't look it up later */
Pc = NULL;
break;
}
/* Hard error */
case FATAL_UNHANDLED_HARD_ERROR:
{
/* Copy bug check data from hard error */
HardErrorParameters = (PULONG_PTR)BugCheckParameter2;
KiBugCheckData[0] = BugCheckParameter1;
KiBugCheckData[1] = HardErrorParameters[0];
KiBugCheckData[2] = HardErrorParameters[1];
KiBugCheckData[3] = HardErrorParameters[2];
KiBugCheckData[4] = HardErrorParameters[3];
/* Remember that this is hard error and set the caption/message */
IsHardError = TRUE;
HardErrCaption = (PCHAR)BugCheckParameter3;
HardErrMessage = (PCHAR)BugCheckParameter4;
break;
}
/* Page fault */
case PAGE_FAULT_IN_NONPAGED_AREA:
{
/* Assume no driver */
DriverBase = NULL;
/* Check if we have a trap frame */
if (!TrapFrame)
{
/* We don't, use parameter 3 if possible */
if (BugCheckParameter3) TrapFrame = (PVOID)BugCheckParameter3;
}
/* Check if we have a frame now */
if (TrapFrame)
{
/* Get the Program Counter */
Pc = (PVOID)KeGetTrapFramePc(TrapFrame);
KiBugCheckData[3] = (ULONG_PTR)Pc;
/* Find out if was in the kernel or drivers */
DriverBase = KiPcToFileHeader(Pc,
&LdrEntry,
FALSE,
&IsSystem);
}
else
{
/* Can't blame a driver, assume system */
IsSystem = TRUE;
}
/* FIXME: Check for session pool in addition to special pool */
/* Special pool has its own bug check codes */
if (MmIsSpecialPoolAddress((PVOID)BugCheckParameter1))
{
if (MmIsSpecialPoolAddressFree((PVOID)BugCheckParameter1))
{
KiBugCheckData[0] = IsSystem
? PAGE_FAULT_IN_FREED_SPECIAL_POOL
: DRIVER_PAGE_FAULT_IN_FREED_SPECIAL_POOL;
}
else
{
KiBugCheckData[0] = IsSystem
? PAGE_FAULT_BEYOND_END_OF_ALLOCATION
: DRIVER_PAGE_FAULT_BEYOND_END_OF_ALLOCATION;
}
}
else if (!DriverBase)
{
/* Find the driver that unloaded at this address */
KiBugCheckDriver = NULL; // FIXME: ROS can't locate
/* Check if the cause was an unloaded driver */
if (KiBugCheckDriver)
{
KiBugCheckData[0] =
DRIVER_UNLOADED_WITHOUT_CANCELLING_PENDING_OPERATIONS;
}
}
break;
}
/* Check if the driver forgot to unlock pages */
case DRIVER_LEFT_LOCKED_PAGES_IN_PROCESS:
/* Program Counter is in parameter 1 */
Pc = (PVOID)BugCheckParameter1;
break;
/* Check if the driver consumed too many PTEs */
case DRIVER_USED_EXCESSIVE_PTES:
/* Loader entry is in parameter 1 */
LdrEntry = (PVOID)BugCheckParameter1;
KiBugCheckDriver = &LdrEntry->BaseDllName;
break;
/* Check if the driver has a stuck thread */
case THREAD_STUCK_IN_DEVICE_DRIVER:
/* The name is in Parameter 3 */
KiBugCheckDriver = (PVOID)BugCheckParameter3;
break;
/* Anything else */
default:
break;
}
/* Do we have a driver name? */
if (KiBugCheckDriver)
{
/* Convert it to ANSI */
KeBugCheckUnicodeToAnsi(KiBugCheckDriver, AnsiName, sizeof(AnsiName));
}
else
{
/* Do we have a Program Counter? */
if (Pc)
{
/* Dump image name */
KiDumpParameterImages(AnsiName,
(PULONG_PTR)&Pc,
1,
KeBugCheckUnicodeToAnsi);
}
}
/* Check if we need to save the context for KD */
if (!KdPitchDebugger) KdDebuggerDataBlock.SavedContext = (ULONG_PTR)&Context;
/* Check if a debugger is connected */
if ((BugCheckCode != MANUALLY_INITIATED_CRASH) && (KdDebuggerEnabled))
{
/* Crash on the debugger console */
DbgPrint("\n*** Fatal System Error: 0x%08lx\n"
" (0x%p,0x%p,0x%p,0x%p)\n\n",
KiBugCheckData[0],
KiBugCheckData[1],
KiBugCheckData[2],
KiBugCheckData[3],
KiBugCheckData[4]);
/* Check if the debugger isn't currently connected */
if (!KdDebuggerNotPresent)
{
/* Check if we have a driver to blame */
if (KiBugCheckDriver)
{
/* Dump it */
DbgPrint("Driver at fault: %s.\n", AnsiName);
}
/* Check if this was a hard error */
if (IsHardError)
{
/* Print caption and message */
if (HardErrCaption) DbgPrint(HardErrCaption);
if (HardErrMessage) DbgPrint(HardErrMessage);
}
/* Break in the debugger */
KiBugCheckDebugBreak(DBG_STATUS_BUGCHECK_FIRST);
}
}
/* Raise IRQL to HIGH_LEVEL */
_disable();
KeRaiseIrql(HIGH_LEVEL, &OldIrql);
/* Avoid recursion */
if (!InterlockedDecrement((PLONG)&KeBugCheckCount))
{
#ifdef CONFIG_SMP
/* Set CPU that is bug checking now */
KeBugCheckOwner = Prcb->Number;
/* Freeze the other CPUs */
KxFreezeExecution();
#endif
/* Display the BSOD */
KiDisplayBlueScreen(MessageId,
IsHardError,
HardErrCaption,
HardErrMessage,
AnsiName);
// TODO/FIXME: Run the registered reason-callbacks from
// the KeBugcheckReasonCallbackListHead list with the
// KbCallbackReserved1 reason.
/* Check if the debugger is disabled but we can enable it */
if (!(KdDebuggerEnabled) && !(KdPitchDebugger))
{
/* Enable it */
KdEnableDebuggerWithLock(FALSE);
}
else
{
/* Otherwise, print the last line */
InbvDisplayString("\r\n");
}
/* Save the context */
Prcb->ProcessorState.ContextFrame = Context;
/* FIXME: Support Triage Dump */
/* FIXME: Write the crash dump */
// TODO: The crash-dump helper must set the Reboot variable.
Reboot = !!IopAutoReboot;
}
else
{
/* Increase recursion count */
KeBugCheckOwnerRecursionCount++;
if (KeBugCheckOwnerRecursionCount == 2)
{
/* Break in the debugger */
KiBugCheckDebugBreak(DBG_STATUS_BUGCHECK_SECOND);
}
else if (KeBugCheckOwnerRecursionCount > 2)
{
/* Halt execution */
while (TRUE);
}
}
/* Call the Callbacks */
KiDoBugCheckCallbacks();
/* FIXME: Call Watchdog if enabled */
/* Check if we have to reboot */
if (Reboot)
{
/* Unload symbols */
DbgUnLoadImageSymbols(NULL, (PVOID)MAXULONG_PTR, 0);
HalReturnToFirmware(HalRebootRoutine);
}
/* Attempt to break in the debugger (otherwise halt CPU) */
KiBugCheckDebugBreak(DBG_STATUS_BUGCHECK_SECOND);
/* Shouldn't get here */
ASSERT(FALSE);
while (TRUE);
} 复制代码
绕过方法
如何突破这一强大的防护机制。我们将探讨两种类型的绕过手段。
注意:这些方法绝非所有现有的绕过技术。我只列举了个人认为最有趣且符合我心目中"真正绕过 PatchGuard"标准的方法——即能够在不受惩罚的情况下修改关键内核结构。
启动时补丁(UEFI/BIOS)
目标是在 PatchGuard 激活前拦截启动过程(BIOS/UEFI),对启动管理器、引导加载程序或内核本身进行修补。例如, EfiGuard 是一款引导工具包,它会在启动时动态修改 bootmgfw.efi、bootmgr.efi 和 winload.efi,同时禁用 PatchGuard 和驱动程序签名强制(DSE)(非本文重点)。类似地,在旧系统中,MBR 引导工具包(如 Fyyre 开发的那些)会在启动时修补内存中的 ntoskrnl.exe,从而有效禁用 PatchGuard。在完整内核加载前,PatchGuard 代码会被修改或阻止初始化。
不过,这种方法存在一些缺点。首先,必须禁用安全启动(除非你恰好掌握 0day 漏洞,当然这是另一回事)。其次,虽然很容易检测到 Windows 内核已被修补且 PatchGuard 不再运行,但由于该防护机制未启动,加载修改关键内核结构(IDT、GDT、MSRs...)的驱动程序不会出现问题,因为正如我们所说,PatchGuard 已从系统中完全移除,仅残留部分缓解代码。
基于虚拟机管理程序(Hypervisor)的root病毒(VT-x/EPT “蓝色药丸”技术)
在Windows内核下方安装一个0级(Ring -1)虚拟机管理程序,意味着操作系统运行在虚拟化层(VMX非root模式)中,而虚拟机管理程序会拦截关键的系统访问。这里所说的是一种1型虚拟机管理程序(Ring -1级),它对扩展页表(EPT,当虚拟机与虚拟机管理程序协同工作时使用,EPT负责管理客户机物理页与主机物理页之间的转换)拥有完全的控制权。通过动态更改内存转换,这种方法能够隐藏代码注入或陷阱,使PatchGuard始终看到内核的原始版本。例如, Gbhv 项目实现了一个使用EPT来隐藏内核代码修改的虚拟机管理程序。在实际操作中,虚拟机管理程序可以拦截系统调用或中断,并将它们重定向到恶意代码,而无需更改Windows所看到的内存。这使攻击者从ntoskrnl.exe初始化开始,到整个操作系统启动、运行直至关闭,都能拥有完全的控制权。
这种方法的主要缺点是,必须禁用安全启动才能加载此类软件。另一个缺点是,必须在BIOS设置中启用虚拟化支持(VT-x/AMD-V),以便允许使用VMXON、VMXOFF、VMRESUME、VMREAD、VMWRITE等CPU指令。尽管大多数现代英特尔和AMD处理器都支持这些功能,但仍有一些处理器不支持。就我个人而言,由于这种方法为攻击者提供了高度的控制权,因此它是我最喜欢的方法。
参考资料
《深入解析Windows操作系统》(第7版 卷2)(有关PatchGuard的内容)论坛链接:https://www.mcsafenet.com/forum. ... d=72&extra=page%3D6 https://standa-note.blogspot.com/2015/10/some-tips-to-analyze-patchguard.html https://www.unknowncheats.me/forum/anti-cheat-bypass/580678-demystifying-patchguard-depth-analysis-practical-engineering.html https://blog.tetrane.com/downloads/Tetrane_PatchGuard_Analysis_RS4_v1.01.pdf
结语
至此,我的PatchGuard内部机制探索之旅告一段落。正如我们所见,这是一种极为强大的防护机制,堪称内核中一个积极运作且经过混淆处理的部分,其随机化的行为模式使得人们难以完全掌握。不过,我们目前所做的不过是浅尝辄止。对于这一防护机制的研究,仍有很长的路要走,而这仅仅是个开端。
早安!倘若我们无缘再见,那么,愿你午后安好,傍晚愉悦,夜晚宁静!
原文链接