上一篇:从头开始了解和使用Hypervisor(第1部分)
DbgView的问题
不幸的是,由于某些未知原因,我无法查看DbgPrint()的结果。如果可以看到结果,则可以跳过此步骤,但是如果中间遇到问题,请执行以下步骤:
在regedit中,添加一个密钥:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter
接着,添加一个名为IHVDRIVER的DWORD值,其值为0xFFFF。
重新启动计算机,你就可以开始了。而且该方法始终有效(除了在MacBook运行外),并且我在许多计算机上也进行过测试。
为了解决此问题,你需要找到一个名为nt!Kd_DEFAULT_Mask的Windows内核全局变量,该变量负责在DbgView中显示结果,它有一个我不知道的掩码,所以我只放置了0xffffffff使其简单地显示所有内容!
为此,你需要使用Windbg进行Windows本地内核调试。
1.以管理员身份打开命令提示符窗口,输入bcdedit /debug on;
2.如果尚未将计算机配置为调试传输的目标,请输入bcdedit/dbgsettings local;
3.重新启动计算机;
之后,你需要使用UAC管理员权限打开Windbg,转到“文件”>“内核调试”>“本地”>按OK,然后在本地Windbg中使用以下命令找到nt!Kd_DEFAULT_Mask:
prlkd> x nt!kd_Default_Mask
fffff801`f5211808 nt!Kd_DEFAULT_Mask =
现在将它的值更改为0xffffffff。
lkd> eb fffff801`f5211808 ff ff ff ff
之后,你应该看到结果,现在你可以开始了。
请记住,以上讲的步骤很重要。因为如果我们看不到任何内核详细信息,那么我们将无法调试。
![f0ed79be3a81560bc331b62986392d75.png](https://i-blog.csdnimg.cn/blog_migrate/4b186da2e8a1b7e0c7f83b8d5cd4e0ab.png)
在启用VT-x之前,首先要考虑发现对vmx的支持,这在之前已经介绍过了。
如果CPUID.1:ECX.VMX[bit 5] = 1,则可以使用CPUID知道VMX的存在,然后支持VMX操作。
首先,我们需要知道我们是否在基于Intel的处理器上运行,这可以通过检查CPUID指令并找到供应商字符串“GenuineIntel“.来理解。
以下的函数以CPUID指令的形式返回供应商字符串:
string GetCpuID()
{
//Initialize used variables
char SysType[13]; //Array consisting of 13 single bytes/characters
string CpuID; //The string that will be used to add all the characters to
//Starting coding in assembly language
_asm
{
//Execute CPUID with EAX = 0 to get the CPU producer
XOR EAX, EAX
CPUID
//MOV EBX to EAX and get the characters one by one by using shift out right bitwise operation.
MOV EAX, EBX
MOV SysType[0], al
MOV SysType[1], ah
SHR EAX, 16
MOV SysType[2], al
MOV SysType[3], ah
//Get the second part the same way but these values are stored in EDX
MOV EAX, EDX
MOV SysType[4], al
MOV SysType[5], ah
SHR EAX, 16
MOV SysType[6], al
MOV SysType[7], ah
//Get the third part
MOV EAX, ECX
MOV SysType[8], al
MOV SysType[9], ah
SHR EAX, 16
MOV SysType[10], al
MOV SysType[11], ah
MOV SysType[12], 00
}
CpuID.assign(SysType, 12);
return CpuID;
}
最后一步是检查VMX是否存在,你可以使用以下代码进行检查:
bool VMX_Support_Detection()
{
bool VMX = false;
__asm {
xor eax, eax
inc eax
cpuid
bt ecx, 0x5
jc VMXSupport
VMXNotSupport :
jmp NopInstr
VMXSupport :
mov VMX, 0x1
NopInstr :
nop
}
return VMX;
}
可以看到,它用EAX=1检查CPUID,如果第5(6)位是1,则支持VMX操作,我们也可以在内核驱动程序中执行相同的操作。
总而言之,我们的主要代码应如下所示:
int main()
{
string CpuID;
CpuID = GetCpuID();
cout << "[*] The CPU Vendor is : " << CpuID << endl;
if (CpuID == "GenuineIntel")
{
cout << "[*] The Processor virtualization technology is VT-x. \n";
}
else
{
cout << "[*] This program is not designed to run in a non-VT-x environemnt !\n";
return 1;
}
if (VMX_Support_Detection())
{
cout << "[*] VMX Operation is supported by your processor .\n";
}
else
{
cout << "[*] VMX Operation is not supported by your processor .\n";
return 1;
}
_getch();
return 0;
}
最终结果如下:
启用VMX操作
如果我们的处理器支持VMX操作,那么就应该启用它。如前所述,IRP_MJ_CREATE是用于启动操作的第一个函数。
在系统软件可以进入VMX操作之前,它会通过设置CR4.VMXE [bit 13] = 1来启用VMX。然后通过执行VMXON指令进入VMX操作。如果以CR4.VMXE = 0执行,VMXON会导致无效操作码异常(#UD)。一旦进入VMX操作,就无法清除CR4.VMXE。系统软件通过执行VMXOFF指令退出VMX操作。执行VMXOFF后,可以在VMX操作之外清除CR4.VMXE。
VMXON也由IA32_FEATURE_CONTROL MSR(MSR地址3AH)控制,当逻辑处理器被重置时,这个MSR被清除为0。
1.位0是锁定位,如果清除此位,则VMXON会导致常规保护异常。如果设置了锁定位,则此MSR的WRMSR会导致一般保护异常;除非重新启动,否则不能修改MSR。系统BIOS可以使用此位为BIOS提供设置选项,以禁用对VMX的支持。要在平台上启用VMX支持,BIOS必须将位1,位2或同时设置这两个位以及锁定位。
2.位1启用了SMX操作中的VMXON,如果清除此位,则在SMX操作中执行VMXON会导致常规保护异常,试图在不同时支持VMX操作和SMX操作的逻辑处理器上设置这个位会导致通用保护异常。
3.位2启用SMX之外的VMXON操作。如果清除此位,则在SMX操作之外执行VMXON会导致常规保护异常,试图在不支持VMX操作的逻辑处理器上设置这个位会导致通用保护异常。
你还记得我告诉过你如何在Windows Driver Kit x64中创建内联程序集吗?
现在,你应该创建一些函数来在汇编中执行此操作。
我们仅需在标头文件(本文示例为Source.h)中声明你的函数:
extern void inline Enable_VMX_Operation(void);
然后在汇编文件(在我的情况下为SourceAsm.asm)中添加此函数,将Cr4的13或14位设置为1。
Enable_VMX_Operation PROC PUBLIC
push rax ; Save the state
xor rax,rax ; Clear the RAX
mov rax,cr4
or rax,02000h ; Set the 14th bit
mov cr4,rax
pop rax ; Restore the state
ret
Enable_VMX_Operation ENDP
另外,在SourceAsm.asm中声明你的函数。
PUBLIC Enable_VMX_Operation
上面的函数应该在DrvCreate中调用:
NTSTATUS DrvCreate(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
Enable_VMX_Operation(); // Enabling VMX Operation
DbgPrint("[*] VMX Operation Enabled Successfully !");
return STATUS_SUCCESS;
}
最后,你应该从用户模式调用以下函数:
HANDLE hWnd = CreateFile(L"\\\\.\\MyHypervisorDevice",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ |
FILE_SHARE_WRITE,
NULL, /// lpSecurityAttirbutes
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL |
FILE_FLAG_OVERLAPPED,
NULL); /// lpTemplateFile
如果看到以下结果,则说明你已成功完成。
重要说明:请注意你的.asm文件应与驱动程序主文件(.c文件)具有不同的名称,例如,如果驱动程序文件为“Source.c”,则使用名称“Source.asm”会在Visual Studio中导致奇怪的链接错误。所以你应将.asm文件的名称更改为“SourceAsm.asm”之类,以避免出现此类链接器错误。
以上,我介绍了创建Windows Driver Kit程序所需了解的基本知识,然后就可以进入虚拟环境。本文所讲的源代码,你可以点此查看。
现在,我们就可以设置我们的第一个虚拟机了。下面我们将演示如何从Windows用户模式(IOCTL 调度程序)与VMM进行交互,然后在特殊的内核中解决关联性和运行代码的问题。我们最终的目的是初始化VMXON区域和VMCS区域,然后将虚拟机管理程序区域加载到每个内核中,并实现自定义函数来使用管理程序指令和许多与虚拟机控制数据结构(VMCS)相关的内容。该过程的完整源代码,你可以点此下载。
从用户模式与VMM驱动程序进行交互
对我们来说,IRP MJ函数中最重要的函数是DrvIOCTLDispatcher(IRP_MJ_DEVICE_CONTROL),这是因为可以从用户模式使用特殊的IOCTL编号调用此函数,这意味着你可以在驱动程序中使用特殊的代码并实现与之对应的特殊函数。另外使用该代码,还可以要求驱动程序执行请求,可见此函数多么有用。
现在,让我们找到调用IOCTL代码的函数,并从内核模式驱动程序中打印代码的函数。
据我所知,有几种方法可以用来调度IOCTL,例如METHOD_BUFFERED,METHOD_NIETHER,METHOD_IN_DIRECT,METHOD_OUT_DIRECT。这些方法应由用户模式调用者实现,区别在于缓冲区在用户模式和内核模式之间转移,反之亦然,我只是复制了一些实现,并从Microsoft的Windows Driver Samples中进行了一些小修改。用户模式和内核模式的完整代码,如果方便你可以了解一下。
假设我们有以下IOCTL代码:
//
// Device type -- in the "User Defined" range."
//
#define SIOCTL_TYPE 40000
//
// The IOCTL function codes from 0x800 to 0xFFF are for customer use.
//
#define IOCTL_SIOCTL_METHOD_IN_DIRECT \
CTL_CODE( SIOCTL_TYPE, 0x900, METHOD_IN_DIRECT, FILE_ANY_ACCESS )
#define IOCTL_SIOCTL_METHOD_OUT_DIRECT \
CTL_CODE( SIOCTL_TYPE, 0x901, METHOD_OUT_DIRECT , FILE_ANY_ACCESS )
#define IOCTL_SIOCTL_METHOD_BUFFERED \
CTL_CODE( SIOCTL_TYPE, 0x902, METHOD_BUFFERED, FILE_ANY_ACCESS )
#define IOCTL_SIOCTL_METHOD_NEITHER \
CTL_CODE( SIOCTL_TYPE, 0x903, METHOD_NEITHER , FILE_ANY_ACCESS )
定义IOCTL的规则是这样的,简单来说,IOCTL是一个32位数字。前两个低位定义“传输类型”,可以是METHOD_OUT_DIRECT,METHOD_IN_DIRECT,METHOD_BUFFERED或METHOD_NEITHER。
下一组位从2到13定义“函数代码”,高位称为“自定义位”。这用于确定用户定义的IOCTL与系统定义的IOCTL。这意味着函数代码0x800及更高版本的定义类似于WM_USER对Windows消息的工作方式。
接下来的两位定义了发出IOCTL所需的访问权限,如果句柄没有以正确的访问方式打开,I/O管理器可以通过这种方式拒绝IOCTL请求。例如,访问类型为FILE_READ_DATA和FILE_WRITE_DATA。
最后一位代表IOCTL写入的设备类型。高位再次代表用户定义的值。
在IOCTL调度程序中,IO_STACK_LOCATION的“Parameters.DeviceIoControl.IoControlCode”包含正在调用的IOCTL代码。
对于METHOD_IN_DIRECT和METHOD_OUT_DIRECT,IN和OUT之间的区别在于使用IN时,你可以使用输出缓冲区来传递数据,而OUT仅用于返回数据。
METHOD_BUFFERED是一个从这个缓冲区中复制数据的缓冲区,缓冲区创建为两个大小中较大的一个,即输入缓冲区或输出缓冲区。然后将读取的缓冲区复制到此新缓冲区,返回之前,你只需将返回数据复制到同一缓冲区中即可。返回值放入IO_STATUS_BLOCK中,并且I/O管理器将数据复制到输出缓冲区中,其中METHOD_NEITHER是相同的。
以以下示例为例,首先,介绍所有需要的变量。请注意,PAGED_CODE宏可确保调用线程在足以允许分页的IRQL中运行。
NTSTATUS DrvIOCTLDispatcher( PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
PIO_STACK_LOCATION irpSp;// Pointer to current stack location
NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success
ULONG inBufLength; // Input buffer length
ULONG outBufLength; // Output buffer length
PCHAR inBuf, outBuf; // pointer to Input and output buffer
PCHAR data = "This String is from Device Driver !!!";
size_t datalen = strlen(data) + 1;//Length of data including null
PMDL mdl = NULL;
PCHAR buffer = NULL;
UNREFERENCED_PARAMETER(DeviceObject);
PAGED_CODE();
irpSp = IoGetCurrentIrpStackLocation(Irp);
inBufLength = irpSp->Parameters.DeviceIoControl.InputBufferLength;
outBufLength = irpSp->Parameters.DeviceIoControl.OutputBufferLength;
if (!inBufLength || !outBufLength)
{
ntStatus = STATUS_INVALID_PARAMETER;
goto End;
}
...
然后,我们必须通过IOCTL使用switch-case,只需复制缓冲区并从DbgPrint()中显示出来即可。
switch (irpSp->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_SIOCTL_METHOD_BUFFERED:
DbgPrint("Called IOCTL_SIOCTL_METHOD_BUFFERED\n");
PrintIrpInfo(Irp);
inBuf = Irp->AssociatedIrp.SystemBuffer;
outBuf = Irp->AssociatedIrp.SystemBuffer;
DbgPrint("\tData from User :");
DbgPrint(inBuf);
PrintChars(inBuf, inBufLength);
RtlCopyBytes(outBuf, data, outBufLength);
DbgPrint(("\tData to User : "));
PrintChars(outBuf, datalen);
Irp->IoStatus.Information = (outBufLength < datalen ? outBufLength : datalen);
break;
...
PrintIrpInfo是这样的:
VOID PrintIrpInfo(PIRP Irp)
{
PIO_STACK_LOCATION irpSp;
irpSp = IoGetCurrentIrpStackLocation(Irp);
PAGED_CODE();
DbgPrint("\tIrp->AssociatedIrp.SystemBuffer = 0x%p\n",
Irp->AssociatedIrp.SystemBuffer);
DbgPrint("\tIrp->UserBuffer = 0x%p\n", Irp->UserBuffer);
DbgPrint("\tirpSp->Parameters.DeviceIoControl.Type3InputBuffer = 0x%p\n",
irpSp->Parameters.DeviceIoControl.Type3InputBuffer);
DbgPrint("\tirpSp->Parameters.DeviceIoControl.InputBufferLength = %d\n",
irpSp->Parameters.DeviceIoControl.InputBufferLength);
DbgPrint("\tirpSp->Parameters.DeviceIoControl.OutputBufferLength = %d\n",
irpSp->Parameters.DeviceIoControl.OutputBufferLength);
return;
}
在本文的其余部分,我们仅使用IOCTL_SIOCTL_METHOD_BUFFERED方法。
如果你还记得上述我们使用CreateFile创建句柄(HANDLE)的示例,那现在我们可以使用DeviceIoControl来调用DrvIOCTLDispatcher (IRP_MJ_DEVICE_CONTROL)以及来自用户模式的参数。
char OutputBuffer[1000];
char InputBuffer[1000];
ULONG bytesReturned;
BOOL Result;
StringCbCopy(InputBuffer, sizeof(InputBuffer),
"This String is from User Application; using METHOD_BUFFERED");
printf("\nCalling DeviceIoControl METHOD_BUFFERED:\n");
memset(OutputBuffer, 0, sizeof(OutputBuffer));
Result = DeviceIoControl(handle,
(DWORD)IOCTL_SIOCTL_METHOD_BUFFERED,
&InputBuffer,
(DWORD)strlen(InputBuffer) + 1,
&OutputBuffer,
sizeof(OutputBuffer),
&bytesReturned,
NULL
);
if (!Result)
{
printf("Error in DeviceIoControl : %d", GetLastError());
return 1;
}
printf(" OutBuffer (%d): %s\n", bytesReturned, OutputBuffer);
你可以点此查看,IOCT调度的不同类型。
接下来,看看如何使用Windows来构建VMM。
每个处理器的配置和关联设置
与特殊逻辑处理器的关联性是使用虚拟机管理程序时应考虑的主要内容之一,不幸的是,在Windows中,没有类似on_each_cpu的内容(就像在Linux内核模块中一样),因此我们必须手动更改关联设置,才能在每个逻辑处理器上运行。在我的Intel Core i7 6820HQ中,我有4个物理内核,且每个内核可以同时运行2个线程(由于存在超线程),因此我们有8个逻辑处理器,当然还有8套寄存器(包括通用寄存器和MSR寄存器),因此我们应该配置VMM,以便在8个逻辑处理器上工作。
要获得逻辑处理器的数量,可以使用KeQueryActiveProcessors(),然后我们应该向KeSetSystemAffinityThread传递一个KAFFINITY掩码,以设置当前线程的系统关联。
可以使用简单的power函数来配置KAFFINITY掩码:
int ipow(int base, int exp) {
int result = 1;
for (;;)
{
if ( exp & 1)
{
result *= base;
}
exp >>= 1;
if (!exp)
{
break;
}
base *= base;
}
return result;
}
那么我们应该使用以下代码来更改处理器的关联性,并在所有逻辑内核中分别运行我们的代码:
KAFFINITY kAffinityMask;
for (size_t i = 0; i < KeQueryActiveProcessors(); i++)
{
kAffinityMask = ipow(2, i);
KeSetSystemAffinityThread(kAffinityMask);
DbgPrint("=====================================================");
DbgPrint("Current thread is executing in %d th logical processor.",i);
// Put you function here !
}
物理地址和虚拟地址之间的转换
VMXON区域和VMCS区域(请参见下文)可以使用物理地址作为VMXON和VMPTRLD指令的操作数,因此我们应该创建一个将虚拟地址转换为物理地址的函数:
UINT64 VirtualAddress_to_PhysicallAddress(void* va)
{
return MmGetPhysicalAddress(va).QuadPart;
}
只要我们不能在保护模式下直接使用物理地址进行修改,那么我们就必须将物理地址转换为虚拟地址。
UINT64 PhysicalAddress_to_VirtualAddress(UINT64 pa)
{
PHYSICAL_ADDRESS PhysicalAddr;
PhysicalAddr.QuadPart = pa;
return MmGetVirtualForPhysical(PhysicalAddr);
}
通过内核查询虚拟机管理程序
现在,我们已经从用户模式查询虚拟机管理程序的存在,但我们也应考虑从内核模式检查虚拟机管理程序。这就减少了将来出现内核错误的可能性,或者有可能使用锁定位禁用了管理程序。顺便说一下,以下代码会检查IA32_FEATURE_CONTROL MSR(MSR地址3AH),以查看是否设置了锁定位 。
BOOLEAN Is_VMX_Supported()
{
CPUID data = { 0 };
// VMX bit
__cpuid((int*)&data, 1);
if ((data.ecx & (1 << 5)) == 0)
return FALSE;
IA32_FEATURE_CONTROL_MSR Control = { 0 };
Control.All = __readmsr(MSR_IA32_FEATURE_CONTROL);
// BIOS lock check
if (Control.Fields.Lock == 0)
{
Control.Fields.Lock = TRUE;
Control.Fields.EnableVmxon = TRUE;
__writemsr(MSR_IA32_FEATURE_CONTROL, Control.All);
}
else if (Control.Fields.EnableVmxon == FALSE)
{
DbgPrint("[*] VMX locked off in BIOS");
return FALSE;
}
return TRUE;
}
以上函数中使用的结构声明如下:
typedef union _IA32_FEATURE_CONTROL_MSR
{
ULONG64 All;
struct
{
ULONG64 Lock : 1; // [0]
ULONG64 EnableSMX : 1; // [1]
ULONG64 EnableVmxon : 1; // [2]
ULONG64 Reserved2 : 5; // [3-7]
ULONG64 EnableLocalSENTER : 7; // [8-14]
ULONG64 EnableGlobalSENTER : 1; // [15]
ULONG64 Reserved3a : 16; //
ULONG64 Reserved3b : 32; // [16-63]
} Fields;
} IA32_FEATURE_CONTROL_MSR, *PIA32_FEATURE_CONTROL_MSR;
typedef struct _CPUID
{
int eax;
int ebx;
int ecx;
int edx;
} CPUID, *PCPUID;
VMXON区域
在执行VMXON之前,软件应分配一个自然对齐的4 KB内存区域,逻辑处理器可以使用该区域来支持VMX操作。该区域称为VMXON区域。VMXON区域的地址(VMXON指针)在一个操作数中提供给VMXON。
VMM可以为每个逻辑处理器使用不同的VMXON区域,否则行为是“未定义的”。
注意:第一个支持VMX操作的处理器要求VMX操作中的以下位为1:CR0.PE,CR0.NE,CR0.PG和CR4.VMXE。对CR0.PE和CR0.PG的限制意味着仅在分页保护模式(包括IA-32e模式)中支持VMX操作。因此,客户软件不能在未分页保护模式或实际地址模式下运行。
由于我们正在配置虚拟机管理程序,因此我们应该有一个描述虚拟机状态的全局变量,为此我创建了以下结构。目前,虽然我们只有两个字段(VMXON_REGION和VMCS_REGION),但是我们将在将来的结构中添加新的字段。
typedef struct _VirtualMachineState
{
UINT64 VMXON_REGION; // VMXON region
UINT64 VMCS_REGION; // VMCS region
} VirtualMachineState, *PVirtualMachineState;
当然,还有一个全局变量。
extern PVirtualMachineState vmState;
为此,我创建了以下函数(在memory.c中)来分配VMXON区域并使用分配区域的指针执行VMXON指令。
BOOLEAN Allocate_VMXON_Region(IN PVirtualMachineState vmState)
{
// at IRQL > DISPATCH_LEVEL memory allocation routines don't work
if (KeGetCurrentIrql() > DISPATCH_LEVEL)
KeRaiseIrqlToDpcLevel();
PHYSICAL_ADDRESS PhysicalMax = { 0 };
PhysicalMax.QuadPart = MAXULONG64;
int VMXONSize = 2 * VMXON_SIZE;
BYTE* Buffer = MmAllocateContiguousMemory(VMXONSize + ALIGNMENT_PAGE_SIZE, PhysicalMax); // Allocating a 4-KByte Contigous Memory region
PHYSICAL_ADDRESS Highest = { 0 }, Lowest = { 0 };
Highest.QuadPart = ~0;
//BYTE* Buffer = MmAllocateContiguousMemorySpecifyCache(VMXONSize + ALIGNMENT_PAGE_SIZE, Lowest, Highest, Lowest, MmNonCached);
if (Buffer == NULL) {
DbgPrint("[*] Error : Couldn't Allocate Buffer for VMXON Region.");
return FALSE;// ntStatus = STATUS_INSUFFICIENT_RESOURCES;
}
UINT64 PhysicalBuffer = VirtualAddress_to_PhysicallAddress(Buffer);
// zero-out memory
RtlSecureZeroMemory(Buffer, VMXONSize + ALIGNMENT_PAGE_SIZE);
UINT64 alignedPhysicalBuffer = (BYTE*)((ULONG_PTR)(PhysicalBuffer + ALIGNMENT_PAGE_SIZE - 1) &~(ALIGNMENT_PAGE_SIZE - 1));
UINT64 alignedVirtualBuffer = (BYTE*)((ULONG_PTR)(Buffer + ALIGNMENT_PAGE_SIZE - 1) &~(ALIGNMENT_PAGE_SIZE - 1));
DbgPrint("[*] Virtual allocated buffer for VMXON at %llx", Buffer);
DbgPrint("[*] Virtual aligned allocated buffer for VMXON at %llx", alignedVirtualBuffer);
DbgPrint("[*] Aligned physical buffer allocated for VMXON at %llx", alignedPhysicalBuffer);
// get IA32_VMX_BASIC_MSR RevisionId
IA32_VMX_BASIC_MSR basic = { 0 };
basic.All = __readmsr(MSR_IA32_VMX_BASIC);
DbgPrint("[*] MSR_IA32_VMX_BASIC (MSR 0x480) Revision Identifier %llx", basic.Fields.RevisionIdentifier);
//* (UINT64 *)alignedVirtualBuffer = 04;
//Changing Revision Identifier
*(UINT64 *)alignedVirtualBuffer = basic.Fields.RevisionIdentifier;
int status = __vmx_on(&alignedPhysicalBuffer);
if (status)
{
DbgPrint("[*] VMXON failed with status %d\n", status);
return FALSE;
}
vmState->VMXON_REGION = alignedPhysicalBuffer;
return TRUE;
}
让我们解释一下上面的函数:
// at IRQL > DISPATCH_LEVEL memory allocation routines don't work
if (KeGetCurrentIrql() > DISPATCH_LEVEL)
KeRaiseIrqlToDpcLevel();
该代码用于将当前的IRQL级别更改为DISPATCH_LEVEL,但是只要我们使用MmAllocateContiguousMemory,我们就可以忽略此代码。但是如果你想为VMXON区域使用另一种类型的内存,则应该使用MmAllocateContiguousMemorySpecifyCache(注释),你可以使用其他类型的内存,具体的请点击这里。
请注意,为确保VMX操作中的正确行为,应在回写机制可缓存内存中维护VMCS区域和相关结构。或者,你可以使用UC内存类型映射这些区域或结构中的任何一个。强烈建议不要这样做,因为这样做会导致使用这些结构的转换性能明显受损。
回写是一种存储方法,其中,每次发生更改时,数据都会写入高速缓存,但是仅在指定的时间间隔或特定条件下,数据才会写入主存储器中的相应位置。可缓存或不可缓存可以从分页结构(PTE)中的缓存禁用位中确定。
顺便说一句,我们应该分配8192字节,因为不能保证Windows会分配对齐的内存,因此我们可以找到以8196字节对齐的4096字节,(对齐是指物理地址可以被4096整除)。
以我的经验,MmAllocateContiguousMemory分配总是对齐的,这可能是因为PFN中的每个页面都是按4096字节分配的,只要我们需要4096字节,它就会对齐。
如果你对页面框架编号(PFN)感兴趣,则可以阅读Windows内部页面框架编号(PFN)–第1部分和Windows内部页面框架编号(PFN)–第2部分。
PHYSICAL_ADDRESS PhysicalMax = { 0 };
PhysicalMax.QuadPart = MAXULONG64;
int VMXONSize = 2 * VMXON_SIZE;
BYTE* Buffer = MmAllocateContiguousMemory(VMXONSize, PhysicalMax); // Allocating a 4-KByte Contigous Memory region
if (Buffer == NULL) {
DbgPrint("[*] Error : Couldn't Allocate Buffer for VMXON Region.");
return FALSE;// ntStatus = STATUS_INSUFFICIENT_RESOURCES;
}
现在,我们应该将分配的内存的地址转换为其物理地址,并确保其对齐。
MmAllocateContiguousMemory分配的内存未初始化。内核模式驱动程序必须首先将此内存设置为零。以以下示例为例,我们应该使用RtlSecureZeroMemory。
UINT64 PhysicalBuffer = VirtualAddress_to_PhysicallAddress(Buffer);
// zero-out memory
RtlSecureZeroMemory(Buffer, VMXONSize + ALIGNMENT_PAGE_SIZE);
UINT64 alignedPhysicalBuffer = (BYTE*)((ULONG_PTR)(PhysicalBuffer + ALIGNMENT_PAGE_SIZE - 1) &~(ALIGNMENT_PAGE_SIZE - 1));
UINT64 alignedVirtualBuffer = (BYTE*)((ULONG_PTR)(Buffer + ALIGNMENT_PAGE_SIZE - 1) &~(ALIGNMENT_PAGE_SIZE - 1));
DbgPrint("[*] Virtual allocated buffer for VMXON at %llx", Buffer);
DbgPrint("[*] Virtual aligned allocated buffer for VMXON at %llx", alignedVirtualBuffer);
DbgPrint("[*] Aligned physical buffer allocated for VMXON at %llx", alignedPhysicalBuffer);
在执行VMXON之前,软件应将VMCS修订标识符写入VMXON区域。具体地说,它应将31位VMCS修订标识符写入VMXON区域的前4个字节的 位30:0;位31被清除。
它不需要以任何其他方式初始化VMXON区域,软件应为每个逻辑处理器使用一个单独的区域,并且不应在该逻辑处理器上执行VMXON和VMXOFF之间访问或修改逻辑处理器的VMXON区域,否则可能导致无法预测的行为。
因此,让我们从IA32_VMX_BASIC_MSR获得修订标识符,并将其写入我们的VMXON区域。
// get IA32_VMX_BASIC_MSR RevisionId
IA32_VMX_BASIC_MSR basic = { 0 };
basic.All = __readmsr(MSR_IA32_VMX_BASIC);
DbgPrint("[*] MSR_IA32_VMX_BASIC (MSR 0x480) Revision Identifier %llx", basic.Fields.RevisionIdentifier);
//Changing Revision Identifier
*(UINT64 *)alignedVirtualBuffer = basic.Fields.RevisionIdentifier;
最后一部分用于执行VMXON指令。
int status = __vmx_on(&alignedPhysicalBuffer);
if (status)
{
DbgPrint("[*] VMXON failed with status %d\n", status);
return FALSE;
}
vmState->VMXON_REGION = alignedPhysicalBuffer;
return TRUE;
__vmx_on是执行VMXON的固有函数:
0表示操作成功;
1表示操作失败,并且扩展状态在当前VMCS的VM指令错误字段中可用;
2表示操作失败,没有可用状态;
如果我们使用VMXON设置VMXON区域,但失败,则状态=1。如果没有任何VMCS,则状态= 2;如果操作成功,则状态= 0。
如果你在不执行VMXOFF的情况下两次执行了以上代码,则肯定会出错。
现在,我们的VMXON区域已经准备就绪。
本文翻译自:https://rayanfam.com/topics/hypervisor-from-scratch-part-2/ 与 https://rayanfam.com/topics/hypervisor-from-scratch-part-3/