«

13

VT调试BluePill的心得

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配合也是个蛋疼的事情

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着手研究的

二、解决断点问题

本以为上文结束后就可以随便调了,不过实际上又有一个奇怪的问题:一旦开启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,有时间我再改进)

 

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>