Exploiting Bug 1051017

Exploiting Bug 1051017

If you haven’t checked out my previous article on this bug, you can read that first here. I wrote this on commit 73f88b5f69077ef33169361f884f31872a6d56ac for an Ubuntu 20.04 machine.


The goal of this post is to slowly walkthrough creating an exploit for a vulnerability that I have previously analyzed. If you are unfamiliar with V8 I recommend checking out my series on V8 exploitation before reading this as I will gloss over many of the topics needed for complete understanding.

Stepping through

Verifying the POC

Thankfully, most of the hard work has already been done. Not every bug report provides a clear path to exploitation, but the POC in this one contains a function that returns an array with a huge OOB read/write primitive. From what I’ve seen, it’s very rare to get such a perfect setup. That makes this bug ideal for an introduction to understanding the exploitation process for a V8 bug.

The very first thing I did was run the POC to verify that it works.

Step 1 Code

madstacks@ubuntu:~/v8/out/x64.release$ ./d8 1.js 

Looks like we’re leaking a pointer!

Creating an Extended Array

Now I’m going to introduce my intended heap layout for the entire exploitation process. Each array’s purpose will become clearer over time, but I’ll explain a little now.

- trigger array elements -
-      trigger array     -
-   oob array elements   -
-        oob array       -
-  victim array elements -
-      victim array      -
-  arb_rw array elements -
-      arb_rw array      -

trigger array - This is the array which the vulnerability will cause an OOB access to be possible.

oob array - The trigger array will be used to overwrite the length of this array. We can now make the length of this array anything we want, not just what was allowed by the vulnerability.

victim array - The victim array will hold objects so that we can create the AddrOf and FakeObject primitives. The oob array will modify its contents to achieve this goal.

arb_rw array - This array will have its backing store pointer overwritten by the oob array in order to achieve arbitrary read and write.

Step 2 Code

Step 2 is focused on using the triggered array to overwrite the length field of the oob array. I modified the POC slightly to create my heap layout. Then, I stored the 4 arrays that get returned as global variables so that I can use them for my other primitives. For this step, I split the exploit into multiple parts. First, I made a section to store variables that we will need for the script. Second, I added a function that will overwrite the length of the oob array. Lastly, I created a section to initialize our global arrays and perform the exploitation steps.

Running this script with DEBUG set to true will show the addresses for each array as well as the address for the elements (backing store). These can be used to verify that the diagram above is correctly set up. It can also be used for the most difficult part of this section, which is finding the TRIGGERED_INDEX_OF_OOB_LENGTH value. The idea is to figure out what index of the triggered array would be the length field of the oob array. That can be determined with this equation:

(((tagged_oob_array_address - 1) + 12) - ((tagged_triggered_elements_address - 1) + 8)) / 4

As it turns out, all of this was unnecessary because the original POC provides an array with a length of 1073741323, but I wanted to show how this technique could be used had the length been shorter.

AddrOf and FakeObject

Next, I created functions for two primitives that are often used in JavaScript engine exploitation. I never wind up using the FakeObject primitive, but I included the function to show how it would usually be written.

Step 3 Code

In this step I included 2 more sections. The first was a set of helper functions, some of which came from this post. Many of the operations in a V8 exploit have to do with using floating point values to encode pointers and vice-versa. In V8, floats are represented by 64 bits while pointers and integers are represented by 32 bits. These functions will be important for easily managing those conversions.

At this point, the oob array can print up to 0x1000 quadwords of memory so I don’t need to use GDB to examine memory. This is my lazy approach to getting offsets between the arrays in my heap layout:

// dump raw memory using our oob array
console.log("OOB array");
for (i = 0; i < 50; i++) {
	console.log(i + " " + hexprintablef(oob[i]));

It’s important to remember that the floating point offsets will count by 64 bits but pointers are only 32 bits.

The other section I included was for the AddrOf and FakeObject primitives. This involved finding the offsets from the oob array to an index within the victim array. I specifically initialized the victim array with 4 objects because each object pointer is 32 bits and my overwrite will cause 2 slots to be overwritten. I didn’t want to have to deal with restoring an important pointer or shifting my addresses by 32 bits, so I’m guaranteeing that I’ll have a full quadword that I can use the lower half of and not have to worry about the upper half. I used %DebugPrint to see where the oob array’s backing store started and where the victim array’s backing store started. Subtracting these and dividing by 8 (rounding up) should be the index I need for OOB_INDEX_OF_VICTIM_SLOT. VICTIM_INDEX is 0 if you don’t have to round up, otherwise it’s 1.

Arbitrary R/W

The next step I implemented was the arbitrary r/w primitive. In my example, this is a read or write of 8 bytes at a memory location within the 32-bit box of our heap.

Step 4 Code

Writing these functions involves finding the index in the oob array that overlays the backing store pointer for the arb_rw array. Once again, I found this using my lazy technique for printing memory. Once I had the index, I had to perform some additional operations to save the upper 32 bits of the quadword that contains the pointer. As it turns out, the upper half is the length of the backing store. Modifying this value can be used to further extend the r/w ability, but since I only modify 8 bytes at a time this is unnecessary. I also included some instructions at the end for verifying that the arb_write function works as intended.

Code Execution

The last thing to do is create a RWX page in the process by introducing WebAssembly.

Step 5 Code

The first new section in this code is just the baseline for what is required to have a WebAssembly module. The other section I added, the final section, shows how an ArrayBuffer can be introduced to provide r/w outside of the current 32-bit context. Understanding this part involves looking at the memory layout of an ArrayBuffer to see how its backing store pointer is 64 bits. Once I knew where this pointer was located relative to the object’s address, I could just use the AddrOf and arbitrary write to move the backing store to any 64-bit address in the process.

The next difficult part is to find the RWX section’s memory address. To do this, I looked in GDB and saw that a pointer to this address is located 0x68 bytes past the end of the wasm_instance object. Again, using the previous primitives make retrieving this address relatively easy

Relation to Other Exploits

For future exploits, I anticipate that the only changes to this script will be the offsets located at the top and the function to trigger the vulnerability. However, there is room for improvement in strengthening the robustness of this code by verifying that the correct heap layout is created. This can be accomplished by adding specific values in each array and then scanning for those values to automatically determine offsets.

From this code I have made a skeleton for future exploits that are similar to this one.


I hope this post explained some of the complexities of turning a vulnerability into a working exploit for V8. I left out many of the details that I learned along the way, but that was intentional. Working through this on your own is truly the only way to understand the difficulties that you’ll encounter in other exploits. For practice, I’d recommend taking the final exploit, deleting the offsets at the top, and seeing if you can get it to work. As always, feel free to reach out to me with any questions!


Bug Report

This post is licensed under CC BY 4.0 by the author.