Miscellaneous Series 2 — A Script Kiddie Diary in v8 Exploit Research Part 1
Introduction
Chrome is often a target by vulnerability researchers and many improvements were made to mitigate against the known exploits techniques used. This article will look at some significant improvements/mitigations and the impact to known exploit techniques.
In our opinion, for most exploit research, there are generally 3 phases, from discovering the vulnerability, to triggering the vulnerability to achieve oob read and write and finally exploiting to achieve code execution.
Our article will not be covering the 1st 2 phases and will focus on the 3rd Phase: Exploiting Vulnerability. To shortcut the whole process we will introduce a deliberate vulnerability (https://anvbis.au/posts/code-execution-in-chromiums-v8-heap-sandbox/) that can be triggered to change the length of an array to achieve out-of-bounds (OOB) read and write. With OOB primitive read and write achieved, we can then demonstrate the different exploit techniques.
We will be using d8 on Windows, the Chrome v8 shell to demonstrate the differences across the different versions, most examples we saw online uses d8 on Linux and we thought of doing something different, although the main difference we observed is mostly in the shellcode portion.
V8 versions and Exploitation Methodology
Over the years, new chrome version (and also v8) were released to address known exploitation techniques. The diagram below showed the version and the exploit techniques that can be used but they no longer work after new commits were released subsequently to address them.
To date, there are many known exploitation techniques, we have chosen 4 exploitation techniques listed in the table below. Note that none of them will work on the latest v8 version today. For those who wish to follow, we have provided the v8 commit that worked for us to demonstrate the techniques and also the reference articles (which explain better than we do). Please remember to apply the patch (https://anvbis.au/posts/code-execution-in-chromiums-v8-heap-sandbox/) as these commits do not have the deliberately introduced vulnerability by default.
S/N 1 will be covered in Part 1 of the article, S/N2 will be covered in Part 2 of the article and S/N 3 will be covered in Part 3 with some learning points on crafting of WASM functions (using WAT and wasm module builder, more on these later). There is no plans to cover S/N 4 as the provided POC is straightfoward to understand after learning more on WASM in Part 3 of the article.
What you need to know first
Because this article will mainly focus on the exploitation portion, we will not cover stuffs such as building v8 shell, Javscript object structures, helper functions such as float to integer conversion (vice-versa), addrof function. Readers are encouraged to read the articles we have collated below.
It is important to have an rough idea of the stuffs mentioned below.
- The location of the map and the position where the value of the first element is stored for Float Array and Object Array
- The position of the backing store for Array Buffer
- The location of the RWX jump table memory section from the base address of the Wasm Instance
Before moving on to the exploit sections, we will do a brief explanation of the fake object function that will be heavily used and thus it is important to understand the purpose.
The purpose of a fake object is create a fake float array structure so that we can manipulate the fake element field to insert any address we want to read and write the content at that address.
In order to create a fake object structure, it is required to able to perform oob read and write (after triggering vulnerability).
A pictorial representation of the memory layout of the required arrays object and what need to be done can be shown below.
The high level steps to create a fake object function is as follows:
- Create a float array (flt_arr) and an object array (obj_arr).
- Create a temp float array (temp_flt_arr) with 3 elements which we will be using it to create a fake float array.
- Using the oob read, get the map of a float array (e.g. for a float array with 2 elements, the map can be obtained by out-of-bound reading the 3rd element)
- Place the map value to index 0 of the temp float array.
- Get the address of the temp float array and subtract 0x18 (3x 64bit element size) to get the address of temp float array index 0 (temp_flt_arr[0]). This will be the starting address of our fake object.
3. Using the oob write, write the address of temp_flt_arr[0] to flt_arr[7] (which is also obj_arr[0]).
- *Important to note that we must write the value to a float array (for it to be represented as float) instead of an address of an object where dereferencing will occur.
4. Declare a variable “fake” and assign obj_arr[0] to this variable. By doing so, we are assigning an object and not a float.
5. To read any value from any address, place the target address — 0x8 to temp_flt_arr[2] and read fake[0].
- *So why is there need to subtract 0x8, this is because when reading the elements of a float array, the address in the element field will be dereferenced and the value at offset 8 from the dereferenced address will be read.
Make sense? Let’s try to understand this better in the next section where we will make use of the fake object function.
Pointer Compression
Since mid 2018 (https://issues.chromium.org/issues/40644166), Chrome and v8 had started to implement Pointer Compression where V8 only saves half of the pointer (the least significant bits) to memory and puts the most significant bits (upper 32 bits) of V8’s heap (known as the isolate root) into a root register (R13). Officially, Pointer compression was announced in Version 9.2 (https://v8.dev/blog/v8-release-92).
This is an example of how a float array memory layout will look like in pre and post pointer compression. Note that in version 7.1, the element field has the full 64 bit address whereas in version 9.1, the element field has only the lower 32bit address.
var testing123 = [1.1,1.2]
With the implementation of pointer compression, we will need the isolate_root to get the full 64 bit address, so let’s use the fake object concept described earlier to find the isolate_root.
According to https://tiszka.com/blog/CVE_2021_21225_exploit.html, Typed Array less than 64 bytes in length will store the isolate root in the heap for faster operation.
Using the following code snippet, we will provide the memory layout for each steps for a more clearer view.
Line 1 and 2 — creation of temp_flt_arr and changing index 0 to the float array map. The float map can be found by out-of-bound reading the 3rd element of the float array
Line 14 — isolate root is at offset 0x2c from the base address. Below is the memory layout of an uint8array and the location of the isolate root:
Address of arr_uint8 = 0x025b08088f18
isolate root is 0x2c from 0x025b08088f18 = 0000025b08088f44
Line 16 — use of fakeobj function with the address of temp_flt_arr — 0x18 (temp_flt_arr[0]) as the parameter. This is the starting address of our fake object.
This is how the fake float array (0x25b08088de8) look like in memory. Note that temp_flt_arr is actually at 0x25b08088e01. Element start at 0x25b08088de0.
The fake map of the float array is at index 0 and the fake element is at Index 1 which contain the target address-0x8. With that, v8 will interpret this address as a float array object due to the float array structure we have created using the elements in the temp float array.
Line 25 — In our example, we are able to get the isolate root value by reading fake[0] (dereferencing the element field at 0x25b08088f3c and reading the value at offset 0x8). Since this is a float array, a float value is returned.
Now that we are done with explaining the fake object function. Let’s move on to the exploit proper.
When life is simpler.. Exploiting on v8 version 9.1
A few years ago before the introduction of Ubercage (v8 Sandbox), if out-of-bounds (oob) read and write are achieved, we would have achieved shellcode execution easily using the following exploit strategy to write shellcode into a RWX memory section.
Key points:
- Array Buffer Object contain a backing store address which point to the buffer used to stored values written to the Array Buffer object.
- WASM Instance has a RWX memory section containing executable function codes
- If we can swap the buffer in the array buffer object to the WASM RWX memory address, then we can replace the content in the WASM instance with our shellcode instead.
Below is the high level description of the steps:
Line 147: an arb_read function is used to read the full 64 bit pointer. Below is the code snippet of the arb_read function
Line 101: The fake element field contain the backing store address pointer — 0x8.
Line 102: The original backing store address is replaced with the WASM Jump Table memory address.
Line 104 and Line 106: A Typed Array view is assigned to read from the Array Buffer and the 1st index which contain RWX memory address from the Jump Table is returned from the function.
Line 116:The original backing store address is replaced with the WASM Instance RWX memory address.
This is the memory layout after overwriting the backing store pointer (0xb50808b443+0x14) with the WASM instance RWX memory address (0x3b66c8c1000).
(*The isolate root is different from the one obtained in the earlier section due to ASLR)
In summary, for this exploit method, the requirements are:
- Array Buffer Backing Store 64 bit pointer
- 64 bit RWX memory address in WASM Instance
What happen if the array buffer backing store 64 bit pointer is removed?? Stay tune to Part 2 of the series where we will explore other location in the heap with 64 bit pointers that we control, replace it with the RWX 64 bit memory address of the WASM instance and then write the shellcode in.
Conclusion
In Part 1, we have briefly introduced the different exploitation techniques, explained how the fake object work through an example to find the isolate root and lastly the very old way of achieving code execution without Ubercage. While this technique no longer work, we felt that it provide the fundamental concepts of how to exploit. You will find usage of similar functions (e.g. addrof, fakeobj, arb_read/write), they are likely to differ as what used to work no longer work so another way is used to achieve the same intent.