vt技术学习
@[TOC]
介绍
VT,就是虚拟化技术(Virtualization Technology)的缩写。Intel VT就是指Intel的虚拟化技术。这种技术简单来说就是可以让一个CPU工作起来就像多个CPU并行运行,从而使得在一台电脑内可以同时运行多个操作系统。只有部分Intel的CPU才支持这种技术。vt是一个嵌套的过程,先启用vt技术的exe能获得更多的权限Intel VT技术,
主要由三部分技术组成:VTx、VTd和VTc。
VTx是处理器技术,提供内存以及虚拟机的硬件隔离,所涉及的技术有页表管理以及地址空间的保护。
VTd是处理有关芯片组的技术,VTc是针对网络提供的管理.
这里学习的是VTx技术
(中间代码见github)
环境配置
还是和之前windows内核的配置环境一样,在加上虚拟机里勾选上vt选项,并在虚拟机里安装好debugview和instdrv工具,方便驱动的加载和查看
安装wdk7600,下载后是个iso文件直接安装即可,安装好后可以在菜单栏看见命令框
http://www.microsoft.com/en-us/download/confirmation.aspx?id=11800
现在写一个简单的驱动程序,来测试环境有没有安装好
SOURCES文件
1 | TARGETNAME=vt_x86 |
entry.c 文件
1 | #include <ntddk.h> |
然后用命令框build,build成功后会在当前路径生产一个文件夹,里面有一个sys文件,拖到虚拟机里
先打开debugview,然后用驱动加载工具安装,启动,在debugview里观察到输出信息,代表环境安装成功
调试环境
在想要调试的源码上下入int 3断点
1 | __asm int 3 |
build 语法高亮 重新build后拖入虚拟机,开启双击调试环境,加载驱动,就会发现windbg断下,并且可以在源码层进行调试
1 | build /gw |
汇编支持
对于有些汇编,如果不支持的话,可以嵌入硬编码
1 | __emit 0xff |
为了可读性,可以加载vtasm.asm文件,同时在SOURCES文件加入
1 | I386_SOURCES=i386\vtasm.asm |
一些名词解释
支持检测
在SOURCES文件里加入vtsystem.c \ 开始编辑vtsystem.c
检测电脑是否支持vt技术,一共需要3个方面检查cpuid 检查msr 检查 cr0 cr4
- cpuid
检测电脑是否支持vt技术
1 把eax设置为1
2 汇编指令cpuid
3观察ecx的第五位 如果为1 则支持vt技术1
2
3
4
5
6
7
8Asm_CPUID(1, &uRet_EAX, &uRet_EBX, &uRet_ECX, &uRet_EDX);
*((PULONG)&uCPUID) = uRet_ECX;
if (uCPUID.VMX != 1)
{
Log("ERROR: 这个CPU不支持VT!",0);
return FALSE;
} - 检测寄存器
msr[3a]是否为1 则支持vt技术1
2
3
4
5
6
7
8*((PULONG)&msr) = (ULONG)Asm_ReadMsr(MSR_IA32_FEATURE_CONTROL);
if (msr.Lock != 1)
{
Log("ERROR:VT指令未被锁定!", 0);
return FALSE;
}
Log("SUCCESS:这个CPU支持VT!", 0);
return TRUE; - 检测cr0 cr4
cr4[13]为1 可以开启vt技术 这个在软件层次可以进行赋值
cr0 [0] ,cr0[ 31] 开启保护模式和开启页保护模式的位总一下就是BOOLEAN IsVTEnabled()函数的编写1
2
3
4
5
6
7
8
9
10
11
12
13
14
15*((PULONG)&uCr0) = Asm_GetCr0();
*((PULONG)&uCr4) = Asm_GetCr4();
if (uCr0.PE != 1 || uCr0.PG!=1 || uCr0.NE!=1)
{
Log("ERROR:这个CPU没有开启VT!",0);
return FALSE;
}
if (uCr4.VMXE == 1)
{
Log("ERROR:这个CPU已经开启了VT!",0);
Log("可能是别的驱动已经占用了VT,你必须关闭它后才能开启。",0);
return FALSE;
}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
48BOOLEAN IsVTEnabled()
{
ULONG uRet_EAX, uRet_ECX, uRet_EDX, uRet_EBX;
_CPUID_ECX uCPUID;
_CR0 uCr0;
_CR4 uCr4;
IA32_FEATURE_CONTROL_MSR msr;
//1. CPUID
Asm_CPUID(1, &uRet_EAX, &uRet_EBX, &uRet_ECX, &uRet_EDX);
*((PULONG)&uCPUID) = uRet_ECX;
if (uCPUID.VMX != 1)
{
Log("ERROR: 这个CPU不支持VT!",0);
return FALSE;
}
// 2. MSR
*((PULONG)&msr) = (ULONG)Asm_ReadMsr(MSR_IA32_FEATURE_CONTROL);
if (msr.Lock != 1)
{
Log("ERROR:VT指令未被锁定!", 0);
return FALSE;
}
Log("SUCCESS:这个CPU支持VT!", 0);
return TRUE;
// 3. CR0 CR4
*((PULONG)&uCr0) = Asm_GetCr0();
*((PULONG)&uCr4) = Asm_GetCr4();
if (uCr0.PE != 1 || uCr0.PG!=1 || uCr0.NE!=1)
{
Log("ERROR:这个CPU没有开启VT!",0);
return FALSE;
}
if (uCr4.VMXE == 1)
{
Log("ERROR:这个CPU已经开启了VT!",0);
Log("可能是别的驱动已经占用了VT,你必须关闭它后才能开启。",0);
return FALSE;
}
}vmxon
先看流程图
开锁—检测支持(cr4)
开柜门—vmxon
拔电源–vmclear
选中机器—vmptrload
装机—vmcs
开机–vmlaunch
拔电源–vmclear
关柜门vmxoff
关锁—软件层取消
在进入vmxon之前首先需要把cr4[13]=1,这样才可以开启vt,这步相当于开锁,而vmxon参数需要申请一块内存,用来记录host相关的信息,最多为4kb,具体大小可以通过msr[480]读取出来
1 | rdmsr 400 |
低四字节为系统的版本号,接下来的两字节为所要的大小,一般为0x1000
申请好以后,我们所拿到的是虚拟地址,而事实上我们需要的是一块物理地址,通过MmGetPhysicalAddress函数即可实现
结束后我们还需要检查cs标志位来观察是否成功
1 |
|
vmcs
vmxon申请的4k是给vmm所用的,而vmcs也要申请4k大小,是给虚拟机使用的,每有一个虚拟机,vmcs就会申请申请一个4k
在vmcs之前还有两个操作,一个是vmclear,当有虚拟机创建后,vmclear进行初始化操作,另一个是vmptrload,它是负责虚拟机的选中,当有多个虚拟机时,通过vmptrload来选中一个虚拟机,进行操作命令
1 | //vmcs申请内存 |
具体的vmcs字段,对应一个虚拟机的大部分信息,包括五个部分,我们需要用Vmx_VmWrite来分别对他们写入
1.Guest State Area
2.Host State Area
3.虚拟机运行控制域
4.VMEntry运行控制域
5.VMExit运行控制域
先说VMEntry运行控制域,设置规则为msr的后两字节如果bit位为1 必须设置为1;而高两位字节如果bit位为0 必须设置为0;其他的位可0可1,所以设置规则需要我们对高位进行与操作,对低位进行或操作
1 | static ULONG VmxAdjustControls(ULONG Ctl, ULONG Msr) |
这两个用Vmx_VmWrite写入
1 | Vmx_VmWrite(PIN_BASED_VM_EXEC_CONTROL, VmxAdjustControls(0, MSR_IA32_VMX_PINBASED_CTLS)); |
VMEntry运行控制域和VMExit运行控制域也是如此
1 | // 4.VMEntry运行控制域 |
接下来说Host State Area 它没有如上的01设置规则,类似于中断返回时宿主机应该做何操作,保存什么寄存器等
- 读取cr0 cr3 cr4到宿主机
1
2
3Vmx_VmWrite(HOST_CR0, Asm_GetCr0());
Vmx_VmWrite(HOST_CR3, Asm_GetCr3());
Vmx_VmWrite(HOST_CR4, Asm_GetCr4()); - 返回到哪里,即eip应该如何设置,栈应该怎么构造入口程序需要另外一个文件exithandler.c来构造一些列的操作,对于栈的话,需要再申请一块内存进行操作
1
2
3Vmx_VmWrite(HOST_RSP, ((ULONG)g_VMXCPU.pStack) + 0x2000); //Host 临时栈
Vmx_VmWrite(HOST_RIP, (ULONG)VMMEntryPoint); //这里定义我们的VMM处理程序入口1
2g_VMXCPU.pStack = ExAllocatePoolWithTag(NonPagedPool, 0x1000, 'stck');//申请虚拟地址
RtlZeroMemory(g_VMXCPU.pStack, 0x1000);//清0 - 读取段选择子
1
2
3
4
5
6
7Vmx_VmWrite(HOST_ES_SELECTOR, Asm_GetEs() & 0xFFF8);
Vmx_VmWrite(HOST_CS_SELECTOR, Asm_GetCs() & 0xFFF8);
Vmx_VmWrite(HOST_DS_SELECTOR, Asm_GetDs() & 0xFFF8);
Vmx_VmWrite(HOST_FS_SELECTOR, Asm_GetFs() & 0xFFF8);
Vmx_VmWrite(HOST_GS_SELECTOR, Asm_GetGs() & 0xFFF8);
Vmx_VmWrite(HOST_SS_SELECTOR, Asm_GetSs() & 0xFFF8);
Vmx_VmWrite(HOST_TR_SELECTOR, Asm_GetTr() & 0xFFF8); - fs,gs,tr,gdtr,idtr的基质
fs和gs可以通过引用的方法更新基质,tr则通过查找搜索硬编码,gdtr,idtr通过汇编指令动态获取
1 | mov ax, fs |
- KiFastCallEntry()函数的入口信息 最后是Guest State Area和host设置类似
1
2
3Vmx_VmWrite(HOST_IA32_SYSENTER_CS, Asm_ReadMsr(MSR_IA32_SYSENTER_CS) & 0xFFFFFFFF);
Vmx_VmWrite(HOST_IA32_SYSENTER_ESP, Asm_ReadMsr(MSR_IA32_SYSENTER_ESP) & 0xFFFFFFFF);
Vmx_VmWrite(HOST_IA32_SYSENTER_EIP, Asm_ReadMsr(MSR_IA32_SYSENTER_EIP) & 0xFFFFFFFF);
1.读取cr0 cr3 cr4到宿主机
1 | Vmx_VmWrite(GUEST_CR0, Asm_GetCr0()); |
- 写dr7为0x400
1
Vmx_VmWrite(GUEST_DR7, 0x400);
- 栈和eip同样的我们需要补充guestentry函数来处理客户机的东西
1
2Vmx_VmWrite(GUEST_RSP, ((ULONG)g_VMXCPU.pStack) + 0x1000); //Guest 临时栈
Vmx_VmWrite(GUEST_RIP, (ULONG)GuestEntry); // 客户机的入口点1
2
3
4void __declspec(naked) GuestEntry()
{
__asm{
} - 读取eflags,段选择子,以及他们的基质 属性,界限
1
2
3
4
5
6
7
8
9Vmx_VmWrite(GUEST_RFLAGS, Asm_GetEflags() & ~0x200);//cli关中断
Vmx_VmWrite(GUEST_ES_SELECTOR, Asm_GetEs() & 0xFFF8);
Vmx_VmWrite(GUEST_CS_SELECTOR, Asm_GetCs() & 0xFFF8);
Vmx_VmWrite(GUEST_DS_SELECTOR, Asm_GetDs() & 0xFFF8);
Vmx_VmWrite(GUEST_FS_SELECTOR, Asm_GetFs() & 0xFFF8);
Vmx_VmWrite(GUEST_GS_SELECTOR, Asm_GetGs() & 0xFFF8);
Vmx_VmWrite(GUEST_SS_SELECTOR, Asm_GetSs() & 0xFFF8);
Vmx_VmWrite(GUEST_TR_SELECTOR, Asm_GetTr() & 0xFFF8);1
2
3
4
5
6
7
8
9
10
11
12
13
14Vmx_VmWrite(GUEST_ES_AR_BYTES, 0x10000);//不可用状态
Vmx_VmWrite(GUEST_FS_AR_BYTES, 0x10000);
Vmx_VmWrite(GUEST_DS_AR_BYTES, 0x10000);
Vmx_VmWrite(GUEST_SS_AR_BYTES, 0x10000);
Vmx_VmWrite(GUEST_GS_AR_BYTES, 0x10000);
Vmx_VmWrite(GUEST_LDTR_AR_BYTES, 0x10000);
Vmx_VmWrite(GUEST_CS_AR_BYTES, 0xc09b);//属性
Vmx_VmWrite(GUEST_CS_BASE, 0);//基质
Vmx_VmWrite(GUEST_CS_LIMIT, 0xffffffff);//界限
Vmx_VmWrite(GUEST_TR_AR_BYTES, 0x008b);
Vmx_VmWrite(GUEST_TR_BASE, 0x80042000);
Vmx_VmWrite(GUEST_TR_LIMIT, 0x20ab);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16__asm {
mov ax, es
mov es, ax
mov ax, ds
mov ds, ax
mov ax, fs
mov fs, ax
mov ax, gs
mov gs, ax
mov ax, ss
mov ss, ax
} - gdtr idtr
1
2
3
4Vmx_VmWrite(GUEST_GDTR_BASE, GdtBase);
Vmx_VmWrite(GUEST_GDTR_LIMIT, Asm_GetGdtLimit());
Vmx_VmWrite(GUEST_IDTR_BASE, IdtBase);
Vmx_VmWrite(GUEST_IDTR_LIMIT, Asm_GetIdtLimit()); - 其他的一些设置
1
2
3
4
5
6
7
8
9Vmx_VmWrite(GUEST_IA32_DEBUGCTL, Asm_ReadMsr(MSR_IA32_DEBUGCTL) & 0xFFFFFFFF);
Vmx_VmWrite(GUEST_IA32_DEBUGCTL_HIGH, Asm_ReadMsr(MSR_IA32_DEBUGCTL) >> 32);
Vmx_VmWrite(GUEST_SYSENTER_CS, Asm_ReadMsr(MSR_IA32_SYSENTER_CS) & 0xFFFFFFFF);
Vmx_VmWrite(GUEST_SYSENTER_ESP, Asm_ReadMsr(MSR_IA32_SYSENTER_ESP) & 0xFFFFFFFF);
Vmx_VmWrite(GUEST_SYSENTER_EIP, Asm_ReadMsr(MSR_IA32_SYSENTER_EIP) & 0xFFFFFFFF); // KiFastCallEntry
Vmx_VmWrite(VMCS_LINK_POINTER, 0xffffffff);//物理地址不用设置为0xffffffff
Vmx_VmWrite(VMCS_LINK_POINTER_HIGH, 0xffffffff);vm-exit
在entry.c文件里的StartVirtualTechnology()函数上下保存上下文环境,让其执行完后可以正常退出
对于exithander我们需要加入一些特有的异常处理函数,来解决常见的问题,包括cpuid,读写,和vmcall等等
1 | switch (ExitReason) |
vmcall其中的一部分用来执行StopVirtualTechnology(),因为guest没有权限执行vmxoff
1 | void HandleVmCall() |
1 | if (g_VMXCPU.bVTStartSuccess) |
到此为止,基本的代码框架已经写完了。
EPT
EPT用于实现内存的虚拟化,它是把guest的物理地址转换为host的物理地址
先解释几个名词
主机物理地址 (Host Physical Address):HPA
主机虚拟地址 (Host Virtual Address): HVA
客户物理地址 (Guest Physical Address):GPA
客户虚拟地址(Guest Virtual Address):GVA
PDBR:页目录表物理基地址寄存器,X86上叫CR3
硬件层面引入EPTP寄存器,来指向EPT页表基地址。Guest运行时,Guest页表被载入PDBR,而 EPT 页表被载入专门的EPT 页表指针寄存器 EPTP。
当Guest中进程访问GVA时,CPU首先就要通过PDBR寄存器去找页目录,但是PDBR中存储的地址是GPA,所以要到EPT中进行GPA->HPA的转换,这个转换过程和物理MMU的工作流程相同。
找到了页目录的HPA基地址,再通过GVA中的Directory offset段,就找到页表的VGA了,这个页表VGA再去EPT中进行GPA->HPA的转换,就找到页表VGA的HPA了。
重复上述过程,依次查找各级页表,最终获得该GVA对应的HPA。如果是三级或者四级页表,也是同样的原理。
流程为:虚拟地址>一级页表>(ept转换)>二级页表>(ept转换)>三级页表>(ept转换)>客户端物理地址
对应上述的图表就是3*4次转换
……..