小结:

  • vmware通过Backdoor实现了虚拟机和宿主机的通信。vmware有一个开源项目open-vm-tools,里面包含了具体实现方式。
  • 远程过程调用接口RPCI(Remote Procedure Call Interface,实现虚拟机向主机发送命令执行)是基于Backdoor机制实现,其在version3和4的DND/CP中均存在堆溢出漏洞:guest发送分片DnD/CP命令数据,host收到后重组,在拷贝时对每个数据包进行长度校验时,默认每个包的binarySize用户不会更改。但是用户更改后,绕过长度检查,从而导致溢出。
  • RPCI命令vmx.capability.dnd_version会检查DnD/CP协议的版本是否已被修改,如果是,就会创建一个对应版本的C++对象。对于version 3,2个大小为0xA8的C++对象会被创建,一个用于DnD命令,另一个用于Copy/Paste命令。可以用它分配大小为0xA8的内存块,并让它分配在C++对象之前,然后利用堆溢出改写C++对象的vtable指针,使其指向可控内存,从而实现代码执行。

vm 虚拟机 vmem vm 虚拟机逃逸_VMware

利用一个堆溢出漏洞实现VMware虚拟机逃逸

作者:李小龙(acez)

1. 介绍

2017年3月,长亭安全研究实验室(Chaitin Security Research Lab)参加了Pwn2Own黑客大赛,我作为团队的一员,一直专注于VMware Workstation Pro的破解,并成功在赛前完成了一个虚拟机逃逸的漏洞利用。(很不)幸运的是,就在Pwn2Own比赛的前一天(3月14日),VMware发布了一个新的版本,其中修复了我们所利用的漏洞。在本文中,我会介绍我们从发现漏洞到完成利用的整个过程。感谢@kelwin在实现漏洞利用过程中给予的帮助,也感谢ZDI的朋友,他们近期也发布了一篇相关博客,正是这篇博文促使我们完成本篇writeup。

本文主要由三部分组成:首先我们会简要介绍VMware中的RPCI机制,其次我们会描述本文使用的漏洞,最后讲解我们是如何利用这一个漏洞来绕过ASLR并实现代码执行的。

2. VMware RPCI机制

VMware实现了多种虚拟机(下文称为guest)与宿主机(下文称文host)之间的通信方式。其中一种方式是通过一个叫做Backdoor的接口,这种方式的设计很有趣,guest只需在用户态就可以通过该接口发送命令。VMware Tools也部分使用了这种接口来和host通信。我们来看部分相关代码(摘自open-vm-tools中的lib/backdoor/backdoorGcc64.c):

void  
Backdoor_InOut(Backdoor_proto *myBp) // IN/OUT  
{
   uint64 dummy;

   __asm__ __volatile__(
#ifdef __APPLE__
        /*
         * Save %rbx on the stack because the Mac OS GCC doesn't want us to
         * clobber it - it erroneously thinks %rbx is the PIC register.
         * (Radar bug 7304232)
         */
        "pushq %%rbx"           "\n\t"
#endif
        "pushq %%rax"           "\n\t"
        "movq 40(%%rax), %%rdi" "\n\t"
        "movq 32(%%rax), %%rsi" "\n\t"
        "movq 24(%%rax), %%rdx" "\n\t"
        "movq 16(%%rax), %%rcx" "\n\t"
        "movq  8(%%rax), %%rbx" "\n\t"
        "movq   (%%rax), %%rax" "\n\t"
        "inl %%dx, %%eax"       "\n\t"  /* NB: There is no inq instruction */
        "xchgq %%rax, (%%rsp)"  "\n\t"
        "movq %%rdi, 40(%%rax)" "\n\t"
        "movq %%rsi, 32(%%rax)" "\n\t"
        "movq %%rdx, 24(%%rax)" "\n\t"
        "movq %%rcx, 16(%%rax)" "\n\t"
        "movq %%rbx,  8(%%rax)" "\n\t"
        "popq          (%%rax)" "\n\t"
#ifdef __APPLE__
        "popq %%rbx"            "\n\t"
#endif
      : "=a" (dummy)
      : "0" (myBp)
      /*
       * vmware can modify the whole VM state without the compiler knowing
       * it. So far it does not modify EFLAGS. --hpreg
       */
      :
#ifndef __APPLE__
      /* %rbx is unchanged at the end of the function on Mac OS. */
      "rbx",
#endif
      "rcx", "rdx", "rsi", "rdi", "memory"
   );
}

上面的代码中出现了一个很奇怪的指令inl。在通常环境下(例如Linux下默认的I/O权限设置),用户态程序是无法执行I/O指令的,因为这条指令只会让用户态程序出错并产生崩溃。而此处这条指令产生的权限错误会被host上的hypervisor捕捉,从而实现通信。Backdoor所引入的这种从guest上的用户态程序直接和host通信的能力,带来了一个有趣的攻击面,这个攻击面正好满足Pwn2Own的要求:“在这个类型(指虚拟机逃逸这一类挑战)中,攻击必须从guest的非管理员帐号发起,并实现在host操作系统中执行任意代码”。guest将0x564D5868存入$eax,I/O端口号0x5658或0x5659存储在$dx中,分别对应低带宽和高带宽通信。其它寄存器被用于传递参数,例如$ecx的低16位被用来存储命令号。对于RPCI通信,命令号会被设为BDOOR_CMD_MESSAGE(=30)。文件lib/include/backdoor_def.h中包含了一些支持的backdoor命令列表。host捕捉到错误后,会读取命令号并分发至相应的处理函数。此处我省略了很多细节,如果你有兴趣可以阅读相关源码。

2.1 RPCI

远程过程调用接口RPCI(Remote Procedure Call Interface)是基于前面提到的Backdoor机制实现的。依赖这个机制,guest能够向host发送请求来完成某些操作,例如,拖放(Drag n Drop)/复制粘贴(Copy Paste)操作、发送或获取信息等等。RPCI请求的格式非常简单:<命令> <参数>。例如RPCI请求info-get guestinfo.ip可以用来获取guest的IP地址。对于每个RPCI命令,在vmware-vmx进程中都有相关注册和处理操作。

需要注意的是有些RPCI命令是基于VMCI套接字实现的,但此内容已超出本文讨论的范畴。

3. 漏洞

花了一些时间逆向各种不同的RPCI处理函数之后,我决定专注于分析拖放(Drag n Drop,下面简称为DnD)和复制粘贴(Copy Paste,下面简称为CP)功能。这部分可能是最复杂的RPCI命令,也是最可能找到漏洞的地方。在深入理解的DnD/CP内部工作机理后,可以很容易发现,在没有用户交互的情况下,这些处理函数中的许多功能是无法调用的。DnD/CP的核心功能维护了一个状态机,在无用户交互(例如拖动鼠标从host到guest中)情况下,许多状态是无法达到的。

我决定看一看Pwnfest 2016上被利用的漏洞,该漏洞在这个VMware安全公告中有所提及。此时我的idb已经标上了很多符号,所以很容易就通过bindiff找到了补丁的位置。下面的代码是修补之前存在漏洞的函数(可以看出services/plugins/dndcp/dnddndCPMsgV4.c中有对应源码,漏洞依然存在于open-vm-tools的git仓库的master分支当中):

static Bool  DnDCPMsgV4IsPacketValid(const uint8 *packet,  
                        size_t packetSize)
{
   DnDCPMsgHdrV4 *msgHdr = NULL;
   ASSERT(packet);

   if (packetSize < DND_CP_MSG_HEADERSIZE_V4) {
      return FALSE;
   }

   msgHdr = (DnDCPMsgHdrV4 *)packet;

   /* Payload size is not valid. */
   if (msgHdr->payloadSize > DND_CP_PACKET_MAX_PAYLOAD_SIZE_V4) {
      return FALSE;
   }

   /* Binary size is not valid. */
   if (msgHdr->binarySize > DND_CP_MSG_MAX_BINARY_SIZE_V4) {
      return FALSE;
   }

   /* Payload size is more than binary size. */
   if (msgHdr->payloadOffset + msgHdr->payloadSize > msgHdr->binarySize) { // [1]每个包的binarySize可以手动设置,但是程序默认为不修改。
      return FALSE;
   }

   return TRUE;
}

Bool  DnDCPMsgV4_UnserializeMultiple(DnDCPMsgV4 *msg,  
                               const uint8 *packet,
                               size_t packetSize)
{
   DnDCPMsgHdrV4 *msgHdr = NULL;
   ASSERT(msg);
   ASSERT(packet);

   if (!DnDCPMsgV4IsPacketValid(packet, packetSize)) {//检查长度
      return FALSE;
   }

   msgHdr = (DnDCPMsgHdrV4 *)packet;

   /*
    * For each session, there is at most 1 big message. If the received
    * sessionId is different with buffered one, the received packet is for
    * another another new message. Destroy old buffered message.
    */
   if (msg->binary &&
       msg->hdr.sessionId != msgHdr->sessionId) {
      DnDCPMsgV4_Destroy(msg);
   }

   /* Offset should be 0 for new message. */
   if (NULL == msg->binary && msgHdr->payloadOffset != 0) {
      return FALSE;
   }

   /* For existing buffered message, the payload offset should match. */
   if (msg->binary &&
       msg->hdr.sessionId == msgHdr->sessionId &&
       msg->hdr.payloadOffset != msgHdr->payloadOffset) {
      return FALSE;
   }

   if (NULL == msg->binary) {
      memcpy(msg, msgHdr, DND_CP_MSG_HEADERSIZE_V4);
      msg->binary =