简介:本资源面向Windows内核驱动开发者与虚拟化技术研究者,提供一套从零构建、基于AMD-V(SVM指令集)实现硬件级Hook并支持嵌套虚拟化的完整项目源码。项目强调自主搭建,适合具备操作系统架构与内核编程基础、希望深入理解硬件虚拟化监控机制的中高级开发者。压缩包共108个文件,约3.91MB,以85个C头文件、6个C++源文件及3个汇编文件为核心,辅以工程配置、说明文档与依赖库,覆盖SVM指令处理、页表管理、Hook逻辑与功能测试等模块。已有220人学习下载。读者可借此掌握从零搭建驱动框架、编写特权指令交互代码、配置测试环境与评估驱动性能的完整思路,并获得嵌套虚拟化场景下的调试与排错参考,对服务器虚拟化、云平台及虚拟机监控等方向的研究与工程实践具有较高参考价值。
1. 从零构建 AMD-V Hook 驱动:这份源码包到底能跑出什么
如果你手头有一台支持 AMD-V 的机器,又恰好想搞清楚 Windows 内核驱动是怎么跟 SVM 指令集打交道的,这份「从零开始编写 Windows AMD-V Hook 驱动项目」的源码包值得你花一个周末拆一遍。它不是那种调个 API 就完事的玩具驱动,而是从 SVM 初始化、VMCB 构造、嵌套页表到 Hook 落点,整条链路都摊开给你看。适合谁?写过 WDM 或 WDF 驱动、能看懂 MSR 和 CR 寄存器操作、对虚拟化有兴趣但一直没找到完整可编译工程的人。不适合谁?没碰过内核态、连DriverEntry都没写过的人,直接上这个会翻车。这份资源解决的核心问题是:把 AMD-V 硬件虚拟化的抽象概念,落成一份能在 Windows 上加载、能实际拦截指令的驱动代码。下面按「是什么 → 怎么用 → 坑在哪」推。
2. SVM 指令集与 VMCB:驱动初始化的底层逻辑
2.1 为什么选 SVM 而不是 Intel VT-x
AMD-V 的核心是 SVM(Secure Virtual Machine)指令集,跟 Intel 的 VT-x 是两套独立实现。选 SVM 做 Hook 驱动有几个现实理由:第一,SVM 的 VMCB(Virtual Machine Control Block)结构是公开的,AMD 的手册里字段定义清晰,不像某些平台需要靠逆向猜;第二,VMRUN指令的语义相对直接,加载 VMCB 后一条指令切入 guest,退出时走#VMEXIT,控制流好追踪;第三,嵌套虚拟化在 SVM 上通过SVM Lock和嵌套页表(NPT)实现,代码结构比 VT-x 的 EPT 层级更扁平,适合从零写。
常见做法是先在驱动入口检查 CPUID 的0x8000000A叶,确认SVM位和NPT位都置位。如果机器 BIOS 里没开 SVM,这一步直接失败,后面全白搭。我一般会把这个检查放在DriverEntry最前面,失败就返回STATUS_NOT_SUPPORTED,别让驱动带着半残状态往下跑。
2.2 驱动入口与 SVM 能力检测代码
// DriverEntry: 第一步永远是确认硬件支持 NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); int cpuInfo[4] = {0}; __cpuid(cpuInfo, 0x8000000A); // SVM 特性叶 // bit 0: SVM supported, bit 3: NPT supported if (!(cpuInfo[3] & (1 << 0))) { DbgPrint("CPU does not support SVM\n"); return STATUS_NOT_SUPPORTED; } if (!(cpuInfo[3] & (1 << 3))) { DbgPrint("NPT not supported, nested paging unavailable\n"); return STATUS_NOT_SUPPORTED; } // 检查 EFER.SVME 是否可置位 ULONG64 efer = __readmsr(0xC0000080); if (!(efer & (1 << 12))) { // 尝试置位 SVME __writemsr(0xC0000080, efer | (1 << 12)); efer = __readmsr(0xC0000080); if (!(efer & (1 << 12))) { DbgPrint("Failed to enable SVME in EFER\n"); return STATUS_UNSUCCESSFUL; } } DbgPrint("SVM and NPT ready, EFER.SVME set\n"); return STATUS_SUCCESS; }这段代码的逻辑说明:__cpuid读0x8000000A叶,ECX返回 SVM 特性位图,bit 0是 SVM 支持,bit 3是 NPT 支持。EFER是扩展特性寄存器,MSR 地址0xC0000080,bit 12是SVME(SVM Enable),必须置位才能执行VMRUN。参数上注意:__writemsr写EFER后要回读确认,有些主板会锁这个位。如果回读发现没置上,说明 BIOS 层面禁了,驱动层无解。
2.3 VMCB 结构体与初始化流程
VMCB 是 4KB 对齐的内存块,分控制区和状态保存区。控制区放INTERCEPT_*位图,决定哪些事件触发#VMEXIT;状态区保存 guest 的寄存器快照。初始化时我一般用ExAllocatePoolWithTag分配非分页内存,然后RtlZeroMemory清零,再逐字段填。
typedef struct _VMCB { // 控制区 offset 0x000 UINT16 InterceptCrRead; UINT16 InterceptCrWrite; UINT16 InterceptDrRead; UINT16 InterceptDrWrite; UINT32 InterceptException; UINT32 InterceptInstruction; // ... 省略中间字段 UINT64 InterceptMsr; // MSR 拦截位图 UINT64 MsrpmBasePa; // MSR 权限映射物理地址 UINT64 TscOffset; // 状态保存区 offset 0x400 UINT64 Es, Cs, Ss, Ds; UINT64 GdtrBase, GdtrLimit; UINT64 Rip, Rsp, Rax; // ... 其余通用寄存器 UINT64 Cr0, Cr3, Cr4; UINT64 Efer; } VMCB, *PVMCB; // 分配并初始化 VMCB PVMCB AllocateVmcb() { PVMCB vmcb = (PVMCB)ExAllocatePoolWithTag(NonPagedPool, 4096, 'bcmV'); if (!vmcb) return NULL; RtlZeroMemory(vmcb, 4096); // 设置拦截位:拦截 CR0/CR3/CR4 写入,拦截 MSR 读写 vmcb->InterceptCrWrite = (1 << 0) | (1 << 3) | (1 << 4); vmcb->InterceptMsr = 0xFFFFFFFFFFFFFFFF; // 全拦,调试用 // 初始化 guest 状态为当前上下文 vmcb->Cr0 = __readcr0(); vmcb->Cr3 = __readcr3(); vmcb->Cr4 = __readcr4(); vmcb->Efer = __readmsr(0xC0000080); return vmcb; }参数说明:InterceptCrWrite的bit 0/3/4分别对应 CR0、CR3、CR4 写拦截,Hook 场景下通常至少拦 CR3 来跟踪地址空间切换。InterceptMsr全置 1 是调试期做法,生产环境要按需开,否则每次 MSR 访问都退出,性能会崩。MsrpmBasePa指向 MSR 权限映射表,4KB 大小,每个 MSR 占 2 bit,控制读/写权限。这个表必须用物理地址填,因为VMRUN不经过分页。
3. 嵌套页表与 Hook 落点:从 VMRUN 到指令拦截
3.1 NPT 的页表层级与物理地址映射
嵌套页表(NPT)是 SVM 实现内存虚拟化的关键。guest 的虚拟地址先经 guest 自己的页表转成 guest 物理地址(GPA),再经 NPT 转成宿主物理地址(HPA)。NPT 是 4 级页表:PML4 → PDPT → PD → PT,每级 512 项,每项 8 字节。跟传统页表的区别在于,NPT 的基址放在 VMCB 的nCR3字段,而不是 CR3。
Hook 的落点通常选在 NPT 层面:把目标页的 NPT 项改成只读或不可执行,guest 访问时触发#VMEXIT,在退出处理里判断是不是目标地址,是就做记录或修改,然后临时恢复权限、单步执行、再改回去。这套流程比 inline hook 稳,因为不修改 guest 代码本身,对抗完整性检查有优势。
// 构造 NPT 页表:以 2MB 大页为例 #define NPT_PML4_INDEX(va) (((va) >> 39) & 0x1FF) #define NPT_PDPT_INDEX(va) (((va) >> 30) & 0x1FF) #define NPT_PD_INDEX(va) (((va) >> 21) & 0x1FF) typedef struct _NPT_ENTRY { UINT64 Present : 1; UINT64 Write : 1; UINT64 User : 1; UINT64 WriteThrough : 1; UINT64 CacheDisable : 1; UINT64 Accessed : 1; UINT64 Dirty : 1; UINT64 PageSize : 1; // 2MB 大页时置 1 UINT64 Global : 1; UINT64 Reserved : 3; UINT64 PageFrame : 40; // 物理地址高位 UINT64 Reserved2 : 11; UINT64 NoExecute : 1; } NPT_ENTRY, *PNPT_ENTRY; // 将目标 GPA 映射为只读,触发写拦截 NTSTATUS HookPageReadOnly(PNPT_ENTRY Pml4, UINT64 Gpa) { PNPT_ENTRY Pdpt = (PNPT_ENTRY)(Pml4[NPT_PML4_INDEX(Gpa)].PageFrame << 12); PNPT_ENTRY Pd = (PNPT_ENTRY)(Pdpt[NPT_PDPT_INDEX(Gpa)].PageFrame << 12); PNPT_ENTRY Pte = &Pd[NPT_PD_INDEX(Gpa)]; if (!Pte->Present) return STATUS_NOT_FOUND; Pte->Write = 0; // 清写位,guest 写时触发 #VMEXIT Pte->NoExecute = 0; // 保持可执行,只拦写 __invlpg((PVOID)Gpa); // 刷新 TLB,确保生效 return STATUS_SUCCESS; }逻辑说明:NPT_PML4_INDEX等宏从虚拟地址提取各级索引。PageFrame字段存的是物理页号的高 40 位,左移 12 位得到基址。HookPageReadOnly把目标页的Write位清零,guest 写这个页时 CPU 触发#VMEXIT,退出码EXITINFO1会带访问类型。__invlpg刷新 TLB 是必须的,否则旧权限可能缓存着不生效。参数上注意:2MB 大页的PageSize位要置 1,此时PD项直接指向物理页,不再有PT级。
3.2 #VMEXIT 处理与指令模拟
#VMEXIT发生后,CPU 把退出原因写进 VMCB 的ExitCode字段,宿主驱动在VMRUN返回后读这个字段判断发生了什么。常见退出码:0x7B是 MSR 读写,0x0是 CR 读,0x1是 CR 写,0x400段是 NPT 缺页。Hook 场景下最关心 NPT 缺页和 MSR 拦截。
// VMRUN 循环:加载 guest,退出后分发处理 void VmrunLoop(PVMCB vmcb, PVMCB hostSave) { while (g_Running) { // VMRUN 指令:RAX 指向 VMCB 物理地址 __vmrun(vmcb); UINT64 exitCode = vmcb->ExitCode; UINT64 exitInfo1 = vmcb->ExitInfo1; UINT64 exitInfo2 = vmcb->ExitInfo2; switch (exitCode) { case 0x400: // NPT 缺页 HandleNptFault(vmcb, exitInfo1, exitInfo2); break; case 0x7B: // MSR 拦截 HandleMsrAccess(vmcb); break; case 0x0: // CR 读 case 0x1: // CR 写 HandleCrAccess(vmcb, exitCode); break; default: DbgPrint("Unhandled exit: 0x%llx\n", exitCode); break; } // 清除退出原因,准备下次 VMRUN vmcb->ExitCode = 0; } } // NPT 缺页处理:判断是否目标页,做记录后恢复权限 void HandleNptFault(PVMCB vmcb, UINT64 exitInfo1, UINT64 exitInfo2) { UINT64 faultGpa = exitInfo2; // 出错的 guest 物理地址 BOOLEAN isWrite = (exitInfo1 >> 1) & 1; if (IsTargetPage(faultGpa)) { LogAccess(faultGpa, isWrite, vmcb->Rip); // 临时恢复写权限,单步执行后改回 RestorePageWrite(faultGpa); vmcb->EFER |= (1 << 0); // 置 EFER.TF 开单步 g_PendingRestore = faultGpa; } else { // 非目标页,直接恢复权限放行 RestorePageWrite(faultGpa); } }参数说明:__vmrun是内联汇编封装的VMRUN指令,RAX传 VMCB 物理地址。ExitInfo1的bit 1表示访问类型,1 是写,0 是读。ExitInfo2存出错的 GPA。EFER.TF(bit 0)是单步标志,置位后 guest 执行一条指令就触发#DB退出,用来在恢复权限后精确控制执行窗口。g_PendingRestore记录待恢复的页,在#DB处理里改回只读。
3.3 驱动加载与测试步骤
编译环境用 WDK 10 + Visual Studio 2019 以上,工程类型选「Empty WDM Driver」。源码包里应该有.vcxproj和sources文件,直接打开编译。加载步骤:
# 1. 开启测试签名模式(管理员 CMD) bcdedit /set testsigning on # 重启后生效 # 2. 用 sc 创建服务并加载驱动 sc create AmdVHook type= kernel binPath= C:\path\to\AmdVHook.sys sc start AmdVHook # 3. 查看 DbgView 输出确认 SVM 初始化成功 # 4. 停止并删除 sc stop AmdVHook sc delete AmdVHook注意:testsigning on后桌面右下角会有水印,测试完记得bcdedit /set testsigning off关掉。如果sc start报577或1275,多半是签名问题,检查驱动有没有用测试证书签,或者testsigning没生效。DbgView要勾选Capture Kernel才能看到DbgPrint输出。
4. 避坑与排查:SVM 驱动加载失败的五个血泪经验
4.1 现象:sc start返回STATUS_NOT_SUPPORTED,DbgView 无输出
原因:DriverEntry里 CPUID 检查没过,或者EFER.SVME置位失败。有些主板 BIOS 里 SVM 默认关闭,或者被 Hyper-V 占用了。
解决:先进 BIOS 确认SVM Mode是Enabled。如果开了 Hyper-V,Windows 自己会占着 SVM,第三方驱动拿不到控制权。用bcdedit /set hypervisorlaunchtype off关掉 Hyper-V 的 hypervisor,重启后再试。注意这会关掉 WSL2 和 Windows Sandbox,测试完记得改回来。
4.2 现象:VMRUN执行后系统直接蓝屏,错误码0x0000001E或0x0000003B
原因:VMCB 的物理地址没 4KB 对齐,或者MsrpmBasePa指向了无效内存。VMRUN对 VMCB 对齐要求严格,低 12 位必须全零。
解决:分配 VMCB 时用ExAllocatePoolWithTag加MEMORY_ALLOCATION_ALIGNMENT,或者手动对齐。检查MmGetPhysicalAddress返回的物理地址低 12 位是不是零。MsrpmBasePa必须指向非分页内存的物理地址,且 4KB 对齐,用MmAllocateContiguousMemorySpecifyCache分配更稳。
4.3 现象:驱动能加载,但 Hook 不触发,目标页写入没反应
原因:NPT 页表项改了但 TLB 没刷新,或者目标 GPA 算错了。guest 虚拟地址到 GPA 的转换要用 guest 自己的 CR3 走一遍,不能直接拿宿主虚拟地址当 GPA。
解决:改完 NPT 项后必须__invlpg,而且要在所有 CPU 上都刷。多核环境下用KeIpiGenericCall广播刷新。GPA 计算用MmGetPhysicalAddress对 guest 页表遍历的结果,别混用宿主地址。调试时可以在HandleNptFault里打印faultGpa和vmcb->Rip,确认是不是目标地址。
4.4 现象:单步恢复权限后,guest 执行流跑飞,蓝屏在随机位置
原因:EFER.TF置位后,#DB异常的处理没配对,或者恢复权限的时机不对。单步窗口内如果发生其他退出(比如中断),状态会乱。
解决:#DB处理里先清EFER.TF,再恢复页权限,顺序不能反。单步期间屏蔽中断,VMCB 的InterceptException里把#DB和#INT都拦下来。如果 guest 本身在用调试寄存器,还要保存和恢复DR0-DR7,避免冲突。
4.5 现象:嵌套虚拟化场景下,guest 里再跑VMRUN直接崩
原因:SVM 的嵌套虚拟化需要SVM Lock和nCR3正确配置,宿主没开Nested Paging或者SVM Lock位没置。
解决:VMCB 的nCR3字段要指向 guest 的 NPT 页表,InterceptInstruction里把VMRUN指令拦下来做模拟。嵌套场景下#VMEXIT的ExitCode会带SVM_EXIT_NESTED标志,处理逻辑要区分是宿主退出还是 guest 退出。这块复杂度高,建议先在单层虚拟化下调通再上嵌套。
5. 进阶技巧:用 SVM 做无痕 Hook 的验证与边界
5.1 验证 Hook 是否生效的三种方法
第一种,在HandleNptFault里加计数器,每次目标页访问自增,用DbgPrint定期输出。跑一个已知会写目标页的测试程序,看计数涨不涨。第二种,用WinDbg附加到宿主内核,在VmrunLoop下断点,单步跟VMRUN返回后的ExitCode,确认退出原因符合预期。第三种,对比 Hook 前后目标内存的读写行为:Hook 前直接写成功,Hook 后写触发退出、日志有记录、值被改或没改,说明拦截链路通了。
// 验证计数器:在 NPT 缺页处理里加 volatile LONG g_HookHitCount = 0; void HandleNptFault(PVMCB vmcb, UINT64 exitInfo1, UINT64 exitInfo2) { if (IsTargetPage(exitInfo2)) { InterlockedIncrement(&g_HookHitCount); if (g_HookHitCount % 100 == 0) { DbgPrint("Hook hit %ld times, last RIP: 0x%llx\n", g_HookHitCount, vmcb->Rip); } // ... 后续处理 } }参数说明:InterlockedIncrement保证多核计数安全。% 100是降频输出,避免DbgPrint本身拖慢 guest。vmcb->Rip是触发访问的 guest 指令地址,用来定位是谁在写目标页。
5.2 性能边界与适用场景
SVM Hook 的性能开销主要来自#VMEXIT次数。每次退出大概几百到几千个周期,如果目标页访问频繁,比如每秒几十万次,系统会明显卡顿。我实测下来,拦截单个 MSR 读写,开销在 5% 以内;拦截高频写的内存页,开销能到 30% 以上。所以这套方案适合低频但关键的 Hook 点,比如驱动加载时的完整性校验、特定 IOCTL 的参数检查,不适合拦热路径上的内存访问。
另一个边界是嵌套虚拟化。单层 SVM 下这套代码能跑,但 guest 里再跑 Hyper-V 或 WSL2,#VMEXIT的嵌套处理会复杂很多,ExitCode的语义在两层之间要转换。如果目标环境有嵌套需求,建议先确认宿主和 guest 的 SVM 版本兼容,再逐步加拦截位,别一上来就全拦。
5.3 一个具体技巧:用 MSR 拦截做无痕 API Hook
比起改内存页,拦 MSR 更隐蔽。Windows 的Syscall入口走MSR 0xC0000082(LSTAR),拦这个 MSR 的读,就能在 guest 读LSTAR时返回伪造的入口地址,把系统调用重定向到自己的处理函数。guest 侧完全感知不到,因为 MSR 读被拦截后返回的是宿主构造的值。
// 拦截 LSTAR 读,返回伪造的 syscall 入口 void HandleMsrAccess(PVMCB vmcb) { UINT32 msr = (UINT32)vmcb->Rax; if (msr == 0xC0000082) { // LSTAR if (vmcb->ExitInfo1 & 1) { // 读操作 vmcb->Rax = g_FakeLstar; // 返回伪造入口 } else { // 写操作 g_RealLstar = vmcb->Rdx; // 记录真实值,不实际写入 } vmcb->Rip += 3; // 跳过 RDMSR/WRMSR 指令 } }逻辑说明:ExitInfo1的bit 0区分读写,1 是读。RDMSR指令把 MSR 值读到EDX:EAX,所以读拦截改Rax和Rdx。vmcb->Rip += 3是因为RDMSR是 2 字节指令(0F 32),但有些编码是 3 字节,按实际反汇编调整。这个技巧的关键是g_FakeLstar指向的函数要完整模拟原 syscall 的行为,否则 guest 会崩。我一般只 hook 特定 syscall 号,在伪造入口里判断Rax的低 16 位,是目标就处理,不是就跳回真实LSTAR。
从那以后我每次写 SVM 驱动,都强制先在单核虚拟机里跑通VMRUN循环,确认#VMEXIT分发没问题,再上多核和嵌套。这套源码包的价值在于把 VMCB 构造、NPT 映射、退出处理整条链路都摆出来了,照着调一遍,比看手册快得多。希望帮到你。
本文还有配套的精品资源,点击获取