从头开始了解和使用Hypervisor(第6部分) - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

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

gejigeji web安全 2019-12-31 11:15:00
708857
收藏

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

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

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

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

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

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

捕获当前计算机的状态

在上一篇文章中,我们了解了如何配置不同的VMCS字段并最终在客户状态下执行我们的指令(HLT)。这部分内容与上一部分非常相似,只是对VMCS属性进行了一些细微的更改,让我们看看其中的区别。

首先需要了解的是,你必须为每个内核创建不同的堆栈,因为我们要同时虚拟化所有内核。只要发生VM-Exit ,就会使用这些堆栈。

  // Allocate stack for the VM Exit Handler.
UINT64 VMM_STACK_VA = ExAllocatePoolWithTag(NonPagedPool, VMM_STACK_SIZE, POOLTAG);
vmState[ProcessorID].VMM_Stack = VMM_STACK_VA;

if (vmState[ProcessorID].VMM_Stack == NULL)
{
DbgPrint("[*] Error in allocating VMM Stack.\n");
return;
}
RtlZeroMemory(vmState[ProcessorID].VMM_Stack, VMM_STACK_SIZE);

从上面的代码中可以看到,我们对每个内核分别使用了VMM_Stack(在_VirtualMachineState结构中定义)。

清除VMCS状态,加载VMCS和执行VMLAUNCH等所有其他操作与上一部分完全相同,因此我不再赘述,但是请参阅负责准备当前要虚拟化的内核的函数。

void VirtualizeCurrentSystem(int ProcessorID, PEPTP EPTP, PVOID GuestStack) {

DbgPrint("\n======================== Virtualizing Current System =============================\n");

PAGED_CODE();

// Allocate stack for the VM Exit Handler.
UINT64 VMM_STACK_VA = ExAllocatePoolWithTag(NonPagedPool, VMM_STACK_SIZE, POOLTAG);
vmState[ProcessorID].VMM_Stack = VMM_STACK_VA;

if (vmState[ProcessorID].VMM_Stack == NULL)
{
DbgPrint("[*] Error in allocating VMM Stack.\n");
return;
}
RtlZeroMemory(vmState[ProcessorID].VMM_Stack, VMM_STACK_SIZE);

// Allocate memory for MSRBitMap
vmState[ProcessorID].MSRBitMap = MmAllocateNonCachedMemory(PAGE_SIZE);  // should be aligned
if (vmState[ProcessorID].MSRBitMap == NULL)
{
DbgPrint("[*] Error in allocating MSRBitMap.\n");
return;
}
RtlZeroMemory(vmState[ProcessorID].MSRBitMap, PAGE_SIZE);
vmState[ProcessorID].MSRBitMapPhysical = VirtualAddress_to_PhysicalAddress(vmState[ProcessorID].MSRBitMap);

// Clear the VMCS State
if (!Clear_VMCS_State(&vmState[ProcessorID])) {
goto ErrorReturn;
}

// Load VMCS (Set the Current VMCS)
if (!Load_VMCS(&vmState[ProcessorID]))
{
goto ErrorReturn;
}

DbgPrint("[*] Setting up VMCS for current system.\n");
Setup_VMCS_Virtualizing_Current_Machine(&vmState[ProcessorID], EPTP, GuestStack);

DbgPrint("[*] Executing VMLAUNCH.\n");
__vmx_vmlaunch();

// if VMLAUNCH succeed will never be here !
ULONG64 ErrorCode = 0;
__vmx_vmread(VM_INSTRUCTION_ERROR, &ErrorCode);
__vmx_off();
DbgPrint("[*] VMLAUNCH Error : 0x%llx\n", ErrorCode);
DbgBreakPoint();

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

ReturnWithoutError:
__vmx_off();
DbgPrint("[*] VMXOFF Executed Successfully. !\n");

return TRUE;
// Return With Error
ErrorReturn:
DbgPrint("[*] Fail to setup VMCS !\n");
return FALSE;
}

在上面的代码中,Setup_VMCS_Virtualizing_Current_Machine是新的,因此,让我们看一下此函数的内容。

配置VMCS字段

VMCS字段不是什么新鲜事物,应该对其进行配置以管理虚拟化内核的状态。

除以下内容外,所有VMCS字段均与上一部分相同:

   DbgPrint("[*] MSR_IA32_VMX_PROCBASED_CTLS : 0x%llx\n", AdjustControls(CPU_BASED_ACTIVATE_MSR_BITMAP | CPU_BASED_ACTIVATE_SECONDARY_CONTROLS, MSR_IA32_VMX_PROCBASED_CTLS));
DbgPrint("[*] MSR_IA32_VMX_PROCBASED_CTLS2 : 0x%llx\n", AdjustControls(CPU_BASED_CTL2_RDTSCP | CPU_BASED_CTL2_ENABLE_INVPCID | CPU_BASED_CTL2_ENABLE_XSAVE_XRSTORS, MSR_IA32_VMX_PROCBASED_CTLS2));

__vmx_vmwrite(CPU_BASED_VM_EXEC_CONTROL, AdjustControls(CPU_BASED_ACTIVATE_MSR_BITMAP | CPU_BASED_ACTIVATE_SECONDARY_CONTROLS , MSR_IA32_VMX_PROCBASED_CTLS));
__vmx_vmwrite(SECONDARY_VM_EXEC_CONTROL, AdjustControls(CPU_BASED_CTL2_RDTSCP | CPU_BASED_CTL2_ENABLE_INVPCID | CPU_BASED_CTL2_ENABLE_XSAVE_XRSTORS, MSR_IA32_VMX_PROCBASED_CTLS2));

对于CPU_BASED_VM_EXEC_CONTROL,我们设置了CPU_BASED_ACTIVATE_MSR_BITMAP,这样你就可以启用MSR BITMAP过滤器。此字段的设置是强制性的,由于Windows在简单的内核执行过程中会访问许多MSR,因此,如果不设置此位,则将在每次MSR访问时退出,将大大降低系统速度。

对于SECONDARY_VM_EXEC_CONTROL,我们使用CPU_BASED_CTL2_RDTSCP启用RDTSCP,使用CPU_BASED_CTL2_ENABLE_INVPCID来启用INVPCID,使用CPU_BASED_CTL2_ENABLE_XSAVE_XRSTORS启用XSAVE和XRSTORS。

这是因为我在Windows 10 1809中运行了上面的代码,并且看到Windows内部使用INVPCID和XSAVE(在支持这些函数的处理器中),因此,如果你在虚拟化内核之前未启用它们,则可能导致错误。

请注意,RDTSCP将处理器的时间戳计数器的当前值(64位MSR)读入EDX:EAX寄存器,还将IA32_TSC_AUX MSR的值(地址C0000103H)读入ECX寄存器。该指令为RDTSC添加了排序,并使性能度量比使用RDTSC更准确。 INVPCID,基于进程上下文标识符(PCID)和XSAVE,使转换后备缓冲区(TLB)和分页结构缓存中的映射无效。将处理器状态组件的全部或部分保存到位于由指定的内存地址的XSAVE区域中目标操作数。

请确保检查你在这些字段中输入的最终值,因为你的处理器可能不支持所有这些函数,因此你必须实现一些其他函数或忽略其中一些函数。

除了作为GUEST_RSP的GuestStack,没有其他的可以解释的了,我稍后会告诉你这个参数里应该添加什么。

__vmx_vmwrite(GUEST_RSP, (ULONG64)GuestStack);     //setup guest sp

好了,现在的问题是,我们可以从哪里开始我们的虚拟机监控程序,我的意思是,如何保存特殊内核的状态,然后在其上执行VMLAUNCH,然后继续执行其余部分。

为此,我更改了DrvCreate例程,因此你必须从用户模式应用程序更改CreateFile(稍后再讨论),实际上,DrvCreate是负责将所有核心放在VMX状态的函数。

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

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

// Start Virtualizing Current System

// Initiating EPTP and VMX
PEPTP EPTP = Initialize_EPTP();
Initiate_VMX();

int LogicalProcessorsCount = KeQueryActiveProcessorCount(0);

for (size_t i = 0; i < LogicalProcessorsCount; i++)
{
// Launching VM for Test (in the all logical processor)
int ProcessorID = i;
RunOnProcessor(i, EPTP, VMXSaveState);

}

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

return STATUS_SUCCESS;
}

我们的微型驱动程序旨在仅在一个内核中使用,或者在两三个,甚至所有内核中同时使用,因此从下面的代码中可以看到,它获得了逻辑处理器数。

    int LogicalProcessorsCount = KeQueryActiveProcessorCount(0);

你可以编辑此行以虚拟化特定数量的内核或仅虚拟化特定内核,但是以上代码默认情况下虚拟化所有内核。

更改所有内核上的IRQL

有一个特殊的函数叫做RunOnProcessor,这个函数的第一个参数是处理器ID,第二个参数是初始化的EPTP指针(前面已经做了说明),第三个参数是一个名为VMXSaveState的特殊例程。RunOnProcessor设置处理器关联一个特殊的内核,然后将IRQL提升到Dispatch Level,以便Windows Scheduler无法启动以更改上下文,从而运行我们的例程,并在从VMXSaveState返回时对当前运行的内核进行虚拟化,因此它可以将IRQL降低到以前的水平,现在Windows可以在hypervisor下继续正常执行。 IRQL代表“中断请求级别”,它是Windows特定的机制,用于管理中断或按中断级别分配优先级,因此提高IRQL意味着你的例程将以比普通Windows代码(PASSIVE LEVEL和APC LEVEL)更高的优先级执行。欲了解更多信息,请点此了解。

BOOLEAN RunOnProcessor(ULONG ProcessorNumber, PEPTP EPTP, PFUNC Routine)
{
KIRQL OldIrql;

KeSetSystemAffinityThread((KAFFINITY)(1 << ProcessorNumber));

OldIrql = KeRaiseIrqlToDpcLevel();

Routine(ProcessorNumber, EPTP);

KeLowerIrql(OldIrql);

KeRevertToUserAffinityThread();

return TRUE;
}

VMXSaveState必须保存状态并调用我们已经实现的函数VirtualizeCurrentSystem。

我们必须在程序集文件(VMXState.asm)中扩展此函数,因为所有VMXSaveState都是在程序集中实现。

EXTERN VirtualizeCurrentSystem:PROC

VMXSaveState的实现方式如下:

VMXSaveState PROC

push rax
push rcx
push rdx
push rbx
push rbp
push rsi
push rdi
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15

sub rsp, 28h

; It a x64 FastCall function but as long as the definition of SaveState is same
; as VirtualizeCurrentSystem so we RCX & RDX both have a correct value
; But VirtualizeCurrentSystem also has a stack so it's the third argument
; and according to FastCall it should be in R8

mov r8, rsp

call VirtualizeCurrentSystem

ret

VMXSaveState ENDP

它首先保存所有寄存器的备份,然后由于影子空间而减去堆栈,以实现快速调用函数,然后将RSP放入r8并调用VirtualizeCurrentSystem。在x64 fastcall参数中,应按以下顺序传递:RCX,RDX,R8,R9 + Stack,这意味着我们对该函数的第三个参数是当前的RSP,此值将在我们的VMCS字段中用作客户_RSP。

如果上面的函数没有错误地运行,则我们永远不要得到“ret”指令,因为该状态稍后将在另一个名为“ VMXRestoreState”的函数中继续。

如你在最终调用Setup_VMCS_Virtualizing_Current_Machine的VirtualizeCurrentSystem中看到的那样,GUEST_RIP指向VMXRestoreState,因此在当前内核中执行的第一个函数(例程)是VMXRestoreState。该函数的定义如下:

VMXRestoreState PROC

add rsp, 28h
pop r15
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rdi
pop rsi
pop rbp
pop rbx
pop rdx
pop rcx
pop rax

ret

VMXRestoreState ENDP

首先,在上述函数中,我们删除影子空间并重启寄存器状态,当我们返回RunOnProcessor时,就该降低IRQL了。

此函数将基于你的逻辑内核数被调用很多次,最终,所有内核都处于VMX操作下,而现在你处于VMX非根操作下。

更改用户模式应用程序

基于上述假设,我们必须对用户模式应用程序进行一些细微的更改,以便在加载驱动程序之后,可以使用它通知内核模式代码开始和结束hypervisor。

使用CreateFile处理

在检查了供应商和hypervisor的存在之后,现在我们必须通过创建文件用户模式函数调用DrvCreate。

   HANDLE handle = CreateFile("\\\\.\\MyHypervisorDevice",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ |
FILE_SHARE_WRITE,
NULL, /// lpSecurityAttirbutes
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL |
FILE_FLAG_OVERLAPPED,
NULL); /// lpTemplateFile 

if (handle == INVALID_HANDLE_VALUE)
{
DWORD errNum = GetLastError();
printf("[*] CreateFile failed : %d\n", errNum);
return 1;

}

CreateFile API为我们提供了一个可以在将来的函数中使用的句柄,但是每当你关闭应用程序或调用CloseHandle时,就会自动调用DrvClose。 DrvClose将关闭hypervisor,并将状态重启到之前的状态,即未虚拟化时的状态。

使用VMX监控函数

配置完上述所有字段之后,现在该使用VMX来使用监控函数了,你将了解在安全应用程序中这些函数的独特之处。

CR3-Target控件

VM执行控制字段包括一组4个CR3目标值和一个CR3目标计数。如果你看到我在Setup_VMCS_Virtualizing_Current_Machine中介绍的VMCS,则可以看到以下几行:

   __vmx_vmwrite(CR3_TARGET_COUNT, 0);
__vmx_vmwrite(CR3_TARGET_VALUE0, 0);
__vmx_vmwrite(CR3_TARGET_VALUE1, 0);
__vmx_vmwrite(CR3_TARGET_VALUE2, 0);
__vmx_vmwrite(CR3_TARGET_VALUE3, 0);

Intel将CR3-Target控件定义为“如果VMX非根操作中的源操作数与其中一个值匹配,则在VMX非根操作中对CR3执行MOV不会导致VM退出。如果CR3目标计数为n,则仅考虑前n个CR3目标值。”

未来的处理器可能会扩展Cr3-Target计数,使用此函数的实现过程如下所示:

// Index starts from 0 , not 1
BOOLEAN SetTargetControls(UINT64 CR3, UINT64 Index) {
if (Index >= 4)
{
// Not supported for more than 4 , at least for now :(
return FALSE;
}

UINT64 temp = 0;

if (CR3 == 0)
{
if (gCR3_Target_Count <= 0)
{
// Invalid command as gCR3_Target_Count cannot be less than zero
return FALSE;
}
else
{
gCR3_Target_Count -= 1;
if (Index == 0)
{
__vmx_vmwrite(CR3_TARGET_VALUE0, 0);
}
if (Index == 1)
{
__vmx_vmwrite(CR3_TARGET_VALUE1, 0);
}
if (Index == 2)
{
__vmx_vmwrite(CR3_TARGET_VALUE2, 0);
}
if (Index == 3)
{
__vmx_vmwrite(CR3_TARGET_VALUE3, 0);
}
}
}
else
{
if (Index == 0)
{
__vmx_vmwrite(CR3_TARGET_VALUE0, CR3);
}
if (Index == 1)
{
__vmx_vmwrite(CR3_TARGET_VALUE1, CR3);
}
if (Index == 2)
{
__vmx_vmwrite(CR3_TARGET_VALUE2, CR3);
}
if (Index == 3)
{
__vmx_vmwrite(CR3_TARGET_VALUE3, CR3);
}
gCR3_Target_Count += 1;
}

__vmx_vmwrite(CR3_TARGET_COUNT, gCR3_Target_Count);
return TRUE;
}

目前,我还没有很好的例子说明此控件在常规Windows中可能有什么帮助,因为每个过程都有成千上万的CR3更改,但它在一些特殊情况下被用来提高整体性能。

处理客户CPUID执行

CPUID是导致VM退出的主要指令之一,如你所知,使用CPUID是因为它允许软件发现处理器的详细信息。我看到软件使用CPUID刷新不支持RDTSCP等指令的处理器的管道,以便他们可以使用CPUID + RDTSC并以某种方式获得更好的结果。

每当具有任何特权级别的任何软件执行CPUID指令时,便会调用你的处理程序,现在你可以决定该软件显示的内容,例如,我之前发表过一篇文章“破坏恶意软件的Anti-VM技术”。本文介绍了如何通过更改CPUID指令结果的方式配置VMWare,以使具有Anti-VM技术的恶意软件无法通过执行CPUID来了解它们是在虚拟化环境中执行的。VMWare和其他虚拟环境执行与处理CPUID完全相同的机制,在以下示例中,我只是将寄存器的状态(发生VM退出后的注册状态)传递给了HandleCPUID。此函数决定所请求的CPUID应该具有修改的结果,还是仅执行CPUID并返回原始结果。

处理每个VM-Exit的默认行为(由VMX Non-root中的CPUID执行引起)是通过使用_cpuidex(它是CPUID的固有函数)来获得原始结果。

__cpuidex(cpu_info, (INT32)state->rax, (INT32)state->rcx);

因此,你可以看到VMX Non-root本身无法执行CPUID,我们可以在VMX Root模式下执行CPUID,并将结果返回给VMX Non-root模式。

现在,我们需要检查RAX(CPUID索引)是否为1,这是因为有一个指示符位显示当前计算机是否在hypervisor下运行,就像许多其他虚拟机一样,我们将HYPERV_HYPERVISOR_PRESENT_BIT设置为表明在hypervisor下运行。

关于hypervisor提供程序还需要进行第二项检查,我们将其设置为“HVFS”,以表明我们的hypervisor是[H] yper [V] isor [F] rom [S] cratch。

   // Check if this was CPUID 1h, which is the features request.
if (state->rax == 1)
{

// Set the Hypervisor Present-bit in RCX, which Intel and AMD have both
// reserved for this indication.
cpu_info[2] |= HYPERV_HYPERVISOR_PRESENT_BIT;
}

else if (state->rax == HYPERV_CPUID_INTERFACE)
{
// Return our interface identifier
cpu_info[0] = 'HVFS'; // [H]yper[v]isor [F]rom [S]cratch 
}

现在,你可以轻松地在上述代码中添加更多检查,并自定义你的CPUID过滤器,例如,更改计算机供应商字符串等。

这是与hypervisor相关的常量的定义:

#define HYPERV_CPUID_VENDOR_AND_MAX_FUNCTIONS   0x40000000
#define HYPERV_CPUID_INTERFACE                  0x40000001
#define HYPERV_CPUID_VERSION                    0x40000002
#define HYPERV_CPUID_FEATURES                   0x40000003
#define HYPERV_CPUID_ENLIGHTMENT_INFO           0x40000004
#define HYPERV_CPUID_IMPLEMENT_LIMITS           0x40000005

#define HYPERV_HYPERVISOR_PRESENT_BIT           0x80000000
#define HYPERV_CPUID_MIN                        0x40000005
#define HYPERV_CPUID_MAX                        0x4000ffff

最后,我们将它们放入寄存器,以便每次执行例程时,客户虚拟机都会获得适当的结果。

   state->rax = cpu_info[0];
state->rbx = cpu_info[1];
state->rcx = cpu_info[2];
state->rdx = cpu_info[3];

将以上所有代码放在一起,就会具有以下函数:

BOOLEAN HandleCPUID(PGUEST_REGS state)
{
INT32 cpu_info[4];

// Check for the magic CPUID sequence, and check that it is coming from
// Ring 0. Technically we could also check the RIP and see if this falls
// in the expected function, but we may want to allow a separate "unload"
// driver or code at some point.

ULONG Mode = 0;
__vmx_vmread(GUEST_CS_SELECTOR, &Mode);
Mode = Mode & RPL_MASK;

if ((state->rax == 0x41414141) && (state->rcx == 0x42424242) && Mode == DPL_SYSTEM)
{
return TRUE; // Indicates we have to turn off VMX
}

// Otherwise, issue the CPUID to the logical processor based on the indexes
// on the VP's GPRs.
__cpuidex(cpu_info, (INT32)state->rax, (INT32)state->rcx);

// Check if this was CPUID 1h, which is the features request.
if (state->rax == 1)
{

// Set the Hypervisor Present-bit in RCX, which Intel and AMD have both
// reserved for this indication.
cpu_info[2] |= HYPERV_HYPERVISOR_PRESENT_BIT;
}

else if (state->rax == HYPERV_CPUID_INTERFACE)
{
// Return our interface identifier
cpu_info[0] = 'HVFS'; // [H]yper[v]isor [F]rom [S]cratch 
}

// Copy the values from the logical processor registers into the VP GPRs.
state->rax = cpu_info[0];
state->rbx = cpu_info[1];
state->rcx = cpu_info[2];
state->rdx = cpu_info[3];

return FALSE; // Indicates we don't have to turn off VMX

}

某种程度上类似于对CPUID的指令级挂钩,通过在下面列出这些指令的基础上配置基于主处理器和辅助处理器的控件,你还可以对许多其他重要指令具有相同的处理函数。

导致VM退出的条件

根据VM执行控件的设置,以下指令会导致VM以VMX非root用户操作退出。

CLTS
ENCLS
HLT
IN, INS/INSB/INSW/INSD, OUT, OUTS/OUTSB/OUTSW/OUTSD.
INVLPG
INVPCID
LGDT, LIDT, LLDT, LTR, SGDT, SIDT, SLDT, STR
LMSW
MONITOR
MOV from CR3/CR8, MOV to CR0/1/3/4/8
MOV DR
MWAIT
PAUSE
RDMSR, WRMSR
RDPMC
RDRAND, RDSEED
RDTSC, RDTSCP
RSM
VMREAD, VMWRITE
WBINVD
XRSTORS, XSAVES

控制寄存器修改检测

检测和处理控制寄存器的修改是hypervisor提供的重要安全函数之一,想象一下,如果有人利用Windows内核(或任何其他操作系统)并希望取消设置控制寄存器的某个位(例如写保护或SMEP),则hypervisor会检测到此修改并阻止进一步执行。

这些特性就是为什么使用hypervisor作为安全机制比使用单独的环(1,2)要好得多的原因。

注意,SMEP代表管理模式执行保护。CR4.SMEP允许保护页面免受主管模式指令的访存,如果CR4.SMEP = 1,则在超级用户模式下运行的软件无法从在用户模式下可访问的线性地址中获取指令,并且WP代表写保护。 CR0.WP允许保护页面免受超级用户模式写入,如果CR0.WP = 0,则允许对具有只读访问权限的线性地址进行超级用户模式写访问;否则,将不进行任何操作。如果CR0.WP = 1,则不允许这样做,无论CR0.WP的值如何,都不允许对具有只读访问权限的线性地址进行用户模式写访问。

首先,让我们阅读一下 VMCS的GUEST_CRs 和EXIT_QUALIFICATION。

   __vmx_vmread(EXIT_QUALIFICATION , &ExitQualification);
__vmx_vmread(GUEST_CR0 , &GuestCR0);
__vmx_vmread(GUEST_CR3 , &GuestCR3);
__vmx_vmread(GUEST_CR4,  &GuestCR4);

如你所见,下图显示了退出指令运行的全过程。

请注意,EXIT_QUALIFCATION是一般的VMCS字段,在某些情况下,例如由无效的VMCS布局,控制寄存器修改,I/O位图和其他事件引起的VM退出。

ä»å¤´å¼å§äºè§£å使ç¨Hypervisorï¼ç¬¬6é¨åï¼

现在,从上图可以看到,让我们基于EXIT_QUALIFICATION创建一些变量来描述这种情况。

movcrControlRegister = (ULONG)(ExitQualification & 0x0000000F);
movcrAccessType = (ULONG)((ExitQualification & 0x00000030) >> 4);
movcrOperandType = (ULONG)((ExitQualification & 0x00000040) >> 6);
movcrGeneralPurposeRegister = (ULONG)((ExitQualification & 0x00000F00) >> 8);

每当由于某些指令(例如MOV CRx,REG)导致VM退出时,我们都必须从VMX根操作手动修改客户 VMCS的CRx。从以下代码中,你可以看到如何使用VMWRITE修改VMCS的客户_CRx字段。

 if (movcrAccessType == 0)
 {
 
 /* CRx rax); break;
 case 1:  __vmx_vmwrite(x, GuestRegs->rcx); break;
 case 2:  __vmx_vmwrite(x, GuestRegs->rdx); break;
 case 3:  __vmx_vmwrite(x, GuestRegs->rbx); break;
 case 4:  __vmx_vmwrite(x, GuestRegs->rsp); break;
 case 5:  __vmx_vmwrite(x, GuestRegs->rbp); break;
 case 6:  __vmx_vmwrite(x, GuestRegs->rsi); break;
 case 7:  __vmx_vmwrite(x, GuestRegs->rdi); break;
 }

否则,我们必须从客户VMCS(不是主机控制寄存器,因为它可能有所不同)读取CRx,然后放入相应的寄存器(在调用VM-Exit处理程序时保存的寄存器中),然后继续执行VMRESUME。这样,客户就认为好像成功执行了MOV reg, CRx。

把它们放在一起,我们就得到了这样一个函数:

void HandleControlRegisterAccess(PGUEST_REGS GuestRegs)
{
ULONG movcrControlRegister = 0;
ULONG movcrAccessType = 0;
ULONG movcrOperandType = 0;
ULONG movcrGeneralPurposeRegister = 0;

ULONG64 ExitQualification = 0;
ULONG64 GuestCR0 = 0;
ULONG64 GuestCR3 = 0;
ULONG64 GuestCR4 = 0;

ULONG64 x = 0;

__vmx_vmread(EXIT_QUALIFICATION , &ExitQualification);
__vmx_vmread(GUEST_CR0 , &GuestCR0);
__vmx_vmread(GUEST_CR3 , &GuestCR3);
__vmx_vmread(GUEST_CR4,  &GuestCR4);

movcrControlRegister = (ULONG)(ExitQualification & 0x0000000F);
movcrAccessType = (ULONG)((ExitQualification & 0x00000030) >> 4);
movcrOperandType = (ULONG)((ExitQualification & 0x00000040) >> 6);
movcrGeneralPurposeRegister = (ULONG)((ExitQualification & 0x00000F00) >> 8);

/* Process the event (only for MOV CRx, REG instructions) */
if (movcrOperandType == 0 && (movcrControlRegister == 0 || movcrControlRegister == 3 || movcrControlRegister == 4))
{
if (movcrAccessType == 0)
{

/* CRx rax); break;
case 1:  __vmx_vmwrite(x, GuestRegs->rcx); break;
case 2:  __vmx_vmwrite(x, GuestRegs->rdx); break;
case 3:  __vmx_vmwrite(x, GuestRegs->rbx); break;
case 4:  __vmx_vmwrite(x, GuestRegs->rsp); break;
case 5:  __vmx_vmwrite(x, GuestRegs->rbp); break;
case 6:  __vmx_vmwrite(x, GuestRegs->rsi); break;
case 7:  __vmx_vmwrite(x, GuestRegs->rdi); break;
}
}
else if (movcrAccessType == 1)
{
/* reg32 rax = x; break;
case 1:  GuestRegs->rcx = x; break;
case 2:  GuestRegs->rdx = x; break;
case 3:  GuestRegs->rbx = x; break;
case 4:  GuestRegs->rsp = x; break;
case 5:  GuestRegs->rbp = x; break;
case 6:  GuestRegs->rsi = x; break;
case 7:  GuestRegs->rdi = x; break;
default: break;
}

}

}

之所以必须强制执行诸如HandleControlRegisterAccess之类的函数,是因为处理器(甚至是最新的Intel处理器)对某些基于处理器的VM执行控件(例如CR3-Load Exiting和CR3-Store Existing)具有1设置,因此你必须管理此类VM -自行退出,但是如果处理器无需进行这些设置也可以继续运行,则强烈建议减少VM退出的数量,因为现代OS的访问控制注册很多,因此,它的性能将受到明显影响。

下一章,我会接着介绍这些函数的实现过程。

本文翻译自:https://rayanfam.com/topics/hypervisor-from-scratch-part-6/如若转载,请注明原文地址:
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务