diff --git a/ps5-kstuff/porting_tool/gdb_rpc.py b/ps5-kstuff/porting_tool/gdb_rpc.py new file mode 100644 index 0000000..103facf --- /dev/null +++ b/ps5-kstuff/porting_tool/gdb_rpc.py @@ -0,0 +1,209 @@ +import subprocess, os, threading, socket, sys, signal, time + +token = os.urandom(16).hex() + +rpc_server = ''' +import sys +sys.stdin = sys.__stdin__ +sys.stdout = sys.__stdout__ +print(%r) + +while True: + prompt = input() + try: ans = eval(prompt) + except gdb.error as e: ans = str(e) + print(repr([[[ans]]])) +'''%token + +class DisconnectedException(Exception): pass + +class GDB: + def __init__(self, ps5_ip, ps5_port=9019): + self.ps5_ip = ps5_ip + self.ps5_port = ps5_port + self.payload = None + self.payload_path = None + self.r0gdb_cflags = None + self.kstuff_cflags = None + self.popen = None + threading.Thread(target=self._monitor, daemon=True).start() + def bind_socket(self): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock0: + sock0.connect((self.ps5_ip, self.ps5_port)) + addr = sock0.getsockname()[0] + sock = socket.socket() + sock.bind((addr, 0)) + sock.listen(1) + return sock, sock.getsockname() + def use_r0gdb(self, flags=[]): + if self.popen == None or self.payload != 'r0gdb' or self.r0gdb_cflags != flags: + self.kill() + if self.r0gdb_cflags != flags: + self.build('prosper0gdb', flags) + self.r0gdb_cflags = flags + self.kstuff_cflags = None + self.send_payload('prosper0gdb/payload.bin') + self.connect_gdb() + self.payload = 'r0gdb' + return True + return False + def use_kstuff(self, flags1=[], flags2=[]): + if self.popen == None or self.payload != 'kstuff' or self.r0gdb_cflags != flags1 or self.kstuff_cflags != flags2: + self.kill() + if self.r0gdb_cflags != flags1: + self.build('prosper0gdb', flags1) + self.r0gdb_cflags = flags1 + self.kstuff_cflags = None + if self.kstuff_cflags != flags2: + self.build('ps5-kstuff', flags2) + self.kstuff_cflags = flags2 + self.send_payload('ps5-kstuff/payload-gdb.bin') + self.connect_gdb() + self.payload = 'kstuff' + return True + return False + def build(self, payload, flags): + assert not subprocess.call(('make', 'EXTRA_CFLAGS='+' '.join(flags), 'clean', 'all'), cwd='../../'+payload) + def send_payload(self, path): + with open('../../'+path, 'rb') as file: + data = memoryview(file.read()) + while True: + with socket.socket() as sock: + sock.settimeout(5) + print('Connecting to PS5...', end='') + sys.stdout.flush() + while True: + try: sock.connect((self.ps5_ip, self.ps5_port)) + except socket.error: + sys.stdout.write('.') + sys.stdout.flush() + time.sleep(1) + continue + break + sock.settimeout(None) + while data: + try: chk = sock.send(data) + except socket.error: break + data = data[chk:] + else: + print(' done') + self.payload_path = path + if self.payload_path.endswith('.bin'): + self.payload_path = self.payload_path[:-4]+'.elf' + return + print(' error, retrying') + def _monitor(self): + alive = False + while True: + p = subprocess.Popen(('ping', '-W', '5', self.ps5_ip), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, preexec_fn=type(self._monitor)(signal.alarm, 5)) + q = p.communicate()[0] + if p.wait() > 0 or b'64 bytes from ' not in q: + if alive: + print() + print('##################################################################################') + print('# Cannot ping PS5. It has probably panicked or hung. Restart the PS5 to proceed. #') + print('##################################################################################') + alive = False + q = self.popen + if q is not None: + q.kill() + else: + alive = True + def kill(self): + self.popen.kill() + self.popen = None + self.stdio = None + def _write(self, chk, tl=None): + if tl is not None: self.stdio.settimeout(tl-time.time()) + else: self.stdio.settimeout(None) + try: self.stdio.sendall(chk) + except OSError: + self.popen.kill() + self.popen = None + self.stdio = None + raise DisconnectedException("write failed") + def _read_until(self, ending, tl=None): + q = b'' + while not q.endswith(ending): + try: + if tl is not None: self.stdio.settimeout(tl-time.time()) + else: self.stdio.settimeout(None) + chk = self.stdio.recv(1) + except OSError: chk = b'' + if not chk: + self.popen.kill() + self.popen = None + self.stdio = None + raise DisconnectedException("read failed") + q += chk + return q + def _read_eval(self, tl=None): + while True: + ln = self._read_until(b'\n', tl) + if ln.startswith(b'[[['): + return eval(ln.decode('ascii'))[0][0][0] + def connect_gdb(self): + assert self.popen == None + print('Connecting GDB... ', end='') + sys.stdout.flush() + self.stdio, stdio = socket.socketpair(socket.AF_UNIX) + self.popen = subprocess.Popen(('gdb', '../../'+self.payload_path, '-ex', 'target remote '+self.ps5_ip+':1234', '-ex', 'py\n'+rpc_server+'\nend'), stdin=stdio, stdout=stdio, bufsize=0) + self._read_until(token.encode('ascii')+b'\n') + print('done') + def execute(self, cmd, timeout=None): + if timeout is not None: timeout += time.time() + assert self.popen != None + self._write(('gdb.execute('+repr(cmd)+', to_string=True)\n').encode(), timeout) + return self._read_eval(timeout) + def eval(self, expr, timeout=None, how='str'): + if timeout is not None: timeout += time.time() + assert self.popen != None + self._write((how+'(gdb.parse_and_eval('+repr(expr)+'))\n').encode(), timeout) + return self._read_eval(timeout) + def ieval(self, expr, timeout=None): + ans = self.eval(expr, timeout, 'int') + if not isinstance(ans, int): + self.kill() + raise DisconnectedException(ans) + return ans + def kill(self): + if self.popen == None: return + try: self.execute('p (int)kill(1, 30)', 5) + except DisconnectedException: return + self.popen.kill() + self.popen = None + self.stdio = None + +class R0GDB: + def __init__(self, gdb, cflags=[], setup_cmd=''): + self.gdb = gdb + self.cflags = cflags + self.trace_size = -1 + self.setup_cmd = setup_cmd + self.setup_cmd_run = False + def use_raw(self): + if self.trace_size not in (-1, -2): + self.gdb.kill() + if self.gdb.use_r0gdb(self.cflags): + self.setup_cmd_run = False + self.trace_size = -1 + if not self.setup_cmd_run: + self.execute(self.setup_cmd) + self.setup_cmd_run = True + if self.trace_size == -1: + self.execute('p r0gdb()') + self.trace_size = -2 + def use_trace(self, trace_size): + if self.trace_size < -1: + self.gdb.kill() + if self.gdb.use_r0gdb(self.cflags): + self.setup_cmd_run = False + self.trace_size = -1 + if not self.setup_cmd_run: + self.execute(self.setup_cmd) + self.setup_cmd_run = True + if trace_size > self.trace_size: + if ' = void\n' not in self.execute('p r0gdb_trace('+str(trace_size)+')'): + self.gdb.kill() + raise DisconnectedException("r0gdb_trace failed") + self.trace_size = trace_size diff --git a/ps5-kstuff/porting_tool/main.py b/ps5-kstuff/porting_tool/main.py new file mode 100644 index 0000000..8be1fe8 --- /dev/null +++ b/ps5-kstuff/porting_tool/main.py @@ -0,0 +1,312 @@ +import sys, json, threading, functools, os.path + +if 'linux' not in sys.platform: + print('This tool only supports GNU/Linux! Use Docker or WSL on other OSes.') + input('Press Enter to exit') + exit(1) +elif len(sys.argv) not in (3, 4, 5): + print('usage: main.py [port for payload loader] [kernel data dump]') + exit(0) + +import gdb_rpc + +gdb = gdb_rpc.GDB(sys.argv[2]) if len(sys.argv) == 3 else gdb_rpc.GDB(sys.argv[2], int(sys.argv[3])) + +with open(sys.argv[1]) as file: + symbols = json.load(file) + +def die(*args): + print(*args) + exit(1) + +def set_symbol(k, v): + assert k not in symbols or symbols[k] == v + if k not in symbols: + print('offset found! %s = %s'%(k, hex(v))) + symbols[k] = v + with open(sys.argv[1], 'w') as file: + json.dump(symbols, file) + +if 'allproc' not in symbols: + die('`allproc` is not defined') + +R0GDB_FLAGS = ['-DMEMRW_FALLBACK', '-DNO_BUILTIN_OFFSETS'] + +def ostr(x): + return str(x % 2**64) + +def retry_on_error(f): + @functools.wraps(f) + def f1(*args): + while True: + try: return f(*args) + except gdb_rpc.DisconnectedException: + print('\nPS5 disconnected, retrying %s...'%f.__name__) + return f1 + +derivations = [] + +def derive_symbol(f): + derivations.append(f) + return f + +def derive_symbols(*names): + def inner(f): + derivations.append((f, names)) + return f + return inner + +@retry_on_error +def dump_kernel(): + if len(sys.argv) == 5 and os.path.exists(sys.argv[4]): + with open(sys.argv[4], 'rb') as file: + data = file.read() + if int.from_bytes(data[8:16], 'little') == len(data) - 16: + return data[16:], int.from_bytes(data[:8], 'little') + gdb.use_r0gdb(R0GDB_FLAGS) + print('dumping kdata... ', end='') + sys.stdout.flush() + kdata_base = gdb.ieval('kdata_base') + gdb.eval('offsets.allproc = '+ostr(kdata_base + symbols['allproc'])) + gdb.eval('r0gdb_init_with_offsets()') + sock, addr = gdb.bind_socket() + with sock: + remote_fd = gdb.ieval('r0gdb_open_socket("%s", %d)'%addr) + remote_buf = gdb.ieval('malloc(1048576)') + one_second = gdb.ieval('(void*)(uint64_t[2]){1, 0}') + local_buf = bytearray() + def appender(): + nonlocal local_buf + with sock.accept()[0] as sock1: + while True: + q = sock1.recv(4096) + local_buf += q + if not q: return + s = str(len(local_buf)) + s += '\b'*len(s) + sys.stdout.write(s) + sys.stdout.flush() + thr = threading.Thread(target=appender, daemon=True) + thr.start() + total_sent = 0 + while total_sent < (134 << 20): + chk0 = gdb.ieval('copyout(%d, %d, %d)'%(remote_buf, kdata_base+total_sent, min(1048576, (134 << 20) - total_sent))) + if chk0 <= 0: break + offset = 0 + while offset < chk0: + chk = gdb.ieval('(int)write(%d, %d, %d)'%(remote_fd, remote_buf+offset, chk0-offset)) + assert chk > 0 + offset += chk + total_sent += chk + # this loop is to detect panics while dumping + while len(local_buf) != total_sent: + gdb.eval('(int)nanosleep(%d)'%one_second) + gdb.eval('(int)close(%d)'%remote_fd) + thr.join() + print() + if len(sys.argv) == 5: + with open(sys.argv[4], 'wb') as file: + file.write(kdata_base.to_bytes(8, 'little')) + file.write(len(local_buf).to_bytes(8, 'little')) + file.write(local_buf) + return bytes(local_buf), kdata_base + +def get_kernel(_cache=[]): + if not _cache: + _cache.append(dump_kernel()) + return _cache[0] + +@derive_symbol +@retry_on_error +def idt(): + kernel, kdata_base = get_kernel() + ks = bytes(kernel[i+2:i+4] == b'\x20\x00' and kernel[i+4] < 8 and kernel[i+5] in (0x8e, 0xee) and kernel[i+8:i+16] == b'\xff\xff\xff\xff\x00\x00\x00\x00' for i in range(0, len(kernel), 16)) + offset = ks.find(b'\1'*256) + assert ks.find(b'\1'*256, offset+1) < 0 + return offset * 16 + +@derive_symbol +@retry_on_error +def gdt_array(): + kernel, kdata_base = get_kernel() + ks = kernel[5::8] + needle = b'\x00\x00\xf3\xf3\x9b\x93\xfb\xf3\xfb\x8b\x00\x00\x00' * 16 + offset = ks.find(needle) + assert ks.find(needle, offset+1) < 0 + return offset * 8 + +@derive_symbol +@retry_on_error +def tss_array(): + kernel, kdata_base = get_kernel() + gdt_array = symbols['gdt_array'] + tss_array = [] + for i in range(16): + j = gdt_array + 0x68 * i + 0x48 + tss_array.append(int.from_bytes(kernel[j+2:j+5]+kernel[j+7:j+12], 'little')) + assert tss_array == list(range(tss_array[0], tss_array[-1]+0x68, 0x68)) + return tss_array[0] - kdata_base + +# XXX: relies on in-structure offsets, is it ok? +@derive_symbol +@retry_on_error +def pcpu_array(): + kernel, kdata_base = get_kernel() + planes = [b''.join(kernel[j+0x34:j+0x38]+kernel[j+0x730:j+0x738] for j in range(i, len(kernel), 0x900)) for i in range(0, 0x900, 4)] + needle = b''.join(i.to_bytes(4, 'little')*3 for i in range(16)) + indices = [i.find(needle) for i in planes] + unique_indices = set(indices) + assert len(unique_indices) == 2 and -1 in unique_indices + unique_indices.discard(-1) + i = unique_indices.pop() + j = indices.index(i) + indices[j] = -1 + assert set(indices) == {-1} + assert planes[j].find(needle, i+1) < 0 + return (i // 12) * 0x900 + j * 4 + +def get_string_xref(name, offset): + kernel, kdata_base = get_kernel() + s = kernel.find((name+'\0').encode('ascii')) + return kernel.find((kdata_base+s).to_bytes(8, 'little')) - offset + +@derive_symbol +@retry_on_error +def sysentvec(): return get_string_xref('Native SELF', 0x48) + +@derive_symbol +@retry_on_error +def sysentvec_ps4(): return get_string_xref('PS4 SELF', 0x48) + +def deref(name, offset=0): + kernel, kdata_base = get_kernel() + return int.from_bytes(kernel[symbols[name]+offset:symbols[name]+offset+8], 'little') - kdata_base + +@derive_symbol +@retry_on_error +def sysents(): return deref('sysentvec', 8) + +@derive_symbol +@retry_on_error +def sysents_ps4(): return deref('sysentvec_ps4', 8) + +# XXX: do we need to also find (calculate?) the header size? +@derive_symbol +@retry_on_error +def mini_syscore_header(): + kernel, kdata_base = get_kernel() + gdb.use_r0gdb(R0GDB_FLAGS) + remote_fd = gdb.ieval('(int)open("/mini-syscore.elf", 0)') + remote_buf = gdb.ieval('malloc(4096)') + assert gdb.ieval('(int)read(%d, %d, 4096)'%(remote_fd, remote_buf)) == 4096 + gdb.execute('set print elements 0') + gdb.execute('set print repeats 0') + ans = gdb.eval('((int)close(%d), {unsigned int[1024]}%d)'%(remote_fd, remote_buf)) + assert ans.startswith('{') and ans.endswith('}') and ans.count(',') == 1023, ans + header = b''.join(int(i).to_bytes(4, 'little') for i in ans[1:-1].split(',')) + return kernel.find(header) + +# https://github.com/cheburek3000/meme_dumper/blob/main/source/main.c#L80, guess_kernel_pmap_store_offset +@derive_symbol +@retry_on_error +def kernel_pmap_store(): + kernel, kdata_base = get_kernel() + needle = (0x1430000 | (4 << 128)).to_bytes(24, 'little') + i = 0 + ans = [] + while True: + i = kernel.find(needle, i) + if i < 0: break + if any(kernel[i+24:i+32]) and kernel[i+24:i+28] == kernel[i+32:i+36] and not any(kernel[i+36:i+40]): + ans.append(i - 8) + i += 1 + return ans[-1] + +@derive_symbol +@retry_on_error +def crypt_singleton_array(): + kernel, kdata_base = get_kernel() + ks = kernel[6::8] + ks1 = kernel[7::8] + needle = b'\xff\x00\xff\xff\xff\x00\x00\xff\x00\xff\xff\x00\x00\xff\x00\x00\x00\x00\xff\x00\xff\x00' + offset = ks.find(needle) + assert ks.find(needle, offset+1) < 0 + assert ks1[offset:offset+len(needle)] == needle + return offset * 8 + +def virt2phys(virt, phys, addr): + #print(hex(virt), hex(phys), hex(addr)) + assert phys == virt % 2**32 + pml = phys + for i in range(39, 3, -9): + idx = (addr >> i) & 511 + pml_next = gdb.ieval('{void*}%d'%(pml+idx*8+virt-phys)) + if pml_next & 128: + ans = (pml_next & (2**48 - 2**i)) | (addr & (2**i - 1)) + break + pml = pml_next & (2**48 - 2**12) + else: + ans = pml | (addr & 4095) + #print('->', hex(ans)) + return ans + +@derive_symbol +@retry_on_error +def doreti_iret(): + gdb.use_r0gdb(R0GDB_FLAGS) + kdata_base = gdb.ieval('kdata_base') + gdb.eval('offsets.allproc = '+ostr(kdata_base + symbols['allproc'])) + gdb.eval('r0gdb_init_with_offsets()') + idt = kdata_base + symbols['idt'] + tss_array = kdata_base + symbols['tss_array'] + #buf = gdb.ieval('{void*}%d'%(tss_array+0x1c+4*8)) + buf = gdb.ieval('kmalloc(2048)') + 2048 + for i in range(16): + tss = tss_array + i * 0x68 + gdb.ieval('{void*}%d = %d'%(tss+0x1c+4*8, buf)) + gdb.ieval('{char}%d = 0'%(idt+1*16+4)) + gdb.ieval('{char}%d = 4'%(idt+13*16+4)) + ptr = gdb.ieval('{void*}({void*}(get_thread()+8)+0x200)+0x300') + virt = gdb.ieval('{void*}%d'%ptr) + phys = gdb.ieval('{void*}%d'%(ptr+8)) + buf_phys = virt2phys(virt, phys, buf) + pages = set() + while True: + page = gdb.ieval('kmalloc(2048)') & -4096 + if page in pages: break + pages.add(page) + gdb.ieval('(void*)({void*[512]}%d = {%s})'%(page, ', '.join(map(str, ((i<<39)|135 for i in range(512)))))) + gdb.ieval('{void*}%d = %d'%(virt+8, virt2phys(virt, phys, page)|7)) + buf_alias = buf_phys | (1 << 39) + #print(hex(buf), hex(buf_alias)) + gdb.eval('bind_to_all_available_cpus()') + assert not gdb.ieval('(int)pthread_create(malloc(8), 0, hammer_thread, (uint64_t[2]){%d, malloc(65536)+65536})'%(buf_alias-32)) + assert not gdb.ieval('bind_to_some_cpu(0)') + if 'Remote connection closed' in gdb.eval('jmp_setcontext(1ull<<50)'): + raise gdb_rpc.DisconnectedException('jmp_setcontext') + pc = gdb.ieval('$pc') + gdb.kill() + assert (pc >> 32) == 16 + pc |= (2**64 - 2**32) + return pc - kdata_base + +print(len(symbols), 'offsets currently known') +print(sum(i.__name__ not in symbols for i in derivations), 'offsets to be found') + +for i in derivations: + if isinstance(i, tuple): + i, names = i + print('Probing offsets `%s`'%'`, `'.join(names)) + if any(j not in symbols for j in names): + try: value = i() + except Exception: + raise Exception("failed to derive `%s`, see above why"%'`, `'.join(names)) + assert len(value) == len(names) + for i, j in zip(names, value): + set_symbol(i, j) + elif i.__name__ not in symbols: + print('Probing offset `%s`'%i.__name__) + try: value = i() + except Exception: + raise Exception("failed to derive `%s`, see above why"%i.__name__) + set_symbol(i.__name__, value)