笔者能力有限(还是太菜了😢),还是推荐看原文:https://blog.tetrane.com/downloads/Tetrane_PatchGuard_Analysis_RS4_v1.01.pdf

PG的初始化阶段

A,调用链

首先找到ntoskrl.exe中最大的函数开始定位

image-20260505142257588

首先比较内核是否处于调试状态,处于调试则不激活PG。(loc_140A1AF13是一个死循环)

image-20260505142517360

重命名sub_140A1AEE4PgInitialization然后查找引用

image-20260505142945649

image-20260505143017062

只是根据参数进行PG的初始化,所以重命名为PgInitialization_w,他的交叉引用又是

image-20260505143130129

根据文献[1]的描述,PG的实例化是KiFilterFiberContext在boot阶段被调用的。

The initialization of PatchGuard is performed mostly by KiFilterFiberContext. This function is called at the beginning of the boot, before any user driver load. KiFilterFiberContext is called in two manners, that are detailed hereafter.[1]

在[1]和[2]中都发现了很有意思的一点,KiFilterFiberContext的调用是通过异常处理实现了,[1]猜测这可能是作为混淆的一种手段。

根据[2]中的步骤查找调用,发现除了自身的递归调用外,在KeInitAmd64SpecificState中存在。但是KeInitAmd64SpecificState伪代码没有直接调用(call)他。

image-20260505143853130

image-20260505143945640

反而是在exception中出现(是否勾起你免杀/CTF的回忆)

image-20260505144337111

image-20260505144302253

根据伪代码也不难看出,当KdPitchDebugger或者KdDebuggerNotPresent被启用后,做除法的idiv

edx eax r8d
0x80000000 0x80000000 0xFFFFFFFF

在内存拼接后的有符号除法中[edx:eax] / r8d有趣的来了

>>> hex(int(0x8000000080000000/0xffffffff))
'0x80000001'

除数和被除数均为负数,所以得到的结果应该是正数,但是这里得到一个负数的结果。造成了divide error,从而触发exception部分,从而跳转到KiFilterFiberContext
根据[1]中的描述还有调用栈

KiFilterFiberContext
ExpLicenseWatchInitWorker
ExInitSystemPhase2
Phase1InitializationDiscard
Phase1Initialization

B,回到PGinit函数

调用过程分析的差不多了就开始回到起点看看pg究竟保存了那些东西。

阶段1

首先复制了CmpAppendDllSection函数

image-20260505151054227

这个函数的代码也是抽象的很

image-20260505151324515

这个函数的作用就是在后面的完整性检查中使用随机的密钥使用XOR进行编码和解码。[2]中发现中引用了诸如 KiWaitAlwaysKiWaitNever 之类的全局变量,用于在 PatchGuard 的 DPC 执行期间对指针进行编码或解码。(该说不说这个Pg的上下文是真的长啊)。还有其他关于全局变量等的搜集可以参考[1]。

image-20260505152436801

image-20260505152509951

阶段2

此阶段收集供后续使用的各类数据,例如 PTE 条目、来自 ntoskrnl 和 hal 的例程,以及其他关键的内核结构。在 Windows 10 RS4 中,该结构中恰好保存了 20 个条目。保存这些条目是为了防范某种绕过攻击。稍后我们将看到,这些 PTE 会在触发 KeBugCheck 之前被恢复。

同时根据[1]中的内容,在这里它保存了一些对系统内核很关键的一些routines。

image-20260505153058196

阶段3

在这里内核使用了一些结构体来保存相关的内容的值。根据[1]中的披露,一种结构体如下

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:保存的类型。是IDT/GDT/SSDT…

  • pData:检查的区域起点

  • szData:检查的大小

  • hash:检查的这块内存的校验和

  • specific:这块有说法的,针对待核查的数据

    例如,在 IDT 检查用例中,该特定数值将存储用于执行操作的目标处理器信息。总体而言,这意味着该结构可能因被检查结构的不同而有所差异,同时也表明针对不同结构的检查代码并非完全一致。[1]

进行检查

使用了KiInitPatchGuardContext进行上下文的初始化,然后使用了不同的方法类别来进行不同特定类型检查(checks)的初始化。这一函数包含了五个参数。

  • Arg1:DPC方法的索引(index)
  • Arg2:调度方法
  • Arg3:用于确定待检查最大大小的随机值
  • Arg4:指向 ExpLicenseWatchInitWorker 结构的指针(仅有 4% 的几率)
  • Arg5:布尔值,用于决定是否需要检查 NT 例程的完整性

关于method对应的检查操作大致如下:

  • 方法 1:利用一个与 DPC(延迟过程调用)结构关联的定时器。PatchGuard 会初始化上下文和 DPC,随后通过 KeSetCoalescableTimer 将二者集成;该定时器在设置完成后 2 到 130 秒之间触发,且具有 0 到 0.001 秒的随机延迟容差。由于该定时器并非周期性触发,因此必须在检查例程结束时对其进行重置。

  • 方法 2 和 3:通过将 DPC 直接隐藏在内核的 PRCB 结构内部,从而避免使用常规定时器。如果传递给 KiInitPatchGuardContext 函数的第二个参数为 1 或 2,系统便会初始化一个上下文及 DPC,并将其隐藏在 PRCB 结构的特定字段中,进而依靠合法的系统函数来对该 DPC 进行排队调度。

    AcpiReserved 字段:DPC 指针被隐藏于此,并通过 HalpTimerDpcRoutine 进行排队调度,触发周期至少为 2 分钟。它利用 HalpTimerLastDpc 变量来追踪上一次事件的发生时间,该变量的取值基于全局的系统运行时间变量。此事件通常由 ACPI 状态转换(例如进入空闲状态)所触发。

  • 方法 3(HalReserved 字段):与前述方法类似,但将 DPC 指针存储在 HalReserved 字段中。该 DPC 由 HalpMcaQueueDpc 函数在 HAL 时钟中断期间(例如 HalpTimerClockInterrupt)进行排队调度。此外,该字段还可能包含一个指向 KI_FILTER_FIBER_PARAM 结构的指针,该结构由 ExpLicenseWatchInitWorker 函数中的 KiFilterFiberContext 例程所使用。

  • 方法 4:以 4% 的概率创建一个新的系统线程,并利用一个 KI_FILTER_FIBER_PARAM 结构来实现。该结构包含一个指向 PsCreateSystemThread 函数的指针,正是该函数负责创建并启动新线程。线程的 StartAddress 字段指向一个用于执行验证任务的函数。作为一种混淆技巧,一旦线程创建完成,其对应的 ETHREAD 结构中的 StartAddressWin32StartAddress 字段便会被覆写为一些常见的函数指针。在实际执行时,系统会从一个包含八个元素的函数指针数组中随机选取一个作为正确的入口点,而该数组中仅有一个指针是有效的。

  • **方法 5:对常规的DPC进行Hook。**要求必须存在一个有效的 KI_FILTER_FIBER_PARAM 结构。如果该项不可用,系统将回退至方法 0。它利用该结构体中的最后一个条目——一个指向全局变量 KiBalanceSetManagerPeriodicDpc 的指针;该全局变量包含一个 KDPC 结构体,且该结构体是在 KiInitSystem 函数中完成初始化的。PatchGuard 会挂钩(hook)这一合法的 DPC 对象,该对象通过 KeClockInterruptNotify 机制每秒执行一次。每执行 120 至 130 次后,PatchGuard 自身的 DPC 就会被排入队列并执行,以此取代原有的 DPC。它会清除该全局变量的副本,并允许其验证例程在完成任务后将其重置。

    image-20260505154754892

  • **方法7:**一种还没有被分析出来的东西。

再次回到KiFilterFiberContext中,其中的PsIntegrityCheckEnabled默认值硬编码为1

image-20260505155259277

这是一个在ntoskrl中不存在的例程(可以看到使用sub_1403DBC50作为了参数)。[1]中依旧是接触他们的调试器发现这个回调初始化位置位于 mssecflt.sys 二进制文件内的SecInitializeKernelIntegrityCheck 函数之中。

检测流程主要从KeBugCheck-》KeBugCheckEx -》KeBugCheck2。其中的KeBugCheck2由于高度抽象的结构在[2]中使用ReactOS的代码[4]解释Pg检测的大致流程

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

绕过方法

这种不敢多说,之前就有文章被平台和谐过,相关的评论也会被河蟹…

Boot-Time Patches:其目标是在 PatchGuard 激活之前,通过拦截启动过程(BIOS/UEFI)来修补启动管理器、引导加载程序或内核本身。例如[4]。然而,这种方法也存在一些弊端。首先,必须禁用“安全启动”(Secure Boot)功能。其次,虽然检测出 Windows 内核已被修补且 PatchGuard 已不再运行相对容易,但正因 PatchGuard 处于非运行状态,加载那些会修改关键内核结构(如 IDT、GDT、MSR 等)的驱动程序便不再受阻;因为正如前文所述,PatchGuard 实际上已从该系统中彻底消失,仅留存着一些缓解代码的残余痕迹。

VT-x/EPT:利用VT这种ring-1级别的虚拟机级别程序拦截关键的访问操作,例如对 EPT(扩展页表)拥有完全的控制权。EPT 机制主要应用于涉及虚拟机(VM)与虚拟机监视器的场景中,负责管理访客系统(Guest)与宿主系统(Host)物理页之间的地址转换。通过动态修改内存地址转换规则,该监视器能够隐藏代码注入或陷阱(Traps)的存在,从而确保 PatchGuard 机制始终检测到的是内核的原始版本,例如[5] [6]。
这种思路类似的还有页表重映射攻击的PTE hook。隔离具体进程的四级页表,使我们的Hook不影响全局[7]。

引用

[1] Tetrane_PatchGuard_Analysis_RS4_v1.01 https://blog.tetrane.com/downloads/Tetrane_PatchGuard_Analysis_RS4_v1.01.pdf

[2] PatchGuard Internals https://r0keb.github.io/posts/PatchGuard-Internals/

[3] Some Tips to Analyze PatchGuard https://standa-note.blogspot.com/2015/10/some-tips-to-analyze-patchguard.html

[4] https://github.com/mattiwatti/efiguard

[5] https://github.com/Gbps/gbhv

[6] https://github.com/everdox/InfinityHook

[7] PTE-Hook https://xz.aliyun.com/news/18999