文章目录
- 参考
- vmware逃逸简介
- 虚拟机和主机通信机制(guest to host)
- 共享内存(弃用)
- backdoor机制
- Message_Send和Message_Recv
- GuestRPC
- 实例`RpcOutSendOneRawWork`
- 实例 `vmware-rpctool 'info-get guestinfo.ip'`
- 各个步骤对应的backdoor操作
- Open RPC channel
- Send RPC command length
- Send RPC command data
- Recieve RPC reply length
- Receive RPC reply data
- Finish receiving RPC reply
- Close RPC channel
- VMWareRPCChannel 和 BufferedRPCChannel 类
- 配置
- 调试
- 工具
参考
2018-RealWorld-Station-Escape
【技术分享】实战VMware虚拟机逃逸漏洞
VMware 逃逸基础知识
从0到1的虚拟机逃逸三部曲
虚拟机逃逸入门(一)
RealWorldCTF2018 Station Escape
VMware 逃逸基础知识
vmware逃逸简介
- 虚拟机操作系统发送敏感请求,使操作系统陷入内核态
- 某些特权指令会进入ring0以下的状态,即交给Hypervisor处理
- 利用Hypervisor的脆弱性漏洞使得Hypervisor执行完特权指令后不产生指令状态的返回,使得执行完指令后依然停留在内核态
- 实现了提权后,可以渗透到Hypervisor和虚拟机的其他区域,破坏虚拟化的隔离机制,完成逃逸操作。
(或者说虚拟机执行某些操作会发送请求到主机vmx,然后主机vmx来处理请求,通过利用vmx中的漏洞来使得达到提权目的)
比如我们发现了一个 Hypervisor 中的漏洞,可以被利用来阻止特权指令执行后的正常状态转换。
- 正常情况:
Guest OS 执行特权指令 -> Trap 到 Hypervisor -> Hypervisor 执行指令 -> 返回 Guest OS 用户态
- 利用漏洞后:
Guest OS 执行特权指令 -> Trap 到 Hypervisor -> Hypervisor 执行指令
-> 漏洞阻止状态转换 -> Guest OS 保持在内核态
具体实现可能如下:
-
攻击者发现 Hypervisor 在处理某些特定的特权指令时存在缓冲区溢出漏洞。
-
攻击者构造一个特殊的输入,触发这个缓冲区溢出。
-
溢出的数据覆盖了 Hypervisor 中负责状态转换的代码或数据结构。
-
当 Hypervisor 执行完特权指令,准备返回 Guest OS 时,由于关键代码被覆盖,无法正确执行状态转换。
-
结果是 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"
);
}
工作原理:
-
使用特殊的I/O指令:
VMware截获了特定的I/O指令(在这个例子中是’in’指令),并将其解释为后门调用。 -
魔术数字和命令:
使用预定义的魔术数字(BDOOR_MAGIC)和命令码来指定操作类型。 -
寄存器传参:
通过特定寄存器传递参数和接收结果。
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中)
}
}
这段代码做了以下事情:
- 设置eax为魔术数字,表明这是一个后门调用。
- 设置ecx为4,指示我们要获取鼠标位置。
- 设置edx为特殊的I/O端口号。
- 执行’in’指令,这在VMware中会被截获并处理。
- 结果存储在eax中返回。
在主函数中:
void main()
{
unsigned mousepos = GetMousePos();
printf("Mouse cursor pos: x=%d,y=%d\n", mousepos >> 16, mousepos & 0xFFFF);
}
- 调用GetMousePos()获取鼠标位置。
- 高16位表示X坐标,低16位表示Y坐标。
举例说明:
假设我们在VMware虚拟机中运行这个程序:
-
在真实机器上:
运行结果: 程序崩溃,因为'in'指令在用户模式下是不允许的。
-
在VMware虚拟机中,鼠标在(100, 200)位置:
运行结果: Mouse cursor pos: x=100,y=200
-
在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 作为底层通信渠道,但提供了更易用和更灵活的接口。
- 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。
- 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。
- 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。
- 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。
- 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:
- 功能:关闭一个预分配的通信通道。
- 参数:通道指针。
- 过程:
- 发送关闭通道请求。
- 释放接收缓冲区(如果是动态分配的)。
- 重置通道信息。
- 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) 请求。
-
通信机制:
RpcOut 提供了一种机制,允许虚拟机内部的程序与 VMware 虚拟化层进行通信。 -
发送请求:
虚拟机可以使用这个接口向 VMware 发送各种请求,如获取信息、执行操作等。 -
接收响应:
RpcOut 不仅可以发送请求,还可以接收来自 VMware 的响应。 -
灵活性:
提供了多种方法来构造和发送 RPC 请求,适应不同的使用场景。 -
主要功能:
- RpcOut_sendOne: 最常用的方法,用于发送单个 RPC 请求并接收响应。
- RpcOut_SendOneRaw: 处理 UTF-8 字符串和可能被 Win32 FormatMessage 函数破坏的数据。
- RpcOut_SendOneRawPreallocated: 允许调用者提供接收缓冲区,避免内部 malloc 调用。
-
生命周期管理:
提供了构造函数 (RpcOut_Construct) 和析构函数 (RpcOut_Destruct) 来管理 RpcOut 对象的生命周期。 -
会话控制:
包含 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 请求。
-
接收请求:
RpcIn 允许虚拟机内部的程序接收来自 VMware 虚拟化层的 RPC 请求。 -
回调机制:
提供了注册回调函数的机制(RpcIn_RegisterCallback),用于处理特定类型的 RPC 请求。 -
事件驱动:
使用事件队列(DblLnkLst_Links *eventQueue)来处理异步的 RPC 请求。 -
生命周期管理:
提供了构造函数(RpcIn_Construct)和析构函数(RpcIn_Destruct)来管理 RpcIn 对象的生命周期。 -
启动和停止:
包含 RpcIn_start 和 RpcIn_stop 方法,用于开始和结束 RPC 监听。 -
错误处理:
支持错误处理函数(RpcIn_ErrorFunc)和清除错误函数(RpcIn_ClearErrorFunc)。 -
灵活性:
允许设置延迟(delay)和重置回调(resetCallback),以适应不同的使用场景。 -
响应设置:
提供 RpcIn_SetRetVals 函数来设置 RPC 调用的返回值。 -
向后兼容:
保留了一些旧的接口(如旧版的 RpcIn_RegisterCallback),以保持向后兼容性。 -
多平台支持:
通过条件编译(#ifdef __cplusplus),支持在 C 和 C++ 环境中使用。 -
自定义处理:
允许注册自定义的回调函数来处理特定名称的 RPC 请求。 -
动态注册和注销:
提供了注册(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'
-
用户输入命令:
用户在虚拟机内执行vmware-rpctool 'info-get guestinfo.ip'
-
vmware-rpctool 处理:
- vmware-rpctool 解析命令,识别为 “info-get” 操作
- 准备 RPC 请求内容:“info-get guestinfo.ip”
-
RpcOut 初始化:
- vmware-rpctool 内部初始化 RpcOut 结构
-
发送请求(RpcOut):
- 调用 RpcOut_send 函数
- 请求通过预定义通道(如 VSockets 或 backdoor)发送到 VMX
-
VMX 接收请求(RpcIn):
- VMX 中的 RpcIn 模块接收到请求
- 触发 HandleRpcIn 函数处理incoming请求
-
请求解析:
- HandleRpcIn 函数解析 “info-get guestinfo.ip” 请求
-
GuestRPC 表查找:
- VMX 在 GuestRPC 表中查找 “info-get” 对应的处理函数
-
执行处理函数:
- 找到并执行 HandleInfoGet 函数
- 此函数专门处理 “info-get” 类型的请求
-
获取 IP 地址:
- HandleInfoGet 函数识别 “guestinfo.ip” 参数
- 调用内部函数获取虚拟机的 IP 地址
-
准备响应:
- 假设 IP 为 “192.168.1.100”
- 准备响应字符串,如 “1 192.168.1.100”
- “1” 表示成功,后面跟着实际 IP
-
设置响应:
- 使用 RpcIn_SetRetVals 函数设置响应内容
-
发送响应(RpcIn):
- 调用 RpcInSend 函数,将响应发送回虚拟机
-
虚拟机接收响应(RpcOut):
- RpcOut_send 函数在虚拟机端接收响应
-
响应处理:
- vmware-rpctool 解析响应,提取 IP 地址
-
显示结果:
- vmware-rpctool 将 IP 地址显示到控制台
-
完成:
- 命令执行完毕,用户看到 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 基础上实现的一个增强类,主要增加了缓冲机制以优化数据传输
配置
一般题目会提供 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 虚拟机中的两个重要模块,它们分别负责不同的功能:
-
vmmon (VMware Monitor):
- 这是一个内核模块,负责虚拟机的核心功能,如CPU、内存和硬件设备的虚拟化。
- 它模拟了一个完整的计算机系统,包括CPU、内存、磁盘、网卡等硬件设备,使得虚拟机能够像运行在物理硬件上一样运行操作系统和应用程序。
- 比如,当你在 VMware 虚拟机中运行 Windows 操作系统时,vmmon 模块就负责将你的物理 CPU 和内存资源虚拟化,让 Windows 系统感觉自己是在独立的硬件上运行。
-
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下载和使用