愛伊米

Pwn2Own 比賽使用的 VirtualBox NAT 網絡卡模擬元件的漏洞分析

0x01 背景介紹

虛擬化軟體VirtualBox是一個非常有趣的目Header。模擬硬體裝置和將資料安全地傳遞到真實硬體的複雜和難度非常大。正應了那句話:哪裡有複雜性,哪裡就有漏洞。

對於 Pwn2Own,以模擬元件為目Header是一個比較好的選擇。在我看來,網路硬體模擬似乎是正確的途徑。我從一個預設元件開始:。NET 格式的 NAT 模擬程式碼在/src/VBox/Devices/Network/DrvNAT。cpp中。

在審計程式碼的過程中,我發現了一些引起我注意的程式碼:

static DECLCALLBACK(void) drvNATSendWorker(PDRVNAT pThis, PPDMSCATTERGATHER pSgBuf)

{

#if 0 /* Assertion happens often to me after resuming a VM —— no time to investigate this now。 */

Assert(pThis->enmLinkState == PDMNETWORKLINKSTATE_UP);

#endif

if (pThis->enmLinkState == PDMNETWORKLINKSTATE_UP)

{

struct mbuf *m = (struct mbuf *)pSgBuf->pvAllocator;

if (m)

{

/*

* A normal frame。

*/

pSgBuf->pvAllocator = NULL;

slirp_input(pThis->pNATState, m, pSgBuf->cbUsed);

}

else

{

/*

* GSO frame, need to segment it。

*/

/** @todo Make the NAT engine grok large frames?  Could be more efficient。。。 */

#if 0 /* this is for testing PDMNetGsoCarveSegmentQD。 */

uint8_t         abHdrScratch[256];

#endif

uint8_t const  *pbFrame = (uint8_t const *)pSgBuf->aSegs[0]。pvSeg;

PCPDMNETWORKGSO pGso    = (PCPDMNETWORKGSO)pSgBuf->pvUser;

uint32_t const  cSegs   = PDMNetGsoCalcSegmentCount(pGso, pSgBuf->cbUsed);  Assert(cSegs > 1);

for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++)

{

size_t cbSeg;

void  *pvSeg;

m = slirp_ext_m_get(pThis->pNATState, pGso->cbHdrsTotal + pGso->cbMaxSeg, &pvSeg, &cbSeg);

if (!m)

break;

#if 1

uint32_t cbPayload, cbHdrs;

uint32_t offPayload = PDMNetGsoCarveSegment(pGso, pbFrame, pSgBuf->cbUsed,

iSeg, cSegs, (uint8_t *)pvSeg, &cbHdrs, &cbPayload);

memcpy((uint8_t *)pvSeg + cbHdrs, pbFrame + offPayload, cbPayload);

slirp_input(pThis->pNATState, m, cbPayload + cbHdrs);

#else

。。。

用於從guest向網路傳送資料包的函式,包含用於通用分段解除安裝 (GSO)幀的單獨程式碼路徑,並呼叫memcpy用於合併資料片段。

在審計了各種程式碼路徑併為所有限制因素編寫了一個簡單的基於 Python 的約束求解器之後,當使 VirtIO 的半虛擬化網路裝置時,發現了一個重大問題。

0x02 半虛擬化網路

完全模擬裝置的另一種方法是使用半虛擬化。與完全虛擬化不同,在完全虛擬化中,guest完全不知道自己是guest,半虛擬化讓guest安裝驅動程式,這些驅動程式知道它們在guest機器中執行,以便與主機一起以更快、更多的速度傳輸資料。

VirtIO 是一個可用於開發半虛擬化驅動程式的介面。有一個這樣的驅動程式叫virtio-net,它與 Linux 原始碼一起提供並用於網路通訊。

https://github。com/torvalds/linux/blob/master/drivers/net/virtio_net。c

VirtualBox 與許多其他虛擬化軟體一樣,支援將其作為網路介面卡:

介面卡型別選項圖

VirtIO 網路透過使用ring形緩衝區在guest和主機(在這種情況下稱為 Virtqueues 或 VQueues)之間傳輸資料來工作。但是,與 e1000  漏洞不同的是,VirtIO 不使用帶有頭尾暫存器的單個ring進行傳輸,而是使用三個獨立的陣列:

◼一個 Descriptor 陣列,每個描述符包含以下資料:

·

Address - 正在傳輸的資料的物理地址。

·

Length – 地址處資料的長度。

·

Flags – 確定 Next 欄位是否正在使用以及緩衝區是讀取還是寫入的Header。

·

Next – 在有連結時使用。

◼可用ring– 一個數組,其中包含正在使用且可由主機讀取的描述符陣列的索引。

◼一個已使用的ring– 主機已讀取的 Descriptor 陣列中的索引陣列。

結構如下所示:

Pwn2Own 比賽使用的 VirtualBox NAT 網絡卡模擬元件的漏洞分析

當guest希望向網路傳送資料包時,它會在描述符表中新增一個條目,將這個描述符的索引新增到可用ring中,然後增加可用索引指標:

Pwn2Own 比賽使用的 VirtualBox NAT 網絡卡模擬元件的漏洞分析

完成此操作後,guest透過將 VQueue 索引寫入佇列通知暫存器來通知主機。這會觸發主機開始處理可用ring中的描述符。處理完描述符後,將其新增到已用ring中,並增加已用索引:

Pwn2Own 比賽使用的 VirtualBox NAT 網絡卡模擬元件的漏洞分析

0x03 通用分段解除安裝

接下來,需要一些 GSO 的背景知識。要了解對 GSO 的需求,重要的是要了解它為網絡卡解決的問題。

最初,在計算傳輸層校驗和或將它們分割成更小的乙太網資料包時,CPU 將處理所有繁重的工作。由於此過程在處理大量傳出網路流量時可能非常緩慢,因此硬體製造商開始為這些操作實施解除安裝,從而減輕作業系統的壓力。

對於分段,這意味著作業系統不必透過網路堆疊傳遞許多小得多的資料包,作業系統只需傳遞一個數據包一次。

這種最佳化可以應用於其他協議,TCP 和 UDP 之外的協議,無需硬體支援,方法是將分段延遲到網路驅動程式接收訊息之前,會建立 GSO。

由於 VirtIO 是半虛擬化裝置,驅動程式知道它在guest機器中,因此可以在guest和主機之間應用 GSO。GSO 在 VirtIO 中透過在網路緩衝區的開頭新增上下文描述符頭來實現,可以在以下結構中看到此Header頭:

struct  VNetHdr

{

uint8_t   u8Flags;

uint8_t   u8GSOType;

uint16_t u16HdrLen;

uint16_t u16GSOSize;

uint16_t u16CSumStart;

uint16_t u16CSumOffset;

};

VirtIO 頭可以被認為是與 e1000漏洞中的上下文描述符類似的概念。

收到此Header頭後,將驗證引數vnetR3ReadHeader某些級別的有效性。然後函式vnetR3SetupGsoCtx用於填充 VirtualBox 在所有網路裝置上使用的標準HeaderGSO 結構:

typedef struct PDMNETWORKGSO

{

/** The type of segmentation offloading we‘re performing (PDMNETWORKGSOTYPE)。 */

uint8_t             u8Type;

/** The total header size。 */

uint8_t             cbHdrsTotal;

/** The max segment size (MSS) to apply。 */

uint16_t            cbMaxSeg;

/** Offset of the first header (IPv4 / IPv6)。  0 if not not needed。 */

uint8_t             offHdr1;

/** Offset of the second header (TCP / UDP)。  0 if not not needed。 */

uint8_t             offHdr2;

/** The header size used for segmentation (equal to offHdr2 in UFO)。 */

uint8_t             cbHdrsSeg;

/** Unused。 */

uint8_t             u8Unused;

} PDMNETWORKGSO;

構建完成後,VirtIO 程式碼就會建立一個 scatter-gatherer 來從各種描述符組裝資料幀:

/* Assemble a complete frame。 */

for (unsigned int i = 1; i < elem。cOut && uSize > 0; i++)

{

unsigned int cbSegment = RT_MIN(uSize, elem。aSegsOut[i]。cb);

PDMDevHlpPhysRead(pDevIns, elem。aSegsOut[i]。addr,

((uint8_t*)pSgBuf->aSegs[0]。pvSeg) + uOffset,

cbSegment);

uOffset += cbSegment;

uSize -= cbSegment;

}

資料幀與新的 GSO 結構一起傳遞給 NAT 程式碼,現在達到了最初引起我興趣的地方。

0x04 漏洞分析

1.CVE-2021-2145 – Oracle VirtualBox NAT 整數下溢許可權提升漏洞

當 NAT 程式碼收到 GSO 幀時,它會獲取完整的乙太網資料包並將其作為mbuf訊息傳遞給Slirp(用於 TCP/IP 模擬的庫)。為了做到這一點,VirtualBox 分配了一條mbuf新訊息並將資料包複製過去。分配函式獲取一個大小並從三個不同的儲存桶中選擇一個最大的分配大小:

MCLBYTES(0x800 位元組)

MJUM9BYTES(0x2400 位元組)

MJUM16BYTES(0x4000 位元組)

struct mbuf *slirp_ext_m_get(PNATState pData, size_t cbMin, void **ppvBuf, size_t *pcbBuf)

{

struct mbuf *m;

int size = MCLBYTES;

LogFlowFunc((“ENTER: cbMin:%d, ppvBuf:%p, pcbBuf:%p\n”, cbMin, ppvBuf, pcbBuf));

if (cbMin < MCLBYTES)

size = MCLBYTES;

else if (cbMin < MJUM9BYTES)

size = MJUM9BYTES;

else if (cbMin < MJUM16BYTES)

size = MJUM16BYTES;

else

AssertMsgFailed((“Unsupported size”));

m = m_getjcl(pData, M_NOWAIT, MT_HEADER, M_PKTHDR, size);

。。。

如果提供的大小大於MJUM16BYTES,就會觸發斷言。不幸的是,此斷言僅在使用RT_STRICT宏時才編譯,而在釋出版本中不這樣。這意味著在命中此斷言後將繼續執行,從而為分配選擇大小為 0x800 的儲存桶。由於實際資料量較大,這會導致使用者資料複製到mbuf。

/** @def AssertMsgFailed

* An assertion failed print a message and a hit breakpoint。

*

* @param   a   printf argument list (in parenthesis)。

*/

#ifdef RT_STRICT

# define AssertMsgFailed(a)  \

do { \

RTAssertMsg1Weak((const char *)0, __LINE__, __FILE__, RT_GCC_EXTENSION __PRETTY_FUNCTION__); \

RTAssertMsg2Weak a; \

RTAssertPanic(); \

} while (0)

#else

# define AssertMsgFailed(a)     do { } while (0)

#endif

2.CVE-2021-2310 - 基於 Oracle VirtualBox NAT 堆的緩衝區溢位許可權提升漏洞

在整個程式碼中,使用了一個呼叫的函式PDMNetGsoIsValid來驗證guest提供的 GSO 引數是否有效。但是無論何時使用它,它都會放在斷言中。例如:

DECLINLINE(uint32_t) PDMNetGsoCalcSegmentCount(PCPDMNETWORKGSO pGso, size_t cbFrame)

{

size_t cbPayload;

Assert(PDMNetGsoIsValid(pGso, sizeof(*pGso), cbFrame));

cbPayload = cbFrame - pGso->cbHdrsSeg;

return (uint32_t)((cbPayload + pGso->cbMaxSeg - 1) / pGso->cbMaxSeg);

}

如前所述,像這樣的斷言不會在釋出版本中編譯。這會導致允許無效的 GSO 引數;給定的大小可能會導致錯誤計算slirp_ext_m_get,使其小於for 迴圈中的memcpy複製總量。在我的PoC中,我用於計算pGso->cbHdrsTotal + pGso->cbMaxSeg的引數cbMin導致分配了 0x4000 位元組,但計算cbPayload導致了memcpy對 0x4065 位元組的呼叫,從而使分配的區域溢位。

3.CVE-2021-2442 - Oracle VirtualBox NAT UDP Header頭越界漏洞

另一種解除安裝機制也很脆弱:校驗和解除安裝。校驗和解除安裝可應用於在其訊息頭中有校驗和的各種協議。模擬時,VirtualBox 支援 TCP 和 UDP。

為了訪問此功能,GSO 幀需要u8Flags設定成員的第一位以指示需要校驗和解除安裝。在 VirtualBox 的情況下,必須始終設定此位,因為它無法在不執行校驗和解除安裝的情況下處理 GSO。當 VirtualBox 使用 GSO 處理 UDP 資料包時,它可能會在某些情況下在PDMNetGsoCarveSegmentQD函式中結束:

case PDMNETWORKGSOTYPE_IPV4_UDP:

if (iSeg == 0)

pdmNetGsoUpdateUdpHdrUfo(RTNetIPv4PseudoChecksum((PRTNETIPV4)&pbFrame[pGso->offHdr1]),

pbSegHdrs, pbFrame, pGso->offHdr2);

該函式pdmNetGsoUpdateUdpHdrUfo使用offHdr2來指示 UDP 報頭在資料包結構中的位置。最終這會呼叫一個名為RTNetUDPChecksum的函式:

RTDECL(uint16_t) RTNetUDPChecksum(uint32_t u32Sum, PCRTNETUDP pUdpHdr)

{

bool fOdd;

u32Sum = rtNetIPv4AddUDPChecksum(pUdpHdr, u32Sum);

fOdd = false;

u32Sum = rtNetIPv4AddDataChecksum(pUdpHdr + 1, RT_BE2H_U16(pUdpHdr->uh_ulen) - sizeof(*pUdpHdr), u32Sum, &fOdd);

return rtNetIPv4FinalizeChecksum(u32Sum);

}

這就是漏洞所在。在此函式中,該uh_ulen屬性是完全可信的,無需任何驗證,這會導致大小超出緩衝區範圍,或者因減去sizeof(*pUdpHdr)而導致整數下溢。

rtNetIPv4AddDataChecksum 接收大小值和資料包頭指標並繼續計算校驗和:

/* iterate the data。 */

while (cbData > 1)

{

u32Sum += *pw;

pw++;

cbData -= 2;

}

從開發的角度來看,將大量越界資料新增在一起似乎不是特別有趣。但是,如果攻擊者能夠為連續的 UDP 資料包重新分配相同的堆位置,並且每次新增兩個位元組的 UDP 大小引數,則可以計算每個校驗和的差異並洩露越界資料。

最重要的是,還可以利用此漏洞對網路中的其他虛擬機器造成拒絕服務:

https://twitter。com/i/status/1421859745380638727

解除安裝支援在現代網路裝置中很常見,因此虛擬化軟體模擬裝置也很自然地做到這一點。雖然大多數公共研究都集中在其主要元件上,例如環形緩衝區,解除安裝模組並沒有受到過多的關注。

參考及來源:https://www。sentinelone。com/labs/gsoh-no-hunting-for-vulnerabilities-in-virtualbox-network-offloads/