CTF-pwn-虚拟化-vmmware 前置

文章目录

  • 参考
  • 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逃逸简介

  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.hvoid
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.htypedef 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.hRpcIn *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,     // OUTsize_t *resultLen,       // OUTconst char *name,        // INconst char *args,        // INsize_t argsSize,         // INvoid *clientData);       // INRpcIn *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 commandsize_t reqLen,         // IN: Size of request bufferchar *callerReply,     // IN: caller supplied reply buffersize_t callerReplyLen, // IN: size of caller supplied bufchar **reply,          // OUT: Resultsize_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.gitcd 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下载和使用

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

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

相关文章

【MySQL进阶之路 | 高级篇】页锁+锁的思想(悲观锁和乐观锁)

1. 页锁 页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录.当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间&#xff0…

Internet Download Manager2024功能特点优势分析及使用建议及注意事项

1. Internet Download Manager简介 2. 功能特点 3. 优势分析 4. 专家评价 5. 使用建议及注意事项 6. 常见问题解答 7. 用户反馈及案例分享 8. IDM下载器的未来发展趋势 文章: 在互联网快速发展的今日,人们对于网络资源的获取和利用越来越频繁。无论…

【Python面试题收录】Python编程基础练习题①(数据类型+函数+文件操作)

本文所有代码打包在Gitee仓库中https://gitee.com/wx114/Python-Interview-Questions 一、数据类型 第一题(str) 请编写一个Python程序,完成以下任务: 去除字符串开头和结尾的空格。使用逗号(","&#…

axios请求大全

本文讲解axios封装方式以及针对各种后台接口的请求方式 axios的介绍和基础配置可以看这个文档: 起步 | Axios中文文档 | Axios中文网 axios的封装 axios封装的重点有三个,一是设置全局config,比如请求的基础路径,超时时间等,第二点是在每次…

kafka服务介绍

kafka 安装使用管理 Kafka Apache Kafka 是一个开源的分布式事件流平台,主要用于实时数据传输和流处理。它最初由 LinkedIn 开发,并在 2011 年成为 Apache 基金会的顶级项目。Kafka 设计的目标是处理大规模的数据流,同时提供高吞吐量、低延迟…

Java语言程序设计——篇八(1)

🌿🌿🌿跟随博主脚步,从这里开始→博主主页🌿🌿🌿 Java常用核心类 主要内容Object: 终极父类toString( )方法equals( )方法getClass( )方法hashCode( )方法clone( )方法finalize( )方法实战演练 …

8. kubernetes资源——ingress

kubernetes资源——ingress 一、ingress介绍1、作用2、实现方式3、核心组件 二、部署ingress1、下载ingress_1.9.6.yaml文件2、事先导入镜像3、部署ingress 三、通过ingress发布k8s中的服务1、创建服务2、创建ingress规则发布服务3、测试访问 一、ingress介绍 1、作用 ingres…

若依+AI项目开发(二)

后端代码分析 二次开发 开始执行 生成成功 创建子模块

docker安装jenkins,并配置jdk、node和maven

拉取jenkins镜像 docker pull jenkins/jenkins:2.468-jdk21 创建一个文件夹,用于二次打包jenkins镜像 mkdir -p /data/jenkins cd /data/jenkins 提前准备好jdk和maven,并放到/data/jenkins下 由于3.8.x以上版本的maven只支持https协议,我们…

深入理解SQL中的INNER JOIN操作

本文介绍了INNER JOIN的定义、使用场景、计算方法及与其他JOIN的比较。INNER JOIN是关系数据库中常用的操作,用于返回两个表中匹配的行,只有在连接条件满足时才返回数据。本文详细解释了INNER JOIN的语法及其在一对多、多对多关系中的应用,通…

Redis实战---分布式锁

1. 什么是Redis分布式锁? 分布式锁,顾名思义,就是分布式系统中使用的锁,在单体应用中我们使用synchronized、ReentrantLock来解决线程时间的共享资源的访问问题,而在分布式系统中,资源贡献问题已经由线程之…

【Ubuntu】安装 Snipaste 截图软件

Snipaste 下载安装并使用 Snipastefor more information报错解决方案每次启动软件需要输入的命令如下添加开机自启动 下载 下载地址 安装并使用 Snipaste 进入终端输入命令 # 1、进入到 Snipaste-2.8.9-Beta-x86_64.AppImage 所在目录(根据自己的下载目录而定&…

Corsearch 用 ClickHouse 替换 MySQL 进行内容和品牌保护

本文字数:3357;估计阅读时间:9 分钟 作者:ClickHouse Team 本文在公众号【ClickHouseInc】首发 Chase Richards 自 2011 年在初创公司 Marketly 担任工程负责人,直到 2020 年公司被收购。他现在是品牌保护公司 Corsear…

JAVA笔记十六

十六、异常Exception 1.概念 异常:非正常情况,包括空的引用、数组下标越界、内存溢出等 Java提供了异常对象描述这类异常情况。 Java提供了异常机制来进行处理,通过异常机制来处理程序运行期间出现的错误,可以更好地提升程序的…

波特率和比特率的区别联系【理解】

波特率(Baud rate):表示单位时间内载波调制状态变化的次数 ,单位为波特(Baud); 【值得注意的是】单位“波特”本身就已经是代表每秒的调制数,不能用“波特每秒”(Baud per second)为…

MySQL练手 --- 1141. 查询近30天活跃用户数

题目链接:1141. 查询近30天活跃用户数 思路: 题目要求:统计截至 2019-07-27(包含2019-07-27),近 30 天的每日活跃用户数(当天只要有一条活动记录,即为活跃用户) 要计算…

react中简单的配置路由

1.安装react-router-dom npm install react-router-dom 2.新建文件 src下新建page文件夹,该文件夹下新建login和index文件夹用于存放登录页面和首页,再在对应文件夹下分别新建入口文件index.js; src下新建router文件用于存放路由配置文件…

「Ant Design」Antd 中卡片如何完全不展示内容区域、按需展示内容区域、不展示标题

前言 下面是默认的 Antd 卡片&#xff0c;由以下区域组成 处理 Antd 的 Card 展示形式大致有下面三种 卡片完全不展示内容区域 const App () > (<Card title"Default size card" extra{<a href"#">More</a>} style{{ width: 300 }}b…

nginx的学习(二):负载均衡和动静分离

简介 nginx的负载均衡和动静分离的简单使用 负载均衡配置 外部访问linux的ip地址:80/edu/a.html地址&#xff0c;会轮询访问Tomcat8080和Tomcat8081服务。 Tomcat的准备 准备两个Tomcat&#xff0c;具体准备步骤在nginx的学习一的反向代理例子2中&#xff0c;在Tomcat8080…