首页
社区
课程
招聘
Hypervisor From Scratch – 第 5 部分:设置 VMCS 和运行来宾代码
2024-3-31 13:22 2748

Hypervisor From Scratch – 第 5 部分:设置 VMCS 和运行来宾代码

2024-3-31 13:22
2748

目录

五、设置VMCS和运行guest代码

5.1.介绍

您好,欢迎来到“ Hypervisor From Scratch ”教程系列的第五部分。今天我们将花时间研究虚拟机控制结构(VMCS)的不同部分,实现额外的VMX指令,创建恢复点,设置不同的VMCS控制结构,最后,我们执行VMLAUNCH,进入硬件虚拟化世界!

5.2.概述

本主题的大部分内容源自英特尔 64 和 IA-32 架构软件开发人员手册 (英特尔 SDM) 合并卷 3 中的第 24 章 –(虚拟机控制结构)和第 26 章–(虚拟机条目)。

这部分的灵感来自于《Hypervisor For Beginner》

在阅读本部分的其余部分之前,请务必阅读前面的部分,因为它为您提供了彻底理解本主题的其余部分所需的知识。

5.3.VMX指令

第3部分 中,我们实现了 VMXOFF 函数,现在让我们实现其他VMX指令函数。 我还在调用 VMXONVMPTRLD 函数方面进行了一些更改,以使其更加模块化。

5.3.1.VMPTRST

VMPTRST 指令将当前VMCS指针存储到指定的内存地址中。 该指令的操作数始终是 64 位,并且始终是内存中的一个位置。

以下函数是VMPTRST的实现 :

1
2
3
4
5
6
7
8
9
10
11
UINT64
VmptrstInstruction()
{
    PHYSICAL_ADDRESS vmcspa;
    vmcspa.QuadPart = 0;
    __vmx_vmptrst((unsigned __int64 *)&vmcspa);
 
    DbgPrint("[*] VMPTRST %llx\n", vmcspa);
 
    return 0;
}

5.3.2.VMCLEAR

该指令适用于 VMCS,其中 VMCS 区域驻留在指令操作数中包含的物理地址。 该指令确保该 VMCS 的 VMCS 数据(其中一些数据当前可能保存在处理器上)被复制到内存中的 VMCS 区域。 它还会初始化 VMCS 区域的某些部分(例如,它将该 VMCS 的启动状态设置为清除)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BOOLEAN
ClearVmcsState(VIRTUAL_MACHINE_STATE * GuestState)
{
    // Clear the state of the VMCS to inactive
    int status = __vmx_vmclear(&GuestState->VmcsRegion);
 
    DbgPrint("[*] VMCS VMCLAEAR Status is : %d\n", status);
    if (status)
    {
        // Otherwise, terminate the VMX
        DbgPrint("[*] VMCS failed to clear with status %d\n", status);
        __vmx_off();
        return FALSE;
    }
    return TRUE;
}

5.3.3.VMPTRLD

该指令将当前 VMCS 指针标记为有效,并使用指令操作数中的物理地址加载它。 如果其操作数未正确对齐、设置不支持的物理地址位或等于 VMXON 指针,则指令将失败。 此外,如果操作数引用的内存中的 32 位与处理器支持的 VMCS 修订标识符不匹配,则该指令会失败。

1
2
3
4
5
6
7
8
9
10
11
BOOLEAN
LoadVmcs(VIRTUAL_MACHINE_STATE * GuestState)
{
    int status = __vmx_vmptrld(&GuestState->VmcsRegion);
    if (status)
    {
        DbgPrint("[*] VMCS failed with status %d\n", status);
        return FALSE;
    }
    return TRUE;
}

为了实现 VMRESUME ,您需要了解一些 VMCS 字段,因此 VMRESUME 指令的解释等我们在实现VMLAUNCH 后。 (本主题稍后)

5.4.增强VM状态结构

正如我之前告诉你的,我们需要一个结构来分别保存每个核心中虚拟机的状态。 我们最新版本的虚拟机管理程序中使用了以下结构。 我们将在本主题的其余部分中描述每个字段。

1
2
3
4
5
6
7
8
9
10
typedef struct _VIRTUAL_MACHINE_STATE
{
    UINT64 VmxoRegion;        // VMXON region
    UINT64 VmcsRegion;        // VMCS region
    UINT64 Eptp;              // Extended-Page-Table Pointer
    UINT64 VmmStack;          // Stack for VMM in VM-Exit State
    UINT64 MsrBitmap;         // MSR Bitmap Virtual Address
    UINT64 MsrBitmapPhysical; // MSR Bitmap Physical Address
 
} VIRTUAL_MACHINE_STATE, *PVIRTUAL_MACHINE_STATE;

请注意,这不是最终的 VIRTUAL_MACHINE_STATE 结构; 我们将来会加强它。

5.5.准备启动VM

在这一部分中,我们只是尝试增强我们的虚拟机管理程序驱动程序。 在以后的部分中,我们将添加一些与驱动程序的用户模式交互,但现在,让我们从修改 DriverEntry 开始,因为它是加载驱动程序时执行的第一个函数。

除了第 2 部分 的所有准备工作之外,我们还添加了以下几行来使用 第 4 部分 (EPT) 结构:

1
2
3
4
5
6
//
// Initiating EPTP and VMX
//
PEPTP EPTP = InitializeEptp();
 
InitiateVmx();

我们还添加了一个名为“ g_VirtualGuestMemoryAddress ”的全局变量的导出,该变量保存guest代码开始的地址。

现在让我们用 \xf4 指令的十六进制表示 填充分配的页面,它是HLT 。 我选择 HLT 是 因为,通过一些特殊配置(如下所述),它将导致 VM-exit并将代码返回到主机处理程序; 因此,这将是这部分的一个很好的例子。

之后,我们开始创建一个名为LaunchVm的函数 ,它负责在特定核心上运行我们的虚拟机。 在这一部分中,我们将仅在 第 0 个 逻辑处理器中测试我们的虚拟机管理程序。 在未来的部分中,我们将扩展我们的虚拟机管理程序以虚拟化整个系统。

请记住,每个逻辑核心都有自己的 VMCS,如果我们希望guest代码在其他逻辑处理器中运行,我们应该单独配置每个逻辑处理器。

要在某个逻辑核心中运行代码,我们应该使用 Windows KeSetSystemAffinityThread函数设置亲和性,并选择特定核心的VIRTUAL_MACHINE_STATE,因为每个核心都有自己独立的 VMXON 和 VMCS 区域。

以下代码描述了如何在不同的逻辑核心中运行我们的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
VOID
LaunchVm(int ProcessorID, PEPTP EPTP)
{
    DbgPrint("\n======================== Launching VM =============================\n");
 
    KAFFINITY AffinityMask;
    AffinityMask = MathPower(2, ProcessorID);
    KeSetSystemAffinityThread(AffinityMask);
 
    DbgPrint("[*]\t\tCurrent thread is executing in %d th logical processor.\n", ProcessorID);
 
    PAGED_CODE();
 
...

现在我们可以指定核心编号并在目标核心中执行代码,现在应该分配一个特定的 堆栈 ,以便每当发生 VM-exit时,我们可以保存寄存器并在 vmx-root 模式下调用其他主机函数。

快速提醒一下,每当发生 vm-exit 时,就会在 vmx-root 模式下调用主机处理程序。 当我们运行 VMRESUME 指令时,处理器切换到VMX non-root; 因此,每个内核模式驱动程序和用户模式应用程序都在 VMX non-root 模式下运行。 只有驱动程序中负责处理主机的部分在 VMX root-mode下执行。

这里我们需要一个用于主机例程的堆栈。 我们有两个选择,第一个选项是使用当前的 RSP ,第二个选项是使用单独的堆栈。 我们为堆栈使用了单独的位置,而不是使用驱动程序的当前 RSP ,但您也可以使用当前堆栈 (RSP)。

以下几行是为了分配和清零 VM-exit处理程序的堆栈而编写的。

1
2
3
4
5
6
7
8
9
10
11
12
//
// Allocate stack for the VM Exit Handler
//
UINT64 VMM_STACK_VA                = ExAllocatePoolWithTag(NonPagedPool, VMM_STACK_SIZE, POOLTAG);
g_GuestState[ProcessorID].VmmStack = VMM_STACK_VA;
 
if (g_GuestState[ProcessorID].VmmStack == NULL)
{
    DbgPrint("[*] Error in allocating VMM Stack.\n");
    return;
}
RtlZeroMemory(g_GuestState[ProcessorID].VmmStack, VMM_STACK_SIZE);

与上面相同,我们将为 MSR Bitmap 分配一个页面并将其添加到 GuestState 。 我稍后将在本主题中描述它们。

1
2
3
4
5
6
7
8
9
10
11
//
// Allocate memory for MSRBitMap
//
g_GuestState[ProcessorID].MsrBitmap = MmAllocateNonCachedMemory(PAGE_SIZE); // should be aligned
if (g_GuestState[ProcessorID].MsrBitmap == NULL)
{
    DbgPrint("[*] Error in allocating MSRBitMap.\n");
    return;
}
RtlZeroMemory(g_GuestState[ProcessorID].MsrBitmap, PAGE_SIZE);
g_GuestState[ProcessorID].MsrBitmapPhysical = VirtualToPhysicalAddress(g_GuestState[ProcessorID].MsrBitmap);

下一步是清除 VMCS 状态并将其加载为特定处理器(在我们的示例中为第 0 个逻辑处理器)中的当前 VMCS。

ClearVmcsStateLoadVmcs函数的使用如上所述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//
// Clear the VMCS State
//
if (!ClearVmcsState(&g_GuestState[ProcessorID]))
{
    goto ErrorReturn;
}
 
//
// Load VMCS (Set the Current VMCS)
//
if (!LoadVmcs(&g_GuestState[ProcessorID]))
{
    goto ErrorReturn;
}

现在是时候设置 VMCS 了。 我们将在本主题后面详细讨论如何配置 VMCS,但现在假设有一个名为 SetupVmcs的函数,它配置 VMCS 结构。

1
2
DbgPrint("[*] Setting up VMCS.\n");
SetupVmcs(&g_GuestState[ProcessorID], EPTP);

最后一步是执行 VMLAUNCH 指令。 然而我们不应该忘记保存堆栈的当前状态( RSPRBP 寄存器)。 这是因为执行 VMLAUNCH 指令后, RIP 寄存器更改为 GUEST_RIP ; 因此,我们需要保存之前的系统状态,以便在从VM函数返回后可以返回到正常的系统例程。 如果我们给驱动程序留下错误的 RSPRBP 寄存器,我们将看到 BSOD。 为此目的, 使用AsmSaveStateForVmxoff函数。

5.6.保存返回点

为了 AsmSaveStateForVmxoff,我们声明两个名为 g_StackPointerForReturningg_BasePointerForReturning 的全局变量。 无需保存 RIP 寄存器,因为堆栈的返回地址始终可用。 只需在汇编文件中 EXTERN 即可:

1
2
EXTERN g_StackPointerForReturning:QWORD
EXTERN g_BasePointerForReturning:QWORD

AsmSaveStateForVmxoff的实现:

1
2
3
4
5
6
7
8
AsmSaveStateForVmxoff PROC PUBLIC
 
    MOV g_StackPointerForReturning, RSP
    MOV g_BasePointerForReturning, RBP
 
    RET
 
AsmSaveStateForVmxoff ENDP

5.7.返回到之前的状态

我们的虚拟机管理程序的最后一步是返回到之前的系统状态并关闭虚拟机管理程序。

我们之前保存了系统状态。 现在,我们可以恢复它( RSPRBP 寄存器)并清除堆栈位置。

在此之前, 执行VMXOFF 指令来关闭管理程序。

看看下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
AsmVmxoffAndRestoreState PROC PUBLIC
 
    VMXOFF  ; turn it off before existing
     
    MOV RSP, g_StackPointerForReturning
    MOV RBP, g_BasePointerForReturning
     
    ; make rsp point to a correct return point
    ADD RSP, 8
     
    ; return True
 
    XOR RAX, RAX
    MOV RAX, 1
     
    ; return section
     
    MOV     RBX, [RSP+28h+8h]
    MOV     RSI, [RSP+28h+10h]
    ADD     RSP, 020h
    POP     RDI
     
    RET
     
AsmVmxoffAndRestoreState ENDP

最后,我们需要精确地清除堆栈。 之前我们称之为 LaunchVm函数并最终形成一个新的 RIP 。 为了正常继续执行,我们需要清除堆栈并返回到调用 LaunchVm函数的位置。 因此,在上面汇编代码的最后一部分,也就是“返回部分”,我用IDA Pro来看了反汇编 LaunchVm,所以我们可以看到这个函数如何清除堆栈,并且我们执行相同的操作,以便我们可以优雅地返回之前的系统状态。 因此,“返回部分”是从LaunchVmIDA Pro 的反汇编中复制的。
图片描述

5.8.VMLAUNCH指令

是时候谈谈 VMLAUNCH 指令了。

看看下面的代码。

1
2
3
4
5
6
7
8
9
10
__vmx_vmlaunch();
 
//
// if VMLAUNCH succeeds will never be here!
//
ULONG64 ErrorCode = 0;
__vmx_vmread(VM_INSTRUCTION_ERROR, &ErrorCode);
__vmx_off();
DbgPrint("[*] VMLAUNCH Error : 0x%llx\n", ErrorCode);
DbgBreakPoint();

__vmx_vmlaunch()是VMLAUNCH 指令的内在函数,而 __vmx_vmreadVMREAD 指令的内在函数。

正如注释所描述的,如果 VMLAUNCH 成功,我们将永远不会执行其他行。 如果VMCS的状态出现错误(这是一个常见问题),我们必须运行 VMREAD 并从 VMCS 的 VM_INSTRUCTION_ERROR 字段读取错误代码。 出现错误时还需要运行 VMXOFF 来关闭虚拟机管理程序,最后我们可以打印错误代码。

DbgBreakPoint只是一个调试断点(int 3),只有当我们使用远程内核 WinDbg 调试器时它才有用。很明显,你无法在本地调试系统中测试它,因为只要没有调试器捕获它,在内核中执行int 3就会冻结你的系统,因此强烈建议创建一个远程内核调试机器并测试您的代码可能存在的错误。

您还可以使用VMware Workstation的嵌套虚拟化来创建远程内核调试连接。 Intel没有“嵌套虚拟化”这样的东西,但提供了一些硬件设施,以便厂商可以自己支持和实现嵌套虚拟化。 例如,您可以在具有嵌套虚拟化支持的 VMware Workstation 上测试您的驱动程序(我还在第一部分中解释了如何在 VMware 上调试虚拟机管理程序驱动程序)。但是,支持 Hyper-V 嵌套虚拟化在实现虚拟机管理程序时需要考虑额外的事情,因此我们无法在 Hyper-V 嵌套虚拟化上测试我们的驱动程序,至少对于这一部分来说是这样。 我将在第 8 部分中解释 Hyper-V 支持。

这些驱动程序在物理机和 VMware Workstation 的嵌套虚拟化上进行了测试。

现在是时候在深入研究 VMCS 的配置之前阅读一些理论了。

5.9.VM Controls

让我们讨论一下 VMCS 中管理guest行为的不同控件。 我们将在本部分中使用其中一些位,并且将在以后的部分中使用其中一些位。 所以,不用担心。 只需看一下这些位的描述并了解它们即可

5.9.1.VM-Execution Controls

为了控制我们的guest功能,我们必须在 VMCS 中设置一些字段。 下表表示Primary Processor-Based VM-Execution Controls 和the Secondary Processor-Based VM-Execution Controls。

图片描述
我们这样定义上表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define CPU_BASED_VIRTUAL_INTR_PENDING        0x00000004
#define CPU_BASED_USE_TSC_OFFSETING           0x00000008
#define CPU_BASED_HLT_EXITING                 0x00000080
#define CPU_BASED_INVLPG_EXITING              0x00000200
#define CPU_BASED_MWAIT_EXITING               0x00000400
#define CPU_BASED_RDPMC_EXITING               0x00000800
#define CPU_BASED_RDTSC_EXITING               0x00001000
#define CPU_BASED_CR3_LOAD_EXITING            0x00008000
#define CPU_BASED_CR3_STORE_EXITING           0x00010000
#define CPU_BASED_CR8_LOAD_EXITING            0x00080000
#define CPU_BASED_CR8_STORE_EXITING           0x00100000
#define CPU_BASED_TPR_SHADOW                  0x00200000
#define CPU_BASED_VIRTUAL_NMI_PENDING         0x00400000
#define CPU_BASED_MOV_DR_EXITING              0x00800000
#define CPU_BASED_UNCOND_IO_EXITING           0x01000000
#define CPU_BASED_ACTIVATE_IO_BITMAP          0x02000000
#define CPU_BASED_MONITOR_TRAP_FLAG           0x08000000
#define CPU_BASED_ACTIVATE_MSR_BITMAP         0x10000000
#define CPU_BASED_MONITOR_EXITING             0x20000000
#define CPU_BASED_PAUSE_EXITING               0x40000000
#define CPU_BASED_ACTIVATE_SECONDARY_CONTROLS 0x80000000

在 VMX 的早期版本中,没有类似Secondary Processor-Based VM-Execution Controls的功能。 现在,如果我们想使用辅助表,我们必须设置第一个表的第31位; 否则,它就像带有零的辅助表字段。
图片描述
上表的定义是这样的(我们忽略一些位,如果你想在虚拟机管理程序中使用它们,你可以定义它们):

1
2
3
4
5
#define CPU_BASED_CTL2_ENABLE_EPT            0x2
#define CPU_BASED_CTL2_RDTSCP                0x8
#define CPU_BASED_CTL2_ENABLE_VPID            0x20
#define CPU_BASED_CTL2_UNRESTRICTED_GUEST    0x80
#define CPU_BASED_CTL2_ENABLE_VMFUNC        0x2000

5.9.2.VM-entry Control Bits

VM-entry controls构成一个 32 位向量,用于管理 VM entries的基本操作

图片描述

1
2
3
4
5
// VM-entry Control Bits
#define VM_ENTRY_IA32E_MODE             0x00000200
#define VM_ENTRY_SMM                    0x00000400
#define VM_ENTRY_DEACT_DUAL_MONITOR     0x00000800
#define VM_ENTRY_LOAD_GUEST_PAT         0x00004000

5.9.3.VM-exit Control Bits

VM-exit controls构成一个 32 位向量,用于管理 VM-exit的基本操作。
图片描述

1
2
3
4
5
// VM-exit Control Bits
#define VM_EXIT_IA32E_MODE              0x00000200
#define VM_EXIT_ACK_INTR_ON_EXIT        0x00008000
#define VM_EXIT_SAVE_GUEST_PAT          0x00040000
#define VM_EXIT_LOAD_HOST_PAT           0x00080000

5.9.4.PIN-Based Execution Control

pin-based VM-execution controls构成一个 32 位向量,用于管理异步事件(例如中断)的处理。 我们将在以后的部分中使用它,但现在,让我们在虚拟机管理程序中定义它。
图片描述

1
2
3
4
5
#define PIN_BASED_VM_EXECUTION_CONTROLS_EXTERNAL_INTERRUPT        0x00000001
#define PIN_BASED_VM_EXECUTION_CONTROLS_NMI_EXITING               0x00000008
#define PIN_BASED_VM_EXECUTION_CONTROLS_VIRTUAL_NMI               0x00000020
#define PIN_BASED_VM_EXECUTION_CONTROLS_ACTIVE_VMX_TIMER          0x00000040
#define PIN_BASED_VM_EXECUTION_CONTROLS_PROCESS_POSTED_INTERRUPTS 0x00000080

5.10.配置VMCS

现在我们已经了解了一些 VMCS 字段和控件的基本概念,是时候完全配置 VMCS 结构以使我们的虚拟化guest做好准备了。

5.10.1.收集VMCS的计算机状态

为了配置我们的Guest-StateHost-State,我们需要了解当前系统状态的详细信息,例如全局描述符表地址(GDT)、中断描述符表(IDT)地址并读取所有段寄存器。.

这些函数描述了如何收集所有这些寄存器和段。
GDT Base:

1
2
3
4
5
6
7
8
9
GetGdtBase PROC
 
    LOCAL   GDTR[10]:BYTE
    SGDT    GDTR
    MOV     RAX, QWORD PTR GDTR[2]
 
    RET
 
GetGdtBase ENDP

CS段寄存器:

1
2
3
4
5
6
GetCs PROC
 
    MOV     RAX, CS
    RET
 
GetCs ENDP

DS段寄存器:

1
2
3
4
5
6
GetDs PROC
 
    MOV     RAX, DS
    RET
 
GetDs ENDP

ES段寄存器:

1
2
3
4
5
6
GetEs PROC
 
    MOV     RAX, ES
    RET
 
GetEs ENDP

SS段寄存器:

1
2
3
4
5
6
GetSs PROC
 
    MOV     RAX, SS
    RET
 
GetSs ENDP

FS段寄存器:

1
2
3
4
5
6
GetFs PROC
 
    MOV     RAX, FS
    RET
 
GetFs ENDP

GS段寄存器:

1
2
3
4
5
6
GetGs PROC
 
    MOV     RAX, GS
    RET
 
GetGs ENDP

LDT:

1
2
3
4
5
6
GetLdtr PROC
 
    SLDT    RAX
    RET
 
GetLdtr ENDP

TR(任务寄存器):

1
2
3
4
5
6
GetTr PROC
 
    STR     RAX
    RET
 
GetTr ENDP

中断描述符表:

1
2
3
4
5
6
7
8
9
GetIdtBase PROC
 
    LOCAL   IDTR[10]:BYTE
     
    SIDT    IDTR
    MOV     RAX, QWORD PTR IDTR[2]
    RET
 
GetIdtBase ENDP

GDT Limit:

1
2
3
4
5
6
7
8
9
10
GetGdtLimit PROC
 
    LOCAL   GDTR[10]:BYTE
 
    SGDT    GDTR
    MOV     AX, WORD PTR GDTR[0]
 
    RET
 
GetGdtLimit ENDP

IDT Limit:

1
2
3
4
5
6
7
8
9
10
GetIdtLimit PROC
 
    LOCAL   IDTR[10]:BYTE
     
    SIDT    IDTR
    MOV     AX, WORD PTR IDTR[0]
 
    RET
 
GetIdtLimit ENDP

RFLAGS:

1
2
3
4
5
6
7
GetRflags PROC
 
    PUSHFQ
    POP     RAX
    RET
 
GetRflags ENDP

5.10.2.设置VMCS

让我们言归正传吧(我们还有很长的路要走)。

本节首先定义一个名为 SetupVmcs的函数.

1
2
BOOLEAN
SetupVmcs(VIRTUAL_MACHINE_STATE * GuestState, PEPTP EPTP);

该函数负责配置与 VMCS 相关的所有选项,当然还有 Guest & Host 状态。

配置和修改 VMCS 是通过使用名为“ VMWRITE”的特殊指令来完成的

VMWRITE 将主源操作数(寄存器或内存)的内容写入 VMCS 中的指定字段。 在VMX-root 操作中,指令写入当前VMCS。 如果在VMX non-root 操作中执行,则该指令写入由当前VMCS中的VMCS链接指针字段引用的VMCS。

VMCS 字段由寄存器辅助源操作数中包含的 VMCS-field encoding指定。

以下枚举包含VMWRITE和VMREAD指令所需的大部分 VMCS 字段。 (较新的处理器添加了较新的字段。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
enum VMCS_FIELDS {
    GUEST_ES_SELECTOR = 0x00000800,
    GUEST_CS_SELECTOR = 0x00000802,
    GUEST_SS_SELECTOR = 0x00000804,
    GUEST_DS_SELECTOR = 0x00000806,
    GUEST_FS_SELECTOR = 0x00000808,
    GUEST_GS_SELECTOR = 0x0000080a,
    GUEST_LDTR_SELECTOR = 0x0000080c,
    GUEST_TR_SELECTOR = 0x0000080e,
    HOST_ES_SELECTOR = 0x00000c00,
    HOST_CS_SELECTOR = 0x00000c02,
    HOST_SS_SELECTOR = 0x00000c04,
    HOST_DS_SELECTOR = 0x00000c06,
    HOST_FS_SELECTOR = 0x00000c08,
    HOST_GS_SELECTOR = 0x00000c0a,
    HOST_TR_SELECTOR = 0x00000c0c,
    IO_BITMAP_A = 0x00002000,
    IO_BITMAP_A_HIGH = 0x00002001,
    IO_BITMAP_B = 0x00002002,
    IO_BITMAP_B_HIGH = 0x00002003,
    MSR_BITMAP = 0x00002004,
    MSR_BITMAP_HIGH = 0x00002005,
    VM_EXIT_MSR_STORE_ADDR = 0x00002006,
    VM_EXIT_MSR_STORE_ADDR_HIGH = 0x00002007,
    VM_EXIT_MSR_LOAD_ADDR = 0x00002008,
    VM_EXIT_MSR_LOAD_ADDR_HIGH = 0x00002009,
    VM_ENTRY_MSR_LOAD_ADDR = 0x0000200a,
    VM_ENTRY_MSR_LOAD_ADDR_HIGH = 0x0000200b,
    TSC_OFFSET = 0x00002010,
    TSC_OFFSET_HIGH = 0x00002011,
    VIRTUAL_APIC_PAGE_ADDR = 0x00002012,
    VIRTUAL_APIC_PAGE_ADDR_HIGH = 0x00002013,
    VMFUNC_CONTROLS = 0x00002018,
    VMFUNC_CONTROLS_HIGH = 0x00002019,
    EPT_POINTER = 0x0000201A,
    EPT_POINTER_HIGH = 0x0000201B,
    EPTP_LIST = 0x00002024,
    EPTP_LIST_HIGH = 0x00002025,
    GUEST_PHYSICAL_ADDRESS = 0x2400,
    GUEST_PHYSICAL_ADDRESS_HIGH = 0x2401,
    VMCS_LINK_POINTER = 0x00002800,
    VMCS_LINK_POINTER_HIGH = 0x00002801,
    GUEST_IA32_DEBUGCTL = 0x00002802,
    GUEST_IA32_DEBUGCTL_HIGH = 0x00002803,
    PIN_BASED_VM_EXEC_CONTROL = 0x00004000,
    CPU_BASED_VM_EXEC_CONTROL = 0x00004002,
    EXCEPTION_BITMAP = 0x00004004,
    PAGE_FAULT_ERROR_CODE_MASK = 0x00004006,
    PAGE_FAULT_ERROR_CODE_MATCH = 0x00004008,
    CR3_TARGET_COUNT = 0x0000400a,
    VM_EXIT_CONTROLS = 0x0000400c,
    VM_EXIT_MSR_STORE_COUNT = 0x0000400e,
    VM_EXIT_MSR_LOAD_COUNT = 0x00004010,
    VM_ENTRY_CONTROLS = 0x00004012,
    VM_ENTRY_MSR_LOAD_COUNT = 0x00004014,
    VM_ENTRY_INTR_INFO_FIELD = 0x00004016,
    VM_ENTRY_EXCEPTION_ERROR_CODE = 0x00004018,
    VM_ENTRY_INSTRUCTION_LEN = 0x0000401a,
    TPR_THRESHOLD = 0x0000401c,
    SECONDARY_VM_EXEC_CONTROL = 0x0000401e,
    VM_INSTRUCTION_ERROR = 0x00004400,
    VM_EXIT_REASON = 0x00004402,
    VM_EXIT_INTR_INFO = 0x00004404,
    VM_EXIT_INTR_ERROR_CODE = 0x00004406,
    IDT_VECTORING_INFO_FIELD = 0x00004408,
    IDT_VECTORING_ERROR_CODE = 0x0000440a,
    VM_EXIT_INSTRUCTION_LEN = 0x0000440c,
    VMX_INSTRUCTION_INFO = 0x0000440e,
    GUEST_ES_LIMIT = 0x00004800,
    GUEST_CS_LIMIT = 0x00004802,
    GUEST_SS_LIMIT = 0x00004804,
    GUEST_DS_LIMIT = 0x00004806,
    GUEST_FS_LIMIT = 0x00004808,
    GUEST_GS_LIMIT = 0x0000480a,
    GUEST_LDTR_LIMIT = 0x0000480c,
    GUEST_TR_LIMIT = 0x0000480e,
    GUEST_GDTR_LIMIT = 0x00004810,
    GUEST_IDTR_LIMIT = 0x00004812,
    GUEST_ES_AR_BYTES = 0x00004814,
    GUEST_CS_AR_BYTES = 0x00004816,
    GUEST_SS_AR_BYTES = 0x00004818,
    GUEST_DS_AR_BYTES = 0x0000481a,
    GUEST_FS_AR_BYTES = 0x0000481c,
    GUEST_GS_AR_BYTES = 0x0000481e,
    GUEST_LDTR_AR_BYTES = 0x00004820,
    GUEST_TR_AR_BYTES = 0x00004822,
    GUEST_INTERRUPTIBILITY_INFO = 0x00004824,
    GUEST_ACTIVITY_STATE = 0x00004826,
    GUEST_SM_BASE = 0x00004828,
    GUEST_SYSENTER_CS = 0x0000482A,
    HOST_IA32_SYSENTER_CS = 0x00004c00,
    CR0_GUEST_HOST_MASK = 0x00006000,
    CR4_GUEST_HOST_MASK = 0x00006002,
    CR0_READ_SHADOW = 0x00006004,
    CR4_READ_SHADOW = 0x00006006,
    CR3_TARGET_VALUE0 = 0x00006008,
    CR3_TARGET_VALUE1 = 0x0000600a,
    CR3_TARGET_VALUE2 = 0x0000600c,
    CR3_TARGET_VALUE3 = 0x0000600e,
    EXIT_QUALIFICATION = 0x00006400,
    GUEST_LINEAR_ADDRESS = 0x0000640a,
    GUEST_CR0 = 0x00006800,
    GUEST_CR3 = 0x00006802,
    GUEST_CR4 = 0x00006804,
    GUEST_ES_BASE = 0x00006806,
    GUEST_CS_BASE = 0x00006808,
    GUEST_SS_BASE = 0x0000680a,
    GUEST_DS_BASE = 0x0000680c,
    GUEST_FS_BASE = 0x0000680e,
    GUEST_GS_BASE = 0x00006810,
    GUEST_LDTR_BASE = 0x00006812,
    GUEST_TR_BASE = 0x00006814,
    GUEST_GDTR_BASE = 0x00006816,
    GUEST_IDTR_BASE = 0x00006818,
    GUEST_DR7 = 0x0000681a,
    GUEST_RSP = 0x0000681c,
    GUEST_RIP = 0x0000681e,
    GUEST_RFLAGS = 0x00006820,
    GUEST_PENDING_DBG_EXCEPTIONS = 0x00006822,
    GUEST_SYSENTER_ESP = 0x00006824,
    GUEST_SYSENTER_EIP = 0x00006826,
    HOST_CR0 = 0x00006c00,
    HOST_CR3 = 0x00006c02,
    HOST_CR4 = 0x00006c04,
    HOST_FS_BASE = 0x00006c06,
    HOST_GS_BASE = 0x00006c08,
    HOST_TR_BASE = 0x00006c0a,
    HOST_GDTR_BASE = 0x00006c0c,
    HOST_IDTR_BASE = 0x00006c0e,
    HOST_IA32_SYSENTER_ESP = 0x00006c10,
    HOST_IA32_SYSENTER_EIP = 0x00006c12,
    HOST_RSP = 0x00006c14,
    HOST_RIP = 0x00006c16,
};

好的,让我们继续我们的配置。

下一步是配置 host段寄存器。

1
2
3
4
5
6
7
__vmx_vmwrite(HOST_ES_SELECTOR, GetEs() & 0xF8);
__vmx_vmwrite(HOST_CS_SELECTOR, GetCs() & 0xF8);
__vmx_vmwrite(HOST_SS_SELECTOR, GetSs() & 0xF8);
__vmx_vmwrite(HOST_DS_SELECTOR, GetDs() & 0xF8);
__vmx_vmwrite(HOST_FS_SELECTOR, GetFs() & 0xF8);
__vmx_vmwrite(HOST_GS_SELECTOR, GetGs() & 0xF8);
__vmx_vmwrite(HOST_TR_SELECTOR, GetTr() & 0xF8);

请记住,以“ HOST_ ”开头的字段与虚拟机管理程序在发生 VM-exit时设置的状态相关,而以“GUEST_”开头的与执行 VMLAUNCH 时虚拟机管理程序为 guest 设置的状态相关

& 0xF8的目的是Intel提到必须清除三个较低有效位; 否则,会导致错误,因为 VMLAUNCH 执行 时会出现无效主机状态 错误。

接下来,我们设置 VMCS_LINK_POINTER,应该是“0xffffffffffffffff”。 因为我们没有额外的 VMCS。 该字段主要用于想要实现嵌套虚拟化行为的虚拟机管理程序(例如 VMware 嵌套虚拟化或 KVM 的 nVMX)。

1
2
3
4
//
// Setting the link pointer to the required value for 4KB VMCS
//
__vmx_vmwrite(VMCS_LINK_POINTER, ~0ULL);

本主题的其余部分旨在虚拟化计算机的当前状态,因此guest和主机配置必须相同。

让我们配置GUEST_IA32_DEBUGCTL。该字段的工作方式与物理机中的IA32_DEBUGCTL MSR相同,如果我们想为每个客户机使用单独的IA32_DEBUGCTL ,我们可以使用它。它提供位字段控制来启用调试跟踪中断、调试跟踪存储、跟踪消息启用、分支上的单步执行、最后分支记录记录以及 LBR 堆栈的控制冻结。

我们不在虚拟机管理程序中使用它,但我们应该将其配置为当前计算机的 MSR_IA32_DEBUGCTL 。 我们用 __readmsr()读取此 MSR (RDMSR) 并将物理机的值放入guest的内部 GUEST_IA32_DEBUGCTL.

1
2
__vmx_vmwrite(GUEST_IA32_DEBUGCTL, __readmsr(MSR_IA32_DEBUGCTL) & 0xFFFFFFFF);
__vmx_vmwrite(GUEST_IA32_DEBUGCTL_HIGH, __readmsr(MSR_IA32_DEBUGCTL) >> 32);

请注意,我们在其上置零的值可以被忽略; 如果你不修改它们,就好像你在它们上加了零。

例如,在当前状态下,配置 TSC 对于我们的虚拟机管理程序并不重要,因此我们将其设置为 0。

1
2
3
4
5
6
7
8
9
10
11
12
/* Time-stamp counter offset */
__vmx_vmwrite(TSC_OFFSET, 0);
__vmx_vmwrite(TSC_OFFSET_HIGH, 0);
 
__vmx_vmwrite(PAGE_FAULT_ERROR_CODE_MASK, 0);
__vmx_vmwrite(PAGE_FAULT_ERROR_CODE_MATCH, 0);
 
__vmx_vmwrite(VM_EXIT_MSR_STORE_COUNT, 0);
__vmx_vmwrite(VM_EXIT_MSR_LOAD_COUNT, 0);
 
__vmx_vmwrite(VM_ENTRY_MSR_LOAD_COUNT, 0);
__vmx_vmwrite(VM_ENTRY_INTR_INFO_FIELD, 0);

这次,我们将根据主机的 GDT 基地址配置段寄存器(当 VM-exit时)。

1
2
3
4
5
6
7
8
9
10
GdtBase = GetGdtBase();
 
FillGuestSelectorData((PVOID)GdtBase, ES, GetEs());
FillGuestSelectorData((PVOID)GdtBase, CS, GetCs());
FillGuestSelectorData((PVOID)GdtBase, SS, GetSs());
FillGuestSelectorData((PVOID)GdtBase, DS, GetDs());
FillGuestSelectorData((PVOID)GdtBase, FS, GetFs());
FillGuestSelectorData((PVOID)GdtBase, GS, GetGs());
FillGuestSelectorData((PVOID)GdtBase, LDTR, GetLdtr());
FillGuestSelectorData((PVOID)GdtBase, TR, GetTr());

GetGdtBase上面在为我们的 VMCS 收集信息的过程中定义了。

FillGuestSelectorData负责设置 VMCS 的 GUEST 选择器、属性、限制和基础。 其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
VOID
FillGuestSelectorData(
    PVOID  GdtBase,
    ULONG  Segreg,
    USHORT Selector)
{
    SEGMENT_SELECTOR SegmentSelector = {0};
    ULONG            AccessRights;
 
    GetSegmentDescriptor(&SegmentSelector, Selector, GdtBase);
    AccessRights = ((PUCHAR)&SegmentSelector.ATTRIBUTES)[0] + (((PUCHAR)&SegmentSelector.ATTRIBUTES)[1] << 12);
 
    if (!Selector)
        AccessRights |= 0x10000;
 
    __vmx_vmwrite(GUEST_ES_SELECTOR + Segreg * 2, Selector);
    __vmx_vmwrite(GUEST_ES_LIMIT + Segreg * 2, SegmentSelector.LIMIT);
    __vmx_vmwrite(GUEST_ES_AR_BYTES + Segreg * 2, AccessRights);
    __vmx_vmwrite(GUEST_ES_BASE + Segreg * 2, SegmentSelector.BASE);
}

GetSegmentDescriptor 的函数体 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
BOOLEAN
GetSegmentDescriptor(PSEGMENT_SELECTOR SegmentSelector,
                     USHORT            Selector,
                     PUCHAR            GdtBase)
{
    PSEGMENT_DESCRIPTOR SegDesc;
 
    if (!SegmentSelector)
        return FALSE;
 
    if (Selector & 0x4)
    {
        return FALSE;
    }
 
    SegDesc = (PSEGMENT_DESCRIPTOR)((PUCHAR)GdtBase + (Selector & ~0x7));
 
    SegmentSelector->SEL               = Selector;
    SegmentSelector->BASE              = SegDesc->BASE0 | SegDesc->BASE1 << 16 | SegDesc->BASE2 << 24;
    SegmentSelector->LIMIT             = SegDesc->LIMIT0 | (SegDesc->LIMIT1ATTR1 & 0xf) << 16;
    SegmentSelector->ATTRIBUTES.UCHARs = SegDesc->ATTR0 | (SegDesc->LIMIT1ATTR1 & 0xf0) << 4;
 
    if (!(SegDesc->ATTR0 & 0x10))
    { // LA_ACCESSED
        ULONG64 Tmp;
        // this is a TSS or callgate etc, save the base high part
        Tmp                   = (*(PULONG64)((PUCHAR)SegDesc + 8));
        SegmentSelector->BASE = (SegmentSelector->BASE & 0xffffffff) | (Tmp << 32);
    }
 
    if (SegmentSelector->ATTRIBUTES.Fields.G)
    {
        // 4096-bit granularity is enabled for this segment, scale the limit
        SegmentSelector->LIMIT = (SegmentSelector->LIMIT << 12) + 0xfff;
    }
 
    return TRUE;
}

另一个名为 IA32_KERNEL_GS_BASE 的 MSR 用于设置内核 GS 基址。 每当执行像 SYSCALL 这样的指令,并且处理器进入环 0 时,我们需要更改当前的 GS 寄存器,这可以使用 SWAPGS 指令来完成。 该指令将 IA32_KERNEL_GS_BASE 的内容复制到 IA32_GS_BASE 中,现在当内核想要重新进入用户模式时使用它。

另一方面,MSR_FS_BASE 没有内核基础,因为它在 32 位模式下使用,而我们有 64 位(长模式)内核。

与上面的MSR一样,我们将根据当前系统的MSR配置IA32_GS_BASE和IA32_FS_BASE MSR。

1
2
__vmx_vmwrite(GUEST_FS_BASE, __readmsr(MSR_FS_BASE));
__vmx_vmwrite(GUEST_GS_BASE, __readmsr(MSR_GS_BASE));

GUEST_INTERRUPTIBILITY_INFO 和 GUEST_ACTIVITY_STATE 设置为零(我们将在以后的部分中描述它们)。

1
2
__vmx_vmwrite(GUEST_INTERRUPTIBILITY_INFO, 0);
__vmx_vmwrite(GUEST_ACTIVITY_STATE, 0);   //Active state

现在我们到达了VMCS的一个重要部分,它是CPU_BASED_VM_EXEC_CONTROL和 SECONDARY_VM_EXEC_CONTROL控件的配置 。

这些字段启用和禁用客户机的一些基本功能,例如,我们可以将 VMCS 配置为在检测到(在客户机中)执行HLT指令时导致 VM-exit 。 您可以阅读本主题的虚拟机执行控制部分中每个位的描述 。

1
2
__vmx_vmwrite(CPU_BASED_VM_EXEC_CONTROL, AdjustControls(CPU_BASED_HLT_EXITING | 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_EPT*/, MSR_IA32_VMX_PROCBASED_CTLS2));

如您所见,我们设置了 CPU_BASED_HLT_EXITING,这将导致 HLT 上的 VM 退出,并使用 CPU_BASED_ACTIVATE_SECONDARY_CONTROLS 位激活辅助控制。

在辅助控件中,我们使用了CPU_BASED_CTL2_RDTSCP,现在注释CPU_BASED_CTL2_ENABLE_EPT,因为我们不需要在这部分处理EPT。 在第七部分中,我详细描述了EPT。

PIN_BASED_VM_EXEC_CONTROL、VM_EXIT_CONTROLS 和 VM_ENTRY_CONTROLS 的描述可在上面找到。 这部分我们没有对这些控件进行任何特殊的配置; 因此,让我们把它们归零。

1
2
3
__vmx_vmwrite(PIN_BASED_VM_EXEC_CONTROL, AdjustControls(0, MSR_IA32_VMX_PINBASED_CTLS));
__vmx_vmwrite(VM_EXIT_CONTROLS, AdjustControls(VM_EXIT_IA32E_MODE | VM_EXIT_ACK_INTR_ON_EXIT, MSR_IA32_VMX_EXIT_CTLS));
__vmx_vmwrite(VM_ENTRY_CONTROLS, AdjustControls(VM_ENTRY_IA32E_MODE, MSR_IA32_VMX_ENTRY_CTLS));

另外, AdjustControls是一个用于配置这些字段的 0 设置和 1 设置的函数(我们将在以后的部分中描述它们),但现在; 它的定义如下:

1
2
3
4
5
6
7
8
9
10
ULONG
AdjustControls(ULONG Ctl, ULONG Msr)
{
    MSR MsrValue = {0};
 
    MsrValue.Content = __readmsr(Msr);
    Ctl &= MsrValue.High; /* bit == 0 in high word ==> must be zero */
    Ctl |= MsrValue.Low;  /* bit == 1 in low word  ==> must be one  */
    return Ctl;
}

下一步是为guest和主机设置控制寄存器和调试寄存器 (DR7)。 我们使用内在函数将它们设置为与当前机器状态相同的值。

1
2
3
4
5
6
7
__vmx_vmwrite(GUEST_CR0, __readcr0());
__vmx_vmwrite(GUEST_CR3, __readcr3());
__vmx_vmwrite(GUEST_CR4, __readcr4());
 
__vmx_vmwrite(HOST_CR0, __readcr0());
__vmx_vmwrite(HOST_CR3, __readcr3());
__vmx_vmwrite(HOST_CR4, __readcr4());

下一部分是为我们的guest设置 IDT 和 GDT 的 BaseLimit 。 一般来说, 为guest和主机使用相同的 IDT(和 GDT)并不是一个好主意 ,但为了保持我们的虚拟机管理程序简单,我们将它们配置为相同的值。

1
2
3
4
__vmx_vmwrite(GUEST_GDTR_BASE, GetGdtBase());
__vmx_vmwrite(GUEST_IDTR_BASE, GetIdtBase());
__vmx_vmwrite(GUEST_GDTR_LIMIT, GetGdtLimit());
__vmx_vmwrite(GUEST_IDTR_LIMIT, GetIdtLimit());

接下来,设置 RFLAGS。

1
__vmx_vmwrite(GUEST_RFLAGS, GetRflags());

如果您想在guest中使用 SYSENTER,您应该配置以下 MSR。 在 x64 Windows 中设置这些值并不重要,因为 Windows 在 x64 版本的 Windows 中不支持 SYSENTER; 相反,它使用 SYSCALL。

相同的指令也适用于 32 位进程。 在32位进程中,Windows首先将执行模式更改为长模式(使用 天堂之门技术 ),然后执行SYSCALL指令。

1
2
3
4
5
6
__vmx_vmwrite(GUEST_SYSENTER_CS, __readmsr(MSR_IA32_SYSENTER_CS));
__vmx_vmwrite(GUEST_SYSENTER_EIP, __readmsr(MSR_IA32_SYSENTER_EIP));
__vmx_vmwrite(GUEST_SYSENTER_ESP, __readmsr(MSR_IA32_SYSENTER_ESP));
__vmx_vmwrite(HOST_IA32_SYSENTER_CS, __readmsr(MSR_IA32_SYSENTER_CS));
__vmx_vmwrite(HOST_IA32_SYSENTER_EIP, __readmsr(MSR_IA32_SYSENTER_EIP));
__vmx_vmwrite(HOST_IA32_SYSENTER_ESP, __readmsr(MSR_IA32_SYSENTER_ESP));

不要忘记为 VMCS 中的主机 配置HOST_FS_BASEHOST_GS_BASEHOST_GDTR_BASEHOST_IDTR_BASEHOST_TR_BASE 。

1
2
3
4
5
6
7
8
GetSegmentDescriptor(&SegmentSelector, GetTr(), (PUCHAR)GetGdtBase());
__vmx_vmwrite(HOST_TR_BASE, SegmentSelector.BASE);
 
__vmx_vmwrite(HOST_FS_BASE, __readmsr(MSR_FS_BASE));
__vmx_vmwrite(HOST_GS_BASE, __readmsr(MSR_GS_BASE));
 
__vmx_vmwrite(HOST_GDTR_BASE, GetGdtBase());
__vmx_vmwrite(HOST_IDTR_BASE, GetIdtBase());

下一个重要部分是在执行 VMLAUNCH 时设置 guest 虚拟机的 RIP 和 RSP 寄存器。 当虚拟机退出时,它从您在本部分中配置的 RIP 以及主机的 RIP 和 RSP 开始。 很明显,主机 RIP 应该指向一个负责根据 VM 退出代码管理 VMX 事件的函数,以及是否决定执行 VMRESUME 或使用 VMXOFF 关闭虚拟机管理程序。

1
2
3
4
5
__vmx_vmwrite(GUEST_RSP, (ULONG64)g_VirtualGuestMemoryAddress); // setup guest sp
__vmx_vmwrite(GUEST_RIP, (ULONG64)g_VirtualGuestMemoryAddress); // setup guest ip
 
__vmx_vmwrite(HOST_RSP, ((ULONG64)GuestState->VmmStack + VMM_STACK_SIZE - 1));
__vmx_vmwrite(HOST_RIP, (ULONG64)AsmVmexitHandler);

HOST_RSP指向我们之前分配的VmmStackHOST_RIP指向AsmVmexitHandler(下面描述的汇编编写的函数)。GUEST_RIP指向g_VirtualGuestMemoryAddress(我们在 EPT 初始化期间配置的全局变量),而GUEST_RSP指向同一地址 ( g_VirtualGuestMemoryAddress ),因为我们没有放置任何使用堆栈的指令,因此对于实际示例,它应该指向一个不同的可写地址。

完毕!我们的 VMCS 即将准备就绪。

5.10.3.检查VMCS布局

不幸的是,检查 VMCS 布局并不像其他部分那么直接。 我们必须控制英特尔 64 和 IA-32 架构软件开发人员手册中的[第 26 章]VM 条目中描述的所有检查表,包括以下部分:

  • 26.2 检查 VMX 控制和主机状态区域
  • 26.3 检查和加载guest状态
  • 26.4 加载 MSRS
  • 26.5 事件注入
  • 26.6 VM 入口的特殊功能
  • 26.7 加载guest状态期间或之后的 VM 进入失败
  • 26.8 VM 进入期间的机器检查事件

此过程中最困难的部分是当我们不知道 VMCS 布局的不正确部分时,或者另一方面,当您错过最终导致失败的某些内容时。

这是因为 Intel 只提供了一个错误号,而没有提供有关 VMCS 布局中具体错误的任何进一步详细信息。

错误如下所示。
图片描述
为了解决这个问题,我创建了一个名为VmcsAuditor的用户模式应用程序。正如它的名字所描述的,如果你有任何错误并且不知道如何解决问题,它可以是一个选择。

请记住, VmcsAuditor 是一个基于 Bochs 模拟器对 VMX 支持的工具,因此所有检查都来自 Bochs,并且它不是一个解决所有问题的 100% 可靠工具,因为我们不知道处理器内部到底发生了什么。 尽管如此,它仍然很方便并且节省时间。

源代码和可执行文件可在 GitHub 上获取:

[ https://github.com/SinaKarvandi/VMCS-Auditor ]

详细说明请参见 此处

作为更好的选择,您可以使用 Satoshi Tanda 的 **[代码 ](https://github.com/SinaKarvandi/Hypervisor-From-Scratch/tree/master/Part 5 - Setting up VMCS %26 Running Guest Code/VMCS-Checks)**来检查guest状态。

5.11.VM退出处理程序

当我们的guest软件退出并将句柄返回给主机时,可能会发生以下虚拟机退出原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#define EXIT_REASON_EXCEPTION_NMI       0
#define EXIT_REASON_EXTERNAL_INTERRUPT  1
#define EXIT_REASON_TRIPLE_FAULT        2
#define EXIT_REASON_INIT                3
#define EXIT_REASON_SIPI                4
#define EXIT_REASON_IO_SMI              5
#define EXIT_REASON_OTHER_SMI           6
#define EXIT_REASON_PENDING_VIRT_INTR   7
#define EXIT_REASON_PENDING_VIRT_NMI    8
#define EXIT_REASON_TASK_SWITCH         9
#define EXIT_REASON_CPUID               10
#define EXIT_REASON_GETSEC              11
#define EXIT_REASON_HLT                 12
#define EXIT_REASON_INVD                13
#define EXIT_REASON_INVLPG              14
#define EXIT_REASON_RDPMC               15
#define EXIT_REASON_RDTSC               16
#define EXIT_REASON_RSM                 17
#define EXIT_REASON_VMCALL              18
#define EXIT_REASON_VMCLEAR             19
#define EXIT_REASON_VMLAUNCH            20
#define EXIT_REASON_VMPTRLD             21
#define EXIT_REASON_VMPTRST             22
#define EXIT_REASON_VMREAD              23
#define EXIT_REASON_VMRESUME            24
#define EXIT_REASON_VMWRITE             25
#define EXIT_REASON_VMXOFF              26
#define EXIT_REASON_VMXON               27
#define EXIT_REASON_CR_ACCESS           28
#define EXIT_REASON_DR_ACCESS           29
#define EXIT_REASON_IO_INSTRUCTION      30
#define EXIT_REASON_MSR_READ            31
#define EXIT_REASON_MSR_WRITE           32
#define EXIT_REASON_INVALID_GUEST_STATE 33
#define EXIT_REASON_MSR_LOADING         34
#define EXIT_REASON_MWAIT_INSTRUCTION   36
#define EXIT_REASON_MONITOR_TRAP_FLAG   37
#define EXIT_REASON_MONITOR_INSTRUCTION 39
#define EXIT_REASON_PAUSE_INSTRUCTION   40
#define EXIT_REASON_MCE_DURING_VMENTRY  41
#define EXIT_REASON_TPR_BELOW_THRESHOLD 43
#define EXIT_REASON_APIC_ACCESS         44
#define EXIT_REASON_ACCESS_GDTR_OR_IDTR 46
#define EXIT_REASON_ACCESS_LDTR_OR_TR   47
#define EXIT_REASON_EPT_VIOLATION       48
#define EXIT_REASON_EPT_MISCONFIG       49
#define EXIT_REASON_INVEPT              50
#define EXIT_REASON_RDTSCP              51
#define EXIT_REASON_VMX_PREEMPTION_TIMER_EXPIRED     52
#define EXIT_REASON_INVVPID             53
#define EXIT_REASON_WBINVD              54
#define EXIT_REASON_XSETBV              55
#define EXIT_REASON_APIC_WRITE          56
#define EXIT_REASON_RDRAND              57
#define EXIT_REASON_INVPCID             58
#define EXIT_REASON_RDSEED              61
#define EXIT_REASON_PML_FULL            62
#define EXIT_REASON_XSAVES              63
#define EXIT_REASON_XRSTORS             64
#define EXIT_REASON_PCOMMIT             65

VMX-exit 处理程序应该是一个汇编函数,因为调用编译函数需要一些准备工作和一些寄存器修改。 VM-exit处理程序中必要的事情是保存寄存器的状态,以便我们稍后可以继续guest。

我创建了一个用于保存和恢复寄存器的示例函数。 在此函数中,我们调用另一个 C 函数来扩展 vm-exit 处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
PUBLIC AsmVmexitHandler
 
EXTERN MainVmexitHandler:PROC
EXTERN VmResumeInstruction:PROC
 
.code _text
 
AsmVmexitHandler PROC
 
    PUSH R15
    PUSH R14
    PUSH R13
    PUSH R12
    PUSH R11
    PUSH R10
    PUSH R9
    PUSH R8       
    PUSH RDI
    PUSH RSI
    PUSH RBP
    PUSH RBP    ; RSP
    PUSH RBX
    PUSH RDX
    PUSH RCX
    PUSH RAX   
 
    MOV RCX, RSP        ; GuestRegs
    SUB RSP, 28h
 
    CALL    MainVmexitHandler
    ADD RSP, 28h   
 
    POP RAX
    POP RCX
    POP RDX
    POP RBX
    POP RBP     ; RSP
    POP RBP
    POP RSI
    POP RDI
    POP R8
    POP R9
    POP R10
    POP R11
    POP R12
    POP R13
    POP R14
    POP R15
 
    SUB RSP, 0100h ; to avoid error in future functions
     
    JMP VmResumeInstruction
     
AsmVmexitHandler ENDP
 
END

主要的 VM-exit处理程序是一个 switch-case 函数,对 VMCS VM_EXIT_REASONEXIT_QUALIFICATION 具有不同的决策。

在这一部分中,我们只是通过EXIT_REASON_HLT执行操作,然后打印结果并正常恢复来宾状态。

从下面的代码中,您可以看到什么事件导致VM退出。 请记住,如果 VMCS 的控制执行字段(如上所述)配置了某些原因,则仅会导致 VM-exit。 例如,如果基于主处理器的 VM 执行控制的第7位允许,则来宾中HLT指令的执行将导致 VM 退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
VOID
MainVmexitHandler(PGUEST_REGS GuestRegs)
{
    ULONG ExitReason = 0;
    __vmx_vmread(VM_EXIT_REASON, &ExitReason);
 
    ULONG ExitQualification = 0;
    __vmx_vmread(EXIT_QUALIFICATION, &ExitQualification);
 
    DbgPrint("\nVM_EXIT_REASION 0x%x\n", ExitReason & 0xffff);
    DbgPrint("\EXIT_QUALIFICATION 0x%x\n", ExitQualification);
 
    switch (ExitReason)
    {
        //
        // 25.1.2  Instructions That Cause VM Exits Unconditionally
        // The following instructions cause VM exits when they are executed in VMX non-root operation: CPUID, GETSEC,
        // INVD, and XSETBV. This is also true of instructions introduced with VMX, which include: INVEPT, INVVPID,
        // VMCALL, VMCLEAR, VMLAUNCH, VMPTRLD, VMPTRST, VMRESUME, VMXOFF, and VMXON.
        //
 
    case EXIT_REASON_VMCLEAR:
    case EXIT_REASON_VMPTRLD:
    case EXIT_REASON_VMPTRST:
    case EXIT_REASON_VMREAD:
    case EXIT_REASON_VMRESUME:
    case EXIT_REASON_VMWRITE:
    case EXIT_REASON_VMXOFF:
    case EXIT_REASON_VMXON:
    case EXIT_REASON_VMLAUNCH:
    {
        break;
    }
    case EXIT_REASON_HLT:
    {
        DbgPrint("[*] Execution of HLT detected... \n");
 
        //
        // that's enough for now ;)
        //
        AsmVmxoffAndRestoreState();
 
        break;
    }
    case EXIT_REASON_EXCEPTION_NMI:
    {
        break;
    }
 
    case EXIT_REASON_CPUID:
    {
        break;
    }
 
    case EXIT_REASON_INVD:
    {
        break;
    }
 
    case EXIT_REASON_VMCALL:
    {
        break;
    }
 
    case EXIT_REASON_CR_ACCESS:
    {
        break;
    }
 
    case EXIT_REASON_MSR_READ:
    {
        break;
    }
 
    case EXIT_REASON_MSR_WRITE:
    {
        break;
    }
 
    case EXIT_REASON_EPT_VIOLATION:
    {
        break;
    }
 
    default:
    {
        // DbgBreakPoint();
        break;
    }
    }
}

5.11.1.继续执行下一条指令

如果发生VM退出(例如,Guest执行了 CPUID 指令),Guest RIP 保持不变,并且由VMM决定是否更改Guest的 RIP ,因此如果我们没有某种功能来管理这种情况,然后处理器执行 CPUID 指令的无限循环,因为我们没有增加 RIP

为了解决这个问题,我们必须读取一个名为 VM_EXIT_INSTRUCTION_LEN 的 VMCS 字段,该字段存储导致 VM-exit的指令的长度。

首先,我们必须从GUEST_RIP读取访客当前的RIP。其次,使用VMREAD读取VM_EXIT_INSTRUCTION_LEN ,第三,读取客户机RIP的指令长度。现在,guest将从下一条指令继续执行,我们就可以开始了。

下面的函数就是为了这个目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
VOID
ResumeToNextInstruction()
{
    PVOID ResumeRIP             = NULL;
    PVOID CurrentRIP            = NULL;
    ULONG ExitInstructionLength = 0;
 
    __vmx_vmread(GUEST_RIP, &CurrentRIP);
    __vmx_vmread(VM_EXIT_INSTRUCTION_LEN, &ExitInstructionLength);
 
    ResumeRIP = (PCHAR)CurrentRIP + ExitInstructionLength;
 
    __vmx_vmwrite(GUEST_RIP, (ULONG64)ResumeRIP);
}

5.12.VMRESUME指令

现在我们已经处理了虚拟机退出,是时候继续guest了。 指令继续执行 我们可以使用VMRESUME

VMRESUME 类似于 VMLAUNCH ,但它用于恢复guest系统。

为了比较这些指令,

  • 如果当前 VMCS 的启动状态不是“清除”,VMLAUNCH 将失败。 如果指令成功,它将启动状态设置为“已启动”。
  • 如果当前 VMCS 的启动状态不是“已启动”,则 VMRESUME 会失败。

所以很明显,如果我们之前执行了VMLAUNCH指令,我们就不能再使用它来恢复客户代码,在这种情况下,需要使用VMRESUME 。

以下代码是VMRESUME 的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
VOID
VmResumeInstruction()
{
    __vmx_vmresume();
 
    // if VMRESUME succeeds will never be here !
 
    ULONG64 ErrorCode = 0;
    __vmx_vmread(VM_INSTRUCTION_ERROR, &ErrorCode);
    __vmx_off();
    DbgPrint("[*] VMRESUME Error : 0x%llx\n", ErrorCode);
 
    //
    // It's such a bad error because we don't where to go!
    // prefer to break
    //
    DbgBreakPoint();
}

5.13.让我们测试一下

好了,我们已经完成了配置,现在是时候运行我们的驱动程序了。 与往常一样,我们应该禁用驱动程序签名强制并运行我们的驱动程序。
图片描述
从上图可以看出(在启动VM区域),首先,我们将当前逻辑处理器设置为0。接下来,我们使用VMCLEAR指令清除VMCS状态 设置VMCS布局并执行 VMLAUNCH 指令。

现在,我们的来宾代码已执行,并且我们将 VMCS 配置为在执行HLT **(CPU_BASED_HLT_EXITING)**指令时导致 VM 退出。

运行 guest 虚拟机后,将调用 VM 退出处理程序,然后调用主 VM 退出处理程序,并且由于 VMCS 退出原因为0xc (EXIT_REASON_HLT) ,因此我们成功检测到guest 虚拟机中HLT的执行。

之后,我们的机器状态保存机制被执行,我们使用 VMXOFF 指令成功关闭虚拟机管理程序,并以成功的 (RAX = 1) 状态返回到第一个调用者。

就是这样! 这不是很容易吗?

5.14.结论

在这一部分中,我们熟悉了配置虚拟机控制结构并最终运行了我们的guest代码。 未来的部分将是对此配置的增强,例如进入 保护模式、 中断注入页面修改日志记录、 虚拟化当前机器 等等。 如果您有任何疑问或问题,可以使用下面的评论部分。

5.15.参考

[1] 第 3C 卷 - 第 24 章 –(虚拟机控制结构 ( https://software.intel.com/en-us/articles/intel-sdm )

[2] 第 3C 卷 - 第 26 章 –(虚拟机条目)( https://software.intel.com/en-us/articles/intel-sdm )

[3] 分段( https://wiki.osdev.org/Segmentation

[4] x86 内存分段 ( https://en.wikipedia.org/wiki/X86_memory_segmentation )

[5] VmcsAuditor – 基于 Bochs 的虚拟机管理程序布局检查器 ( https://rayanfam.com/topics/vmcsauditor-a-bochs-based-hypervisor-layout-checker/ )

[6] Rohaaan/Hypervisor 初学者 ( https://github.com/rohaaan/hypervisor-for-beginners )

[7] SWAPGS — 交换 GS 基址寄存器 ( https://www.felixcloutier.com/x86/SWAPGS.html )

[8] 敲开天堂之门 - 动态处理器模式切换 ( http://rce.co/knockin-on-heavens-gate-dynamic-processor-mode-switching/ )


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-3-31 17:18 被zhang_derek编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (1)
雪    币: 19389
活跃值: (29037)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-3-31 21:36
2
1
感谢分享
游客
登录 | 注册 方可回帖
返回