A Ph.D. A Hacker. My personal website: https://pwnbykenny.com
I have introduced a v8 heap overflow bug before: V8 Array Overflow Exploitation: 2019 KCTF Problem 5 小虎还乡. This is another one: *CTF 2019 oob-v8. The interesting things I’m going to show you in this post are:
1) This bug only allows you to read or overwrite specific 8 bytes. But you can use it to achieve arbitrary reads and writes.
2) This is a different heap overflow bug. But you can exploit it in a very similar way to the 2019 KCTF Problem 5.
In fact, many heap overflow bugs can all be exploited in such a similar way. To show you this, I will use the same headings as the 2019 KCTF Problem 5 post. Feel free to compare the two posts!
PoC code is what triggers a bug. For this *CTF 2019 oob-v8 bug, we are able to read out-of-bounds with “JSArray.oob();”, and write out-of-bounds with “JSArray.oob(value);”. JSArray is a v8 object used to represent an array. For example, if you write code like “var arr = [1.1];”, you will have a JSArray object in the memory. And “arr.oob();” allows you to read the next 8 bytes beyond the array’s element area. “arr.oob(2.2);” allows you to overwrite the 8 bytes with 2.2.
For more information about JSArray, please visit V8 Objects and Their Structures.
Here is a summary on how we are gonna exploit the v8 heap overflow bug *CTF 2019 oob-v8.
The following exploitation steps use knowledge about v8 objects’ structures a lot.
First, download oob.diff from here. Second, read this section and build the vulnerable v8: V8 Architectures & Build V8. Therein, at Command 5, the [commit-hash-number] is 6dc88c191f5ecc5389dc26efa3ca0907faef3598. Before Command 8, run: git apply < oob.diff. Remember to put oob.diff into the folder “v8”.
They are exactly the same as 2019 KCTF Problem 5.
This is corresponding to the first step in the Exploitation Idea section. We need an object array and a double array. And we also need the value of their type field for the second step.
If you have read the post about objects’ structures. You will know that obj_arr is represented like this in the memory:
obj_arr.oob() reads the next 8 bytes beyond its element area. So it reads the value at offset +0x18 which is the type value of obj_arr. This is similar to double_arr.
This section corresponds to step 2 in the Exploitation Idea section.
The key idea of the two functions is that the type field controls what’s in the first element cell. If the type is double, then the cell stores a double number. If the type is object, then the cell stores an object pointer. If it’s an object pointer, we can’t get the pointer value. What we can get is the object itself instead of the pointer value. This is why we need to change the type to double. This is similar to FakeObjFrom.
At line A, we define a double array as a container. The container contains a fake double array – fake_double_arr, constructed according to a double array’s structure. We are able to change fake_double_arr’s Element Area Pointer to any address. This is how we achieve arbitrary reads and writes. This is also why we don’t directly define a double array. Direct definition doesn’t allow us to change its Element Area Pointer.
At line B, we get the container’s address. At line C, we calculate the fake_double_arr’s address by adding an offset to container_addr. You need to replace the question mark with the real offset. It may change on your machine. For me, it’s 0x30. You can find it out using v8 native functions: %DebugPrint, %SystemBreak. At line D, we finally let the address be recognized as an object pointer. So we have our fake_double_arr.
At line E, we define a Read64 function. First, we set its Element Area Pointer to an address that we are interested in. Second, we read and return its value. The Element Area has a structure. The 0x10 is the structure’s header size. Write64 is similar.
Finally, we get to the shellcode injection part. We want to inject to a RWX page. So we need to create such a page first. Fortunately, Wasm code is stored on such a page. Line 1 through line 4 uses Wasm code to define a function f. Line 5 and 6 traces into the structure of wasmInstance and retrieves the address of the Wasm code.
The address is at a certain offset from the beginning of the wasmInstance object. The offset may change on your machine. Remember to replace the question mark with the real offset. You can find it out using v8 native functions: %DebugPrint, %SystemBreak. For me, the offset is 0x88.
Line 7 defines shellcode. I wrote a post talking about shellcode. Please search it on the home page for more information about shellcode. Line 8, 10, 11 overwrites the backing store pointer of data_buf. A backing store is a buffer where an ArrayBuffer object stores its values. 0x20 is the offset of the backing store pointer within the structure of an ArrayBuffer object.
Line 10 replaces the backing store pointer with the RWX page’s address. Line 9, 12, 13, 14 writes the shellcode to the backing store, i.e. the RWX page, the address where the Wasm code is stored. Now the code of function f is overwritten to the shellcode. So we are able to execute the shellcode by calling f();.
This post describes the exploitation of an off-by-one bug *CTF 2019 oob-v8 that only gives you so limited ability. But the interesting point is that you are able to achieve much more powerful primitives by the bug. Besides, I also want to show you that many heap overflow bugs can be exploited in a similar way. If you compare this post and the “2019 KCTF Problem 5” post, you will find their exploitation process is similar.
If you concatenate the code snippets in section 5, you will have a working exploit. However, do replace the two question marks with their real value. Do some experiments/practices!
If you like my post or find it helpful, please help me share it on social media ~ Thank you!
Previously published at https://pwnbykenny.com/2020/12/21/novel-point-exploit-heap-overflow-ctf-2019-oob-v8/
Create your free account to unlock your custom reading experience.