
简介面向64位Windows平台安全开发者的VT-x虚拟化源码工程聚焦VT技术在进程保护与进程隐藏场景中的落地实现。压缩包共26个文件总大小240KB以C源码、头文件和驱动配置为主另包含汇编、符号及Visual Studio工程配置覆盖从VMX初始化到进程隐藏的核心代码路径。头文件定义了VMCS、内存与关键结构C文件对应VT启动及各功能实现可直接衔接驱动加载流程。已有1451人学习下载适合具备内核驱动基础、希望借助VT-x强化系统安全能力的开发者参考。通过研究源码可了解64位环境下虚拟化扩展的启用流程、核心数据结构组织方式以及如何借助VT能力实现关键进程的硬件级保护与隐藏为自研反调试、反恶意软件或安全监控组件提供直接可借鉴的工程模板。1. VT_X64源码把64位进程藏进虚拟化层的东西拿到这份VT_X64源码包时我先确认了一件事这不是一个普通的驱动模板而是把64位进程保护这件事直接下沉到了VMX root模式。VT源码里常见的东西是虚拟机监视器但这套源码的目标不是跑虚拟机而是让目标进程在x86_64下获得“内存不可写、进程不可见”的硬件级保护。它解决的核心问题很具体在当前Windows进程模型里Ring0驱动一旦被穿透保护逻辑等于没有而VT_X64的方式是把保护逻辑放到hypervisor层让被保护进程的地址空间和任务链都受VMX控制。适合的读者是已经有驱动开发经验、想上硬件虚拟化做安全产品的开发者不适合零基础入门者——你需要先懂CR4、VMCS、EPT再看这份源码才有意义。2. 初始化关VMXON的时序与x64编译环境2.1 为什么VT保护在x64下必须走VMX root先明确一个概念64位系统下内核驱动只能停留在“无特权但高权限”的Ring0而VT技术则多了一个VMX root模式CPU指令行为、内存访问、中断投递全部由VMCS里的控制域接管。在做进程保护时常见做法是在普通Ring0驱动里挂钩SSDT或inline hook但这类方案在PatchGuard和现代反篡改机制面前很容易暴露。VT_X64源码走的路径不同它先把CPU切换到VMX root然后让目标进程所在VM产生可控的VM-exit在exit handler里拦截内存访问和任务链枚举。这样做的好处是保护逻辑本身不在被监控的地址空间里恶意程序哪怕拿到了Ring0权限也看不到VT层自身的数据。这套源码里vtstart.c和asm64.asm是入口Driver.c负责驱动主流程。启动时序我建议按“先检测CPU能力再分配VMXON区域最后置位CR4.VMXE”走不要反过来。原因很实际一旦CR4.VMXE置位后CPU进入VMX操作模式失败系统会直接#GP异常没有后悔药。2.2 打开CR4.VMXE初始化三段式与失败兜底VT初始化的三段式可以简化为检测CPUID.1:ECX.VMX[bit5] → 分配并清零VMXON region → 执行VMXON指令。核心代码逻辑如下注意这段代码我做了简化真实源码里还包含VMCS region的物理地址对齐和IA32_VMX_BASIC MSR校验。// vtstart.c 初始化核心流程示意 BOOLEAN VT_Init(VOID) { ULONG_PTR cr4; PVOID vmxon_region; if (!VT_CheckCpuSupport()) { // 检测CPUID VMX位 return FALSE; } // 分配VMXON region必须按4KB对齐 vmxon_region MmAllocateNonPagedPoolWithTag( sizeof(VMXON_REGION), XTVM ); if (vmxon_region NULL) return FALSE; // 置位CR4.VMXE开启虚拟化扩展 cr4 __readcr4(); __writecr4(cr4 | 0x2000); // 0x2000 CR4.VMXE _disable(); // 关闭中断防止切换过程被打断 if (VMXON(vmxon_region) ! STATUS_SUCCESS) { _enable(); __writecr4(cr4); // 失败后恢复原CR4值 return FALSE; } _enable(); return TRUE; }逻辑说明__readcr4和__writecr4是编译器内建指令直接控制CR4寄存器。VMXON指令需要一个物理地址指向VMXON region这个region的内容在Intel手册里有明确要求前4字节必须是VMCS revision identifier。我一般会从MSR IA32_VMX_BASIC里读出来填进去直接用0会翻车。另外asm64.asm里提供了汇编版本的VMXON封装C调用时注意参数传递约定x64下前四个参数走rcx/rdx/r8/r9别用x86的栈传参思维。参数说明代码里的0x2000是CR4.VMXE位不同架构手册上固定不变。MmAllocateNonPagedPool的前缀参数不用纠结重点是返回地址必须是物理连续的否则VMXON时CPu会拒绝执行。VT_CheckCpuSupport里我会额外查一下IA32_FEATURE_CONTROL MSR的锁定位如果固件锁定了VMX直接返回FALSE比在VMXON时蓝屏更体面。2.3 编译工程x64 Win7Release配置与驱动签名源码包里带的是VT_X64.vcxproj和VT_X64.sln从文件列表看是Visual Studio工程配置目录写着x64 Win7Release说明目标平台是Win7 x64。我实际操作时发现这个工程在Win10 SDK下也能编译但要注意WDK版本用Win10 WDK编译出来的驱动在Win7上跑需要改inf文件里的版本号否则安装时会被拒绝。编译命令我一般直接走MSBuild不用IDEmsbuild VT_X64.sln /p:ConfigurationWin7Release /p:Platformx64 /t:Clean,Rebuild /m逻辑说明/m参数并行编译能快不少。但如果vtmisc.c和BaseFunction.c之间有依赖关系偶尔会因为并行构建导致头文件生成时序问题——这里两个文件不涉及代码生成所以可以放心开/O2优化。编译完成后驱动签名是必须跨过的坎测试机上先关掉驱动签名强制或者用测试签名模式加载。setbcd工具在Win7 x64下使用test signing on可以加载自签名驱动但Win10 1607之后建议直接用Attestation签名或走WHQL别在研究过程中浪费时间在签名上。3. VMCS与VM-exit把缺页异常变成自己的门禁3.1 VMCS关键域配置VM-execution controls优先级进程保护的核心拦截点不在VMXON而在VMCS的配置。VMCS是VT的内存数据结构里面记录了VM退出条件、Guest状态、Host状态。VT_X64源码中的vmxstruct.h把这些域定义成了结构体配置时最容易踩坑的是VM-execution control fields的优先级必须先把Primary Processor-Based VM-Execution Controls里的“启用EPT”位配好再去设置Secondary Controls否则CPU会认为VMCS非法。// vmxstruct.h / vtstart.c 中的VMCS配置示意 // 打开VMCS区域的初始化写操作 void VmcsWrite(DWORD field, SIZE_T value) { __vmx_vmwrite(field, value); } // 配置执行控制域 VmcsWrite(VMCS_CTRL_PIN_BASED, 0x0000003F); VmcsWrite(VMCS_CTRL_PRIMARY_PROC_BASED, 0x0000001A | CPU_BASED_ACTIVATE_SECONDARY_CONTROLS); VmcsWrite(VMCS_CTRL_SECONDARY_PROC_BASED, SECONDARY_ENABLE_EPT | SECONDARY_ENABLE_VPID);逻辑说明VMCS_CTRL_PIN_BASED控制外部中断和NMI的退出行为0x3F表示中断发生时产生VM-exit。Primary域里必须把bit31activate secondary controls打开Secondary域才能生效。这里启用了EPT和VPIDVPID主要用来避免TLB flush对进程保护场景里的性能有帮助。整个配置顺序搞反了VMXON之后第一次VMLAUNCH就会失败错误码在VMX_INSTRUCTION_ERROR字段里可以查到常见值是7或8代表“无效的VMCS域值”。参数说明这些控制域的具体bit位定义在Intel SDM Vol.3C第24章里源码注释里基本都标了。如果要从Win7迁移到Win10/11注意Secondary域里多出的bit比如ENABLE_VMFUNC、ENABLE_EPT_SUPER_PAGE要在初始化时按Capability MSR校准强行打开CPU不支持的位会直接VMfail。3.2 挂VM-exit处理对缺页异常做内存访问仲裁VM-exit handler是整个保护机制的门禁。当被保护进程访问到特定内存页时CPU触发EPT violation产生VM-exit控制权交到VT层的handler。这段逻辑在BaseFunction.c里核心是区分“这个访问是不是我们允许的”// BaseFunction.c VM-exit handler简化逻辑 SIZE_T VT_VmExitHandler(PVOID GuestRip, PVOID GuestRsp) { VMX_EXIT_QUALIFICATION qual; ULONG exit_reason; exit_reason (ULONG)VmcsRead(VMCS_EXIT_REASON); qual.raw VmcsRead(VMCS_EXIT_QUALIFICATION); if (exit_reason EXIT_REASON_EPT_VIOLATION) { if (IsProtectedPage((PVOID)qual.fault_address)) { return HandleProtectedAccess(GuestRip, GuestRsp); } // 非保护页面的EPT violation直接放行 return VmcsRead(VMCS_GUEST_RIP) GetInstructionLength(); } return VmcsRead(VMCS_GUEST_RIP) GetInstructionLength(); }逻辑说明VM-exit之后Guest的RIP在VMCS_GUEST_RIP里处理完要手动加上指令长度否则回到Guest会重复执行同一条指令形成死循环。HandleProtectedAccess是我根据项目语义补全的仲裁函数如果访问来自受信任的Ring0调用改页表属性后放行如果来自未知模块则修改Guest RIP跳过该指令或者注入一个异常给Guest——这块是进程保护的核心决定点。参数说明exit_reason为0x30EPT violation时Exit qualification的低48位是访问的Guest物理地址。这里注意VMCS_EXIT_QUALIFICATION读到的是物理地址如果要判断是否命中保护页需要把虚拟地址转成物理地址后比对否则会漏判。常见做法是用EPT页表维护一张保护页面的PFN集合查询O(1)避免每次exit都跑全表。3.3 修改物理页属性让目标进程地址只读不可写保护进程的内存不被篡改最直接的方式是在EPT层面把目标页改成只读。VT_X64源码里用mem.c和BaseFunction.c来维护一张EPT映射表核心动作是把保护页从RW改成RO// mem.c 修改EPT页表项示意 void EptSetPageReadOnly(PHYSICAL_ADDRESS PagePhysAddr) { PVOID ept_pte; SIZE_T pte_index; // 定位到EPT PTE硬件黑色盒子里找映射 ept_pte EptGetPte(PagePhysAddr); pte_index EptGetPteIndex(PagePhysAddr); // 清写位保留读位 ept_pte[pte_index] ~EPT_PTE_WRITE; __invvpid(VPID_TAG, 0); // 让TLB/EPT缓存失效 }逻辑说明EPT页表结构类似普通页表但只读位是bit1写位是bit2。修改PTE后必须做INVVPID否则CPU缓存的EPT条目不刷新修改不生效。这段代码在源码里分布在mem.c和asm64.asm里VPID指令的封装在汇编里做C传tag过去。这里最容易被忽略的点是EPT页表本身要标记为“不属于任何Guest”否则Guest里的恶意代码可以通过改CR3尝试访问EPT页表。参数说明EPT_PTE_WRITE在我看的源码头文件里没有显式定义需要自己按VMM语义补值是4bit2。INVVPID的类型参数0表示individual address如果整片地址空间都改了建议用类型1做全域flush代价是一次VM-exit但对多进程保护场景更可靠。4. 进程隐藏的三种实现路径4.1 链式摘除让任务管理器看不到进程隐藏进程最直接的做法是摘除EPROCESS链。在普通Ring0下这个操作会被各种反作弊和EDR拉黑因为ActiveProcessLinks摘除后所有基于遍历的检测工具都将失效但这也会导致进程相关功能异常。VT_X64源码在VMX root下做摘除动作本身还是写EPROCESS里的链表指针但区别在于摘除动作发生在VT层Guest里的检测代码看不到是谁改的也无法在系统调用层拦截这个动作。// BaseFunction.c 摘除进程链示意片段 // 参数TargetEProcess - 目标进程EPROCESS地址 VOID HideProcessByLink(PVOID TargetEProcess) { PLIST_ENTRY link; link (PLIST_ENTRY)((PUCHAR)TargetEProcess 0x188); // Win10 20H1偏移 if (link-Flink link-Blink) { link-Blink-Flink link-Flink; link-Flink-Blink link-Blink; } // 置链自指向防止二次摘除 link-Flink link-Blink link; }逻辑说明不同系统的EPROCESS偏移不同Win7是0x188附近Win10 1607是0x2E81809又改成0x448源码包大概率按Win7写死移植到新系统要生成偏移表。我在做模拟项目X时习惯用动态获取方式——从PsActiveProcessHead开始遍历比较进程名再算偏移而不是硬编码。摘除链后任务管理器直接看不到进程但进程的PID仍然存在有经验的检测方会用断链检测法反查你。参数说明摘链后必须把Flink和Blink指向自身否则系统在进程退出时做Unlink会访问野指针蓝屏。另外如果进程被多线程同时枚举链表操作要加自旋锁最简单做法是在VT层用原子比较交换别直接赋值。4.2 虚拟化身影VMX root下隐写内核任务链链式摘除只能骗过任务管理器骗不过专门的反隐藏工具。VT源码里另外一条隐藏路径是“身”思路是在另一个Guest里构造一个假的进程列表把真实进程从枚举视图里抹掉。这个方案的工程量不小需要维护两套进程映射真实Guest里跑业务检测者看到的进程枚举由VT层伪造响应。具体到实现上是把NtQuerySystemInformation的调用路径拉长当Guest发起SystemProcessInformation查询时VT层捕获VM-exit把返回缓冲里目标进程的条目替换成别的进程信息。这套逻辑在源码里没有完整实现只有BaseFunction.c里的部分回调框架。常见做法是直接在VMCS的IO指令拦截点上做对触发查询的进程重定向它的内存读取到伪造缓冲区。代价是性能有损耗每次进程枚举都会产生大量VM-exit所以源码里这种“伪造”模式一般只保护一个进程不会全局生效。4.3 反检测采样延迟响应和篡改响应进程隐藏还有一个容易忽略的维度时间。就算你摘了链、改了枚举检测工具只要周期性采样进程列表就能通过“前后差异”发现被隐藏的进程。VT_X64源码里有个思路我觉得很值得参考在VMX root下让Guest内部的查询请求产生“错觉”——把目标进程的CPU时间、内存计数都伪装成系统空闲进程。// vtstart.c 伪造响应数据示意 // 拦截到进程信息查询后对目标进程条目做篡改 VOID ForgeProcessInfo(PVOID SystemInfoBuffer, ULONG TargetPid) { PSYSTEM_PROCESS_INFORMATION p; for (p SystemInfoBuffer; p; p NEXT_PROCESS_INFO(p)) { if ((ULONG)p-UniqueProcessId TargetPid) { p-UniqueProcessId 0; // 伪装成System进程PID p-InheritedFromUniqueProcessId 0; p-ThreadCount 0; } } }逻辑说明把目标PID改成0进程名不一定是System但很多检测工具依赖PID来计算进程树改成0后父子关系整个断掉后续的路径分析就失效了。这里有个边界伪造后的PID和实际PID对不上如果目标进程有外部IPC服务注册的session可能引发句柄访问异常。我在自己的项目里只对“非交互式服务进程”启用这个伪装交互进程藏起来会引发桌面会话错乱。参数说明NEXT_PROCESS_INFO宏是按SystemProcessInformation结构的NextEntryOffset偏移走的实际结构里的偏移是内核版本敏感的。这段代码如果要搬到另一台机器建议直接根据NextEntryOffset动态遍历别按固定结构体硬解析。5. VT源码常见问题与避坑手册5.1 现象驱动加载后首次VMXON就蓝屏错误码0x101原因CR4.VMXE已经置位但VMXON region的物理地址没按4KB对齐或者region里的VMCS revision ID填错。我在源码里看到的是直接调用MmAllocateNonPagedPool这个函数返回的虚拟地址是8字节对齐不是4KB对齐必须手动取整到4KB边界。否则CPU执行VMXON时会报VMXON with invalid VMCS region。解决分配时多分配4095字节然后ALIGN_UP到4KB边界再调用虚拟机监视器指令初始化完成后把这个地址存到全局变量里VMLAUNCH/VMRESUME也要用同一个地址。5.2 现象VM-exit处理后Guest RIP跳飞直接IRQL蓝屏原因处理器退出VM后VM-entry时Guest RIP恢复有偏差代码里GetInstructionLength()拿到的指令长度是错的。常见触发场景是处理EPT violation时Guest里执行的是SSE/AVX指令——这类指令长度可能超过15字节而VT返回的Exit qualification里没有指令长度只能靠指令解码。解决我一般直接用Shadow Instruction Decoder库去算真正的指令长度而不是加一个固定字节数。VT_X64源码里的vtmisc.c应该有基础解码逻辑但覆盖不到所有指令前缀组合建议补几条边界指令的测试用例。5.3 现象进程保护只对单核生效多核调度后保护失效原因VT初始化通常只针对当前CPUWindows多核系统下目标进程被调度到其他核时那个核上没有VMCS上下文保护逻辑就不会触发。解决要么给每个CPU都做VMXON并复制一份VMCS要么用SetThreadAffinityMask把目标进程钉在固定核上。源码包里的VT_X64没有主动做多核初始化vtstart.c只写了启动核。我自己的做法是配合KeSetSystemAffinityThread把目标线程绑核同时在DPC里对所有离线CPU发起远程VMXON。5.4 现象驱动卸载时死锁系统句柄泄漏原因VT层保护了某个进程的地址空间卸载时Guest里的进程还在运行EPT页表被释放后进程访问了自由内存。解决卸载顺序必须严格是“先让保护进程退出或解除保护再释放EPT页表最后VMXOFF”倒过来必蓝屏。可以在DriverUnload里加一个定时等待函数最多等3秒让受保护进程摘除保护超时就直接拒绝卸载。6. 验证技巧从加载驱动到确认进程不可见拿到源码编译出驱动后怎么确认它真的生效了我会用一套固定的验证脚本不用虚拟机监视器调试器也行但加上Windbg能省不少事。先把测试环境跑起来安装签名驱动后用命令行工具开一个保护进程# 验证进程是否在系统进程列表中可见管理员权限运行 tasklist /fi PID eq 1234 sc query vt_x64逻辑说明sc query是查驱动服务状态如果vtx64服务显示running说明VMXON没崩。tasklist查不到目标进程说明链式摘除或者枚举伪装生效了。这里要区分两种情况摘链的隐藏方式下tasklist直接空伪枚举方式下tasklist有进程但PID被改成0。进一步验证用的是进程内存保护是否生效对一个被保护的进程地址发起写操作如果保护逻辑正常会触发VT层拦截写入失败且进程不蓝屏。我通常是直接给目标进程投递一个内存写入工具看看返回值是否被拒绝。更深入的验证用Windbg连上虚拟机# Windbg 命令附加到目标系统后检查VT初始化状态 !process 0 0 ProtectedProcess.exe !pte 目标进程地址逻辑说明!process能确认进程实体存在但不在任务链枚举里!pte看页表属性如果EPT改了只读PTE的写位会是0。注意!pte显示的是普通页表不是EPT页表——想看EPT需要额外的VMM扩展命令所以这一步只能做辅助判断核心验证还是靠写入测试。验证流程走完后我通常会再做一个回归项被隐藏的进程能否正常创建线程、访问网络、读写自己的配置文件。很多进程隐藏方案把进程整成“看得见摸不着”的状态业务代码跑几步就崩。VT方案的优势在于物理层拦截但如果EPT页属性改得过狠进程内部的一切写操作全被拦那保护就成了自残。我一般会在保护策略里加一个“写白名单”目标进程自身的堆栈页和代码段写操作放行只拦外部写入修改。这么做需要额外维护一层权限判断但可用性至少能保证。从那以后我每次给客户演示VT进程保护都强制走一遍“先隐藏再访问再篡改”三步验证确认隐藏不崩、写保护生效、业务不受影响才算通过。VT_X64这套源码的价值就在于把这几件事的物理层基础打好了——后面要怎么发力全看你在EPT和VM-exit handler里怎么补自己的策略。希望帮到你。本文还有配套的精品资源点击获取