
简介这是一份面向Windows内核驱动开发者与虚拟化技术研究者的实战项目源码聚焦AMD-V硬件虚拟化从零构建基于SVM指令集的内核驱动实现硬件级Hook并支持嵌套虚拟化。项目适合具备操作系统架构、内核编程接口与硬件指令集基础的中高级开发者用于服务器虚拟化、云平台及虚拟机监控等场景的实验与二次开发。压缩包共108个文件约3.91MB以85个C头文件与6个C源文件为主体辅以3个汇编文件处理底层指令、2个静态库及工程配置与说明文档结构完整、自主性强。目前已有220人学习下载。读者可从中获得从零搭建驱动框架的完整思路、SVM与页表操作等核心模块的实现参考、汇编层Hook的落地方式以及嵌套虚拟化测试环境的配置指引是理解硬件级虚拟化与内核Hook不可多得的实践素材。1. 从零编写 AMD-V Hook 驱动为什么有人放着现成框架不用偏要自己造轮子手上有个 Windows 内核驱动项目目标是在 AMD 平台上用 SVM 指令集做硬件级 Hook还要支持嵌套虚拟化。听起来像天方夜谭但拆开看无非是几件事让 CPU 进入 guest 模式、拦截特定指令或内存访问、在拦截点插入自己的逻辑、再把控制权还回去。AMD-V 把这套机制做进了硬件SVM 指令集就是操作入口。做安全研究的、搞沙箱的、写反作弊的迟早会碰到这个方向。现成的虚拟化框架不是没有但要么太重要么不给你细粒度控制要么在嵌套场景下直接罢工。自己写驱动的价值在于你知道每一条 VMRUN 指令背后发生了什么出问题时能定位到具体是哪个拦截位没设对而不是对着黑匣子干瞪眼。这篇笔记按“先跑通最小闭环、再补拦截逻辑、最后处理嵌套和性能”的顺序展开每一步都给可复现的代码和参数说明。2. 先把 SVM 最小闭环跑通从 VM_HSAVE_PA 到第一次 VMRUN2.1 为什么必须从“空转”开始而不是直接写 Hook很多人一上来就想拦截MSR读写或者CPUID指令结果驱动加载后直接蓝屏连日志都来不及打。血泪经验是先让 CPU 能进 guest 再出来什么拦截都不开确认 VMRUN 和 VMRUN 之后的 VMM 退出路径是通的。这个最小闭环包含四步分配 VMCB、设置 host 保存区、执行 VMRUN、处理退出。AMD 的 SVM 架构里VMCBVirtual Machine Control Block是一块 4KB 对齐的内存里面分控制区和状态保存区。控制区决定哪些事件触发退出状态区保存 guest 的寄存器。host 状态则保存在物理地址由VM_HSAVE_PA指向的区域。先看 VMCB 和 host 保存区的分配代码// 分配 VMCB必须 4KB 对齐 PHYSICAL_ADDRESS low {0}, high {0xFFFFFFFFFFFFFFFF}; PVOID vmcb_va MmAllocateContiguousMemorySpecifyCache( PAGE_SIZE, low, high, low, MmCached); if (!vmcb_va) return STATUS_INSUFFICIENT_RESOURCES; PHYSICAL_ADDRESS vmcb_pa MmGetPhysicalAddress(vmcb_va); // 分配 host 保存区大小由 CPUID 0x8000000A 的 EDX 给出 // 通常 4KB 足够但必须按 CPUID 报告的大小来 PVOID host_save_va MmAllocateContiguousMemorySpecifyCache( PAGE_SIZE, low, high, low, MmCached); PHYSICAL_ADDRESS host_save_pa MmGetPhysicalAddress(host_save_va); // 写入 VM_HSAVE_PA MSR __writemsr(0xC0010117, host_save_pa.QuadPart);逻辑说明MmAllocateContiguousMemorySpecifyCache保证物理连续VMCB 要求 4KB 对齐所以分配一页就够。VM_HSAVE_PA是 AMD 定义的 MSR地址0xC0010117写入 host 保存区的物理地址。参数上host 保存区的大小不是固定的必须先用CPUID 0x8000000A查 EDX 寄存器它告诉你需要多少字节。我一般分配一页但如果 EDX 大于 4096就得按实际值来。2.2 VMCB 控制区的关键字段与初始化顺序VMCB 的控制区偏移从 0x000 开始最重要的几个字段INTERCEPT_CR控制 CR 寄存器拦截INTERCEPT_DR控制 DR 寄存器INTERCEPT_EXCEPTION控制异常拦截INTERCEPT_INSTRUCTION分三组控制指令拦截。最小闭环里全部清零只保留必须的INTERCEPT_VMRUN和INTERCEPT_VMMCALL防止 guest 里再执行 VMRUN 导致嵌套混乱。初始化顺序有讲究先清空整个 VMCB再设置ES、CS、SS、DS的段属性然后设置RIP指向 guest 入口RSP指向 guest 栈。段属性里ATTRIB字段容易翻车实模式下是 0x0093保护模式下代码段是 0x029B数据段是 0x0293。设错了不会立刻崩但 guest 第一条指令取指就异常。// 清空 VMCB RtlZeroMemory(vmcb_va, PAGE_SIZE); // 设置段寄存器以 64 位保护模式为例 PVMCB vmcb (PVMCB)vmcb_va; vmcb-save.cs.selector 0x0008; vmcb-save.cs.attrib 0x029B; vmcb-save.cs.limit 0xFFFFFFFF; vmcb-save.cs.base 0; vmcb-save.ds.selector 0x0010; vmcb-save.ds.attrib 0x0293; vmcb-save.ds.limit 0xFFFFFFFF; vmcb-save.ds.base 0; // 设置 guest 入口和栈 vmcb-save.rip (UINT64)guest_entry; vmcb-save.rsp (UINT64)guest_stack_top; // 设置 EFER必须开启 SVME 位 vmcb-save.efer EFER_SVME | EFER_NXE | EFER_LME | EFER_LMA; // 控制区拦截 VMRUN 和 VMMCALL vmcb-control.intercept (1 INTERCEPT_VMRUN) | (1 INTERCEPT_VMMCALL);参数说明EFER的SVME位是 SVM 使能位host 侧也要在EFER里打开否则 VMRUN 直接#UD。INTERCEPT_VMRUN和INTERCEPT_VMMCALL的位号查 AMD 手册 APM Volume 2 的 VMCB 布局表不同版本位号可能微调以手头 CPU 的文档为准。2.3 第一次 VMRUN 与退出处理的最小骨架VMRUN 指令的语法是VMRUN rax其中RAX保存 VMCB 的物理地址。执行后 CPU 进入 guest直到某个拦截事件触发CPU 保存 guest 状态到 VMCB 状态区然后跳回 host 的VMRUN下一条指令。host 侧要做的第一件事是读 VMCB 的EXITCODE字段判断退出原因。// 进入 guest __vmrun(vmcb_pa.QuadPart); // 退出后立即读退出码 UINT64 exit_code vmcb-control.exit_code; switch (exit_code) { case VMEXIT_VMRUN: // guest 里执行了 VMRUN说明嵌套层数超了 // 处理方式注入 #UD 或直接终止 guest break; case VMEXIT_VMMCALL: // guest 调用了 VMMCALL通常是请求 hypercall // 在这里实现自己的 hypercall 分发 break; case VMEXIT_MSR: // MSR 读写拦截后面 Hook 章节细讲 break; default: // 未知退出记录日志并终止 break; }逻辑说明__vmrun是内联汇编封装参数是 VMCB 物理地址。退出码在 VMCB 控制区的EXITCODE偏移处AMD 手册里每个退出码都有对应的EXITINFO1和EXITINFO2提供细节。最小闭环里只处理VMRUN和VMMCALL其他退出码先记录再返回。注意VMRUN 执行后如果没有任何拦截guest 会一直跑所以必须至少开一个拦截否则 host 永远拿不回控制权。3. 硬件级 Hook 的拦截点选择MSR、CR 与内存断点怎么配3.1 MSR 拦截最干净的 Hook 入口MSR 拦截是 SVM 里最常用的 Hook 手段。原理很简单在 VMCB 控制区打开INTERCEPT_MSR位guest 每次执行RDMSR或WRMSR都会触发VMEXIT_MSR。退出时EXITINFO1告诉你访问的是哪个 MSREXITINFO2告诉你读还是写。你可以在 host 侧修改返回值或者直接模拟一个假值。// 在 VMCB 控制区打开 MSR 拦截 vmcb-control.intercept | (1ULL INTERCEPT_MSR); // 退出处理 case VMEXIT_MSR: { UINT32 msr (UINT32)vmcb-control.exit_info_1; UINT32 is_write (UINT32)(vmcb-control.exit_info_1 32) 1; if (msr 0x174) { // 示例拦截 IA32_SYSENTER_CS if (is_write) { // 记录写入值可选择放行或篡改 UINT64 val vmcb-save.rax; // 篡改val fake_value; vmcb-save.rax val; // 写回 guest } else { // 读操作返回伪造值 vmcb-save.rax fake_msr_value; } // 跳过原始 MSR 访问直接推进 RIP vmcb-save.rip 2; // RDMSR/WRMSR 指令长度 } break; }参数说明EXITINFO1低 32 位是 MSR 编号高 32 位 bit0 表示写操作。RDMSR和WRMSR指令长度都是 2 字节所以RIP 2跳过。注意如果 MSR 访问涉及RAX和RDX的组合读操作返回时RAX放低 32 位RDX放高 32 位写操作则相反。这个细节手册里写得很清楚但实际写代码时容易只改RAX忘了RDX。3.2 CR 寄存器拦截与影子页表的关系CR 拦截分两类CR0、CR4的读写拦截和CR3的写拦截。CR3写拦截常用于监控进程切换但 AMD 的 SVM 里CR3拦截和嵌套页表NPT有交互。如果开了 NPTguest 的CR3写入的是 guest 物理地址host 侧看到的是 GPA需要转换成 HPA 才能操作真实页表。// 打开 CR3 写拦截 vmcb-control.intercept | (1ULL INTERCEPT_CR3_WRITE); // 退出处理 case VMEXIT_CR3_WRITE: { UINT64 new_cr3 vmcb-save.cr3; // 如果开了 NPTnew_cr3 是 GPA // 用 NPT 的转换函数拿到 HPA UINT64 hpa npt_translate(new_cr3); // 在这里做进程监控或页表篡改 break; }注意CR0和CR4的拦截要小心guest 里很多指令会隐式写CR0比如MOV CR0拦截太频繁会严重拖慢性能。我一般只在调试阶段开CR0拦截生产环境只开CR3写拦截。3.3 内存断点用 NPT 做不可见 Hook内存断点是硬件级 Hook 里最隐蔽的一种。原理是利用 NPTNested Page Table把目标物理页设为只读guest 写入时触发VMEXIT_NPFNested Page Fault。退出时EXITINFO1给出出错的 GPA 和错误码你可以判断是读还是写然后决定放行还是篡改。// 设置 NPT 页表项为只读 void npt_set_readonly(UINT64 gpa) { UINT64 *pte npt_get_pte(gpa); *pte ~(1ULL 1); // 清除可写位 // 刷新 TLB __invlpg(gpa); } // NPF 退出处理 case VMEXIT_NPF: { UINT64 gpa vmcb-control.exit_info_2 0xFFFFFFFFF000; UINT32 error (UINT32)vmcb-control.exit_info_1; if (error 0x2) { // 写操作 // 记录写入或篡改后放行 // 临时恢复可写单步执行后再设回只读 npt_set_writable(gpa); vmcb-control.intercept | (1ULL INTERCEPT_MTF); } break; }参数说明EXITINFO2是出错的 guest 物理地址EXITINFO1是错误码bit1 表示写。INTERCEPT_MTF是 Monitor Trap Flag用于单步执行执行完一条指令后再触发退出此时把页表设回只读。这个方案性能开销大但隐蔽性极强guest 里读页表看不到任何异常。4. 嵌套虚拟化guest 里再跑 VMRUN 时怎么不翻车4.1 嵌套的两种模式SVM-on-SVM 与影子 VMCB嵌套虚拟化有两种实现路径一种是让 guest 直接执行 VMRUNhost 拦截后模拟整个 SVM 环境这叫影子 VMCB 方案另一种是硬件辅助的嵌套AMD 从 Zen 2 开始支持VMCB的嵌套扩展但需要 BIOS 和微码配合。我一般用影子 VMCB兼容性好可控性强。影子 VMCB 的核心思路guest 里执行 VMRUN 时触发VMEXIT_VMRUNhost 读取 guest 的 VMCB 物理地址复制一份到 host 侧然后用自己的 VMCB 执行 VMRUN 进入 L2 guest。L2 退出时host 把状态同步回 guest 的 VMCB再执行 VMRUN 回到 L1。// 处理 guest 的 VMRUN case VMEXIT_VMRUN: { UINT64 guest_vmcb_pa vmcb-save.rax; // 复制 guest VMCB 到影子 VMCB PVMCB shadow get_shadow_vmcb(); RtlCopyMemory(shadow, phys_to_virt(guest_vmcb_pa), PAGE_SIZE); // 设置 host 保存区为当前 host 的 __writemsr(0xC0010117, host_save_pa.QuadPart); // 执行 VMRUN 进入 L2 __vmrun(shadow_pa.QuadPart); // L2 退出后同步状态回 guest VMCB RtlCopyMemory(phys_to_virt(guest_vmcb_pa), shadow, PAGE_SIZE); // 设置 guest 的 RAX 为退出码 vmcb-save.rax shadow-control.exit_code; break; }注意影子 VMCB 方案里L2 的退出码要正确映射回 L1 的 VMCB否则 L1 里的 hypervisor 会收到错误的退出原因。另外VM_HSAVE_PA在每次 VMRUN 前都要重设因为 L1 和 L2 的 host 保存区不同。4.2 嵌套场景下的拦截位继承与冲突处理L1 hypervisor 设置的拦截位在 L2 运行时必须继续生效。但 L2 自己也可能设置拦截位两者冲突时以 L1 为准。实现上影子 VMCB 的拦截位是 L1 和 L2 拦截位的并集。退出时先判断是 L1 的拦截还是 L2 的拦截如果是 L2 的注入到 L1 的 VMCB 里让 L1 处理。// 合并拦截位 shadow-control.intercept l1_vmcb-control.intercept | l2_vmcb-control.intercept; // 退出时判断归属 if (exit_code l1_vmcb-control.intercept) { // L1 的拦截直接返回给 L1 } else { // L2 的拦截注入到 L1 l1_vmcb-control.exit_code exit_code; l1_vmcb-control.exit_info_1 shadow-control.exit_info_1; l1_vmcb-control.exit_info_2 shadow-control.exit_info_2; }这个逻辑看起来简单但实际调试时容易漏掉EXITINFO的同步导致 L1 里的 hypervisor 收到退出码但拿不到细节行为异常。4.3 嵌套性能什么时候该关掉拦截嵌套虚拟化本身就有性能损耗每次 L2 退出都要经过 L1 再回到 host。如果 L1 的拦截位开得太多L2 里几乎每条指令都触发退出性能直接崩掉。我的经验是嵌套场景下只开必要的拦截比如MSR和CR3CPUID拦截能关就关因为CPUID在 guest 里调用频繁拦截一次的开销远大于模拟一次。5. 避坑与排查蓝屏、死锁和性能断崖的常见原因5.1 加载驱动直接蓝屏错误码 0x00000050现象驱动DriverEntry里初始化 VMCB 后执行 VMRUN 瞬间蓝屏PAGE_FAULT_IN_NONPAGED_AREA。原因VMCB 或 host 保存区的物理地址没对齐或者VM_HSAVE_PA写入的地址无效。解决用MmAllocateContiguousMemorySpecifyCache分配检查返回的物理地址低 12 位是否为 0。host 保存区大小必须按CPUID 0x8000000A的 EDX 来不能拍脑袋写 4096。5.2 VMRUN 后 host 永远拿不回控制权现象执行 VMRUN 后系统卡死没有蓝屏但鼠标键盘全无响应。原因VMCB 控制区没有打开任何拦截位guest 里执行了HLT或死循环host 无法退出。解决至少打开INTERCEPT_HLT和INTERCEPT_VMRUN确保 guest 停机或嵌套时能退出。另外guest 的RIP要指向有效代码指向空内存会触发#PF但#PF也需要拦截才能退出。5.3 MSR 拦截后 guest 行为异常现象拦截IA32_SYSENTER_CS后guest 里调用系统调用直接崩溃。原因RDMSR返回时只改了RAX没改RDXguest 拿到的高 32 位是旧值。解决读操作返回时RAX放低 32 位RDX放高 32 位写操作时从RAX和RDX组合出 64 位值。这个细节在 AMD 手册里写得很清楚但代码里容易漏。5.4 嵌套场景下 L2 退出码丢失现象L1 里的 hypervisor 收到退出事件但EXITINFO1和EXITINFO2全是 0。原因影子 VMCB 退出后只同步了EXITCODE忘了同步EXITINFO。解决在RtlCopyMemory同步状态时确保control区域的exit_code、exit_info_1、exit_info_2三个字段都复制回 L1 的 VMCB。5.5 性能断崖开了 NPT 反而更慢现象开启 NPT 后guest 里内存访问性能下降超过 50%。原因NPT 页表项没设大页每个 4KB 页都触发 TLB miss。解决在 NPT 里用 2MB 大页映射减少 TLB 压力。另外INVLPG指令要慎用每次刷新整个 TLB 的开销很大尽量用INVLPGA按地址刷新。6. 进阶技巧用 MTF 单步执行验证 Hook 是否生效6.1 MTF 的原理与开启方式MTFMonitor Trap Flag是 SVM 提供的一个调试利器。在 VMCB 控制区设置INTERCEPT_MTF位后guest 每执行一条指令就触发一次VMEXIT_MTF。这相当于硬件单步不需要修改 guest 代码也不依赖调试寄存器。我一般用它来验证内存断点是否生效设置页只读触发 NPF 后临时恢复可写并开启 MTF单步执行完写入指令后再设回只读。// 开启 MTF vmcb-control.intercept | (1ULL INTERCEPT_MTF); // MTF 退出处理 case VMEXIT_MTF: { // 单步完成关闭 MTF vmcb-control.intercept ~(1ULL INTERCEPT_MTF); // 恢复页表只读 npt_set_readonly(current_gpa); break; }参数说明INTERCEPT_MTF的位号查手册开启后每次退出都要手动关闭否则会无限单步。VMEXIT_MTF的退出码是固定的处理时不需要读EXITINFO。6.2 用 MTF 验证 MSR Hook 的返回值MSR Hook 写完后怎么确认 guest 真的拿到了伪造值开 MTF在RDMSR指令执行后单步一次读 guest 的RAX和RDX看是否等于伪造值。这个方法比在 guest 里写测试代码更直接因为不依赖 guest 里的任何工具。// 在 MSR 退出处理里开启 MTF vmcb-save.rax fake_low; vmcb-save.rdx fake_high; vmcb-control.intercept | (1ULL INTERCEPT_MTF); vmcb-save.rip 2; // 跳过 RDMSR // 下一次 MTF 退出时检查 case VMEXIT_MTF: { UINT64 guest_rax vmcb-save.rax; UINT64 guest_rdx vmcb-save.rdx; // 比较是否等于伪造值 if (guest_rax ! fake_low || guest_rdx ! fake_high) { DbgPrint(MSR Hook 失败: RAX%llx RDX%llx\n, guest_rax, guest_rdx); } vmcb-control.intercept ~(1ULL INTERCEPT_MTF); break; }6.3 几个我踩过的 MTF 坑第一个坑MTF 开启后如果 guest 执行了HLTCPU 会挂起MTF 不会触发host 也拿不回控制权。解决同时打开INTERCEPT_HLT在HLT退出里关闭 MTF。第二个坑MTF 和 NPF 同时触发时退出码优先级是 NPF 高于 MTF处理时要先判断EXITCODE再决定走哪个分支。第三个坑MTF 单步时如果 guest 执行了VMRUN会触发嵌套退出此时 MTF 状态要保存到影子 VMCB 里否则 L2 退出后 MTF 丢失。6.4 一个验证 Hook 生效的完整检查清单检查项方法预期结果VMCB 物理地址对齐检查低 12 位全 0VM_HSAVE_PA 写入读回 MSR等于分配地址EFER.SVME读 EFERbit12 为 1拦截位设置读 VMCB control目标位为 1MSR 返回值MTF 单步后读 RAX/RDX等于伪造值NPT 只读写目标页触发 NPF嵌套退出码L1 读 VMCB非 0 且与 L2 一致这张表是我每次调试新 Hook 时必过的流程少一项都可能留下玄学问题。最后说个习惯每次改完 VMCB 控制区先把整个 VMCB dump 出来看一眼确认没有残留的旧拦截位。这个动作花不了几秒钟但能省掉大量“为什么 guest 行为不对”的排查时间。希望帮到你。本文还有配套的精品资源点击获取