VT虚拟化调试器 – 第3部分:设置我们的第一个虚拟机
介绍这是教程“从头开始的Hypervisor ”的第三部分。您可能已经注意到,前面的部分一直在变得越来越复杂。本部分应教您如何开始创建自己的VMM,我们将演示如何从Windows用户模式(IOCTL Dispatcher)与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。这些方法应遵循用户模式调用者(的区别是在地方缓冲区的用户模式和内核模式,反之亦然之间传输),我只是有一些小的修改形式实现复制微软的Windows驱动程序示例,您可以看到user-mode和kernel-mode的完整代码。
假设我们有以下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及更高版本的定义与Windows消息的WM_USER的工作方式类似。
接下来的两位定义了发出IOCTL所需的访问权限。如果尚未使用正确的访问权限打开句柄,这就是I / O管理器可以拒绝IOCTL请求的方式。访问类型例如是FILE_READ_DATA和FILE_WRITE_DATA。
最后一位代表为IOCTL写入的设备类型。高位再次代表用户定义的值。
在IOCTL调度,将“ Parameters.DeviceIoControl .IoControlCode了” IO_STACK_LOCATION包含被调用的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_LOCATIONirpSp;// 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中使用切换用例(只需复制缓冲区并从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_LOCATIONirpSp;
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;
}即使您可以在我的GitHub上看到所有实现,也足够了,但在本文的其余部分中,我们仅使用IOCTL_SIOCTL_METHOD_BUFFERED方法。
现在从用户模式开始,如果您还记得上一部分中使用CreateFile创建句柄(HANDLE)的地方,那么我们现在可以使用 DeviceIoControl 调用DrvIOCTLDispatcher(IRP_MJ_DEVICE_CONTROL)以及我们在用户模式下的参数。
char OutputBuffer;
char InputBuffer;
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调度的不同类型。
我认为我们已经完成了WDK基础知识,现在该看看如何使用Windows来构建我们的VMM。
https://rayanfam.com/wp-content/uploads/sites/2/2018/09/Sad-Anime.jpg
每个处理器的配置和设置亲和力
与特殊逻辑处理器的相似性是使用虚拟机管理程序时应考虑的主要内容之一。
不幸的是,在Windows中,没有类似on_each_cpu的东西(就像在Linux内核模块中一样),因此我们必须手动更改亲和力才能在每个逻辑处理器上运行。在我的 Intel Core i7 6820HQ中, 我有4个物理内核,每个内核可以同时运行2个线程(由于存在超线程),因此我们有8个逻辑处理器,当然还有8套所有寄存器(包括通用寄存器和MSR寄存器),因此我们应该将VMM配置为可在8个逻辑处理器上工作。
要获得逻辑处理器的数,您可以使用KeQueryActiveProcessorCount(0) ,那么我们就应该通过一个 KAFFINITY 面具到 KeSetSystemAffinityThread 这台当前线程的系统亲和力。
可以使用简单的幂函数来配置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 < KeQueryActiveProcessorCount(0); 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);
}
从内核中查询有关Hypervisor的信息
在上一部分中,我们从用户模式查询虚拟机管理程序的存在,但我们也应考虑从内核模式检查虚拟机管理程序。这减少了将来发生内核错误的可能性,或者可能使用锁定位禁用了虚拟机监控程序,顺便说一下,以下代码检查 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; //
ULONG64 EnableSMX : 1; //
ULONG64 EnableVmxon : 1; //
ULONG64 Reserved2 : 5; //
ULONG64 EnableLocalSENTER : 7; //
ULONG64 EnableGlobalSENTER : 1;//
ULONG64 Reserved3a : 16; //
ULONG64 Reserved3b : 32; //
} 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;
}
让我们解释一下上面的功能,
<blockquote><span style="white-space:pre"> </span>// at IRQL > DISPATCH_LEVEL memory allocation routines don't work
该代码用于将当前的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);
英特尔手册(24.11.5 VMXON Region):
在执行VMXON之前,软件应将VMCS修订标识符写入VMXON区域。(具体来说,它应将31位VMCS修订标识符写入VMXON区域的前4个字节的30:0位;应将位31清除为0。)
它不需要以任何其他方式初始化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的固有函数。状态码显示diffrenet的含义。
价值意义
0操作成功。
1个操作失败,VM-instruction error field 当前VMCS的扩展状态可用 。
2个操作失败,没有可用状态。
如果我们使用VMXON设置VMXON区域,但失败,则状态=1。如果没有任何VMCS,则状态= 2,如果操作成功,则状态= 0。
如果两次执行以上代码而不执行VMXOFF,则肯定会出错。
现在,我们的VMXON区域已准备就绪,我们准备出发了。
虚拟机器控制数据结构(VMCS)
逻辑处理器在VMX操作中时会使用虚拟机控制数据结构(VMCS)。这些管理进出VMX非根操作(VM入口和VM出口)以及VMX非根操作中的处理器行为的过渡。该结构由新指令VMCLEAR,VMPTRLD,VMREAD和VMWRITE操纵。
https://rayanfam.com/wp-content/uploads/sites/2/2018/09/VMXLifecycle.png
上图说明了VMCS Region上的生命周期VMX操作。
初始化VMCS区域
VMM可以(应该)使用不同的VMCS区域,因此您需要设置逻辑处理器相似性并多次运行初始化例程。
VMCS所在的位置称为“ VMCS区域”。
VMCS区域是一个
4 KB(位11:0必须为零)
必须与4KB边界对齐
该指针不得设置超出处理器物理地址宽度的位(软件可以通过在EAX中执行80000008H执行CPUID来确定处理器的物理地址宽度。物理地址宽度在EAX的位7:0中返回。)
处理器中可能同时存在多个VMCS,但其中只有一个处于活动状态,并且VMLAUNCH,VMREAD,VMRESUME和VMWRITE指令仅在当前VMCS上运行。
使用VMPTRLD可以在逻辑处理器上设置当前的VMCS。
VMCLEAR指令的内存操作数也是VMCS的地址。执行该指令后,该VMCS在逻辑处理器上既不是活动的也不是当前的。如果VMCS在逻辑处理器上是最新的,则逻辑处理器不再具有当前的VMCS。
VMPTRST负责提供当前的VMCS指针,如果没有当前的VMCS,它将存储值FFFFFFFFFFFFFFFFH。
VMCS的启动状态确定该VMCS应使用哪个VM输入指令。VMLAUNCH指令要求VMCS的启动状态为“清除”。VMRESUME指令需要启动状态为“已启动”的VMCS。逻辑处理器在相应的VMCS区域中维护VMCS的启动状态。
如果当前VMCS的启动状态为“清除”,则成功执行VMLAUNCH指令会将启动状态更改为“已启动”。
VMCLEAR指令的内存操作数是VMCS的地址。执行指令后,该VMCS的启动状态为“清除”。
没有其他方法可以修改VMCS的启动状态(无法使用VMWRITE对其进行修改),也无法直接发现它(无法使用VMREAD对其进行读取)。
下图说明了VMCS区域的内容。https://rayanfam.com/wp-content/uploads/sites/2/2018/09/Init-VMCS.png
以下代码负责分配VMCS Region:
BOOLEAN Allocate_VMCS_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 VMCSSize = 2 * VMCS_SIZE;
BYTE* Buffer = MmAllocateContiguousMemory(VMCSSize + 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);
UINT64 PhysicalBuffer = VirtualAddress_to_PhysicallAddress(Buffer);
if (Buffer == NULL) {
DbgPrint("[*] Error : Couldn't Allocate Buffer for VMCS Region.");
return FALSE;// ntStatus = STATUS_INSUFFICIENT_RESOURCES;
}
// zero-out memory
RtlSecureZeroMemory(Buffer, VMCSSize + 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 VMCS at %llx", Buffer);
DbgPrint("[*] Virtual aligned allocated buffer for VMCS at %llx", alignedVirtualBuffer);
DbgPrint("[*] Aligned physical buffer allocated for VMCS 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);
//Changing Revision Identifier
*(UINT64 *)alignedVirtualBuffer = basic.Fields.RevisionIdentifier;
int status = __vmx_vmptrld(&alignedPhysicalBuffer);
if (status)
{
DbgPrint("[*] VMCS failed with status %d\n", status);
return FALSE;
}
vmState->VMCS_REGION = alignedPhysicalBuffer;
return TRUE;
}上面的代码与VMXON区域完全相同,除了 __vmx_vmptrld而不是 __vmx_on之外,__ vmx_vmptrld是VMPTRLD指令的固有函数。
在VMCS中,我们还应该从MSR_IA32_VMX_BASIC中找到修订版标识符, 并在执行VMPTRLD之前写入VMCS Region。
MSR_IA32_VMX_BASIC定义如下。
typedef union _IA32_VMX_BASIC_MSR
{
ULONG64 All;
struct
{
ULONG32 RevisionIdentifier : 31; //
ULONG32 Reserved1 : 1; //
ULONG32 RegionSize : 12; //
ULONG32 RegionClear : 1; //
ULONG32 Reserved2 : 3; //
ULONG32 SupportedIA64 : 1; //
ULONG32 SupportedDualMoniter : 1;//
ULONG32 MemoryType : 4; //
ULONG32 VmExitReport : 1; //
ULONG32 VmxCapabilityHint : 1; //
ULONG32 Reserved3 : 8; //
} Fields;
} IA32_VMX_BASIC_MSR, *PIA32_VMX_BASIC_MSR;VMXOFF
配置完上述区域之后,现在当用户模式应用程序不再维护驱动程序的句柄时,该考虑一下DrvClose了。此时,我们应该终止VMX并释放之前分配的每个内存。
以下函数负责执行VMXOFF,然后调用MmFreeContiguousMemory以释放分配的内存:
void Terminate_VMX(void) {
DbgPrint("\n[*] Terminating VMX...\n");
KAFFINITY kAffinityMask;
for (size_t i = 0; i < ProcessorCounts; i++)
{
kAffinityMask = ipow(2, i);
KeSetSystemAffinityThread(kAffinityMask);
DbgPrint("\t\tCurrent thread is executing in %d th logical processor.", i);
__vmx_off();
MmFreeContiguousMemory(PhysicalAddress_to_VirtualAddress(vmState.VMXON_REGION));
MmFreeContiguousMemory(PhysicalAddress_to_VirtualAddress(vmState.VMCS_REGION));
}
DbgPrint("[*] VMX Operation turned off successfully. \n");
}请记住将VMXON和VMCS区域转换为虚拟地址,因为 MmFreeContiguousMemory 接受VA,否则将导致BSOD。
好的,快完成了!
测试我们的VMM
https://rayanfam.com/wp-content/uploads/sites/2/2018/09/at_work_computer.jpg
让我们为代码创建一个测试用例,首先是一个用于通过所有逻辑处理器初始化VMXON和VMCS区域的函数。
PVirtualMachineState vmState;
int ProcessorCounts;
PVirtualMachineState Initiate_VMX(void) {
if (!Is_VMX_Supported())
{
DbgPrint("[*] VMX is not supported in this machine !");
return NULL;
}
ProcessorCounts = KeQueryActiveProcessorCount(0);
vmState = ExAllocatePoolWithTag(NonPagedPool, sizeof(VirtualMachineState)* ProcessorCounts, POOLTAG);
DbgPrint("\n=====================================================\n");
KAFFINITY kAffinityMask;
for (size_t i = 0; i < ProcessorCounts; i++)
{
kAffinityMask = ipow(2, i);
KeSetSystemAffinityThread(kAffinityMask);
// do st here !
DbgPrint("\t\tCurrent thread is executing in %d th logical processor.", i);
Enable_VMX_Operation(); // Enabling VMX Operation
DbgPrint("[*] VMX Operation Enabled Successfully !");
Allocate_VMXON_Region(&vmState);
Allocate_VMCS_Region(&vmState);
DbgPrint("[*] VMCS Region is allocated at===============> %llx", vmState.VMCS_REGION);
DbgPrint("[*] VMXON Region is allocated at ===============> %llx", vmState.VMXON_REGION);
DbgPrint("\n=====================================================\n");
}
}上面的函数应该从IRP MJ CREATE调用,因此让我们将DrvCreate修改 为:
NTSTATUS DrvCreate(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
DbgPrint("[*] DrvCreate Called !");
if (Initiate_VMX()) {
DbgPrint("[*] VMX Initiated Successfully.");
}
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}并将DrvClose修改为:
NTSTATUS DrvClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
DbgPrint("[*] DrvClose Called !");
// executing VMXOFF on every logical processor
Terminate_VMX();
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}现在,运行代码,在创建句柄的情况下(您可以看到我们的区域已成功分配)。
https://rayanfam.com/wp-content/uploads/sites/2/2018/09/VMXONandVMCS.png
当我们从用户模式调用CloseHandle时:
https://rayanfam.com/wp-content/uploads/sites/2/2018/09/TerminateVMX.png
注意:请记住,虚拟机管理程序会随时间变化,因为操作系统中添加了新功能或使用了新技术,例如,对Meltdown&Spectre的更新对虚拟机管理程序进行了很多更改,因此,如果要使用虚拟机管理程序从头开始在您的项目,研究或任何工作中,您必须使用本系列教程的最新部分中的驱动程序,因为本教程已得到积极更新,并且更改已应用于较新的部分(保持较早的部分不变),因此您可能会遇到错误和因此,较早部分的不稳定问题确保在实际项目中使用最新部分。
结论
在这一部分中,我们了解了不同类型的IOCTL调度,然后在Windows中看到了用于管理虚拟机监控程序VMM的不同功能,并初始化了VMXON区域和VMCS区域,然后终止了它们。
在未来的部分中,我们将重点介绍VMCS和可在VMCS区域中执行的各种操作,以控制客户机软件。
页:
[1]