最新消息:图 床

48 小時逃逸 Virtualbox 虛擬機——記一次 CTF 中的 0day 之旅

COOL IAM 253浏览 0评论

作者:@flyyy
長亭科技安全研究員,曾獲得GeekPwn 2018“最佳技術獎”,入選極棒名人堂。
來源:長亭技術專欄

35C3CTF中niklasb出了一道關於virtualbox逃逸的0day題目,想從這個題目給大家介紹virtualbox的一個新的攻擊面(其實類似的攻擊面也同樣存在於其他虛擬化類軟件),這裡記錄一下和@kelwin一起解題的過程(被dalao帶飛真爽)

題目描述

chromacity 477
Solves: 2
Please escape VirtualBox. 3D acceleration is enabled for your convenience.

No need to analyze the 6.0 patches, they should not contain security fixes.

Once you're done, submit your exploit at https://vms.35c3ctf.ccc.ac/, but assume that all passwords are different on the remote setup.

Challenge files. Password for the encrypted VM image is the flag for "sanity check".

Setup

UPDATE: You might need to enable nested virtualization.

Hint: https://github.com/niklasb/3dpwn/ might be useful

Hint 2: this photo was taken earlier today at C3

Difficulty estimate: hard

題目描述中可以看出:

  1. 虛擬機配置中顯卡開啟了3D加速功能
  2. 6.0的patch沒用,參考virtualbox 6.0的發布時間推測是出題人來不及用最新版適配環境等等,所以是一道0day題目

題目前前後後給出了四個附件,一個是img文件,一個是通過qemu+kvm虛擬機運行該img的.sh文件,這個虛擬機就是遠程運行的host的環境,host當中有一個5.28 release版的virtualbox,也就是我們逃逸的目標。(算上啟動host環境中的virtualbox,如果你的主機是windows+vmware workstation的話。。。滿眼都是淚),另外還有兩張圖片,一張是關於目標virtualbox虛擬機的配置,一張是niklasb和他電腦屏幕的照片。電腦屏幕上顯示的是這個頁面,看樣子題目應該跟glShaderSource這個opengl的api有關。

同時給出的兩個hint,一個是niklasb自己關於3dpwn的github鏈接,其中有他之前通過攻擊virtual box 3D加速模塊實現逃逸的源碼和相關分析文章。另一個就是附件中關於niklasb的照片。

題目分析

通過題目描述我們可以比較確定的是出題人希望我們去找virtualbox 3D加速部分的0day漏洞來實現逃逸,同時通過他給出的github鏈接中的文章和題目名我們可以很快把目標鎖定在3D加速部分的Chromium代碼上(並不是同名的瀏覽器項目)。

簡單來說,virtualbox通過引入OpenGL的共享庫來引入3D加速功能,而Chromium負責解析Virtualbox。Chromium定義了一套用來描述OpenGL不同操作的網絡協議。但是這個Chromium庫最後一次更新源碼已經是在十二年前了。同時通過這個庫我們大概可以猜到之前hint中那張照片的用意了。如果排除掉去直接挖掘OpenGL的0day的可能性,那Virtualbox代碼中關於glShaderSource的部分就只有Chromium中關於這個api的協議解析的部分了。而恰好niklasb的github中的源碼和文章都是關於Chromium部分的漏洞及其利用的。

源碼分析

Virtualbox的Guest additions類似於VMware workstation中的vmware-tools。不同的地方在於,VMware workstation通過暴漏固定的端口給guest來實現guest與host的通信,而Guest additions是通過增加一個自定義的虛擬硬件vboxguest來實現guest與host的交互。而3D加速是作為一個virtualbox自定義的hgcm服務進程存在的。

gdb-peda$ i thread
  Id   Target Id         Frame 
* 1    Thread 0x7fe77f6d9780 (LWP 14933) "VirtualBoxVM" 0x00007fe77b0acbf9 in __GI___poll (fds=0x55fe988e82b0, nfds=0x2, timeout=0x63) at ../sysdeps/unix/sysv/linux/poll.c:29
......
  15   Thread 0x7fe72f86a700 (LWP 14965) "ShCrOpenGL" 0x00007fe77a4959f3 in futex_wait_cancelable (private=<optimized out>, expected=0x0, futex_word=0x7fe720004068)
......
  35   Thread 0x7fe6d0cd6700 (LWP 14985) "nspr-3" 0x00007fe77a4959f3 in futex_wait_cancelable (private=<optimized out>, expected=0x0, futex_word=0x55fe9868ed70)
    at ../sysdeps/unix/sysv/linux/futex-internal.h:88
  36   Thread 0x7fe6b9b61700 (LWP 14986) "SHCLIP" 0x00007fe77b0acbf9 in __GI___poll (fds=0x7fe6b4000b20, nfds=0x2, timeout=0xffffffff) at ../sysdeps/unix/sysv/linux/poll.c:29
gdb-peda$ thread 15
[Switching to thread 15 (Thread 0x7fe72f86a700 (LWP 14965))]
#0  0x00007fe77a4959f3 in futex_wait_cancelable (private=<optimized out>, expected=0x0, futex_word=0x7fe720004068) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
88  ../sysdeps/unix/sysv/linux/futex-internal.h: No such file or directory.
gdb-peda$ bt
#0  0x00007fe77a4959f3 in futex_wait_cancelable (private=<optimized out>, expected=0x0, futex_word=0x7fe720004068) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
#1  __pthread_cond_wait_common (abstime=0x0, mutex=0x7fe720004070, cond=0x7fe720004040) at pthread_cond_wait.c:502
#2  __pthread_cond_wait (cond=0x7fe720004040, mutex=0x7fe720004070) at pthread_cond_wait.c:655
#3  0x00007fe77e0e5cc8 in rtSemEventWait (fAutoResume=0x1, cMillies=0xffffffff, hEventSem=0x7fe720004040)
    at /home/f1yyy/Desktop/VirtualBox-6.0.0/src/VBox/Runtime/r3/linux/../posix/semevent-posix.cpp:369
#4  RTSemEventWait (hEventSem=0x7fe720004040, cMillies=0xffffffff) at /home/f1yyy/Desktop/VirtualBox-6.0.0/src/VBox/Runtime/r3/linux/../posix/semevent-posix.cpp:482
#5  0x00007fe75d3b09aa in HGCMThread::MsgGet (this=0x7fe720003f60, ppMsg=0x7fe72f869cf0) at /home/f1yyy/Desktop/VirtualBox-6.0.0/src/VBox/Main/src-client/HGCMThread.cpp:549
#6  0x00007fe75d3b147f in hgcmMsgGet (pThread=0x7fe720003f60, ppMsg=0x7fe72f869cf0) at /home/f1yyy/Desktop/VirtualBox-6.0.0/src/VBox/Main/src-client/HGCMThread.cpp:734
#7  0x00007fe75d3b265c in hgcmServiceThread (pThread=0x7fe720003f60, pvUser=0x7fe720003e00) at /home/f1yyy/Desktop/VirtualBox-6.0.0/src/VBox/Main/src-client/HGCM.cpp:608
#8  0x00007fe75d3af940 in hgcmWorkerThreadFunc (hThreadSelf=0x7fe720004340, pvUser=0x7fe720003f60) at /home/f1yyy/Desktop/VirtualBox-6.0.0/src/VBox/Main/src-client/HGCMThread.cpp:200
#9  0x00007fe77df95501 in rtThreadMain (pThread=0x7fe720004340, NativeThread=0x7fe72f86a700, pszThreadName=0x7fe720004c20 "ShCrOpenGL")
    at /home/f1yyy/Desktop/VirtualBox-6.0.0/src/VBox/Runtime/common/misc/thread.cpp:719
#10 0x00007fe77e0df882 in rtThreadNativeMain (pvArgs=0x7fe720004340) at /home/f1yyy/Desktop/VirtualBox-6.0.0/src/VBox/Runtime/r3/posix/thread-posix.cpp:327
#11 0x00007fe77a48f6db in start_thread (arg=0x7fe72f86a700) at pthread_create.c:463
#12 0x00007fe77b0b988f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

也就是說,當我們想要在guest中想要調用一個OpenGL的某個接口,需要根據我們的請求先進行Chromium的協議封裝,再進行hgcm的協議封裝。具體關於virtualbox在這兩部分的實現細節,請閱讀virtualbox相關源碼,這裡不再詳述。

niklasb在其github上已經封裝好了調用Chromium代碼部分的函數及例子,比如下面這兩行代碼:

client = hgcm_connect("VBoxSharedCrOpenGL")
hgcm_call(client, SHCRGL_GUEST_FN_SET_VERSION, [9, 1])

最終在源碼中會觸發到src/vbox/hostservices/sharedopengl/crservice/crservice.cpp中的switch下的SHCRGL_GUEST_FN_SET_VERSION部分,其中的vMajor和vMinor會分別為9和1。

再次回到題目上來,題目已經提醒了漏洞存在的位置可能在Chromium中glShaderSource相關的接口位置,通過在源碼中的尋找與分析,我們把目標鎖定在了crUnpackExtendShaderSource函數中。crUnpackExtendShaderSource代碼如下:

void crUnpackExtendShaderSource(void)
{
    GLint *length = NULL;
    GLuint shader = READ_DATA(8, GLuint);
    GLsizei count = READ_DATA(12, GLsizei);
    GLint hasNonLocalLen = READ_DATA(16, GLsizei);
    GLint *pLocalLength = DATA_POINTER(20, GLint);
    char **ppStrings = NULL;
    GLsizei i, j, jUpTo;
    int pos, pos_check;

    if (count >= UINT32_MAX / sizeof(char *) / 4)
    {
        crError("crUnpackExtendShaderSource: count %u is out of range", count);
        return;
    }

    pos = 20 + count * sizeof(*pLocalLength);

    if (hasNonLocalLen > 0)
    {
        length = DATA_POINTER(pos, GLint);
        pos += count * sizeof(*length);
    }

    pos_check = pos;

    if (!DATA_POINTER_CHECK(pos_check))
    {
        crError("crUnpackExtendShaderSource: pos %d is out of range", pos_check);
        return;
    }

    for (i = 0; i < count; ++i)
    {
        if (pLocalLength[i] <= 0 || pos_check >= INT32_MAX - pLocalLength[i] || !DATA_POINTER_CHECK(pos_check))
        {
            crError("crUnpackExtendShaderSource: pos %d is out of range", pos_check);
            return;
        }

        pos_check += pLocalLength[i];
    }

    ppStrings = crAlloc(count * sizeof(char*));
    if (!ppStrings) return;

    for (i = 0; i < count; ++i)
    {
        ppStrings[i] = DATA_POINTER(pos, char);
        pos += pLocalLength[i];
        if (!length)
        {
            pLocalLength[i] -= 1;
        }

        Assert(pLocalLength[i] > 0);
        jUpTo = i == count -1 ? pLocalLength[i] - 1 : pLocalLength[i];
        for (j = 0; j < jUpTo; ++j)
        {
            char *pString = ppStrings[i];

            if (pString[j] == '/0')
            {
                Assert(j == jUpTo - 1);
                pString[j] = '/n';
            }
        }
    }

//    cr_unpackDispatch.ShaderSource(shader, count, ppStrings, length ? length : pLocalLength);
    cr_unpackDispatch.ShaderSource(shader, 1, (const char**)ppStrings, 0);

    crFree(ppStrings);
}

仔細看會發現在中間一段for循環檢查pLocalLength數組的每個元素跟所有元素的和的大小是否越界時,並未檢查最後一層循環過後pos_check是否越界,據此我們可以在最後的兩層嵌套循環中的內層中實現越界寫,而這個越界寫也很有趣:

        for (j = 0; j < jUpTo; ++j)
        {
            char *pString = ppStrings[i];

            if (pString[j] == '/0')
            {
                Assert(j == jUpTo - 1);
                pString[j] = '/n';
            }
        }

它可以將越界部分所有的’/0’替換為’/n’。通過這個漏洞我們可以越界寫一塊堆內存,將其後面內存中若干的’/0’替換為’/n’。(注意:Assert在release版中是不存在的!)之後我們會介紹如何通過這個越界寫實現任意地址寫。

當然只有一個越界寫可能利用起來還是十分困難,我們仔細看了看niklasb寫的文章,發現在很多類似的unpack函數中均存在類似於CVE-2018-3055的漏洞,比如crUnpackExtendGetUniformLocation:

void crUnpackExtendGetUniformLocation(void)
{
    int packet_length = READ_DATA(0, int);
    GLuint program = READ_DATA(8, GLuint);
    const char *name = DATA_POINTER(12, const char);
    SET_RETURN_PTR(packet_length-16);
    SET_WRITEBACK_PTR(packet_length-8);
    cr_unpackDispatch.GetUniformLocation(program, name);
}

漏洞的成因完全與CVE-2018-3055相同,簡單來說SET_RETURN_PTR和SET_WRITEBACK_PTR指向的內存會寫回到guest,而這裡因為沒有對packet_length做對應的檢查導致我們可以在堆上實現越界讀。

漏洞利用

通過以上的代碼分析,我們現在有一個堆越界讀和一個堆越界寫,接下來我們來分析如何去完成完整的漏洞利用。

因為信息泄露部分完全與CVE-2018-3055基本相同,我們選擇直接復用niklasb之前的exp leak部分的代碼。重寫make_oob_read后通過leak_stuff我們可以泄露一個CRConnection結構體的位置,而niklasb的exp中就是通過修改pHostBuffer和cbHostBuffer來實現任意地址讀。因此,當我們有任意地址寫的條件之後我們就可以任意地址讀了。

接下來的關鍵就是如何用我們神奇的堆溢出來實現任意地址寫了。@kelwin找到了一個很好用的結構體CRVBOXSVCBUFFER_t,也就是niklasb的代碼中alloc_buf使用的結構體:

typedef struct _CRVBOXSVCBUFFER_t {
    uint32_t uiId;
    uint32_t uiSize;
    void*    pData;
    _CRVBOXSVCBUFFER_t *pNext, *pPrev;
} CRVBOXSVCBUFFER_t;

如果可以在堆上我們可以越界寫的內存後面恰好布置這樣一個結構體,越界寫它對應的uiSize部分,再通過SHCRGL_GUEST_FN_WRITE_BUFFER就可以越界寫這個buffer所對應的pData的內容,之後再越界寫另一個相同的結構體,就可以實現任意地址寫了。實現任意地址寫的具體過程如下:

  1. n次調用alloc_buf,對應的buffer填充為可以觸發越界寫的部分,從而確保在我們可以越界寫的堆後有可用的CRVBOXSVCBUFFER_t結構體。此時內存分佈如下:


    转载请注明:IAMCOOL » 48 小時逃逸 Virtualbox 虛擬機——記一次 CTF 中的 0day 之旅

5 1 vote
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x