You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

修改页表后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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 16:25:02