修改页表后CPU异常执行不存在代码的问题排查
基于PTE修改的Windows内核函数挂钩递归问题排查
问题场景
通过修改进程页表项(PTE)的物理页号(PFN),将目标函数MyRoutine所在虚拟页指向包含跳转钩子的物理页,实现内核函数挂钩。钩子逻辑中会先恢复原PFN、刷新TLB后调用原函数,再恢复挂钩页表。单步调试完全正常,但自由运行时CPU未识别页表修改,调用MyRoutine仍触发钩子递归,尝试过页表项失效、全TLB刷新、重建页表、__wbinvd等操作均未解决。
核心代码如下:
void HookFunctionViaPTE() { // 获取函数"MyRoutine"对应的PTE指针 PPTE pte = RetrievePTE(&MyRoutine); pte->PFN = g_HijackedCodePfn; // g_HijackedCodePfn是包含原页副本的物理页PFN,在对应位置写入跳转到MyRoutine_Hook的指令 memcpy(g_HijackedCodePtr + VirtualAddress.PFNOffset, hookCode, sizeof(hookCode)); } void MyRoutine_Hook( PVOID context ) { // 前置处理逻辑 // 恢复原PFN并刷新TLB,准备调用原函数 RestoreOriginalPFNInPTE(); __writecr3(__readcr3()); // 预期调用原函数而非递归进入钩子 MyRoutine(); // 恢复挂钩页表并刷新TLB RestoreHackedPFNInPTE(); __writecr3(__readcr3()); // 后置处理逻辑 }
可能成因
- 指令预取/分支预测缓存未失效:CPU会提前预取指令并缓存分支预测信息,单步调试时缓存会被清空,但自由运行时,预取的钩子指令仍在缓存中,导致即使页表修改,CPU还是执行缓存里的钩子代码。
__writecr3只会刷新TLB,不会清空指令缓存(ICache)。 - SMP系统下的跨CPU缓存一致性问题:多CPU环境中,仅在当前CPU刷新TLB,其他CPU的TLB仍缓存旧的PTE映射,当线程被调度到其他CPU时,依然会使用旧映射触发递归。
RestoreOriginalPFNInPTE实现缺陷:该函数可能仅修改了PFN,未正确恢复原PTE的所有属性(如NX、RW、Present位等),导致页表项无效;或者错误修改了内核页表而非当前进程的页表。- 编译器优化导致地址解析异常:
MyRoutine的地址被编译器硬编码或内联优化,钩子函数中调用MyRoutine时,地址解析依然指向挂钩后的映射,而非恢复后的原页。 - 内存屏障缺失:页表修改和TLB刷新操作被CPU乱序执行,TLB刷新可能在页表修改完成前执行,导致TLB仍缓存旧映射。
排查方向
- 强制刷新指令缓存:在
__writecr3后添加指令缓存刷新操作,x86架构下可用__invlpg针对目标地址刷新TLB,同时用cpuid序列化指令流并清空ICache:RestoreOriginalPFNInPTE(); __invlpg(&MyRoutine); // 针对目标地址刷新TLB __writecr3(__readcr3()); // 插入指令缓存刷新逻辑 __asm { cpuid } - 处理多CPU场景:遍历系统所有CPU,切换到目标CPU执行页表恢复和TLB刷新,确保所有CPU的TLB都同步:
KAFFINITY oldAffinity = KeSetSystemAffinityThread(1ULL << cpuId); RestoreOriginalPFNInPTE(); __writecr3(__readcr3()); KeSetSystemAffinityThread(oldAffinity); - 验证
RestoreOriginalPFNInPTE正确性:恢复PFN后,打印原PTE和恢复后PTE的所有字段(PFN、Present、RW、NX等),确认完全一致;同时对比当前CR3值,确认修改的是当前进程的页表。 - 禁用编译器优化:将
MyRoutine和MyRoutine_Hook标记为__declspec(noinline)禁止内联,用volatile修饰PTE指针防止编译器缓存值:volatile PPTE pte = RetrievePTE(&MyRoutine); - 添加内存屏障:在页表修改后、TLB刷新前插入
__mfence,确保页表修改完成后再执行刷新:RestoreOriginalPFNInPTE(); __mfence(); // 内存屏障保证修改可见性 __writecr3(__readcr3()); - 内核调试验证:用内核调试器在
RestoreOriginalPFNInPTE后、MyRoutine调用前,检查目标地址的PTE是否恢复为原PFN,同时用!tlb命令查看TLB映射是否正确。
内容的提问来源于stack exchange,提问作者Arush Agarampur
相关产品推荐
相关产品推荐

