从头开始了解和使用Hypervisor(第7部分)
导语:毫不夸张地说,学习完本文,你完全可以创建自己的虚拟环境,并且可以了解VMWare,VirtualBox,KVM和其他虚拟化软件如何使用处理器的函数来创建虚拟环境。
MSR位图
这里的一切都取决于你是否设置了基于控制的第28位主处理器,在支持“使用MSR位图”vm执行控件1-设置的处理器上,vm执行控件字段包括4个连续MSR位图的64位物理地址,每个位图的大小为1-KByte。不支持该控件的1-设置的处理器上不存在此字段。
在Intel SDM中,MSR位图的定义非常清楚,因此我只是从原始手册中复制了它们,在阅读它们之后,我们将开始实现并将其放入hypervisor中。
1.读取低MSR位图(位于MSR位图地址),对于每个MSR地址,该位包含一个位,范围为00000000H至00001FFFH。该位确定执行应用于该M00000000HSR的RDMSR是否导致VM退出。
2.读取高MSR的位图(位于MSR位图地址要再加上1024),对于C0000000H toC0001FFFH范围内的每个MSR地址,它包含一个位,该位决定应用于该MSR的RDMSR的执行是否会导致VM退出。
3.为低MSR写入位图(位于MSR位图地址加2048),对于每个MSR地址,该位包含一个位,范围为00000000H至00001FFFH,该位确定执行应用于该MSR的WRMSR是否导致VM退出。
4. 为高MSR写入位图(位于MSR位图地址加3072),对于C0000000H toC0001FFFH范围内的每个MSR地址,它包含一个位,这个位决定了WRMSR的执行是否会导致VM退出。
好的,让我们执行上面的语句,如果RDMSR或WRMSR中的任何一个导致VM退出,那么我们必须手动执行RDMSR或WRMSR并将结果设置到寄存器中,因此,我们具有管理RDMSR的函数,例如:
处理MSR读取
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 rcx rcx); } else { msr.Content = 0; } GuestRegs->rax = msr.Low; GuestRegs->rdx = msr.High; }
你会看到,它只是检查MSR的完整性,然后执行RDMSR,最后将结果放入RAX和RDX中(因为非虚拟化的RDMSR会执行相同的操作)。
处理MSR写入
还有另一个用于处理WRMSR VM-Exit的函数:
void HandleMSRWrite(PGUEST_REGS GuestRegs) { MSR msr = { 0 }; // Check for sanity of MSR if ((GuestRegs->rcx rcx rax; msr.High = (ULONG)GuestRegs->rdx; MSRWrite((ULONG)GuestRegs->rcx, msr.Content); } }
该函数的功能很简单,到目前为止,你可能应该已经了解到,所有挂钩的RDMSR和WRMSR都应最终调用这些函数,但你真正值得尝试的一件事是避免在CPU_BASED_VM_EXEC_CONTROL中设置CPU_BASED_ACTIVATE_MSR_BITMAP,你将看到所有MSR读取和修改的过程将导致VM退出,其原因如下:
EXIT_REASON_MSR_READ EXIT_REASON_MSR_WRITE
此时,你必须将所有内容传递给上述函数并记录这些VM-Exit,以便可以看到Windows在hypervisor中运行时使用的MSR,但是正如我在上文中告诉你的那样,Windows执行了大量的MSR指令,因此它会使你的系统速度变慢。
好的,让我们回到MSR位图,我们需要两个函数来设置MSR位图的位,
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); } }
用于检索特殊位的另一个函数:
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),则在MSRBitmap虚拟地址中设置相应的位,如果是低MSR的写入(wrmsr),则修改MSRBitmap + 2048(如Intel手册中所述)并精确对于高MSR(在0xC0000000和0xC0001FFF之间),情况也是一样,但不要忘记删减(0xC0000000),因为0xC000nnnn不是有效位:d。
BOOLEAN SetMSRBitmap(ULONG64 msr, int ProcessID, BOOLEAN ReadDetection, BOOLEAN WriteDetection) { if (!ReadDetection && !WriteDetection) { // Invalid Command return FALSE; } if (msr <= 0x00001FFF) { if (ReadDetection) { SetBit(vmState[ProcessID].MSRBitMap, msr, TRUE); } if (WriteDetection) { SetBit(vmState[ProcessID].MSRBitMap + 2048, msr, TRUE); } } else if ((0xC0000000 <= msr) && (msr <= 0xC0001FFF)) { if (ReadDetection) { SetBit(vmState[ProcessID].MSRBitMap + 1024, msr - 0xC0000000, TRUE); } if (WriteDetection) { SetBit(vmState[ProcessID].MSRBitMap + 3072, msr - 0xC0000000, TRUE); } } else { return FALSE; } return TRUE; }
还要记住一件事,只有上述MSR范围当前在Intel处理器中有效,因此,即使任何其他RDMSR和WRMSR也会导致VM退出,但是此处的完整性检查是强制性的,因为客户可能会发送无效的MSR并导致整个系统崩溃(在VMX根模式下)!!!
关闭VMX并退出Hypervisor
现在该关闭我们的hypervisor,并将处理器状态重启为运行hypervisor之前的状态。
就像我们如何进入hypervisor(VMLAUNCH)的方式一样,我们必须将C函数与Assembly例程结合起来以保存状态,然后执行VMXOFF并释放所有先前分配的池,最后重启状态。
该例程的VMXOFF部分应在VMX Root操作中执行,你不能仅在其中一个驱动程序函数中执行__vmx_vmxoff,否则它会因为Windows关闭hypervisor,并且其所有驱动程序当前都在非root用户的VMX中执行其中的VMX指令就像VM-Exit,其原因之一是。
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
要关闭hypervisor,最好使用IRP主要函数,在本例中,我们使用DrvClose,因为它总是在关闭设备的句柄时得到通知,如果你还记得上面的内容,则可以从设备中创建句柄使用CreateFile(DrvCreate),现在是时候使用DrvClose关闭句柄了。
NTSTATUS DrvClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { DbgPrint("[*] DrvClose Called !\n"); // executing VMXOFF (From CPUID) on every logical processor Terminate_VMX(); Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; }
上面的函数没什么特别的,只是Terminate_VMX()。
该函数类似于执行VMLAUNCH的例程,只是它执行的是VMXOFF。
void Terminate_VMX(void) { 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(PhysicalAddress_to_VirtualAddress(vmState[i].VMXON_REGION)); MmFreeContiguousMemory(PhysicalAddress_to_VirtualAddress(vmState[i].VMCS_REGION)); MmFreeContiguousMemory(vmState[i].VMM_Stack); MmFreeContiguousMemory(vmState[i].MSRBitMap); } DbgPrint("[*] VMX Operation turned off successfully. \n"); }
如你所见,它在所有正在运行的逻辑内核上执行RunOnProcessorForTerminateVMX,然后使用MmFreeContiguousMemory为VMXON_REGION,VMCS_REGION,VMM_Stack和MSRBitMap释放分配的缓冲区。当然,在需要时将物理设备转换为虚拟设备。
请注意,如果你虚拟化了部分内核(不是全部),则必须自己修改此函数。
正如我告诉你的那样,在RunOnProcessorForTerminateVMX中,我们必须告诉我们的VMX根操作有关关闭hypervisor的信息,这是因为我们无法在此处执行任何VMX指令。很显然,如果没有任何机制来处理这种情况,VMX根操作可以阻止我们这个操作。
你可以使用多种方法来告知VMX根操作有关VMXOFF的信息,但在本例中,我使用了CPUID。
现在,你绝对知道执行CPUID会导致VM退出,现在在我们的CPUID退出处理程序例程中,我们管理每当执行RAX = 0x41414141和RCX = 0x42424242的CPUID时,你都必须返回true,并向调用者显示hypervisor需要关闭。
if ((state->rax == 0x41414141) && (state->rcx == 0x42424242) && Mode == DPL_SYSTEM) { return TRUE; // Indicates we have to turn off VMX }
还有另一种DPL检查:
ULONG Mode = 0; __vmx_vmread(GUEST_CS_SELECTOR, &Mode); Mode = Mode & RPL_MASK;
此检查确保具有RAX = 0x41414141和RCX = 0x42424242的CPUID在系统权限级别(内核模式)下执行,因此没有用户模式应用程序能够执行此任务。
即使执行了此检查,但没有此检查也不意味着用户模式应用程序可以关闭hypervisor,这是因为我们没有将CR3更改为目标用户模式进程,也不将当前权限级别更改为用户模式,因此,如果你希望用户模式应用程序能够执行此任务,则必须考虑这些情况。
现在,我们的RunOnProcessorForTerminateVMX在所有内核上分别执行CPUID。
BOOLEAN RunOnProcessorForTerminateVMX(ULONG ProcessorNumber) { KIRQL OldIrql; KeSetSystemAffinityThread((KAFFINITY)(1 << ProcessorNumber)); OldIrql = KeRaiseIrqlToDpcLevel(); // Our routine is VMXOFF INT32 cpu_info[4]; __cpuidex(cpu_info, 0x41414141, 0x42424242); KeLowerIrql(OldIrql); KeRevertToUserAffinityThread(); return TRUE; }
在我们的EXIT_REASON_CPUID中,我们知道如果处理程序返回true,则必须将其关闭,因此你应该考虑其他事项。例如,Windows希望每当VM退出处理程序返回时都运行客户_RIP和GUEST_RSP,因此我们必须将它们保存在某些位置,并在以后使用它们还原Windows状态。
我们还必须增加GUEST_RIP,因为我们想在CPUID之后恢复状态。
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; gGuestRIP = 0; gGuestRSP = 0; __vmx_vmread(GUEST_RIP, &gGuestRIP); __vmx_vmread(GUEST_RSP, &gGuestRSP); __vmx_vmread(VM_EXIT_INSTRUCTION_LEN, &ExitInstructionLength); gGuestRIP += ExitInstructionLength; } break; }
MainVMExitHandler又被称为VMExitHandler(来自VMExitHandler.asm的组装函数),让我们详细了解一下。
首先,我们得把以前定义的一些变量代入。
EXTERN gGuestRIP:QWORD EXTERN gGuestRSP:QWORD
现在我们的VMExitHandler像这样工作,每当发生VM退出时,我们的逻辑内核都会按照HOST_RIP中定义的方式执行VMExitHandler,并且我们的RSP设置为HOST_RSP,那么我们必须保存所有寄存器,这意味着我们必须为此创建一个结构目的,使我们能够以类似C结构读取和修改寄存器。
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),然后删减一些阴影空间。
VMExitHandler如下所示:
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 VM_Resumer VMExitHandler ENDP
从上面的代码中,当我们从MainVMExitHandler返回时,我们必须检查MainVMExitHandler的返回结果(在RAX中)是否告诉我们关闭hypervisor还是继续执行hypervisor。
如果需要继续,则重启寄存器状态并跳转到我们的VM_Resumer函数。
VM_Resumer只执行_vmx_vmresume,处理器把RIP设为GUEST_RIP。
VOID VM_Resumer(VOID) { __vmx_vmresume(); // if VMRESUME succeed 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(); }
但是,如果需要将其关闭呢?
然后基于AL,它跳转到另一个名为VMXOFFHandler的函数。这是一个简单的函数,它执行VMXOFF并关闭hypervisor(在当前的逻辑内核中),然后将寄存器重启到它们先前的状态,就像我们将它们保存在VMExitHandler中一样。
我们唯一要做的就是将堆栈指针更改为GUEST_RSP(我们保存在gGuestRSP里),然后跳转到GUEST_RIP(保存在gGuestRIP里)。
VMXOFFHandler PROC ; Turn VMXOFF VMXOFF ;INT 3 ;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, gGuestRSP JMP gGuestRIP VMXOFFHandler ENDP
现在一切都完成了,我们执行了常规的Windows(驱动程序)例程,我的意思是从RunOnProcessorForTerminateVMX执行的最后一个CPUID之后开始执行,但是现在我们不在VMX操作中。
VM退出处理程序
将以上所有代码放在一起,现在我们必须管理不同类型的VM-Exit,因此我们需要修改前面说明的MainVMExitHandler。
我们需要管理的第一件事是检测在VMX非根操作中执行的每条VMX指令,这可以使用以下代码完成:
// 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: { 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"); DbgBreakPoint(); ULONG RFLAGS = 0; __vmx_vmread(GUEST_RFLAGS, &RFLAGS); __vmx_vmwrite(GUEST_RFLAGS, RFLAGS | 0x1); // cf=1 indicate vm instructions fail break; }
正如我在DbgPrint中告诉你的那样,执行此类VMX指令最终将导致BSOD,因为在我们的hypervisor出现之前可能会对hypervisor的存在进行某些检查,因此执行这些指令的例程(当然,它来自内核)可能认为可以执行这些指令,如果对它们的管理不善(很常见),则会看到BSOD,因此必须找出调用此类指令的原因并手动将其禁用。
如果你配置了任何基于CPU的控件,或者你的处理器支持任何CR Access Exit控件的1-设置,则可以使用上文所述的函数进行管理。
case EXIT_REASON_CR_ACCESS: { HandleControlRegisterAccess(GuestRegs); break; }
如果你未设置任何MSR位(因此导致每个RDMSR和WRMSR退出),或者你在MSRBitMaps中设置了任何位,则MSR访问也是如此:那么你必须使用以下RDMSR函数来管理它们:
case EXIT_REASON_MSR_READ: { ULONG ECX = GuestRegs->rcx & 0xffffffff; DbgPrint("[*] RDMSR (based on bitmap) : 0x%llx\n", ECX); HandleMSRRead(GuestRegs); break; }
然后这段代码用于管理WRMSR:
case EXIT_REASON_MSR_WRITE: { ULONG ECX = GuestRegs->rcx & 0xffffffff; DbgPrint("[*] WRMSR (based on bitmap) : 0x%llx\n", ECX); HandleMSRWrite(GuestRegs); break; }
如果要检测I/O指令执行,则:
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 Handler,它调用HandleCPUID(如上所述),如果结果为true,则保存GUEST_RSP和GUEST_RIP,以便在我们的内核中执行VMXOFF之后可以使用这些值来恢复状态。
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; gGuestRIP = 0; gGuestRSP = 0; __vmx_vmread(GUEST_RIP, &gGuestRIP); __vmx_vmread(GUEST_RSP, &gGuestRSP); __vmx_vmread(VM_EXIT_INSTRUCTION_LEN, &ExitInstructionLength); gGuestRIP += ExitInstructionLength; } break; }
让我们测试一下!
现在是时候测试我们的hypervisor了。
虚拟化所有内核
首先,我们必须加载驱动程序。
运行驱动程序
然后调用了DriverEntry,因此我们必须运行我们的用户模式应用程序以虚拟化所有内核。
来自 Scratch App的hypervisor
你会看到,如果你按任意键或关闭此窗口,则会调用DrvClose并重启状态(VMXOFF)。
驱动日志
现在,所有内核都位于hypervisor。
使用Hypervisor更改CPUID
现在让我们测试一下hypervisor的存在,在本例中,我使用了Immunity Debugger通过自定义EAX执行CPUID。不过,你可以使用任何其他调试器或任何自定义应用程序。
将0x40000001设置为RAX
你必须将EAX手动设置为HYPERV_CPUID_INTERFACE(0x40000001),然后执行CPUID。
HVFS在RAX中
如你所见,HVFS(0x48564653)在EAX上,因此我们使用hypervisor成功挂钩了CPUID执行。
在没有hypervisor的情况下测试HYPERV_CPUID_INTERFACE
现在,你必须关闭用户模式应用程序窗口,以便在所有内核上执行VMXOFF,让我们再次测试上面的示例。
在没有hypervisor的情况下测试CPUID
此时,你可以看到原始结果出现了。
检测MSR读写(MSRBitmap)
为了测试MSR位图,我创建了一个本地内核调试器(使用Windbg)。在Windbg中,你可以执行RDMSR和WRMSR来读取和写入MSR,就像使用系统驱动程序执行RDMSR和WRMSR一样。
在我们的VirtualizeCurrentSystem函数中,添加了以下行。
SetMSRBitmap(0xc0000082, ProcessorID, TRUE, TRUE);
Windbg本地调试器(RDMSR和WRMSR)
在远程调试器系统中,你可以看到如下结果:
检测到远程内核调试器RDMSR执行!
可以看出,检测到RDMSR的执行。
总结
在这7部分中,我们看到了如何通过为每个逻辑内核分别配置VMCS字段来虚拟化已经运行的系统。然后,我们使用hypervisor来更改CPUID指令的结果,并监控对控制寄存器或MSR的每次访问,在这些操作以后我们的hypervisor准备完成,就可以使用扩展页表了。我个人认为,hypervisor中最有趣的工作都可以使用EPT来完成,因为它具有特殊的日志记录机制,例如页面读写访问检测。
发表评论