A Ph.D. A Hacker. My personal website: https://pwnbykenny.com
Have you ever thought about exploiting dynamically generated code? Do you know that an exploit can also be source code instead of data? The root cause of this bug is an inconsistency in the JIT compiler of v8. The inconsistency tricked the JIT compiler to dynamically generate code that contains the array overflow bug. Our final exploit for the bug is some source code. This is very different from normal exploitation whose exploit is data.
The post gives a summary of the exploitation process.
If you concatenate all the code snippets in this post, you will get an exploit that spawns a shell. However, sometimes you need to change the offsets provided in this post: fmap_offset, oele_offset, fakeFloat_offset. And the shellcode provided here is tested on x86_64 & Linux. It’s not guaranteed that the shellcode will work on other CPU architectures and operating systems.
If you download the folder from here, you will see there is a vulnerable d8 in it. And you can directly run it: ./d8.
The two functions iadd and i2f are used in the exploitation. iadd adds an integer offset to an address of floating-point format. i2f converts two 32-bit integers to a 64-bit floating-point number.
Line 9–11 defines 3 array objects: buggy, obj_arr, float_arr. The three objects are allocated in the heap from low address to high address. buggy is the overflow source. Line 12 uses the overflow source to overwrite the length field of buggy to the new_length. This line enables us to further access obj_arr and float_arr with buggy. Line 14 triggers the array overflow bug. The loop triggers the JIT compiler. The JIT compiler dynamically generates some code. The bug exists in the code. So when the code is executed, the bug is triggered.
Line 15–17 defines 3 important offsets. fmap_offset is the offset from buggy to the type field of float_arr. oele_offset is the offset from buggy to the element area of obj_arr. fakeFloat_offset is explained later when it’s used.
This section uses buggy and obj_arr. obj_arr is an object array. It stores addresses of objects. Each element in obj_arr is regarded as an object by v8. buggy is a double-precision array. It stores primitive floating-point numbers without any modifications such as pointer tagging.
In order to get the address of a given object, we need to first store this object into obj_arr, so that its address is an element of obj_arr. Next, we read the element using buggy because it doesn’t modify the address. oele_offset is the offset from buggy to the 0th element of obj_arr.
In order to fake an object from a given address, we first use buggy to put the address to the 0th element of obj_arr. Next, we use obj_arr to read the address. This will give us a fake object because the elements in obj_arr are always recognized as objects.
What’s important in the code are the two functions: read64 & write64. They are used to access arbitrary memory addresses. Let me explain from the beginning.
fmap_offset is the offset from buggy to the type field of float_arr. Line 1 retrieves the type of float_arr.
At line 2, we save our fake object in the element area of fakeArr. The fake object is a double-precision array. We call it fakeFloat. Its structure should be consistent with the structure of a JSArray. What’s important about fakeArr are its 0th, 2nd, and 3rd elements. The 0th element is a type value that indicates fakeFloat is a double-precision array. The 2nd element is a pointer to the element area of fakeFloat. We do arbitrary reads and writes by modifying the pointer. The 3rd element is the number of elements in fakeFloat. It doesn’t have to be 0x10. We use i2f to convert 0x10 to a floating-point number because fakeArr is a floating-point array.
Line 3 gets the address of fakeArr: fakeArr_addr. Line 4 adds an offset to fakeArr_addr so that we get the address of fakeFloat: fakeFloat_addr. Next, we use fakeobj to make v8 think that there is an object at the address fakeFloat_addr. So we have the fake object — fakeFloat now.
Line 6 defines read64 which does arbitrary reads. In order to read a memory address, we need to make the address become the element area of fakeFloat by modifying its element area pointer. This is done by line 14. Next, we get the value in the address by directly using fakeFloat to read its oth element.
Line 7 defined write64 which allows us to do arbitrary writes. It has the same mechanism as read64.
Line 1 through line 8 are wasm-related code generated by WasmFiddle. Line 1 stores the primitive wasm code in an array. Line 6 through line 8 encapsulate it into a function f. After the encapsulation, the primitive wasm code is stored into a RWX page. And a pointer to the page is saved in the wasmInstance’s structure.
Line 9gets the address of wasmInstance. Line 10 follows into its structure and reads out the pointer.
Line 11 is the shellcode that we want to inject to the address which the pointer points to. The shellcode will generate a shell.
Line 16 through line 19 prepare an ArrayBuffer object. We set its backing store pointer to the rwx_page_addr. The 0x20 is the offset from the beginning of ArrayBuffer to its backing store pointer.
Line 20 through line 22 write the shellcode to the rwx_page_addr. At line 23, we execute the shellcode by calling f. And then you will see this symbol: $, which means a shell is generated.
The post gives a summary of the exploitation process. For more details and the necessary background knowledge, please visit this link. You will find a more detailed walk-through there.
Also published at: https://medium.com/bugbountywriteup/v8-array-overflow-exploitation-2019-kctf-problem-5-%E5%B0%8F%E8%99%8E%E8%BF%98%E4%B9%A1-1aa51b9b2be6
Create your free account to unlock your custom reading experience.