轉:利用一個堆溢出漏洞實現VMware虛擬機逃逸
轉:https://zhuanlan.zhihu.com/p/27733895?utm_source=tuicool&utm_medium=referral
利用一個堆溢出漏洞實現VMware虛擬機逃逸
[作者:李小龍(acez),中文翻譯:kelwin]
1. 介紹
2017年3月,長亭安全研究實驗室(Chaitin Security Research Lab)參加了Pwn2Own黑客大賽,我作為團隊的一員,一直專註於VMware Workstation Pro的破解,並成功在賽前完成了一個虛擬機逃逸的漏洞利用。(很不)幸運的是,就在Pwn2Own比賽的前一天(3月14日),VMware發布了一個新的版本,其中修復了我們所利用的漏洞。在本文中,我會介紹我們從發現漏洞到完成利用的整個過程。感謝@kelwin在實現漏洞利用過程中給予的幫助,也感謝ZDI的朋友,他們近期也發布了一篇相關博客,正是這篇博文促使我們完成本篇writeup。
本文主要由三部分組成:首先我們會簡要介紹VMware中的RPCI機制,其次我們會描述本文使用的漏洞,最後講解我們是如何利用這一個漏洞來繞過ASLR並實現代碼執行的。
2. VMware RPCI機制
VMware實現了多種虛擬機(下文稱為guest)與宿主機(下文稱文host)之間的通信方式。其中一種方式是通過一個叫做Backdoor的接口,這種方式的設計很有趣,guest只需在用戶態就可以通過該接口發送命令。VMware Tools也部分使用了這種接口來和host通信。我們來看部分相關代碼(摘自open-vm-tools中的lib/backdoor/backdoorGcc64.c):
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"
"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"
#ifdef __APPLE__
"popq %%rbx" "\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 __APPLE__
/* %rbx is unchanged at the end of the function on Mac OS. */
"rbx",
#endif
"rcx", "rdx", "rsi", "rdi", "memory"
);
}
上面的代碼中出現了一個很奇怪的指令inl。在通常環境下(例如Linux下默認的I/O權限設置),用戶態程序是無法執行I/O指令的,因為這條指令只會讓用戶態程序出錯並產生崩潰。而此處這條指令產生的權限錯誤會被host上的hypervisor捕捉,從而實現通信。Backdoor所引入的這種從guest上的用戶態程序直接和host通信的能力,帶來了一個有趣的攻擊面,這個攻擊面正好滿足Pwn2Own的要求:“在這個類型(指虛擬機逃逸這一類挑戰)中,攻擊必須從guest的非管理員帳號發起,並實現在host操作系統中執行任意代碼”。guest將0x564D5868存入$eax,I/O端口號0x5658或0x5659存儲在$dx中,分別對應低帶寬和高帶寬通信。其它寄存器被用於傳遞參數,例如$ecx的低16位被用來存儲命令號。對於RPCI通信,命令號會被設為BDOOR_CMD_MESSAGE(=30)。文件lib/include/backdoor_def.h中包含了一些支持的backdoor命令列表。host捕捉到錯誤後,會讀取命令號並分發至相應的處理函數。此處我省略了很多細節,如果你有興趣可以閱讀相關源碼。
2.1 RPCI
遠程過程調用接口RPCI(Remote Procedure Call Interface)是基於前面提到的Backdoor機制實現的。依賴這個機制,guest能夠向host發送請求來完成某些操作,例如,拖放(Drag n Drop)/復制粘貼(Copy Paste)操作、發送或獲取信息等等。RPCI請求的格式非常簡單:<命令> <參數>。例如RPCI請求info-get guestinfo.ip可以用來獲取guest的IP地址。對於每個RPCI命令,在vmware-vmx進程中都有相關註冊和處理操作。
需要註意的是有些RPCI命令是基於VMCI套接字實現的,但此內容已超出本文討論的範疇。
3. 漏洞
花了一些時間逆向各種不同的RPCI處理函數之後,我決定專註於分析拖放(Drag n Drop,下面簡稱為DnD)和復制粘貼(Copy Paste,下面簡稱為CP)功能。這部分可能是最復雜的RPCI命令,也是最可能找到漏洞的地方。在深入理解的DnD/CP內部工作機理後,可以很容易發現,在沒有用戶交互的情況下,這些處理函數中的許多功能是無法調用的。DnD/CP的核心功能維護了一個狀態機,在無用戶交互(例如拖動鼠標從host到guest中)情況下,許多狀態是無法達到的。
我決定看一看Pwnfest 2016上被利用的漏洞,該漏洞在這個VMware安全公告中有所提及。此時我的idb已經標上了很多符號,所以很容易就通過bindiff找到了補丁的位置。下面的代碼是修補之前存在漏洞的函數(可以看出services/plugins/dndcp/dnddndCPMsgV4.c中有對應源碼,漏洞依然存在於open-vm-tools的git倉庫的master分支當中):
static Bool DnDCPMsgV4IsPacketValid(const uint8 *packet,
size_t packetSize)
{
DnDCPMsgHdrV4 *msgHdr = NULL;
ASSERT(packet);
if (packetSize < DND_CP_MSG_HEADERSIZE_V4) {
return FALSE;
}
msgHdr = (DnDCPMsgHdrV4 *)packet;
/* Payload size is not valid. */
if (msgHdr->payloadSize > DND_CP_PACKET_MAX_PAYLOAD_SIZE_V4) {
return FALSE;
}
/* Binary size is not valid. */
if (msgHdr->binarySize > DND_CP_MSG_MAX_BINARY_SIZE_V4) {
return FALSE;
}
/* Payload size is more than binary size. */
if (msgHdr->payloadOffset + msgHdr->payloadSize > msgHdr->binarySize) { // [1]每個包的binarySize可以手動設置,但是程序默認為不修改。
return FALSE;
}
return TRUE;
}
Bool
DnDCPMsgV4_UnserializeMultiple(DnDCPMsgV4 *msg,
const uint8 *packet,
size_t packetSize)
{
DnDCPMsgHdrV4 *msgHdr = NULL;
ASSERT(msg);
ASSERT(packet);
if (!DnDCPMsgV4IsPacketValid(packet, packetSize)) {//檢查長度
return FALSE;
}
msgHdr = (DnDCPMsgHdrV4 *)packet;
/*
* For each session, there is at most 1 big message. If the received
* sessionId is different with buffered one, the received packet is for
* another another new message. Destroy old buffered message.
*/
if (msg->binary &&
msg->hdr.sessionId != msgHdr->sessionId) {
DnDCPMsgV4_Destroy(msg);
}
/* Offset should be 0 for new message. */
if (NULL == msg->binary && msgHdr->payloadOffset != 0) {
return FALSE;
}
/* For existing buffered message, the payload offset should match. */
if (msg->binary &&
msg->hdr.sessionId == msgHdr->sessionId &&
msg->hdr.payloadOffset != msgHdr->payloadOffset) {
return FALSE;
}
if (NULL == msg->binary) {
memcpy(msg, msgHdr, DND_CP_MSG_HEADERSIZE_V4);
msg->