
[CVE-2017-10235] विवरण और PoC VirtualBox E1000 डिवाइस बफर ओवरफ्लो
निम्नलिखित दस्तावेज़ VirtualBox v5.1.22 में पाए गए एक बग का विवरण देता है (अब v5.1.24 में ठीक किया गया है), जो गेस्ट डिवाइस एमुलेशन घटक DevE1000 (Intel 82540EM Ethernet Controller Emulation), फ़ंक्शन e1kFallbackAddToFrame में है, जो होस्ट में बफर ओवरफ्लो का कारण बनता है जब गेस्ट OS पर हमलावर का नियंत्रण होता है।
इस बग को Oracle ने जुलाई 2017 के CPU में जारी CVE-2017-10235 के साथ स्वीकार किया था।
इस भेद्यता की पुष्टि Linux (Ubuntu 16.04) और Windows (v8.1) दोनों होस्ट पर एक Linux (Ubuntu 16.04) गेस्ट चलाकर की गई थी, लेकिन यह भेद्यता विभिन्न होस्ट/गेस्ट संयोजनों में ट्रिगर हो सकती है। सभी परिदृश्यों में डिफ़ॉल्ट नेटवर्क कॉन्फ़िगरेशन माना जाता है: केवल एक नेटवर्क एडेप्टर NAT से जुड़ा और प्रकार Intel PRO/1000 MT Desktop (82540EM) का।
चूंकि नियंत्रण संरचनाओं (फ़ंक्शन पॉइंटर्स सहित) को हमलावर-नियंत्रित डेटा से ओवरराइट किया जा सकता है, यह मानना सुरक्षित है कि कई परिदृश्यों में रिमोट कोड एक्ज़ीक्यूशन (RCE) प्राप्त किया जा सकता है। Oracle ने इस बग को कम CVSS स्कोर दिया क्योंकि उन्होंने इसे None गोपनीयता जोखिम और Low अखंडता जोखिम माना, जो हमारी राय में इस बग की पूर्ण संभावित क्षति को नहीं दर्शाता है (RCE की संभावना का स्पष्टीकरण नीचे दिया गया है)।
VirtualBox कोड जो Intel 82540EM Ethernet Controller के एमुलेशन को लागू करता है (फ़ाइल src/VBox/Devices/Network/DevE1000.cpp में), फ़ंक्शन e1kFallbackAddToFrame में, हार्डवेयर TCP सेगमेंटेशन को लागू करता है:
static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc,
bool fOnWorkerThread)
{
#ifdef VBOX_STRICT
PPDMSCATTERGATHER pTxSg = pThis->CTX_SUFF(pTxSg);
Assert(e1kGetDescType(pDesc) == E1K_DTYP_DATA);
Assert(pDesc->data.cmd.fTSE);
Assert(!e1kXmitIsGsoBuf(pTxSg));
#endif
uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN +
pThis->contextTSE.dw3.u16MSS;
Assert(u16MaxPktLen != 0);
Assert(u16MaxPktLen < E1K_MAX_TX_PKT_SIZE);
यह फ़ंक्शन सही ढंग से जाँचता है कि अधिकतम TX पैकेट लंबाई (u16MaxPktLen) मानक अधिकतम 16288 बाइट्स (E1K_MAX_TX_PKT_SIZE) से कम है, लेकिन यह Assert मैक्रो के रूप में जाँच करता है जो रिलीज़ बिल्ड में अक्षम हो जाएगा, प्रभावी रूप से अंतिम उपयोगकर्ता के लिए जाँच को बेकार छोड़ देता है। इसकी तुलना समान फ़ंक्शन e1kAddToFrame से की जा सकती है, जो Assert के बजाय एक स्पष्ट if के साथ जाँच लागू करता है:
static bool e1kAddToFrame(PE1KSTATE pThis, RTGCPHYS PhysAddr,
uint32_t cbFragment)
{
PPDMSCATTERGATHER pTxSg = pThis->CTX_SUFF(pTxSg);
bool const fGso = e1kXmitIsGsoBuf(pTxSg);
uint32_t const cbNewPkt = cbFragment + pThis->u16TxPktLen;
if (RT_UNLIKELY( !fGso && cbNewPkt > E1K_MAX_TX_PKT_SIZE ))
{
E1kLog(("%s Transmit packet is too large: %u > %u(max)\n",
pThis->szPrf, cbNewPkt, E1K_MAX_TX_PKT_SIZE));
return false;
}
सामान्य फ़ंक्शन (e1kAddToFrame) और फ़ॉलबैक (e1kFallbackAddToFrame) के उपयोग के बीच का अंतर e1kXmitDesc() में तय किया जाता है और यह दो कारकों पर निर्भर करता है: कि डेटा/कॉन्टेक्स्ट डिस्क्रिप्टर में TSE फ्लैग सक्षम है (गेस्ट मशीन का उपयोग करके OS द्वारा नियंत्रित) और GSO फ्लैग अक्षम है। उत्तरार्द्ध कई कारकों पर निर्भर करता है, और इसलिए इसे अक्षम करने के कई तरीके हैं, लेकिन सबसे सुविधाजनक है लूपबैक मोड को सक्षम करना, जो रिसीव कंट्रोल रजिस्टर (RCTL.LBM बिट्स) के माध्यम से कॉन्फ़िगर किया जाता है, जो गेस्ट OS द्वारा भी नियंत्रित होता है।
लूपबैक मोड को सक्षम करने से फ़ंक्शन e1kXmitAllocBuf PDM स्कैटर/गैदर बफर के आवंटन के लिए aTxPacketFallback बफर (Transmit packet buffer use for TSE fallback and loopback) का उपयोग करेगा, जिसकी लंबाई 16288 बाइट्स (E1K_MAX_TX_PKT_SIZE) है, और यह संकेत देगा कि GSO अक्षम हो जाएगा (pvUser में NULL सेट करके)।
if (RT_LIKELY(GET_BITS(RCTL, LBM) != RCTL_LBM_TCVR))
{
...
}
else
{
/* Create a loopback using the fallback buffer and preallocated SG. */
AssertCompileMemberSize(E1KSTATE, uTxFallback.Sg, 8 * sizeof(size_t));
pSg = &pThis->uTxFallback.Sg;
pSg->fFlags = PDMSCATTERGATHER_FLAGS_MAGIC |
PDMSCATTERGATHER_FLAGS_OWNER_3;
pSg->cbUsed = 0;
pSg->cbAvailable = 0;
pSg->pvAllocator = pThis;
pSg->pvUser = NULL; /* No GSO here. */
pSg->cSegs = 1;
pSg->aSegs[0].pvSeg = pThis->aTxPacketFallback;
pSg->aSegs[0].cbSeg = sizeof(pThis->aTxPacketFallback);
}
इससे फ़ंक्शन e1kXmitIsGsoBuf (अंदर e1kXmitDesc) का कॉल False लौटाएगा और, डेटा डिस्क्रिप्टर में TSE सक्षम होने के साथ, निष्पादन प्रवाह e1kFallbackAddToFrame (सुरक्षित फ़ंक्शन e1kAddToFrame के सही जाँच के साथ) की ओर जाएगा।
/*
* Add the descriptor data to the frame. If the frame is complete,
* transmit it and reset the u16TxPktLen field.
*/
if (e1kXmitIsGsoBuf(pThis->CTX_SUFF(pTxSg)))
{
...
}
else if (!pDesc->data.cmd.fTSE)
{
...
}
else
{
STAM_COUNTER_INC(&pThis->StatTxPathFallback);
rc = e1kFallbackAddToFrame(pThis, pDesc, fOnWorkerThread);
}
e1kFallbackAddToFrame के अंदर, ऊपर उल्लिखित जाँच रिलीज़ बिल्ड में अक्षम होने के कारण, MSS को मनमाने ढंग से बड़ा (HDRLEN तक 64K तक) सेट किया जा सकता है, इस प्रकार e1kFallbackAddSegment को एक मनमाने ढंग से बड़ा DTALEN पास करने की अनुमति देता है:
/*
* Carve out segments.
*/
int rc;
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);
फ़ंक्शन e1kFallbackAddSegment इस मान (अब आर्गुमेंट u16Len के रूप में) का उपयोग गेस्ट मेमोरी से होस्ट मेमोरी में बफर aTxPacketFallback में कॉपी करने के लिए करेगा (PDMDevHlpPhysRead के माध्यम से), इस लंबाई की आगे कोई जाँच किए बिना, इस प्रकार बफर ओवरफ्लो (16288 बाइट्स की बफर क्षमता के साथ 64K तक का मेमोरी आकार) का कारण बनेगा।
static int e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr,
uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
int rc = VINF_SUCCESS;
/* TCP header being transmitted */
struct E1kTcpHeader *pTcpHdr = (struct E1kTcpHeader *)
(pThis->aTxPacketFallback + pThis->contextTSE.tu.u8CSS);
/* IP header being transmitted */
struct E1kIpHeader *pIpHdr = (struct E1kIpHeader *)
(pThis->aTxPacketFallback + pThis->contextTSE.ip.u8CSS);
E1kLog3(("%s e1kFallbackAddSegment: Length=%x, remaining payload=%x,
header=%x, send=%RTbool\n", pThis->szPrf, u16Len,
pThis->u32PayRemain, pThis->u16HdrRemain, fSend));
Assert(pThis->u32PayRemain + pThis->u16HdrRemain > 0);
PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);
इस भेद्यता को RCE के लिए अधिक अनुकूल बनाने के लिए, यह ध्यान दिया जाना चाहिए कि बफर के ठीक बाद का वेरिएबल इसका इंडेक्स (u16TxPktLen) है, जिसका उपयोग उस पर लिखने के लिए किया जाता है (PDMDevHlpPhysRead के आर्गुमेंट में ऑफसेट के रूप में)। प्रारंभिक बफर ओवरफ्लो (लंबाई E1K_MAX_TX_PKT_SIZE + 2 बाइट्स के पहले डेटा डिस्क्रिप्टर के कारण) के साथ इस मान को नियंत्रित करने से फिर (दूसरे डेटा डिस्क्रिप्टर के साथ PDMDevHlpPhysRead के दूसरे कॉल में) बफर से 64K तक की दूरी पर किसी भी मेमोरी एड्रेस पर लिखा जा सकता है, बिना बीच की सारी मेमोरी को ओवरराइट किए (जो हमले को और जटिल बनाता, संभावित क्रैश से बचने की कोशिश में)।
लक्ष्य बफर aTxPacketFallback के करीब, कुछ पंक्तियाँ नीचे और 64K रेंज के भीतर, g_aE1kRegMap संरचना परिभाषित है, जिसमें फ़ंक्शन पॉइंटर्स का एक वेक्टर शामिल है जो पढ़ने और लिखने के हैंडलर (pfnRead और pfnWrite) को लागू करते हैं, जो दूसरे बफर ओवरफ्लो के लिए एक आदर्श लक्ष्य होगा ताकि RCE को सुगम बनाया जा सके।
e1kXmitAllocBuf में बगपूर्णता के लिए इस हमले वेक्टर में एक (मामूली) जटिलता का उल्लेख करना उचित है: फ़ंक्शन e1kXmitAllocBuf में एक बग प्रतीत होता है, जहां लूपबैक मोड में होने पर, cbTxAlloc (अगले पैकेट में बाइट्स की संख्या) को शून्य पर रीसेट नहीं किया जाता है, जैसा कि सामान्य मामले में (इसके if की दूसरी शाखा में) किया जाता है। इससे थ्रेड e1kLocateTxPacket के while लूप में फंस जाता है (e1kXmitPending के अंदर):
while (e1kLocateTxPacket(pThis))
{
fIncomplete = false;
/* Found a complete packet, allocate it. */
rc = e1kXmitAllocBuf(pThis, pThis->fGSO);
/* If we're out of bandwidth we'll come back later. */
if (RT_FAILURE(rc))
goto out;
/* Copy the packet to allocated buffer and send it. */
rc = e1kXmitPacket(pThis, fOnWorkerThread);
/* If we're out of bandwidth we'll come back later. */
if (RT_FAILURE(rc))
goto out;
}
ऐसा प्रतीत होता है क्योंकि e1kLocateTxPacket समय से पहले True लौटाता है जब cbTxAlloc शून्य नहीं होता है, और उस कोड तक नहीं पहुंचता है जो जाँचता है कि iTxDCurrent nTxDFetched के बराबर है या नहीं (सामान्य मामला जहां सभी डिस्क्रिप्टर संसाधित हो चुके हैं), जो सामान्यतः फ़ंक्शन को False लौटाने का कारण बनता है, प्रभावी रूप से उपरोक्त लूप को समाप्त करता है।
static bool e1kLocateTxPacket(PE1KSTATE pThis)
{
LogFlow(("%s e1kLocateTxPacket: ENTER cbTxAlloc=%d\n",
pThis->szPrf, pThis->cbTxAlloc));
/* Check if we have located the packet already. */
if (pThis->cbTxAlloc)
{
LogFlow(("%s e1kLocateTxPacket: RET true cbTxAlloc=%d\n",
pThis->szPrf, pThis->cbTxAlloc));
return true;
}
इसका अनुवाद यह है कि डिवाइस को भेजा गया पहला पैकेट (लूपबैक मोड सेट करने के बाद) वही होना चाहिए जो ओवरफ्लो को ट्रिगर करता है, अन्यथा VM हैंग हो जाएगा (RCE के बजाय DoS में समाप्त होगा)।
चूंकि नेटवर्क डिवाइस का सेटअप बहुत तुच्छ नहीं है, और इसके लिए एक कस्टम ड्राइवर बनाने से बचने के लिए, एक सामान्य Linux कर्नेल के E1000 ड्राइवर को संशोधित किया गया ताकि वे डिस्क्रिप्टर (कॉन्टेक्स्ट और डेटा दोनों) उत्पन्न हो सकें जो ओवरफ्लो को ट्रिगर करते हैं। यह संशोधित कर्नेल इस रिपॉजिटरी से डाउनलोड के लिए उपलब्ध है। इसका परीक्षण Ubuntu 16.04 गेस्ट में किया गया है, जिससे Linux और Windows दोनों होस्ट में क्रैश हुआ। एक विस्तृत विवरण यहाँ उपलब्ध है।
भेद्यता को Changeset 67974 (bugref:8881) में ठीक किया गया। e1kFallbackAddToFrame में Assert के रूप में की गई जाँच को स्पष्ट if स्टेटमेंट में बदल दिया गया, जो अब रिलीज़ बिल्ड में सक्रिय रहती हैं (e1kAddToFrame में पहले से किए गए कार्य के समान)। साथ ही, cbTxAlloc को अब e1kXmitAllocBuf में दोनों शाखाओं (लूपबैक मोड और सामान्य मोड) में शून्य पर सेट किया गया है।
यहां सुझाया गया एक अतिरिक्त (रक्षात्मक) जाँच, जो चेंजसेट में लागू नहीं की गई है, वह यह हो सकती है कि e1kFallbackAddSegment (और इसी प्रकार e1kAddToFrame में), PDMDevHlpPhysRead के कॉल से पहले, गेस्ट मेमोरी के साथ बफर के संभावित ओवरफ्लो की स्पष्ट रूप से जाँच की जाए (मुख्य रूप से यह कि u16TxPktLen और u16Len का योग aTxPacketFallback बफर की लंबाई से कम हो)।