参考

2018-RealWorld-Station-Escape

【技术分享】实战VMware虚拟机逃逸漏洞
VMware 逃逸基础知识
从0到1的虚拟机逃逸三部曲
虚拟机逃逸入门(一)
RealWorldCTF2018 Station Escape

VMware 逃逸基础知识

vmware逃逸简介

  1. 虚拟机操作系统发送敏感请求,使操作系统陷入内核态
  2. 某些特权指令会进入ring0以下的状态,即交给Hypervisor处理
  3. 利用Hypervisor的脆弱性漏洞使得Hypervisor执行完特权指令后不产生指令状态的返回,使得执行完指令后依然停留在内核态
  4. 实现了提权后,可以渗透到Hypervisor和虚拟机的其他区域,破坏虚拟化的隔离机制,完成逃逸操作。

(或者说虚拟机执行某些操作会发送请求到主机vmx,然后主机vmx来处理请求,通过利用vmx中的漏洞来使得达到提权目的)
在这里插入图片描述

比如我们发现了一个 Hypervisor 中的漏洞,可以被利用来阻止特权指令执行后的正常状态转换。

  1. 正常情况:
Guest OS 执行特权指令 -> Trap 到 Hypervisor -> Hypervisor 执行指令 -> 返回 Guest OS 用户态
  1. 利用漏洞后:
Guest OS 执行特权指令 -> Trap 到 Hypervisor -> Hypervisor 执行指令 
-> 漏洞阻止状态转换 -> Guest OS 保持在内核态

具体实现可能如下:

  1. 攻击者发现 Hypervisor 在处理某些特定的特权指令时存在缓冲区溢出漏洞。

  2. 攻击者构造一个特殊的输入,触发这个缓冲区溢出。

  3. 溢出的数据覆盖了 Hypervisor 中负责状态转换的代码或数据结构。

  4. 当 Hypervisor 执行完特权指令,准备返回 Guest OS 时,由于关键代码被覆盖,无法正确执行状态转换。

  5. 结果是 Guest OS 在特权指令执行后仍保持在内核态,而不是正常转回用户态。

这种攻击可能导致严重的安全问题,因为它打破了用户态和内核态的隔离,可能被进一步利用来提升权限或执行其他恶意操作。

虚拟机和主机通信机制(guest to host)

https://sysprogs.com/legacy/articles/kdvmware/guestrpc.shtml

经常使用vmware虚拟机的人一定会熟悉其拖拽功能,即Guest和host之间的文件传递以及复制之类的操作,都是基于拖拽实现的,拖拽的背后是Guest和host之间的通信机制。而Vm类型的逃逸中,利用的就是该通信机制,这类机制被设计是现在了vmtools当中,高版本的vmware,vmtools消失,直接被自带安装。

共享内存(弃用)

  • 优点:速度快
  • 缺点:需要持续检查状态位,导致100%CPU占用
  • 例子:假设我们有一个共享内存区域,虚拟机和主机都可以访问:
    struct SharedMemory {
        int newRequestFlag;
        char requestData[1024];
    };
    
    // 在虚拟机中持续检查新请求
    while(1) {
        if(sharedMemory->newRequestFlag) {
            processRequest(sharedMemory->requestData);
            sharedMemory->newRequestFlag = 0;
        }
    }
    

backdoor机制

相关源码

open-vm-tools/lib/backdoor/backdoor_def.h

#define BDOOR_MAGIC 0x564D5868

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

#define BDOOR_PORT        0x5658

/* Flags used by the hypercall interface. */


#define   BDOOR_CMD_GETMHZ                    1
/*
 * BDOOR_CMD_APMFUNCTION is used by:
 *
 * o The FrobOS code, which instead should either program the virtual chipset
 *   (like the new BIOS code does, Matthias Hausner offered to implement that),
 *   or not use any VM-specific code (which requires that we correctly
 *   implement "power off on CLI HLT" for SMP VMs, Boris Weissman offered to
 *   implement that)
 *
 * o The old BIOS code, which will soon be jettisoned
 */
#define   BDOOR_CMD_APMFUNCTION               2 /* CPL0 only. */
#define   BDOOR_CMD_GETDISKGEO                3
#define   BDOOR_CMD_GETPTRLOCATION            4
#define   BDOOR_CMD_SETPTRLOCATION            5
#define   BDOOR_CMD_GETSELLENGTH              6
#define   BDOOR_CMD_GETNEXTPIECE              7
#define   BDOOR_CMD_SETSELLENGTH              8
#define   BDOOR_CMD_SETNEXTPIECE              9
#define   BDOOR_CMD_GETVERSION               10
#define   BDOOR_CMD_GETDEVICELISTELEMENT     11
……………………还有很多

open-vm-tools/lib/include/backdoor_types.h
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;


open-vm-tools/lib/backdoor/backdoorGcc64.c.h

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" //恢复之前的压入rax并将rax刚开始的内容给栈
        "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"//恢复原来rax刚开始部分
#ifdef __APPLE__
        "popq %%rbx"            "\n\t"
#endif
      : "=a" (dummy) 
//       输出操作数:
// : "=a" (dummy)
// 这定义了输出操作数。"=a" 表示使用 %rax 寄存器,结果存储在 dummy 变量中。
      : "0" (myBp)
//       输入操作数:
// : "0" (myBp)
// 这定义了输入操作数。"0" 表示使用与第一个输出操作数相同的寄存器(即 %rax),值来自 myBp。
// myBp 被加载到 rax 中(通过 "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"
   );
}


工作原理:

  1. 使用特殊的I/O指令:
    VMware截获了特定的I/O指令(在这个例子中是’in’指令),并将其解释为后门调用。

  2. 魔术数字和命令:
    使用预定义的魔术数字(BDOOR_MAGIC)和命令码来指定操作类型。

  3. 寄存器传参:
    通过特定寄存器传递参数和接收结果。

unsigned __declspec(naked) GetMousePos()
{
    __asm
    {
        mov eax, 564D5868h  // 设置魔术数字 (BDOOR_MAGIC)
        mov ecx, 4          // 设置命令码 (BDOOR_CMD_GETPTRLOCATION)
        mov edx, 5658h      // 设置I/O端口 (BDOOR_PORT)
        in eax, dx          // 执行后门调用
        ret                 // 返回结果(在eax中)
    }
}

这段代码做了以下事情:

  1. 设置eax为魔术数字,表明这是一个后门调用。
  2. 设置ecx为4,指示我们要获取鼠标位置。
  3. 设置edx为特殊的I/O端口号。
  4. 执行’in’指令,这在VMware中会被截获并处理。
  5. 结果存储在eax中返回。

在主函数中:

void main()
{
    unsigned mousepos = GetMousePos();
    printf("Mouse cursor pos: x=%d,y=%d\n", mousepos >> 16, mousepos & 0xFFFF);
}
  1. 调用GetMousePos()获取鼠标位置。
  2. 高16位表示X坐标,低16位表示Y坐标。

举例说明:

假设我们在VMware虚拟机中运行这个程序:

  1. 在真实机器上:

    运行结果: 程序崩溃,因为'in'指令在用户模式下是不允许的。
    
  2. 在VMware虚拟机中,鼠标在(100, 200)位置:

    运行结果: Mouse cursor pos: x=100,y=200
    
  3. 在VMware虚拟机中,鼠标在(500, 300)位置:

    运行结果: Mouse cursor pos: x=500,y=300
    

其中有一条特权指令,in,这条指令在正常的操作系统执行会报错,但是在vm中的guest机器执行这条指令,这个异常会被 vmtools捕获,然后传递给vmware-vmx.exe进行通信操作。

重点在于,backdoor普通用户也可以执行,所以,guest中,执行相应的代码,让操作系统陷入hypervisor层,然后再利用backdoor和host进行通信,触发此bug。

Message_Send和Message_Recv

Message相关函数是客户机应用程序和 VMware 之间的内部通信通道的第二层,open-vmtools中也有实现。Message_Send和Message_Recv等,它们是建立在 backdoor 机制之上的更高级别的抽象。它们使用 backdoor 作为底层通信渠道,但提供了更易用和更灵活的接口。

  1. Message_OpenAllocated 简化版:
Bool Message_OpenAllocated(uint32 proto, Message_Channel *chan, char *receiveBuffer, size_t receiveBufferSize)
{
    Backdoor_proto bp;
    
    bp.in.cx.halfs.high = MESSAGE_TYPE_OPEN;
    bp.in.size = proto | GUESTMSG_FLAG_COOKIE;
    bp.in.cx.halfs.low = BDOOR_CMD_MESSAGE;
    
    Backdoor(&bp);
    
    if (!(bp.in.cx.halfs.high & MESSAGE_STATUS_SUCCESS)) {
        return FALSE;
    }
    
    chan->id = bp.in.dx.halfs.high;
    chan->cookieHigh = bp.out.si.word;
    chan->cookieLow = bp.out.di.word;
    
    chan->in = (unsigned char *)receiveBuffer;
    chan->inAlloc = receiveBufferSize;
    chan->inPreallocated = receiveBuffer != NULL;
    
    return TRUE;
}

Message_OpenAllocated:

  • 功能:打开一个预分配的通信通道。
  • 参数:协议类型、通道结构指针、接收缓冲区及其大小。
  • 过程:
    • 使用 Backdoor 机制发送打开通道请求。
    • 如果成功,设置通道 ID 和 cookie。
    • 初始化接收缓冲区信息。
  • 返回:成功返回 TRUE,失败返回 FALSE。
  1. Message_Open简化版:
Message_Channel* Message_Open(uint32 proto)
{
    Message_Channel *chan = malloc(sizeof(Message_Channel));
    if (chan == NULL) {
        return NULL;
    }

    if (!Message_OpenAllocated(proto, chan, NULL, 0)) {
        free(chan);
        return NULL;
    }

    return chan;
}

Message_Open:

  • 功能:分配并打开一个新的通信通道。
  • 过程:
    • 分配 Message_Channel 结构。
    • 调用 Message_OpenAllocated 初始化通道。
  • 返回:成功返回通道指针,失败返回 NULL。
  1. Message_Send 简化版:
Bool Message_Send(Message_Channel *chan, const unsigned char *buf, size_t bufSize)
{
    Backdoor_proto bp;
    
    bp.in.cx.halfs.high = MESSAGE_TYPE_SENDSIZE;
    bp.in.dx.halfs.high = chan->id;
    bp.in.si.word = chan->cookieHigh;
    bp.in.di.word = chan->cookieLow;
    bp.in.size = bufSize;
    bp.in.cx.halfs.low = BDOOR_CMD_MESSAGE;
    
    Backdoor(&bp);
    
    if (!(bp.in.cx.halfs.high & MESSAGE_STATUS_SUCCESS)) {
        return FALSE;
    }
    
    if (bp.in.cx.halfs.high & MESSAGE_STATUS_HB) {
        // 高带宽传输
        Backdoor_proto_hb bphb;
        // 设置 bphb...
        Backdoor_HbOut(&bphb);
    } else {
        // 低带宽传输
        while (bufSize > 0) {
            // 设置 bp 发送数据块...
            Backdoor(&bp);
            // 更新 buf 和 bufSize...
        }
    }
    
    return TRUE;
}

Message_Send:

  • 功能:通过通道发送消息。
  • 参数:通道指针、消息缓冲区、消息大小。
  • 过程:
    • 首先发送消息大小。
    • 根据是否支持高带宽,选择发送方式:
      • 高带宽:一次性发送整个消息。
      • 低带宽:分块发送消息。
  • 返回:成功返回 TRUE,失败返回 FALSE。
  1. Message_Receive 简化版:
Bool Message_Receive(Message_Channel *chan, unsigned char **buf, size_t *bufSize)
{
    Backdoor_proto bp;
    
    bp.in.cx.halfs.high = MESSAGE_TYPE_RECVSIZE;
    bp.in.dx.halfs.high = chan->id;
    bp.in.si.word = chan->cookieHigh;
    bp.in.di.word = chan->cookieLow;
    bp.in.cx.halfs.low = BDOOR_CMD_MESSAGE;
    
    Backdoor(&bp);
    
    if (!(bp.in.cx.halfs.high & MESSAGE_STATUS_SUCCESS)) {
        return FALSE;
    }
    
    if (!(bp.in.cx.halfs.high & MESSAGE_STATUS_DORECV)) {
        *bufSize = 0;
        return TRUE;
    }
    
    *bufSize = bp.out.bx.word;
    
    // 分配或检查缓冲区大小...
    
    if (bp.in.cx.halfs.high & MESSAGE_STATUS_HB) {
        // 高带宽接收
        // 使用 Backdoor_HbIn...
    } else {
        // 低带宽接收
        // 循环使用 Backdoor 接收数据...
    }
    
    return TRUE;
}

Message_Receive:

  • 功能:从通道接收消息。
  • 参数:通道指针、接收缓冲区指针的指针、接收大小的指针。
  • 过程:
    • 检查是否有消息可接收。
    • 如果有,获取消息大小。
    • 根据是否支持高带宽,选择接收方式。
    • 分配或检查接收缓冲区大小。
  • 返回:成功返回 TRUE,失败返回 FALSE。
  1. Message_CloseAllocated 简化版:
void Message_CloseAllocated(Message_Channel *chan)
{
    Backdoor_proto bp;

    if (chan == NULL) {
        return;
    }

    bp.in.cx.halfs.high = MESSAGE_TYPE_CLOSE;
    bp.in.dx.halfs.high = chan->id;
    bp.in.si.word = chan->cookieHigh;
    bp.in.di.word = chan->cookieLow;
    bp.in.cx.halfs.low = BDOOR_CMD_MESSAGE;

    Backdoor(&bp);

    if (!chan->inPreallocated && chan->in != NULL) {
        free(chan->in);
    }

    chan->in = NULL;
    chan->inAlloc = 0;
}

. Message_CloseAllocated:

  • 功能:关闭一个预分配的通信通道。
  • 参数:通道指针。
  • 过程:
    • 发送关闭通道请求。
    • 释放接收缓冲区(如果是动态分配的)。
    • 重置通道信息。
  1. Message_Close 简化版:
void Message_Close(Message_Channel *chan)
{
    if (chan == NULL) {
        return;
    }

    Message_CloseAllocated(chan);
    free(chan);
}

Message_Close:

  • 功能:关闭并释放一个通信通道。
  • 参数:通道指针。
  • 过程:
    • 调用 Message_CloseAllocated 关闭通道。
    • 释放通道结构内存。

GuestRPC

在backdoor和Message_Send/Receive基础上实现的更为灵活的通信方式

Backdoor -> Message_Send/Receive -> GuestRPC
每一层都建立在下一层的基础之上,提供更高级的抽象和功能。

当执行一个 GuestRPC 调用时,数据流通常是:
GuestRPC 命令 -> Message_Send 处理 -> Backdoor , 如Rpcout_start->Message_OpenAllocated->Backdoor

执行单个 GuestRPC 调用由一系列请求组成:

  • 打开 GuestRPC 通道
  • 发送命令长度
  • 发送命令数据
  • 接收回复大小
  • 接收回复数据
  • 发出接收结束信号
  • 关闭GuestRPC 通道

每个请求过程如下
在这里插入图片描述

/open-vm-tools/lib/rpcOut/rpcout.c

/open-vm-tools/lib/include/rpcout.h

typedef struct RpcOut RpcOut;

RpcOut *RpcOut_Construct(void);
void RpcOut_Destruct(RpcOut *out);
Bool RpcOut_start(RpcOut *out);
Bool RpcOut_send(RpcOut *out, char const *request, size_t reqLen,
                 Bool *rpcStatus, char const **reply, size_t *repLen);
Bool RpcOut_stop(RpcOut *out);


/*
 * This is the only method needed to send a message to vmware for
 * 99% of uses. I'm leaving the others defined here so people know
 * they can be exported again if the need arises. [greg]
 */
Bool RpcOut_sendOne(char **reply, size_t *repLen, char const *reqFmt, ...);

/* 
 * A version of the RpcOut_sendOne function that works with UTF-8
 * strings and other data that would be corrupted by Win32's
 * FormatMessage function (which is used by RpcOut_sendOne()).
 */

Bool RpcOut_SendOneRaw(void *request, size_t reqLen, char **reply, size_t *repLen);

/* 
 * A variant of the RpcOut_SendOneRaw in which the caller supplies the
 * receive buffer so as to avoid the need to call malloc internally.
 * Useful in situations where calling malloc is not allowed.
 */

Bool RpcOut_SendOneRawPreallocated(void *request, size_t reqLen, char *reply,
                                   size_t repLen);

这段代码定义了 RpcOut 模块的接口,用于实现从虚拟机向 VMware 虚拟化层发送 RPC (Remote Procedure Call) 请求。

  1. 通信机制:
    RpcOut 提供了一种机制,允许虚拟机内部的程序与 VMware 虚拟化层进行通信。

  2. 发送请求:
    虚拟机可以使用这个接口向 VMware 发送各种请求,如获取信息、执行操作等。

  3. 接收响应:
    RpcOut 不仅可以发送请求,还可以接收来自 VMware 的响应。

  4. 灵活性:
    提供了多种方法来构造和发送 RPC 请求,适应不同的使用场景。

  5. 主要功能:

    • RpcOut_sendOne: 最常用的方法,用于发送单个 RPC 请求并接收响应。
    • RpcOut_SendOneRaw: 处理 UTF-8 字符串和可能被 Win32 FormatMessage 函数破坏的数据。
    • RpcOut_SendOneRawPreallocated: 允许调用者提供接收缓冲区,避免内部 malloc 调用。
  6. 生命周期管理:
    提供了构造函数 (RpcOut_Construct) 和析构函数 (RpcOut_Destruct) 来管理 RpcOut 对象的生命周期。

  7. 会话控制:
    包含 RpcOut_start 和 RpcOut_stop 方法,用于开始和结束 RPC 会话。

/open-vm-tools/lib/include/rpin.h

RpcIn *RpcIn_Construct(GMainContext *mainCtx,
                       RpcIn_Callback dispatch,
                       gpointer clientData);

Bool RpcIn_start(RpcIn *in, unsigned int delay,
                 RpcIn_ErrorFunc *errorFunc,
                 RpcIn_ClearErrorFunc *clearErrorFunc,
                 void *errorData);

#else /* } { */

#include "dbllnklst.h"

/*
 * Type for old RpcIn callbacks. Don't use this anymore - this is here
 * for backwards compatibility.
 */
typedef Bool
(*RpcIn_Callback)(char const **result,     // OUT
                  size_t *resultLen,       // OUT
                  const char *name,        // IN
                  const char *args,        // IN
                  size_t argsSize,         // IN
                  void *clientData);       // IN

RpcIn *RpcIn_Construct(DblLnkLst_Links *eventQueue);

Bool RpcIn_start(RpcIn *in, unsigned int delay,
                 RpcIn_Callback resetCallback, void *resetClientData,
                 RpcIn_ErrorFunc *errorFunc,
                 RpcIn_ClearErrorFunc *clearErrorFunc,
                 void *errorData);


/*
 * Don't use this function anymore - it's here only for backwards compatibility.
 * Use RpcIn_RegisterCallbackEx() instead.
 */
void RpcIn_RegisterCallback(RpcIn *in, const char *name,
                            RpcIn_Callback callback, void *clientData);

void RpcIn_UnregisterCallback(RpcIn *in, const char *name);

unsigned int RpcIn_SetRetVals(char const **result, size_t *resultLen,
                              const char *resultVal, Bool retVal);

#endif /* } */

void RpcIn_Destruct(RpcIn *in);
void RpcIn_stop(RpcIn *in);

RpcIn 模块的主要用途是处理从 VMware 虚拟化层到虚拟机内部的 RPC (Remote Procedure Call) 请求。RpcIn 模块为虚拟机内部的程序提供了一个框架,用于接收和处理来自 VMware 虚拟化层的 RPC 请求。

  1. 接收请求:
    RpcIn 允许虚拟机内部的程序接收来自 VMware 虚拟化层的 RPC 请求。

  2. 回调机制:
    提供了注册回调函数的机制(RpcIn_RegisterCallback),用于处理特定类型的 RPC 请求。

  3. 事件驱动:
    使用事件队列(DblLnkLst_Links *eventQueue)来处理异步的 RPC 请求。

  4. 生命周期管理:
    提供了构造函数(RpcIn_Construct)和析构函数(RpcIn_Destruct)来管理 RpcIn 对象的生命周期。

  5. 启动和停止:
    包含 RpcIn_start 和 RpcIn_stop 方法,用于开始和结束 RPC 监听。

  6. 错误处理:
    支持错误处理函数(RpcIn_ErrorFunc)和清除错误函数(RpcIn_ClearErrorFunc)。

  7. 灵活性:
    允许设置延迟(delay)和重置回调(resetCallback),以适应不同的使用场景。

  8. 响应设置:
    提供 RpcIn_SetRetVals 函数来设置 RPC 调用的返回值。

  9. 向后兼容:
    保留了一些旧的接口(如旧版的 RpcIn_RegisterCallback),以保持向后兼容性。

  10. 多平台支持:
    通过条件编译(#ifdef __cplusplus),支持在 C 和 C++ 环境中使用。

  11. 自定义处理:
    允许注册自定义的回调函数来处理特定名称的 RPC 请求。

  12. 动态注册和注销:
    提供了注册(RpcIn_RegisterCallback)和注销(RpcIn_UnregisterCallback)回调的方法,允许动态地添加或移除 RPC 处理程序。

实例RpcOutSendOneRawWork


/*
 *-----------------------------------------------------------------------------
 *
 * RpcOutSendOneRawWork --
 *
 *    Helper function to make VMware execute a RPCI command.  See
 *    RpcOut_SendOneRaw and RpcOut_SendOneRawPreallocated.
 *
 *-----------------------------------------------------------------------------
 */

static Bool
RpcOutSendOneRawWork(void *request,         // IN: RPCI command
                     size_t reqLen,         // IN: Size of request buffer
                     char *callerReply,     // IN: caller supplied reply buffer
                     size_t callerReplyLen, // IN: size of caller supplied buf
                     char **reply,          // OUT: Result
                     size_t *repLen)        // OUT: Length of the result
{
   Bool status;
   Bool rpcStatus;
   /* Stack allocate so this can be used in kernel logging.  See 1389199. */
   RpcOut out;
   char const *myReply;
   size_t myRepLen;

   Debug("Rpci: Sending request='%s'\n", (char *)request);
   RpcOutInitialize(&out);
   if (!RpcOut_startWithReceiveBuffer(&out, callerReply, callerReplyLen)) {
      myReply = "RpcOut: Unable to open the communication channel";
      myRepLen = strlen(myReply);

      if (callerReply != NULL) {
         unsigned s = MIN(callerReplyLen - 1, myRepLen);
         ASSERT(reply == NULL);
         memcpy(callerReply, myReply, s);
         callerReply[s] = '\0';
      }
      if (reply != NULL) {
         *reply = NULL;
      }
      return FALSE;
   }

   status = RpcOut_send(&out, request, reqLen,
                        &rpcStatus, &myReply, &myRepLen);
   /* On failure, we already have the description of the error */

   Debug("Rpci: Sent request='%s', reply='%s', len=%"FMTSZ"u, "
         "status=%d, rpcStatus=%d\n",
         (char *)request, myReply, myRepLen, status, rpcStatus);

   if (reply != NULL) {
      /*
       * If we got a non-NULL reply, make a copy of it, because the reply
       * we got back is inside the channel buffer, which will get destroyed
       * at the end of this function.
       */
      if (myReply != NULL) {
         /*
          * We previously used strdup to duplicate myReply, but that
          * breaks if you are sending binary (not string) data over the
          * backdoor. Don't assume the data is a string.
          */
         *reply = malloc(myRepLen + 1);
         if (*reply != NULL) {
            memcpy(*reply, myReply, myRepLen);
            /*
             * The message layer already writes a trailing NUL but we might
             * change that someday, so do it again here.
             */
            (*reply)[myRepLen] = 0;
         }
      } else {
         /*
          * Our reply was NULL, so just pass the NULL back up to the caller.
          */
         *reply = NULL;
      }

      /*
       * Only set the length if the caller wanted it and if we got a good
       * reply.
       */
      if (repLen != NULL && *reply != NULL) {
         *repLen = myRepLen;
      }
   }

   if (RpcOut_stop(&out) == FALSE) {
      /*
       * We couldn't stop the channel. Free anything we allocated, give our
       * client a reply of NULL, and return FALSE.
       */

      if (reply != NULL) {
         free(*reply);
         *reply = NULL;
      }
      Debug("Rpci: unable to close the communication channel\n");
      status = FALSE;
   }

   return status && rpcStatus;
}

在 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 过程。

实例 vmware-rpctool 'info-get guestinfo.ip'

在这里插入图片描述

  1. 用户输入命令:
    用户在虚拟机内执行 vmware-rpctool 'info-get guestinfo.ip'

  2. vmware-rpctool 处理:

    • vmware-rpctool 解析命令,识别为 “info-get” 操作
    • 准备 RPC 请求内容:“info-get guestinfo.ip”
  3. RpcOut 初始化:

    • vmware-rpctool 内部初始化 RpcOut 结构
  4. 发送请求(RpcOut):

    • 调用 RpcOut_send 函数
    • 请求通过预定义通道(如 VSockets 或 backdoor)发送到 VMX
  5. VMX 接收请求(RpcIn):

    • VMX 中的 RpcIn 模块接收到请求
    • 触发 HandleRpcIn 函数处理incoming请求
  6. 请求解析:

    • HandleRpcIn 函数解析 “info-get guestinfo.ip” 请求
  7. GuestRPC 表查找:

    • VMX 在 GuestRPC 表中查找 “info-get” 对应的处理函数
  8. 执行处理函数:

    • 找到并执行 HandleInfoGet 函数
    • 此函数专门处理 “info-get” 类型的请求
  9. 获取 IP 地址:

    • HandleInfoGet 函数识别 “guestinfo.ip” 参数
    • 调用内部函数获取虚拟机的 IP 地址
  10. 准备响应:

    • 假设 IP 为 “192.168.1.100”
    • 准备响应字符串,如 “1 192.168.1.100”
    • “1” 表示成功,后面跟着实际 IP
  11. 设置响应:

    • 使用 RpcIn_SetRetVals 函数设置响应内容
  12. 发送响应(RpcIn):

    • 调用 RpcInSend 函数,将响应发送回虚拟机
  13. 虚拟机接收响应(RpcOut):

    • RpcOut_send 函数在虚拟机端接收响应
  14. 响应处理:

    • vmware-rpctool 解析响应,提取 IP 地址
  15. 显示结果:

    • vmware-rpctool 将 IP 地址显示到控制台
  16. 完成:

    • 命令执行完毕,用户看到 IP 地址输出

各个步骤对应的backdoor操作

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( reply type from subcommand 03h 就是之前的rpc成功还是失败) ,他决定了执行的路径(根据不同的 reply type 值,程序会执行不同的处理逻辑。也就是说, reply type 决定了后续的执行路径)。和发送数据一样,每次只能够接受 4 个字节的数据(所以vmx也是一样每次rpc请求只会返回四个字节过来,所以需要多次rpc请求才能将相关结果全部返回过来)。需要注意的是,在 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 04h
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 from subcommand 03h 。在接收完 reply 的数据后,调用此命令。如果没有通过 Receive RPC reply data 接收完整个 reply 数据(四个字节)的话(就是 Receive RPC reply data执行失败),就会返回 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"
        );
}

VMWareRPCChannel 和 BufferedRPCChannel 类

vmx还提供了一种方便的面向对象的方式来执行 GuestRPC 命令:VMWareRPCChannel 和 BufferedRPCChannel 类。通过使用 VMWareRPCChannel 类,可以执行 VMWare 支持的任意 GuestRPC 请求

VMWareRPCChannel 和 BufferedRPCChannel 是通过 VMware 实现虚拟机和主机之间通信的类。它们用于管理和优化远程过程调用(RPC)的数据传输。它们是对之前一系列的封装

BufferedRPCChannel 是在 VMWareRPCChannel 基础上实现的一个增强类,主要增加了缓冲机制以优化数据传输

配置

vmmware脚本安装
一般题目会提供 vmware 版本和 patch 过的 vmware-vmx 二进制文件,这就需要我们能够找到对应版本的 vmware 安装脚本。找到对应的linux安装脚本(只要能让 vmware-vmx 跑起来就可以 ),如果装不上也可以选择找这个驱动项目下载下来然后手动编译安装

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

cd vmware-host-modules
git checkout w15.5.0

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

cd vmmon-only
make
cd ../vmnet-only
make
cd ..
sudo insmod vmmon.o
sudo insmod vmnet.o

vmmon 和 vmnet 是 VMware 虚拟机中的两个重要模块,它们分别负责不同的功能:

  1. vmmon (VMware Monitor):

    • 这是一个内核模块,负责虚拟机的核心功能,如CPU、内存和硬件设备的虚拟化。
    • 它模拟了一个完整的计算机系统,包括CPU、内存、磁盘、网卡等硬件设备,使得虚拟机能够像运行在物理硬件上一样运行操作系统和应用程序。
    • 比如,当你在 VMware 虚拟机中运行 Windows 操作系统时,vmmon 模块就负责将你的物理 CPU 和内存资源虚拟化,让 Windows 系统感觉自己是在独立的硬件上运行。
  2. vmnet (VMware Network):

    • 这是一个用于虚拟网络的内核模块。
    • 它负责创建和管理虚拟网络设备,如虚拟交换机、虚拟路由器等,以及虚拟机之间的网络连接。
    • 比如,当你在 VMware 虚拟机中设置了一个"桥接"网络模式时,vmnet 模块就会创建一个虚拟交换机,将虚拟机的网卡连接到物理网络上,使虚拟机能够像物理机一样访问外部网络。

存在虚拟机嵌套,最好使用带有英特尔的 CPU 的电脑进行。

 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 
将当前目录下的 vmware-vmx_patched 文件复制到 /usr/lib/vmware/bin/ 目录,并命名为 vmware-vmx。
如果目标位置已经存在同名文件,这个命令会覆盖它。

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

安装虚拟机
在安装好的 vmware 中安装 ubuntu 18.04.1 。
在这里插入图片描述
启动过程慢的惊人,我电脑太辣鸡了
在这里插入图片描述
成功,心情舒畅

调试

  • 在host里我们使用sudo gdb ./vmware-vmx_patched -q启动gdb,
  • 之后启动VMware和guest,然后使用ps -aux | grep vmware-vmx得到进程pid,在gdb中使用attach $pidattach到该进程,
  • -为了方便先echo 0 > /proc/sys/kernel/randomize_va_space关闭地址随机化,
  • 之后b* 0x0000555555554000 + 偏移 下个断点再continue让虚拟机进程继续。
  • 然后虚拟机里执行相关的rpc函数来动态调试跟进到漏洞

工具

bindiff下载和使用

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐