前言
虚拟化框架千千万,这里只采取两个我认为不错的框架进行参考
- Noirvisor
一款github开源的虚拟化框架,支持AMD、intel。支持type I 和type II,带有uefi驱动。支持各种无痕hook和anti vt
- hv
这个人的项目被大手子用的比较多,基本上都是改的他的,他的反检测做的很好。但是只支持vt-x,不支持svm和npt,用于参考,代码写的比较简单。
这里只看typeII的虚拟化的创建。
Intel-Vtx
检测vt是否开启
初始化就不赘述了,就是判断bios中vt是否开启,都是用MSR进行判断。
而norvisor判断的很细节,包括是否支持ept、以及是否支持加速vmcs的nested-virtualization,都进行了判断,下面先展示vt-x的相关判断,amd后面再看
// for intel virtualization
static bool is_vt_supported()
{
bool is = false;
// first,check cpuid eax =1 ,ecx =0 | 3C 23.6
KernelEx::array<int, 4> infor = {};
__cpuidex(infor.data(), 1, 0);
// ecx bit 5 is vmx enable(this is from bios)
if (_bittest((const long*)&infor[2], 5))
{
is = true;
}
return is;
}
static bool is_ept_supported()
{
bool is = true;
// in order use ept,we need require following features:
// 1. execute-only translations(for stealth hook)
// 2. 2Mb/1Gb Page for identity paging
// 3. support invept instruction
ia32_vmx_ept_vpid_cap_register ept_cap = {};
ept_cap.flags = __readmsr(IA32_VMX_EPT_VPID_CAP);
// why we need to judge support wb,because we alloc memory is usually wb.
is = ept_cap.memory_type_write_back;
is &= ept_cap.execute_only_pages;
is &= ept_cap.invept;
is &= ept_cap.pde_2mb_pages;
// is &= ept_cap.pdpte_1gb_pages;
is &= ept_cap.invept_all_contexts;
is &= ept_cap.invept_single_context;
return is;
}
// this is vt-x advanced tech,for nested-virtualization
// if it supports, we can have a better performance in nested-virtualization
// vmcs-shadowing can not trigger vm exit when guest execute vmread/vmwrite
static bool is_vmcs_shadowing_supported()
{
bool is = false;
ia32_vmx_basic_register vt_basic = {};
ia32_vmx_procbased_ctls_register procbased_msr = {};
vt_basic.flags = __readmsr(IA32_VMX_BASIC);
if (vt_basic.vmx_controls)
{
// use true msr
procbased_msr.flags = __readmsr(IA32_VMX_TRUE_PROCBASED_CTLS);
}
else
{
procbased_msr.flags = __readmsr(IA32_VMX_PROCBASED_CTLS);
}
// only if support secondary proc-based msr control,we can
if (procbased_msr.activate_secondary_controls)
{
ia32_vmx_procbased_ctls2_register sec_procbased_msr = {};
sec_procbased_msr.flags = __readmsr(IA32_VMX_PROCBASED_CTLS2);
is = sec_procbased_msr.vmcs_shadowing;
}
return is;
}
发现基本上都是用的MSR寄存器进行判断的。
进入Vtx事前准备
主要是构建host cr3、host stack、host ept-identity等一些相关的内存,毕竟type 2类型的vt是不能再host随便分配内存,所以进入vmx operation之前要准备好,具体的分配细节就不看了,挑几个比较有意思的点展示:
mtrr处理
几乎所有的vt框架、文章都说了,mtrr的GPA和HPA(未进入vt之前的mtrr属性)要保持一致。
其实保持一致归根结底是因为有些内存,不能随便 设置mtrr的属性,典型的就是低1Mb的内存,这部分内存是bios的rom映射的固件内存,有多种属性,如果用WB立刻死机。因为他们不能用缓存。
这一点hv处理的和noirvisor相比还是差的比较多的。
首先,noirvisor用的ept页表是1Gb大页面,这里统一用的是def_type,然后去看他的update_by_mtrr
上面提到的低1Mb
内存有多种mtrr属性,这个其实要根据msr进行查的。
首先,要看是否是mtrr开启(这个一般都开启了,Noirvisor做的很仔细),然后去访问IA32_MTRR_DEF_TYPE
这个msr寄存器,去看一下fix-range是否开启,开启了才用可以使用fix进行设置低2Mb的内存。
noirvisor设置完之后,然后开始获取mtrr-range
询问了mtrr_cap,来看有几个range,然后就是常规操作,循环获取物理地址对应的mtrr_type,这个都是老生常谈了。这个函数也人如其名,去根据mtrr去更更新所有的内存属性。
而下面这个地方就是和hv的差异了:
如果有fix-range的mtrr寄存器,那么就需要遍历这么多fix-range,挨个设置mtrr的type属性
注意,这类就得开始split成4kb了,因为这个低2Mb内存type是比较多样的。询问出来的东西,是一个64bit,是以一个字节为单位,分8个,然后再拆分几个页面,如此反复设置。
总体来说,遍历起来很复杂。最后值得一提的是,他把这个covered位设置false,表示以后的物理内存属性都由ept决定,而非mtrr,不过奇怪的是hv没有处理这个,我猜测可能是默认赋值成0了
然后去设置ept_pointer的属性
不过这个地方的write_back的逻辑其实没太懂,这里有个关键点是每个vcpu都有自己的ept manager,这个其实是为了解决多核心cpu数据竞争问题,想象如下情况,1.vcpu1访问,vcpu2执行,这样就会涉及到多核心竞争一个ept maneger,只能做同步,否则会触发错误的ept violation不触发。
noirvisor做了一些其他的初始化,主要也是为了仿真虚拟机
这个xsave是之前没见过的。
这样,每个vcpu要初始化的东西就初始化完毕了
hypervisor全局数据
上面提到了初始化每个vcpu的数据,包括ept、stack、vmcs、vmxon、nested vmcs、xsave等,接着开始初始化一些hypervisor全局所持有的
这个主要是
- msr bitmap
- io bitmap
- build host cr3
函数里面,可以发现,他分配了一个page_size作为host cr3,然后分配pdpte作为host cr3的第一个pml4e的pdpt,然后往这个里面填充,从0~512Gb作为host读取物理内存的方式:
先分配cr3,然后拷贝system cr3 的所有pml4e。hv实现起来比较暴力,直接copy全部的pml4e的高256个,也就是kernelspace。
坏处是没办法去快速地读写任意物理地址了。
IOMMU
IOMMU是集成在主板或者硬件中的页表翻译单元,主要结构如下
[CPU/芯片组]
|
|-[IOMMU]
| |
| [Root Complex]
| |
| [PCIe Switch/Bridge]
| |
|--[设备1]--[设备2]--[设备3]
设备发起DMA请求,经过Pcie总线到Root Complex
,然后IOMMU此时会受到这个请求,然后进行页表转换,从而让dma访问实际的物理内存。
而所谓的Vt-d,其实就是通过acpi找到DMARemapping寄存器,通过这个寄存器找到iommu_drhd(MA Remapping Hardware Definition
),这描述了一个iommu单元,然后去更改IOMMU的base_address。
注意,这个IOMMU的BaseAddress(BAR)其实是一个MMIO,用于配置这个IOMMU单元的
代码里面分了三步:
// IOMMU Initialization follows a three-pass procedure.
// First pass will enumerate all IOMMU hardware and check their features.
// Second pass will setup optimized page tables.
// Third pass will activate DMA Remapping on all IOMMU Hardware.
然后就是根据IOMMU到底支持多少位的物理地址,判断是几级分页,分配页表。
简而言之就是要分配256个页表,用于支持不同的设备?
上面只是分配了顶层页表,可以理解为cr3,然后noir_enum_physical_memory_ranges
就是枚举所有的物理页面,然后进行identity map。
接着就使用nvc_vt_iommu_initalize_ci
,这个是为了保证代码完整性的,由此也引出了为什么要初始化或者修改IOMMU,为了保证设备不会通过DMA来强行修改我们的vmm代码。因为cpu读写内存,会走ept,但是设备读写物理内存,不走ept,而是直接的host pa
可以发现,他是这样进行隐藏vmm的。
此时设备的页表已经准备好了,往里面写入就行了:
其实就是写入root_table_ptr
,这是一个页表的数组,里面存了256个页目录机制(IOMMU通过这个解析每个设备的)
最后,通过ept来控制对于这个bar的读写
这样无论是配置修改IOMMU的bar,还是vmm本身,都被隐藏了,不会被物理设备修改。
但是这种仿真没啥必要,这疑似有些太完美了。
anti hypervisor detect
一个hypevisor关键就是anti detect做的怎么样,这一点noirvisor和hv做得都不错,noirvisor再预先进入host的时候就做了如下操作来防止检测hypervisor:
-
io-hook
但是这个似乎啥都没有做,好像是to do?
-
虚拟化透传
cpuid的这个位如果是1,代表hypervisor有意像你表面我开了hypervisor
-
hide from processor trace
intel的processor trace机制,必须设置这个来保证不会被PMI中断。
-
MSR-hook
为了仿真vt环境,一些msrhook是必不可少的:
一些是为了仿真,一些则是为了嵌套虚拟化的hook
这些是用到的虚拟化一些msr寄存器,主要是拦截读取,而不用拦截写入,这里看注释也可以知道,进入了vt,处理器会自动拦截他们的写入
值得一提是更新微码操作,他这里也给拦截了:
看他是如果type-I,因为涉及GVA->HPA的转换,所以忽略了,但是如果是typeII,直接把这个虚拟地址填上去了。理论上来说应该是没有问题的…就怕pml4e改变。
所以个人认为这里哪怕是typeii忽略应该也是没有问题的。
而至于其他寄存器的拦截,主要是以阻止为主:
这里他去判断了cr0.cd(cache disable)位,这个位主要是和cr0.NW共同控制处理器缓存属性的
CD=0, NW=0:正常缓存操作(Write-Back)
CD=1, NW=0:缓存完全禁用
CD=1, NW=1:无缓存,写穿透
CD=0, NW=1:无效组合(不应该使用)
这里判断了,如果是CD开启,那么不需要考虑缓存,只需要设置mtrr_dirty=1,然后等待CD reset的时候,判断dirty,然后更新mtrr即可,顺便刷新下vept
否则,那就得在此进行设置了。
主要是刷新ept的tlb,这里可能会疑惑为什么要刷新tlb,GPA
->HPA
,你可能觉得这个映射并没有变化,但是ept-tlb不止cache了这些,还有EPT页表的属性,这个时候肯定是改了的,所以需要刷新下ept-tlb。
-
protect vmm
所谓的protect vmm就是用ept吧vmm的关键数据给保护起来,包括以下地方
-
MMIO Hook
同时通过ept 来hook了MMIO,这个本质上和IOMMU是一个操作,主要就是通过ept 来hook dmar的一些bar寄存器的
把他放在了一个avl里面,然后后面setup_mmio_hooks就可以保护不被改变了。
开抄