找回密码
 立即注册
搜索
查看: 721|回复: 0

[翻译] PatchGuard 内部机制

[复制链接]

251

主题

0

回帖

2345

积分

管理员

积分
2345
发表于 2025-10-11 18:20:05 | 显示全部楼层 |阅读模式
    早上好!在今天的博客中,我们将探讨 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 中寻找最大的函数。
20250529190119.png

    sub_140BD3620():
20250529182330.png
    我们可以看到,这个函数所做的第一件事就是检查是否已附加调试器,如果已附加,PatchGuard 将不会激活。
    如果我们列出交叉引用(即调用该函数的其他位置),会发现它是由另一个函数调用的:
20250529190659.png
    sub_140BFABF0():
20250529190715.png
    如图所示,这个函数非常简短,但它使用了一个指针来获取传递给我们的主函数 sub_140BD3620() 的参数。
    以下是伪代码:
  1. void __fastcall sub_140BFABF0(_BYTE *Parameter)
  2. {
  3.   Parameter[28] = sub_140BD3620(
  4.                     *(_DWORD *)Parameter,
  5.                     *((_DWORD *)Parameter + 1),
  6.                     *((_DWORD *)Parameter + 2),
  7.                     *((_QWORD *)Parameter + 2),
  8.                     *((_DWORD *)Parameter + 6));
  9. }
复制代码
    为了简化表述,在本文接下来的内容中,我们将对以下函数进行重命名:将 sub_140BD3620() 重命名为 PgInitialization(),将 sub_140BFABF0() 重命名为 PgWrapper2PgInit 。
20250529202226.png
    我们可以看到,从函数 KiFilterFiberContext() 中,我们找到了对负责初始化 PatchGuard 的包装函数的调用引用。这个函数(KiFilterFiberContext())在参与这项臭名昭著的缓解技术初始化方面是众所周知的。
    (尽管我们也看到 KeCheckedKernelInitialize() 中存在对该函数的调用)

20250529202520.png
    那么接下来,我们将对 KiFilterFiberContext() 函数进行分析。
    KiFilterFiberContext()
  1. _BOOL8 __fastcall KiFilterFiberContext(__int64 a1)
  2. {
  3.   NTSTATUS v2; // r12d
  4.   unsigned __int64 v3; // rax
  5.   unsigned __int128 v4; // rax
  6.   unsigned __int64 v5; // rbx
  7.   unsigned __int64 v6; // rax
  8.   unsigned __int128 v7; // rax
  9.   __int64 v8; // r9
  10.   unsigned __int64 v9; // r10
  11.   unsigned __int128 v10; // rax
  12.   unsigned __int64 v11; // r15
  13.   NTSTATUS v12; // eax
  14.   char v13; // di
  15.   unsigned __int64 v14; // rax
  16.   unsigned __int128 v15; // rax
  17.   int v16; // r8d
  18.   unsigned __int64 v17; // rax
  19.   unsigned __int128 v18; // rax
  20.   NTSTATUS v19; // eax
  21.   char v20; // cl
  22.   int v21; // eax
  23.   NTSTATUS v22; // eax
  24.   char v23; // cl
  25.   int v24; // ecx
  26.   __int64 *v25; // rax
  27.   __int64 v26; // rdx
  28.   _DWORD Parameter[4]; // [rsp+40h] [rbp-89h] BYREF
  29.   __int64 v29; // [rsp+50h] [rbp-79h]
  30.   int v30; // [rsp+58h] [rbp-71h]
  31.   char v31; // [rsp+5Ch] [rbp-6Dh]
  32.   _DWORD v32[4]; // [rsp+60h] [rbp-69h] BYREF
  33.   __int64 v33; // [rsp+70h] [rbp-59h]
  34.   int v34; // [rsp+78h] [rbp-51h]
  35.   char v35; // [rsp+7Ch] [rbp-4Dh]
  36.   _DWORD v36[4]; // [rsp+80h] [rbp-49h] BYREF
  37.   __int64 v37; // [rsp+90h] [rbp-39h]
  38.   int v38; // [rsp+98h] [rbp-31h]
  39.   char v39; // [rsp+9Ch] [rbp-2Dh]
  40.   __int64 v40; // [rsp+A0h] [rbp-29h]
  41.   __int64 v41; // [rsp+A8h] [rbp-21h]
  42.   OBJECT_ATTRIBUTES ObjectAttributes; // [rsp+B0h] [rbp-19h] BYREF
  43.   PCALLBACK_OBJECT CallbackObject; // [rsp+130h] [rbp+67h] BYREF
  44.   __int64 v44; // [rsp+138h] [rbp+6Fh]
  45.   __int64 v45; // [rsp+140h] [rbp+77h]
  46.   __int64 v46; // [rsp+148h] [rbp+7Fh]
  47.   v2 = KdDisableDebugger();
  48.   KeKeepData(KiFilterFiberContext);
  49.   _disable();
  50.   if ( !(_BYTE)KdDebuggerNotPresent )
  51.   {
  52.     while ( 1 )
  53.       ;
  54.   }
  55.   _enable();
  56.   v3 = __rdtsc();
  57.   v4 = (__ROR8__(v3, 3) ^ v3) * (unsigned __int128)0x7010008004002001uLL;
  58.   v44 = *((_QWORD *)&v4 + 1);
  59.   v5 = ((unsigned __int64)v4 ^ *((_QWORD *)&v4 + 1)) % 0xA;
  60.   if ( !*(_QWORD *)&MaxDataSize && !a1 && !__2c )
  61.   {
  62.     if ( PsIntegrityCheckEnabled )
  63.     {
  64.       ObjectAttributes.Length = 0x30;
  65.       ObjectAttributes.ObjectName = (PUNICODE_STRING)L"TV";
  66.       ObjectAttributes.RootDirectory = 0;
  67.       ObjectAttributes.Attributes = 0x40;
  68.       *(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0;
  69.       if ( ExCreateCallback(&CallbackObject, &ObjectAttributes, 0, 0) >= 0 )
  70.       {
  71.         ExNotifyCallback(CallbackObject, sub_140510650, &__24);
  72.         ObfDereferenceObject(CallbackObject);
  73.         if ( __24 )
  74.           __2c = 1;
  75.         ExInitializeNPagedLookasideList(&stru_140E0EF80, 0, 0, 0x200u, 0xB38u, 0x746E494Bu, 0);
  76.       }
  77.     }
  78.   }
  79.   v6 = __rdtsc();
  80.   v7 = (__ROR8__(v6, 3) ^ v6) * (unsigned __int128)0x7010008004002001uLL;
  81.   v45 = *((_QWORD *)&v7 + 1);
  82.   v8 = v7;
  83.   *(_QWORD *)&v7 = __rdtsc();
  84.   v9 = v8 ^ *((_QWORD *)&v7 + 1);
  85.   Parameter[2] = (v5 < 6) + 1;
  86.   v29 = a1;
  87.   v30 = 1;
  88.   v31 = 0;
  89.   v10 = (__ROR8__(v7, 3) ^ (unsigned __int64)v7) * (unsigned __int128)0x7010008004002001uLL;
  90.   v46 = *((_QWORD *)&v10 + 1);
  91.   v11 = ((unsigned __int64)v10 ^ *((_QWORD *)&v10 + 1)) % 6;
  92.   Parameter[1] = v11;
  93.   Parameter[0] = v9 % 0xD;
  94.   v12 = KeExpandKernelStackAndCallout((PEXPAND_STACK_CALLOUT)Wrapper2PgInit, Parameter, 0xC000u);
  95.   v13 = v31;
  96.   if ( v12 < 0 )
  97.     v13 = 0;
  98.   v31 = v13;
  99.   if ( v13 )
  100.   {
  101.     if ( v5 >= 6 )
  102.       goto LABEL_21;
  103.     v14 = __rdtsc();
  104.     v15 = (__ROR8__(v14, 3) ^ v14) * (unsigned __int128)0x7010008004002001uLL;
  105.     v40 = *((_QWORD *)&v15 + 1);
  106.     v16 = ((unsigned __int64)v15 ^ *((_QWORD *)&v15 + 1)) % 0xD;
  107.     do
  108.     {
  109.       v17 = __rdtsc();
  110.       v18 = (__ROR8__(v17, 3) ^ v17) * (unsigned __int128)0x7010008004002001uLL;
  111.       v41 = *((_QWORD *)&v18 + 1);
  112.     }
  113.     while ( (_DWORD)v11 && ((unsigned __int64)v18 ^ *((_QWORD *)&v18 + 1)) % 6 == (_DWORD)v11 );
  114.     v32[0] = v16;
  115.     v32[1] = ((unsigned __int64)v18 ^ *((_QWORD *)&v18 + 1)) % 6;
  116.     v32[2] = (v5 < 6) + 1;
  117.     v33 = a1;
  118.     v34 = 0;
  119.     v35 = 0;
  120.     v19 = KeExpandKernelStackAndCallout((PEXPAND_STACK_CALLOUT)Wrapper2PgInit, v32, 0xC000u);
  121.     v20 = v35;
  122.     if ( v19 < 0 )
  123.       v20 = 0;
  124.     v35 = v20;
  125.     v13 = v20;
  126.     if ( v20 )
  127.     {
  128. LABEL_21:
  129.       if ( *(_QWORD *)&MaxDataSize )
  130.         goto LABEL_29;
  131.       if ( a1 )
  132.         goto LABEL_37;
  133.       if ( (int)KiSwInterruptPresent() < 0 && !__2c )
  134.       {
  135. LABEL_30:
  136.         if ( qword_141006660 )
  137.           ExFreePool(qword_141006660);
  138.         v24 = 24;
  139.         v25 = &__25;
  140.         v26 = 3;
  141.         do
  142.         {
  143.           *v25 = 0;
  144.           v24 -= 8;
  145.           ++v25;
  146.           --v26;
  147.         }
  148.         while ( v26 );
  149.         for ( ; v24; --v24 )
  150.         {
  151.           *(_BYTE *)v25 = 0;
  152.           v25 = (__int64 *)((char *)v25 + 1);
  153.         }
  154.         __2e = 0;
  155.         __26 = 0;
  156.         __27 = 0;
  157.         dword_140E0EEC0 = 0;
  158.         qword_141006080 = 0;
  159.         goto LABEL_37;
  160.       }
  161.       v36[0] = 0;
  162.       v36[1] = 7;
  163.       v36[2] = 1;
  164.       v37 = 0;
  165.       v21 = KiSwInterruptPresent();
  166.       v39 = 0;
  167.       v38 = (v21 >> 31) & 8;
  168.       v22 = KeExpandKernelStackAndCallout((PEXPAND_STACK_CALLOUT)Wrapper2PgInit, v36, 0xC000u);
  169.       v23 = v39;
  170.       if ( v22 < 0 )
  171.         v23 = 0;
  172.       v39 = v23;
  173.       v13 = v23;
  174.     }
  175.     if ( !v13 )
  176.       goto LABEL_37;
  177. LABEL_29:
  178.     if ( a1 )
  179.       goto LABEL_37;
  180.     goto LABEL_30;
  181.   }
  182. LABEL_37:
  183.   _disable();
  184.   if ( !(_BYTE)KdDebuggerNotPresent )
  185.   {
  186.     while ( 1 )
  187.       ;
  188.   }
  189.   _enable();
  190.   _disable();
  191.   _enable();
  192.   if ( v2 >= 0 )
  193.     KdEnableDebugger();
  194.   return v13 != 0;
  195. }
复制代码

20250529205644.png
    进入这个函数后,我们看到如下伪代码:
  1. __int64 KeInitAmd64SpecificState()
  2. {
  3.   __int64 result; // rax
  4.   _mm_lfence();
  5.   if ( *(_QWORD *)&HvlpVsmVtlCallVa || !(_DWORD)InitSafeBootMode )
  6.     return (unsigned int)(__ROR4__((unsigned __int8)KdPitchDebugger | (unsigned __int8)KdDebuggerNotPresent, 1)
  7.                         / (((unsigned __int8)KdPitchDebugger | (unsigned __int8)KdDebuggerNotPresent) != 0 ? -1 : 17));
  8.   return result;
  9. }
复制代码
    乍一看,似乎没有直接引用到 KiFilterFiberContext() —— 但如果我们查看反汇编代码……
20250529210908.png
    进入异常处理程序后,我们看到如下内容:
20250529211110.png

    如图所示,该调用确实是在那个 __except() 块内完成的。
    但让我们聚焦核心问题:KiFilterFiberContext() 是什么?
  • 它是 PatchGuard 初始化过程中的一个关键函数,在 Windows 启动期间会被调用两次。其中一次调用是在 KeInitAmd64SpecificState() 内部的一个异常处理程序(__except())中完成的。
  • 该函数的激活是通过在 KeInitAmd64SpecificState() 起始处强制触发一个错误来实现的,在此过程中会用到 KdDebuggerNotPresent() 和 KdPitchDebugger() 函数。

20250530225352.png

上下文信息
    PatchGuard 上下文是一个大型内存结构,用于监控受 PatchGuard 保护的内核结构。一些研究人员将此定义扩展为,把检查方法也涵盖在内。因此,狭义的定义仅指该结构本身,而广义的定义则既包括该结构,也包括 PatchGuard 用于初始化和验证的方法。

第一部分
    PatchGuard 会将 CmpAppendDllSection 函数的代码复制到自身的结构中,并利用该代码通过与随机密钥进行异或(XOR)操作来解密其余部分。具体可参见以下伪代码:
  1. __int64 __fastcall CmpAppendDllSection(_QWORD *a1, __int64 a2)
  2. {
  3.   _QWORD *v2; // rcx
  4.   __int64 v3; // rax
  5.   _QWORD *v4; // rdx
  6.   __int64 v5; // rcx
  7.   __int64 v6; // rax
  8.   __int64 v7; // rax
  9.   *a1 ^= a2;
  10.   a1[1] ^= a2;
  11.   a1[2] ^= a2;
  12.   a1[3] ^= a2;
  13.   a1[4] ^= a2;
  14.   a1[5] ^= a2;
  15.   a1[6] ^= a2;
  16.   a1[7] ^= a2;
  17.   a1[8] ^= a2;
  18.   a1[9] ^= a2;
  19.   a1[10] ^= a2;
  20.   a1[11] ^= a2;
  21.   a1[12] ^= a2;
  22.   a1[13] ^= a2;
  23.   a1[14] ^= a2;
  24.   a1[15] ^= a2;
  25.   v2 = a1 + 15;
  26.   v2[1] ^= a2;
  27.   v2[2] ^= a2;
  28.   v2[3] ^= a2;
  29.   v2[4] ^= a2;
  30.   v2[5] ^= a2;
  31.   v2[6] ^= a2;
  32.   v2[7] ^= a2;
  33.   v2[8] ^= a2;
  34.   v2[9] ^= a2;
  35.   v2 -= 15;
  36.   *(_DWORD *)v2 ^= a2;
  37.   v3 = a2;
  38.   v4 = v2;
  39.   v5 = *((unsigned int *)v2 + 49);
  40.   if ( v3 )
  41.   {
  42.     do
  43.     {
  44.       v4[v5 + 24] ^= v3;
  45.       v6 = __ROR8__(v3, v5);
  46.       v3 = v6 ^ (1LL << v6);
  47.       --v5;
  48.     }
  49.     while ( v5 );
  50.   }
  51.   v7 = ((__int64 (__fastcall *)(__int64))((char *)v4 + *((unsigned int *)v4 + 514)))(v5);
  52.   return (*(__int64 (__fastcall **)(__int64, __int64))(v7 + 288))(v7 + 1976, 1);
  53. }
复制代码
   存在对 KiWaitAlways 和 KiWaitNever 等全局变量的引用,这些变量用于在 PatchGuard 执行延迟过程调用(DPC,Deferred Procedure Call)期间对指针进行编码或解码。

20250531111821.png
20250531111920.png
    在这里,我们看到对 KiWaitAlways 的引用:
20250531112125.png
    继续向下滚动查看,我们还发现了来自 PgInit 的引用:
20250531112417.png
    许多来自 ntoskrnl.exe 的指针也被复制到了 PatchGuard 上下文中,这使得 PatchGuard 能够在不依赖内核导出表的情况下调用函数。

第二部分
    此阶段会收集后续将用到的数据,例如页表项(PTE entries)、来自 ntoskrnl 和硬件抽象层(HAL,Hardware Abstraction Layer)的例程,以及其他关键的内核结构。

第三部分
    第三阶段包含一个结构体数组,其中每个结构体负责一项特定的验证工作,例如:

  • IDT
  • GDT
  • SSDT, MSRs…

    每个结构体包含以下内容:

  • 一个 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 中并不存在的回调函数:

20250531120002.png

有趣的关键例程
    为了更深入地理解 PatchGuard 那些不同寻常的内部运作机制,让我们来剖析一些除已讨论过的函数之外的其他关键函数。
    KeBugCheck()
    该函数是 KeBugCheckEx 的封装函数:
  1. void __stdcall __noreturn KeBugCheck(ULONG BugCheckCode)
  2. {
  3.   ULONG_PTR v1; // rdx
  4.   ULONG_PTR v2; // r8
  5.   ULONG_PTR v3; // r9
  6.   ULONG_PTR v4; // [rsp+20h] [rbp-8h]
  7.   KeBugCheckEx(BugCheckCode, v1, v2, v3, v4);
  8. }
复制代码
    KeBugCheckEx()
    KeBugCheckEx 的目标是调用 KeBugCheck2,不过它并非一个简单的封装函数。它会对参数执行多项检查,并从上下文(Context)中提取相关值,但最终仍会调用 KeBugCheck2:
  1. // local variable allocation has failed, the output may be wrong!
  2. void __stdcall __noreturn KeBugCheckEx(
  3.         ULONG BugCheckCode,
  4.         ULONG_PTR BugCheckParameter1,
  5.         ULONG_PTR BugCheckParameter2,
  6.         ULONG_PTR BugCheckParameter3,
  7.         ULONG_PTR BugCheckParameter4)
  8. {
  9.   _CONTEXT *Context; // r10
  10.   char **v6; // r8
  11.   void *v7; // r9
  12.   signed __int8 CurrentIrql; // al
  13.   __int64 v9; // [rsp+30h] [rbp-8h]
  14.   char *retaddr; // [rsp+38h] [rbp+0h] BYREF
  15.   unsigned __int64 var_BugCheckCode; // [rsp+40h] [rbp+8h]
  16.   int var_BugCheckParameter1; // [rsp+48h] [rbp+10h]
  17.   int var_BugCheckParameter2; // [rsp+50h] [rbp+18h]
  18.   int var_BugCheckParameter3; // [rsp+58h] [rbp+20h]
  19.   char v15; // [rsp+68h] [rbp+30h] BYREF
  20.   var_BugCheckCode = *(_QWORD *)&BugCheckCode;
  21.   var_BugCheckParameter1 = BugCheckParameter1;
  22.   var_BugCheckParameter2 = BugCheckParameter2;
  23.   var_BugCheckParameter3 = BugCheckParameter3;
  24.   _disable();
  25.   RtlCaptureContext(KeGetCurrentPrcb()->Context);
  26.   KiSaveProcessorControlState(&KeGetCurrentPrcb()->ProcessorState);
  27.   Context = KeGetCurrentPrcb()->Context;
  28.   Context->Rcx = var_BugCheckCode;
  29.   *(_QWORD *)&Context->EFlags = v9;
  30.   if ( &byte_1403FDFD9 == retaddr )
  31.   {
  32.     v6 = (char **)&v15;
  33.     v7 = KeBugCheck;
  34.   }
  35.   else
  36.   {
  37.     v6 = &retaddr;
  38.     v7 = KeBugCheckEx;
  39.   }
  40.   Context->Rsp = (unsigned __int64)v6;
  41.   Context->Rip = (unsigned __int64)v7;
  42.   CurrentIrql = KeGetCurrentIrql();
  43.   __writegsbyte(0x8018u, CurrentIrql);
  44.   if ( CurrentIrql < 2 )
  45.     __writecr8(2u);
  46.   if ( (v9 & 0x200) != 0 )
  47.     _enable();
  48.   _InterlockedIncrement(&KiHardwareTrigger);
  49.   if ( &byte_1403FDFD9 != retaddr )
  50.     KeBugCheck2(
  51.       var_BugCheckCode,
  52.       var_BugCheckParameter1,
  53.       var_BugCheckParameter2,
  54.       var_BugCheckParameter3,
  55.       BugCheckParameter4,
  56.       0);
  57.   KeBugCheck2(var_BugCheckCode, 0, 0, 0, 0, 0);
  58. }
复制代码
    这个函数在诸如 KiInitializeKernel 等关键例程中被引用:
20250529173933.png
    KeBugCheck2()
    这是该调用链中的最终函数。
20250529014747.png
    没错,就是这个。鉴于其代码规模较大,我们参考一个研究 Windows 内部机制的最佳资料来源:ReactOS 项目。
    以下是 KeBugCheckEx 的代码逻辑,它会调用 KeBugCheckWithTf:

  1. DECLSPEC_NORETURN
  2. VOID
  3. NTAPI
  4. KeBugCheckEx(IN ULONG BugCheckCode,
  5.              IN ULONG_PTR BugCheckParameter1,
  6.              IN ULONG_PTR BugCheckParameter2,
  7.              IN ULONG_PTR BugCheckParameter3,
  8.              IN ULONG_PTR BugCheckParameter4)
  9. {
  10.     /* Call the internal API */
  11.     KeBugCheckWithTf(BugCheckCode,
  12.                      BugCheckParameter1,
  13.                      BugCheckParameter2,
  14.                      BugCheckParameter3,
  15.                      BugCheckParameter4,
  16.                      NULL);
  17. }
复制代码
   这段代码显然已过时,且可能存在不准确之处,但它能让我们对这一关键的 PatchGuard 函数有扎实的理解:

  1. DECLSPEC_NORETURN
  2. VOID
  3. NTAPI
  4. KeBugCheckWithTf(IN ULONG BugCheckCode,
  5.                  IN ULONG_PTR BugCheckParameter1,
  6.                  IN ULONG_PTR BugCheckParameter2,
  7.                  IN ULONG_PTR BugCheckParameter3,
  8.                  IN ULONG_PTR BugCheckParameter4,
  9.                  IN PKTRAP_FRAME TrapFrame)
  10. {
  11.     PKPRCB Prcb = KeGetCurrentPrcb();
  12.     CONTEXT Context;
  13.     ULONG MessageId;
  14.     CHAR AnsiName[128];
  15.     BOOLEAN IsSystem, IsHardError = FALSE, Reboot = FALSE;
  16.     PCHAR HardErrCaption = NULL, HardErrMessage = NULL;
  17.     PVOID Pc = NULL, Memory;
  18.     PVOID DriverBase;
  19.     PLDR_DATA_TABLE_ENTRY LdrEntry;
  20.     PULONG_PTR HardErrorParameters;
  21.     KIRQL OldIrql;
  22.     /* Set active bugcheck */
  23.     KeBugCheckActive = TRUE;
  24.     KiBugCheckDriver = NULL;
  25.     /* Check if this is power failure simulation */
  26.     if (BugCheckCode == POWER_FAILURE_SIMULATE)
  27.     {
  28.         /* Call the Callbacks and reboot */
  29.         KiDoBugCheckCallbacks();
  30.         HalReturnToFirmware(HalRebootRoutine);
  31.     }
  32.     /* Save the IRQL and set hardware trigger */
  33.     Prcb->DebuggerSavedIRQL = KeGetCurrentIrql();
  34.     InterlockedIncrement((PLONG)&KiHardwareTrigger);
  35.     /* Capture the CPU Context */
  36.     RtlCaptureContext(&Prcb->ProcessorState.ContextFrame);
  37.     KiSaveProcessorControlState(&Prcb->ProcessorState);
  38.     Context = Prcb->ProcessorState.ContextFrame;
  39.     /* FIXME: Call the Watchdog if it's registered */
  40.     /* Check which bugcode this is */
  41.     switch (BugCheckCode)
  42.     {
  43.         /* These bug checks already have detailed messages, keep them */
  44.         case UNEXPECTED_KERNEL_MODE_TRAP:
  45.         case DRIVER_CORRUPTED_EXPOOL:
  46.         case ACPI_BIOS_ERROR:
  47.         case ACPI_BIOS_FATAL_ERROR:
  48.         case THREAD_STUCK_IN_DEVICE_DRIVER:
  49.         case DATA_BUS_ERROR:
  50.         case FAT_FILE_SYSTEM:
  51.         case NO_MORE_SYSTEM_PTES:
  52.         case INACCESSIBLE_BOOT_DEVICE:
  53.             /* Keep the same code */
  54.             MessageId = BugCheckCode;
  55.             break;
  56.         /* Check if this is a kernel-mode exception */
  57.         case KERNEL_MODE_EXCEPTION_NOT_HANDLED:
  58.         case SYSTEM_THREAD_EXCEPTION_NOT_HANDLED:
  59.         case KMODE_EXCEPTION_NOT_HANDLED:
  60.             /* Use the generic text message */
  61.             MessageId = KMODE_EXCEPTION_NOT_HANDLED;
  62.             break;
  63.         /* File-system errors */
  64.         case NTFS_FILE_SYSTEM:
  65.             /* Use the generic message for FAT */
  66.             MessageId = FAT_FILE_SYSTEM;
  67.             break;
  68.         /* Check if this is a coruption of the Mm's Pool */
  69.         case DRIVER_CORRUPTED_MMPOOL:
  70.             /* Use generic corruption message */
  71.             MessageId = DRIVER_CORRUPTED_EXPOOL;
  72.             break;
  73.         /* Check if this is a signature check failure */
  74.         case STATUS_SYSTEM_IMAGE_BAD_SIGNATURE:
  75.             /* Use the generic corruption message */
  76.             MessageId = BUGCODE_PSS_MESSAGE_SIGNATURE;
  77.             break;
  78.         /* All other codes */
  79.         default:
  80.             /* Use the default bugcheck message */
  81.             MessageId = BUGCODE_PSS_MESSAGE;
  82.             break;
  83.     }
  84.     /* Save bugcheck data */
  85.     KiBugCheckData[0] = BugCheckCode;
  86.     KiBugCheckData[1] = BugCheckParameter1;
  87.     KiBugCheckData[2] = BugCheckParameter2;
  88.     KiBugCheckData[3] = BugCheckParameter3;
  89.     KiBugCheckData[4] = BugCheckParameter4;
  90.     /* Now check what bugcheck this is */
  91.     switch (BugCheckCode)
  92.     {
  93.         /* Invalid access to R/O memory or Unhandled KM Exception */
  94.         case KERNEL_MODE_EXCEPTION_NOT_HANDLED:
  95.         case ATTEMPTED_WRITE_TO_READONLY_MEMORY:
  96.         case ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY:
  97.         {
  98.             /* Check if we have a trap frame */
  99.             if (!TrapFrame)
  100.             {
  101.                 /* Use parameter 3 as a trap frame, if it exists */
  102.                 if (BugCheckParameter3) TrapFrame = (PVOID)BugCheckParameter3;
  103.             }
  104.             /* Check if we got one now and if we need to get the Program Counter */
  105.             if ((TrapFrame) &&
  106.                 (BugCheckCode != KERNEL_MODE_EXCEPTION_NOT_HANDLED))
  107.             {
  108.                 /* Get the Program Counter */
  109.                 Pc = (PVOID)KeGetTrapFramePc(TrapFrame);
  110.             }
  111.             break;
  112.         }
  113.         /* Wrong IRQL */
  114.         case IRQL_NOT_LESS_OR_EQUAL:
  115.         {
  116.             /*
  117.              * The NT kernel has 3 special sections:
  118.              * MISYSPTE, POOLMI and POOLCODE. The bug check code can
  119.              * determine in which of these sections this bugcode happened
  120.              * and provide a more detailed analysis. For now, we don't.
  121.              */
  122.             /* Program Counter is in parameter 4 */
  123.             Pc = (PVOID)BugCheckParameter4;
  124.             /* Get the driver base */
  125.             DriverBase = KiPcToFileHeader(Pc,
  126.                                           &LdrEntry,
  127.                                           FALSE,
  128.                                           &IsSystem);
  129.             if (IsSystem)
  130.             {
  131.                 /*
  132.                  * The error happened inside the kernel or HAL.
  133.                  * Get the memory address that was being referenced.
  134.                  */
  135.                 Memory = (PVOID)BugCheckParameter1;
  136.                 /* Find to which driver it belongs */
  137.                 DriverBase = KiPcToFileHeader(Memory,
  138.                                               &LdrEntry,
  139.                                               TRUE,
  140.                                               &IsSystem);
  141.                 if (DriverBase)
  142.                 {
  143.                     /* Get the driver name and update the bug code */
  144.                     KiBugCheckDriver = &LdrEntry->BaseDllName;
  145.                     KiBugCheckData[0] = DRIVER_PORTION_MUST_BE_NONPAGED;
  146.                 }
  147.                 else
  148.                 {
  149.                     /* Find the driver that unloaded at this address */
  150.                     KiBugCheckDriver = NULL; // FIXME: ROS can't locate
  151.                     /* Check if the cause was an unloaded driver */
  152.                     if (KiBugCheckDriver)
  153.                     {
  154.                         /* Update bug check code */
  155.                         KiBugCheckData[0] =
  156.                             SYSTEM_SCAN_AT_RAISED_IRQL_CAUGHT_IMPROPER_DRIVER_UNLOAD;
  157.                     }
  158.                 }
  159.             }
  160.             else
  161.             {
  162.                 /* Update the bug check code */
  163.                 KiBugCheckData[0] = DRIVER_IRQL_NOT_LESS_OR_EQUAL;
  164.             }
  165.             /* Clear Pc so we don't look it up later */
  166.             Pc = NULL;
  167.             break;
  168.         }
  169.         /* Hard error */
  170.         case FATAL_UNHANDLED_HARD_ERROR:
  171.         {
  172.             /* Copy bug check data from hard error */
  173.             HardErrorParameters = (PULONG_PTR)BugCheckParameter2;
  174.             KiBugCheckData[0] = BugCheckParameter1;
  175.             KiBugCheckData[1] = HardErrorParameters[0];
  176.             KiBugCheckData[2] = HardErrorParameters[1];
  177.             KiBugCheckData[3] = HardErrorParameters[2];
  178.             KiBugCheckData[4] = HardErrorParameters[3];
  179.             /* Remember that this is hard error and set the caption/message */
  180.             IsHardError = TRUE;
  181.             HardErrCaption = (PCHAR)BugCheckParameter3;
  182.             HardErrMessage = (PCHAR)BugCheckParameter4;
  183.             break;
  184.         }
  185.         /* Page fault */
  186.         case PAGE_FAULT_IN_NONPAGED_AREA:
  187.         {
  188.             /* Assume no driver */
  189.             DriverBase = NULL;
  190.             /* Check if we have a trap frame */
  191.             if (!TrapFrame)
  192.             {
  193.                 /* We don't, use parameter 3 if possible */
  194.                 if (BugCheckParameter3) TrapFrame = (PVOID)BugCheckParameter3;
  195.             }
  196.             /* Check if we have a frame now */
  197.             if (TrapFrame)
  198.             {
  199.                 /* Get the Program Counter */
  200.                 Pc = (PVOID)KeGetTrapFramePc(TrapFrame);
  201.                 KiBugCheckData[3] = (ULONG_PTR)Pc;
  202.                 /* Find out if was in the kernel or drivers */
  203.                 DriverBase = KiPcToFileHeader(Pc,
  204.                                               &LdrEntry,
  205.                                               FALSE,
  206.                                               &IsSystem);
  207.             }
  208.             else
  209.             {
  210.                 /* Can't blame a driver, assume system */
  211.                 IsSystem = TRUE;
  212.             }
  213.             /* FIXME: Check for session pool in addition to special pool */
  214.             /* Special pool has its own bug check codes */
  215.             if (MmIsSpecialPoolAddress((PVOID)BugCheckParameter1))
  216.             {
  217.                 if (MmIsSpecialPoolAddressFree((PVOID)BugCheckParameter1))
  218.                 {
  219.                     KiBugCheckData[0] = IsSystem
  220.                         ? PAGE_FAULT_IN_FREED_SPECIAL_POOL
  221.                         : DRIVER_PAGE_FAULT_IN_FREED_SPECIAL_POOL;
  222.                 }
  223.                 else
  224.                 {
  225.                     KiBugCheckData[0] = IsSystem
  226.                         ? PAGE_FAULT_BEYOND_END_OF_ALLOCATION
  227.                         : DRIVER_PAGE_FAULT_BEYOND_END_OF_ALLOCATION;
  228.                 }
  229.             }
  230.             else if (!DriverBase)
  231.             {
  232.                 /* Find the driver that unloaded at this address */
  233.                 KiBugCheckDriver = NULL; // FIXME: ROS can't locate
  234.                 /* Check if the cause was an unloaded driver */
  235.                 if (KiBugCheckDriver)
  236.                 {
  237.                     KiBugCheckData[0] =
  238.                         DRIVER_UNLOADED_WITHOUT_CANCELLING_PENDING_OPERATIONS;
  239.                 }
  240.             }
  241.             break;
  242.         }
  243.         /* Check if the driver forgot to unlock pages */
  244.         case DRIVER_LEFT_LOCKED_PAGES_IN_PROCESS:
  245.             /* Program Counter is in parameter 1 */
  246.             Pc = (PVOID)BugCheckParameter1;
  247.             break;
  248.         /* Check if the driver consumed too many PTEs */
  249.         case DRIVER_USED_EXCESSIVE_PTES:
  250.             /* Loader entry is in parameter 1 */
  251.             LdrEntry = (PVOID)BugCheckParameter1;
  252.             KiBugCheckDriver = &LdrEntry->BaseDllName;
  253.             break;
  254.         /* Check if the driver has a stuck thread */
  255.         case THREAD_STUCK_IN_DEVICE_DRIVER:
  256.             /* The name is in Parameter 3 */
  257.             KiBugCheckDriver = (PVOID)BugCheckParameter3;
  258.             break;
  259.         /* Anything else */
  260.         default:
  261.             break;
  262.     }
  263.     /* Do we have a driver name? */
  264.     if (KiBugCheckDriver)
  265.     {
  266.         /* Convert it to ANSI */
  267.         KeBugCheckUnicodeToAnsi(KiBugCheckDriver, AnsiName, sizeof(AnsiName));
  268.     }
  269.     else
  270.     {
  271.         /* Do we have a Program Counter? */
  272.         if (Pc)
  273.         {
  274.             /* Dump image name */
  275.             KiDumpParameterImages(AnsiName,
  276.                                   (PULONG_PTR)&Pc,
  277.                                   1,
  278.                                   KeBugCheckUnicodeToAnsi);
  279.         }
  280.     }
  281.     /* Check if we need to save the context for KD */
  282.     if (!KdPitchDebugger) KdDebuggerDataBlock.SavedContext = (ULONG_PTR)&Context;
  283.     /* Check if a debugger is connected */
  284.     if ((BugCheckCode != MANUALLY_INITIATED_CRASH) && (KdDebuggerEnabled))
  285.     {
  286.         /* Crash on the debugger console */
  287.         DbgPrint("\n*** Fatal System Error: 0x%08lx\n"
  288.                  "                       (0x%p,0x%p,0x%p,0x%p)\n\n",
  289.                  KiBugCheckData[0],
  290.                  KiBugCheckData[1],
  291.                  KiBugCheckData[2],
  292.                  KiBugCheckData[3],
  293.                  KiBugCheckData[4]);
  294.         /* Check if the debugger isn't currently connected */
  295.         if (!KdDebuggerNotPresent)
  296.         {
  297.             /* Check if we have a driver to blame */
  298.             if (KiBugCheckDriver)
  299.             {
  300.                 /* Dump it */
  301.                 DbgPrint("Driver at fault: %s.\n", AnsiName);
  302.             }
  303.             /* Check if this was a hard error */
  304.             if (IsHardError)
  305.             {
  306.                 /* Print caption and message */
  307.                 if (HardErrCaption) DbgPrint(HardErrCaption);
  308.                 if (HardErrMessage) DbgPrint(HardErrMessage);
  309.             }
  310.             /* Break in the debugger */
  311.             KiBugCheckDebugBreak(DBG_STATUS_BUGCHECK_FIRST);
  312.         }
  313.     }
  314.     /* Raise IRQL to HIGH_LEVEL */
  315.     _disable();
  316.     KeRaiseIrql(HIGH_LEVEL, &OldIrql);
  317.     /* Avoid recursion */
  318.     if (!InterlockedDecrement((PLONG)&KeBugCheckCount))
  319.     {
  320. #ifdef CONFIG_SMP
  321.         /* Set CPU that is bug checking now */
  322.         KeBugCheckOwner = Prcb->Number;
  323.         /* Freeze the other CPUs */
  324.         KxFreezeExecution();
  325. #endif
  326.         /* Display the BSOD */
  327.         KiDisplayBlueScreen(MessageId,
  328.                             IsHardError,
  329.                             HardErrCaption,
  330.                             HardErrMessage,
  331.                             AnsiName);
  332.         // TODO/FIXME: Run the registered reason-callbacks from
  333.         // the KeBugcheckReasonCallbackListHead list with the
  334.         // KbCallbackReserved1 reason.
  335.         /* Check if the debugger is disabled but we can enable it */
  336.         if (!(KdDebuggerEnabled) && !(KdPitchDebugger))
  337.         {
  338.             /* Enable it */
  339.             KdEnableDebuggerWithLock(FALSE);
  340.         }
  341.         else
  342.         {
  343.             /* Otherwise, print the last line */
  344.             InbvDisplayString("\r\n");
  345.         }
  346.         /* Save the context */
  347.         Prcb->ProcessorState.ContextFrame = Context;
  348.         /* FIXME: Support Triage Dump */
  349.         /* FIXME: Write the crash dump */
  350.         // TODO: The crash-dump helper must set the Reboot variable.
  351.         Reboot = !!IopAutoReboot;
  352.     }
  353.     else
  354.     {
  355.         /* Increase recursion count */
  356.         KeBugCheckOwnerRecursionCount++;
  357.         if (KeBugCheckOwnerRecursionCount == 2)
  358.         {
  359.             /* Break in the debugger */
  360.             KiBugCheckDebugBreak(DBG_STATUS_BUGCHECK_SECOND);
  361.         }
  362.         else if (KeBugCheckOwnerRecursionCount > 2)
  363.         {
  364.             /* Halt execution */
  365.             while (TRUE);
  366.         }
  367.     }
  368.     /* Call the Callbacks */
  369.     KiDoBugCheckCallbacks();
  370.     /* FIXME: Call Watchdog if enabled */
  371.     /* Check if we have to reboot */
  372.     if (Reboot)
  373.     {
  374.         /* Unload symbols */
  375.         DbgUnLoadImageSymbols(NULL, (PVOID)MAXULONG_PTR, 0);
  376.         HalReturnToFirmware(HalRebootRoutine);
  377.     }
  378.     /* Attempt to break in the debugger (otherwise halt CPU) */
  379.     KiBugCheckDebugBreak(DBG_STATUS_BUGCHECK_SECOND);
  380.     /* Shouldn't get here */
  381.     ASSERT(FALSE);
  382.     while (TRUE);
  383. }
复制代码

绕过方法
    如何突破这一强大的防护机制。我们将探讨两种类型的绕过手段。

    注意:这些方法绝非所有现有的绕过技术。我只列举了个人认为最有趣且符合我心目中"真正绕过 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内部机制探索之旅告一段落。正如我们所见,这是一种极为强大的防护机制,堪称内核中一个积极运作且经过混淆处理的部分,其随机化的行为模式使得人们难以完全掌握。不过,我们目前所做的不过是浅尝辄止。对于这一防护机制的研究,仍有很长的路要走,而这仅仅是个开端。

    早安!倘若我们无缘再见,那么,愿你午后安好,傍晚愉悦,夜晚宁静!


原文链接

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表