初识虚拟化
0x01 硬件虚拟化
早期的虚拟机都是进程级虚拟机,也就是作为已有操作系统的一个进程,完全通过软件的手段来模拟硬件,软件再翻译内存地址的方法实现物理机器的模拟,这样虚拟效率较低,资源利用率低。之后Intel和AMD相继推出了支持硬件虚拟化的处理器,所谓硬件虚拟化,也就是在硬件抽象层对虚拟技术提供直接支持,提高虚拟效率。我们之前提到过在进程级虚拟机中的客户机访问的物理地址需要经过软件的再次转化成真实机器的物理地址,而且也需要给不同的客户机操作系统编写不同的虚拟设备驱动程序从而可以共享同一真实的硬件资源。而硬件虚拟化则是实现了内存地址甚至与I/O设备的直接映射,无需经过再一次的转换。而硬件虚拟化技术中引出了一个重要的概念——VMM(Virtual Machine Monitor),使硬件虚拟化技术产生的一个新的特权级,用来处理虚拟硬件和真实硬件的通信和一些事件的处理,因此其系统权限在操作系统之上,产生了一个新的特权级“Ring -1”。可以简单理解为利用硬件虚拟化技术可以让我们的代码运行在操作系统之下,监视整个操作系统的运行。
Intel VIrtual Techonlogy , intel 硬件虚拟化技术 ,在硬件级别上完成计算机的虚拟化
为实现硬件虚拟化 ,VT增加了 12条新的 VMX指令:
[VMCS控制 5 条]
VMPTRLD
VMPTRST
VMCLEAR
VMREAD
VMWRITE
[VMX命令 5条]
VMLAUNCH
VMCALL
VMXON
VMXOFF
VMRESUME
[Guest software 2条]
INVEPT
INVVPID
12条指令对应的机器码(xen-3.4.1\xen\include\asm-x86\hvm\vmx\vmx.h)
代码中使用 _emit
#define VMCALL_OPCODE ".byte 0x0f,0x01,0xc1\n"
#define VMCLEAR_OPCODE ".byte 0x66,0x0f,0xc7\n" /* reg/opcode: /6 */
#define VMLAUNCH_OPCODE ".byte 0x0f,0x01,0xc2\n"
#define VMPTRLD_OPCODE ".byte 0x0f,0xc7\n" /* reg/opcode: /6 */
#define VMPTRST_OPCODE ".byte 0x0f,0xc7\n" /* reg/opcode: /7 */
#define VMREAD_OPCODE ".byte 0x0f,0x78\n"
#define VMRESUME_OPCODE ".byte 0x0f,0x01,0xc3\n"
#define VMWRITE_OPCODE ".byte 0x0f,0x79\n"
#define INVEPT_OPCODE ".byte 0x66,0x0f,0x38,0x80\n" /* m128,r64/32 */
#define INVVPID_OPCODE ".byte 0x66,0x0f,0x38,0x81\n" /* m128,r64/32 */
#define VMXOFF_OPCODE ".byte 0x0f,0x01,0xc4\n"
#define VMXON_OPCODE ".byte 0xf3,0x0f,0xc7\n"
驱动初在始化一个VMCS (Virtual Machine Control Structures)内存区域后,启动VMM (Virtual Machine Monitor) ,
得到最高权限从而管理硬件资源, 操作系统是运行于ring0下
VMM要监控管理整个系统的资源,因而VMM的权限是大于操作系统,它处于一个全新的级别ring -1
0x02 虚拟机的体系结构
Intel提供了处理器级的VMX(Virtual-Machine Extensions),从硬件层面支持VT技术。Intel VMX的体系结构可划分为两层:VMM和VM。
VMM(Virtual-Machine Monitors)作为host,具有对processor(s)和平台硬件的完全控制权限。它为guest提供VCPU(virtual processor)的抽象,并允许guest直接运行在逻辑处理器(logical processor)上。VMM具有对处理器资源、物理内存、中断和IO的选择控制的权利。(举例:Xen就是一种VMM。)
VM(Virtual-Machine)相应地作为guest,其实是提供了一种guest软件环境:它维护一个栈,其中包含了OS和application software。其每个操作都独立于其他的VM,并且使用由同一个物理平台所提供的对处理器、内存、硬盘、显卡、IO访问的统一接口。此外,这个栈并不知道VMM的存在。运行于VM中的软件其权限是受限的,这样才能保障VMM对整个平台资源的完全控制。(举相应的例子:Xen上跑的Guest OS就是VM。)
0x03 VMX operation
支持虚拟化的处理器其虚拟化相关的操作被称为VMX operation 。它分为两类:VMX root operation 和 VMX non-root operation 。通常来说,VMM 运行于 VMX root operation 而 guest 运行于 VMX non-root operation 。两种operation之间的转换被称为VMX transitions:从root到non-root被称为VM entries,而从non-root到root则称为VM exits。
处于VMX root operation的CPU其行为与在VMX operation之外是基本一样的,最根本的不同之处在于其增加了一套新的VMX指令集,且能存储到特定控制寄存器的值是有限的。而处于VMX non-root operation的CPU起行为是受限的,且经过了修改以帮助实现virtualization。与其普通的operation不同,特定的指令(包括新增的VMCALL指令)和事件将导致VM exits从而进入VMM:由于这些VM exits代替了以前正常的行为,所以在VMX non-root operation中的软件的功能是受限的(也正是这种限制保证了VMM能够始终具有控制处理器资源的能力)。
从guest的角度,没有任何一个Guest可见的位来指示一个逻辑处理器是否处于VMX non-root operation,这样VMX就能保证guest并不知道其正在运行于一个VM中。即便是CPL(current privilege level)为0,VMX operation也给guest加了限制,这样guest software就可以完全不必改变其原始的设计,这也简化了VMM的开发。
看一下VMM与Guest之间的交互了。大体的流程是这样子的:
- software执行VMXON指令进入VMX operation
- 通过VM entries,VMM就可以进入VM的guest中(VMM通过VMLANCH和VMRESUME来触发VM entry,并通过VM exits重新获得控制权)
- VM exits将控制权转移到由VMM定义的entry point(VMM可以采取适当的动作来触发VM exit,然后再使用一个VM entry就可以返回到VM中)。
- 最后,VMM通过VMXOFF指令关闭自身并退出VMX operation
0x04 VMCS(Virtual-Machine Control Structure)
VMCS(Virtual-Machine Control Structure)是一个非常重要的数据结构,这个数据结构在下文中还要非常详细地介绍。对VMCS的访问是由一组被称作VMCS pointer(每个logical processor有一个VMCS pointer)的处理器状态来管理的。VMCS pointer是一个64位的VMCS地址,可通过VMPTRST和VMPTRLD指令对其进行读写;VMM可以使用VMREAD、VMWRITE、VMCLEAR指令对VMCS进行配置。对VMM所管理的每个VM,VMM可以使用不同的VMCS;且对VM中的每个logical processor(or vcpu),VMM也可以为每个vcpu使用不同的VMCS。
VMX operation需要处理器支持,那么如何从软件层面断定一个处理器是否支持VMX operation:通过CPUID --- 如果CPUID.1:ECX.VMX[bit 5] = 1,那说明该CPU支持VMX operation。现有的VMX体系结构的设计具有良好的可扩展性,software可以使用一个VMX capability MSRs集来获得VMX新增的扩展特性。
现在再来看看如何使能和进入VMX operation:进入VMX operation之前,system software通过设置CR4.VMXE[bit 13] = 1 来使能VMX,之后就可以通过VMXON指令来进入VMX operation。如果CR4.VMXE = 0,VMXON将导致一个invalid-opcode异常(#UD),而且一旦进入VMX operation,CR4.VMXE就无法在此中被清零;system software通过VMXOFF离开VMX operation,只有在VMX operation外部CR4.VMXE才能被清零。
VMXON由IA32_FEATURE_CONTROL MSR (MSR address 3AH)所控制,当一个logical processor被rest时,该MSR被清零。此MSR的相关位如下:
- Bit 0 is the lock bit. 若该位被清零,VMXON就会触发一个general-protection exception;若该位被置1,向此MSR进行WRMSR也会触发general-protection exception,直到a power up reset condition发生时该MSR才能被修改。系统BIOS可以通过该比特位来禁用VMX,若要打开VMX支持,BIOS必须设置该MSR的bit 0, bit 1, bit 2。
- Bit 1 enables VMXON in SMX operation. 若该位被清零,SMX operation中的VMXON将触发一个general-protection exception。若在不同时支持VMX和SMX的logical processors上试图设置该位,将会触发general-protection exceptions。(若一个logical processor自从最后一次执行GETSEC[SENTER]为止GETSEC[SEXIT]还未被执行,则称其in SMX operation)
- Bit 2 enables VMXON outside SMX operation. 若该位被清零,在SMX operation外部进行VMXON将触发一个general-protection exception。在不支持VMX的logical processors上试图设置该位将触发general-protection exceptions。(若一个logical processor还未执行GETSEC[SENTER]或最后一次执行过GETSEC[SENTER]后又执行了GETSEC[SEXIT],则称其outside SMX operation)
在执行VMXON之前,software应该分配出(保留)一块4KB对齐的内存区域供logical processor用以支持VMX operation。这个内存区域就被称为VMXON region。
最后,简单说说VMX operation上的限制:VMX operation对processor operation作了一些限制,诸如,
- 在VMX operation中,处理器将对CR0和CR4的某些具体位填充固定值(CR0.PE, CR0.NE, CR0.PG, cR4.VMXE的值都必须为1)。如果这些位中任何一位的值并不是它应该的值,VMXON就会fail。在VMX operation中任何试图使用CLTS, LMSW, MOV CR等指令改变这些位的值都将导致general-protection exception。VM entry或VM exit都无法将这些位的值设置为其不应该的值。(CR0.PE和CR0.PG的限制就说明了VMX operation必须处于paged protected mode,这也使得guest software无法运行于unpaged protected mode或real-address mode中)
- 若logical processor处于A20M mode,VMXON会fail。一旦处理器进入VMX operation, A20M的中断就会被block,这样VMX operation中A20M mode当然是不可能的了。
- 只要logical processor处于VMX root operation,INIT signal就会被block;在VMX non-root operation中它是不会被block的,此时INITs将触发VM exits。
从源码级别,Xen里面的vmx.c描述的就是Intel VMX体系结构相关的VM Exits支持:对照着Intel的Manual可以找到Xen对其的具体实现。
0x05 初识VMM的建立过程
首先通过CPUID检查CPU是否支持VT
硬件环境的检测,通过指令CPUID检测CPU是否支持VT
ECX的第5个bit标志代表对 VT 的支持与否
初始化主要的内存区域
使用VMXON 进入 VMX (虚拟机指令扩展指令集)操作
执行VMXON指令 , EFLAGS.CF 可判断执行是否成功
__asm
{
PUSH 0
PUSH PhysicalVMXONRegionPtr.LowPart
_emit 0xF3 // VMXON [ESP]
_emit 0x0F
_emit 0xC7
_emit 0x34
_emit 0x24
PUSHFD
POP eFlags
ADD ESP, 8
}
接下来开始初始化VMCS区域,(相当于部署军队,准备起义)
这个结构只能够被VMCLEAR, VMPTRLD, VMREAD,和VMWRITE操作。
VMM运行后guest 执行这些指令时会引发#VMExit事件
VMCS 是一个4K的内存区域
在逻辑上,虚拟机控制结构被划分为 6 部分:
1) GUEST-STATE 域:虚拟机从根操作模式进入非根操作模式时,处理器所处的状态;
2) HOST-STATE 域:虚拟机从非根操作模式退出到根操作模式时,处理器所处的状态;
3) VM 执行控制域:虚拟机在非根操作模式运行的时候,控制处理器非根操作模式退出到根操作模式;
4) VM 退出控制域:虚拟机从非根操作模式下退出时,需要保存的信息;
5) VM 进入控制域:虚拟机从根操作模式进入非根操作模式时,需要读取的信息;
6) VM 退出信息域:虚拟机从非根操作模式退出到根操作模式时,将退出的原因保存到该域中。
在这里初始化的代码比较多,要初始化 host 与 guest 的 CR0,CR3,CR4,IDTR,GDTR,LDTR,Rflag,SYSENTER_CS,SYSENTER_EIP,SYSENTER_ESP等
初始化时使用VMWRITE 指令设置 , 设置时主要的宏是
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,
…
使用 VMWRITE 指令集成的函数
VOID WriteVMCS( ULONG encoding, ULONG value )
{
__asm
{
PUSHAD
PUSH value
MOV EAX, encoding
_emit 0x0F
_emit 0x79
_emit 0x04
_emit 0x24
POP EAX
POPAD
}
}
例如初始化 GDT , IDT 使用的是以下代码
__asm
{
SGDT gdt_reg
}
temp32 = 0;
temp32 = gdt_reg.BaseHi;
temp32 <<= 16;
temp32 |= gdt_reg.BaseLo;
Log( "Setting Host GDTR Base" , temp32 );
WriteVMCS( HOST_GDTR_BASE, temp32 );
__asm
{
SIDT idt_reg
}
temp32 = 0;
temp32 = idt_reg.BaseHi;
temp32 <<= 16;
temp32 |= idt_reg.BaseLo;
Log( "Setting Host IDTR Base" , temp32 );
WriteVMCS( HOST_IDTR_BASE, temp32 );
比较重要的是设置 #VMExit 事件处理入口 HOST_RIP
WriteVMCS( HOST_RIP, (ULONG)VMMEntryPoint ); //0x6C16
最后执行 VMLAUNCH 指令 ,正式启动虚拟机(建立了新的政权)
__asm
{
_emit 0x0F
_emit 0x01
_emit 0xC2
}
至此,整个VMM的帝国已经运行起来,帝国将会管理整个系统资源
接下来的就是#VMExit的事件循环处理(类似在调试器中下断点后,等待事件发生)
运行过程中Guest OS 遇到要监控的指令,会发生 #VMExit 事件 (相当于被调试程序执行到中断事件)
此时由VMM 处理,例如cpuid ,
处理完后由 VMRESUME 继续执行 (相当于在 OD 中 F9 继续运行)
#VMExit事件的类型
#VMExit事件分为二种
1 无条件事件CPUID GETSEC INVD XSETBV 所有 VMX指令
2 有条件事件I/O访问,中断事件, MSR寄存器访问, HTL 等 (要设置VMCS相应部分触发)
VMM使用VMREAD读取虚拟机状态,主要的一些变量有
VM_EXIT_REASON = 0x00004402, //退出代码
VM_EXIT_INTR_INFO = 0x00004404, //中断信息
VM_EXIT_INTR_ERROR_CODE = 0x00004406,
IDT_VECTORING_INFO = 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,
…
其中最重要的就是VM_EXIT_REASON , 可以看作是消息类型
主要的类型有
#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_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
…
DWORD VmxRead(DWORD in_code)
{
DWORD m_vmread = 0;
__asm
{
PUSHAD
MOV EAX, in_code
_emit 0x0F // VMREAD EBX, EAX
_emit 0x78
_emit 0xC3
MOV m_vmread, EBX
POPAD
}
return m_vmread;
}
VOID VMMReadGuestState( )
{
HandlerLogging = 0;
ExitReason = VmxRead(VM_EXIT_REASON); //0x4402
ExitInterruptionInformation = VmxRead( VM_EXIT_INTR_INFO); //0x4404
ExitInstructionLength = VmxRead(VM_EXIT_INSTRUCTION_LEN); //0x440c
ExitQualification = VmxRead(EXIT_QUALIFICATION) ; //0x6400
ExitInterruptionInformation = VmxRead(VM_EXIT_INTR_INFO); //0x4404
ExitInterruptionErrorCode = VmxRead(VM_EXIT_INTR_ERROR_CODE); //0x4406
IDTVectoringInformationField = VmxRead(IDT_VECTORING_INFO); //0X00004408 // IDT-Vectoring Information Field
IDTVectoringErrorCode = VmxRead(IDT_VECTORING_ERROR_CODE); //0X0000440A // IDT-Vectoring Error Code
ExitInstructionLength = VmxRead(VM_EXIT_INSTRUCTION_LEN); //0x0000440C // VM-Exit Instruction Length
ExitInstructionInformation = VmxRead(VMX_INSTRUCTION_INFO) ; //0x0000440E //VM-Exit Instruction Information
GuestEIP = VmxRead(GUEST_RIP); //0x0000681E; //GuestEIP
GuestESP = VmxRead(GUEST_RSP); //0x0000681c //esp
GuestCR3 = VmxRead(GUEST_CR3); //0X6802 GuestCR3
}
CPUID指令的拦截与修改
当 guest OS 执行 cupid 时,会得到这样一个错误号 10 (VMX_EXIT_CPUID )
当执行到CPUID这条指令的时候,响应,然后就可以修改cpuid的返回值
if( ExitReason == VMX_EXIT_CPUID )
{
if( GuestEAX == 0 )
{
DbgPrint("CPUID EIP == %08X \n" , GuestEIP );
//0x34EC2B
__asm
{
POPAD
MOV EAX, 0
CPUID
//修改CPUID返回值
MOV EBX, 0x80808080
MOV ECX, 0x90909090
MOV EDX, 0x10101010
JMP Resume
}
}
else
{
__asm
{
POPAD
MOV EAX, GuestEAX
CPUID
JMP Resume
}
}
}