VT虽然已经流行多年,但是因为参考资料较少,所以研究起来有很多困难
要学习VT,最好的参考资料之一就是NewBluePill源代码,该代码是Invisible Things Lab在2007年公开的,实现了一个基本的VMM,并加入了内存隐藏技术。阅读nbp的代码,并结合Intel开发手册作为参考,可以学习到Intel-Vt的方方面面。
因为最近在学校做一个项目,要用VT实现一些功能,所以我开始研究nbp的代码,期间遇到很多问题,困扰了我很久,经过N次BSOD后终于算是勉强把nbp弄明白了
在这里跟大家分享一下经验。
一.搭建调试环境
我搜索了论坛里有关VT调试的帖子,综合了网上的各种说法,普遍认为调试VT有3种方法:
1.Windbg+bochs虚拟机
好处是可以用我们熟悉的windbg
缺点是需要手工修改一些代码并重新编译bochs,很难自己装系统(因为太慢了),最后系统跑起来也感觉特别脆,容易出错,我试了很久好像也没弄好
2.IDA+GDB+bochs
不清楚,因为我一点也不了解GDB,而且我的IDA版本好像有问题,跟bochs配合也是个蛋疼的事情
(看了评论后发现IDA+GDB+VMWare似乎是最好的解决方案,可以独立于操作系统进行调试,不依赖系统资源)
3.两个物理主机通过COM连线,进行双机调试
我有两台机器,一个台式机(有COM口),一个笔记本(没COM,使用USB转COM插头),然后买了个母对母的COM连接线(因为我两个机器端都是公头),连好后却怎么也通讯不了,也不知道问题在哪,感觉特别无奈。。后来某次跟一个做嵌入式的同学聊了聊,他说可能是因为COM线的RXD引脚和TXD引脚需要交叉接,然后就帮我焊了个小板子,回去一试还真行了
这个方法好处是可以用windbg,而且不会出现虚拟机的奇怪问题,绝对原生。缺点是需要两台物理主机,而且还得买线,对环境要求较高。调试速度也一般,因为串口通讯速率毕竟有限。
以上几个方法都不尽人意,但最后我经FC牛的提醒发现一种最完美的方法
4.VMWare10+Windbg
之前我们宁愿用bochs也不用VMWare,是因为VMWare的早期版本并不支持VT的模拟,但是却没有多少人发现VMWare10已经支持VT了。只需要选上这个选项(btw,VM10是原生中文,用着非常爽~)
后面的步骤大家就都懂了,配置一下VMWare跟Windbg的双机调试,就可以用一台电脑拿着windbg调VT了~缺点简直没有,因为这已经是我能想象的最好情况了。
顺便说一下,nbp0.32公开版本的推荐运行环境是Windows 2003 x64,所以我一开始也是先从2k3着手研究的
本文提出的方法不一定正确,但目前可以在一定程度上解决VT调试的一些问题。至于Windbg、内核调试引擎、VMM这三者之间相互作用的细节仍有待实验探究
二、解决断点问题
本以为上文结束后就可以随便调了,不过实际上又有一个奇怪的问题:一旦开启VMLAUNCH了以后,无法在VMM的代码上下断点,比如bp newbp!VmxVmexitHandler,如果断点被触发的话,虚拟机就失去了响应,Windbg也定在那里不动了。这样的话我们根本没法调关键代码,print调试大法必然是不能接受的。
这个问题也让我百思不得其解,直到现在我也不知道为什么会出现这种现象。但是我发现这跟nbp的内存隐藏有关
(后记:有待进一步验证) ,我把nbp自己的内存管理系统取缔以后,就可以任意下断点了。哪位大牛能解释下这个BUG的细节,还望不吝赐教
nbp的内存管理主要实现在paging.c里,观察nbp分配内存使用的MmAllocatePages函数,可以看出它首先用ExAllocatePoolWithTag申请内存,然后会对页做一些处理,来实现自己的内存管理(这部分我还没细看)。我们首先要把后续操作注释掉(MmAllocateContiguousPages和MmAllocateContiguousPagesSpecifyCache同理)
PVOID NTAPI MmAllocatePages (
ULONG uNumberOfPages,
PPHYSICAL_ADDRESS pFirstPagePA
)
{
PVOID PageVA, FirstPage;
PHYSICAL_ADDRESS PagePA;
NTSTATUS Status;
ULONG i;
if (!uNumberOfPages)
return NULL;
FirstPage = PageVA = ExAllocatePoolWithTag (NonPagedPool, uNumberOfPages * PAGE_SIZE, ITL_TAG);
if (!PageVA)
return NULL;
RtlZeroMemory (PageVA, uNumberOfPages * PAGE_SIZE);
if (pFirstPagePA)
*pFirstPagePA = MmGetPhysicalAddress (PageVA);
/*
for (i = 0; i < uNumberOfPages; i++) {
// map to the same addresses in the host pagetables as they are in guest's
PagePA = MmGetPhysicalAddress (PageVA);
Status = MmSavePage (PagePA, PageVA, PageVA, !i ? PAT_POOL : PAT_DONT_FREE, uNumberOfPages, 0);
if (!NT_SUCCESS (Status)) {
DbgPrint ("MmAllocatePages(): MmSavePage() failed with status 0x%08X\n", Status);
return NULL;
}
Status = MmCreateMapping (PagePA, PageVA, FALSE);
if (!NT_SUCCESS (Status)) {
DbgPrint
("MmAllocatePages(): MmCreateMapping() failed to map PA 0x%p with status 0x%08X\n", PagePA.QuadPart, Status);
return NULL;
}
PageVA = (PUCHAR) PageVA + PAGE_SIZE;
}
*/
return FirstPage;
}
还要注释掉HvmSetupGdt函数里的一句话
//MmMapGuestTSS64 ((PTSS64) GuestTssBase, GuestTssLimit);
更改VmxSetupVMCS中设置VMCS.CR3的部分,让GuestOS使用当前系统的页目录指针,而非nbp自己的页目录
//VmxWrite (HOST_CR3, g_PageMapBasePhysicalAddress.QuadPart);
VmxWrite (HOST_CR3, RegGetCr3 ());
最后要把DriverEntry中MmInitManager之类的调用删掉,卸载时也不需要MmShutdownManager。
至此便移除了nbp自己实现的内存管理(似乎这技术才是亮点..以后再好好看看)。
三、解决卸载问题
nbp在卸载时会死机,因为卸载必须在VMM下进行,而DriverUnload是在non-root模式里,所以要通过Hypercall的方法把任务交给VMM,Hypercall的实现在hypercall.c里,DriverUnload最终会调用到HcMakeHypercall,但是nbp却没有对VMCALL指令作处理,只是把它当做一般的VM指令,直接返回执行失败,所以卸载的Hypercall没有被处理,系统仍在虚拟机中运行,但是随后DriverUnload会释放资源,进而导致出错死机。要让它正常卸载,我们只要处理好Hypercall即可。
首先添加对VMCALL的处理函数,修改VmxRegisterTraps
//
// 为所有VM指令造成的VMExit设置一个无用的处理函数,VMCALL则作为Hypercall处理
//
for (i = 0; i < sizeof (TableOfVmxExits) / sizeof (ULONG32); i++) {
if (TableOfVmxExits[i] == EXIT_REASON_VMCALL) {
if (!NT_SUCCESS (Status = TrInitializeGeneralTrap (Cpu, EXIT_REASON_VMCALL, 0, // length of the instruction, 0 means length need to be get from vmcs later.
VmxDispatchHypercall, &Trap))) {
_KdPrint (("VmxRegisterTraps(): Failed to register VmxDispatchHypercall with status 0x%08hX\n", Status));
return Status;
}
} else {
if (!NT_SUCCESS (Status = TrInitializeGeneralTrap (Cpu, TableOfVmxExits[i], 0, // length of the instruction, 0 means length need to be get from vmcs later.
VmxDispatchVmxInstrDummy, &Trap))) {
_KdPrint (("VmxRegisterTraps(): Failed to register VmxDispatchVmon with status 0x%08hX\n", Status));
return Status;
}
}
TrRegisterTrap (Cpu, Trap);
}
然后实现Hypercall的处理函数,只需要简单地交给HcDispatchHypercall即可(其实nbp把工作都做好了,只不过把桥梁移除了,感觉nbp代码里很多问题都是作者故意的)。
static BOOLEAN NTAPI VmxDispatchHypercall (
PCPU Cpu,
PGUEST_REGS GuestRegs,
PNBP_TRAP Trap,
BOOLEAN WillBeAlsoHandledByGuestHv
)
{
ULONG64 inst_len;
ULONG32 exit_qualification;
if (!Cpu || !GuestRegs)
return TRUE;
_KdPrint (("VmxDispatchVminstructionDummy(): Nested virtualization not supported in this build!\n"));
inst_len = VmxRead (VM_EXIT_INSTRUCTION_LEN);
Trap->General.RipDelta = inst_len;
HcDispatchHypercall(Cpu, GuestRegs);
return TRUE;
}
然后就可以正常卸载nbp了。
还有几个地方比较麻烦,要改的地方比较多。一个是Svm,这个是nbp在AMD处理器下的实现,相当于AMD版的Vt,这个用的比较少,而且内容跟Vt相似,如果只是研究Vt的话可以移除Svm的代码。另外就是x86的移植问题,我感觉nbp本身肯定是在x86下测试过的,但是发布版里故意加了一些BUG,都是一些小毛病,对照着编译错误细心改一下就行,不用很多添加新代码。
最后给出我修改的nbp代码(改的不是很细致,似乎某些多核情况下会BSOD,有时间我再改进)
ps:打个广告,最近在做图像匹配和机器学习方面的研究,不知有没有这方面的大牛能指教一下
qq474516292
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
上传的附件: