首页
社区
课程
招聘
Hypervisor From Scratch – 第 6 部分:虚拟化已经运行的系统
2024-3-31 15:45 2639

Hypervisor From Scratch – 第 6 部分:虚拟化已经运行的系统

2024-3-31 15:45
2639

目录

六、虚拟化已经运行的系统

6.1.介绍

您好,欢迎来到教程Hypervisor From Scratch的第六部分。在这一部分中,我们将学习如何使用定制的虚拟机管理程序虚拟化已经运行的系统。与其他部分一样,本部分依赖于前面的部分,因此请务必先阅读它们。

6.2.概述

在第六部分中,我们将了解如何通过配置 VMCS 虚拟化当前运行的系统。我们使用VMX的监控功能来检测CPUID等重要指令的执行(并从用户模式和内核模式更改CPUID的结果),检测不同控制寄存器上的修改,并描述不同微架构上的VMX功能,谈论MSR位图和许多其他很酷的东西。

6.3.VMX 0设置和1设置

在前面的部分中,我们实现了一个名为 AdjustControl 的函数。 这是每个虚拟机管理程序的重要组成部分,因为您可能希望在具有不同微架构的许多不同处理器上运行虚拟机管理程序。 我们应该了解我们的处理器功能,以避免未定义的行为和 VM-Entry 错误。

这是这样工作的; 首先,向以下函数发送MSR,该MSR指示需要修改的VMCS控制。 然后我们查看相应的MSR来了解该控件的1设置和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;
}

如果您还记得上一部分,我们在 4 种情况下使用了上述函数。

1
2
3
4
5
6
__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));
 
__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));

简要浏览一下 附录 A -VMX 能力报告工具 说明 即可了解有关保留控制和默认设置的 。 在 Intel VMX 中,某些控制被保留,并且必须设置为由处理器确定的特定值(0 或 1)。 保留控件必须设置的特定值是其 默认设置 。 这些类型的设置因每个处理器和微体系结构而异,但一般来说,存在三种类型的类:

  • 始终灵活 :这些从未被保留。
  • Default0 :这些被(或已经)保留,默认设置为 0。
  • Default1 :它们被(或已经)保留,默认设置为 1。

现在,有针对 pin-based VM-execution controls, primary processor-based VM-execution controls, VM-Entry Controls, VM-Exit Controls and secondary processor-based VM-execution controls 的单独功能 MSR 。

这些 MSR 用于检查上述控制:

  • MSR_IA32_VMX_PROCBASED_CTLS
  • MSR_IA32_VMX_PROCBASED_CTLS2
  • MSR_IA32_VMX_EXIT_CTLS
  • MSR_IA32_VMX_ENTRY_CTLS
  • MSR_IA32_VMX_PINBASED_CTLS

在所有上述 MSR 中,位 31:0 指示这些控制允许的 0 设置。 如果 MSR 中的位 X 被清除为 0,则 VM 条目允许控制 X(位 X)为 0; 如果 MSR 中的位 X 设置为 1,则如果控制 X 为 0,则 VM 进入失败。同时,位 63:32 指示这些控制允许的 1 设置。 如果 MSR 中的位 32+X 设置为 1,VM 条目允许控制 X 为 1; 如果 MSR 中的位 32+X 清除为 0,且控制 X 为 1,则 VM 进入失败。

虽然也有一些例外,但是现在你应该明白AdjustControls的用途了,它首先读取VM执行控件对应的MSR,然后调整0设置和1设置,并返回最终结果。

我建议查看专门针对 MSR_IA32_VMX_PROCBASED_CTLS 和 MSR_IA32_VMX_PROCBASED_CTLS2 的 AdjustControls 结果,因为您可能会无意中将某些位设置为 1,因此,您应该制定一个根据您的特定处理器处理某些 VM 退出的计划。

6.4.CR0和CR4中VMX固定位

对于 CR0、 IA32_VMX_CR0_FIXED0 MSR(索引 486H)和 IA32_VMX_CR0_FIXED1 MSR(索引 487H)以及对于 CR4 IA32_VMX_CR4_FIXED0 MSR(索引 488H)和 IA32_VMX_CR4_FIXED1 MSR(索引 489H)指示如何在 VMX 操作中设置 CR0 和 CR4 中的位。 如果 IA32_VMX_CRx_FIXED0 中的 X 位为 1,则 CRx 的该位在 VMX 操作中固定为 1。 类似地,如果 IA32_VMX_CRx_FIXED1 中的位 X 为 0,则 CRx 的该位在 VMX 操作中固定为 0。 通常情况下,如果 IA32_VMX_CRx_FIXEDx 中的位 X 为 1,则 IA32_VMX_CRx_FIXED1 中的该位也为 1。

6.5.捕获当前机器的状态

在第五部分中,我们了解了如何配置不同的 VMCS 字段并最终在guest上下文下执行我们的指令 (HLT)。 这部分与上一部分类似,只是在一些VMCS属性上有一些细微的变化。 让我们回顾一下并看看其中的差异。

您需要知道的第一件事是,您必须为每个核心创建不同的堆栈,因为我们将同时虚拟化所有核心。 每当发生 VM-exit时就会使用这些堆栈。

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

从上面的代码可以看出,我们使用 VmmStack分别为每个核心(定义在 VIRTUAL_MACHINE_STATE结构)。

所有其他的事情,比如清除 VMCS 状态、加载 VMCS 和执行 VMLAUNCH 都与前面的部分完全相同,因此我不想再次描述它们,而是看看负责准备我们当前核心进行虚拟化的函数。

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
VOID
VirtualizeCurrentSystem(int ProcessorID, PEPTP EPTP, PVOID GuestStack)
{
    DbgPrint("\n======================== Virtualizing Current System (Logical Core 0x%x) =============================\n", ProcessorID);
 
    //
    // Clear the VMCS State
    //
    if (!ClearVmcsState(&g_GuestState[ProcessorID]))
    {
        goto ErrorReturn;
    }
 
    //
    // Load VMCS (Set the Current VMCS)
    //
    if (!LoadVmcs(&g_GuestState[ProcessorID]))
    {
        goto ErrorReturn;
    }
 
    DbgPrint("[*] Setting up VMCS for current system.\n");
    SetupVmcsAndVirtualizeMachine(&g_GuestState[ProcessorID], EPTP, GuestStack);
 
    //
    // Change this hook (detect modification of MSRs using RDMSR & WRMSR)
    //
    // DbgPrint("[*] Setting up MSR bitmaps.\n");
 
    DbgPrint("[*] Executing VMLAUNCH.\n");
    __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();
 
    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;
}

从上面的代码来看, SetupVmcsAndVirtualizeMachine是新的,所以让我们看看这个函数里面有什么。

6.5.1.配置VMCS字段

VMCS 字段并不是什么新鲜事。 我们应该配置这些字段来管理虚拟化核心的状态。

除VMCS控制位的配置外,所有VMCS字段与最后部分相同:

1
2
3
4
5
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 访问时退出,当然,我们的 VMX Exit-Handler 会被调用,从而清除该位为零会使系统速度大大减慢。

对于 SECONDARY_VM_EXEC_CONTROL ,我们使用 CPU_BASED_CTL2_RDTSCP 启用 RDTSCP ,使用 CPU_BASED_CTL2_ENABLE_INVPCID 启用 INVPCID使用CPU_BASED_CTL2_ENABLE_XSAVE_XRSTORS 启用 XSAVEXRSTORS

这是因为我在我的Windows 10 1809中运行上述代码,看到Windows将 INVPCIDXSAVE 用于其内部使用(在支持这些功能的处理器中),所以如果你在虚拟化核心之前没有启用它们,那么它可能导致错误。

请注意, RDTSCP 将处理器时间戳计数器的当前值读入 EDX:EAX 寄存器,并将 IA32_TSC_AUX MSR(地址 C0000103H)的值读入 ECX 寄存器。 该指令向 RDTSC 添加排序,并使性能测量比 RDTSC 更准确。

INVPCID 根据进程上下文标识符 (PCID) 使转换后备缓冲区 (TLB) 和分页结构高速缓存中的映射无效,并且 XSAVE 将处理器状态组件全部或部分保存到位于 XSAVE 由 指定的内存地址的 区域目标操作数。

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

除了用作GUEST_RSP的GuestStack之外,该函数中没有留下任何内容。稍后我会告诉你在这个论证中要加入什么。

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

好的,现在的问题是我们可以从哪里启动虚拟机管理程序。 我的意思是,如何保存特定核心的状态,然后在其上执行 VMLAUNCH 指令,然后继续执行其余部分。

为此,我更改了例程DrvCreate,因此您必须CreateFile从用户模式应用程序进行更改(我将在稍后讨论)。事实上,DrvCreate就是负责将所有核心置于VMX状态的函数。首先,它查询核心的计数,然后为每个核心执行必要的初始化。

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
NTSTATUS
DrvCreate(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    DbgPrint("[*] DrvCreate Called !\n");
 
    //
    // *** Start Virtualizing Current System ***
    //
 
    //
    // Initiating EPTP and VMX
    //
    PEPTP EPTP = InitializeEptp();
    InitializeVmx();
 
    int LogicalProcessorsCount = KeQueryActiveProcessorCount(0);
 
    for (size_t i = 0; i < LogicalProcessorsCount; i++)
    {
        // Launching VM for Test (in the all logical processor)
        int ProcessorID = i;
 
        // Allocating VMM Stack
        AllocateVmmStack(ProcessorID);
 
        // Allocating MSR Bit
        AllocateMsrBitmap(ProcessorID);
 
        RunOnProcessor(i, EPTP, VmxSaveState);
        DbgPrint("\n======================================================================================================\n", ProcessorID);
    }
 
    Irp->IoStatus.Status      = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
    return STATUS_SUCCESS;
}

我们的微型驱动程序设计用于单核、两核、三核甚至所有核。 从下面的代码中可以看到,它查询逻辑处理器计数。

1
int LogicalProcessorsCount = KeQueryActiveProcessorCount(0);

您可以编辑此行以虚拟化一定数量的核心或仅虚拟化特定核心,但上述代码默认虚拟化所有核心。

6.5.2.更改所有内核上的IRQL

有一个函数叫做RunOnProcessor.该函数将处理器 ID 作为其第一个参数,EPTP 指针(在第四部分中解释)作为第二个参数,以及调用的特定例程VmxSaveState作为第三个参数。

RunOnProcessor 将处理器亲和力设置为特殊内核,然后将 IRQL 提升到调度级别,以便 Windows 调度程序无法启动来更改上下文; 因此,它运行我们的例程,当它从 VmxSaveState 返回时,当前运行的核心被虚拟化,因此它可以将 IRQL 降低到之前的水平。 现在,Windows 可以在虚拟机管理程序的管理下继续正常执行。 IRQL 代表中断请求级别,这是一种特定于 Windows 的机制,用于管理中断或按级别赋予优先级,因此提高 IRQL 意味着您的例程将以比正常 Windows 代码(PASSIVE_LEVEL 和 APC_LEVEL)更高的优先级执行。 欲了解更多信息,您可以访问这里

RunOnProcessor代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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功能 都是在汇编中实现的。 对于使用C函数,在汇编中,我们可以编写函数名称并使用EXTERN关键字。

以下示例显示了如何在程序集文件中使用 VirtualizeCurrentSystem。

1
EXTERN VirtualizeCurrentSystem:PROC

VMXSaveState是这样实现的(在汇编中):

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
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 the 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

它首先保存所有寄存器的状态,由于Shadow Space而减去堆栈以实现快速调用函数,然后将RSP放入R8并调用VirtualizeCurrentSystem。 RSP 应该移到 R8 中(正如我在 GuestStack 中告诉您的那样),因为 x64 fastcall 参数应该按以下顺序传递:RCX、RDX、R8、R9 + Stack。 这意味着该函数的第三个参数是当前 RSP,并且该值将用作 VMCS 中的 GUEST_RSP。

如果上述函数运行没有错误,我们永远不应该到达 ret 指令,因为该状态稍后将在另一个名为 VmxRestoreState 的函数中继续。

正如我们在VirtualizeCurrentSystem中看到的,它最终调用SetupVmcsAndVirtualizeMachine,GUEST_RIP指向VmxRestoreState,因此当前核心中执行的第一个例程是VmxRestoreState。

这个函数的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

在上面的函数中,首先,我们删除 Shadow Space 并恢复寄存器状态。

当我们回到 RunOnProcessor,是时候降低 IRQL 了。

这个函数会被调用很多次(基于我们的逻辑核心数),最终,我们所有的核心都处于VMX操作下,现在我们处于 VMX non-root操作 中。

6.6.更改用户模式应用程序

基于上述假设,我们必须在用户模式应用程序中进行一些细微的更改,以便在加载驱动程序后,它可以用于通知内核模式代码开始并最终结束加载虚拟机管理程序。

6.6.1.使用 CreateFile 获取句柄

在对供应商和虚拟机管理程序的存在进行一些检查之后,现在我们必须从内核模式调用 DrvCreate,它是通过 CreateFile 用户模式函数调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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为我们提供了一个句柄,可以在我们未来的函数中使用它来与我们的驱动程序交互。 尽管如此,每当我们关闭应用程序或调用 CloseHandle在用户模式下, DrvClose会在内核中自动调用。 DrvClose关闭虚拟机管理程序并将状态恢复到之前的状态(未虚拟化)。

6.7.使用VMX监控功能

配置完上述所有字段后,就可以使用 VMX 的监控功能了。 我们将了解这些功能在安全应用程序或逆向工程任务中有何独特之处。 作为额外资源,您可以使用 HyperDbg Debugger 。 HyperDbg 是一个基于虚拟机管理程序的调试器,它允许我们在调试过程中使用大部分 VT-x 功能。

6.7.1.CR3目标控制

VM-execution control fields 包括一组 4个CR3-target values and a CR3-target count。 如果您还记得我之前在 SetupVmcsAndVirtualizeMachine 中介绍的 VMCS 字段,您可以看到以下几行:

1
2
3
4
5
__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 non-root操作中执行 MOV 到 CR3 不会导致 VM-exit。 如果 CR3-target count为 n,则仅考虑前 n 个 CR3 目标值。

使用这个功能的实现是这样的:

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
BOOLEAN
SetTargetControls(UINT64 CR3, UINT64 Index)
{
    //
    // Index starts from 0 , not 1
    //
    if (Index >= 4)
    {
        //
        // Not supported for more than 4 , at least for now :(
        //
        return FALSE;
    }
 
    UINT64 temp = 0;
 
    if (CR3 == 0)
    {
        if (g_Cr3TargetCount <= 0)
        {
            //
            // Invalid command as g_Cr3TargetCount cannot be less than zero
            // s
            return FALSE;
        }
        else
        {
            g_Cr3TargetCount -= 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);
        }
        g_Cr3TargetCount += 1;
    }
 
    __vmx_vmwrite(CR3_TARGET_COUNT, g_Cr3TargetCount);
    return TRUE;
}

我没有任何好的例子来说明此控件在常规 Windows 中如何发挥作用,因为每个进程都有数千个 CR3 更改。 不过,我的一位朋友告诉我,它在科学项目的一些特殊情况下使用,以提高整体性能。

6.7.2.处理guest CPUID执行

CPUID 是无条件导致VM退出的指令。 如您所知, 使用CPUID 是因为它允许软件发现处理器的详细信息。 它还用于刷新不支持 RDTSCP 等指令的处理器的管道,因此它们可以使用 CPUID + RDTSC 并使用 CPUID 作为屏障。

每当任何特权级别的任何软件执行 CPUID 指令时,都会调用我们的 vm-exit 处理程序,现在我们可以决定要向软件显示的内容。 例如,之前我发表了一篇文章“ 击败恶意软件的反虚拟机技术(基于CPUID的指令) ”。 本文介绍如何通过更改 CPUID 指令结果的方式配置 VMware Workstation,以便具有反 VM 技术的恶意软件无法了解它们正在虚拟化环境中执行。 VMware Workstation(和其他虚拟环境)执行相同的机制来处理 CPUID 。 在下面的示例中,我只是将寄存器的状态(VM-exit之前的寄存器状态)传递给 HandleCPUID。 该函数决定请求的 CPUID 是否应具有修改后的结果或仅执行直通原始结果。

处理每个 VM 退出(由 VMX 非 root 中执行 CPUID 引起)的默认行为是使用 _cpuidex 获取原始结果,这是 CPUID 的内部函数。

1
__cpuidex(CpuInfo, (INT32)state->rax, (INT32)state->rcx);

如您所见,VMX non-root 本身无法执行 CPUID ,我们可以在 VMX root 模式下执行 CPUID 并将结果返回给 VMX non-root 模式。

我们需要检查 RAX(CPUID 索引)是否为 1。 这是因为有一个指示位可以显示当前机器是否在虚拟机管理程序下运行。 与许多其他虚拟机一样,我们设置当前虚拟机管理程序位(本示例中使用的常量类似于 hyper-v 的位 HYPERV_HYPERVISOR_PRESENT_BIT)以表明我们正在虚拟机管理程序下运行。

还有关于虚拟机管理程序提供商的第二次检查。 我们将其设置为“ HVFS ”以表明我们的虚拟机管理程序是[ H ]yper[ V ]isor[ F ]rom[ S ]cratch。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//
// 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
    //
    CpuInfo[2] |= HYPERV_HYPERVISOR_PRESENT_BIT;
}
 
else if (state->rax == HYPERV_CPUID_INTERFACE)
{
    //
    // Return our interface identifier
    //
    CpuInfo[0] = 'HVFS'; // [H]yper[V]isor [F]rom [S]cratch
}

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

最后,我们将它们放入寄存器中,以便客户每次执行例程时都能得到正确的结果。

1
2
3
4
5
6
7
//
// Copy the values from the logical processor registers into the VP GPRs
//
state->rax = CpuInfo[0];
state->rbx = CpuInfo[1];
state->rcx = CpuInfo[2];
state->rdx = CpuInfo[3];

将以上所有代码放在一起,我们有以下功能:

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
BOOLEAN
HandleCPUID(PGUEST_REGS state)
{
    INT32 CpuInfo[4];
    ULONG Mode = 0;
 
    //
    // 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
    //
 
    __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(CpuInfo, (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
        //
        CpuInfo[2] |= HYPERV_HYPERVISOR_PRESENT_BIT;
    }
 
    else if (state->rax == HYPERV_CPUID_INTERFACE)
    {
        //
        // Return our interface identifier
        //
        CpuInfo[0] = 'HVFS'; // [H]yper[V]isor [F]rom [S]cratch
    }
 
    //
    // Copy the values from the logical processor registers into the VP GPRs
    //
    state->rax = CpuInfo[0];
    state->rbx = CpuInfo[1];
    state->rcx = CpuInfo[2];
    state->rdx = CpuInfo[3];
 
    return FALSE; // Indicates we don't have to turn off VMX
}

它在某种程度上类似于 CPUID 的指令级挂钩。 此外,通过配置primary and secondary processor-based controls,您可以为许多其他重要指令提供相同的处理功能。 稍后我们将描述其中一些说明。

6.7.3.防止CPUID时序泄露

作为有关虚拟机管理程序的额外说明, CPUID 是导致用户模式或内核模式软件通过使用增量定时旁路攻击来检测虚拟机管理程序是否存在的方法之一。 它源于这样一个事实:该指令会导致无条件 VM-exit,在虚拟机管理程序的情况下,与非虚拟机相比,执行时间要长得多。

这些攻击的描述超出了本文的范围,但如果您有兴趣,可以阅读 本文 中有关这些攻击的详细说明。

6.7.4.导致VM有条件退出的指令

以下是在 VMX 非 root 操作中导致 VM-exit的指令列表,具体取决于 VM 执行控件的设置。 - 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

6.7.5.控制寄存器修改检测

检测和处理控制寄存器 (CR) 修改是虚拟机管理程序提供的重要监视功能之一。

想象一下,如果有人利用 Windows 内核(或任何其他操作系统)并想要取消设置控制寄存器位之一(比方说写保护或 SMEP ); 然后虚拟机管理程序会检测到此修改并阻止进一步执行。

1
请注意,SMEP 代表管理员模式执行保护。 CR4.SMEP 允许保护页面免受管理模式指令提取的影响。 如果 CR4.SMEP = 1 指令, WP 代表 写 保护 ,则在管理模式下运行的软件无法从用户模式下可访问的线性地址中获取 。 CR0.WP 允许保护页面免受管理员模式写入的影响。 如果 CR0.WP = 0 ,则允许对具有只读访问权限的线性地址进行管理员模式写访问; 如果 CR0.WP = 1 ,则它们不是(无论 CR0.WP 的值如何,都不允许对具有只读访问权限的线性地址进行用户模式写访问)。

现在是时候实现我们的功能了。

首先,让我们阅读 VMCS 的 GUEST_CR 和 EXIT_QUALIFICATION。。

1
2
3
4
__vmx_vmread(EXIT_QUALIFICATION , &ExitQualification);
__vmx_vmread(GUEST_CR0 , &GuestCR0);
__vmx_vmread(GUEST_CR3 , &GuestCR3);
__vmx_vmread(GUEST_CR4,  &GuestCR4);

正如您所看到的,下图显示了我们如何解释退出资格
图片描述
请注意,EXIT_QUALIFICATION 在某种程度上是一个通用 VMCS 字段,在某些情况下(例如由无效 VMCS 布局、控制寄存器修改、I/O 位图和其他事件导致的 VM 退出),它提供有关 VM 退出原因的附加信息。

从上图可以看出,我们根据EXIT_QUALIFICATION制作一些变量来描述情况。

每当由 MOV CRx、REG 等指令导致 VM 退出时,我们必须从 VMX-root 模式手动修改 guest VMCS 的 CRx。 以下代码显示如何使用 VMWRITE 更改 VMCS 的 GUEST_CRx 字段。

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
case TYPE_MOV_TO_CR:
{
    switch (data->Fields.ControlRegister)
    {
    case 0:
        __vmx_vmwrite(GUEST_CR0, *RegPtr);
        __vmx_vmwrite(CR0_READ_SHADOW, *RegPtr);
        break;
    case 3:
 
        __vmx_vmwrite(GUEST_CR3, (*RegPtr & ~(1ULL << 63)));
 
        //
        // In the case of using EPT, the context of EPT/VPID should be
        // invalidated
        //
        break;
    case 4:
        __vmx_vmwrite(GUEST_CR4, *RegPtr);
        __vmx_vmwrite(CR4_READ_SHADOW, *RegPtr);
        break;
    default:
        DbgPrint("[*] Unsupported register %d\n", data->Fields.ControlRegister);
        break;
    }
}
break;

否则,我们必须从guestVMCS(不是主机控制寄存器,因为它可能不同)读取 CRx ,然后将其放入相应的寄存器中(在调用VM退出处理程序时我们保存的寄存器中),然后继续 VMRESUME 。 这样,guest就认为它执行了 MOV reg, CRx成功地。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
case TYPE_MOV_FROM_CR:
{
    switch (data->Fields.ControlRegister)
    {
    case 0:
        __vmx_vmread(GUEST_CR0, RegPtr);
        break;
    case 3:
        __vmx_vmread(GUEST_CR3, RegPtr);
        break;
    case 4:
        __vmx_vmread(GUEST_CR4, RegPtr);
        break;
    default:
        DbgPrint("[*] Unsupported register %d\n", data->Fields.ControlRegister);
        break;
    }
}

把它们放在一起,我们有一个像这样的函数:

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
VOID
HandleControlRegisterAccess(PGUEST_REGS GuestState)
{
    ULONG ExitQualification = 0;
 
    __vmx_vmread(EXIT_QUALIFICATION, &ExitQualification);
 
    PMOV_CR_QUALIFICATION data = (PMOV_CR_QUALIFICATION)&ExitQualification;
 
    PULONG64 RegPtr = (PULONG64)&GuestState->rax + data->Fields.Register;
 
    //
    // Because its RSP and as we didn't save RSP correctly (because of pushes)
    // so we have to make it points to the GUEST_RSP
    //
    if (data->Fields.Register == 4)
    {
        INT64 RSP = 0;
        __vmx_vmread(GUEST_RSP, &RSP);
        *RegPtr = RSP;
    }
 
    switch (data->Fields.AccessType)
    {
    case TYPE_MOV_TO_CR:
    {
        switch (data->Fields.ControlRegister)
        {
        case 0:
            __vmx_vmwrite(GUEST_CR0, *RegPtr);
            __vmx_vmwrite(CR0_READ_SHADOW, *RegPtr);
            break;
        case 3:
 
            __vmx_vmwrite(GUEST_CR3, (*RegPtr & ~(1ULL << 63)));
 
            //
            // In the case of using EPT, the context of EPT/VPID should be
            // invalidated
            //
            break;
        case 4:
            __vmx_vmwrite(GUEST_CR4, *RegPtr);
            __vmx_vmwrite(CR4_READ_SHADOW, *RegPtr);
            break;
        default:
            DbgPrint("[*] Unsupported register %d\n", data->Fields.ControlRegister);
            break;
        }
    }
    break;
 
    case TYPE_MOV_FROM_CR:
    {
        switch (data->Fields.ControlRegister)
        {
        case 0:
            __vmx_vmread(GUEST_CR0, RegPtr);
            break;
        case 3:
            __vmx_vmread(GUEST_CR3, RegPtr);
            break;
        case 4:
            __vmx_vmread(GUEST_CR4, RegPtr);
            break;
        default:
            DbgPrint("[*] Unsupported register %d\n", data->Fields.ControlRegister);
            break;
        }
    }
    break;
 
    default:
        DbgPrint("[*] Unsupported operation %d\n", data->Fields.AccessType);
        break;
    }
}

之所以需要实现像HandleControlRegisterAccess这样的函数,是因为有些处理器对一些processor-based VM-execution controls有1-settings,比如CR3-Load Exiting和CR3-Store Existing,所以我们必须自己管理这些类型的VM-exit ,但是如果我们的处理器可以在没有这些设置的情况下继续运行,则强烈建议减少 VM 退出的数量并避免配置导致此类 VM 退出的设置,因为现代操作系统访问控制寄存器很多; 因此,它会带来严重的性能损失。

6.7.6.MSR位图

这里的一切都取决于您是否设置了Primary Processor Based controls的第 28 位。

在支持“使用 MSR 位图”VM-execution control设置为 1 的处理器上,VM-execution control字段包括四个连续 MSR 位图的 64 位物理地址,每个位图大小为 1 KB。

Intel SDM中MSR位图的定义非常清楚,所以我只是从原始手册中复制它们。 阅读完它们后,我们将开始实现它们并将它们放入我们的虚拟机管理程序中。

  • 读取低 MSR 的位图(位于 MSR 位图地址)。 这对于 00000000H 到 00001FFFH 范围内的每个 MSR 地址包含一位。 该位确定应用于该 MSR 的 RDMSR 的执行是否会导致 VM-exit。
  • 读取高 MSR 的位图(位于 MSR 位图地址加 1024 处)。 这对于 C0000000H 到 C0001FFFH 范围内的每个 MSR 地址包含一位。 该位确定应用于该 MSR 的 RDMSR 的执行是否会导致 VM-exit。
  • 写入低 MSR 的位图(位于 MSR 位图地址加 2048)。 这对于 00000000H 到 00001FFFH 范围内的每个 MSR 地址包含一位。 该位确定应用于该 MSR 的 WRMSR 的执行是否会导致 VM-exit。
  • 写入高 MSR 的位图(位于 MSR 位图地址加 3072 处)。 这对于 C0000000H 到 C0001FFFH 范围内的每个 MSR 地址包含一位。 该位确定应用于该 MSR 的 WRMSR 的执行是否会导致 VM-exit。

OK,我们把上面这句话带入代码中。 首先,我们将为 MSR VM 退出编写处理程序。

处理 MSR 阅读

在我们的直通虚拟机管理程序中,如果 RDMSR 或 WRMSR 中的任何一个导致 VM-exit,我们必须手动执行 RDMSR 或 WRMSR 并将结果设置到寄存器中。 因此,我们有一个功能来管理 RDMSR,例如:

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
VOID
HandleMSRRead(PGUEST_REGS GuestRegs)
{
    MSR msr = {0};
 
    //
    // RDMSR. The RDMSR instruction causes a VM exit if any of the following are true:
    //
    // The "use MSR bitmaps" VM-execution control is 0.
    // The value of ECX is not in the ranges 00000000H - 00001FFFH and C0000000H - C0001FFFH
    // The value of ECX is in the range 00000000H - 00001FFFH and bit n in read bitmap for low MSRs is 1,
    //   where n is the value of ECX.
    // The value of ECX is in the range C0000000H - C0001FFFH and bit n in read bitmap for high MSRs is 1,
    //   where n is the value of ECX & 00001FFFH.
    //
 
    if (((GuestRegs->rcx <= 0x00001FFF)) || ((0xC0000000 <= GuestRegs->rcx) && (GuestRegs->rcx <= 0xC0001FFF)))
    {
        msr.Content = MSRRead((ULONG)GuestRegs->rcx);
    }
    else
    {
        msr.Content = 0;
    }
 
    GuestRegs->rax = msr.Low;
    GuestRegs->rdx = msr.High;
}

您可以看到它只是检查 MSR 的健全性,然后执行 RDMSR 并最终将结果放入 RAXRDX (因为非虚拟化的 RDMSR 会做同样的事情)。

处理MSR写入

还有另一个处理 WRMSR VM-exits 的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
VOID
HandleMSRWrite(PGUEST_REGS GuestRegs)
{
    MSR msr = {0};
 
    //
    // Check for the sanity of MSR
    //
    if ((GuestRegs->rcx <= 0x00001FFF) || ((0xC0000000 <= GuestRegs->rcx) && (GuestRegs->rcx <= 0xC0001FFF)))
    {
        msr.Low  = (ULONG)GuestRegs->rax;
        msr.High = (ULONG)GuestRegs->rdx;
        MSRWrite((ULONG)GuestRegs->rcx, msr.Content);
    }
}

该函数的功能很简单。 尽管如此,值得您自己尝试的一件事是避免在 CPU_BASED_VM_EXEC_CONTROL 中设置 CPU_BASED_ACTIVATE_MSR_BITMAP,您将看到所有 MSR 读取和修改都会导致 VM 退出,原因如下:

  • EXIT_REASON_MSR_READ
  • EXIT_REASON_MSR_WRITE

这次,我们必须将所有内容传递给上述函数并记录这些 VM-exit,以便您可以看到 Windows 在虚拟机管理程序中运行时使用的 MSR。 正如我上面告诉你的,Windows 执行大量的 MSR 指令,因此它会使你的系统慢得多,超出你的承受能力。

好的,让我们回到 MSR 位图。 我们需要两个函数来设置 MSR 位图的位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
VOID
SetBit(PVOID Addr, UINT64 Bit, BOOLEAN Set)
{
    PAGED_CODE();
 
    UINT64 Byte = Bit / 8;
    UINT64 Temp = Bit % 8;
    UINT64 N    = 7 - Temp;
 
    BYTE * Addr2 = Addr;
    if (Set)
    {
        Addr2[Byte] |= (1 << N);
    }
    else
    {
        Addr2[Byte] &= ~(1 << N);
    }
}

另一个函数用于检索特定位。

1
2
3
4
5
6
7
8
9
10
VOID
GetBit(PVOID Addr, UINT64 Bit)
{
    UINT64 Byte = 0, K = 0;
    Byte         = Bit / 8;
    K            = 7 - Bit % 8;
    BYTE * Addr2 = Addr;
 
    return Addr2[Byte] & (1 << K);
}

现在是时候根据上述有关 MSR 位图的描述将所有内容收集到一个函数中了。 以下函数首先检查 MSR 的健全性; 然后,它更改目标逻辑核心的 MSR 位图(这就是为什么我们同时保存 MSR 位图的物理地址和虚拟地址、VMCS 字段的物理地址和虚拟地址,以方便修改和将来的释放)。 如果是低MSR的读(RDMSR),则设置MSR位图虚拟地址中的相应位,如果是低MSR的写(WRMSR),则修改MSR位图+ 2048(如Intel手册中所述)并精确对于高 MSR(0xC0000000 和 0xC0001FFF 之间)也是如此,但不要忘记减法(0xC0000000),因为 0xC000nnnn 不是有效位。

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
BOOLEAN
SetMsrBitmap(ULONG64 Msr, int ProcessID, BOOLEAN ReadDetection, BOOLEAN WriteDetection)
{
    if (!ReadDetection && !WriteDetection)
    {
        //
        // Invalid Command
        //
        return FALSE;
    }
 
    if (Msr <= 0x00001FFF)
    {
        if (ReadDetection)
        {
            SetBit(g_GuestState[ProcessID].MsrBitmap, Msr, TRUE);
        }
        if (WriteDetection)
        {
            SetBit(g_GuestState[ProcessID].MsrBitmap + 2048, Msr, TRUE);
        }
    }
    else if ((0xC0000000 <= Msr) && (Msr <= 0xC0001FFF))
    {
        if (ReadDetection)
        {
            SetBit(g_GuestState[ProcessID].MsrBitmap + 1024, Msr - 0xC0000000, TRUE);
        }
        if (WriteDetection)
        {
            SetBit(g_GuestState[ProcessID].MsrBitmap + 3072, Msr - 0xC0000000, TRUE);
        }
    }
    else
    {
        return FALSE;
    }
    return TRUE;
}

还要记住一件事,当前只有上述 MSR 范围在 Intel 处理器中有效,因此即使任何其他 RDMSR 和 WRMSR 也会导致 VM-exit。 尽管如此,这里的健全性检查是强制性的,因为guest可能会发送无效的 MSR 并导致整个系统崩溃(在 VMX root-mode下)。 在以后的部分中,当我们了解事件注入时,我们将在客户机尝试访问无效 MSR 的情况下,通过向客户机注入事件来模拟物理机的行为。

6.8.关闭VMX并退出虚拟机管理程序

是时候关闭虚拟机管理程序并将处理器状态恢复到运行虚拟机管理程序之前的状态了。

就像我们进入虚拟机管理程序 (VMLAUNCH) 的方式一样,我们必须将 C 函数与汇编例程结合起来,以保存状态、执行 VMXOFF、释放所有先前分配的池,最后恢复状态。

该例程的 VMXOFF 部分应在 VMX 根模式下执行。 您不能只在驱动程序函数之一中执行 __vmx_vmxoff 并期望它关闭虚拟机管理程序,因为 Windows 及其所有驱动程序当前都在 VMX 非 root 中运行,因此执行任何 VMX 指令就像使用一个 VM 退出出于以下原因。

  • EXIT_REASON_VMCLEAR
  • EXIT_REASON_VMPTRLD
  • EXIT_REASON_VMPTRST
  • EXIT_REASON_VMREAD
  • EXIT_REASON_VMRESUME
  • EXIT_REASON_VMWRITE
  • EXIT_REASON_VMXOFF
  • EXIT_REASON_VMXON
  • EXIT_REASON_VMLAUNCH

要关闭虚拟机管理程序,最好使用我们的 IRP 主要功能之一。 在我们的例子中,我们使用了 DrvClose因为每当我们设备的句柄关闭时它总是会收到通知。 如果您还记得上面的内容,我们使用我们的设备创建一个句柄 CreateFile ( DrvCreate),现在是时候使用以下命令关闭我们的句柄了 DrvClose.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NTSTATUS
DrvClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    DbgPrint("[*] DrvClose Called !\n");
 
    // executing VMXOFF (From CPUID) on every logical processor
    TerminateVmx();
 
    Irp->IoStatus.Status      = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
    return STATUS_SUCCESS;
}

上面的函数没有什么特别的; 只有 TerminateVmx被添加。

此函数类似于执行 VMLAUNCH 的例程,只是它运行的是 VMXOFF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VOID
TerminateVmx()
{
    DbgPrint("\n[*] Terminating VMX...\n");
 
    int LogicalProcessorsCount = KeQueryActiveProcessorCount(0);
 
    for (size_t i = 0; i < LogicalProcessorsCount; i++)
    {
        DbgPrint("\t\t + Terminating VMX on processor %d\n", i);
        RunOnProcessorForTerminateVMX(i);
 
        //
        // Free the destination memory
        //
        MmFreeContiguousMemory(PhysicalToVirtualAddress(g_GuestState[i].VmxonRegion));
        MmFreeContiguousMemory(PhysicalToVirtualAddress(g_GuestState[i].VmcsRegion));
        ExFreePoolWithTag(g_GuestState[i].VmmStack, POOLTAG);
        ExFreePoolWithTag(g_GuestState[i].MsrBitmap, POOLTAG);
    }
 
    DbgPrint("[*] VMX Operation turned off successfully. \n");
}

如您所见,它在所有正在运行的逻辑核心上执行 RunOnProcessorForTerminateVMX。 然后,它使用 MmFreeContigouslyMemory 释放为 VmxonRegion、VmcsRegion、VmmStack 和 MsrBitmap 分配的缓冲区,当然,还会在需要时将物理数据转换为虚拟数据.

请注意,如果虚拟化了部分核心(不是全部),则必须修改此函数。

在 RunOnProcessorForTerminateVMX 中,我们必须告诉 VMX 根操作有关关闭虚拟机管理程序的信息。 正如我告诉你的,这是因为我们无法在常规驱动程序例程中执行任何 VMX 指令,而且很明显,如果没有任何机制来处理这种情况,或者我们没有足够的特权来卸载虚拟机管理程序。

有多种方法可以告诉我们的 VMX 根操作有关 VMXOFF 的 信息,但在我们的例子中,我们将使用 CPUID

现在你肯定知道执行CPUID会导致VM退出。 现在,在我们的 CPUID 退出处理程序例程中,我们管理每当执行 RAX = 0x41414141 和 RCX = 0x42424242 的 CPUID 时,我们就必须返回 true,并且它向调用者表明虚拟机管理程序需要关闭。

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

还有另一项 DPL 检查,以确保 RAX = 0x41414141 和 RCX = 0x42424242 的 CPUID 仅在系统特权级别(内核模式)下执行。 因此,任何用户模式应用程序都无法卸载我们的虚拟机管理程序。

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

现在,我们的 RunOnProcessorForTerminateVMX 会分别执行 CPUID 并将调整后的值存入所有内核的寄存器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BOOLEAN
RunOnProcessorForTerminateVMX(ULONG ProcessorNumber)
{
    KIRQL OldIrql;
    INT32 CpuInfo[4];
 
    KeSetSystemAffinityThread((KAFFINITY)(1 << ProcessorNumber));
 
    OldIrql = KeRaiseIrqlToDpcLevel();
 
    //
    // Our routine is VMXOFF
    //
    __cpuidex(CpuInfo, 0x41414141, 0x42424242);
 
    KeLowerIrql(OldIrql);
 
    KeRevertToUserAffinityThread();
 
    return TRUE;
}

在 EXIT_REASON_CPUID 处理程序中,我们知道如果处理程序返回 true,那么我们必须将其关闭,因此我们应该考虑其他一些事情。 例如,Windows 期望稍后从 GUEST_RIP 继续,并且每当 VM 退出处理程序返回时都需要其先前的 GUEST_RSP; 因此,我们必须将它们保存在某些位置,并在以后使用它们来恢复 Windows 状态。

另外,我们必须增加GUEST_RIP,因为我们想恢复CPUID之后的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
case EXIT_REASON_CPUID:
{
    Status = HandleCPUID(GuestRegs); // Detect whether we have to turn off VMX or Not
    if (Status)
    {
        // We have to save GUEST_RIP & GUEST_RSP somewhere to restore them directly
 
        ULONG ExitInstructionLength = 0;
        g_GuestRIP                  = 0;
        g_GuestRSP                  = 0;
        __vmx_vmread(GUEST_RIP, &g_GuestRIP);
        __vmx_vmread(GUEST_RSP, &g_GuestRSP);
        __vmx_vmread(VM_EXIT_INSTRUCTION_LEN, &ExitInstructionLength);
 
        g_GuestRIP += ExitInstructionLength;
    }
    break;
}

从第五部分,您可能知道MainVmexitHandler是从VmexitHandler调用的(来自VMExitHandler.asm的汇编函数)

让我们详细看看。

首先,我们必须 extern 一些先前定义的变量。

1
2
EXTERN g_GuestRIP:QWORD
EXTERN g_GuestRSP:QWORD

现在我们的VmexitHandler是这样工作的,每当VM退出发生时,目标逻辑核心就会执行HOST_RIP中定义的VmexitHandler,并且我们的RSP设置为HOST_RSP,然后我们必须保存所有寄存器。 这意味着我们必须创建一个允许我们读取和修改类似 C 结构中的寄存器的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct _GUEST_REGS
{
    ULONG64 rax; // 0x00         // NOT VALID FOR SVM
    ULONG64 rcx;
    ULONG64 rdx; // 0x10
    ULONG64 rbx;
    ULONG64 rsp; // 0x20         // rsp is not stored here on SVM
    ULONG64 rbp;
    ULONG64 rsi; // 0x30
    ULONG64 rdi;
    ULONG64 r8; // 0x40
    ULONG64 r9;
    ULONG64 r10; // 0x50
    ULONG64 r11;
    ULONG64 r12; // 0x60
    ULONG64 r13;
    ULONG64 r14; // 0x70
    ULONG64 r15;
} GUEST_REGS, *PGUEST_REGS;

只需按 GUEST_REGS 结构顺序推送所有寄存器,并将 RSP 作为第一个参数推送到 MainVmexitHandler (Fastcall RCX),然后对 Shadow Space 进行一些减法。

您可以在这里看到 VmexitHandler:

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
VmexitHandler 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        ; Fast CALL argument to PGUEST_REGS
    SUB RSP, 28h        ; Free some space for Shadow Section
 
    CALL    MainVmexitHandler
 
    ADD RSP, 28h        ; Restore the state
 
    ; Check whether we have to turn off VMX or Not (the result is in RAX)
 
    CMP AL, 1
    JE      VmxoffHandler
 
    ; Restore the state
    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
     
 
VmexitHandler ENDP

从上面的代码来看,当我们从 MainVmexitHandler 返回时,我们必须检查 MainVmexitHandler(在 RAX 中)的返回结果是否告诉我们关闭虚拟机管理程序或继续。

如果需要继续,恢复寄存器状态并跳转到我们的VmResumeInstruction函数。

VmResumeInstruction 执行 __vmx_vmresume 并且处理器将 RIP 寄存器设置为 GUEST_RIP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
VOID
VmResumeInstruction()
{
    ULONG64 ErrorCode = 0;
 
    __vmx_vmresume();
 
    //
    // if VMRESUME succeeds will never be here!
    //
    __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();
}

但如果需要关闭怎么办?

然后根据AL寄存器,跳转到另一个名为VmxoffHandler的函数。 该函数执行VMXOFF指令,关闭虚拟机管理程序(在当前逻辑核心中),然后将寄存器恢复到我们将它们保存在VmexitHandler中的先前状态。

我们在这里唯一要做的就是将堆栈指针更改为 GUEST_RSP(我们将它们保存在 g_GuestRSP 中)并跳转到 GUEST_RIP(保存在 g_GuestRIP 中)。

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
VmxoffHandler PROC
 
    ; Turn VMXOFF
    VMXOFF
 
    ; Restore the state
 
    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
 
    ; Set guest RIP and RSP
 
    MOV     RSP, g_GuestRSP
 
    JMP     g_GuestRIP
 
VmxoffHandler ENDP

现在一切都完成了,我们执行正常的 Windows(驱动程序)例程; 我的意思是,在从 RunOnProcessorForTerminateVMX 执行的最后一个 CPUID 之后开始执行,但现在我们不处于 VMX 操作中。

6.9.VM退出处理程序

将以上所有代码放在一起,现在我们必须管理不同类型的VM-exit,因此我们需要修改之前解释的(第5部分)VM-exit处理程序; 如果您忘记了,请查看第五部分( VM-Exit Handler ),它完全相同,但由于各种退出原因而具有不同的操作。

我们需要管理的第一件事是检测在VMX non-root操作中执行的每条VMX指令; 可以使用以下代码来完成:

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
//
// 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:
{
    // DbgBreakPoint();
 
    /*  DbgPrint("\n [*] Target guest tries to execute VM Instruction ,"
            "it probably causes a fatal error or system halt as the system might"
            " think it has VMX feature enabled while it's not available due to our use of hypervisor.\n");
            */
 
    ULONG RFLAGS = 0;
    __vmx_vmread(GUEST_RFLAGS, &RFLAGS);
    __vmx_vmwrite(GUEST_RFLAGS, RFLAGS | 0x1); // cf=1 indicate vm instructions fail
    break;
}

正如我在 DbgPrint 中告诉您的,执行这些类型的 VMX 指令最终会导致 BSOD,因为在我们的虚拟机管理程序到来之前可能会检查虚拟机管理程序是否存在。 因此,执行这些指令的例程(当然,它来自内核)可能认为它可以执行这些指令。 如果它没有很好地管理它们(这很常见),您将看到 BSOD。 因此,您必须找出调用此类指令的原因并手动禁用它们。

如果您配置了任何基于 CPU 的控件,或者您的处理器支持任何 CR 访问退出控件的 1 设置,则可以使用以下 VM-exit来管理它们。

1
2
3
4
5
case EXIT_REASON_CR_ACCESS:
{
    HandleControlRegisterAccess(GuestRegs);
    break;
}

MSR 访问也是如此。 如果我们没有设置任何 MSR 位,每个 RDMSRWRMSR 都会导致退出,或者如果我们设置了任何位 MsrBitmap,那么我们必须使用以下 RDMSR 函数来管理它们:

1
2
3
4
5
6
7
8
9
case EXIT_REASON_MSR_READ:
{
    ULONG ECX = GuestRegs->rcx & 0xffffffff;
 
    // DbgPrint("[*] RDMSR (based on bitmap) : 0x%llx\n", ECX);
    HandleMSRRead(GuestRegs);
 
    break;
}

管理WRMSR 的代码:

1
2
3
4
5
6
7
8
9
case EXIT_REASON_MSR_WRITE:
{
    ULONG ECX = GuestRegs->rcx & 0xffffffff;
 
    DbgPrint("[*] WRMSR (based on bitmap) : 0x%llx\n", ECX);
    HandleMSRWrite(GuestRegs);
 
    break;
}

如果你想检测I/O指令的执行情况,那么:

1
2
3
4
5
6
7
8
9
10
case EXIT_REASON_IO_INSTRUCTION:
{
    UINT64 RIP = 0;
    __vmx_vmread(GUEST_RIP, &RIP);
 
    DbgPrint("[*] RIP executed IO instruction : 0x%llx\n", RIP);
    DbgBreakPoint();
 
    break;
}

如果您想使用上述功能,请不要忘记设置足够的基于 CPU 的控制字段。

对我们来说最后重要的是 CPUID 处理程序。 它调用HandleCPUID(如上所述),如果结果为true,则保存GUEST_RSP和GUEST_RIP,以便这些值可以用于恢复目标核心中执行VMXOFF后的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
case EXIT_REASON_CPUID:
{
    Status = HandleCPUID(GuestRegs); // Detect whether we have to turn off VMX or Not
    if (Status)
    {
        // We have to save GUEST_RIP & GUEST_RSP somewhere to restore them directly
 
        ULONG ExitInstructionLength = 0;
        g_GuestRIP                  = 0;
        g_GuestRSP                  = 0;
        __vmx_vmread(GUEST_RIP, &g_GuestRIP);
        __vmx_vmread(GUEST_RSP, &g_GuestRSP);
        __vmx_vmread(VM_EXIT_INSTRUCTION_LEN, &ExitInstructionLength);
 
        g_GuestRIP += ExitInstructionLength;
    }
    break;
}

6.10.让我门测试一下

现在是时候测试我们的虚拟机管理程序了。

6.10.1.虚拟化所有核心

首先,我们必须加载我们的驱动程序。

图片描述
然后我们的 DriverEntry被调用,所以我们必须运行用户模式应用程序来虚拟化所有核心。
图片描述
你可以看到,如果你按任意键或关闭这个窗口,它会调用 DrvClose并恢复状态( VMXOFF )。
图片描述
上图为驱动日志。 此时,所有核心均位于虚拟机管理程序之下。

6.10.2.使用虚拟机管理程序更改CPUID

现在让我们测试虚拟机管理程序是否存在。 对于本例,我使用 Immunity Debugger 通过自定义 EAX 执行 CPUID。 您可以使用任何其他调试器或任何自定义应用程序。
图片描述
我们必须手动将 EAX 设置为 0x40000001HYPERV_CPUID_INTERFACE然后执行 CPUID
图片描述
上图显示了没有虚拟机管理程序的 HYPERV_CPUID_INTERFACE。

最后,我们必须关闭用户模式应用程序窗口,因此它在所有内核上执行 VMXOFF。 让我们再次测试一下上面的例子。
图片描述
您可以看到实际结果已经出现,因为我们不再处于虚拟机管理程序之下。

6.10.3.检测MSR读写(MSR位图)

为了测试 MSR 位图,我创建了一个本地内核调试器(使用 WinDbg)。 在WinDbg中,我们可以执行rdmsr和wrmsr命令来读写MSR。 这与使用系统驱动程序执行 RDMSR 和 WRMSR 完全相同。

在 VirtualizeCurrentSystem 函数中,添加以下行。

1
SetMSRBitmap(0xc0000082, ProcessorID, TRUE, TRUE);

图片描述

在WinDbg本地调试器中,我们执行了上述命令,在远程调试器中,我们可以看到结果如下,
图片描述
可以看到,检测到RDMSR的执行。 我们的虚拟机管理程序运行完美!

就是这样,伙计们。

6.11.结论

在这一部分中,我们了解了如何通过为每个逻辑核心单独配置 VMCS 字段来虚拟化已运行的系统。 然后,我们使用虚拟机管理程序更改 CPUID 指令的结果并监视对控制寄存器或 MSR 的每次访问。 在这部分之后,我们的虚拟机管理程序几乎准备好用于实际项目了。 后续部分是关于使用扩展页表(如前面第 4 部分所述)。 我相信虚拟机管理程序中的大多数令人兴奋的工作都可以使用 EPT 来执行,因为它具有特殊的日志记录机制,例如页面读/写访问检测以及您将在下一部分中看到的许多其他很酷的东西。

下一部分见。

6.12.参考

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

[2] cpu-internals ( https://github.com/LordNoteworthy/cpu-internals )

[3] RDTSCP — 读取时间戳计数器和处理器 ID ( https://www.felixcloutier.com/x86/rdtscp )

[4] INVPCID — 无效进程上下文标识符 ( https://www.felixcloutier.com/x86/invpcid )

[5] XSAVE — 保存处理器扩展状态 ( https://www.felixcloutier.com/x86/xsave )

[6] XRSTORS — 恢复处理器扩展状态监控器 ( https://www.felixcloutier.com/x86/xrstors )

[7]什么是IRQL? ( https://blogs.msdn.microsoft.com/doronh/2010/02/02/what-is-irql/


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

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