1. 程式人生 > 其它 >VLC播放器載入惡意字幕檔案導致執行任意程式碼漏洞分析與POC實現

VLC播放器載入惡意字幕檔案導致執行任意程式碼漏洞分析與POC實現

今年5月23號的時候,聽說checkpoint搞了個大新聞:vlc等播放器載入特定字幕可以完全控制使用者電腦。當時我就震驚了:還有何種操作。想想看,當你吃著辣條,看著電影,突然就彈了個計算器,這電影真高階(滑稽。**震驚之餘就有點好奇到底是怎麼做到的,但是當時checkpoint說考慮到影響,暫時不會公佈細節。剛好這幾天有空,就分析了一下。**

1. 官方公告

這是checkpoint的新聞。Checkpoint對這個漏洞的描述是:VLC ParseJSS Null Skip Subtitle Remote Code Execution

http://blog.checkpoint.com/2017/05/23/hacked-in-translation/

這篇裡面有對應的cve列表

>https://threatpost.com/subtitle-hack-leaves-200-million-vulnerable-to-remote-code-execution/125868/

CVE列表

https://nvd.nist.gov/vuln/detail/CVE-2017-8313

https://nvd.nist.gov/vuln/detail/CVE-2017-8312

https://nvd.nist.gov/vuln/detail/CVE-2017-8311

對應的程式碼patch地址,在cve連結裡面有

這裡用vlc2.2.4版本的原始碼和32bit release來分析,大家可以自己到vlc官網下載。

有問題的函式程式碼貼在文章最後面,方便分析。

2. 分析漏洞

大致閱讀以下ParseJSS函式的程式碼,可以猜測漏洞應該跟緩衝區溢位有關,而且是堆上的緩衝區。

堆緩衝區溢位的利用思路一般是實現out-of-bounds write,根據write的資料不同,又有更具體的細分(實際上有的利用方法在最新的os裡面已經失效了)

覆蓋heap連結串列的元資料,實現write what where 覆蓋相鄰heap上的物件的虛表 覆蓋相鄰heap上的函式指標 覆蓋相鄰heap上的FILE物件 覆蓋相鄰heap上的陣列的元資料實現記憶體任意讀寫 等等

總的來說一句話:先實現out-of-bounds write,這是最關鍵的一步

CVE-2017-8313

這個cve的描述大致是由於在迴圈遍歷字串字元的時候,沒有檢查字串終止標記(0字元),導致out-of-bounds read。下面是對應的patch

這個改動很好理解,就不多說了。

其實我一度以為這個patch對應checkpoint對漏洞的描述:VLC ParseJSS Null Skip Subtitle Remote Code Execution。但實際上並不是。。。

不過這裡是out-of-bounds read,最多也就拋異常,如果能覆蓋SEH結構的話,倒還有點用,但是並沒有。所以先跳過這個。

CVE-2017-8312

這個cve的大概描述是由於沒有檢查字串長度,導致越界讀記憶體(out-of-bounds read),可能會讀到沒有初始化的資料。

從patch裡面可以看到,shift似乎受我們控制,但是這裡只能實現越界讀,並不能實現越界寫。

剩下最後一個了,看看有沒有驚喜。

CVE-2017-8311

這個cve的描述大概是由於跳過字串終止標記導致緩衝區溢位,從而導致執行任意程式碼。

看起來就是關鍵啊,先來看看patch。

這部分程式碼是在switch的這個分支裡面:case ‘’:

這裡psz_text被加了兩次,然後switch的break出去之後,還有一次psz_text++;總共加了3次。

所以如果剛好*(psz_text + 2) ==‘0’的話,會導致這個0字元被跳過,然後就溢位了。

問題是,看起來這個0字元後面的資料不受我們控制啊。

如果你嘗試構造一下類似的字串測試,會發現提前就被截斷了:

abcd‘0’ efg

efg這部分資料到不了後面的程式碼路徑。

怎麼辦?

如果你用偵錯程式自己一遍執行流程的話,你會發現,psz_text的地址似乎有可能每次都一樣的。

是不是想到了什麼?對的,就是類似heap spraying。

假設有兩個字串,1的長度比2的長

那麼載入1,首先在記憶體裡看到的是

BBBBBBBBBBBBBBB

然後載入2,在記憶體裡看到的是

aaaaaaaaaaa’0’BBBB

0字元後面的資料是受我們控制的

3. poc

考慮到影響,更進一步的分析就不做了,這裡放出供測試用的poc

把這段字串複製到文字檔案裡面,儲存為jss字尾的檔案就可以了。

0:0:0.0 0:0:0
0:0:0.0 0:0:0.0 [ BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
0:0:0.0 0:0:0.0 [ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaC

在偵錯程式裡驗證的結果

4.ParseJSS的程式碼方便參考

static int ParseJSS( demux_t *p_demux, subtitle_t *p_subtitle, int i_idx )
{
    VLC_UNUSED( i_idx );

    demux_sys_t  *p_sys = p_demux->p_sys;
    text_t       *txt = &p_sys->txt;
    char         *psz_text, *psz_orig;
    char         *psz_text2, *psz_orig2;
    int h1, h2, m1, m2, s1, s2, f1, f2;

    if( !p_sys->jss.b_inited )
    {
        p_sys->jss.i_comment = 0;
        p_sys->jss.i_time_resolution = 30;
        p_sys->jss.i_time_shift = 0;

        p_sys->jss.b_inited = true;
    }

    /* Parse the main lines */
    for( ;; )
    {
        const char *s = TextGetLine( txt );
        if( !s )
            return VLC_EGENERIC;

        psz_orig = malloc( strlen( s ) + 1 );
        if( !psz_orig )
            return VLC_ENOMEM;
        psz_text = psz_orig;

        /* Complete time lines */
        if( sscanf( s, "%d:%d:%d.%d %d:%d:%d.%d %[^nr]",
                    &h1, &m1, &s1, &f1, &h2, &m2, &s2, &f2, psz_text ) == 9 )
        {
            p_subtitle->i_start = ( (int64_t)( h1 *3600 + m1 * 60 + s1 ) +
                (int64_t)( ( f1 +  p_sys->jss.i_time_shift ) /  p_sys->jss.i_time_resolution ) )
                * 1000000;
            p_subtitle->i_stop = ( (int64_t)( h2 *3600 + m2 * 60 + s2 ) +
                (int64_t)( ( f2 +  p_sys->jss.i_time_shift ) /  p_sys->jss.i_time_resolution ) )
                * 1000000;
            break;
        }
        /* Short time lines */
        else if( sscanf( s, "@%d @%d %[^nr]", &f1, &f2, psz_text ) == 3 )
        {
            p_subtitle->i_start = (int64_t)(
                    ( f1 + p_sys->jss.i_time_shift ) / p_sys->jss.i_time_resolution * 1000000.0 );
            p_subtitle->i_stop = (int64_t)(
                    ( f2 + p_sys->jss.i_time_shift ) / p_sys->jss.i_time_resolution * 1000000.0 );
            break;
        }
        /* General Directive lines */
        /* Only TIME and SHIFT are supported so far */
        else if( s[0] == '#' )
        {
            int h = 0, m =0, sec = 1, f = 1;
            unsigned shift = 1;
            int inv = 1;

            strcpy( psz_text, s );

            switch( toupper( (unsigned char)psz_text[1] ) )
            {
            case 'S':
                 shift = isalpha( (unsigned char)psz_text[2] ) ? 6 : 2 ;

                 if( sscanf( &psz_text[shift], "%d", &h ) )
                 {
                     /* Negative shifting */
                     if( h < 0 )
                     {
                         h *= -1;
                         inv = -1;
                     }

                     if( sscanf( &psz_text[shift], "%*d:%d", &m ) )
                     {
                         if( sscanf( &psz_text[shift], "%*d:%*d:%d", &sec ) )
                         {
                             sscanf( &psz_text[shift], "%*d:%*d:%*d.%d", &f );
                         }
                         else
                         {
                             h = 0;
                             sscanf( &psz_text[shift], "%d:%d.%d",
                                     &m, &sec, &f );
                             m *= inv;
                         }
                     }
                     else
                     {
                         h = m = 0;
                         sscanf( &psz_text[shift], "%d.%d", &sec, &f);
                         sec *= inv;
                     }
                     p_sys->jss.i_time_shift = ( ( h * 3600 + m * 60 + sec )
                         * p_sys->jss.i_time_resolution + f ) * inv;
                 }
                 break;

            case 'T':
                shift = isalpha( (unsigned char)psz_text[2] ) ? 8 : 2 ;

                sscanf( &psz_text[shift], "%d", &p_sys->jss.i_time_resolution );
                break;
            }
            free( psz_orig );
            continue;
        }
        else
            /* Unkown type line, probably a comment */
        {
            free( psz_orig );
            continue;
        }
    }

    while( psz_text[ strlen( psz_text ) - 1 ] == '\' )
    {
        const char *s2 = TextGetLine( txt );

        if( !s2 )
        {
            free( psz_orig );
            return VLC_EGENERIC;
        }

        int i_len = strlen( s2 );
        if( i_len == 0 )
            break;

        int i_old = strlen( psz_text );

        psz_text = realloc_or_free( psz_text, i_old + i_len + 1 );
        if( !psz_text )
             return VLC_ENOMEM;

        psz_orig = psz_text;
        strcat( psz_text, s2 );
    }

    /* Skip the blanks */
    while( *psz_text == ' ' || *psz_text == 't' ) psz_text++;

    /* Parse the directives */
    if( isalpha( (unsigned char)*psz_text ) || *psz_text == '[' )
    {
        while( *psz_text != ' ' )
        { psz_text++ ;};

        /* Directives are NOT parsed yet */
        /* This has probably a better place in a decoder ? */
        /* directive = malloc( strlen( psz_text ) + 1 );
           if( sscanf( psz_text, "%s %[^nr]", directive, psz_text2 ) == 2 )*/
    }

    /* Skip the blanks after directives */
    while( *psz_text == ' ' || *psz_text == 't' ) psz_text++;

    /* Clean all the lines from inline comments and other stuffs */
    psz_orig2 = calloc( strlen( psz_text) + 1, 1 );
    psz_text2 = psz_orig2;

    for( ; *psz_text != '