文章目录
- 前言
- VT架构基础
- VT框架编写步骤一:检测VT是否开启
- VMM和VM
- VMON和VMCS
- VT框架编写步骤二 填充VMON
- VT框架编写步骤三 进入VT
- VT框架编写步骤四 初始化VMCS
- VT框架编写步骤五 初始化VMCS数据区
- VT框架编写步骤六 处理必要事件
前言
学习VT相关的知识,需要具备WIN32基础和内核相关的知识,包括Windows段机制,页机制等等,VT里面都会有所涉及,也就是得把Windows编程和驱动基本通关,新手建议劝退。
整个VT的知识结构有大量的新的概念,新的寄存器,新的指令还有各种标志位,初次学习VT建议结合代码来理解相关的理论基础,重点在于梳理整个VT的流程,不要深究具体的某个细节。整个流程走完之后就有了阅读别人代码的能力,等实际应用的时候再去查阅相关资料补齐技术细节的部分。我的笔记,很多能省的基本都省掉了,只把整体必须的流程整理出来。
VT架构基础
**什么是VT:**我理解的VT是将操作系统运行在自己模拟的一个虚拟环境之上,然后通过这个来截获所有的事件和中断,达到过反调试和过保护的效果,可以理解为一个HOOK系统吧,毕竟也是通过拦截的方式来处理事件。
三种虚拟化技术:
- 基于处理器的虚拟化VT-x
- 基于PCI总线设备实现的IO虚拟化 VT-d
- 基于网络的虚拟化 VT-c
目前主要研究的是基于处理器的虚拟化技术VT-x
VT框架编写步骤一:检测VT是否开启
编写VT框架的第一步,就是用代码实现判断当前环境是否支持VT。这里有三个地方需要进行判断
- 首先检测CPUID是否支持VT,ecx第六位,如果为1支持VT,否则不支持
typedef union _CPUID_ECX
{
struct
{
unsigned SSE3 : 1;
unsigned PCLMULQDQ : 1;
unsigned DTES64 : 1;
unsigned MONITOR : 1;
unsigned DS_CPL : 1;
unsigned VMX : 1;
unsigned SMX : 1;
unsigned EIST : 1;
unsigned TM2 : 1;
unsigned SSSE3 : 1;
unsigned Reserved : 22;
};
}CPUID_ECX,*PCPUID_ECX;
//检测CPUID是否支持VT
BOOLEAN VmxCheckCPUIDIsSupportVT()
{
int cpuidInfo[4];
__cpuidex(cpuidInfo, 1, 0);
PCPUID_ECX cpuid = &cpuidInfo[2];
return cpuid->VMX;
}
尽量用这种结构体位判断的写法,不要用左移右移的那种算法,比较直观,可读性好。
- 检测BIOS是否支持开启VT,在MSR寄存器里面
typedef union _IA32_FEATURE_CONTROL_MSR
{
ULONG Value;
struct
{
unsigned Lock : 1; // Bit 0 is the lock bit - cannot be modified once lock is set
unsigned Reserved1 : 1; // Undefined
unsigned EnableVmxon : 1; // Bit 2. If this bit is clear, VMXON causes a general protection exception
unsigned Reserved2 : 29; // Undefined
unsigned Reserved3 : 32; // Undefined
}Fields;
} IA32_FEATURE_CONTROL_MSR,*PIA32_FEATURE_CONTROL_MSR;
//检测BIOS是否开启VT
BOOLEAN VmxCheckBIOSIsSupportVT()
{
IA32_FEATURE_CONTROL_MSR msr;
msr.Value = __readmsr(IA32_FEATURE_CONTROL);
return msr.Fields.Lock;
}
- 检测CR4.VMXE位,这个位如果被设置为1代表有VT已经开启了,你不能再开启,否则可以开启
BOOLEAN VmxCheckCR4IsSupportVT()
{
_CR0 cr0 = { .Value = __readcr0() };
_CR4 cr4 = { .Value = __readcr4() };
if (cr0.Fields.PE != 1 || cr0.Fields.PG != 1 || cr0.Fields.NE != 1)
{
return FALSE;
}
if (cr4.Fields.VMXE==1)
{
return FALSE;
}
return TRUE;
}
VMM和VM
在VMX架构下定义了两类软件的角色和环境
- VMM 虚拟机监管者
- VM 虚拟机
VMM代表一类在VMX架构下的管理者角色,拥有控制权,监管每个VM的运行。
VM代表虚拟机实例,一个VMX架构可以有多个实例,每个VM都是一套独立的环境,且VM本身不会意识到自己运行在虚拟机的环境里。
host端对应VMM,guest端对应VM
VMON和VMCS
在VMX架构下,至少有一个VMON和VMCS的物理区域,一个VMM可以有多个VM,所以也可以有多块区域。
VMON区域是给VMM用的,VMM使用这块区域对数据进行记录和维护;每个VM也需要有自己的VMCS区域。VMM使用VMCS区域来配置VM的运行环境
VT框架编写步骤二 填充VMON
在进入VMX模式前,必须为VMM准备一份VMXON区域,和VMCS区域,并配置VM的运行环境
填充VMON也分为三个步骤:
- 进入前的相关寄存器设置
- 申请VMON物理内存
- 初始化VMON区域
相关寄存器设置:
- CR0寄存器需要满足PG位 NE位 PE位都为1
- CR4需要开启VMXE位
代码如下:
//初始化CR0 CR4
ULONG64 vcr00 = __readmsr(IA32_VMX_CR0_FIXED0);
ULONG64 vcr01 = __readmsr(IA32_VMX_CR0_FIXED1);
ULONG64 vcr04 = __readmsr(IA32_VMX_CR4_FIXED0);
ULONG64 vcr14 = __readmsr(IA32_VMX_CR4_FIXED1);
ULONG64 mcr4 = __readcr4();
ULONG64 mcr0 = __readcr0();
mcr4 |= vcr04;
mcr4 &= vcr14;
mcr0 |= vcr00;
mcr0 &= vcr01;
//修改CR0 CR4
__writecr0(mcr0);
__writecr4(mcr4);
申请VMON物理内存
接着需要分配一块物理内存作为VMON区域,以4KB对齐,大小和内存cache类型可以通过检IA32_VMX_BASIC寄存器来获得
//给VMON申请一块内存
PHYSICAL_ADDRESS lowphys,highphy;
lowphys.QuadPart = 0;
highphy.QuadPart = -1;
pVcpucb->VmxOnAddr = MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE,
lowphys, highphy, lowphys, MmCached);
if (!pVcpucb->VmxOnAddr)
{
return -1;
}
RtlZeroMemory(pVcpucb->VmxOnAddr, PAGE_SIZE);
//获取物理地址
pVcpucb->VmxOnAddrPhys = MmGetPhysicalAddress(pVcpucb->VmxOnAddr);
初始化VMON区域
VMXON区域前四个字节是VMCS ID,下一个四字节是VMX- abort indicator字段(这个不需要我们填),如图(其实就是一个指针):
VMCS ID的值可以通过IA32_VMX_BASIC
获取,执行VMXON指令前必须将这个ID写入第一个四字节的位置。示例代码如下:
//读IA32_VMX_BASIC寄存器
ULONG64 vmxBasic= __readmsr(IA32_VMX_BASIC);
//填充ID
*(PULONG)pVcpucb->VmxOnAddr = (ULONG)vmxBasic;
VT框架编写步骤三 进入VT
处理器进入VMX模式需要执行VMXON指令,而这个指令需要提供一个指向VMXON区域的物理指针
int error= __vmx_on(&pVcpucb->VmxOnAddrPhys.QuadPart);
成功调用__vmx_on
指令,表示处理器已经成功进入VMX Operation模式,也就是已经处于root环境。到这里我们就已经进入VT环境了,上来一堆概念和乱八七糟的字段标志已经给我干蒙了,没办法,先把流程整理清楚,后面再慢慢理解。
VT框架编写步骤四 初始化VMCS
进入VT之后,我们还需要再设置一些结构。VMCS结构存放在一个物理区域内,也是4KB对齐,有一个8字节的头部额VMCS数据区域。
VMCS的前8个字节也是由IA32_VMX_BASIC
寄存器得到,VMCS的初始化代码和VMON区域几乎差不多
//初始化VMCS区域
int VmxInitVmcs()
{
//获取当前核
PVMXCPUPCB pVcpucb = VmxGetCurrentCpucb();
//给VMCS申请一块内存
PHYSICAL_ADDRESS lowphys, highphy;
lowphys.QuadPart = 0;
highphy.QuadPart = -1;
pVcpucb->VmxCsAddr = MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE,lowphys, highphy, lowphys, MmCached);
if (!pVcpucb->VmxCsAddr)
{
return -1;
}
RtlZeroMemory(pVcpucb->VmxCsAddr, PAGE_SIZE);
//获取物理地址
pVcpucb->VmxCsAddrPhys = MmGetPhysicalAddress(pVcpucb->VmxCsAddr);
//读IA32_VMX_BASIC寄存器
ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);
//填充ID
*(PULONG)pVcpucb->VmxCsAddr = (ULONG)vmxBasic;
//清空以前的状态
__vmx_vmclear(&pVcpucb->VmxCsAddrPhys.QuadPart);
//加载新的VMCS
__vmx_vmptrld(&pVcpucb->VmxCsAddrPhys.QuadPart);
}
VT框架编写步骤五 初始化VMCS数据区
上面我们的VMCS区域还没有初始化完成,只是初始化了前8个字节,还有一块数据区需要进行初始化。
VMCS的数据区包括了6个区域:
- guest-state区域,保存处理器的状态信息
- host-state区域,保存处理器的状态信息
- VM-execution控制区域,这里就是VM控制区了,可以用来设置和拦截中断
- VM-exit控制区域,处理退出VM的行为
- VM-entry控制区域,处理进入VM的行为
- VM-exit信息区域,记录退出VM事件的原因和相关信息和错误码
这六个数据区,都要一一进行初始化设置。
访问VMCS字段
软件必须通过VMREAD和VMWRITE来访问VMCS字段,每个字段定义一个唯一ID值来对应,这个字段ID值提供给VMREAD作为index值来访问对应字段。
关于字段的值和具体含义,这个不需要记,有定义好的宏配合__vmx_vmwrite
函数,直接拿来用就行了。
初始化VMHost和VMGuest就是把这些乱八七糟的位都给他置上对应的值,太多了,就不一个个拆开细讲了。
void VmxInitVmGuest(ULONG64 GuestEip, ULONG64 GuestEsp)
{
//填充GDT表项
FillGdtDataItem(0, AsmReadES());
FillGdtDataItem(1, AsmReadCS());
FillGdtDataItem(2, AsmReadSS());
FillGdtDataItem(3, AsmReadDS());
FillGdtDataItem(4, AsmReadFS());
FillGdtDataItem(5, AsmReadGS());
FillGdtDataItem(6, AsmReadLDTR());
//取GDT表
GdtTable gdtTable = { 0 };
AsmGetGdtTable(&gdtTable);
ULONG trSelector = AsmReadTR();
//可能是三环的 要处理下
trSelector &= 0xFFF8;
ULONG trlimit = __segmentlimit(trSelector);
LARGE_INTEGER trBase = { 0 };
PULONG trItem = (PULONG)(gdtTable.Base + trSelector);
//读TR.Base
trBase.LowPart = ((trItem[0] >> 16) & 0xFFFF)|((trItem[1]&0xFF)<<16)| (trItem[1] & 0xFF000000);
trBase.HighPart = trItem[2];
//属性
ULONG attr = (trItem[1] & 0x00F0FF00) >> 8;
__vmx_vmwrite(GUEST_TR_BASE, trBase.QuadPart);
__vmx_vmwrite(GUEST_TR_LIMIT , trlimit);
__vmx_vmwrite(GUEST_TR_AR_BYTES , attr);
__vmx_vmwrite(GUEST_TR_SELECTOR, trSelector);
__vmx_vmwrite(GUEST_CR0, __readcr0());
__vmx_vmwrite(GUEST_CR3, __readcr3());
__vmx_vmwrite(GUEST_CR4, __readcr4());
__vmx_vmwrite(GUEST_DR7, __readdr(7));
__vmx_vmwrite(GUEST_RFLAGS, __readeflags());
__vmx_vmwrite(GUEST_RSP, GuestEsp);
__vmx_vmwrite(GUEST_RIP, GuestEip);
__vmx_vmwrite(GUEST_SYSENTER_CS, __readmsr(0x174));
__vmx_vmwrite(GUEST_SYSENTER_ESP, __readmsr(0x175));
__vmx_vmwrite(GUEST_SYSENTER_EIP, __readmsr(0x176));
__vmx_vmwrite(GUEST_IA32_DEBUGCTL, __readmsr(IA32_MSR_DEBUGCTL));
__vmx_vmwrite(GUEST_IA32_PAT, __readmsr(IA32_MSR_PAT));
__vmx_vmwrite(GUEST_IA32_EFER, __readmsr(IA32_MSR_EFER));
__vmx_vmwrite(GUEST_FS_BASE, __readmsr(IA32_FS_BASE));
__vmx_vmwrite(GUEST_GS_BASE, __readmsr(IA32_GS_BASE));
__vmx_vmwrite(VMCS_LINK_POINTER, -1);
//IDT GDT
GdtTable idtTable;
__sidt(&idtTable);
__vmx_vmwrite(GUEST_GDTR_BASE, gdtTable.Base);
__vmx_vmwrite(GUEST_GDTR_LIMIT, gdtTable.limit);
__vmx_vmwrite(GUEST_IDTR_BASE, idtTable.Base);
__vmx_vmwrite(GUEST_IDTR_LIMIT, idtTable.limit);
}
初始化Host也跟上面的代码类似。
初始化VM_ENTRY
void VMInitEntry()
{
ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);
ULONG64 mseregister = ((vmxBasic >> 55) & 1) ? IA32_MSR_VMX_TRUE_ENTRY_CTLS : IA32_VMX_ENTRY_CTLS;
ULONG64 value= VmxAdjustContorls(0x200, mseregister);
__vmx_vmwrite(VM_ENTRY_CONTROLS, value);
__vmx_vmwrite(VM_ENTRY_MSR_LOAD_COUNT, 0);
__vmx_vmwrite(VM_ENTRY_INTR_INFO_FIELD, 0);
}
初始化VM_EXIT
void VMInitExit()
{
ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);
ULONG64 mseregister = ((vmxBasic >> 55) & 1) ? IA32_MSR_VMX_TRUE_EXIT_CTLS : IA32_MSR_VMX_TRUE_EXIT_CTLS;
ULONG64 value = VmxAdjustContorls(0x200|0x8000, mseregister);
__vmx_vmwrite(VM_EXIT_CONTROLS, value);
__vmx_vmwrite(VM_EXIT_MSR_LOAD_COUNT, 0);
__vmx_vmwrite(VM_EXIT_INTR_INFO, 0);
}
初始化控制区
//初始化VM控制区
void VMInitControls()
{
ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);
ULONG64 mseregister = ((vmxBasic >> 55) & 1) ? IA32_MSR_VMX_TRUE_PINBASED_CTLS : IA32_MSR_VMX_PINBASED_CTLS;
ULONG64 value = VmxAdjustContorls(0, mseregister);
__vmx_vmwrite(PIN_BASED_VM_EXEC_CONTROL, value);
mseregister = ((vmxBasic >> 55) & 1) ? IA32_MSR_VMX_TRUE_PROCBASED_CTLS : IA32_MSR_VMX_PROCBASED_CTLS;
value = VmxAdjustContorls(0, mseregister);
__vmx_vmwrite(CPU_BASED_VM_EXEC_CONTROL, value);
}
至于为什么要这么初始化,我也不知道,先把流程整理出来再说。到这里整个VT框架就完成了,下一步就可以开始拦截事件。
VT框架编写步骤六 处理必要事件
到这里我们的框架就已经完成了,现在可以开始处理自己想要拦截的事件。但是在这些事件里面,有一些事件是我们必须要处理掉的,我们写完这一部分的代码,才能开始拦截我们自己想要拦截的事件
如上图所示,上图中的指令引起的VM-exit事件,就是我们必须要处理掉的,代码如下:
EXTERN_C VOID VmxExitHandler(PGuestContext context)
{
ULONG64 reason = 0;
ULONG64 intstLen = 0;
ULONG64 instinfo = 0;
ULONG64 mrip = 0;
ULONG64 mrsp = 0;
//退出事件码
__vmx_vmread(VM_EXIT_REASON, &reason);
//指令长度
__vmx_vmread(VM_EXIT_INSTRUCTION_LEN, &intstLen);
//指令详细信息
__vmx_vmread(VMX_INSTRUCTION_INFO, &instinfo);
//获取客户机触发VT事件的地址
__vmx_vmread(GUEST_RIP, &mrip);
__vmx_vmread(GUEST_RSP, &mrsp);
//拿退出码的前16位 对下面的事件进行处理
reason = reason & 0xFFFF;
switch (reason)
{
case EXIT_REASON_CPUID:
VmxExitHandlerCpuid(context);
break;
case EXIT_REASON_GETSEC:
{
DbgBreakPoint();
DbgPrint("EXIT_REASON_GETSEC reson:%x rip:%llx\n",reason,mrip);
}
break;
case EXIT_REASON_TRIPLE_FAULT:
{
DbgBreakPoint();
DbgPrint("EXIT_REASON_TRIPLE_FAULT reson:%x rip:%llx\n", reason, mrip);
}
break;
case EXIT_REASON_INVD:
{
AsmInvd();
}
break;
case EXIT_REASON_VMCALL:
case EXIT_REASON_VMCLEAR:
case EXIT_REASON_VMLAUNCH:
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:
{
//统一返回错误,执行我们的VT后 就不往下执行了
ULONG64 rflags = 0;
__vmx_vmread(GUEST_RFLAGS, &rflags);
rflags |= 0x41;
__vmx_vmwrite(GUEST_RFLAGS, &rflags);
}
break;
case EXIT_REASON_MSR_READ:
{
VmxExitHandlerReadMsr(context);
}
break;
case EXIT_REASON_MSR_WRITE:
{
VmxExitHandlerWriteMsr(context);
}
break;
case EXIT_REASON_XSETBV:
{
ULONG64 value = MAKE_REG(context->mRax , context->mRdx);
_xsetbv(context->mRcx, value);
}
break;
default:
break;
}
//写入RIP和RSP 让VT能正确返回
__vmx_vmwrite(GUEST_RIP, mrip+intstLen);
__vmx_vmwrite(GUEST_RSP, mrsp);
}
到这里,我们的VT框架必须填充的部分就已经完成了。
VOID VmxHandlerCpuid(PGuestContext context)
{
if (context->mRax == 0x8888)
{
context->mRax = 0x11111111;
context->mRbx = 0x22222222;
context->mRcx = 0x33333333;
context->mRdx = 0x44444444;
}
else
{
int cpuids[4] = {0};
__cpuidex(cpuids,context->mRax, context->mRcx);
context->mRax = cpuids[0];
context->mRbx = cpuids[1];
context->mRcx = cpuids[2];
context->mRdx = cpuids[3];
}
}
这里我们加一个测试的代码,拦截cpuid指令,然后修改周围的寄存器。
这里比较蛋疼的一个点,框架部分代码一旦出现错误没法调试,也没有错误信息,直接就是虚拟机关闭。
而且这个部分的代码都是初始化和设置各种标志位和字段,能单步调试,其实也没有多大意义,根本看不出来异常,只能一行一行代码对照。
如果你的VT框架可以正常加载,并且执行这两条指令以后,寄存器被修改,说明事件可以正常拦截,那么恭喜各位框架搭建成功。