Sploitus

Exploit for vbox_cve_2017_10235

kitploit · 2026-08-28

Exploit Code

MARKDOWN218 lines
## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-FUNDACION-SADOSKY-VBOX_CVE_2017_10235
# CVE-2017-10235: تجاوز سعة المخزن المؤقت لجهاز VirtualBox E1000

## مقدمة

توثق الوثيقة التالية خطأً تم العثور عليه في VirtualBox الإصدار v5.1.22 (تم إصلاحه الآن في v5.1.24)، في مكوّن محاكاة جهاز الضيف `DevE1000` (_محاكاة وحدة التحكم في الإيثرنت Intel 82540EM_)، في الدالة `e1kFallbackAddToFrame`، والذي يؤدي إلى تجاوز سعة المخزن المؤقت في المضيف عندما يكون نظام تشغيل الضيف خاضعًا لسيطرة مهاجم.

تم اعتراف Oracle بالثغرة في التحديث الحرج لشهر يوليو 2017 مع إصدار CVE-2017-10235.

تم تأكيد الثغرة على كل من مضيف Linux (Ubuntu 16.04) ومضيف Windows (v8.1) يشغّلان ضيفًا بنظام Linux (أيضًا Ubuntu 16.04)، لكن يمكن تشغيل الثغرة في العديد من التركيبات المختلفة للمضيف/الضيف. في جميع السيناريوهات يُفترض إعداد الشبكة الافتراضي: محول شبكة واحد فقط **موصول بـ NAT** من النوع **Intel PRO/1000 MT Desktop (82540EM)**.

بما أن بنى التحكم (بما في ذلك مؤشرات الدوال) يمكن استبدالها ببيانات يتحكم بها المهاجم، فمن الآمن الافتراض أن تنفيذ التعليمات البرمجية عن بُعد يمكن تحقيقه في العديد من السيناريوهات. خصصت Oracle درجة CVSS منخفضة لهذه الثغرة لأنها رأت أن لها خطر سرية `None` وسلامة `Low`، وهو ما نعتقد أنه لا يعكس الإمكانات الكاملة لاختراق هذه الثغرة (يوجد أدناه شرح لإمكانية تنفيذ التعليمات البرمجية عن بُعد).

## وصف الثغرة واستغلالها

كود VirtualBox الذي ينفّذ محاكاة وحدة تحكم الإيثرنت Intel 82540EM (في `src/VBox/Devices/Network/DevE1000.cpp`)، في الدالة `e1kFallbackAddToFrame`، ينفّذ تجزئة TCP بالأجهزة (hardware TCP Segmentation):

root@kitploit:~
    
    
    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);
    

تتحقق هذه الدالة بشكل صحيح من أن أقصى طول لحزمة الإرسال (`u16MaxPktLen`) أقل من الحد الأقصى القياسي البالغ 16288 بايتًا (`E1K_MAX_TX_PKT_SIZE`)، لكنها تفعل ذلك في شكل ماكرو `Assert` سيتم تعطيله في بناء الإصدار (release build)، مما يترك التحقق عديم الفائدة عمليًا للمستخدم النهائي. ويمكن مقارنة ذلك بالدالة المماثلة `e1kAddToFrame`، التي تفرض التحقق باستخدام `if` صريح بدلاً من `Assert`:

root@kitploit:~
    
    
    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 مفعّلة في واصفات البيانات/السياق (التي يتحكم بها نظام التشغيل باستخدام جهاز الضيف)، وأن تكون علامة GSO معطّلة. يعتمد الأخير على عوامل كثيرة، وبالتالي هناك طرق عديدة لتعطيله، لكن الأكثر ملاءمة هو تفعيل وضع الاسترجاع (loopback)، الذي يُضبط عبر سجل التحكم في الاستقبال (بتات `RCTL.LBM`)، وهو أيضًا خاضع لسيطرة نظام تشغيل الضيف.

سيجعل تفعيل وضع الاسترجاع الدالة `e1kXmitAllocBuf` تستخدم المخزن المؤقت `aTxPacketFallback` (_مخزن حزم الإرسال المستخدم للاسترجاع الاحتياطي TSE والاسترجاع_) لتخصيص مخزن PDM للتشتت/التجميع (scatter/gather)، بالطول المذكور البالغ 16288 بايتًا (`E1K_MAX_TX_PKT_SIZE`)، والإشارة إلى أن GSO سيكون معطلاً (بتعيين `NULL` في `pvUser`).

root@kitploit:~
    
    
    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` ذات التحقق الصحيح).

root@kitploit:~
    
    
    /*
     * 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`، ومع تعطيل التحقق المذكور أعلاه في بناء الإصدار (release build)، يمكن ضبط MSS على قيمة كبيرة بشكل تعسفي (حتى 64K ناقص HDRLEN)، مما يسمح بتمرير قيمة `DTALEN` كبيرة بشكل تعسفي إلى `e1kFallbackAddSegment`:

root@kitploit:~
    
    
    /*
    * 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).

root@kitploit:~
    
    
    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)

لجعل هذه الثغرة أكثر قابلية للتحول إلى تنفيذ تعليمات برمجية عن بُعد (RCE)، تجدر الإشارة إلى أن المتغير الذي يلي المخزن المؤقت مباشرة هو فهرسه (`u16TxPktLen`)، الذي يُستخدم للكتابة فيه (كإزاحة في وسيط `PDMDevHlpPhysRead`). إن التحكم في هذه القيمة من خلال تجاوز سعة أولي للمخزن المؤقت (ناتج عن واصف بيانات أول بطول `E1K_MAX_TX_PKT_SIZE` \+ 2 بايت) سيسمح بعدها بالكتابة (في استدعاء ثانٍ لـ `PDMDevHlpPhysRead` مع واصف بيانات ثانٍ) إلى أي عنوان ذاكرة يبعد حتى 64K عن المخزن المؤقت، دون الحاجة إلى استبدال كل الذاكرة الواقعة بينهما (وهو ما كان سيجعل الهجوم أكثر تعقيدًا، في محاولة لتجنب انهيار محتمل).

بالقرب من المخزن المؤقت المستهدف `aTxPacketFallback`، بعد بضعة أسطر بالأسفل وضمن نطاق 64K، يُعرَّف الهيكل `g_aE1kRegMap`، الذي يتضمن مصفوفة من مؤشرات الدوال التي تنفّذ معالجات القراءة والكتابة (`pfnRead` و`pfnWrite`)، وهو ما سيكون هدفًا مثاليًا لتجاوز سعة المخزن المؤقت الثاني لتسهيل تنفيذ التعليمات البرمجية عن بُعد.

## خطأ في `e1kXmitAllocBuf`

تجدر الإشارة من باب الاكتمال إلى تعقيد (بسيط) في ناقل الهجوم هذا: يبدو أن هناك خطأ في الدالة `e1kXmitAllocBuf`، حيث في حالة وضع الاسترجاع، لا يُعاد تعيين `cbTxAlloc` (_عدد البايتات في الحزمة التالية_) إلى الصفر، كما يحدث في الحالة العادية (في الفرع الآخر من جملة `if` الخاصة بها). يؤدي هذا إلى تعليق الخيط في حلقة `while` الخاصة بـ `e1kLocateTxPacket` (داخل `e1kXmitPending`):

root@kitploit:~
    
    
    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` في الأحوال العادية، مما ينهي الحلقة المذكورة أعلاه فعليًا.

root@kitploit:~
    
    
    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;
       }
    

يترجم هذا إلى اشتراط أن تكون أول حزمة تُرسل إلى الجهاز (بعد ضبط وضع الاسترجاع) هي الحزمة التي تسبب التجاوز، وإلا ستتجمد الآلة الافتراضية (وينتهي الأمر بحرمان من الخدمة DoS بدلاً من تنفيذ التعليمات البرمجية عن بُعد RCE).

## إثبات المفهوم

نظرًا لأن إعداد جهاز الشبكة بعيد كل البعد عن البساطة، ولتجنب بناء برنامج تشغيل مخصص له، تم تعديل برنامج تشغيل E1000 الخاص بنواة Linux عامة لتوليد الواصفات (السياق والبيانات معًا) التي تسبب التجاوز. هذه النواة المعدلة متاحة للـتنزيل من هذا المستودع. تم اختبارها على ضيف Ubuntu 16.04، مما تسبب في انهيار على كل من مضيفي Linux وWindows. يتوفر وصف مفصّل هنا.

## الحلول الممكنة

تم إصلاح الثغرة في التغيير 67974 (`bugref:8881`). تم تحويل الفحوصات التي كانت على شكل `Assert` في `e1kFallbackAddToFrame` إلى فحوصات صريحة على شكل جمل `if`، تبقى الآن مفعّلة في بناء الإصدار (release build) (على غرار ما تم القيام به بالفعل في `e1kAddToFrame`). كما أصبح `cbTxAlloc` يُضبط الآن على الصفر في كلا الفرعين (وضع الاسترجاع والوضع العادي) في `e1kXmitAllocBuf`.

يمكن أن يكون الفحص الإضافي (الدفاعي) المقترح هنا، وغير المنفّذ في التغيير، هو وضع فحص في `e1kFallbackAddSegment` (وبالمثل في `e1kAddToFrame`)، قبل استدعاء `PDMDevHlpPhysRead`، للتحقق صراحةً من احتمال تجاوز المخزن المؤقت بذاكرة الضيف (بشكل أساسي أن يكون `u16TxPktLen` زائد `u16Len` أقل من طول المخزن المؤقت `aTxPacketFallback`).