Files
najdek 4488577730 feat: v0.6.0 release - unify architecture on ps5-webkit-remote-loader
- Unify userland exploit runner, ROP primitives, and payload pipeline on ps5-webkit-remote-loader
- Drop legacy umtx2 (FW <= 5.50)
- Improve Relapse stability
2026-10-07 17:14:39 +02:00

117 lines
4.8 KiB
JavaScript

// Offline checks for the third payload: stage the ELF, hand it to elfldr.
//
// payloads/autoload.js runs as payload 3 of 3, after the chain exploit has run
// and published api.krw, so there is no chain selection here to test: the page
// is already built for one chain. What is left is a syscall against a socket and
// a copy into a mapping, all of which can be driven from here with a fake api.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const source = fs.readFileSync('payloads/autoload.js', 'utf8');
function pointer(low) {
return { low, hi: 0, add32(n) { return pointer(this.low + n); } };
}
async function run(options = {}) {
const bytes = new Uint8Array(0x1007);
bytes.set([0x7f, 0x45, 0x4c, 0x46]);
for (let i = 4; i < bytes.length; i++) bytes[i] = i & 255;
const memory = new Map();
const events = [];
const sent = [];
const mapped = pointer(0x30000000);
let next = 0x20000000;
let connections = 0;
let closed = 0;
let unmapped = 0;
const expectedElf = (options.args && options.args[0]) || 'autoload.elf';
const api = {
// Payload 3: the exploit already ran, so krw is present unless we are
// testing the case where it did not.
krw: options.noKrw ? null : {},
args: options.args || [],
log: async () => {},
p: {
malloc(size) { const p = pointer(next); next += size; return p; },
write1(p, n) { memory.set(p.low, n & 255); },
write4(p, n) { for (let i = 0; i < 4; i++) memory.set(p.low + i, (n >>> (i * 8)) & 255); },
read4(p) { return [0, 1, 2, 3].reduce((v, i) => v | (memory.get(p.low + i) << (i * 8)), 0); },
},
chain: { async syscall(n, a, b, c) {
if (n === 0x61) return pointer(12);
if (n === 0x62) {
connections++;
assert.deepEqual(Array.from({ length: 8 }, (_, i) => memory.get(b.low + i)), [16, 2, 0x23, 0x3d, 127, 0, 0, 1]);
// The exploit brings elfldr up; options.loaderFails makes it never listen.
return pointer(options.loaderFails ? -1 : 0);
}
if (n === 6) { closed++; return pointer(0); }
if (n === 0x1dd) { events.push('map'); return pointer(options.mmapFails ? -1 : mapped.low); }
if (n === 0x49) { unmapped++; return pointer(0); }
if (n === 4) {
assert(api.krw, 'the payload must not send an ELF without kernel access');
if (options.writeFails) return pointer(-1);
const count = Math.min(c, 113); // Exercise short writes and the final tail.
for (let i = 0; i < count; i++) sent.push(memory.get(b.low + i));
return pointer(count);
}
throw new Error('Unexpected syscall ' + n);
} },
};
const sandbox = {
api, console, DataView, Uint8Array,
setTimeout(fn) { fn(); },
fetch: async (path) => {
events.push(path);
assert.equal(path, expectedElf, 'only the embedded ELF is fetched');
return {
ok: !options.fetchFails,
arrayBuffer: async () => options.badElf ? new ArrayBuffer(8) : bytes.buffer,
};
},
};
vm.createContext(sandbox);
let error;
try { await vm.runInContext(`(async function () { ${source}\n})().then(fn => fn(api))`, sandbox); }
catch (e) { error = e; }
return { error, events, sent, bytes, closed, unmapped, connections };
}
(async () => {
// The happy path: the whole ELF arrives intact.
const ok = await run();
assert.ifError(ok.error);
assert.deepEqual(ok.sent, [...ok.bytes]);
assert.equal(ok.unmapped, 1);
assert.equal(ok.closed, 1);
assert.equal(ok.connections, 1);
assert.deepEqual(ok.events, ['autoload.elf', 'map']);
// Custom named payload passed via payload args.
const custom = await run({ args: ['ps5-unified-autoloader-v0.1.5-915a65e.elf'] });
assert.ifError(custom.error);
assert.deepEqual(custom.sent, [...custom.bytes]);
assert.deepEqual(custom.events, ['ps5-unified-autoloader-v0.1.5-915a65e.elf', 'map']);
// No kernel access means nothing is sent at all, and nothing is mapped.
for (const options of [
{ noKrw: true }, { fetchFails: true }, { badElf: true }, { mmapFails: true },
]) {
const result = await run(options);
assert(result.error, 'expected a failure for ' + JSON.stringify(options));
assert.equal(result.sent.length, 0);
assert.equal(result.unmapped, 0);
}
// Failures after the ELF is mapped must still clean up.
for (const options of [{ loaderFails: true }, { writeFails: true }]) {
const result = await run(options);
assert(result.error, 'expected a failure for ' + JSON.stringify(options));
assert.equal(result.sent.length, 0);
assert.equal(result.unmapped, 1, 'the mapping must be released');
}
console.log('Autoload checks passed: ELF staging, hand-off to elfldr, short writes and cleanup.');
})().catch(error => { console.error(error); process.exitCode = 1; });