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 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 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.
", 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_t rxPacket[E1K_MAX_RX_PKT_SIZE];
...
if (status.fVP)
{
...
}
else
memcpy(rxPacket, pvBuf, cb);
如你所見,我們將整數下溢轉換為了經典的堆棧緩衝區溢出。上面的兩個溢出——堆和堆棧,我們將在下一篇文章的漏洞利用階段中使用。
※通過iqy文件傳播FlawedAmmyy惡意軟體
※如何緩解易受攻擊的Wireshark?
TAG:嘶吼RoarTalk |