VirtualBox虛擬機器最新逃逸漏洞E1000 0 day詳細分析(上)
近日,俄羅斯安全研究人員Sergey Zelenyuk釋出了有關VirtualBox 5.2.20及早期版本的0 day漏洞的詳細資訊,這些版本可以讓攻擊者逃離虛擬機器並在主機上執行RING 3層的程式碼。然後,攻擊者可以利用傳統的攻擊技術將許可權提升至RING 0層。
下面是Sergey Zelenyuk在他的Github中披露的關於該漏洞的全部細節:
為什麼要披露漏洞詳情
我喜歡VirtualBox,它與我為什麼釋出0 day漏洞無關。原因是我對當代資訊保安狀態有不同的意見,尤其是安全研究的漏洞獎勵:
1.從提交漏洞開始等待半年,直到修補了漏洞為止。
2.在bug賞金描述中:
·等待最多一個月,直到驗證了提交的漏洞,然後才決定購買或不購買。
· 動態的改變決定。今天你在軟體中找出了bug賞金計劃中列出的購買漏洞,但一週之後你可能會發現,他們已經對這些漏洞和漏洞利用程式“失去感興趣”。
· 沒有一個精確的軟體列表來說明bug賞金規則。對於bug賞金髮布者很方便,但對於安全研究人員來說這很尷尬。
· 沒有精確的漏洞價格下限和上限。影響價格的因素有很多,但安全研究人員需要知道什麼是值得研究的,什麼不是。
3.妄想的廢話:命名漏洞併為他們建立網站; 一年內召開上千次會議; 誇大自己作為安全研究員的工作的重要性; 認為自己是“世界救世主”。
我已經厭倦了前兩個事情,因此我的作風是全面披露。Infosec,請繼續向前進!
一般資訊
易受攻擊的軟體: VirtualBox 5.2.20及以前的版本。
宿主機作業系統: 任何作業系統,這個漏洞是在共享程式碼庫中。
虛擬機器作業系統: 任何作業系統。
虛擬機器配置: 預設(唯一的要求是網絡卡需要是Intel PRO/1000 MT Desktop(82540EM),網絡卡模式是NAT)。
如何保護自己
在VirtualBox的安全補丁構建完成之前,你可以將虛擬機器的網絡卡的模式更改為 PCnet(兩者之一)或者是半虛擬化網路。如果不能,請將模式從NAT更改為另一個模式。前一種方式更安全。
介紹
預設的VirtualBox虛擬網路裝置是Intel PRO/1000 MT Desktop(82540EM),預設的網路模式是NAT。我們將其稱為E1000。
E1000有一個漏洞,利用這個漏洞攻擊者在虛擬機器中拿到了root或者administrator許可權後,就可以逃逸到宿主機的RING3層。然後,攻擊者可以使用現有的提權技術通過/dev/vboxdrv將許可權升級為RING0。
漏洞詳細資訊
E1000 101
如果要傳送網路資料包,虛擬機器需要像常見的PC那樣操作:需要配置網絡卡併為虛擬機器提供網路資料包。資料包是資料鏈路層幀和其他更高級別的網路協議頭。提供給介面卡的資料包包裝在Tx描述符中(Tx表示傳送)。Tx描述符是一個在82540EM資料表(317453006EN.PDF,Revision 4.0)中描述的資料結構。它儲存了資料包大小,VLAN標籤,啟用TCP/IP分段的標誌等元資訊。
82540EM資料表提供了三種Tx描述符型別:遺留(legacy),上下文(context),資料(data)。legacy應該已被棄用。另外兩個需要一起使用。我們唯一關心的是上下文描述符設定的最大資料包大小並切換TCP/IP分段,並且資料描述符儲存了網路資料包的實體地址及其大小。資料描述符的資料包大小必須小於上下文描述符的最大資料包大小。通常將上下文描述符在資料描述符之前提供給網絡卡。
為了向網絡卡提供Tx描述符,虛擬機器將它寫入了Tx Ring。這是一個駐留在預定義地址的實體記憶體中的RING緩衝區。當所有描述符被寫入Tx RING時,虛擬機器會更新E1000 MMIO TDT暫存器(Transmit Descriptor Tail)來告知宿主機有新的描述符要處理。
輸入
假設有以下Tx描述符陣列:
[context_1, data_2, data_3, context_4, data_5]
讓我們按如下方式填充結構欄位(欄位的名稱已經假設為人類可讀的字詞,但欄位的值直接對映到了82540EM規範):
context_1.header_length = 0 context_1.maximum_segment_size = 0x3010 context_1.tcp_segmentation_enabled = true data_2.data_length = 0x10 data_2.end_of_packet = false data_2.tcp_segmentation_enabled = true data_3.data_length = 0 data_3.end_of_packet = true data_3.tcp_segmentation_enabled = true context_4.header_length = 0 context_4.maximum_segment_size = 0xF context_4.tcp_segmentation_enabled = true data_5.data_length = 0x4188 data_5.end_of_packet = true data_5.tcp_segmentation_enabled = true
我們將逐步分析,瞭解它們為什麼是這樣被填充的。
根本原因分析
[context_1,data_2,data_3]處理過程
假設上面的描述符以指定的順序寫入Tx RING,並且虛擬機器更新TDT暫存器。現在宿主機將在src/VBox/Devices/Network/DevE1000.cpp檔案中執行e1kXmitPending函式(為了便於閱讀程式碼,已經刪掉了大部分註釋):
static int e1kXmitPending(PE1KSTATE pThis, bool fOnWorkerThread) { ... while (!pThis->fLocked && e1kTxDLazyLoad(pThis)) { while (e1kLocateTxPacket(pThis)) { fIncomplete = false; rc = e1kXmitAllocBuf(pThis, pThis->fGSO); if (RT_FAILURE(rc)) goto out; rc = e1kXmitPacket(pThis, fOnWorkerThread); if (RT_FAILURE(rc)) goto out; }
e1kTxDLazyLoad函式將讀取Tx RING中的所有5個Tx描述符。然後會第一次呼叫e1kLocateTxPacket。此函式會遍歷所有的描述符並設定初始狀態,但實際上並未處理它們。在我們的例子中,對e1kLocateTxPacket的第一次呼叫將處理context_1,data_2和data_3這三個描述符。其餘兩個描述符context_4和data_5將在while迴圈的第二次迭代中處理(我們將在下一節中介紹第二次迭代)。這個由兩部分組成的陣列分配對於觸發漏洞至關重要,因此讓我們弄清楚其中的原因。
e1kLocateTxPacket的程式碼看起來像這樣:
static bool e1kLocateTxPacket(PE1KSTATE pThis) { ... for (int i = pThis->iTxDCurrent; i < pThis->nTxDFetched; ++i) { E1KTXDESC *pDesc = &pThis->aTxDescriptors[i]; switch (e1kGetDescType(pDesc)) { case E1K_DTYP_CONTEXT: e1kUpdateTxContext(pThis, pDesc); continue; case E1K_DTYP_LEGACY: ... break; case E1K_DTYP_DATA: if (!pDesc->data.u64BufAddr || !pDesc->data.cmd.u20DTALEN) break; ... break; default: AssertMsgFailed(("Impossible descriptor type!")); }
第一個描述符(context_1)是E1K_DTYP_CONTEXT,因此呼叫e1kUpdateTxContext函式。如果為描述符啟用了TCP分段,則此功能會更新TCP分段上下文。同樣,對於context_1也是如此,因此也會更新TCP分段上下文。(TCP分段上下文更新實際上做了什麼,並不重要,我們將使用它來引用下面的程式碼)。
第二個描述符(data_2)是E1K_DTYP_DATA,將執行一些沒必要在此討論的一些操作。
第三個描述符(data_3)也是E1K_DTYP_DATA,但由於data_3.data_length == 0,因此不執行任何操作。
目前,開頭的三個描述符已經處理了,還剩下兩個描述符。現在的事情是:在switch語句之後,檢查描述符的end_of_packet欄位是否已設定。對於data_3描述符(data_3.end_of_packet == true)也是如此。程式碼執行了一些操作並從函式返回:
if (pDesc->legacy.cmd.fEOP) { ... return true; }
如果data_3.end_of_packet為false,則將處理剩餘的context_4和data_5描述符,並且將繞過該漏洞。下面你會看到為什麼從函式返回會觸發漏洞。
在e1kLocateTxPacket函式結束時,我們準備好這幾個描述符來解包網路資料包併發送到網路中:context_1,data_2,data_3。然後e1kXmitPending的內部迴圈會呼叫e1kXmitPacket。這個函式迭代遍歷所有的描述符(在我們的例子中是第五個描述符)來實際處理它們:
static int e1kXmitPacket(PE1KSTATE pThis, bool fOnWorkerThread) { ... while (pThis->iTxDCurrent < pThis->nTxDFetched) { E1KTXDESC *pDesc = &pThis->aTxDescriptors[pThis->iTxDCurrent]; ... rc = e1kXmitDesc(pThis, pDesc, e1kDescAddr(TDBAH, TDBAL, TDH), fOnWorkerThread); ... if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP) break; }
每個描述符都會呼叫並傳入e1kXmitDesc函式:
static int e1kXmitDesc(PE1KSTATE pThis, E1KTXDESC *pDesc, RTGCPHYS addr, bool fOnWorkerThread) { ... switch (e1kGetDescType(pDesc)) { case E1K_DTYP_CONTEXT: ... break; case E1K_DTYP_DATA: { ... if (pDesc->data.cmd.u20DTALEN == 0 || pDesc->data.u64BufAddr == 0) { E1kLog2(("% Empty data descriptor, skipped.\n", pThis->szPrf)); } else { if (e1kXmitIsGsoBuf(pThis->CTX_SUFF(pTxSg))) { ... } else if (!pDesc->data.cmd.fTSE) { ... } else { STAM_COUNTER_INC(&pThis->StatTxPathFallback); rc = e1kFallbackAddToFrame(pThis, pDesc, fOnWorkerThread); } } ...
傳遞給e1kXmitDesc的第一個描述符是context_1。該函式對上下文描述符不起作用。
傳遞給e1kXmitDesc的第二個描述符是data_2。由於我們所有的資料描述符都有tcp_segmentation_enable == true(上面的pDesc-> data.cmd.fTSE)這個欄位和值,因此我們呼叫e1kFallbackAddToFrame函式,在處理data_5時會出現整數下溢。
static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc, bool fOnWorkerThread) { ... uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN + pThis->contextTSE.dw3.u16MSS; /* * Carve out segments. */ int rc = VINF_SUCCESS; do { /* Calculate how many bytes we have left in this TCP segment */ uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen; if (cb > pDesc->data.cmd.u20DTALEN) { /* This descriptor fits completely into current segment */ cb = pDesc->data.cmd.u20DTALEN; rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread); } else { ... } pDesc->data.u64BufAddr+= cb; pDesc->data.cmd.u20DTALEN -= cb; } while (pDesc->data.cmd.u20DTALEN > 0 && RT_SUCCESS(rc)); if (pDesc->data.cmd.fEOP) { ... pThis->u16TxPktLen = 0; ... } return VINF_SUCCESS; /// @todo consider rc; }
這裡最重要的變數是u16MaxPktLen,pThis-> u16TxPktLen和pDesc-> data.cmd.u20DTALEN。
讓我們繪製一個表,對比一下兩個資料描述符在執行e1kFallbackAddToFrame函式之前和之後指定的一些變數的值的變化情況。
你只需要注意,當處理data_3時,pThis-> u16TxPktLen等於0x10。
接下來是最重要的部分。請再看一下e1kXmitPacket函式程式碼的結尾:
if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP) break;
由於data_3的型別!= E1K_DTYP_CONTEXT並且data_3.end_of_packet == true,我們從迴圈中斷,儘管還有context_4和data_5要處理。它為什麼如此重要呢?理解漏洞的關鍵是要了解所有上下文描述符都是在資料描述符之前處理的。在e1kLocateTxPacket中的TCP分段上下文更新期間處理上下文描述符。稍後在e1kXmitPacket函式內的迴圈中處理資料描述符。開發人員的意圖是在處理一些資料後禁止更改u16MaxPktLen以防止程式碼中的整數下溢:
uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
但我們能夠繞過這種保護:回想一下,在e1kLocateTxPacket中,由於data_3.end_of_packet == true,我們強制函式返回。因此,我們有兩個描述符(context_4和data_5)還在等待處理,儘管pThis-> u16TxPktLen是0x10,而不是0.所以有可能使用context_4.maximum_segment_size來改變u16MaxPktLen來產生整數下溢。
[context_4,data_5]處理過程
現在,當處理前三個描述符時,我們再次進入e1kXmitPending的內部迴圈:
while (e1kLocateTxPacket(pThis)) { fIncomplete = false; rc = e1kXmitAllocBuf(pThis, pThis->fGSO); if (RT_FAILURE(rc)) goto out; rc = e1kXmitPacket(pThis, fOnWorkerThread); if (RT_FAILURE(rc)) goto out; }
這裡我們可以看作是e1kLocateTxPacket對context_4和data_5描述符進行初始化處理。我們可以將context_4.maximum_segment_size設定為小於已讀取資料的大小,即小於0x10。回想一下我們輸入的Tx描述符:
context_4.header_length = 0 context_4.maximum_segment_size = 0xF context_4.tcp_segmentation_enabled = true data_5.data_length = 0x4188 data_5.end_of_packet = true data_5.tcp_segmentation_enabled = true
作為呼叫e1kLocateTxPacket的結果,我們可以將最大分段大小的值設定為等於0xF,而已讀取的資料大小到值設定為0x10。
最後,當處理data_5時,我們再次進入e1kFallbackAddToFrame並具有以下變數值:
因此我們有一個整數下溢:
uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen; => uint32_t cb = 0xF - 0x10 = 0xFFFFFFFF;
整數下溢導致以下檢查為真,因為0xFFFFFFFF> 0x4188:
if (cb > pDesc->data.cmd.u20DTALEN) { cb = pDesc->data.cmd.u20DTALEN; rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread); }
接下來將呼叫e1kFallbackAddSegment函式,傳入的cb的大小為0x4188。如果沒有此漏洞,則無法使用cb值的大小超過0x3FA0(E1K_MAX_TX_PKT_SIZE == 0x3FA0)來呼叫e1kFallbackAddSegment,因為在e1kUpdateTxContext中的TCP分段上下文更新期間,會檢查最大段大小是否小於或等於0x3FA0:
DECLINLINE(void) e1kUpdateTxContext(PE1KSTATE pThis, E1KTXDESC *pDesc) { ... uint32_t cbMaxSegmentSize = pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4; /*VTAG*/ if (RT_UNLIKELY(cbMaxSegmentSize > E1K_MAX_TX_PKT_SIZE)) { pThis->contextTSE.dw3.u16MSS = E1K_MAX_TX_PKT_SIZE - pThis->contextTSE.dw3.u8HDRLEN - 4; /*VTAG*/ ... }
緩衝區溢位
我們使用大小為0x4188呼叫了e1kFallbackAddSegment。這怎麼可以被濫用?我發現至少有兩種可能性。首先,資料將從客戶虛擬機器讀入堆緩衝區:
static int e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr, uint16_t u16Len, bool fSend, bool fOnWorkerThread) { ... PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr, pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);
這裡pThis-> aTxPacketFallback是大小為0x3FA0的緩衝區,u16Len是0x4188 —“可以明顯導致溢位,例如,函式指標覆蓋。
其次,如果我們深入挖掘,發現e1kFallbackAddSegment呼叫了e1kTransmitFrame,可以通過一定的E1000暫存器配置呼叫e1kHandleRxPacket函式。此函式分配一個大小為0x4000的堆疊緩衝區,然後將指定長度的資料(在我們的例子中為0x4188)複製到緩衝區而不進行任何檢查:
static int e1kHandleRxPacket(PE1KSTATE pThis, const void *pvBuf, size_t cb, E1KRXDST status) { #if defined(IN_RING3) uint8_trxPacket[E1K_MAX_RX_PKT_SIZE]; ... if (status.fVP) { ... } else memcpy(rxPacket, pvBuf, cb);
如你所見,我們將整數下溢轉換為了經典的堆疊緩衝區溢位。上面的兩個溢位——堆和堆疊,我們將在下一篇文章的漏洞利用階段中使用。