从头开始了解和使用Hypervisor(第3部分)
2019-12-16 11:40:19 Author: www.4hou.com(查看原文) 阅读量:234 收藏

导语:毫不夸张地说,学习完本文,你完全可以创建自己的虚拟环境,并且可以了解VMWare,VirtualBox,KVM和其他虚拟化软件如何使用处理器的函数来创建虚拟环境。

从头开始了解和使用Hypervisor(第1部分)

从头开始了解和使用Hypervisor(第2部分)

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

逻辑处理器在VMX操作中时会使用虚拟机控制数据结构(VMCS),它们管理进出VMX非根操作(VM项和VM出口)的转换,以及VMX非根操作中的处理器行为,该结构由新指令VMCLEAR,VMPTRLD,VMREAD和VMWRITE操作。

22.png

上图演示了VMCS区域上的生命周期VMX操作。

初始化VMCS区域

VMM可以使用不同的VMCS区域,因此你需要设置逻辑处理器关联性并多次运行初始化例程。

VMCS所在的位置称为“VMCS区域”。

VMCS区域的特点如下:

1、4 KB(位11:0必须为零);

2、必须与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的启动状态为“clear”,则成功执行VMLAUNCH指令会将启动状态更改为“launch”。

VMCLEAR指令的内存操作数是VMCS的地址,执行指令后,该VMCS的启动状态为“clear”。

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

下图就是VMCS区域的内容。

23.png

以下代码负责分配VMCS区域:

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;
}

除了__vmx_vmptrld而不是__vmx_on以外,以上代码与VMXON区域完全相同,__ vmx_vmptrld是VMPTRLD指令的固有函数。

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

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;

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[i].VMXON_REGION));
		MmFreeContiguousMemory(PhysicalAddress_to_VirtualAddress(vmState[i].VMCS_REGION));

	}

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

}

请记住将VMXON和VMCS区域转换为虚拟地址,因为MmFreeContiguousMemory接受VA,否则会导致BSOD。

测试VMM

现在,让我们为代码创建一个测试案例,首先是一个通过所有逻辑处理器初始化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[i]);
		Allocate_VMCS_Region(&vmState[i]);


		DbgPrint("[*] VMCS Region is allocated at  ===============> %llx", vmState[i].VMCS_REGION);
		DbgPrint("[*] VMXON Region is allocated at ===============> %llx", vmState[i].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;
}

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

31.png

从用户模式调用CloseHandle的情况如下:

32.png

本部分所说的源代码可在GitHub上找到。

在这一部分中,我们了解了不同类型的IOCTL调度,然后在Windows中看到了用于管理hypervisorVMM的不同函数,并初始化了VMXON区域和VMCS区域,然后终止了它们。

接下来,我们将重点介绍VMCS和可在VMCS区域中执行的各种操作,以控制客户软件。

首先,你还应该对分页机制以及页表的工作原理有基本的了解,这里有一篇关于分页表的好文章,你可以了解一下。该教程的完整源代码可在GitHub上找到。

二级地址转换(SLAT)或嵌套分页是分页机制中的扩展层,用于将基于硬件的虚拟化虚拟地址映射到物理内存中。

自第三代Opteron处理器和微架构代号Barcelona推出以来,AMD通过称为嵌套页表(NPT)的快速虚拟索引(RVI)技术实施了SLAT。自从引入微体系结构代码名称Nehalem及其称为扩展页表(EPT)以来,Intel还在Intel®VT-x技术中实现了SLAT,并在Core i9,Core i7,Core i5和Core i3处理器中使用。

有两种方法,第一种是“影子页表”,第二种是“扩展页表”。

软件辅助分页(影子页表)

hypervisor使用影子页表来跟踪物理内存的状态,在该状态中,客户认为其可以访问物理内存,但是在现实世界中,硬件会阻止其访问硬件内存,否则它将控制主机和它不是它想要的。

在这种情况下,VMM维护影子页表,这些影子页表将客户虚拟页面直接映射到机器页面,并且任何客户对V-> P表的修改都同步到VMM V-> M影子页表。

1.png

顺便说一句,本文不建议使用影子页表,因为它总是会导致VMM陷阱(这会导致大量VM退出),并且由于每台交换机上的TLB刷新而导致性能损失,另一个警告是由于客户页表的影子复制而导致的内存开销。

硬件辅助分页(扩展页表)

2.jpg

为了降低影子页表的复杂性并避免过多的虚拟机退出并减少TLB刷新次数,EPT是一种硬件辅助分页策略,旨在提高性能。

根据VMware评估报告:

“ EPT在MMU密集型基准测试中可将性能提升高达48%,在MMU密集型微基准测试中可将性能提升高达600%”。

EPT还实现了另一页表层次结构,以将客户虚拟地址映射到在主内存中有效的客户物理地址。

在EPT中:

1.客户操作系统维护一个页表,该页表用于生成客户物理地址。

2.另一个页表由VMM维护,用于将客户物理地址映射到主机物理地址。

因此,对于每个内存访问操作,EPT MMU都会直接从客户页表中获取客户物理地址,然后通过VMM映射表自动获取主机物理地址。

扩展页表与影子页表

EPT:

1.便利任何要求的地址;

1.1适用于在执行时有大量页表丢失的程序;

1.2退出虚拟机的机会更少(上下文切换更少);

2.两层EPT:

2.1意味着每次访问都需要遍历两张表;

3.更容易开发

3.1许多特殊的寄存器;

3.2硬件帮助客户操作系统通知VMM

SPT:

1.仅在SPT进入未命中时运行:

1.1适用于仅经常访问某些地址的程序;

1.2VMM可能会拦截每次访问(许多陷阱)

2一个参考

2.1点击页面时快速便捷;

3.难以发展

3.1两层结构;

3.2复杂的反向图;

3.3权限模拟;

检测对EPT,NPT的支持

如果要在不使用程序集(CPUID)的情况下查看系统是否支持Intel处理器上的EPT或AMD处理器上的NPT,则可以从Sysinternals下载coreinfo.exe,然后运行它。最后一行将显示你的处理器是否支持EPT或NPT。

3.png

EPT转换

EPT定义了一个地址转换层,可增强线性地址的转换。

扩展页表机制(EPT)是一项可用于支持物理内存虚拟化的函数,使用EPT时,通常会被视为物理地址(并用于访问内存)的某些地址将被视为客户物理地址。通过遍历一组EPT分页结构来转换客户物理地址,以生成用于访问内存的物理地址。

EPT在“启用EPT” VM执行控件为1时使用,它将转换VMX非根操作中使用的客户物理地址以及VM项用于事件注入的客户物理地址。

EPT转换与常规分页转换完全一样,但有一些细微的差异。在分页中,处理器将虚拟地址转换为物理地址,而在EPT转换中,你要将客户虚拟地址转换为主机物理地址。

如果你熟悉分页,则第3个控制寄存器(CR3)是PML4表的基地址(在x64处理器中或更普遍地,它指向根分页目录),在EPT 客户虚拟机中不知道EPT转换,因此它具有CR3,但是此CR3用于将客户虚拟地址转换为客户物理地址。只要你找到目标客户物理地址,它都是EPT机制,将客户物理地址视为虚拟地址,并且EPTP是CR3。

因此,你的目标物理地址应分为4部分,前9位指向EPT PML4E(请注意PML4基址在EPTP中),后9位指向EPT PDPT项(PDPT的基址来自EPT PML4E ),后9位指向EPT PD项(PD的基地址来自EPT PDPTE),客户物理地址的后9位指向EPT PT表中的项(PT的基地址来自EPT PDE ),现在EPT PT项指向相应页面的主机物理地址。

4.png

一个简单的虚拟到物理地址的转换涉及到访问4个物理地址,会发生什么呢?

答案是处理器在内部一一转换所有表的物理地址,这就是为什么客户软件中的分页和访问内存比常规地址转换要慢的原因。下图说明了从客户虚拟地址到主机物理地址的操作:

5.png

如果你想考虑x86 EPT虚拟化,例如,假设CR4.PAE = CR4.PSE =0。则32位线性地址的转换如下:

1.线性地址的位31:22在客户页面目录中的CR3客户物理地址中选择一个项,客户页面目录项(PDE)的客户物理地址通过EPT转换,以确定客户PDE的物理地址。

2.线性地址的位21:12在客户PDE中的客户物理地址处的客户页表中选择一个项,客户页表项(PTE)的客户物理地址通过EPT转换,以确定客户PTE的物理地址。

3.线性地址的位11:0是位于客户PTE中客户物理地址处的页面框架中的偏移量,由该偏移量确定的客户物理地址通过EPT转换,以确定原始线性地址转换为的物理地址。

请注意,PAE代表物理地址扩展,它是x86体系结构的内存管理函数,用于扩展地址空间,PSE代表页面大小扩展,它是指x86处理器的函数,该函数允许页面大于传统的4 KiB大小。

除了将客户物理地址转换为主机物理地址之外,EPT还指定访问该地址时允许软件的特权,尝试进行不允许的访问称为EPT违规,并导致VM退出。

请记住,在没有访问权限的情况下,地址永远不会通过EPT转换。在访问(读取或写入)内存中的该位置之前,永远不会使用你的客户物理地址。

实施扩展页表(EPT)

此时,我们应该将(VMWRITE)EPTP或扩展页表指针写入VMCS。 EPTP结构如下所述:

6.png

可以使用以下结构来描述以上表格:

// See Table 24-8. Format of Extended-Page-Table Pointer
typedef union _EPTP {
	ULONG64 All;
	struct {
		UINT64 MemoryType : 3; // bit 2:0 (0 = Uncacheable (UC) - 6 = Write - back(WB))
		UINT64 PageWalkLength : 3; // bit 5:3 (This value is 1 less than the EPT page-walk length) 
		UINT64 DirtyAndAceessEnabled : 1; // bit 6  (Setting this control to 1 enables accessed and dirty flags for EPT)
		UINT64 Reserved1 : 5; // bit 11:7 
		UINT64 PML4Address : 36;
		UINT64 Reserved2 : 16;
	}Fields;
}EPTP, *PEPTP;

所有EPT表中的每个项均为64位长。 EPT PML4E和EPT PDPTE和EPT PD相同,但EPT PTE略有不同。

一个EPT项是这样的:

8.png

第一个表是PML4,下表显示了EPT PML4项(PML4E)的格式。

9.png

PML4E可以是如下的结构:

// See Table 28-1. 
typedef union _EPT_PML4E {
	ULONG64 All;
	struct {
		UINT64 Read : 1; // bit 0
		UINT64 Write : 1; // bit 1
		UINT64 Execute : 1; // bit 2
		UINT64 Reserved1 : 5; // bit 7:3 (Must be Zero)
		UINT64 Accessed : 1; // bit 8
		UINT64 Ignored1 : 1; // bit 9
		UINT64 ExecuteForUserMode : 1; // bit 10
		UINT64 Ignored2 : 1; // bit 11
		UINT64 PhysicalAddress : 36; // bit (N-1):12 or Page-Frame-Number
		UINT64 Reserved2 : 4; // bit 51:N
		UINT64 Ignored3 : 12; // bit 63:52
	}Fields;
}EPT_PML4E, *PEPT_PML4E;

只要我们要进行4级分页,第二个表就是EPT Page-Directory-Pointer-Table (PDTP),下图说明了PDPTE的格式:

11.png

PDPTE的结构如下:

// See Table 28-3
typedef union _EPT_PDPTE {
	ULONG64 All;
	struct {
		UINT64 Read : 1; // bit 0
		UINT64 Write : 1; // bit 1
		UINT64 Execute : 1; // bit 2
		UINT64 Reserved1 : 5; // bit 7:3 (Must be Zero)
		UINT64 Accessed : 1; // bit 8
		UINT64 Ignored1 : 1; // bit 9
		UINT64 ExecuteForUserMode : 1; // bit 10
		UINT64 Ignored2 : 1; // bit 11
		UINT64 PhysicalAddress : 36; // bit (N-1):12 or Page-Frame-Number
		UINT64 Reserved2 : 4; // bit 51:N
		UINT64 Ignored3 : 12; // bit 63:52
	}Fields;
}EPT_PDPTE, *PEPT_PDPTE;

对于第三个分页表,我们应该实现EPT页面目录项(PDE),如下所述:

13.png

PDE的结构:

// See Table 28-5
typedef union _EPT_PDE {
	ULONG64 All;
	struct {
		UINT64 Read : 1; // bit 0
		UINT64 Write : 1; // bit 1
		UINT64 Execute : 1; // bit 2
		UINT64 Reserved1 : 5; // bit 7:3 (Must be Zero)
		UINT64 Accessed : 1; // bit 8
		UINT64 Ignored1 : 1; // bit 9
		UINT64 ExecuteForUserMode : 1; // bit 10
		UINT64 Ignored2 : 1; // bit 11
		UINT64 PhysicalAddress : 36; // bit (N-1):12 or Page-Frame-Number
		UINT64 Reserved2 : 4; // bit 51:N
		UINT64 Ignored3 : 12; // bit 63:52
	}Fields;
}EPT_PDE, *PEPT_PDE;

最后一页是EPT,如下所述。

15.png

PTE将是:

请注意,除了上述页面之外,你还有EPTMemoryType,IgnorePAT,DirtyFlag和SuppressVE。

// See Table 28-6
typedef union _EPT_PTE {
	ULONG64 All;
	struct {
		UINT64 Read : 1; // bit 0
		UINT64 Write : 1; // bit 1
		UINT64 Execute : 1; // bit 2
		UINT64 EPTMemoryType : 3; // bit 5:3 (EPT Memory type)
		UINT64 IgnorePAT : 1; // bit 6
		UINT64 Ignored1 : 1; // bit 7
		UINT64 AccessedFlag : 1; // bit 8	
		UINT64 DirtyFlag : 1; // bit 9
		UINT64 ExecuteForUserMode : 1; // bit 10
		UINT64 Ignored2 : 1; // bit 11
		UINT64 PhysicalAddress : 36; // bit (N-1):12 or Page-Frame-Number
		UINT64 Reserved : 4; // bit 51:N
		UINT64 Ignored3 : 11; // bit 62:52
		UINT64 SuppressVE : 1; // bit 63
	}Fields;
}EPT_PTE, *PEPT_PTE;

还有其他类型的实现页面走(2或3级分页),如果你设置PDPTE的第7位(图1 GB)或7的PDE(图2 MB)而不是实现4层分页(像我们想做的主题)设置这些位但记住相应的表是不同的。Alex Ionescu的SimpleVisor就是这样实现的一个例子。

需要注意的是,几乎所有上述结构都具有36位物理地址,这意味着我们的hypervisor仅支持4级分页。这是因为每个页表(和每个EPT页表)都由512个项组成,这意味着你需要9位才能选择一个项,并且只要我们有4个级别表,我们就不能使用超过36个(4 * 9)位。在所有主要的操作系统(例如Windows或Linux)中都未实现另一种具有更广泛地址范围的方法。我将在本主题的稍后部分简要描述EPT PML5E,但由于它尚不流行,因此我们不会在hypervisor中实现它。

顺便说一下,N是处理器支持的物理地址宽度,在EAX中80000008H的CPUID提供了在EAX位7:0中支持的宽度。

让我们看一下其余的代码,以下代码是Initialize_EPTP函数,该函数负责分配和映射EPTP。

请注意,PAGED_CODE()宏可确保调用线程以足够低的IRQL运行以允许分页。

UINT64 Initialize_EPTP()
{
	PAGED_CODE();
        ...

首先,分配EPTP并将其置零。

	// Allocate EPTP
	PEPTP EPTPointer = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, POOLTAG);

	if (!EPTPointer) {
		return NULL;
	}
	RtlZeroMemory(EPTPointer, PAGE_SIZE);

现在,我们的EPT PML4表需要一个空白页。

	//	Allocate EPT PML4
	PEPT_PML4E EPT_PML4 = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, POOLTAG);
	if (!EPT_PML4) {
		ExFreePoolWithTag(EPTPointer, POOLTAG);
		return NULL;
	}
	RtlZeroMemory(EPT_PML4, PAGE_SIZE);

而PDPT的另一个空白页:

//	Allocate EPT Page-Directory-Pointer-Table
	PEPT_PDPTE EPT_PDPT = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, POOLTAG);
	if (!EPT_PDPT) {
		ExFreePoolWithTag(EPT_PML4, POOLTAG);
		ExFreePoolWithTag(EPTPointer, POOLTAG);
		return NULL;
	}
	RtlZeroMemory(EPT_PDPT, PAGE_SIZE);

当然,有关页面目录表的情况也是如此。

	//	Allocate EPT Page-Directory
	PEPT_PDE EPT_PD = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, POOLTAG);

	if (!EPT_PD) {
		ExFreePoolWithTag(EPT_PDPT, POOLTAG);
		ExFreePoolWithTag(EPT_PML4, POOLTAG);
		ExFreePoolWithTag(EPTPointer, POOLTAG);
		return NULL;
	}
	RtlZeroMemory(EPT_PD, PAGE_SIZE);

最后一张表是EPT页表的空白页。

	//	Allocate EPT Page-Table
	PEPT_PTE EPT_PT = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, POOLTAG);

	if (!EPT_PT) {
		ExFreePoolWithTag(EPT_PD, POOLTAG);
		ExFreePoolWithTag(EPT_PDPT, POOLTAG);
		ExFreePoolWithTag(EPT_PML4, POOLTAG);
		ExFreePoolWithTag(EPTPointer, POOLTAG);
		return NULL;
	}
	RtlZeroMemory(EPT_PT, PAGE_SIZE);

现在我们已经有了所有页面,现在让我们连续分配两页(2 * 4096),因为我们需要一个页面来启动RIP,另一个页面来启动堆栈(RSP)。之后,我们需要两个具有执行,读取和写入权限的EPT页表项(PTE)。物理地址应除以4096(PAGE_SIZE),因为如果我们将十六进制数除以4096(0x1000),则从右边开始的12位数字(为零)将消失,并且这12位数字用于在4096个字节之间进行选择。

顺便说一句,我们也让堆栈可以执行,这是因为在常规VM中,我们应该将RWX放到所有页面上,因为这是内部页表负责设置或清除NX位的责任。为了特殊目的,我们需要从EPT表中更改它们(例如,截取特定页面的指令)。从EPT表中进行更改将导致EPT违规,这样我们就可以拦截这些事件。

实际需要两个页面,但是我们需要在客户软件中构建页表,因此我们最多分配了10个页面。

本文翻译自:https://rayanfam.com/topics/hypervisor-from-scratch-part-3/ 与 https://rayanfam.com/topics/hypervisor-from-scratch-part-4/如若转载,请注明原文地址: https://www.4hou.com/web/22061.html


文章来源: https://www.4hou.com/web/22061.html
如有侵权请联系:admin#unsafe.sh