虽然网上关于 PatchGuard 原理分析的资料已经较多,但缺乏完整性且部分地方的分析不够细致。因此,出于学习的目的,此贴仅记录本人在分析 pg 过程中的笔记,希望对他人有所裨益。
如果错误之处,还望指正!触发检查的 DeferredRoutine 函数是从以下函数池中选择出来的。ExpTimerDpcRoutine -> KiCustomAccessRoutine0FsRtlTruncateSmallMcb -> KiCustomAccessRoutine0IopTimerDispatch -> KiCustomAccessRoutine1IopIrpStackProfilerDpcRoutine -> KiCustomAccessRoutine2PopThermalZoneDpc -> KiCustomAccessRoutine3CmpEnableLazyFlushDpcRoutine -> KiCustomAccessRoutine4CmpLazyFlushDpcRoutine -> KiCustomAccessRoutine5KiBalanceSetManagerDeferredRoutine -> KiCustomAccessRoutine6ExpTimeRefreshDpcRoutine -> KiCustomAccessRoutine7ExpTimeZoneDpcRoutine -> KiCustomAccessRoutine8ExpCenturyDpcRoutine -> KiCustomAccessRoutine9KiTimerDispatch KiDpcDispatch KiDispatchCallout 枚举线程插入APC调用?函数组 KiCustomAccessRoutine_n 使用异常处理程序来激活检查,而 KiTimerDispatch 和 KiDpcDispatch 则直接调用 DPC,而不使用此异常技巧。通过校验 _KPDC::DeferredContext 是否为规范地址来判断当前 DPC 是否与 PathGuard 相关。 需要注意的是,参数 SystemArgument1 和 dpc->SystemArgument1 的值并不相同,SystemArgument2 也是类似的情况。需要进一步分析...KiCustomAccessRoutine_n 函数内部调用 KiCustomRecurseRoutine_n,取 DeferredContext 的低2位再加1作为循环轮次(count)。调用顺序为 0~9 的循环,每次循环 count 减1;直到计数结束,返回 DeferredContext 指针的值。
由于 DeferredContext 是无效指针,因此会触发 #GP 异常进入异常处理块 KiBalanceSetManagerDeferredRoutine$fin$0。该函数接收两个参数:UnwindReason(是否因异常展开 "exception unwinding" 而退出) 和栈帧的 Rsp。
函数 ExpTimerDpcRoutine 中被加密的变量,诸如 SystemArgument1、Dpc 等在异常处理块中被解密。
__ROR8__ 刚好对应于函数 KiBalanceSetManagerDeferredRoutine 中的 "*(QWORD *)((char *)&pgContext[0x34] + 2) = _ROL8(DeferredContext, SystemArgument1);"。因此,dpc->DeferredContext 的解密算法为 dpc->DeferredContext ^ DeferredContextMask | 0xffff800000000000。后续,设置 DeferredContext 的前4字节硬编码,即函数 CmpAppendDllSection 的首条指令 xor [rcx], rdx 开始自解密。 DeferredContext 所指向地址的首条指令恰好为函数 CmpAppendDllSection函数的首条指令。
函数 CmpAppendDllSection 会逐行进行自解密,后完成对 context 的解密,因此在该函数中下软件断点将会出现异常。自解密完成后,将会调用context的入口函数 PatchGuardEntryPoint。 指令 "xor qword ptr [rcx], rdx", 导致前4字节被覆盖, 后4字节指令被解密。每次执行4B,解密8字节。指令 "jmp r8" 插入工作项(ExQueueWorkItem),参数 pgContext[0x798] 指定工作项指针。_WORK_QUEUE_ITEM::Parameter 指向当前 pgContext,_WORK_QUEUE_ITEM::WorkerRoutine 为 FsRtlUninitializeSmallMcb。key = 0xe499f116fbceb8f6,函数入口硬编码 opcodes = 0xecc8c05e1131482e,则 key ^ opcodes = 0x08513148eafff0d8。
PatchGuardEntryPoint 函数为 context 的入口函数。临时替换 idt 表和清空 Dr7 寄存器,从而依赖于 #DB 异常的硬件断点和单步调试失效。
首先检验 context 自身是否被篡改?可有效避免诸如软中断(int3),先前已通过清空 dr7 导致硬件中断失效。第一部分的 context 自检长度为 0x620,每次校验128B,共计12轮。数据校验时,利用内存预取技术 _mm_prefetch 将 context 缓存至 cache 中。以第 2 轮自检为例,每轮可细分为8次运算,包括 ^/__ROL8__。接着,第二部分的 context 自检校验剩余的32B。 如果 context 自检失败,则会最终调用 SdbpCheckDll 函数触发 KeBugCheckEx 蓝屏。arg1: pgContext[0x900] 指向 pgContext 首地址;arg2: pgContext[0x908] 被置空;arg3: pgContext[0x918] 校验和;arg4: pgContext[0x910] 错误码。 只要在 context 自检时机前清除 int3 软中断即可。当 pgContext[0x7e0] 被设置时(当前内核完整性校验次数>0),将进入内核完整性校验阶段。pgContext[0x808]+pgContext 指向内核完整性校验描述符表 KICDT (Kernel integrity check descriptor table),不同类型的内核完整性校验对象的表项长度不同。对于函数而言,每个表项长度为 0x30。第一部分以128B为单位进行校验,第二部分将剩余不足128B的数据以8B为单位进行校验。 若剩余字节不足8B,将进入第三部分,以单字节为单位进行校验。 如果校验失败,则判断当前是否开启了虚拟化相关的安全策略导致内核完
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-289804.htm
[原创] PatchGuard原理分析 (update 1/19)
93 浏览
14 回复
感谢分享
学习一下
感谢分享。
感谢分享
感谢分享
回复来看看,先谢过楼主!
感谢分享
感谢分享
感谢分享
学习一下学习一下
好好学习一下
mark
谢谢分享
mark