vmware漏洞之一——轉:利用一個堆溢出漏洞實現VMware虛擬機逃逸

轉:https://zhuanlan.zhihu.com/p/27733895?utm_source=tuicool&utm_medium=referralhtml

小結:react

  • vmware經過Backdoor實現了虛擬機和宿主機的通訊。vmware有一個開源項目open-vm-tools,裏面包含了具體實現方式。
  • 遠程過程調用接口RPCI(Remote Procedure Call Interface,實現虛擬機向主機發送命令執行)是基於Backdoor機制實現,其在version3和4的DND/CP中均存在堆溢出漏洞:guest發送分片DnD/CP命令數據,host收到後重組,在拷貝時對每一個數據包進行長度校驗時,默認每一個包的binarySize用戶不會更改。可是用戶更改後,繞過長度檢查,從而致使溢出。
  • RPCI命令vmx.capability.dnd_version會檢查DnD/CP協議的版本是否已被修改,若是是,就會建立一個對應版本的C++對象。對於version 3,2個大小爲0xA8的C++對象會被建立,一個用於DnD命令,另外一個用於Copy/Paste命令。能夠用它分配大小爲0xA8的內存塊,並讓它分配在C++對象以前,而後利用堆溢出改寫C++對象的vtable指針,使其指向可控內存,從而實現代碼執行。
利用一個堆溢出漏洞實現VMware虛擬機逃逸

利用一個堆溢出漏洞實現VMware虛擬機逃逸

[做者:李小龍(acez),中文翻譯:kelwin]git

1. 介紹

2017年3月,長亭安全研究實驗室(Chaitin Security Research Lab)參加了Pwn2Own黑客大賽,我做爲團隊的一員,一直專一於VMware Workstation Pro的破解,併成功在賽前完成了一個虛擬機逃逸的漏洞利用。(很不)幸運的是,就在Pwn2Own比賽的前一天(3月14日),VMware發佈了一個新的版本,其中修復了咱們所利用的漏洞。在本文中,我會介紹咱們從發現漏洞到完成利用的整個過程。感謝@kelwin在實現漏洞利用過程當中給予的幫助,也感謝ZDI的朋友,他們近期也發佈了一篇相關博客,正是這篇博文促使咱們完成本篇writeup。github

本文主要由三部分組成:首先咱們會簡要介紹VMware中的RPCI機制,其次咱們會描述本文使用的漏洞,最後講解咱們是如何利用這一個漏洞來繞過ASLR並實現代碼執行的。安全

2. VMware RPCI機制

VMware實現了多種虛擬機(下文稱爲guest)與宿主機(下文稱文host)之間的通訊方式。其中一種方式是經過一個叫作Backdoor的接口,這種方式的設計頗有趣,guest只需在用戶態就能夠經過該接口發送命令。VMware Tools也部分使用了這種接口來和host通訊。咱們來看部分相關代碼(摘自open-vm-tools中的lib/backdoor/backdoorGcc64.c):session

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捕捉到錯誤後,會讀取命令號並分發至相應的處理函數。此處我省略了不少細節,若是你有興趣能夠閱讀相關源碼。ide

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中)狀況下,許多狀態是沒法達到的。post

我決定看一看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->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].void *memcpy(void *dest, const void *src, size_t n); msg->hdr.payloadOffset += msgHdr->payloadSize; return TRUE; } 

於Version 4的DnD/CP功能,當guest發送分片DnD/CP命令數據包時,host會調用上面的函數來重組guest發送的DnD/CP消息。接收的第一個包必須知足payloadOffset爲0binarySize表明堆上分配的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);//Assert通常只存在於Debug版本中 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對象?),每次溢出後都檢查泄露的數據。因爲DnD和CP對象的vtable距離vmware-vmx基地址的偏移是固定的(檢查是否包含7f),每次溢出後只須要檢查最低一些數據位,就可以判斷溢出是否到達了目標對象

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(存在於cp對象後面固定偏移處。在覆蓋cp對象時拷貝過去)

4.2.2 覆蓋DnD對象

對於DnD對象,咱們不能只覆蓋vtable指針,由於在發生溢出以後vtable會立馬被訪問(覆蓋完後的上一個函數會立刻調用call dnd+0x38),另外一個虛函數會被調用,而目前咱們只能經過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(與start不一樣的時,其會在start+10?偏移存儲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中存在,!address命令能夠查看)。

4.3 穩定性討論

正如前面說起的,由於Windows LFH堆的隨機化,當前的exploit沒法作到100%成功率。不過能夠嘗試下列方法來提升成功率:

  • 觀察0xA8大小的內存分配,考慮是否能夠經過一些malloc和free的調用來實現肯定性的LFH分配,參考這裏這裏
  • 尋找堆上的其餘C++對象,尤爲是那些能夠在堆上噴射的
  • 尋找堆上其餘帶有函數指針的對象,尤爲是那些能夠在堆上噴射的
  • 找到一個獨立的信息泄漏漏洞
  • 打開更多腦洞

4.4 演示效果


演示視頻:

 
VMware workstation 12.5.3逃逸演示—在線播放—優酷網,視頻高清在線觀看
視頻
 

5. 感想與總結

「No pwn no fun」,若是你想參加Pwn2Own這樣的比賽,你就須要準備多個漏洞,或者找到高質量的漏洞。

相關文章
相關標籤/搜索