写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
看此教程之前,问几个问题,基础知识储备好了吗?保护模式篇学会了吗?练习做完了吗?没有的话就不要继续了。
华丽的分割线
概述
在学习如何实现最小VT
框架的时候,我们先看一下流程图:
本篇我们介绍进入虚拟机模式前这部分内容,剩下的部分我们在下一篇继续。
如下是更清晰的一些流程,以后我们重点看下面的图:
如上实现都必须在具有0环的权限才可以,最方便的当然是在驱动内实现,如何写驱动我就不赘述了,自己回头复习去。如下是我们驱动代码的基本框架:
#include
#include
#define DbgPrintLine(X) DbgPrint(X##"\n")
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
DbgPrintLine("Unloaded Successfully!");
return STATUS_SUCCESS;
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;
DbgPrintLine("Loaded Successfully!");
return STATUS_SUCCESS;
}
intrin.h
这个头文件的作用我就不说了,但是提前说一句,并不是所有的指令在32位都是包装好的,这些指令都是内联函数。有些包装好的指令是在64位才能用的,比如__vmx_on
等需要传QWORD
参数的函数我们需要自己实现,微软并没有帮我们实现该功能。当然在64位的情况下,我们就可以用更多的指令了,由于我们是32位的,就自己实现好了,虽然有点小麻烦。
话不多说,开始进入正题。
VT 支持启用检测
在之前的老的CPU
,它是不支持VT
的。现在的新的支持VT
的CPU
,也是有个开关的。就算CPU
支持VT
,如果被关掉禁用了,你也没法用,就需要我们进行检测是否能够支持启用VT
,故写了一个函数,如下是完整代码,后续详细讲解:
BOOLEAN CheckVTEnabled()
{
//此内部函数将指令返回的受支持功能和 CPU 信息存储在 cpuInfo 中,
// cpuInfo 是一个由四个 32 位整数组成的数组,其中依次填充了 EAX、EBX、ECX 和 EDX 寄存器的值。
int CPUInfo[4];
__cpuid(CPUInfo, 1); //调用 CPUID 需要一个参数,参数就是1,通过 ECX 的值的索引5位就是 VMX
int Info = CPUInfo[2];
if (!(Info & VMXBit))
{
DbgPrintLine("Error : CPUID");
return FALSE;
}
ULONGLONG CONTROL_MSR = __readmsr(IA32_FEATURE_CONTROL_MSR);
if (!(CONTROL_MSR & IA32_FEATURE_CONTROL_MSR_Lock))
{
DbgPrintLine("Error : FEATURE CONTROL MSR");
return FALSE;
}
ULONG cr0 = __readcr0();
if (!((cr0 & CR0_PE) && (cr0 & CR0_NE) && (cr0 & CR0_PG)))
{
DbgPrintLine("Error : CR0");
return FALSE;
}
ULONG cr4 = __readcr4();
if (cr4 & CR4_VMXE)
{
DbgPrintLine("VT Has Been Occupied!");
return FALSE;
}
return TRUE;
}
__cpuid
是对CPUID
汇编指令的封装,我们先看看Intel
是怎样说明该函数的:
CPUID returns processor identification and feature information in the EAX, EBX, ECX, and EDX registers. The instruction’s output is dependent on the contents of the EAX register upon execution (in some cases, ECX as well).
现在的CPU
一般都支持CPUID
,如果实在不放心的话可以检测EFLAGS
的索引21二进制位是否可以修改设置,如下是白皮书说明:
The ID flag (bit 21) in the EFLAGS register indicates support for the CPUID instruction. If a software procedure can set and clear this flag, the processor executing the procedure supports the CPUID instruction.
这里我认为现在使用的CPU
都支持CPUID
指令。该指令是一个非常复杂的指令,具体可以查看白皮书的第764
页,有关eax
这个参数每个值的含义,具体看白皮书的第765
页,我们使用的参数是1
,我们可以看一下它的内容:
代码注释我明确说明用到的是ecx
,我们看一下为什么:
这只是表格的一部分,但对于我们有用的就足够了。注意我们的VMX
位,就在这个里面。通过这条CPUID
指令我们只是判断CPU
是否支持VT
,但通过vmxon
指令启用VT
还有一些必备条件的。
白皮书开头说我们的CR4
的VMXE
位需要置1,并且在MSR
的MSR_IA32_FEATURE_CONTROL
成员的索引0位必须是1,否则使用vmxon
指令启用VT
会触发通用保护异常。这个只能在BIOS
内进行设置,否则也会触发,通过这个我们就可以判断VT
是否被禁用了,如下是相关的中文说明:
当然这些条件远远不够,如下是白皮书的说明:
The first processors to support VMX operation require that the following bits be 1 in VMX operation: CR0.PE, CR0.NE, CR0.PG, and CR4.VMXE. The restrictions on CR0.PE and CR0.PG imply that VMX operation is supported only in paged protected mode (including IA-32e mode). Therefore, guest software cannot be run in unpaged protected mode or in real-address mode.
也就是说,必须在带有分页的保护模式下才能正常使用VT
,为了简单处理我们不使用虚拟机嵌套,所以CR4.VMXE
这个位如果是1,说明我再启用就是套娃了,不跟你套。
如上是我写的函数的所有细节了,我们来做个实验,在做实验之前我们的驱动入口代码修改为如下:
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;
DbgPrintLine("Loaded Successfully!");
if (CheckVTEnabled())
{
DbgPrintLine("MiniVT : VT Support!");
}
return STATUS_SUCCESS;
}
然后编译,拖到虚拟机里进行加载,通过DbgView
我们就可以得到如下结果,表示成功:
VMXON
上面我们实现了VT
是否支持启用的检测函数,下面我们再实现两个函数,实现VT
技术的启用和关闭,它的函数原型如下:
BOOLEAN StartVT();
BOOLEAN StopVT();
由于__vmx_on
微软并没有在32位帮我们封装起来,需要我们自行实现,函数如下:
BOOLEAN __vmx_on(DWORD32 LVMXONRegionPA, DWORD32 HVMXONRegionPA)
{
_asm
{
push[HVMXONRegionPA];
push[LVMXONRegionPA];
_emit 0xF3;
_emit 0x0F;
_emit 0xC7;
_emit 0x34;
_emit 0x24; // vmxon qword ptr [esp]
add esp, 8;
}
UINT32 eflags = __readeflags();
if (eflags & EFLAG_CF)
{
return FALSE;
}
return TRUE;
}
使用_emit
是因为编译器并不支持vmxon
编译,所以只能内嵌了。vmxon
这个指令并不是一定会成功的,如果失败会放到EFLAG
的CF
位,是0表示成功,如下是白皮书的说明:
Execute VMXON with the physical address of the VMXON region as the operand. Check successful execution of VMXON by checking if EFLAGS.CF = 0.
好,我们开始实现启用VT
的代码,具体代码如下:
BOOLEAN StartVT()
{
if (CheckVTEnabled())
{
DbgPrintLine("MiniVT : VT Support!");
__writecr4(__readcr4() | CR4_VMXE);
PVOID pVMXONRegion = ExAllocatePoolWithTag(NonPagedPool, 0x1000, 'vmx');
if (!pVMXONRegion)
{
DbgPrintLine("Error : vmx Alloc Error");
return FALSE;
}
RtlZeroMemory(pVMXONRegion, 0x1000);
*(UINT32*)pVMXONRegion = (UINT32)__readmsr(MSR_IA32_VMX_BASIC)&0x7FFFFFFF;
g_VMXCPU.pVMXONRegion = pVMXONRegion;
g_VMXCPU.pVMXONRegion_PA = MmGetPhysicalAddress(pVMXONRegion);
return __vmx_on(g_VMXCPU.pVMXONRegion_PA.LowPart, g_VMXCPU.pVMXONRegion_PA.HighPart);
}
return FALSE;
}
在解释之前,我们来看看白皮书是咋说的:
前两个小黑点我们已经做完了,下面继续,它让我们申请一个4KB
对齐的VMXON Region
,至于到底多大呢?我们需要查阅IA32_VMX_BASIC_MSR
这个寄存器,这个寄存器的信息说明如下:
然后我们注意到这一句话:
Bits 44:32 report the number of bytes that software should allocate for the VMXON region and any VMCS region. It is a value greater than 0 and at most 4096 (bit 44 is set if and only if bits 43:32 are clear).
你要4KB
对齐,又最大4KB
,那我直接申请这么大不就行了?
Initialize the version identifier in the VMXON region (the first 31 bits) with the VMCS revision identifier reported by capability MSRs. Clear bit 31 of the first 4 bytes of the VMXON region.
上面的几句话说明前4个字节位说明版本号,以让CPU
如何处理VT
,这个同样在IA32_VMX_BASIC_MSR
这个寄存器,前31位就是版本号。对于剩下的字节,需要清0。
Execute VMXON with the physical address of the VMXON region as the operand.
我们使用vmxon
指令需要的是它的物理地址,而不是线性地址,所以需要转化一下,最后调用我们封装好的__vmx_on
函数,就开启了VT
。
既然开启了,我们也得会关闭,如下是关闭VT
的代码,实现不难,就不细说了。
BOOLEAN StopVT()
{
__vmx_off();
__writecr4(__readcr4() & ~CR4_VMXE);
ExFreePool(g_VMXCPU.pVMXONRegion);
return TRUE;
}
到现在,我们需要略微修改驱动的加载和卸载函数代码,以做实验验证是否成功,具体代码如下:
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
DbgPrintLine("Unloaded Successfully!");
return StopVT() ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL;
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;
DbgPrintLine("Loaded Successfully!");
return StartVT() ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL;
}
如果成功的话,它的输出和我们VT
支持启用检测的调试输出是一样的,并且驱动加载是成功并且不会蓝屏。对于后面的部分,下一篇继续。
下一篇
VT 入门篇——最小 VT 实现(下)