VMware 逃逸基础知识

news2024/12/24 2:31:07

虚拟化相关概念

  • 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_SendMessage_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 版本较老,因此为了确保能把 vmmonvmnet 两个驱动能装上,这里我们选择 ubuntu 18.04.1 的系统来安装 vmware 。

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

下载这个项目

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

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

cd vmware-host-modules
git checkout w15.5.0

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

cd vmmon-only
make
cd ../vmnet-only
make
cd ..
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 https://github.com/vmware/open-vm-tools.git
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 https://www.cabextract.org.uk/libmspack/libmspack-0.10.1alpha.tar.gz
tar zxvf libmspack-0.10.1alpha.tar.gz
cd libmspack-0.10.1alpha
./configure
make
sudo make install

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/661362.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《MySQL是怎么运行的》阅读笔记

mysql运行的整体架构简介 Mysql是由两部分构成&#xff0c;一部分是服务器程序&#xff0c;一部分是客户端程序。 服务器程序又包括两部分&#xff1a; 第一部分server层包括连接器、查询缓存、分析器、优化器、执行器等。涵盖 MySQL 的大多数核心服务功能&#xff0c;以及所有…

【Android复习笔记】Glide核心原理

Glide 核心流程 Glide中RequestManager会管理两个队列,一个请求队列,一个等待队列,当生命周期onStop停止时,把运行队列中的任务添加到等待队列中,当生命周期onResume恢复时,又从等待队列中添加到运行队列执行。 关于 Glide.with(this).load(url).into(iv); 主要搞清楚三…

Doris初认识

Doris初认识 文章目录 Doris初认识1. Doris 概述2. 核心特性3. 使用场景4. 架构 1. Doris 概述 Apache Doris 是由百度大数据部研发&#xff08;之前叫百度 Palo&#xff0c;2018 年贡献到 Apache 社区后&#xff0c;更名为 Doris &#xff09;&#xff0c;在百度内部&#xff…

Schneider Electric EcoStruxure Control Expert 15.3 Crack

Schneider Electric EcoStruxure Control Expert 15.3专为使用施耐德电气的Modicon PLC&#xff08;可编程逻辑控制器&#xff09;的控制和自动化工程师量身定制。它提供了一系列功能和工具&#xff0c;以方便自动化应用程序的开发、部署和维护。 施耐德电气EcoStruxure控制专家…

优化命令测试

命令内容&#xff1a; nload、free 目录 一、nload 1.删除yum仓库本地源 local.repo 2.nload命令的环境依赖文件只有在epel商城才有 所以需要安装epel 3.压力测试 二、free 1.free功能 2.free参数​编辑 4.实验&#xff1a;使用压力测试工具dd 查看内存使用的变化 总…

KUKA机器人出现“如果文件以写入方式打开,则不允许选择或执行程序”的解决办法

KUKA机器人出现“如果文件以写入方式打开,则不允许选择或执行程序”的解决办法 情景再现: 在操作KUKA机器人时可能会遇到这样的情况:如下图所示,即操作人员登录了管理员权限后,打开了config配置文件, 正常情况下,应该像下图所示点击左侧的来关闭config配置文件,但是…

Java012——引用数据类型String的简单学习

回顾Java数据类型 本次要学习的是Java引用数据类型String 一、对String类简单说明 说明&#xff1a;String是Java中的一个类 二、String类的作用 作用&#xff1a;主要用来创建和操作字符串。 三、使用String类 3.1、创建字符串 注意&#xff1a; 1、字符串使用双引号&qu…

【SQL应知应会】分析函数的点点滴滴(二)

欢迎来到爱书不爱输的程序猿的博客, 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 本文收录于SQL应知应会专栏,本专栏主要用于记录对于数据库的一些学习&#xff0c;有基础也有进阶&#xff0c;有MySQL也有Oracle 分析函数的点点滴滴 1.什么是分析函数&#xff1a;…

Linux中新建用户使用sudo问题

文章目录 sudo问题 sudo问题 sudo&#xff1a;权限提示指令&#xff0c;当使用sudo这条指令时&#xff0c;会将普通用户的权限提升为root权限 但是在命令行新建用户&#xff0c;这个用户使用sudo指令对一条指令提权是用不了的 这个用户没有在sudoers file这个文件中&#xff…

读发布!设计与部署稳定的分布式系统(第2版)笔记05_层叠

1. 水平扩展 1.1. 增加服务器来增加容量 1.1.1. 这些服务器集群被称为“农场” 1.2. 负载均衡的服务器集群 1.2.1. 不易遭遇单点系统失效 1.2.2. 高负载比低负载更易导致竞态条件的并发缺陷 1.3. 如果双节点集群出现失效&#xff0c;那么幸存服务器的工作负载将增加一倍 …

SpringBoot集成企业微信自建应用消息

目录 参考文档概述发送限制成员收不到应用推送消息的原因创建应用五、发送消息1、文本 text2、卡片文本3、图文news4、markdown 参考文档 官方文档 企业微信群机器人应用 概述 消息群发&#xff0c;是企业微信提供的用于企业向成员推送企业通知的工具&#xff0c;支持管理端…

AN13743-移植-无eSE

PN553/PN557 to PN7160 migration guidelines 1目的 本文件提供了从PN553/PN557 NFC控制器迁移到PN7160 NFC控制器的指南。它旨在从硬件和软件的角度描述PN7160 NFC控制器与PN553/PN557 NFC控制器的主要区别和新功能。软件部分逐步介绍如何适应Android开源项目 为PN553/PN55…

React学习笔记(二)组件详解

一、组件的概念&#xff1a; 当你开始学习 React 的时候&#xff0c;你会了解到 React 组件是 React 应用程序的基本构建块。组件是一个隔离的、可重复使用的代码块&#xff0c;由 HTML 元素、其他组件或自定义的 UI 元素组成&#xff0c;组件也就是react的核心思想&#xff0c…

Visual Studio Code 插件安装

目录 1、在线安装 1.1 单个插件的安装 1.2 根据配置文件一次性安装多个插件 1.3 同步配置的方式 1.3.1 第一种方式使用VSCode自带的同步功能。 1.3.2 第二种方式&#xff0c;首先需要先安装插件 Settings Sync 2、离线安装 2.1 去官方下载离线安装包进行安装 2.2 直接…

DolphinScheduler任务调度工具

任务调度&#xff1a;系统为了自动完成特定任务&#xff0c;在约定的特定时刻去执行任务的过程 分布式调度&#xff1a; 分布性&#xff1a;每个部分可以独立部署&#xff0c;服务之间通过网络通信伸缩性&#xff1a;每个部分可以集群部署&#xff0c;动态伸缩扩容高可用 jd…

IntelliJ IDEA 2022.3.1 (Community Edition)代码注释风格设置

Setting -> Editor -> Code Style -> Java -> Comment Code

Unity 之 最新Ads原生广告接入流程详解和工具类分享

Unity 之 Ads接入流程详解 一&#xff0c;注册 Unity Ads 广告 SDK二&#xff0c;下载 Unity Ads 广告 SDK三&#xff0c;配置 Unity Ads 广告 SDK3.1 广告位展示流程3.2 代码初始化 四&#xff0c;集成 Unity Ads 广告 SDK4.1 相关介绍4.2 代码分享 五&#xff0c;测试 Unity …

C语言变量学习2

前文已经学习了C语言变量&#xff1b; C语言变量_c语言变量块_bcbobo21cn的博客-CSDN博客 继续再学习&#xff1b;VC6新建一个单文档工程&#xff1b; void CVtestView::OnDraw(CDC* pDC) {CVtestDoc* pDoc GetDocument();ASSERT_VALID(pDoc);// TODO: add draw code for na…

【C语言复习】第三篇、Gitee码云的创建和使用

目录 第一部分、Gitee码云的用处 1、为什么要使用Gitee&#xff1f; 2、我参考的视频 第二部分、软件安装流程 1、下载Git for windows软件和TortoiseGit软件 2、Git for windows软件的安装流程 3、TortoiseGit软件软件的安装流程 第三部分、Gitee如何创建仓库&#xf…

jquery和jquery ui有什么区别

jquery和jquery ui有什么区别 jquery和jquery ui有什么区别 jQuery UI 与 jquery 的主要区别是&#xff1a;(1) jQuery是一个js库&#xff0c;主要提供的功能是选择器&#xff0c;属性修改和事件绑定等等。(2) jQuery UI则是在jQuery的基础上&#xff0c;利用jQuery的扩展性&…