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逃逸简介
- 虚拟机操作系统发送敏感请求,使操作系统陷入内核态
- 某些特权指令会进入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.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");
}
工作原理:
-
使用特殊的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.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) 请求。
-
通信机制:
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.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 请求。
-
接收请求:
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 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'

-
用户输入命令:
用户在虚拟机内执行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 基础上实现的一个增强类,主要增加了缓冲机制以优化数据传输
配置
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 虚拟机中的两个重要模块,它们分别负责不同的功能:
-
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下载和使用
相关文章:
CTF-pwn-虚拟化-vmmware 前置
文章目录 参考vmware逃逸简介虚拟机和主机通信机制(guest to host)共享内存(弃用)backdoor机制Message_Send和Message_RecvGuestRPC实例RpcOutSendOneRawWork实例 vmware-rpctool info-get guestinfo.ip各个步骤对应的backdoor操作Open RPC channelSend …...
thinkphp8结合layui2.9 图片上传验证
<?php declare (strict_types 1);namespace app\index\validate;use think\Validate;class Upload extends Validate {/*** 定义验证规则* 格式:字段名 > [规则1,规则2...]** var array*/protected $rule [image > fileExt:jpg,png|fileSize:204800|fi…...
农村污水处理难题:探索低成本高效解决方案
农村污水处理难题:探索低成本高效解决方案 农村污水处理作为国家生态文明建设的重要一环,面临着诸多挑战,尤其是技术落后、管理分散、资源匮乏等问题。物联网技术的引入,为解决这些痛点提供了创新途径,实现了对污水处…...
lightningcss介绍及使用
lightningcss介绍及使用 一款使用 rust 编写的 css 解析器,转换器、及压缩器。 特性 特别快:可以在毫秒级别解析、压缩大量的 css 文件,而且比其他工具的打包结果更小给值添加类型:许多其他css解析器会将值解析成一个无类型的 …...
HTTP服务的应用
1、编辑json请求参数; 2、把json发送到服务url,接收服务的返回参数; 3、解析返回参数。 procedure TfrmCustomQuery.btnFullUpdateClick(Sender: TObject); varfrm: TfrmInputQueryConditionEX;b_OK: Boolean;sBeginDate, sEndDate, sJSON…...
uni-app:踩坑路---scroll-view内使用fixed定位,无效的问题
前言: emmm,说起来这个问题整得还挺好笑的,本人在公司内,奋笔疾书写代码,愉快的提交测试的时候,测试跟我说,在苹果手机上你这个样式有bug,我倒是要看看,是什么bug。 安卓…...
MySQL4.索引及视图
1.建库 create database mydb15_indexstu; use mydb15_indexstu;2.建表 2.1 student表学(sno)号为主键,姓名(sname)不能重名,性别(ssex)仅能输入男或女,默认所在系别&a…...
MongoDB - 聚合阶段 $match、$sort、$limit
文章目录 1. $match 聚合阶段1. 构造测试数据2. $match 示例3. $match 示例 2. $sort 聚合阶段1. 排序一致性问题2. $sort 示例 3. $limit 聚合阶段 1. $match 聚合阶段 $match 接受一个指定查询条件的文档。 $match 阶段语法: { $match: { <query> } }$ma…...
ModuleNotFoundError: No module named ‘scrapy.utils.reqser‘
在scrapy中使用scrapy-rabbitmq-scheduler会出现报错 ModuleNotFoundError: No module named scrapy.utils.reqser原因是新的版本的scrapy已经摒弃了该方法,但是scrapy-rabbitmq-scheduler 没有及时的更新,所以此时有两种解决方法 方法一.将scrapy回退至旧版本,找到对应的旧版…...
vue3+ts+vite+electron+electron-packager打包成exe文件
目录 1、创建vite项目 2、添加需求文件 3、根据package.json文件安装依赖 4、打包 5、electron命令运行 6、electron-packager打包成exe文件 Build cross-platform desktop apps with JavaScript, HTML, and CSS | Electron 1、创建vite项目 npm create vitelatest 2、添…...
使用脚本搭建MySQL数据库基础环境
数据库的基本概念 数据(Data) 描述事物的符号记录 包括数字,文字,图形。图像,声音,档案记录等。 以记录形式按统一格式进行存储 表 将不同的记录组织在一起 用来储存具体数据 数据库 表的集合,是…...
Parameter index out of range (2 > number of parameters, which is 1【已解决】
文章目录 1、SysLogMapper.xml添加注释导致的2、解决方法3、总结 1、SysLogMapper.xml添加注释导致的 <!--定义一个查询方法,用于获取日志列表--><!--方法ID为getLogList,返回类型com.main.server.api.model.SysLogModel,参数类型为com.main.se…...
rk3588s 定制版 USB adb , USB2.0与USB3.0 区别,adb 由typeC 转换到USB3.0(第二部分)
硬件资源: rk3588s 核心板定制的地板 软件资源: 网盘上的 android12 源码 1 硬件上 客户只想使用 type c 接口中的 usb2.0 OTG 。在硬件上,甚至连 CC芯片都没有连接。 关于一些前置的知识。 1 USB2.0 与 USB3.0 的区别。 usb3.0 兼容2.0 …...
Cookie与Session 实现登录操作
Cookie Cookie 是网络编程中使用最广泛的一项技术,主要用于辨识用户身份。 客户端(浏览器)与网站服务端通讯的过程如下图所示: 从图中看,服务端既要返回 Cookie 给客户端,也要读取客户端提交的 Cookie。所…...
通过IEC104转MQTT网关轻松接入阿里云平台
随着智能电网和物联网技术的飞速发展,电力系统中的传统IEC 104协议设备正面临向现代化、智能化转型的迫切需求。阿里云作为全球领先的云计算服务提供商,其强大的物联网平台为IEC 104设备的接入与数据处理提供了强大的支持。本文将深入探讨钡铼网关在MQTT…...
lua 游戏架构 之 游戏 AI (五)ai_autofight_find_way
这段Lua脚本定义了一个名为 ai_autofight_find_way 的类,继承自 ai_base 类。 lua 游戏架构 之 游戏 AI (一)ai_base-CSDN博客文章浏览阅读238次。定义了一套接口和属性,可以基于这个基础类派生出具有特定行为的AI组件。例如&…...
vue3+openLayers点击标记事件
<template><!--地图--><div class"distributeMap" id"distributeMap"></div> </template> <script lang"ts" setup> import { onMounted, reactive } from "vue"; import { Feature, Map, View }…...
深入分析 Android ContentProvider (三)
文章目录 深入分析 Android ContentProvider (三)ContentProvider 的高级使用和性能优化1. 高级使用场景1.1. 数据分页加载示例:分页加载 1.2. 使用 Loader 实现异步加载示例:使用 CursorLoader 加载数据 1.3. ContentProvider 与权限管理示例࿱…...
养宠浮毛异味双困扰?性价比高的宠物空气净化器推荐
家里养了两只银渐层,谁懂啊!一下班打开家门就看到家里飘满了猫浮毛雪,空气中还传来隐隐约约的异味。每天不是在吸毛的路上,就是在洗猫砂盆的路上,而且空气中的浮毛还很难清理干净,这是最让人头疼的问题。 …...
maven项目容器化运行之3-优雅的利用Jenkins和maven使用docker插件调用远程docker构建服务并在1Panel中运行
一.背景 在《maven项目容器化运行之1》中,我们开启了1Panel环境中docker构建服务给到了局域网。在《maven项目容器化运行之2》中,我们基本实现了maven工程创建、远程调用docker构建镜像、在1Panel选择镜像运行容器三大步骤。 但是,存在一个问…...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...
无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...
linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...
ardupilot 开发环境eclipse 中import 缺少C++
目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...
回溯算法学习
一、电话号码的字母组合 import java.util.ArrayList; import java.util.List;import javax.management.loading.PrivateClassLoader;public class letterCombinations {private static final String[] KEYPAD {"", //0"", //1"abc", //2"…...
springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...
20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
网页端 js 读取发票里的二维码信息(图片和PDF格式)
起因 为了实现在报销流程中,发票不能重用的限制,发票上传后,希望能读出发票号,并记录发票号已用,下次不再可用于报销。 基于上面的需求,研究了OCR 的方式和读PDF的方式,实际是可行的ÿ…...
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not explicitly defined.
这个警告表明您在使用Vue的esm-bundler构建版本时,未明确定义编译时特性标志。以下是详细解释和解决方案: 问题原因: 该标志是Vue 3.4引入的编译时特性标志,用于控制生产环境下SSR水合不匹配错误的详细报告1使用esm-bundler…...
