Hypervisor From Scratch:设置我们的第一个虚拟机


1


介绍


这是教程“Hypervisor From Scratch”的第三部分。在这一部分中,我们将继续学习虚拟机管理程序以及如何开始创建自定义 VMM。在上一部分中,我们学习了如何制作 WDK 驱动程序来处理用户模式请求并启用处理器中的 VMX 位。在这一部分中,我们扩展驱动程序并向 VMM 添加 VMX 功能。最后,我们在VMM中使用不同的VT-x指令。



2


概述


在这一部分中,我们演示如何从 Windows 用户模式(IOCTL Dispatcher)与 VMM 交互,然后解决亲和性和在特定核心中运行代码的问题。最后,我们熟悉初始化VMXON 区域VMCS 区域,然后将虚拟机管理程序加载到每个核心中并实现自定义函数以使用虚拟机管理程序指令以及与虚拟机控制数据结构 (VMCS) 相关的许多内容。


一些实现源自HyperBone(带钩子的简约 VT-X 虚拟机管理程序)、Satoshi Tanda的HyperPlatform和hvpp (我的朋友Petr Beneš的出色工作)。



3


从用户模式与驱动程序交互


IRP MJ 函数中对我们来说最重要的函数是DrvIoctlDispatcher或 (IRP_MJ_DEVICE_CONTROL) 主要函数,这是因为可以使用特定的 IOCTL 编号从用户模式调用该函数,这意味着我们可以在驱动程序中拥有特殊的代码并实现与此代码相对应的独特功能,然后通过了解代码(来自用户模式),我们可以要求我们的驱动程序执行请求,这样我们就可以从内核请求某些功能。



4


IOCTL结构


Hypervisor From Scratch:设置我们的第一个虚拟机

有一个定义 IOCTL 的约定,如此处所述(https://www.codeproject.com/Articles/9575/Driver-Development-Part-2-Introduction-to-Implemen)。IOCTL 是一个 32 位数字。前两位低位代表“传输类型”,可以是 METHOD_OUT_DIRECT、METHOD_IN_DIRECT、METHOD_BUFFERED 或 METHOD_NEITHER。


下一组从 2 到 13 的位定义“功能代码”。高位被称为“自定义位”。这用于确定用户定义的 IOCTL 与系统定义的 IOCTL。这意味着 0x800 及更大的功能代码是为 Windows 消息自定义定义的。


接下来的两位定义发出 IOCTL 所需的访问权限。如果未使用正确的访问权限打开句柄,则 I/O 管理器可以通过这种方式拒绝 IOCTL 请求。访问类型有 FILE_READ_DATA、FILE_WRITE_DATA 等。


最后几位代表写入 IOCTL 的设备类型。高位再次代表用户定义的值。


我们可以使用下面定义的宏来创建我们的 IOCTL 代码。


#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)


例如,可以定义以下IOCTL代码。


#define IOCTL_SIOCTL_METHOD_BUFFERED
CTL_CODE(SIOCTL_TYPE, 0x902, METHOD_BUFFERED, FILE_ANY_ACCESS)




5


IOCTL调度程序


现在让我们实现调度 IOCTL 代码的函数。


请注意,PAGED_CODE()宏确保调用线程以足够低的 IRQL 运行以允许分页,该宏用于确保启用分页,例如,当前执行不在 DISPATCH_LEVEL 下。不用担心,我们将在以后的部分中详细讨论 IRQL。


下一步是检查输入缓冲区和输出缓冲区的长度。我们将检查它,因为我们需要确保用户为内核提供缓冲区并期望接收缓冲区。以下代码从 IO_STACK_LOCATION 获取输入和输出缓冲区长度


IrpStack = IoGetCurrentIrpStackLocation(Irp);
InBufLength = IrpStack->Parameters.DeviceIoControl.InputBufferLength;
OutBufLength = IrpStack->Parameters.DeviceIoControl.OutputBufferLength;

if (!InBufLength || OutBufLength < DataLen)
{
NtStatus = STATUS_INVALID_PARAMETER;
goto End;
}


PrintIrpInfo是这样的


VOID PrintIrpInfo(PIRP Irp)
{
PIO_STACK_LOCATION IrpStack;
IrpStack = IoGetCurrentIrpStackLocation(Irp);

PAGED_CODE();

DbgPrint("tIrp->AssociatedIrp.SystemBuffer = 0x%pn",
Irp->AssociatedIrp.SystemBuffer);
DbgPrint("tIrp->UserBuffer = 0x%pn", Irp->UserBuffer);
DbgPrint("tIrpStack->Parameters.DeviceIoControl.Type3InputBuffer = 0x%pn",
IrpStack->Parameters.DeviceIoControl.Type3InputBuffer);
DbgPrint("tIrpStack->Parameters.DeviceIoControl.InputBufferLength = %dn",
IrpStack->Parameters.DeviceIoControl.InputBufferLength);
DbgPrint("tIrpStack->Parameters.DeviceIoControl.OutputBufferLength = %dn",
IrpStack->Parameters.DeviceIoControl.OutputBufferLength);
return;
}


如果您还记得上一部分中我们使用CreateFile创建了句柄 (HANDLE) ,现在我们可以使用DeviceIoControl使用先前的句柄并调用DrvIoctlDispatcher或(IRP_MJ_DEVICE_CONTROL)以及我们在内核中提供的缓冲区。

    

char OutputBuffer[1000];
char InputBuffer[1000];
ULONG BytesReturned;
BOOL Result;

//
// Performing METHOD_BUFFERED
//
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 false;
}
printf(" OutBuffer (%d): %sn", BytesReturned, OutputBuffer);


我们已经完成了 WDK 基础知识!现在是时候看看我们如何使用 Windows 来构建我们的 VMM 了。



6

每个处理器配置


使用虚拟机管理程序时,与特殊逻辑处理器的关联性是主要考虑因素之一。


在我的Intel Core i7 6820HQ中,我有四个物理核心,每个核心可以同时运行两个线程(由于超线程);因此,我们有八个逻辑处理器,当然还有八组所有寄存器(包括通用寄存器和MSR寄存器),更重要的是,八组VMCS和VMXON区域等。所以我们应该配置我们的VMM来工作八个逻辑处理器。



7


设置亲和力


要获取逻辑处理器的数量,我们可以使用KeQueryActiveProcessorCount(0)。然后我们应该将KAFFINITY掩码传递给KeSetSystemAffinityThread,它设置当前线程的系统关联性。


KAFFINITY掩码可以使用简单的幂函数进行配置:


int
MathPower(int Base, int Exponent)
{
int Result = 1;
for (;;)
{
if (Exponent & 1)
{
Result *= Base;
}

Exponent >>= 1;
if (!Exponent)
{
break;
}
Base *= Base;
}
return Result;
}


之后,我们应该使用以下代码来更改处理器的亲和性,并分别在所有逻辑核心中运行我们的代码:


KAFFINITY AffinityMask;
for (size_t i = 0; i < KeQueryActiveProcessors(); i++)
{
AffinityMask = MathPower(2, i);
KeSetSystemAffinityThread(AffinityMask);

DbgPrint("=====================================================");
DbgPrint("Current thread is executing in %d th logical processor.", i);

// run code here
}


这样,我们就可以在不同的逻辑核心中运行我们的代码。现在,让我们看看虚拟机管理程序所需的其他基本功能。



8


转换物理地址和虚拟地址


VMXON 区域和 VMCS 区域(见下文)使用物理地址作为 VMXON 和 VMPTRLD 指令的操作数,因此我们应该创建函数将虚拟地址转换为物理地址:


UINT64
VirtualToPhysicalAddress(void * Va)
{
return MmGetPhysicalAddress(Va).QuadPart;
}


只要我们不能直接使用物理地址在保护模式下进行修改,我们就必须将物理地址转换为虚拟地址。


UINT64
PhysicalToVirtualAddress(UINT64 Pa)
{
PHYSICAL_ADDRESS PhysicalAddr;
PhysicalAddr.QuadPart = Pa;

return MmGetVirtualForPhysical(PhysicalAddr);
}




9


检查内核中VMX的支持


在上一部分中,我们从用户模式查询虚拟机管理程序是否存在,但我们也应该考虑从内核模式检查虚拟机管理程序。这减少了将来出现内核错误的可能性,或者可能存在使用锁定禁用虚拟机管理程序的情况。顺便说一句,以下代码检查IA32_FEATURE_CONTROLMSR(MSR 地址 3AH)以查看锁定位是否已设置。


BOOLEAN
IsVmxSupported()
{
CPUID Data = {0};

//
// Check for the 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;




10


VMXON区域


VMX 中使用多个区域来处理虚拟机状态。在本部分中,我们将介绍 VMXON 区域和 VMCS 区域。


在执行 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 的限制意味着 VMX 操作仅在分页保护模式下受支持。因此,guest软件不能在未分页保护模式或实地址模式下运行。


现在我们正在配置虚拟机管理程序,我们应该有一个描述虚拟机状态的全局变量。为此目的创建了以下结构。目前,我们有两个名为(VMXON_REGIONVMCS_REGION)的字段,但我们将来会添加新字段并增强此结构。


typedef struct _VIRTUAL_MACHINE_STATE
{
UINT64 VmxonRegion; // VMXON region
UINT64 VmcsRegion; // VMCS region
} VIRTUAL_MACHINE_STATE, *PVIRTUAL_MACHINE_STATE;


当然,还有一个全局变量:


extern VIRTUAL_MACHINE_STATE* g_GuestState;



3.10.1.分配VMXON区域


以下函数(在“Memory.c”中)分配VMXON区域并使用分配区域的指针执行VMXON指令。


BOOLEAN
AllocateVmxonRegion(IN VIRTUAL_MACHINE_STATE * GuestState)
{
// 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 = VirtualToPhysicalAddress(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);

// Changing Revision Identifier
*(UINT64 *)AlignedVirtualBuffer = basic.Fields.RevisionIdentifier;

int Status = __vmx_on(&AlignedPhysicalBuffer);
if (Status)
{
DbgPrint("[*] VMXON failed with status %dn", Status);
return FALSE;
}

g_GuestState->VmxonRegion = AlignedPhysicalBuffer;

return TRUE;
}


我们来解释一下上面的函数。在上面的函数中,我们使用了MmAllocateContiguousMemory分配连续且对齐的页面。我们还可以使用MmAllocateContiguousMemorySpecifyCache指定分配内存的缓存类型。

您可以阅读此链接(https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/wdm/ne-wdm-_memory_caching_type)以了解不同类型的内存缓存。


为了确保 VMX 操作中的正确行为,我们应该在回写式缓存内存中维护 VMCS 区域和相关结构。或者,我们可以将这些区域或结构中的任何一个映射到 UC(未缓存)内存类型。除非必要,否则强烈建议不要这样做,因为这会导致使用这些结构的转换性能受到显着影响。


Writeback是一种每次发生变化时将数据写入缓存的存储方法,但仅在指定的时间间隔或在特定条件下才将数据写入主存中的相应位置。可缓存或不可缓存可以通过分页结构 (PTE) 和内存类型范围寄存器 (MTRR) 中的缓存禁用位来确定,这在本系列的第 7 部分中进行了详细描述。


顺便说一句,我们分配了8192字节,因为不能保证Windows分配对齐的内存,以便我们能找到一块8196字节对齐的4096字节。(通过对齐,我的意思是物理地址应该可以被4096整除,无需任何提醒)。


根据我的经验,MmAllocateContigouslyMemory分配始终是对齐的。可能是因为PFN中每个页都是分配4096字节的,只要我们需要4096字节,就对齐了。


如果您对页框号 (PFN) 感兴趣,您可以阅读Inside Windows Page Frame Number (PFN) – Part 1(https://rayanfam.com/topics/inside-windows-page-frame-number-part1/)和Inside Windows Page Frame Number (PFN) – Part 2(https://rayanfam.com/topics/inside-windows-page-frame-number-part2/)。


现在我们应该将分配的内存地址转换为其物理地址并确保其对齐。

    

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


MmAllocateContigouslyMemory 分配的内存未初始化。内核模式驱动程序必须首先将此内存设置为零,我们使用 RtlSecureZeroMemory 来实现此目的。

    

UINT64 PhysicalBuffer = VirtualToPhysicalAddress(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 %dn", Status);
return FALSE;
}

g_GuestState->VmxonRegion = AlignedPhysicalBuffer;


__vmx_on是执行VMXON 的内部函数。状态码显示不同的含义。


Hypervisor From Scratch:设置我们的第一个虚拟机


如果我们使用 VMXON 设置 VMXON 区域并且失败,则状态等于 1。如果没有任何 VMCS,则状态等于 2,如果操作成功,则状态为零。如果我们执行上述代码两次而不执行 VMXOFF,则会出现错误。


现在,VMXON 区域已准备就绪,我们可以开始了。



11


虚拟机控制数据结构(VMCS)


逻辑处理器在 VMX 操作时使用虚拟机控制数据结构 (VMCS)。它们管理进出 VMX non-root操作(VM 进入和 VM-exit)的转换以及 VMX non-root操作中的处理器行为。该结构由 VMCLEAR、VMPTRLD、VMREAD 和 VMWRITE 指令操作。


Hypervisor From Scratch:设置我们的第一个虚拟机

上图说明了 VMCS 区域中 VMX 操作的生命周期。


3.11.1.初始化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指令需要启动状态为“clear”的VMCS; VMRESUME 指令需要启动状态为“已启动”的 VMCS。 逻辑处理器在相应的VMCS区域中维护VMCS的启动状态。


如果当前VMCS的启动状态为“清除”,则成功执行VMLAUNCH指令将启动状态更改为“已启动”。


VMCLEAR指令的存储器操作数是VMCS的地址。执行指令后,VMCS 的启动状态为“清除”。


没有其他方法可以修改 VMCS 的启动状态(无法使用 VMWRITE 进行修改),并且没有直接的方法来发现它(无法使用 VMREAD 进行读取)。

下图说明了 VMCS 区域的内容。


Hypervisor From Scratch:设置我们的第一个虚拟机

以下代码负责分配 VMCS Region :


BOOLEAN
AllocateVmcsRegion(IN VIRTUAL_MACHINE_STATE * GuestState)
{
//
// 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 = VirtualToPhysicalAddress(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 %dn", Status);
return FALSE;
}

g_GuestState->VmcsRegion = AlignedPhysicalBuffer;

return TRUE;
}


上面的代码与 VMXON Region 完全相同,只是__vmx_vmptrld而不是__vmx_on__vmx_vmptrld是 VMPTRLD 指令的内部函数。


在VMCS中,我们应该在执行VMPTRLD之前从MSR_IA32_VMX_BASIC中找到修订标识符并将其写入VMCS Region中。


MSR_IA32_VMX_BASIC 定义如下:


typedef union _IA32_VMX_BASIC_MSR
{
ULONG64 All;
struct
{
ULONG32 RevisionIdentifier : 31; // [0-30]
ULONG32 Reserved1 : 1; // [31]
ULONG32 RegionSize : 12; // [32-43]
ULONG32 RegionClear : 1; // [44]
ULONG32 Reserved2 : 3; // [45-47]
ULONG32 SupportedIA64 : 1; // [48]
ULONG32 SupportedDualMoniter : 1; // [49]
ULONG32 MemoryType : 4; // [50-53]
ULONG32 VmExitReport : 1; // [54]
ULONG32 VmxCapabilityHint : 1; // [55]
ULONG32 Reserved3 : 8; // [56-63]
} Fields;
} IA32_VMX_BASIC_MSR, *PIA32_VMX_BASIC_MSR;




12


VMXOFF指令


配置完上述区域后,现在是时候考虑当用户模式应用程序不再维护驱动程序句柄时的 DrvClose 了。这时,我们应该终止VMX并释放我们之前分配的所有内存。


以下函数负责执行 VMXOFF,然后调用MmFreeContigouslyMemory来释放分配的内存:


VOID
TerminateVmx()
{
DbgPrint("n[*] Terminating VMX...n");

KAFFINITY AffinityMask;
for (size_t i = 0; i < ProcessorCounts; i++)
{
AffinityMask = MathPower(2, i);
KeSetSystemAffinityThread(AffinityMask);
DbgPrint("ttCurrent thread is executing in %d th logical processor.", i);

__vmx_off();
MmFreeContiguousMemory(PhysicalToVirtualAddress(g_GuestState[i].VmxonRegion));
MmFreeContiguousMemory(PhysicalToVirtualAddress(g_GuestState[i].VmcsRegion));
}

DbgPrint("[*] VMX Operation turned off successfully. n");
}


请记住将 VMXON 和 VMCS Region 转换为虚拟地址,因为MmFreeContigouslyMemory接受虚拟地址;否则,会导致 BSOD。

好的,快完成了!



13


测试VMM


让我们为代码创建一个测试用例,首先是一个通过所有逻辑处理器启动 VMXON 和 VMCS 区域的函数。


VIRTUAL_MACHINE_STATE * g_GuestState;
int ProcessorCounts;

BOOLEAN
InitializeVmx()
{
if (!IsVmxSupported())
{
DbgPrint("[*] VMX is not supported in this machine !");
return FALSE;
}

ProcessorCounts = KeQueryActiveProcessorCount(0);
g_GuestState = ExAllocatePoolWithTag(NonPagedPool,
sizeof(VIRTUAL_MACHINE_STATE) * ProcessorCounts,
POOLTAG);

DbgPrint("n=====================================================n");

KAFFINITY AffinityMask;
for (size_t i = 0; i < ProcessorCounts; i++)
{
AffinityMask = MathPower(2, i);

KeSetSystemAffinityThread(AffinityMask);

DbgPrint("ttCurrent thread is executing in %d th logical processor.", i);

//
// Enabling VMX Operation
//
AsmEnableVmxOperation();

DbgPrint("[*] VMX Operation Enabled Successfully !");

AllocateVmxonRegion(&g_GuestState[i]);
AllocateVmcsRegion(&g_GuestState[i]);

DbgPrint("[*] VMCS Region is allocated at ===============> %llx", g_GuestState[i].VmcsRegion);
DbgPrint("[*] VMXON Region is allocated at ===============> %llx", g_GuestState[i].VmxonRegion);

DbgPrint("n=====================================================n");
}

return TRUE;
}


上面的函数应该从 IRP MJ CREATE 调用,所以让我们将DrvCreate修改为:


NTSTATUS DrvCreate(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{

DbgPrint("[*] DrvCreate Called !");

if (InitializeVmx()) {
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
//
TerminateVmx();

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);

return STATUS_SUCCESS;
}


现在,运行代码。在创建句柄的情况下(您可以看到我们的区域已成功分配)。


Hypervisor From Scratch:设置我们的第一个虚拟机

当我们调用CloseHandle时:从用户模式


Hypervisor From Scratch:设置我们的第一个虚拟机



14


结论


在这一部分中,我们了解了不同类型的 IOCTL 调度。我们在 Windows 中看到了不同的功能来管理 VMM,我们初始化了 VMXON 区域和 VMCS 区域,然后终止了它们。


将来,我们将重点关注扩展页表 (EPT)、VMCS 以及可以在 VMCS 区域中执行以控制guest软件的不同操作。




15


参考


[1] 英特尔® 64 和 IA-32 架构软件开发人员手册合并第 3 卷 (https://software.intel.com/en-us/articles/intel-sdm)
[2] Windows 驱动程序示例 (https://github.com/Microsoft/Windows-driver-samples)
[3] 驱动程序开发第 2 部分:IOCTL 实现简介 (https://www.codeproject.com/Articles/9575/Driver-Development-Part-2-Introduction-to-Implemen)
[3] 超平台(https://github.com/tandasat/HyperPlatform)
[4] PAGED_CODE 宏 (https://technet.microsoft.com/en-us/ff558773(v=vs.96))
[5]HVPP(https://github.com/wbenny/hvpp)
[6] HyperBone 项目 (https://github.com/DarthTon/HyperBone)
[7] 内存缓存类型 (https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/wdm/ne-wdm-_memory_caching_type )
[8] 什么是回写缓存?(https://whatis.techtarget.com/definition/write-back)
[9] I/O 控制代码的缓冲区描述 (https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/buffer-descriptions-for-io-control-codes)
[10] 定义 I/O 控制代码 (https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/defining-io-control-codes)



Hypervisor From Scratch:设置我们的第一个虚拟机


看雪ID:zhang_derek

https://bbs.kanxue.com/user-home-939298.htm

*本文为看雪论坛优秀文章,由 zhang_derek 原创,转载请注明来自看雪社区

Hypervisor From Scratch:设置我们的第一个虚拟机



# 往期推荐

1、自定义Linker实现分析之路

2、逆向分析VT加持的无畏契约纯内核挂

3、阿里云CTF2024-暴力ENOTYOURWORLD题解

4、Hypervisor From Scratch – 基本概念和配置测试环境、进入 VMX 操作

5、V8漏洞利用之对象伪造漏洞利用模板


Hypervisor From Scratch:设置我们的第一个虚拟机


Hypervisor From Scratch:设置我们的第一个虚拟机

球分享

Hypervisor From Scratch:设置我们的第一个虚拟机

球点赞

Hypervisor From Scratch:设置我们的第一个虚拟机

球在看



Hypervisor From Scratch:设置我们的第一个虚拟机

点击阅读原文查看更多

原文始发于微信公众号(看雪学苑):Hypervisor From Scratch:设置我们的第一个虚拟机

版权声明:admin 发表于 2024年4月18日 下午6:00。
转载请注明:Hypervisor From Scratch:设置我们的第一个虚拟机 | CTF导航

相关文章