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]
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->binary = Util_SafeMalloc(msg->hdr.binarySize);
}
/* msg->hdr.payloadOffset is used as received binary size. */
memcpy(msg->binary + msg->hdr.payloadOffset,
packet + DND_CP_MSG_HEADERSIZE_V4,
msgHdr->payloadSize); // [2]
msg->hdr.payloadOffset += msgHdr->payloadSize;
return TRUE;
}
對于 Version 4 的 DnD/CP 功能,當 guest 發送分片 DnD/CP 命令數據包時,host 會調用上面的函數來重組 guest 發送的 DnD/CP 消息。接收的第一個包必須滿足 payloadOffset 為 0,binarySize 代表堆上分配的 buffer 長度。[1]處的檢查比較了包頭中的 binarySize,用來確保 payloadOffset 和 payloadSize 不會越界。在[2]處,數據會被拷入分配的 buffer 中。但是[1]處的檢查存在問題,它只對接收的第一個包有效,對于后續的數據包,這個檢查是無效的,因為代碼預期包頭中的 binarySize 和分片流中的第一個包相同,但實際上你可以在后續的包中指定更大的 binarySize 來滿足檢查,并觸發堆溢出。
所以,該漏洞可以通過發送下面的兩個分片來觸發:
packet 1{
...
binarySize = 0x100
payloadOffset = 0
payloadSize = 0x50
sessionId = 0x41414141
...
#...0x50 bytes...#
}
packet 2{
...
binarySize = 0x1000
payloadOffset = 0x50
payloadSize = 0x100
sessionId = 0x41414141
...
#...0x100 bytes...#
}
有了以上的知識,我決定看看 Version 3 中的 DnD/CP 功能中是不是也存在類似的問題。令人驚訝的是,幾乎相同的漏洞存在于 Version 3 的代碼中(這個漏洞最初通過逆向分析來發現,但是我們后來意識到 v3 的代碼也在 open-vm-tools 的 git 倉庫中):
Bool
DnD_TransportBufAppendPacket(DnDTransportBuffer *buf, // IN/OUT
DnDTransportPacketHeader *packet, // IN
size_t packetSize) // IN
{
ASSERT(buf);
ASSERT(packetSize == (packet->payloadSize + DND_TRANSPORT_PACKET_HEADER_SIZE) &&
packetSize <= DND_MAX_TRANSPORT_PACKET_SIZE &&
(packet->payloadSize + packet->offset) <= packet->totalSize &&
packet->totalSize <= DNDMSG_MAX_ARGSZ);
if (packetSize != (packet->payloadSize + DND_TRANSPORT_PACKET_HEADER_SIZE) ||
packetSize > DND_MAX_TRANSPORT_PACKET_SIZE ||
(packet->payloadSize + packet->offset) > packet->totalSize || //[1]
packet->totalSize > DNDMSG_MAX_ARGSZ) {
goto error;
}
/*
* If seqNum does not match, it means either this is the first packet, or there
* is a timeout in another side. Reset the buffer in all cases.
*/
if (buf->seqNum != packet->seqNum) {
DnD_TransportBufReset(buf);
}
if (!buf->buffer) {
ASSERT(!packet->offset);
if (packet->offset) {
goto error;
}
buf->buffer = Util_SafeMalloc(packet->totalSize);
buf->totalSize = packet->totalSize;
buf->seqNum = packet->seqNum;
buf->offset = 0;
}
if (buf->offset != packet->offset) {
goto error;
}
memcpy(buf->buffer + buf->offset,
packet->payload,
packet->payloadSize);
buf->offset += packet->payloadSize;
return TRUE;
error:
DnD_TransportBufReset(buf);
return FALSE;
}
Version 3 的 DnD/CP 在分片重組時,上面的函數會被調用。此處我們可以在[1]處看到與之前相同的情形,代碼依然假設后續分片中的 totalSize 會和第一個分片一致。因此這個漏洞可以用和之前相同的方法觸發:
packet 1{
...
totalSize = 0x100
payloadOffset = 0
payloadSize = 0x50
seqNum = 0x41414141
...
#...0x50 bytes...#
}
packet 2{
...
totalSize = 0x1000
payloadOffset = 0x50
payloadSize = 0x100
seqNum = 0x41414141
...
#...0x100 bytes...#
}
在 Pwn2Own 這樣的比賽中,這個漏洞是很弱的,因為它只是受到之前漏洞的啟發,而且甚至可以說是同一個。因此,這樣的漏洞在賽前被修補并不驚訝(好吧,也許我們并不希望這個漏洞在比賽前一天被修復)。對應的 VMware 安全公告在這里。受到這個漏洞影響的 VMWare Workstation Pro 最新版本是12.5.3。
接下來,讓我們看一看這個漏洞是如何被用來完成從 guest 到 host 的逃逸的!
4. 漏洞利用
為了實現代碼執行,我們需要在堆上覆蓋一個函數指針,或者破壞C++對象的虛表指針。
首先讓我們看一看如何將 DnD/CP 協議的設置為 version 3,依次發送下列 RPCI 命令即可:
tools.capability.dnd_version 3
tools.capability.copypaste_version 3
vmx.capability.dnd_version
vmx.capability.copypaste_version
前兩行消息分別設置了 DnD 和 Copy/Paste 的版本,后續兩行用來查詢版本,這是必須的,因為只有查詢版本才會真正觸發版本切換。RPCI 命令 vmx.capability.dnd_version 會檢查 DnD/CP 協議的版本是否已被修改,如果是,就會創建一個對應版本的C對象。對于 version 3,2個大小為 0xA8 的C對象會被創建,一個用于 DnD 命令,另一個用于 Copy/Paste 命令。
這個漏洞不僅可以讓我們控制分配的大小和溢出的大小,而且能夠讓我們進行多次越界寫。理想的話,我們可以用它分配大小為0xA8的內存塊,并讓它分配在C對象之前,然后利用堆溢出改寫C對象的 vtable 指針,使其指向可控內存,從而實現代碼執行。
這并非易事,在此之前我們必須解決一些其他問題。首先我們需要找到一個方法來繞過 ASLR,同時處理好Windows Low Fragmented Heap。
4.1 繞過ASLR
一般來說,我們需要找到一個對象,通過溢出來影響它,然后實現信息泄露。例如破壞一個帶有長度或者數據指針的對象,并且可以從guest讀取,然而我們沒有找到這種對象。于是我們逆向了更多的RPCI命令處理函數,來尋找可用的東西。那些成對的命令特別引人關注,例如你能用一個命令來設置一些數據,同時又能用相關命令來取回數據,最終我們找到的是一對命令info-set和info-get:
info-set guestinfo.KEY VALUE
info-get guestinfo.KEY
VALUE 是一個字符串,字符串的長度可以控制堆上 buffer 的分配長度,而且我們可以分配任意多的字符串。但是如何用這些字符串來泄露數據呢?我們可以通過溢出來覆蓋結尾的null字節,讓字符串連接上相鄰的內存塊。如果我們能夠在發生溢出的內存塊和 DnD 或 CP 對象之間分配一個字符串,那么我們就能泄露對象的 vtable 地址,從而我們就可以知道 vmware-vmx 的地址。盡管 Windows 的 LFH 堆分配存在隨機化,但我們能夠分配任意多的字符串,因此可以增加實現上述堆布局的可能性,但是我們仍然無法控制溢出buffer后面分配的是 DnD 還是 CP 對象。經過我們的測試,通過調整一些參數,例如分配和釋放不同數量的字符串,我們可以實現60%到80%的成功率。
下圖總結了我們構建的堆布局情況(Ov代表溢出內存塊,S代表String,T代表目標對象)。

我們的策略是:首先分配一些填滿“A”的字符串,然后通過溢出寫入一些“B”,接下來讀取所有分配的字符串,其中含有“B”的就是被溢出的字符串。這樣我們就找到了一個字符串可以被用來讀取泄露的數據,然后以 bucket 的內存塊大小 0xA8 的粒度繼續溢出,每次溢出后都檢查泄露的數據。由于 DnD 和 CP 對象的 vtable 距離 vmware-vmx 基地址的偏移是固定的,每次溢出后只需要檢查最低一些數據位,就能夠判斷溢出是否到達了目標對象。
4.2 獲取代碼執行
現在我們實現了信息泄露,也能知道溢出的是哪個C++對象,接下來要實現代碼執行。我們需要處理兩種情形:溢出 CopyPaste 和 DnD 。需要指出的是能利用的代碼路徑有很多,我們只是選擇了其中一個。
4.2.1 覆蓋 CopyPaste 對象
對于 CopyPaste 對象,我們可以覆蓋虛表指針,讓它指向我們可控的其他數據。我們需要找到一個指針,指針指向的數據是可控并被用做對象的虛表。為此我們使用了另一個 RPCI 命令 unity.window.contents.start。這個命令主要用于 Unity 模式下,在 host 上繪制一些圖像。這個操作可以讓我們往相對 vmware-vmx 偏移已知的位置寫入一些數據。該命令接收的參數是圖像的寬度和高度,二者都是32位,合并起來我們就在已知位置獲得了一個64位的數據。我們用它來作為虛表中的一個指針,通過發送一個 CopyPast 命令即可觸發該虛函數調用,步驟如下:
- 發送
unity.window.contents.start命令,通過指定參數寬度和高度,往全局變量處寫入一個64位的棧遷移 gadget 地址 - 覆蓋對象虛表指針,指向偽造的虛表(調整虛表地址偏移)
- 發送 CopyPaste 命令,觸發虛函數調用
- ROP
4.2.2 覆蓋DnD對象
對于 DnD 對象,我們不能只覆蓋 vtable 指針,因為在發生溢出之后 vtable 會立馬被訪問,另一個虛函數會被調用,而目前我們只能通過 unity 圖像的寬度和高度控制一個 qword,所以無法控制更大的虛表。
讓我們看一看 DnD 和 CP 對象的結構,總結如下(一些類似的結構可以在 open-vm-tools 中找到,但是在 vmware-vmx 中會略有區別):
DnD_CopyPaste_RpcV3{
void * vtable;
...
uint64_t ifacetype;
RpcUtil{
void * vtable;
RpcBase * mRpc;
DnDTransportBuffer{
uint64_t seqNum;
uint8_t * buffer;
uint64_t totalSize;
uint64_t offset;
...
}
...
}
}
RpcBase{
void * vtable;
...
}
我們在此省略了結構中很多與本文無關的屬性。對象中有個指針指向另一個C++對象 RpcBase,如果我們能用一個可控數據的指針的指針覆蓋 mRpc 這個域,那我們就控制了 RpcBase 的 vtable。對此我們可以繼續使用 unity.window.contents.start 命令來來控制 mRpc,該命令的另一個參數是 imgsize,這個參數代表分配的圖像 buffer 的大小。這個 buffer 分配出來后,它的地址會存在 vmware-vmx 的固定偏移處。我們可以使用命令 unity.window.contents.chunk 來填充 buffer 的內容。步驟如下:
- 發送unity.window.contents.start命令來分配一個buffer,后續我們用它來存儲一個偽造的vtable。
- 發送unity.window.contents.chunk命令來填充偽造的vtable,其中填入一個棧遷移的gadget
- 通過溢出覆蓋DnD對象的mRpc域,讓它指向存儲buffer地址的地方(某全局變量處),即寫入一個指針的指針
- 通過發送DnD命令來觸發mRpc域的虛函數調用
- ROP
P.S:vmware-vmx 進程中有一個可讀可寫可執行的內存頁(至少在版本12.5.3中存在)。
4.3 穩定性討論
正如前面提及的,因為 Windows LFH 堆的隨機化,當前的 exploit 無法做到 100% 成功率。不過可以嘗試下列方法來提高成功率:
- 觀察 0xA8 大小的內存分配,考慮是否可以通過一些malloc和free的調用來實現確定性的LFH分配,參考這里和這里。
- 尋找堆上的其他C++對象,尤其是那些可以在堆上噴射的
- 尋找堆上其他帶有函數指針的對象,尤其是那些可以在堆上噴射的
- 找到一個獨立的信息泄漏漏洞
- 打開更多腦洞
4.4 演示效果

演示視頻:VMware workstation 12.5.3逃逸演示
5. 感想與總結
“No pwn no fun”,如果你想參加 Pwn2Own 這樣的比賽,你就需要準備多個漏洞,或者找到高質量的漏洞。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/348/
暫無評論