"""Link the eboot with the game's bundled system modules into one probe image. Successor of link_libc.py for several modules (default: libc.prx, then libSceFios2.prx). Every module is placed after the previous one; imports of all modules share one import table, so the C runtime can override any function (host contracts first) and otherwise binds the native export. Exports are matched by (NID, library identity, module identity). Imports of libSceLibcInternal are served by libc.prx's same-NID exports: the internal library is not shipped with the game and exposes the same functions. Output: out/boot-linked.bin (format BBPROBE5) and out/link.json. """ import bisect import collections import hashlib import json import os from pathlib import Path import struct from prepare import parse_self, span, unpack from link_libc import encode_id DEFAULT_MODULES = ('libc.prx', 'libSceFios2.prx') FS_LOAD = bytes.fromhex('64488b042500000000') RED_ZONE_SITES = Path(__file__).resolve().parent.parent / 'patches' / 'redzone.json' RED_ZONE_FUNCTIONS = Path(__file__).resolve().parent.parent / 'patches' / 'redzone_functions.json' LEA_DOWN = bytes.fromhex('488d642480') LEA_UP = bytes.fromhex('488da42480000000') def module(path): source = path.read_bytes() elf, header, ph, _, missing = parse_self(source) dp = next(p for p in ph if p['type'] == 2) dynamic = [] for pos in range(dp['offset'], dp['offset'] + dp['filesz'], 16): tag, value = unpack('> 48) identity = (string(value & 0xffffffff), (value >> 32) & 65535) if key in table and table[key] != identity: raise ValueError('conflicting module/library IDs') table[key] = identity def identity(name): parts = name.split('#') if len(parts) != 3: return None n, lib, mod = parts return (n, libraries[lib], modules[mod]) symbols = [] table = span(blob, tags[0x61000039], tags[0x6100003f]) for pos in range(0, len(table), 24): n, info, other, section, value, size = unpack('> 4, section=section, value=value, size=size, identity=identity(name) if name else None)) relocations = [] for ot, st in ((0x61000029, 0x6100002d), (0x6100002f, 0x61000031)): table = span(blob, tags[ot], tags[st]) for pos in range(0, len(table), 24): target, info, addend = unpack('> 32, addend)) return dict(elf=elf, header=header, ph=ph, tags=tags, symbols=symbols, relocs=relocations, identity=identity, libraries=libraries, modules=modules, sha256=hashlib.sha256(source).hexdigest(), missing=missing) def patch_fs_loads(image, ph, base): """Rewrite initial-exec `mov rax, fs:[0]` to GS: glibc owns FS on Linux.""" patched = 0 for p in ph: if p['type'] != 1 or not p['flags'] & 1: continue start, end = base + p['vaddr'], base + p['vaddr'] + p['filesz'] at = image.find(FS_LOAD, start, end) while at >= 0: image[at] = 0x65 patched += 1 at = image.find(FS_LOAD, at + len(FS_LOAD), end) return patched def protect_red_zones(image, sites, base, skip=()): """Run guest stores of red-zone leaf functions with rsp 128 bytes lower (Windows). Where Windows saves extended state in the compact XSAVE layout (Intel 12th gen and later), an exception's frame holds the AVX registers up to 0x28 bytes below rsp, over the System V red zone where leaf functions keep live locals (PS4 and Linux leave those 128 bytes alone). A store that faults on a page the GPU tracker write-protected is resumed with the locals overwritten: the attack-trail writer at 0x28ce7b0 then loads NULL from [rsp-0x60]. Each site (tools/redzone_sites.py: offset, original bytes, instruction lengths, negative = store) jumps to a copy at `base` where the stores run below the red zone. Sites whose bytes differ or that overlap a relocation (`skip`: sorted targets) stay as they are. Returns the copies and the number of sites redirected.""" code = bytearray() applied = 0 for offset, original, lengths in sites: original = bytes.fromhex(original) end = offset + len(original) near = bisect.bisect_right(skip, offset - 8) if image[offset:end] != original or (near < len(skip) and skip[near] < end): continue here = base + len(code) pos = offset for length in lengths: body = image[pos:pos + abs(length)] code += LEA_DOWN + body + LEA_UP if length < 0 else body pos += abs(length) code += b'\xe9' + struct.pack(' 1 else 0 insn = bytearray(image[start + pos:start + pos + n]) at[pos] = len(body) if kind == 0: body += insn elif kind == 1: modrm, disp, width = item[2], item[3], item[4] old = 0 if width == 0 else struct.unpack_from(' 512 * 1024 * 1024: raise ValueError('linked image exceeds probe limit') image.extend(bytes(base + modsize - len(image))) def mapped(address, length=8, loads=loads): return any(p['vaddr'] <= address and address + length <= p['vaddr'] + p['memsz'] for p in loads) for p in loads: if p['filesz'] > p['memsz']: raise ValueError(f'{filename}: invalid segment size') image[base + p['vaddr']:base + p['vaddr'] + p['filesz']] = span(m['elf'], p['offset'], p['filesz']) segments.append((base + p['vaddr'], p['memsz'], p['flags'])) tls = next((p for p in m['ph'] if p['type'] == 7), None) module_id = 0 if tls: if tls['filesz'] > tls['memsz'] or not mapped(tls['vaddr'], tls['memsz']): raise ValueError(f'{filename}: unsupported TLS layout') module_id = tls_module tls_module += 1 tls_relocations = 0 for target, kind, symid, addend in m['relocs']: if not mapped(target): raise ValueError(f'{filename}: relocation outside image {target:#x}') target += base if kind == 8: relocs.append((target, 0, base + addend, 0)) elif kind == 16: symbol = m['symbols'][symid] if symbol['name'] or symbol['type'] != 3 or addend or not module_id: raise ValueError(f'{filename}: unsupported external TLS relocation') struct.pack_into(' main_tls['memsz'] or main_tls['memsz'] > 1024 * 1024 or main_tls['vaddr'] + main_tls['filesz'] > size): raise ValueError('unsupported eboot TLS layout') procparam = next(p for p in main['ph'] if p['type'] == 0x61000001) with (out / 'boot-linked.bin').open('wb') as f: f.write(struct.pack('<8s6Q', b'BBPROBE5', len(image), entry, len(segments), len(relocs), len(names), flags)) f.write(struct.pack('= 128: raise ValueError('name too long') f.write(name.encode().ljust(128, b'\0')) for relocation in relocs: f.write(struct.pack('gs patched={fs_patched}, red-zone-safe stores at {red_zone_sites} sites') if __name__ == '__main__': import argparse p = argparse.ArgumentParser(description=__doc__) p.add_argument('game', type=Path) p.add_argument('--out', type=Path, default=Path(__file__).resolve().parent.parent / 'out') p.add_argument('--modules', nargs='*', default=list(DEFAULT_MODULES)) a = p.parse_args() link(a.game, a.out, a.modules)