引子
最近在研究基于AMD-V的轻量级虚拟机监控程序(VMM),遇到一个很有意思的问题:如何在硬件层面保护一个函数不被读取和写入,但又能正常执行?
这个需求在游戏反外挂、安全防御、代码完整性保护等场景下非常常见。然而,AMD的嵌套页表(NPT)却有一个让人头疼的硬性限制。
1. 核心挑战:NPT不支持"只执行"
在AMD-V虚拟化中,嵌套页表(Nested Page Tables, NPT) 负责将客户机的物理地址转换为宿主机的物理地址,并控制每一页的访问权限(读、写、执行)。
但NPT有一个众所周知的限制:它不提供"只执行(Execute-Only)"权限。
这意味着什么?
假设你想保护一个关键函数:
不允许任何代码读取它的机器码(防止被扫描/脱钩)
不允许任何代码覆写它(防止被篡改)
但允许CPU正常调用执行它
在NPT的规则下,你做不到。因为"可执行"必须搭配"可读",一旦可读,别人就能读走你的原始字节。
2. 转机:AMD的隐藏特性"内存保护密钥(PK)"
正当我觉得此路不通时,翻了一下AMD的编程手册(文档号24593),发现了一个被很多人忽略的特性——Memory Protection Keys(内存保护密钥)。
这个特性原本的设计目的是给用户态进程提供一种更细粒度的内存权限管理,但它恰好能解决我们的问题。
工作原理
PK的核心机制是这样的:
给内存页打标签:每个内存页可以通过页表项中的几个bit,设置一个"Key"值(0~15)。
全局权限控制:CPU内部有一个PKRU寄存器(32位),其中每2个bit控制一个Key的读/写权限。
硬件自动检查:当CPU访问某个内存页时,会检查该页的Key对应的PKRU位,决定是否允许读/写。
关键转折点
仔细看AMD的手册会发现一条重要描述:
PK的权限检查不影响指令获取(Instruction Fetch)。
翻译成人话就是:
如果一页被PK标记为"不可读、不可写"
那么任何mov eax, [地址](读)或mov [地址], eax(写)都会触发#PF页错误
但call 地址或jmp 地址(指令执行)完全不受影响,正常进行
这不就是我们想要的"只执行"吗?
3. 实战工作流
基于这个发现,我们可以这样操作:
text
步骤1:为目标函数所在的内存页分配一个专用的PK Key(比如Key 1)
步骤2:设置PKRU寄存器,禁止对Key 1进行读和写
步骤3:正常执行call 函数地址 → 不受影响,CPU取指执行
步骤4:任何代码尝试读取该页 → 触发#PF → VMM接管
步骤5:任何代码尝试写入该页 → 触发#PF → VMM接管
效果:我们在AMD-V上完美"模拟"出了NPT原生不支持的Execute-Only页。
4. 这个技术的妙用
有了这个能力,你可以实现:
隐蔽的Hook点:在函数头插入INT3,外部读取时永远看到原始字节(因为读操作会被拦截,VMM可以动态还原)
防内存扫描:任何试图dump你代码的扫描器都会触发异常,被VMM悄无声息地处理掉
代码完整性保护:关键逻辑页的只执行保护,防止被篡改
5. 检测与反制
当然,这个技术并非无懈可击。检测者可以通过检查CR4寄存器中的PKE位来判断PK是否启用。
应对思路也很直接:在VMM中虚拟化所有对CR4的读写操作,当客户机读取CR4时,伪造一个PKE位为0的值返回;当客户机尝试修改时,忽略对PKE位的写入。
这样从客户机操作系统的视角来看,PK功能仿佛从未开启,完美隐匿。
总结
AMD-V的NPT虽然不支持Execute-Only,但通过巧妙地借用Memory Protection Keys,我们可以绕过这个硬件限制,实现真正意义上的"只执行"内存保护。
这是一个典型的"用硬件特性解决硬件限制"的思路,也是虚拟化底层对抗中一个非常实用的技术点。
如果你也对虚拟化、内核安全、反外挂技术感兴趣,欢迎留言交流。
参考项目:MemHvHooked(基于AMD-V的轻量级Hypervisor,实现了上述机制)
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于 2026-7-12 12:40
被Cubic编辑
,原因: