一
基本概念和配置测试环境
欢迎来到名为“ Hypervisor From Scratch”的多部分系列教程的第一部分。顾名思义,本课程包含创建基于硬件虚拟化的基本虚拟机的技术细节。如果您遵循本教程,您将能够创建自己的虚拟环境,并了解 VMWare、VirtualBox、KVM 和其他虚拟化软件如何使用处理器的设施来创建虚拟环境。此外,您还可以了解HyperDbg调试器(https://github.com/HyperDbg/HyperDbg)的“VMM”模块内部是如何工作的。
本教程的完整源代码可在 GitHub 上获取:
[https://github.com/SinaKarvandi/Hypervisor-From-Scratch]
为了设置嵌套虚拟化环境,请确保在虚拟机中启用以下功能:
我们将在本系列的其余部分中经常使用一些关键字,您应该了解它们(大多数定义源自英特尔软件开发人员手册,第 3C 卷)。
VMCS 由六个逻辑组组成:
◆guest-state区域:在 VM-entry时,处理器的状态信息从guest-state区域中加载。在VM-exit时,处理器的当前状态信息保存在guest-state区域。
◆host-state区域:在VM-exit时,处理器的状态信息从host-state区域中加载。
◆VM-execution 控制区域:在进入VM后,处理器的行为由VM-execution控制区域中的字段提供控制。例如,可以设置某些条件使得在guest执行中产生 VM-exit.。
◆VM-exit控制区域:控制处理器在处理VM-exit时的行为,也影响返回VMM后处理器的某些状态。
◆VM-entry控制区域:控制处理器在处理VM-entry时的行为,也决定进入VM后处理器的某些状态。
◆VM-exit信息区域:记录引起VM-exit事件的原因及相关的明细信息。也可以记录VMX指令执行失败后的错误编号。
我发现了一篇很好的作品来说明 VMCS。
不用担心这些字段,我将在后面的部分中清楚地解释其中的大部分内容。请记住,VMCS 结构因处理器的不同版本而异。
VMX引入了一下新指令:
以下几项总结了 VMM 及其guest软件的生命周期以及它们之间的交互:
现在就够了!
其他与虚拟机管理程序相关的作品和材料。
Awesome virtualization(介绍书籍、论文、项目、课程、CVE 和其他与虚拟机管理程序相关的作品)-https://github.com/Wenzel/awesome-virtualization
7 天虚拟化:虚拟机管理程序开发系列 - (https://revers.engineering/7-days-to-virtualization-a-series-on-hypervisor-development/)
最后,如果您想使用虚拟机管理程序进行调试、研究或逆向工程,您可以使用HyperDbg Debugger,因为该调试器中实现了许多基于虚拟机管理程序的创新方法,这将为您的逆向之旅提供帮助。
在这一部分中,我们研究了我们应该注意的一般关键字,并为我们将来的测试创建了一个简单的测试环境。在下一部分中,我将解释如何使用我们上面制作的驱动程序在您的计算机上启用 VMX。然后我们将调查虚拟化的其余部分,所以我们下一部分见。
二
进入VMX操作
这是名为“ Hypervisor From Scratch”的多个系列教程的第二部分。在这一部分中,我们将讨论 WDK 驱动程序并最终开始启用 VT-x。
在本节中,我们将了解检测处理器对虚拟机管理程序的支持,然后我们简单地配置基本操作以启用 VMX、进入 VMX 操作,然后我们将了解有关Window Driver Kit (WDK) 的更多信息。
除了我们的内核模式驱动程序(“MyHypervisorDriver ”)之外,我们还将创建一个名为“MyHypervisorApp”的用户模式应用程序。首先,我应该鼓励您在用户模式而不是内核模式下编写大部分代码(只要可能),这是因为您可能没有正确处理异常。因此,它会导致 BSOD,或者另一方面,在内核模式下运行较少的代码会减少出现一些令人讨厌的内核模式错误的可能性。
如果您还记得上一部分,我们创建了一个 Windows 驱动程序。现在我们想要扩展我们的项目以支持更多 IRP 主要功能。
IRP 主要功能位于为每个设备创建的传统 Windows 表中。一旦我们在 Windows 中注册了一个设备,我们就必须为这些 IRP 主要函数引入一个处理程序。
这就像每个设备都有一个主要功能表。每当用户模式应用程序调用任何这些函数时,Windows 都会找到相应的函数(如果设备驱动程序支持该 MJ 函数),然后将 IRP 传递给内核驱动程序。
那么,什么是IRP?IRP 是表示 I/O 请求数据包的结构。该数据包包含有关其调用者、参数、数据包状态等的许多详细信息。我们从 IRP 数据包中提取调用者参数。
现在,我们可以根据 IRP 提供的详细信息在内核中处理用户模式请求。
请记住,当内核驱动程序中的函数接收到 IRP 数据包时,我们的代码有责任调查调用者并检查其权限等。
注册设备后(前面已经介绍过),我们需要介绍设备的主要功能。
以下代码负责配置不同的 IRP MJ 函数并引入自定义内核模式函数作为 IRP 处理程序。
if (NtStatus == STATUS_SUCCESS)
{
for (Index = 0; Index < IRP_MJ_MAXIMUM_FUNCTION; Index++)
{
DriverObject->MajorFunction[Index] = DrvUnsupported;
}DbgPrint("[*] Setting Devices major functions.");
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DrvClose;
DriverObject->MajorFunction[IRP_MJ_CREATE] = DrvCreate;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DrvIoctlDispatcher;DriverObject->MajorFunction[IRP_MJ_READ] = DrvRead;
DriverObject->MajorFunction[IRP_MJ_WRITE] = DrvWrite;DriverObject->DriverUnload = DrvUnload;
IoCreateSymbolicLink(&DosDeviceName, &DriverName);
}
else
{
DbgPrint("[*] There were some errors in creating device.");
}
您可以看到我们对所有功能都使用了“DrvUnsupported”。该函数处理所有 MJ 函数并告诉用户它不受支持。
“ DrvUnsupported”的主体是这样的:
NTSTATUS
DrvUnsupported(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
DbgPrint("[*] This function is not supported :( !");Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);return STATUS_SUCCESS;
}
我们还介绍了我们设备所必需的其他主要功能。我们将在以后的部分中完成其中一些 MJ 函数的实现。
NTSTATUS
DrvRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
DbgPrint("[*] Not implemented yet :( !");Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);return STATUS_SUCCESS;
}NTSTATUS
DrvWrite(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
DbgPrint("[*] Not implemented yet :( !");Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);return STATUS_SUCCESS;
}NTSTATUS
DrvClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
DbgPrint("[*] Not implemented yet :( !");Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);return STATUS_SUCCESS;
}
现在让我们看看 IRP MJ 函数列表和其他类型的 Windows 驱动程序工具包处理程序例程。
我们可以使用此 IRP 主要函数列表在 WDK 驱动程序中执行不同的操作。
#define IRP_MJ_CREATE 0x00
#define IRP_MJ_CREATE_NAMED_PIPE 0x01
#define IRP_MJ_CLOSE 0x02
#define IRP_MJ_READ 0x03
#define IRP_MJ_WRITE 0x04
#define IRP_MJ_QUERY_INFORMATION 0x05
#define IRP_MJ_SET_INFORMATION 0x06
#define IRP_MJ_QUERY_EA 0x07
#define IRP_MJ_SET_EA 0x08
#define IRP_MJ_FLUSH_BUFFERS 0x09
#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a
#define IRP_MJ_SET_VOLUME_INFORMATION 0x0b
#define IRP_MJ_DIRECTORY_CONTROL 0x0c
#define IRP_MJ_FILE_SYSTEM_CONTROL 0x0d
#define IRP_MJ_DEVICE_CONTROL 0x0e
#define IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f
#define IRP_MJ_SHUTDOWN 0x10
#define IRP_MJ_LOCK_CONTROL 0x11
#define IRP_MJ_CLEANUP 0x12
#define IRP_MJ_CREATE_MAILSLOT 0x13
#define IRP_MJ_QUERY_SECURITY 0x14
#define IRP_MJ_SET_SECURITY 0x15
#define IRP_MJ_POWER 0x16
#define IRP_MJ_SYSTEM_CONTROL 0x17
#define IRP_MJ_DEVICE_CHANGE 0x18
#define IRP_MJ_QUERY_QUOTA 0x19
#define IRP_MJ_SET_QUOTA 0x1a
#define IRP_MJ_PNP 0x1b
#define IRP_MJ_PNP_POWER IRP_MJ_PNP // Obsolete....
#define IRP_MJ_MAXIMUM_FUNCTION 0x1b
每个主要函数只有在我们从用户模式调用其相应函数时才会触发。例如,有一个名为CreateFile的函数(在用户模式下) (及其所有变体,例如用于ASCII和Unicode 的CreateFileA和CreateFileW),因此每次我们调用CreateFile时,都会调用注册为IRP_MJ_CREATE的函数 ,如果我们调用ReadFile然后IRP_MJ_READ或WriteFile然后IRP_MJ_WRITE将被触发。
您可以看到 Windows 将其设备视为文件,并且我们需要从用户模式传递到内核模式的所有内容都可以在带有IRP *
类型的参数中使用,并且可以用作内核 IRP MJ 函数处理程序的缓冲区。Windows 负责将用户模式缓冲区复制到内核模式堆栈。
不用担心;我们在项目的其余部分中经常使用它,但我们仅在这部分中支持IRP_MJ_CREATE,而其他部分则在将来的部分中实现。
还有其他术语称为“IRP 次要函数”。我们保留了这些功能,因为本系列中未使用它们。
在“regedit.exe”中,添加一个键:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter
在其下添加一个名为 IHVDRIVER 且值为 0xFFFF 的 DWORD 值。
在启用VT-x之前,我们首先应该考虑发现对VMX的支持。英特尔软件开发人员手册第 3C卷第 23.6 节**“发现对 VMX 的支持”**对此进行了介绍。
如果CPUID.1:ECX.VMX[bit 5] = 1,则支持 VMX 操作,您可以使用CPUID知道 VMX 的存在。
首先,我们需要知道我们是否在基于英特尔的处理器上运行。我们可以使用CPUID
指令并找到供应商字符串“GenuineIntel”来理解这一点。
以下函数使用指令返回供应商字符串CPUID
。
std::string
GetCpuID()
{
// Initialize used variables
char SysType[13]; // Array consisting of 13 single bytes/characters
string CpuID; // The string that will be used to add all the characters to
// Starting coding in assembly language
_asm
{
// Execute CPUID with EAX = 0 to get the CPU producer
XOR EAX, EAX
CPUID
// MOV EBX to EAX and get the characters one by one by using shift out right bitwise operation.
MOV EAX, EBX
MOV SysType[0], AL
MOV SysType[1], AH
SHR EAX, 16
MOV SysType[2], AL
MOV SysType[3], AH
// Get the second part the same way but these values are stored in EDX
MOV EAX, EDX
MOV SysType[4], AL
MOV SysType[5], AH
SHR EAX, 16
MOV SysType[6], AL
MOV SysType[7], AH
// Get the third part
MOV EAX, ECX
MOV SysType[8], AL
MOV SysType[9], AH
SHR EAX, 16
MOV SysType[10], AL
MOV SysType[11], AH
MOV SysType[12], 00
}
CpuID.assign(SysType, 12);
return CpuID;
}
最后一步是检查VMX是否存在 。我们可以使用以下代码来检查它:
bool
DetectVmxSupport()
{
bool VMX = false;
__asm {
XOR EAX, EAX
INC EAX
CPUID
BT ECX, 0x5
JC VMXSupport
VMXNotSupport :
JMP NopInstr
VMXSupport :
MOV VMX, 0x1
NopInstr :
NOP
}return VMX;
}
正如你所看到的,它检查CPUID.1
,如果第bit 5位为1,则支持VMX操作。我们也可以在内核驱动程序中执行相同的操作。
总而言之,我们检测VMX支持的主要代码应该是这样的:
int
main()
{
std::string CpuId;PrintAppearance();
CpuId = GetCpuID();
printf("[*] The CPU Vendor is : %s \n", CpuID.c_str());
if (CpuId == "GenuineIntel")
{
printf("[*] The Processor virtualization technology is VT-x. \n");
}
else
{
printf("[*] This program is not designed to run in a non-VT-x environment !\n");
return 1;
}if (DetectVmxSupport())
{
printf("[*] VMX Operation is supported by your processor .\n");
}
else
{
printf("[*] VMX Operation is not supported by your processor .\n");
return 1;
}HANDLE hWnd = CreateFile(L"\\\\.\\MyHypervisorDevice",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ |
FILE_SHARE_WRITE,
NULL, /// lpSecurityAttirbutes
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL |
FILE_FLAG_OVERLAPPED,
NULL); /// lpTemplateFile_getch();
return 0;
}
最终结果:
如果处理器支持 VMX 操作,则需要启用它。正如我上面告诉您的,IRP_MJ_CREATE是应该用于启动操作的第一个函数。
在进入VMX操作之前,我们应该通过设置CR4.VMXE[bit 13] = 1来使能VMX。然后通过执行VMXON指令进入VMX操作。如果在 CR4.VMXE = 0 的情况下执行,VMXON 会导致无效操作码异常 (#UD)。一旦进入 VMX 操作,就无法清除 CR4.VMXE。
之后,我们可以通过执行VMXOFF指令来退出VMX操作,此时CR4.VMXE可以被清零。
VMXON 还由 IA32_FEATURE_CONTROL MSR(MSR 地址 3AH)控制。当逻辑处理器复位时,该 MSR 被清零。
让我们看看这个 MSR 的第一部分:
◆位 0 是锁定位。如果该位清零,VMXON 会导致一般保护 (#GP) 异常。如果锁定位被设置,则对该 MSR 的 WRMSR 会导致一般保护异常;在上电复位之前,MSR 无法修改。
这是什么意思?这意味着我们可以禁用 VMX 功能,而无法再次启用。只有系统重置后,我们才能启用VMX。
系统 BIOS 可以使用该位为 BIOS 提供设置选项以禁用对 VMX 的支持。要在平台中启用 VMX 支持,BIOS 必须设置位 1、位 2 或两者,以及锁定位。
现在我们应该创建一些函数来在汇编中执行此操作。
只需在头文件(在我的例子中Source.h)中声明您的函数:
extern void inline AsmEnableVmxOperation(void);
然后在汇编文件(在我的例子中为“SourceAsm.asm”)中,添加此函数(设置 CR4 的bit 13)。
AsmEnableVmxOperation PROC PUBLIC
PUSH RAX ; Save the state
XOR RAX, RAX ; Clear the RAX
MOV RAX, CR4OR RAX,02000h ; Set the 14th bit
MOV CR4, RAXPOP RAX ; Restore the state
RETAsmEnableVmxOperation ENDP
另外,在 SourceAsm.asm 的上面声明您的函数。
PUBLIC AsmEnableVmxOperation
此汇编函数应在DrvCreate中调用 :
NTSTATUS
DrvCreate(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
//
// Enabling VMX Operation
//
AsmEnableVmxOperation();
DbgPrint("[*] VMX Operation Enabled Successfully !");Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);return STATUS_SUCCESS;
}
最后,我们应该从用户模式调用以下函数:
HANDLE hWnd = CreateFile(L"\\\\.\\MyHypervisorDevice",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ |
FILE_SHARE_WRITE,
NULL, /// lpSecurityAttirbutes
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL |
FILE_FLAG_OVERLAPPED,
NULL); /// lpTemplateFile
如果您看到以下结果,则您已成功完成第二部分。
在这一部分中,我们了解了创建 Windows Driver Kit 程序所需的基本知识,然后进入虚拟环境为其余部分构建基石。在第三部分中,我们将深入了解 Intel VT-x,并使我们的驱动程序更加先进。
注意:请记住,虚拟机管理程序会随着时间的推移而发生变化,因为新功能会添加到操作系统中或使用新技术。例如,Meltdown & Spectre 的更新对虚拟机管理程序进行了大量更改,因此,如果您想在项目、研究或其他任何用途中使用虚拟机管理程序 From Scratch,则必须使用这些教程系列最新部分中的驱动程序由于本教程正在积极更新,并且更改已应用于较新的部分(较早的部分保持不变),因此您可能会在较早的部分中遇到错误和不稳定问题,因此请确保在实际项目中使用最新的部分。
看雪ID:zhang_derek
https://bbs.kanxue.com/user-home-939298.htm
# 往期推荐
2、.NET 恶意软件 101:分析 .NET 可执行文件结构
球分享
球点赞
球在看
点击阅读原文查看更多