diff --git a/README.md b/README.md index cc8d05a..8e1a780 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,18 @@ That's Discord's voice engine — it requests 1280x720 from every camera **Remote Play involved?** Never. The payload reads the display engine on the console itself. +## 🔬 Reverse engineering + +The whole story of how the console's display engine was mapped — the scan-out +ring, the pan, the chroma plane, the kernel-side notes and the delivery format +for kernel payloads — lives in +[`docs/REVERSE_ENGINEERING.md`](docs/REVERSE_ENGINEERING.md). +The bench used to get there is in [`tools/`](tools/): the library dumper +payload, the disassembly hunters, the deterministic frame renderers. + +> No Sony binaries are distributed with this project: `tools/dumplib/` extracts +> what you need **from your own console** to reproduce every finding. + ## 📄 License **Source-available**, copyright © 2026 **fstat13**. You may read and study the diff --git a/docs/REVERSE_ENGINEERING.md b/docs/REVERSE_ENGINEERING.md new file mode 100644 index 0000000..41ab436 --- /dev/null +++ b/docs/REVERSE_ENGINEERING.md @@ -0,0 +1,168 @@ +# PSCapture — Reverse Engineering Notes + +Everything in this document was obtained on a **personally owned, jailbroken +PS5** for interoperability purposes (reading the console's own frame buffer to +use it as a capture device). **No Sony binaries are distributed in this +repository** — the extracted libraries below stay on the researcher's machine; +the `tools/` folder contains only our own code, and `tools/dumplib/` is the +payload that extracts the libraries **from your own console** so you can +reproduce every finding yourself. + +--- + +## 1. Extracting the system libraries (`tools/dumplib/`) + +Method: a payload running under the jailbreak asks the kernel for the module +list of a live process and dumps each module to `/data` (FTP out). + +Getting there took two false walls: + +1. **`0xCC` fill** — reading a module image through the process' user mappings + yields trap padding for unloaded segments. The fix: read the *file image* + (the module as loaded from its source file), not the mapped view. +2. **`kernel_dynlib_handle` semantics** — the handle index counts modules per + process; querying the wrong process silently fails. The reliable recipe: + `kernel_dynlib_handle(pid, "libXxx.sprx") → handle → dump via the kernel's + own copy primitives`. + +What a full pass pulls from the console (kept locally, catalogued here): + +| File | Why it mattered | +|---|---| +| `libSceAvcap2.sprx`, `libSceAvcap2mvr.sprx` | the "capture" service family | +| `libSceVideoOut.sprx`, `libSceVideoOutSecondary.sprx` | video output | +| `libSceVideoRecording.sprx` | recording service (128 MB session file) | +| `libSceJpegEnc.sprx` | the hardware JPEG encoder | +| `libSceScreenShot.sprx` | system screenshot API | +| `libSceAvSetting.sprx`, `libSceAgc.sprx`, `libSceVenc.sprx`, `libSceVdecCore.native.sprx` | AV pipeline | +| `libSceIpmi.sprx`, `libSceMbus.sprx`, `libSceLncUtil.sprx` | IPC plumbing used by the services | +| `libSceSysmodule.sprx`, `libSceSystemService.sprx` | module loading / services | +| `libkernel.sprx`, `libkernel_sys.sprx` | syscall surface | +| `daemon/SceSysAvControl.elf` | **the key**: the AV control daemon | +| `daemon/AgcCompositor.elf`, `daemon/SceSysCore.elf` | companions used for cross-checks | + +Loaded-library inventory (which process has what) is produced by the same +payload (`recon` style listing). + +--- + +## 2. Where the pixels are + +The picture never travels through any "capture" API that apps can call +(that was the *other* project — the one with 128 MB session files and the IPMI +wall). Instead, the display controller (**DCE**) continuously writes the +scan-out frame into a ring buffer in memory, and a system daemon exposes the +geometry of that buffer. + +### The scan-out ring + +- Ring virtual address: `0x200200000`, ring data at `+0x4200000`. +- Four surfaces, `SURF_STRIDE = 0x600000` (6 MB) apart, written at 60 Hz. +- Each surface holds the frame as **P010** (10-bit), row stride `3840` bytes + (1920 px × 2 bytes). +- The daemon structure (`ScaninControl` @ `0x4f1958` inside + `SceSysAvControl.elf`, vtable `0x4e5078`) exposes: + - `+0x528` / `+0x530`: physical addresses of the two tap buffers, + - `+0x550` / `+0x554`: dimensions, + - `+0x55c`: flag, `+0x560`: slot table `{u64 w; u64 h; u32 pid; u32 tapPoint} × 3`, + - `getScaninBufferOffset` (`0x435a30`) returns the buffer paddr and the VA. + +### P010 decoding + +10-bit samples store the useful byte at **odd** indices; extracting the odd +bytes yields a clean 8-bit plane (the "even = low bits = noise" mistake was +made once and is documented so it is not repeated). + +### The pan ("dimension") + +The scan-out base inside the ring **drifts**: the frame's first pixel does not +sit at a fixed offset. Reading at a fixed offset yields the classic +"image rotates and slides" artifact. + +- Exact metric (v10): continuity across a cut. For a candidate cut `c*`, the + strip `[row r, col 0..]` must match `[row r-1, col c*..]`; minimize + `Σ |Y[r][j*2+1] − Y[r-1][(c+j)*2+1]|` over ~64 sampled rows. +- Pan conversion: `pan = ((1920 − c) % 1920) * 2` — no `+960` offset. +- Twin ambiguity: two minima 1920 bytes apart; the usable pair on this unit is + `scan + 1920`. **Field-verified constant: pan 2230** (kept fixed; drift is + ~2 B/s and below visibility). A user-controlled fine shift (PC-side) handles + the last few pixels. + +### The chroma plane (the hard one) + +- The chroma data starts at byte **`0x3EA300` (4 104 960)** — *not* after the + luma plane. The region before it is a dead zone. +- **In this plane the 16-bit words are V-first**: the pair order must be + swapped when building NV12 (verified byte-to-byte: error 15.5 → 1.7 → + 0.06 against a reference frame with the same content). +- Valid rows: 527 of 540; the tail is not yet written on read — replicate the + last good row (the famous "green bar" was the tail being read into the gap). +- The historical v10 build read 11 rows later (`0x3F4800 + pan`): same + palette, wrong position — that is why "v10 colors looked right but red-ish". + +### Color & geometry verification loop + +A reference frame (NV12 dump) plus tiny offline tools: + +- `tools/p010png.py` / `tools/nv12png.py` — deterministic renderers (same + math as the driver's YUY2 path, so screenshots match what Discord shows); +- byte-level diffs against a stored reference frame (mean |Δ| per plane); +- anchors for alignment: the centered title text of the setup screen + (993 px → 959 px after correction, on a 1920-wide frame); +- the correction itself: a cyclic horizontal shift, **even** values only + (an odd shift re-labels U/V pairs and hue-inverts the image). + +--- + +## 3. Kernel-side notes (why the payload is careful) + +- **Reads of kernel memory** go through a page-table walk (validation of every + translation step) and 8-byte reads via the direct-map alias. **Never** + `kernel_copyout` on a kernel address: a fault there is a kernel panic + (learned the hard way, once). +- **Ring mapping trick**: a cacheable user mapping of the DCE physical range + is built by writing one PML4 entry pointing at our own fresh page-table + subtree (the daemon's own PDE flags replicated: write-back, no PCD/PWT). + This is what makes bulk frame reads fast (4 GB/s instead of 45 MB/s through + the syscall path). +- **Quiescence**: each surface is being rewritten at 60 Hz; the reader copies, + waits 2 ms, compares, and moves on if the surface changed — avoids tearing. +- **Service setter**: `sceAvControlChangeScanInMode` is never called (it can + wedge the display engine and, in the worst case, the host app). + +## 4. Running code with kernel privileges (delivery) + +- **PLD format**: `eb 0b 'P' 'L' 'D'` + u64 `code_size`, then code, then data. + A loader on port **9019** maps `code_size` bytes executable and the rest RW + behind it, then calls the entry. This is how the ecosystem's kernel payloads + ship — and how further experiments (USB NIC driver) are delivered. +- Kernel modules compile against a vendored **FreeBSD 9.0** header set (the + Orbis kernel base, `__FreeBSD_version 900044`); per-firmware offset tables + (e.g. `allproc`, IDT, sysentvec for 13.60) exist in the scene and were + cross-checked. + +## 5. Tools in this repo + +| Tool | Purpose | +|---|---| +| `tools/dumplib/` | payload that extracts system libraries from your own console | +| `tools/sprx.py` | sprx ELF lister/extractor (exports, imports, sections) | +| `tools/callsites.py`, `tools/scan_refs.py` | xref hunt in disassembly (RIP-relative call sites) | +| `tools/whoimports.py`, `tools/stubmap.py` | import graph across the dumped libraries | +| `tools/p010png.py`, `tools/nv12png.py` | deterministic frame renderers (P010 / NV12) | +| `tools/flat.py`, `tools/asmwin.py` | binary utilities used during the hunt | + +## 6. Timeline (condensed) + +1. Capture through the official recording stack → dead end (session files, + IPMI client wall). +2. Direct read of the DCE ring → correct pixels, but the image "rotated": + the pan was discovered. +3. Pan metric, twin resolution, fixed 2230 — geometry solved. +4. Chroma plane hunt: base, dead zones, V-first words, tail replication — + colors solved (byte-verified). +5. Ring mapped cacheable → 1080p at ~19-35 fps over plain TCP to the PC. +6. Kernel-side delivery studied (PLD, 9019) → foundation for the next step: + a real USB network driver for full-rate/60 fps streaming. + +*Reported, reproduced, and maintained by **fstat13**.* diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..752aec9 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,14 @@ +# tools/ — the reverse-engineering bench + +All scripts here are PSCapture's own tooling (no Sony code). They are the +instruments used to produce the findings in `docs/REVERSE_ENGINEERING.md`. + +- `dumplib/` payload: extracts the system libraries from YOUR console +- `sprx.py` sprx (ELF) inspection/extraction +- `callsites.py` find RIP-relative call sites to an address +- `scan_refs.py` scan a disassembly for references to a target +- `whoimports.py`, `stubmap.py` import/stub mapping across libraries +- `p010png.py`, `nv12png.py` deterministic renderers of captured frames +- `flat.py`, `asmwin.py` misc binary helpers + +Usage examples are in the headers of each file. diff --git a/tools/asmwin.py b/tools/asmwin.py new file mode 100644 index 0000000..86ffa27 --- /dev/null +++ b/tools/asmwin.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +"""Stampa le righe di un disassemblato objdump il cui indirizzo cade in [lo,hi).""" +import re, sys +path, lo, hi = sys.argv[1], int(sys.argv[2], 0), int(sys.argv[3], 0) +pat = re.compile(r'^\s*([0-9a-f]+):') +for line in open(path, errors='replace'): + m = pat.match(line) + if not m: + continue + a = int(m.group(1), 16) + if lo <= a < hi: + sys.stdout.write(line) diff --git a/tools/callsites.py b/tools/callsites.py new file mode 100644 index 0000000..3da1d35 --- /dev/null +++ b/tools/callsites.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""Trova i call site di funzioni importate dentro un .sprx PS5. + +In un ET_SCE_DYNAMIC le chiamate alle librerie esterne passano dalla PLT, e la +PLT e' anonima (nessun nome, nessuna sezione). Ma ogni stub PLT e' un +`jmp *disp32(%rip)` che punta a uno slot GOT, e quello slot e' descritto da una +rilocazione in .rela.plt il cui indice di simbolo punta al dynsym: li' c'e' il +NID. Quindi: + + rilocazione .rela.plt -> (slot GOT -> NID) + scansione `ff 25` -> (stub PLT -> slot GOT) + scansione `e8` -> (call site -> stub PLT) + +Da li' si legge come il chiamante riempie le strutture: molto piu' veloce che +ricostruirle a tentativi sulla console. +""" +import struct +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import sprx as sprxmod + + +def parse_full(path): + d = open(path, 'rb').read() + phoff = struct.unpack_from(' NID, dall'insieme delle rilocazioni PLT.""" + d, v2f, tag = r['data'], r['v2f'], r['tag'] + jmprel = tag.get(0x17) # DT_JMPREL + pltrelsz = tag.get(2) or tag.get(0x2b) # DT_PLTRELSZ (a volte tag 2) + out = {} + if not jmprel or not pltrelsz: + return out + off = v2f(jmprel) + if off is None: + off = jmprel + for i in range(pltrelsz // 24): + o = off + i * 24 + r_offset, r_info, r_addend = struct.unpack_from('> 32 + if symidx < len(r['symnames']): + name = r['symnames'][symidx] + nid = name.split('#')[0] if '#' in name else name + out[r_offset] = nid + return out + + +def scan(r): + """Ritorna (stub PLT -> NID) e (call site -> stub PLT).""" + d, v2f, ph = r['data'], r['v2f'], r['ph'] + got2nid = plt_map(r) + + exec_segs = [p for p in ph if p[0] == 1 and (p[1] & 1)] + stub2nid = {} + call2stub = [] + + for t, fl, off, va, fsz, msz in exec_segs: + code = d[off:off + fsz] + # jmp *disp32(%rip) = ff 25 xx xx xx xx + for i in range(len(code) - 6): + if code[i] == 0xff and code[i + 1] == 0x25: + disp = struct.unpack_from(' %-12s %s' % (a, nid, want_nids[nid])) + + print('--- call site ---') + for a, tgt in calls: + nid = stub2nid.get(tgt) + if not nid or nid not in want_nids: + continue + print() + print(' ### %s chiamata da 0x%x' % (want_nids[nid], a)) + # decodifica il contesto attorno alla chiamata + off = v2f(a) + if off is None: + continue + start = max(0, off - ctx * 8) + lines = [] + i = start + while i < off + 8: + b = d[i:i + 1] + lines.append('%08x %s' % (i, ' '.join('%02x' % c for c in d[i:i + 8]))) + i += 8 + for l in lines: + print(' ' + l) + + +if __name__ == '__main__': + want = { + 'svzPXluOz8U': 'sceAvcap2Initialize', + 'gojqghDU+1Y': 'sceAvcap2Terminate', + '4jwUgirLFrA': 'sceAvcap2OpenVideo', + '8CGNCwBsItI': 'sceAvcap2Start', + 'pN0hIQZsMzc': 'sceAvcap2ReadVideo', + 'jkv9MlN7gvs': 'sceAvcap2ReadVideo2', + 'tR7gPe1i8hw': 'sceAvcap2Close', + 'YaXDckFygvw': 'sceAvcap2RequestKeyFrame', + 'Dht3tcbRoEY': 'sceAvcap2QueryVideoEncoderMemorySize', + 'X8c-f8LgzHo': 'sceAvcap2MapMemory', + '2GC2UnBJHIA': 'sceAvcap2GetFramePitch', + 'RW88AiwmL+Y': 'sceAvcap2GetSupportInformation', + '-NotZDC2pQc': 'sceAvcap2SetVideoEncodeConfig', + 'NeKlrUv+lbY': 'sceAvcap2GetNonVclNalUnitStream', + 'CYgd06bO5q0': 'sceAvcap2AllocateVideoEncoderMemory', + 'j7ClJpPRVQI': 'sceAvcap2GetImagePasteMemory', + } + for p in sys.argv[1:]: + report(p, want) diff --git a/tools/dumplib/Makefile b/tools/dumplib/Makefile new file mode 100644 index 0000000..9a479f7 --- /dev/null +++ b/tools/dumplib/Makefile @@ -0,0 +1,28 @@ +# Modellato sul sample hello_world dell'SDK. +# PS5_PORT 9021 = elfldr: e' la porta aperta sulla console. + +PS5_HOST ?= 192.168.0.36 +PS5_PORT ?= 9021 + +ifdef PS5_PAYLOAD_SDK + include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk +else + $(error PS5_PAYLOAD_SDK is undefined — compila dentro il container ps5sdk) +endif + +ELF := dumplib.elf +CFLAGS := -Wall -Werror -g -O2 + +all: $(ELF) + +$(ELF): main.c + $(CC) $(CFLAGS) -o $@ $^ + +clean: + rm -f $(ELF) + +# Spedisce il payload a elfldr sulla console; gira subito. +deploy: $(ELF) + $(PS5_DEPLOY) -h $(PS5_HOST) -p $(PS5_PORT) $^ + +.PHONY: all clean deploy diff --git a/tools/dumplib/main.c b/tools/dumplib/main.c new file mode 100644 index 0000000..4bd0f36 --- /dev/null +++ b/tools/dumplib/main.c @@ -0,0 +1,2262 @@ +/* ps5-dumplib — enumera ed estrae le librerie di sistema (.sprx) dalla memoria + * dei processi che le hanno caricate. + * + * Perche' funziona: con kstuff il payload gira con accesso kernel. Le librerie di + * sistema stanno mappate nella memoria dei processi che le usano, e il filesystem + * non le espone (vivono dentro le immagini PFS cifrate). Quindi non le scarichiamo: + * le leggiamo direttamente da chi le ha in RAM, gia' decifrate dalla console. + * + * La catena: + * kernel_get_proc(pid) -> kproc + * read(kproc + 0x3e8) -> testa della lista dynlib + * walk .next, leggi .path / .mapbase -> cosa e' caricato e dove + * proc_read(pid, base, buf, len) -> copia la memoria di quel processo + * + * La libreria e' un ELF, quindi si autodescrive: leggiamo header e program header, + * poi per ogni PT_LOAD copiamo p_filesz byte da base+p_vaddr scrivendoli nel file + * all'offset p_offset. Cosi' si ricostruisce il file. + * + * Nota: in memoria le rilocazioni possono essere gia' applicate, quindi il file + * ricostruito e' ottimo per l'analisi statica ma non identico byte-per-byte + * all'originale. Per farci reverse engineering va benissimo. + * + * uso: dumplib.elf lista tutto (processi + librerie caricate) + * dumplib.elf list idem + * dumplib.elf targets estrae la lista di default (encoder e affini) + * dumplib.elf [ ...] estrae quelle librerie in /data/dumps + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include /* struct kinfo_proc */ +#include + +#include +#include + + +#define OUTDIR "/data/dumps" +#define MAX_SEG (16) +#define MAX_LIBS (400) /* tetto per non far esplodere l'output */ +#define MAX_PATH (256) + +/* Offset della testa della lista dynlib dentro struct proc. + * Vale per i firmware gestiti da kernel.c (13.40, 13.60, ...). */ +#define KPROC_DYNLIB_HEAD (0x3e8) + + +/* ELF64 definiti a mano: qui non e' garantito. */ +typedef struct { + uint8_t e_ident[16]; + uint16_t e_type, e_machine; + uint32_t e_version; + uint64_t e_entry, e_phoff, e_shoff; + uint32_t e_flags; + uint16_t e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx; +} elf64_ehdr_t; + +typedef struct { + uint32_t p_type, p_flags; + uint64_t p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align; +} elf64_phdr_t; + +#define PT_LOAD (1) + + +/* dynlib_obj_t (crt/kernel.h). Il layout va rispettato esattamente, padding compreso. + * offset 0 next + * offset 8 path puntatore alla stringa del path completo + * offset 40 handle + * offset 48 mapbase base della MAPPATURA (range 0x8_0000_0000 = GPU/direct) + * offset 56 mapsize + * offset 64 textsize + * offset 120 vaddrbase base dell'indirizzo VIRTUALE utente <- candidato per l'ELF + */ +typedef struct { + uint64_t next; + uint64_t path; + uint64_t unknown0[2]; + uint32_t refcount; + uint32_t pad; + uint64_t handle; + uint64_t mapbase; + uint64_t mapsize; + uint64_t textsize; + uint64_t database; + uint64_t datasize; + uint64_t unknown1; + uint64_t unknown1size; + uint64_t entry; + uint64_t unknown2; + uint64_t vaddrbase; +} dynlib_head_t; + +#define OFF_VADDRBASE (120) + + +/* Nomi confermati dalla scansione: sono quelli che la console ha davvero caricati. */ +static const char *targets[] = { + "libSceVenc.sprx", /* encoder video hardware */ + "libSceVdecCore.native.sprx", /* decoder */ + "libSceVcodec.sprx", /* codec generico */ + "libSceJpegEnc.sprx", /* encoder JPEG hardware */ + "libSceShareInternal.native.sprx", /* pipeline share/registrazione */ + "libSceSharePlay.sprx", + "libSceShareFactoryUtil.sprx", + "libSceRemoteplay.sprx", /* server Remote Play */ + "libSceRemotePlayClientIpc.sprx", + "libSceAvcap2.sprx", /* cattura audio/video */ + "libSceVideoOut.sprx", + "libSceVideoRecording.native.sprx", /* registrazione video */ + "libSceVideoStreamingEngine_sys.sprx",/* motore di streaming */ + "webrtc_daemon.self", /* il daemon WebRTC */ + "libwebrtc.sprx", /* stack WebRTC completo */ + NULL +}; + +/* Le directory in cui la console tiene le librerie di sistema, provate in ordine. */ +static const char *lib_dirs[] = { + "/system/common/lib/", + "/system/priv/lib/", + "/system/common_ex/lib/", + NULL +}; + + +#define LOG(fmt, ...) do { \ + printf(fmt "\n", ##__VA_ARGS__); \ + klog_printf("[dumplib] " fmt, ##__VA_ARGS__); \ + } while(0) + + +/* Nodo dynlib e pid correnti: walk_dynlibs li aggiorna prima di ogni callback, + * cosi' le callback che devono leggere altri campi del nodo (enumerazione degli + * export) sanno dove guardare. */ +static unsigned long g_node; +static pid_t g_pid; + + +static const char *basename_of(const char *path); + + +/* --- lettura della memoria di un altro processo, saltando mdbg --- + * + * kernel_proc_copyout() dell'SDK prova mdbg_copyout() come PRIMA cosa e si fida + * del suo esito: + * + * if(!mdbg_copyout(pid, addr, buf, len)) return 0; // "successo" + * + * Su questo firmware mdbg risponde successo senza copiare niente, quindi il + * buffer resta col contenuto che aveva prima (0xCC, riempimento di stack) e il + * percorso buono — page-table walk + direct map — non viene mai raggiunto. + * Risultato: ogni lettura sembra riuscire e restituisce spazzatura. + * + * Qui rifacciamo la stessa cosa senza mdbg: traduciamo il virtuale in fisico + * camminando le page table del processo, poi leggiamo dal direct map. + * + * I due simboli esistono nel crt linkato ma non sono nell'header pubblico + * , quindi li dichiariamo qui. */ +extern unsigned long KERNEL_ADDRESS_DMAP_BASE; +extern int kernel_proc_getpaddr(pid_t pid, unsigned long vaddr, + unsigned long *paddr, unsigned long *plen); + +static int +proc_read(pid_t pid, intptr_t addr, void *buf, size_t len) +{ + size_t done = 0; + + while(done < len) { + unsigned long paddr = 0; + unsigned long plen = 0; + + if(kernel_proc_getpaddr(pid, (unsigned long)addr + done, &paddr, &plen)) { + return -1; + } + if(!plen) { + return -1; + } + if((size_t)plen > len - done) { + plen = len - done; + } + if(kernel_copyout(KERNEL_ADDRESS_DMAP_BASE + paddr, + (char*)buf + done, plen)) { + return -1; + } + done += plen; + } + + return 0; +} + + +/* Elenca i processi vivi e ritorna il buffer sysctl; il chiamante libera. */ +static void * +list_procs(size_t *out_size) +{ + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0}; + size_t sz = 0; + void *buf; + + if(sysctl(mib, 4, NULL, &sz, NULL, 0)) { + LOG("sysctl(size) fallita: %s", strerror(errno)); + return NULL; + } + if(!(buf = malloc(sz))) { + LOG("malloc(%zu) fallita", sz); + return NULL; + } + if(sysctl(mib, 4, buf, &sz, NULL, 0)) { + LOG("sysctl(proc) fallita: %s", strerror(errno)); + free(buf); + return NULL; + } + + *out_size = sz; + return buf; +} + + +/* Cammina la lista dynlib di un processo e chiama cb() per ogni voce. + * Ritorna il numero di voci viste. Se la testa non e' leggibile ritorna -1: + * e' il segnale che l'offset KPROC_DYNLIB_HEAD non vale per questo firmware. */ +static int +walk_dynlibs(pid_t pid, void (*cb)(pid_t, dynlib_head_t*, const char*, void*), + void *ud) +{ + unsigned long kproc; + uint64_t node; + char path[MAX_PATH]; + int n = 0; + + if(!(kproc = kernel_get_proc(pid))) { + return -1; + } + if(kernel_copyout(kproc + KPROC_DYNLIB_HEAD, &node, sizeof(node))) { + return -1; + } + + while(node && n < MAX_LIBS) { + dynlib_head_t d; + + if(kernel_copyout(node, &d, sizeof(d))) { + break; + } + + /* le callback che ne hanno bisogno (enumerazione export) leggono il nodo */ + g_node = node; + g_pid = pid; + + path[0] = '\0'; + if(d.path) { + if(kernel_copyout(d.path, path, sizeof(path) - 1)) { + strcpy(path, ""); + } + path[MAX_PATH - 1] = '\0'; + } + + cb(pid, &d, path, ud); + n++; + + if(d.next == node) { /* guardia contro un ciclo */ + break; + } + node = d.next; + } + + return n; +} + + +/* ---- callback: stampa ---- */ +static void +cb_print(pid_t pid, dynlib_head_t *d, const char *path, void *ud) +{ + (void)ud; + LOG(" pid %-6d handle=%-4llu mapbase=0x%012llx size=%-9llu %s", + (int)pid, (unsigned long long)d->handle, + (unsigned long long)d->mapbase, + (unsigned long long)d->mapsize, path); +} + + +/* ---- dump ---- */ + +/* Carica la libreria nel processo del payload. + * sceKernelLoadStartModule(path, argc, argv, flags, unk, unk, &handle) */ +extern int sceKernelLoadStartModule(const char *path, size_t argc, + const void *argv, unsigned int flags, + void *unk1, unsigned int *unk2, + int *handle); + + +/* Cerca una mappatura per basename fra le dynlib di UN processo. */ +typedef struct { + const char *want; + intptr_t base; + unsigned long size; +} find_ctx_t; + +static void +cb_find(pid_t pid, dynlib_head_t *d, const char *path, void *ud) +{ + find_ctx_t *c = ud; + (void)pid; + + if(c->base || !path[0]) { + return; + } + if(!strcmp(basename_of(path), c->want)) { + c->base = d->mapbase; + c->size = d->mapsize; + } +} + + +static intptr_t +find_in_process(pid_t pid, const char *name, unsigned long *out_size) +{ + find_ctx_t c = { name, 0, 0 }; + + walk_dynlibs(pid, cb_find, &c); + if(out_size) { + *out_size = c.size; + } + return c.base; +} + + +/* Ricostruisce il file leggendo dalla PROPRIA memoria: nessuna traduzione + * virtuale->fisico, quindi nessun 0xCC. */ +static int +dump_local(intptr_t base, const char *name, const char *outdir) +{ + elf64_ehdr_t eh; + elf64_phdr_t ph[MAX_SEG]; + char path[512]; + int fd; + + memcpy(&eh, (void*)base, sizeof(eh)); + + if(memcmp(eh.e_ident, "\x7f" "ELF", 4)) { + LOG("%s: non e' un ELF (base 0x%lx)", name, (unsigned long)base); + return -1; + } + if(eh.e_phnum > MAX_SEG) { + LOG("%s: %u program header, oltre il limite %d", name, eh.e_phnum, MAX_SEG); + return -1; + } + + memcpy(ph, (void*)(base + eh.e_phoff), (size_t)eh.e_phnum * sizeof(elf64_phdr_t)); + + snprintf(path, sizeof(path), "%s/%s", outdir, name); + + if((fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) { + LOG("%s: open(%s) fallita: %s", name, path, strerror(errno)); + return -1; + } + + size_t written = 0; + for(int i = 0; i < eh.e_phnum; i++) { + if(ph[i].p_type != PT_LOAD || ph[i].p_filesz == 0) { + continue; + } + if(ph[i].p_filesz > (64u << 20)) { + LOG("%s: segmento %d da %llu byte, sospetto — salto", name, i, + (unsigned long long)ph[i].p_filesz); + continue; + } + if(pwrite(fd, (void*)(base + ph[i].p_vaddr), ph[i].p_filesz, + ph[i].p_offset) != (ssize_t)ph[i].p_filesz) { + LOG("%s: scrittura segmento %d fallita", name, i); + close(fd); + return -1; + } + written += ph[i].p_filesz; + } + + close(fd); + chmod(path, 0644); + LOG("%s -> %s (%zu byte, base 0x%lx)", name, path, written, + (unsigned long)base); + return 0; +} + + +/* Carica la libreria nel proprio processo e la estrae. + * + * Leggere la memoria di un ALTRO processo richiede di tradurre virtuale->fisico, + * ed e' li' che la lettura torna 0xCC. Se invece la libreria sta nel NOSTRO + * address space si legge con un memcpy normale: nessuna traduzione, nessun + * direct map, nessun mdbg. Quindi la carichiamo e poi la copiamo da noi stessi. */ +__attribute__((unused)) +static int +load_and_dump(const char *name, const char *outdir) +{ + pid_t me = getpid(); + intptr_t base; + char full[512]; + int err; + int loaded = 0; + + /* gia' in casa? (es. se l'abbiamo caricata in un giro precedente) */ + base = find_in_process(me, name, NULL); + if(base) { + LOG("%s: gia' presente nel processo del payload", name); + return dump_local(base, name, outdir); + } + + for(int i = 0; lib_dirs[i]; i++) { + int handle = 0; + + snprintf(full, sizeof(full), "%s%s", lib_dirs[i], name); + err = sceKernelLoadStartModule(full, 0, NULL, 0, NULL, NULL, &handle); + + if(!err) { + LOG("%s: caricata da %s (handle %d)", name, full, handle); + loaded = 1; + break; + } + } + + if(!loaded) { + LOG("%s: caricamento fallito (ultimo errore 0x%08x)", name, err); + return -1; + } + + base = find_in_process(me, name, NULL); + if(!base) { + LOG("%s: caricata ma non trovata nel nostro spazio", name); + return -1; + } + + return dump_local(base, name, outdir); +} + +/* Controlla se all'indirizzo indicato c'e' la magic di un ELF. */ +static int +looks_like_elf(pid_t pid, intptr_t base) +{ + unsigned char m[4]; + + if(!base) { + return 0; + } + if(proc_read(pid, base, m, sizeof(m))) { + return 0; + } + return !memcmp(m, "\x7f" "ELF", 4); +} + + +/* Stampa i primi byte a un indirizzo usando DUE meccanismi diversi, per capire + * quale dei due legge davvero: + * - kernel_proc_copyout: mdbg oppure page-table walk + direct map + * - kernel_copyout: pipe trick su indirizzo kernel diretto + * Se danno risultati diversi, sappiamo quale strada e' quella giusta. */ +static void +probe(pid_t pid, const char *tag, intptr_t addr) +{ + unsigned char a[16], b[16]; + char ha[16 * 3 + 1], hb[16 * 3 + 1]; + int ra, rb, i; + + if(!addr) { + LOG(" %-10s = 0x0", tag); + return; + } + + ra = proc_read(pid, addr, a, sizeof(a)); + rb = kernel_copyout(addr, b, sizeof(b)); + + for(i = 0; i < 16; i++) { + snprintf(ha + i * 3, 4, "%02x ", a[i]); + snprintf(hb + i * 3, 4, "%02x ", b[i]); + } + + LOG(" %-10s = 0x%012lx", tag, (unsigned long)addr); + LOG(" proc [%s] %s", ra ? "fail" : " ok ", ra ? "-" : ha); + LOG(" kern [%s] %s", rb ? "fail" : " ok ", rb ? "-" : hb); +} + + +/* Cerca la magic ELF dentro una mappatura, a passi di pagina. + * Ritorna l'offset rispetto a base, oppure -1. */ +static long +scan_for_elf(pid_t pid, intptr_t base, unsigned long size) +{ + unsigned char page[4096]; + unsigned long off; + long limit = (long)(size > (4u << 20) ? (4u << 20) : size); /* tetto: 4 MiB */ + + for(off = 0; off + sizeof(page) <= (unsigned long)limit; off += sizeof(page)) { + if(proc_read(pid, base + off, page, sizeof(page))) { + continue; + } + for(size_t i = 0; i + 4 <= sizeof(page); i++) { + if(!memcmp(page + i, "\x7f" "ELF", 4)) { + return (long)(off + i); + } + } + } + return -1; +} +static int +dump_elf(pid_t pid, intptr_t base, const char *name, const char *outdir) +{ + elf64_ehdr_t eh; + elf64_phdr_t ph[MAX_SEG]; + char path[512]; + int fd; + + if(proc_read(pid, base, &eh, sizeof(eh))) { + LOG("%s: header illeggibile (pid %d)", name, (int)pid); + return -1; + } + if(memcmp(eh.e_ident, "\x7f" "ELF", 4)) { + LOG("%s: non e' un ELF (pid %d)", name, (int)pid); + return -1; + } + if(eh.e_phnum > MAX_SEG) { + LOG("%s: %u program header, oltre il limite %d", name, eh.e_phnum, MAX_SEG); + return -1; + } + if(proc_read(pid, base + eh.e_phoff, ph, + (size_t)eh.e_phnum * sizeof(elf64_phdr_t))) { + LOG("%s: program header illeggibili", name); + return -1; + } + + snprintf(path, sizeof(path), "%s/%s", outdir, name); + + if((fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) { + LOG("%s: open(%s) fallita: %s", name, path, strerror(errno)); + return -1; + } + + size_t written = 0; + for(int i = 0; i < eh.e_phnum; i++) { + void *seg; + + if(ph[i].p_type != PT_LOAD || ph[i].p_filesz == 0) { + continue; + } + /* Guardia: un p_filesz assurdo significa che stiamo leggendo spazzatura. */ + if(ph[i].p_filesz > (64u << 20)) { + LOG("%s: segmento %d da %llu byte, sospetto — salto", + name, i, (unsigned long long)ph[i].p_filesz); + continue; + } + if(!(seg = malloc(ph[i].p_filesz))) { + LOG("%s: malloc(%llu) fallita", name, + (unsigned long long)ph[i].p_filesz); + close(fd); + return -1; + } + if(proc_read(pid, base + ph[i].p_vaddr, seg, ph[i].p_filesz)) { + LOG("%s: lettura segmento %d fallita", name, i); + free(seg); + close(fd); + return -1; + } + if(pwrite(fd, seg, ph[i].p_filesz, ph[i].p_offset) != (ssize_t)ph[i].p_filesz) { + LOG("%s: scrittura segmento %d fallita", name, i); + free(seg); + close(fd); + return -1; + } + written += ph[i].p_filesz; + free(seg); + } + + close(fd); + chmod(path, 0644); + LOG("%s -> %s (%zu byte da pid %d)", name, path, written, (int)pid); + return 0; +} + + +/* Elenca /dev. + * + * libSceVideoOut cita /dev/dce (Display Controller Engine) e /dev/dipsw: sono i + * device della display controller, cioe' l'hardware che scandisce il frame + * finale verso l'HDMI. E' la pista piu' concreta per la cattura: se un payload + * puo' aprirli, da li' si puo' arrivare al buffer di scanout senza hookare la + * GPU di un altro processo. + * + * Il server FTP non elenca le sottodirectory della root, quindi lo facciamo + * da dentro il payload, che gira sulla console. */ +__attribute__((unused)) +static void +list_dev(void) +{ + DIR *d; + struct dirent *e; + int n = 0; + + if(!(d = opendir("/dev"))) { + LOG("/dev: opendir fallita: %s", strerror(errno)); + return; + } + + LOG("=== /dev ==="); + while((e = readdir(d)) != NULL && n < 80) { + if(!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) { + continue; + } + LOG(" %s", e->d_name); + n++; + } + closedir(d); + LOG("(/dev: %d voci)", n); +} + + +/* Prova ad aprire i device legati a video/display e vedere cosa rispondono. + * + * La cattura e' il pezzo incerto di tutto il progetto. Nella /dev non c'e' un + * device che dia il frame composto, ma ci sono hdmi e dipsw (che libSceVideoOut + * cita) e i deci_mp4_*. Se uno di questi espone riferimenti al buffer di + * scanout, la strada "framebuffer via kernel" diventa praticabile. Se + * rispondono solo con mode/timing o negano l'accesso, la strada e' chiusa e + * lo sappiamo adesso invece che dopo una settimana di RE. + * + * Tutto in sola lettura: apriamo, proviamo a leggere, chiudiamo. */ +__attribute__((unused)) +static void +probe_devices(void) +{ + static const char *devs[] = { + "/dev/hdmi", "/dev/dipsw", "/dev/dce", + "/dev/mbus_av", "/dev/s3da", + NULL + /* deci_mp4_* esclusi: aprono ma la read() blocca anche con O_NONBLOCK, + * e appendono il payload. */ + }; + + LOG("=== device video/display ==="); + + for(int i = 0; devs[i]; i++) { + unsigned char buf[32]; + int fd = open(devs[i], O_RDONLY | O_NONBLOCK); + ssize_t n; + + /* O_NONBLOCK e' obbligatorio: un device senza dati pronti bloccherebbe la + * read() e appenderebbe il payload, esattamente come e' successo al primo + * tentativo senza questa flag. */ + if(fd < 0) { + int rd_err = errno; + fd = open(devs[i], O_RDWR | O_NONBLOCK); + if(fd < 0) { + LOG(" %-20s R:%-18s RW:%s", devs[i], strerror(rd_err), + strerror(errno)); + continue; + } + LOG(" %-20s RW ok (fd %d) [sola lettura negata: %s]", + devs[i], fd, strerror(rd_err)); + } else { + LOG(" %-20s R ok (fd %d)", devs[i], fd); + } + + memset(buf, 0, sizeof(buf)); + n = read(fd, buf, sizeof(buf)); + if(n < 0) { + LOG(" read -> %s", strerror(errno)); + } else if(n == 0) { + LOG(" read -> 0 byte (EOF/nessun dato)"); + } else { + LOG(" read -> %zd byte: %02x %02x %02x %02x %02x %02x %02x %02x", + n, buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]); + } + + close(fd); + } +} + + +/* Enumerazione dei sysmodule: quali moduli di sistema sono raggiungibili dal + * payload, e con quale ID. + * + * sceSysmoduleIsLoaded() e' una QUERY: non carica niente, non prende lock, non + * puo' bloccare. E' il modo sicuro di sondare se l'encoder (o la libreria + * Remote Play) e' alla portata di un payload — che e' il lucchetto vero di + * tutta la strada "stream nostro". */ +/* ---- risolvere e chiamare funzioni di sistema SENZA linkare stub ---- + * + * Gli stub dell'SDK non sono linkati (solo libc, pthread, ufs, dl) e + * sceKernelLoadStartModule si blocca, quindi sembrava un vicolo cieco. + * + * Ma c'e' una via che non passa da nessuno dei due: + * 1. la libreria di sistema e' GIA' mappata nel nostro processo + * (libSceSysmodule lo e' — l'abbiamo visto nella lista delle nostre dynlib) + * 2. kernel_dynlib_resolve() risolve per NID, e restituisce l'indirizzo + * DENTRO IL NOSTRO spazio di indirizzamento + * 3. nid_encode() (include/ps5/nid.h) converte il nome umano nel NID + * + * Quindi: nessun caricamento, nessun blocco, e l'indirizzo e' chiamabile + * direttamente perche' sta nel nostro spazio. Vale per QUALUNQUE funzione di + * QUALUNQUE libreria che il processo ospite abbia gia' caricato. */ + +#include + +extern intptr_t kernel_dynlib_resolve(pid_t pid, unsigned int handle, + const char *nid); + + +typedef struct { + const char *want; + unsigned int handle; +} hctx_t; + +static void +cb_h(pid_t pid, dynlib_head_t *d, const char *path, void *ud) +{ + hctx_t *c = ud; + (void)pid; + + if(c->handle || !path[0]) { + return; + } + if(!strcmp(basename_of(path), c->want)) { + c->handle = (unsigned int)d->handle; + } +} + + +static unsigned int +handle_of(const char *lib) +{ + hctx_t c = { lib, 0 }; + + walk_dynlibs(getpid(), cb_h, &c); + return c.handle; +} + + +/* Risolve un simbolo di una libreria caricata nel nostro processo. + * Ritorna l'indirizzo, oppure 0. */ +static intptr_t +resolve_self(const char *lib, const char *sym) +{ + unsigned int h = handle_of(lib); + char nid[12] = {0}; + intptr_t fn; + + if(!h) { + LOG(" %s: non caricata nel nostro processo", lib); + return 0; + } + + nid_encode(sym, nid); + fn = kernel_dynlib_resolve(getpid(), h, nid); + + LOG(" %s!%s NID=%s -> 0x%lx", lib, sym, nid, (unsigned long)fn); + return fn; +} + + +/* Sonda i sysmodule via la funzione risolta a runtime. + * sceSysmoduleIsLoaded e' una query: non carica, non prende lock, non blocca. */ +__attribute__((unused)) +static void +probe_sysmodules(void) +{ + intptr_t fn = resolve_self("libSceSysmodule.sprx", "sceSysmoduleIsLoaded"); + int (*is_loaded)(unsigned int); + int n = 0; + + if(!fn) { + LOG("sceSysmoduleIsLoaded non risolta: niente enumerazione"); + return; + } + + is_loaded = (int (*)(unsigned int))fn; + + LOG("=== sysmodule caricati (via NID, nessun link) ==="); + for(unsigned int id = 0; id < 0x100; id++) { + if(is_loaded(id) == 0) { + LOG(" id 0x%03x", id); + n++; + } + } + LOG("(%d caricati su 0x100)", n); +} + + +/* ---- esperimento controllato ---- + * + * Tutto il problema delle letture sta nella catena + * VA -> (page-walk) -> paddr -> (direct map) -> byte + * che finora ha sempre prodotto 0xCC. Per capire QUALE anello e' rotto serve + * un caso di cui conosciamo la risposta esatta. + * + * Ce l'abbiamo: un buffer del nostro stesso processo. Ne conosciamo il VA, + * ci scriviamo un pattern noto, e poi facciamo il giro completo: + * 1. traduciamo il VA in fisico col page-walk + * 2. leggiamo dal direct map + * 3. confrontiamo col pattern che abbiamo scritto + * + * Se combacia, la catena e' sana e il problema e' negli indirizzi delle librerie. + * Se non combacia, sappiamo che e' il page-walk o il dmap base, e possiamo + * restringere ulteriormente. */ +__attribute__((unused)) +static void +self_test(void) +{ + static char marker[4096] __attribute__((aligned(4096))); + char buf[64]; + unsigned long paddr = 0; + unsigned long plen = 0; + pid_t me = getpid(); + const char *witness = "CONTROLLO-1234567890-abcdefghij"; + + memset(marker, 0, sizeof(marker)); + memcpy(marker, witness, 30); + + LOG("self-test: pid=%d marker VA=0x%lx", (int)me, (unsigned long)marker); + + if(kernel_proc_getpaddr(me, (unsigned long)marker, &paddr, &plen)) { + LOG(" page-walk FALLITO sul nostro stesso processo"); + return; + } + LOG(" paddr=0x%012lx plen=0x%lx dmap=0x%012lx", paddr, plen, + (unsigned long)(KERNEL_ADDRESS_DMAP_BASE + paddr)); + + memset(buf, 0, sizeof(buf)); + if(kernel_copyout(KERNEL_ADDRESS_DMAP_BASE + paddr, buf, sizeof(buf))) { + LOG(" lettura dal dmap FALLITA"); + return; + } + + LOG(" atteso : %.30s", witness); + LOG(" letto : %.30s", buf); + LOG(" ESITO : %s", memcmp(buf, witness, 30) ? "DIVERSO -> catena rotta" + : "UGUALE -> catena sana"); +} + + +/* ---- test cross-process ---- + * + * Il self-test prova la catena VA->paddr->dmap sul NOSTRO processo. Ma il + * problema riguarda gli ALTRI processi, quindi serve un caso di controllo la'. + * + * Ce n'e' uno pulito: ogni processo ha il proprio eboot.bin mappato a 0x400000, + * e un eseguibile ELF comincia per \x7f E L F. Se leggendo li' troviamo la magic, + * il walk funziona anche cross-process e il colpevole e' specifico degli + * indirizzi delle librerie. Se troviamo 0xCC, il walk cross-process e' rotto. */ +typedef struct { + pid_t pid; + intptr_t base; +} xctx_t; + +static void +cb_x(pid_t pid, dynlib_head_t *d, const char *path, void *ud) +{ + xctx_t *c = ud; + + if(c->base) { + return; + } + if(!strcmp(basename_of(path), "eboot.bin")) { + c->pid = pid; + c->base = d->mapbase; + } +} + +__attribute__((unused)) +static void +cross_test(void) +{ + size_t sz; + void *buf = list_procs(&sz); + xctx_t c = { 0, 0 }; + + if(!buf) { + return; + } + + for(void *ptr = buf; ptr < (buf + sz);) { + struct kinfo_proc *ki = (struct kinfo_proc*)ptr; + + if(ki->ki_structsize <= 0) { + break; + } + ptr += ki->ki_structsize; + + if(ki->ki_pid > 0) { + walk_dynlibs(ki->ki_pid, cb_x, &c); + if(c.base) { + break; + } + } + } + free(buf); + + if(!c.base) { + LOG("cross-test: nessun eboot.bin trovato"); + return; + } + + { + unsigned char b[16] = {0}; + unsigned long paddr = 0, plen = 0; + + LOG("cross-test: eboot.bin di pid %d a 0x%lx", (int)c.pid, + (unsigned long)c.base); + + if(proc_read(c.pid, c.base, b, sizeof(b))) { + LOG(" lettura FALLITA"); + return; + } + + LOG(" primi byte: %02x %02x %02x %02x %02x %02x %02x %02x", + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]); + LOG(" e' un ELF? %s", + memcmp(b, "\x7f" "ELF", 4) ? "NO -> walk cross-process rotto" + : "SI -> walk cross-process sano"); + + if(!kernel_proc_getpaddr(c.pid, (unsigned long)c.base, &paddr, &plen)) { + LOG(" paddr=0x%012lx plen=0x%lx", paddr, plen); + } + } +} + + +/* ---- scandaglio di una libreria ---- + * + * Se una mappatura fosse tutta 0xCC sarebbe sospetto: del codice che gira non + * puo' essere riempimento. Se invece solo alcune pagine lo sono, allora la + * mappatura e' parzialmente residente e stiamo leggendo quella parte. + * Leggiamo punti sparsi invece del solo inizio, che ci ha ingannati finora. */ +typedef struct { + const char *want; + pid_t pid; + intptr_t base; + unsigned long size; +} lctx_t; + +static void +cb_l(pid_t pid, dynlib_head_t *d, const char *path, void *ud) +{ + lctx_t *c = ud; + + if(c->base) { + return; + } + if(!strcmp(basename_of(path), c->want)) { + c->pid = pid; + c->base = d->mapbase; + c->size = d->mapsize; + } +} + +__attribute__((unused)) +static void +scan_library(const char *name) +{ + size_t sz; + void *buf = list_procs(&sz); + lctx_t c = { name, 0, 0, 0 }; + + if(!buf) { + return; + } + + for(void *ptr = buf; ptr < (buf + sz);) { + struct kinfo_proc *ki = (struct kinfo_proc*)ptr; + + if(ki->ki_structsize <= 0) { + break; + } + ptr += ki->ki_structsize; + + if(ki->ki_pid > 0) { + walk_dynlibs(ki->ki_pid, cb_l, &c); + if(c.base) { + break; + } + } + } + free(buf); + + if(!c.base) { + LOG("scan %s: non trovata", name); + return; + } + + LOG("scan %s: pid %d base=0x%lx size=%lu", + name, (int)c.pid, (unsigned long)c.base, c.size); + + { + static const unsigned long offs[] = { + 0x0, 0x1000, 0x2000, 0x10000, 0x40000, 0x80000 + }; + + for(unsigned i = 0; i < sizeof(offs) / sizeof(offs[0]); i++) { + unsigned char b[16] = {0}; + unsigned long paddr = 0, plen = 0; + + if(offs[i] >= c.size) { + break; + } + if(proc_read(c.pid, c.base + offs[i], b, sizeof(b))) { + LOG(" +0x%-7lx ", offs[i]); + continue; + } + if(!kernel_proc_getpaddr(c.pid, (unsigned long)c.base + offs[i], + &paddr, &plen)) { + LOG(" +0x%-7lx paddr=0x%012lx %02x %02x %02x %02x %02x %02x %02x %02x", + offs[i], paddr, b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]); + } + } + } +} + + +/* ---- dump di una libreria: mappatura + ELF sintetico ---- + * + * Il loader PS5 riempie di 0xCC la prima pagina di un .sprx caricato: l'header + * ELF originale non sopravvive, quindi ricostruire il file com'era e' impossibile. + * Ma al reverse engineering serve il CODICE, e quello e' intatto da +0x1000 in poi. + * + * Quindi non inseguiamo il file originale: dumpiamo la mappatura e le costruiamo + * attorno un header ELF64 valido, con un PT_LOAD che copre tutto a p_vaddr = base. + * Il risultato si apre direttamente in Ghidra, objdump o nel container rea, con + * gli indirizzi corretti. + * + * Layout del file prodotto: + * 0x0000 Elf64_Ehdr + 1 Elf64_Phdr (dentro la prima pagina) + * 0x1000 in poi la mappatura, identica byte per byte + */ +/* Ritorna il numero di byte LEGGIBILI, non solo 0/-1: alcune mappature sono + * parzialmente non residenti (pagine mai toccate), e fra piu' processi che + * mappano la stessa libreria quello col maggior numero di pagine residenti e' + * il candidato migliore. */ +static long +dump_library(pid_t pid, intptr_t base, unsigned long size, + const char *name, const char *outdir) +{ + const unsigned long PAGE = 0x1000; + elf64_ehdr_t eh; + elf64_phdr_t ph; + unsigned char preamble[PAGE]; + char path[512]; + unsigned char *chunk; + int fd; + + if(size < PAGE || size > (64u << 20)) { + LOG("%s: dimensione mappatura sospetta (%lu)", name, size); + return -1; + } + + memset(&eh, 0, sizeof(eh)); + memset(&ph, 0, sizeof(ph)); + memset(preamble, 0, sizeof(preamble)); + + /* --- header ELF64 --- */ + eh.e_ident[0] = 0x7f; + eh.e_ident[1] = 'E'; + eh.e_ident[2] = 'L'; + eh.e_ident[3] = 'F'; + eh.e_ident[4] = 2; /* ELFCLASS64 */ + eh.e_ident[5] = 1; /* ELFDATA2LSB */ + eh.e_ident[6] = 1; /* EV_CURRENT */ + eh.e_ident[7] = 0; /* ELFOSABI_SYSV */ + eh.e_type = 3; /* ET_DYN */ + eh.e_machine = 62; /* EM_X86_64 */ + eh.e_version = 1; + eh.e_entry = (uint64_t)base + PAGE; + eh.e_phoff = sizeof(elf64_ehdr_t); + eh.e_shoff = 0; + eh.e_ehsize = sizeof(elf64_ehdr_t); + eh.e_phentsize = sizeof(elf64_phdr_t); + eh.e_phnum = 1; + + /* --- un PT_LOAD che copre la mappatura --- */ + ph.p_type = PT_LOAD; + ph.p_flags = 7; /* R|W|X */ + ph.p_offset = PAGE; /* i dati iniziano dopo la prima pagina */ + ph.p_vaddr = (uint64_t)base; + ph.p_paddr = (uint64_t)base; + ph.p_filesz = (uint64_t)size; + ph.p_memsz = (uint64_t)size; + ph.p_align = PAGE; + + memcpy(preamble, &eh, sizeof(eh)); + memcpy(preamble + sizeof(eh), &ph, sizeof(ph)); + + snprintf(path, sizeof(path), "%s/%s", outdir, name); + + if((fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) { + LOG("%s: open(%s) fallita: %s", name, path, strerror(errno)); + return -1; + } + + if(pwrite(fd, preamble, PAGE, 0) != (ssize_t)PAGE) { + LOG("%s: scrittura preambolo fallita", name); + close(fd); + return -1; + } + + /* --- la mappatura, a blocchi --- */ + if(!(chunk = malloc(64 * 1024))) { + LOG("%s: malloc fallita", name); + close(fd); + return -1; + } + + unsigned long off = 0; + unsigned long good = 0; + + /* Lettura PAGINA PER PAGINA, non a blocchi da 64 KB. + * + * proc_read fallisce l'intero blocco se anche una sola pagina al suo interno + * non e' residente. Con blocchi da 64 KB bastava una pagina mancante per + * perdere 15 pagine buone — ed e' quello che faceva sembrare illeggibili + * libwebrtc (0%) e libSceVideoRecording (6%), quando un campione a 4 KB + * mostrava 8 pagine su 8 leggibili. */ + const unsigned long CH = 4096; + + while(off < size) { + unsigned long n = size - off; + + if(n > CH) { + n = CH; + } + + if(proc_read(pid, base + off, chunk, n)) { + /* pagina non residente: la riempiamo di zero e proseguiamo. + * Meglio un buco che perdere tutto il resto. */ + memset(chunk, 0, n); + } else { + good += n; + } + + if(pwrite(fd, chunk, n, (off_t)(PAGE + off)) != (ssize_t)n) { + LOG("%s: scrittura a offset 0x%lx fallita", name, off); + free(chunk); + close(fd); + return -1; + } + off += n; + } + + free(chunk); + close(fd); + chmod(path, 0644); + + LOG("%s <- pid %d (0x%lx): %lu byte su %lu leggibili (%lu%%)", + name, (int)pid, (unsigned long)base, good, size, + size ? (good * 100 / size) : 0); + return (long)good; +} + + +/* La stessa libreria e' mappata in PIU' processi, e non tutti sono leggibili: + * alcuni danno 0 byte su tutta la mappatura pur avendo i metadati accessibili. + * Quindi raccogliamo tutti i candidati e li proviamo in ordine, finche' uno + * si lascia leggere, invece di fermarci al primo trovato. */ +typedef struct { + const char *want; + int n; + pid_t pid[8]; + intptr_t base[8]; + unsigned long size[8]; +} mcand_t; + +static void +cb_m(pid_t pid, dynlib_head_t *d, const char *path, void *ud) +{ + mcand_t *c = ud; + + if(c->n >= 8) { + return; + } + if(!path[0] || strcmp(basename_of(path), c->want)) { + return; + } + + c->pid[c->n] = pid; + c->base[c->n] = d->mapbase; + c->size[c->n] = d->mapsize; + c->n++; +} + + +static int +dump_library_by_name(const char *name, const char *outdir) +{ + size_t sz; + void *buf = list_procs(&sz); + mcand_t c; + + memset(&c, 0, sizeof(c)); + c.want = name; + + if(!buf) { + return -1; + } + + for(void *ptr = buf; ptr < (buf + sz);) { + struct kinfo_proc *ki = (struct kinfo_proc*)ptr; + + if(ki->ki_structsize <= 0) { + break; + } + ptr += ki->ki_structsize; + + if(ki->ki_pid > 0) { + walk_dynlibs(ki->ki_pid, cb_m, &c); + } + } + free(buf); + + if(!c.n) { + LOG("%s: non trovata in nessun processo", name); + return -1; + } + + LOG("%s: %d processi la mappano, provo tutti e tengo il migliore", name, c.n); + + long best = 0; + + for(int i = 0; i < c.n; i++) { + long g = dump_library(c.pid[i], c.base[i], c.size[i], name, outdir); + + if(g > best) { + best = g; + } + /* mappatura completamente leggibile: non c'e' di meglio da cercare */ + if(g > 0 && (unsigned long)g == c.size[i]) { + return 0; + } + } + + if(!best) { + char path[512]; + + snprintf(path, sizeof(path), "%s/%s", outdir, name); + unlink(path); + LOG("%s: nessun processo con pagine leggibili", name); + return -1; + } + + LOG("%s: migliore parziale, %ld byte", name, best); + return 0; +} + + +/* Chiede al sistema di caricare l'encoder nel NOSTRO processo. + * + * Gli ID vengono dalla tabella dei nomi dentro libSceSysmodule (dumpata prima): + * le voci sono in ordine di ID, e contando solo i nomi di modulo — scartando le + * stringhe d'errore interlacciate — libSceVenc risulta la voce 117. + * + * Differenza cruciale rispetto al primo tentativo: quello spazzava ID da 0 a 0xFF + * alla cieca, e le API Sony fanno abort() su argomenti fuori range — ha ucciso il + * processo ospite. Qui si prova UN ID plausibile, e solo se fallisce il vicino. + * + * Il caricamento lo facciamo con la funzione risolta per NID, non linkata: e' + * l'unica via visto che gli stub non sono linkati e sceKernelLoadStartModule + * si blocca. */ +/* Calibra la corrispondenza indice-in-tabella -> module ID. + * + * La tabella dei nomi in libSceSysmodule e' in ordine di ID, ma non sappiamo a + * quale ID corrisponda la prima voce. Per scoprirlo usiamo due riferimenti che + * conosciamo per certo: libSceLibcInternal (indice 33) e libSceNet (indice 89) + * sono ENTRAMBE caricate nel nostro processo — le abbiamo viste nella lista + * delle nostre dynlib. + * + * Chiedendo al sistema quali ID risultano caricati, l'ID che combacia con 33 e + * quello che combacia con 89 ci danno la base. Con quella, l'ID dell'encoder + * smette di essere una stima. + * + * Sondiamo 1..149: la tabella ha 411 voci, quindi qualunque sia la base questo + * intervallo e' dentro il range valido — e gli ID fuori range sono quelli che + * fanno abortire, non quelli validi. */ +__attribute__((unused)) +static void +calibrate_ids(void) +{ + intptr_t fn = resolve_self("libSceSysmodule.sprx", "sceSysmoduleIsLoaded"); + int (*is_loaded)(unsigned int); + int n = 0; + + if(!fn) { + LOG("sceSysmoduleIsLoaded non risolta: niente calibrazione"); + return; + } + is_loaded = (int (*)(unsigned int))fn; + + LOG("=== calibrazione ID (query, nessun caricamento) ==="); + + for(unsigned int id = 1; id < 150; id++) { + int r = is_loaded(id); + if(r == 0) { + LOG(" id %u -> caricato", id); + n++; + } + } + LOG("(%d caricati su 1..149)", n); +} + + +/* Sonda LoadModule su un intervallo di ID e si ferma appena libSceVenc compare + * fra le NOSTRE librerie. + * + * E' auto-verificante e sicuro: + * - verificato due volte che un ID non valido restituisce 0x805a1000 e NON aborta + * (l'abort di prima era di sceSysmoduleIsLoaded, non di LoadModule) + * - l'esito si legge dal risultato, non si deduce: se libSceVenc entra nel + * nostro spazio, la funzione di ricerca la vede + * + * Tetto basso di proposito: caricare centinaia di moduli in un'app VSH che ha + * ~20 MB liberi la farebbe esaurire. Se dopo MAX_TRIES non l'ha trovata, ci + * fermiamo e lo diciamo. */ +#define SWEEP_MAX (140) + +__attribute__((unused)) +static void +load_encoder(void) +{ + intptr_t fn = resolve_self("libSceSysmodule.sprx", "sceSysmoduleLoadModule"); + int (*load)(unsigned int); + int tried = 0, ok = 0; + + if(!fn) { + LOG("sceSysmoduleLoadModule non risolta"); + return; + } + load = (int (*)(unsigned int))fn; + + LOG("=== sonda LoadModule, cerco libSceVenc ==="); + + for(unsigned int id = 1; id <= SWEEP_MAX && !tried; id++) { + int r = load(id); + + if(r != 0) { + continue; /* non valido o non caricabile: normale, si prosegue */ + } + + ok++; + LOG(" id %u: caricato", id); + + if(find_in_process(getpid(), "libSceVenc.sprx", NULL)) { + LOG(" *** libSceVenc CARICATA con id %u ***", id); + tried = id; + } + } + + if(tried) { + intptr_t base = find_in_process(getpid(), "libSceVenc.sprx", NULL); + LOG("libSceVenc nel mio processo a 0x%lx", (unsigned long)base); + dump_library(getpid(), base, 704512, "libSceVenc.sprx", OUTDIR); + return; + } + + LOG("libSceVenc non comparsa (id provati 1..%d, caricati %d)", SWEEP_MAX, ok); +} + + +__attribute__((unused)) +static void +load_encoder_old(void) +{ + intptr_t fn = resolve_self("libSceSysmodule.sprx", "sceSysmoduleLoadModule"); + int (*load)(unsigned int); + unsigned int ids[2] = { 117, 118 }; + int done = 0; + + if(!fn) { + LOG("sceSysmoduleLoadModule non risolta: non posso caricare"); + return; + } + + load = (int (*)(unsigned int))fn; + + for(int i = 0; i < 2 && !done; i++) { + int r = load(ids[i]); + LOG(" load(%u) -> %d (0x%08x)", ids[i], r, (unsigned)r); + if(r == 0) { + done = 1; + LOG(" ^^ CARICATO con id %u", ids[i]); + } + } + + if(!done) { + LOG("nessuno dei due ID ha caricato l'encoder"); + return; + } + + /* ora dovrebbe essere nel nostro processo: dumpiamola */ + { + intptr_t base = find_in_process(getpid(), "libSceVenc.sprx", NULL); + + if(!base) { + LOG("caricato ma non lo vedo nel mio spazio"); + return; + } + LOG("libSceVenc ora a 0x%lx nel mio processo", (unsigned long)base); + dump_library(getpid(), base, 704512, "libSceVenc.sprx", OUTDIR); + } +} + + +/* Legge /dev/klog e ne stampa la coda. + * + * Il loader dei sysmodule scrive nel kernel log il MOTIVO del fallimento, + * compreso il nome del PRX e il riferimento all'errore nel Kernel Reference. + * Invece di indovinare dal codice 0x805a1000, ci facciamo dire cosa e' andato + * storto davvero. */ +__attribute__((unused)) +static void +read_klog(void) +{ + char buf[2048]; + ssize_t n; + int fd = open("/dev/klog", O_RDONLY | O_NONBLOCK); + int got = 0; + + if(fd < 0) { + LOG("/dev/klog: %s", strerror(errno)); + return; + } + + LOG("=== /dev/klog ==="); + while(got < 40 && (n = read(fd, buf, sizeof(buf) - 1)) > 0) { + buf[n] = '\0'; + printf("%s", buf); + got++; + } + if(got == 0) { + LOG(" (nessun dato leggibile)"); + } + close(fd); + LOG("=== fine /dev/klog ==="); +} + + +/* ---- enumerazione degli export di una libreria caricata ---- + * + * Il problema di fondo era: per chiamare una funzione servono NID e indirizzo, + * e per averli serviva la libreria... che non riuscivamo a leggere. + * + * Ma dynlib_obj_t ha un campo dynsec che punta alla dynamic section della + * libreria CARICATA. Da li' si arriva a DT_SYMTAB / DT_STRTAB, e quindi + * all'elenco completo degli export: per ognuno il NID (il nome del simbolo) e + * l'indirizzo. + * + * Questo non richiede di leggere il codice: legge solo le strutture di + * collegamento, che sono piccole e sempre residenti. E vale per qualunque + * libreria mappata, in qualunque processo. */ +#define OFF_DYNLIB_DYNSEC (328) + +typedef struct { uint64_t tag, val; } dyn_ent_t; + +#define DT_NULL 0 +#define DT_HASH 4 +#define DT_STRTAB 5 +#define DT_SYMTAB 6 +#define DT_STRSZ 10 +#define DT_SYMENT 11 + +typedef struct { + uint32_t st_name; + uint8_t st_info; + uint8_t st_other; + uint16_t st_shndx; + uint64_t st_value; + uint64_t st_size; +} elf64_sym_t; + +/* Prova a leggere un processo che finora ci e' risultato opaco (pid 75, + * webrtc_daemon). Ritorna il numero di pagine leggibili su un campione. */ +static int +probe_opaque(pid_t target, intptr_t base, unsigned long size) +{ + unsigned char buf[64]; + int ok = 0; + + for(unsigned long off = 0; off + sizeof(buf) <= size && off < 0x8000; + off += 4096) { + if(!kernel_proc_copyout(target, base + off, buf, sizeof(buf))) { + ok++; + } + } + return ok; +} + + +/* Alza le capability del NOSTRO processo e vede se i processi opachi diventano + * leggibili. + * + * Ipotesi da verificare: webrtc_daemon (pid 75) e' trasparente alle nostre + * letture non perche' le sue pagine siano assenti, ma perche' non abbiamo la + * capability di attraversarlo. Con kstuff la cosa e' reversibile e il confronto + * prima/dopo dice se l'ipotesi regge. */ +static void +try_elevate(void) +{ + unsigned char caps[16]; + unsigned char all[16]; + pid_t me = getpid(); + int before = 0, after = 0; + char hex[16 * 3 + 1]; + int i; + + /* pid 75 e la sua mappatura, dalla scansione precedente */ + const pid_t target = 75; + const intptr_t tbase = 0x82c880000; /* libwebrtc.sprx */ + + LOG("=== lettura di un campione da pid %d PRIMA ===", (int)target); + before = probe_opaque(target, tbase, 0x8000); + LOG(" pagine leggibili: %d", before); + + if(!kernel_get_ucred_caps(me, caps)) { + for(i = 0; i < 16; i++) { + snprintf(hex + i * 3, 4, "%02x ", caps[i]); + } + LOG(" nostre caps: %s", hex); + } else { + LOG(" kernel_get_ucred_caps fallita"); + } + + LOG(" nostro authid: 0x%lx", (unsigned long)kernel_get_ucred_authid(me)); + + memset(all, 0xff, sizeof(all)); + LOG(" set caps a ff*16 -> %d", kernel_set_ucred_caps(me, all)); + + if(!kernel_get_ucred_caps(me, caps)) { + for(i = 0; i < 16; i++) { + snprintf(hex + i * 3, 4, "%02x ", caps[i]); + } + LOG(" caps dopo: %s", hex); + } + + LOG("=== stesso campione DOPO ==="); + after = probe_opaque(target, tbase, 0x8000); + LOG(" pagine leggibili: %d", after); + + LOG(" esito: %s", after > before ? "ELEVAZIONE HA FUNZIONATO" + : "nessun cambiamento"); +} + + +/* Legge da un indirizzo che puo' essere indifferentemente kernel o utente. + * + * Nel nodo dynlib i puntatori sono un misto: mapbase e' un VA utente del + * processo, dynsec e' un indirizzo del direct map kernel, e symtab/strtab + * dipendono da come il loader li ha rilocati. Invece di indovinare ogni volta + * quale lettore usare, li proviamo entrambi. */ +static int +read_any(pid_t pid, uint64_t addr, void *buf, size_t len) +{ + if(!addr) { + return -1; + } + if(!kernel_copyout(addr, buf, len)) { + return 0; + } + if(!kernel_proc_copyout(pid, addr, buf, len)) { + return 0; + } + return -1; +} + + +/* g_node / g_pid sono dichiarati in cima al file: walk_dynlibs li aggiorna. */ +static void +enum_exports(pid_t pid, unsigned long node, const char *libname) +{ + uint64_t dynsec = 0; + uint64_t mapbase = 0, mapsize = 0; + + /* Prima di tutto: mappa empirica del nodo. Il layout di dynlib_obj_t che + * avevo ricavato a mano deriva a meta', quindi invece di contare i campi + * cerco, dentro il nodo, i valori che puntano DENTRO la mappatura della + * libreria. dynsec e' per forza uno di quelli. */ + /* Il nodo dynlib sta nell'heap KERNEL: si legge con kernel_copyout, non con + * kernel_proc_copyout (che traduce indirizzi UTENTE del processo target). + * E' l'errore che faceva tornare mapbase=0 e dynsec illeggibile. */ + if(kernel_copyout(node + 48, &mapbase, sizeof(mapbase)) || + kernel_copyout(node + 56, &mapsize, sizeof(mapsize))) { + LOG("%s: mapbase illeggibile", libname); + return; + } + + LOG("%s: mapbase=0x%lx mapsize=%lu", libname, + (unsigned long)mapbase, (unsigned long)mapsize); + + { + unsigned char raw[640]; + int n = 0; + + if(!kernel_copyout(node, raw, sizeof(raw))) { + for(int off = 0; off + 8 <= (int)sizeof(raw); off += 8) { + uint64_t v; + + memcpy(&v, raw + off, 8); + if(mapbase && v >= mapbase && v < mapbase + mapsize) { + LOG(" campo @%3d -> 0x%lx", off, (unsigned long)v); + n++; + + /* Prova: questo indirizzo e' una dynamic section? + * Non mi fido dell'offset calcolato a mano — un campo che credevo + * dynsec non lo era. Lascio che sia il contenuto a dirlo. */ + { + dyn_ent_t t; + uint64_t st = 0, sr = 0; + + for(int k = 0; k < 48; k++) { + if(read_any(pid, v + (unsigned long)k * 16, &t, sizeof(t))) { + break; + } + if(t.tag == DT_NULL) { + break; + } + if(t.tag == DT_SYMTAB) st = t.val; + if(t.tag == DT_STRTAB) sr = t.val; + } + if(st && sr) { + LOG(" ^^ E' LA DYNAMIC SECTION: symtab=0x%lx strtab=0x%lx", + (unsigned long)st, (unsigned long)sr); + } + } + } + } + } + LOG(" (%d puntatori interni trovati)", n); + } + + uint64_t symtab = 0, strtab = 0, hash = 0, strsz = 0; + dyn_ent_t e; + int shown = 0; + + if(kernel_copyout(node + OFF_DYNLIB_DYNSEC, &dynsec, sizeof(dynsec)) + || !dynsec) { + LOG("%s: dynsec non leggibile", libname); + return; + } + + LOG("%s: dynsec=0x%lx cerco symtab/strtab", libname, (unsigned long)dynsec); + + for(int i = 0; i < 64; i++) { + if(read_any(pid, dynsec + (unsigned long)i * sizeof(e), &e, sizeof(e))) { + break; + } + if(e.tag == DT_NULL) { + break; + } + if(e.tag == DT_SYMTAB) symtab = e.val; + if(e.tag == DT_STRTAB) strtab = e.val; + if(e.tag == DT_HASH) hash = e.val; + if(e.tag == DT_STRSZ) strsz = e.val; + } + + LOG(" symtab=0x%lx strtab=0x%lx hash=0x%lx strsz=%lu", + (unsigned long)symtab, (unsigned long)strtab, + (unsigned long)hash, (unsigned long)strsz); + + if(!symtab || !strtab) { + LOG(" niente symbol table"); + return; + } + + /* nchain dalla hash table: seconda parola */ + unsigned long nsym = 0; + if(hash) { + uint32_t w[2] = {0, 0}; + if(!read_any(pid, hash, w, sizeof(w))) { + nsym = w[1]; + } + } + if(!nsym || nsym > 20000) { + nsym = 512; /* stima prudente */ + } + + LOG(" %lu simboli, ecco gli export:", nsym); + + for(unsigned long i = 0; i < nsym && shown < 60; i++) { + elf64_sym_t s; + char name[64]; + + if(read_any(pid, symtab + i * sizeof(s), &s, sizeof(s))) { + break; + } + if(!s.st_name || !s.st_value || s.st_shndx == 0) { + continue; /* vuoto o non definito: non e' un export */ + } + if(s.st_name >= (strsz ? strsz : 0x10000)) { + continue; + } + if(read_any(pid, strtab + s.st_name, name, sizeof(name) - 1)) { + continue; + } + name[sizeof(name) - 1] = '\0'; + + if(!name[0]) { + continue; + } + + LOG(" %-14s 0x%lx", name, (unsigned long)s.st_value); + shown++; + } + + LOG(" (%d export mostrati)", shown); +} + + +typedef struct { const char *want; int done; } expctx_t; + +static void +cb_exp(pid_t pid, dynlib_head_t *d, const char *path, void *ud) +{ + expctx_t *c = ud; + const char *name = basename_of(path); + + (void)d; + + if(c->done || !name[0]) { + return; + } + if(strcmp(name, c->want)) { + return; + } + + c->done = 1; + enum_exports(pid, g_node, name); +} + + +__attribute__((unused)) +static void +show_exports(const char *libname) +{ + expctx_t c = { libname, 0 }; + + /* cerca in tutti i processi: la libreria puo' stare in un altro */ + { + size_t sz; + void *buf = list_procs(&sz); + + if(!buf) { + return; + } + for(void *ptr = buf; ptr < (buf + sz);) { + struct kinfo_proc *ki = (struct kinfo_proc*)ptr; + + if(ki->ki_structsize <= 0) { + break; + } + ptr += ki->ki_structsize; + + if(ki->ki_pid > 0) { + walk_dynlibs(ki->ki_pid, cb_exp, &c); + if(c.done) { + break; + } + } + } + free(buf); + } + + if(!c.done) { + LOG("%s: non trovata", libname); + } +} + + +/* Mappa dell'infrastruttura di streaming/cattura. + * + * Il kernel log ha rivelato processi che non conoscevamo: SceGameLiveStreaming, + * webrtc_daemon.self, SceDiscordDaemon, SceAvCapture, AgcCompositor. Sono + * infrastruttura di streaming nativa, distinta da Remote Play. + * + * Qui cerco quali processi hanno caricate le librerie che contano, per capire + * dove sta la pipeline — e quindi in quale host avrebbe senso far girare un + * payload che la usi. */ +static int +is_media_lib(const char *n) +{ + static const char *pat[] = { + "Stream", "WebRTC", "webrtc", "Streaming", "Capture", "Avcap", + "Venc", "Vdec", "VideoCore", "Share", "Video", NULL + }; + + for(int i = 0; pat[i]; i++) { + if(strstr(n, pat[i])) { + return 1; + } + } + return 0; +} + + +static void +cb_scan(pid_t pid, dynlib_head_t *d, const char *path, void *ud) +{ + int *n = ud; + const char *name = basename_of(path); + + if(!name[0] || !is_media_lib(name)) { + return; + } + LOG(" pid %-6d base=0x%012lx %8lu %s", + (int)pid, (unsigned long)d->mapbase, d->mapsize, name); + (*n)++; +} + + +__attribute__((unused)) +static void +scan_media(void) +{ + size_t sz; + void *buf = list_procs(&sz); + int n = 0; + + if(!buf) { + return; + } + + LOG("=== librerie video/streaming in TUTTI i processi ==="); + + for(void *ptr = buf; ptr < (buf + sz);) { + struct kinfo_proc *ki = (struct kinfo_proc*)ptr; + + if(ki->ki_structsize <= 0) { + break; + } + ptr += ki->ki_structsize; + + if(ki->ki_pid > 0) { + walk_dynlibs(ki->ki_pid, cb_scan, &n); + } + } + free(buf); + LOG("(%d voci)", n); +} + + +/* ---- dump delle librerie del NOSTRO processo (solo memcpy) ---- + * + * Questa e' la strada piu' sicura di tutto il payload: le librerie caricate nel + * processo che ci ospita stanno nel NOSTRO spazio di indirizzamento, quindi si + * leggono con un memcpy. Nessuna kernel read, nessun page walk, nessun device, + * nessuna chiamata di sistema. Non puo' far male a niente. + * + * E ci serve esattamente questo: libSceSysmodule e' fra queste, e contiene la + * corrispondenza fra ID numerico e nome del modulo — cioe' quello che manca + * per chiedere il caricamento dell'encoder con un ID VALIDO, invece di sondare + * a tentoni come ho fatto prima (e che ha fatto abortire il processo ospite). */ +__attribute__((unused)) +static void +dump_local_raw(intptr_t base, unsigned long size, const char *name, + const char *outdir) +{ + const unsigned long PAGE = 0x1000; + elf64_ehdr_t eh; + elf64_phdr_t ph; + unsigned char preamble[PAGE]; + char path[512]; + int fd; + + if(size < PAGE || size > (64u << 20)) { + LOG(" %s: dimensione sospetta (%lu), salto", name, size); + return; + } + + memset(&eh, 0, sizeof(eh)); + memset(&ph, 0, sizeof(ph)); + memset(preamble, 0, sizeof(preamble)); + + eh.e_ident[0] = 0x7f; + eh.e_ident[1] = 'E'; + eh.e_ident[2] = 'L'; + eh.e_ident[3] = 'F'; + eh.e_ident[4] = 2; + eh.e_ident[5] = 1; + eh.e_ident[6] = 1; + eh.e_type = 3; + eh.e_machine = 62; + eh.e_version = 1; + eh.e_entry = (uint64_t)base + PAGE; + eh.e_phoff = sizeof(elf64_ehdr_t); + eh.e_ehsize = sizeof(elf64_ehdr_t); + eh.e_phentsize = sizeof(elf64_phdr_t); + eh.e_phnum = 1; + + ph.p_type = PT_LOAD; + ph.p_flags = 7; + ph.p_offset = PAGE; + ph.p_vaddr = (uint64_t)base; + ph.p_paddr = (uint64_t)base; + ph.p_filesz = (uint64_t)size; + ph.p_memsz = (uint64_t)size; + ph.p_align = PAGE; + + memcpy(preamble, &eh, sizeof(eh)); + memcpy(preamble + sizeof(eh), &ph, sizeof(ph)); + + snprintf(path, sizeof(path), "%s/self_%s", outdir, name); + + if((fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) { + LOG(" %s: open fallita: %s", name, strerror(errno)); + return; + } + + if(pwrite(fd, preamble, PAGE, 0) != (ssize_t)PAGE) { + close(fd); + return; + } + + /* Scrittura a blocchi da 64 KB. Una pwrite unica da qualche MB fallisce: + * i dump che funzionano scrivono cosi', ed e' la differenza che conta. */ + { + const size_t CH = 64 * 1024; + unsigned long off = 0; + + while(off < size) { + size_t n = size - off; + + if(n > CH) { + n = CH; + } + if(pwrite(fd, (void*)(base + off), n, (off_t)(PAGE + off)) + != (ssize_t)n) { + LOG(" %s: scrittura fallita a offset 0x%lx (%s)", + name, off, strerror(errno)); + close(fd); + return; + } + off += n; + } + } + + close(fd); + chmod(path, 0644); + LOG(" %s -> %s (%lu byte, vaddr 0x%lx)", name, path, size, + (unsigned long)base); +} + + +static void +cb_selfdump(pid_t pid, dynlib_head_t *d, const char *path, void *ud) +{ + int *n = ud; + const char *name = basename_of(path); + + (void)pid; + + if(!name[0] || !d->mapbase || d->mapsize < 4096) { + return; + } + /* l'eseguibile dell'host: grande e non ci serve */ + if(strstr(name, "eboot.bin")) { + return; + } + + /* NON usiamo il memcpy: il mapbase della libreria non e' mappato nello spazio + * del payload (il walk lo trova perche' legge via kernel). Passiamo dalla + * stessa lettura kernel che usiamo per gli altri processi — funziona anche + * sul proprio pid, ed e' gia' collaudata. */ + if(dump_library(pid, d->mapbase, d->mapsize, name, OUTDIR) > 0) { + (*n)++; + } +} + + +__attribute__((unused)) +static void +dump_self_all(void) +{ + int n = 0; + + LOG("=== dump delle librerie del MIO processo (memcpy) ==="); + walk_dynlibs(getpid(), cb_selfdump, &n); + LOG("(%d librerie)", n); +} + + +/* ---- modalita' dump ---- */ + +typedef struct { + const char *want[32]; /* NULL-terminata; vuota = niente */ + int nwant; + char done[MAX_LIBS][MAX_PATH / 2]; /* nomi gia' estratti */ + int ndone; + int dumped; +} dump_filter_t; + +static const char * +basename_of(const char *path) +{ + const char *slash = strrchr(path, '/'); + return slash ? slash + 1 : path; +} + + +static void +cb_dump(pid_t pid, dynlib_head_t *d, const char *path, void *ud) +{ + dump_filter_t *f = ud; + const char *name = basename_of(path); + intptr_t base = 0; + + if(!name[0] || d->mapsize < 64) { + return; + } + + /* filtra per nome, se e' stata passata una lista */ + if(f->nwant) { + int hit = 0; + for(int i = 0; i < f->nwant; i++) { + if(!strcmp(f->want[i], name)) { + hit = 1; + break; + } + } + if(!hit) { + return; + } + } + + /* la stessa libreria compare in piu' processi: la estraiamo una volta sola */ + for(int i = 0; i < f->ndone; i++) { + if(!strcmp(f->done[i], name)) { + return; + } + } + + /* L'header ELF non e' detto che stia esattamente a mapbase: proviamo i candidati + * diretti e poi una scansione della mappatura. */ + if(looks_like_elf(pid, d->mapbase)) { + base = d->mapbase; + } else if(d->vaddrbase && d->vaddrbase != d->mapbase && + looks_like_elf(pid, d->vaddrbase)) { + base = d->vaddrbase; + } else { + long off = scan_for_elf(pid, d->mapbase, d->mapsize); + + if(off > 0) { + LOG("%s (pid %d): ELF trovato a mapbase+0x%lx", name, (int)pid, off); + base = d->mapbase + off; + } + } + + if(!base) { + /* Niente da fare: mostriamo cosa c'e' davvero, invece di tirare a indovinare. */ + LOG("%s (pid %d) mapbase=0x%lx vaddrbase=0x%lx size=%lu -- nessun ELF", + name, (int)pid, (unsigned long)d->mapbase, + (unsigned long)d->vaddrbase, (unsigned long)d->mapsize); + probe(pid, "mapbase", d->mapbase); + probe(pid, "vaddrbase", d->vaddrbase); + probe(pid, "entry", d->entry); + return; + } + + if(!dump_elf(pid, base, name, OUTDIR)) { + if(f->ndone < MAX_LIBS) { + snprintf(f->done[f->ndone], sizeof(f->done[0]), "%s", name); + f->ndone++; + } + f->dumped++; + } +} + + +/* Cammina le dynlib di tutti i processi ed estrae cio' che passa il filtro. + * + * Non usiamo kernel_dynlib_handle() qui, e il motivo e' importante: quella + * funzione ritorna -1 al PRIMO kernel_copyout fallito, e la testa della lista + * dynlib contiene una voce spazzatura illeggibile. Risultato: abortisce prima + * di raggiungere le librerie vere e riporta "non trovata" per tutto. + * Camminando a mano la voce guasta si salta e si continua. */ +static int +dump_matching(const char **want) +{ + size_t sz; + void *buf = list_procs(&sz); + static dump_filter_t f; + + memset(&f, 0, sizeof(f)); + if(want) { + for(int i = 0; want[i] && f.nwant < 31; i++) { + f.want[f.nwant++] = want[i]; + } + } + + if(!buf) { + return -1; + } + + for(void *ptr = buf; ptr < (buf + sz);) { + struct kinfo_proc *ki = (struct kinfo_proc*)ptr; + pid_t pid = ki->ki_pid; + + if(ki->ki_structsize <= 0) { + break; + } + ptr += ki->ki_structsize; + + if(pid <= 0) { + continue; + } + + walk_dynlibs(pid, cb_dump, &f); + } + + free(buf); + LOG("%d librerie estratte in %s", f.dumped, OUTDIR); + return f.dumped ? 0 : -1; +} + + +int +main(int argc, char *argv[]) +{ + /* Senza argomenti NON e' "list": deve cadere nel dump, che e' il default. */ + const char *mode = (argc > 1) ? argv[1] : ""; + + /* stdout non bufferizzato: se il payload si blocca a meta', vogliamo comunque + * vedere quello che ha gia' stampato. Con il buffer pieno l'output resta + * invisibile fino all'esecuzione completa, ed e' quello che ci ha ingannato. */ + setvbuf(stdout, NULL, _IONBF, 0); + + LOG("firmware 0x%08x", kernel_get_fw_version()); + mkdir(OUTDIR, 0755); + + /* Diagnostica sempre attiva: se il direct map non e' inizializzato, ogni + * lettura dal physical address finisce nel posto sbagliato e torna riempimento. + * Proviamo la traduzione su un indirizzo noto (l'entry point degli executable). */ + LOG("KERNEL_ADDRESS_DMAP_BASE = 0x%012lx", + (unsigned long)KERNEL_ADDRESS_DMAP_BASE); + + /* Il payload viene INIETTATO da elfldr dentro un processo gia' esistente. + * Se quel processo ha gia' caricate le librerie video, le loro funzioni sono + * mappate nel NOSTRO spazio e possiamo chiamarle direttamente: il muro del + * loader (sceKernelLoadStartModule che si blocca) sparirebbe, perche' non + * c'e' niente da caricare. + * + * Non l'abbiamo mai verificato. E' la cosa piu' importante da sapere. */ + LOG("=== librerie del MIO processo (pid %d) ===", (int)getpid()); + walk_dynlibs(getpid(), cb_print, NULL); + + try_elevate(); + { + size_t sz; + void *buf = list_procs(&sz); + + if(buf) { + for(void *ptr = buf; ptr < (buf + sz);) { + struct kinfo_proc *ki = (struct kinfo_proc*)ptr; + pid_t pid = ki->ki_pid; + + if(ki->ki_structsize <= 0) { + break; + } + ptr += ki->ki_structsize; + + if(pid > 0) { + unsigned long paddr = 0, plen = 0; + + if(!kernel_proc_getpaddr(pid, 0x400000, &paddr, &plen)) { + unsigned char b[8] = {0}; + int r = kernel_copyout(KERNEL_ADDRESS_DMAP_BASE + paddr, + b, sizeof(b)); + + LOG("probe pid %d: 0x400000 -> paddr=0x%012lx plen=0x%lx dmap=0x%012lx" + " read=%s %02x %02x %02x %02x", + (int)pid, paddr, plen, + (unsigned long)(KERNEL_ADDRESS_DMAP_BASE + paddr), + r ? "FAIL" : "ok", b[0], b[1], b[2], b[3]); + break; + } + } + } + free(buf); + } + } + + /* --- modalita' lista: fa parlare la console --- */ + if(!strcmp(mode, "list")) { + size_t sz; + void *buf = list_procs(&sz); + int procs = 0, libs = 0; + + if(!buf) { + return 1; + } + + for(void *ptr = buf; ptr < (buf + sz);) { + struct kinfo_proc *ki = (struct kinfo_proc*)ptr; + pid_t pid = ki->ki_pid; + int n; + + if(ki->ki_structsize <= 0) { + break; + } + ptr += ki->ki_structsize; + + if(pid <= 0) { + continue; + } + + n = walk_dynlibs(pid, cb_print, NULL); + if(n > 0) { + LOG("pid %d: %d librerie", (int)pid, n); + procs++; + libs += n; + } + } + + free(buf); + LOG("totale: %d processi con librerie, %d voci", procs, libs); + if(!libs) { + LOG("nessuna libreria letta: probabile offset KPROC_DYNLIB_HEAD (0x%x) " + "sbagliato per questo firmware", KPROC_DYNLIB_HEAD); + } + return 0; + } + + /* --- modalita' dump --- + * prospero-deploy manda solo l'ELF su TCP, senza argomenti: quindi il dump + * dev'essere il comportamento di default. "list" resta raggiungibile per chi + * carica il payload con un loader che passa argv. */ + if(!strcmp(mode, "targets")) { + return dump_matching(targets) ? 1 : 0; /* cross-process, come fallback */ + } + + if(argc > 1) { + return dump_matching((const char**)&argv[1]) ? 1 : 0; + } + + /* Default: dumpa le librerie. + * + * NOTA: la variante "carica la libreria nel proprio processo" (load_and_dump) + * e' disattivata perche' sceKernelLoadStartModule si BLOCCA quando un payload + * homebrew prova a caricare una libreria di sistema, e il deploy resta appeso. + * + * E NON usiamo dump_matching(): quella cercava l'header ELF a inizio mappatura, + * che il loader riempie di 0xCC. dump_library_by_name() dumpa la mappatura + * intera e ci costruisce attorno un ELF valido. */ + { + int ok = 0; + + for(int i = 0; targets[i]; i++) { + if(!dump_library_by_name(targets[i], OUTDIR)) { + ok++; + } + } + LOG("%d librerie estratte in %s", ok, OUTDIR); + return ok ? 0 : 1; + } +} diff --git a/tools/flat.py b/tools/flat.py new file mode 100644 index 0000000..80d3bbd --- /dev/null +++ b/tools/flat.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Estrae un segmento PT_LOAD di un .sprx come binario piatto, per objdump.""" +import struct, sys +src, dst = sys.argv[1], sys.argv[2] +want = sys.argv[3] if len(sys.argv) > 3 else 'exec' # exec | all +d=open(src,'rb').read() +phoff=struct.unpack_from(' [W H STRIDEY STRIDEUV] +# +import struct +import sys +import zlib + + +def png_gray(path, w, h, rows): + raw = b''.join(b'\x00' + r for r in rows) + def chunk(tag, data): + c = tag + data + return struct.pack('>I', len(data)) + c + struct.pack('>I', zlib.crc32(c) & 0xffffffff) + png = b'\x89PNG\r\n\x1a\n' + png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 0, 0, 0, 0)) + png += chunk(b'IDAT', zlib.compress(raw, 6)) + png += chunk(b'IEND', b'') + open(path, 'wb').write(png) + + +def png_rgb(path, w, h, rgb): + raw = b''.join(b'\x00' + rgb[y] for y in range(h)) + def chunk(tag, data): + c = tag + data + return struct.pack('>I', len(data)) + c + struct.pack('>I', zlib.crc32(c) & 0xffffffff) + png = b'\x89PNG\r\n\x1a\n' + png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)) + png += chunk(b'IDAT', zlib.compress(raw, 6)) + png += chunk(b'IEND', b'') + open(path, 'wb').write(png) + + +def clamp(v): + return 0 if v < 0 else (255 if v > 255 else v) + + +def main(): + path = sys.argv[1] + out = sys.argv[2] + data = open(path, 'rb').read() + if len(sys.argv) > 6: + w, h, sy, suv = (int(x) for x in sys.argv[3:7]) + else: + w, h, sy, suv = 1920, 1080, 1920, 1920 + y_plane = data[:sy * h] + uv_off = sy * h + uv = data[uv_off:uv_off + suv * (h // 2)] + + rr = [] + for yy in range(h): + row = bytearray() + ybase = yy * sy + uvrow = (yy // 2) * suv + for xx in range(w): + Y = y_plane[ybase + xx] + if uv_off and len(uv) >= uvrow + (xx // 2) * 2 + 2: + U = uv[uvrow + (xx // 2) * 2] - 128 + V = uv[uvrow + (xx // 2) * 2 + 1] - 128 + else: + U = V = 0 + c = Y - 16 + if c < 0: + c = 0 + R = clamp((298 * c + 409 * V + 128) >> 8) + G = clamp((298 * c - 100 * U - 208 * V + 128) >> 8) + B = clamp((298 * c + 516 * U + 128) >> 8) + row += bytes((R, G, B)) + rr.append(bytes(row)) + png_rgb(out, w, h, rr) + print('scritto %s (%dx%d, strideY %d, strideUV %d)' % (out, w, h, sy, suv)) + + +if __name__ == '__main__': + main() diff --git a/tools/p010png.py b/tools/p010png.py new file mode 100644 index 0000000..58cc58a --- /dev/null +++ b/tools/p010png.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: latin-1 -*- +# Rende un frame P010 (10 bit in contenitori da 16) in PNG. +# +# python -I p010png.py [W H ] +# +import struct +import sys +import zlib + + +def chunk(tag, data): + c = tag + data + return struct.pack('>I', len(data)) + c + struct.pack('>I', zlib.crc32(c) & 0xffffffff) + + +def png_rgb(path, w, h, rows): + raw = b''.join(b'\x00' + rows[y] for y in range(h)) + png = b'\x89PNG\r\n\x1a\n' + png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)) + png += chunk(b'IDAT', zlib.compress(raw, 6)) + png += chunk(b'IEND', b'') + open(path, 'wb').write(png) + + +def clamp(v): + return 0 if v < 0 else (255 if v > 255 else v) + + +def main(): + path = sys.argv[1] + off = int(sys.argv[2], 0) + out = sys.argv[3] + w = int(sys.argv[4]) if len(sys.argv) > 4 else 1920 + h = int(sys.argv[5]) if len(sys.argv) > 5 else 1080 + d = open(path, 'rb').read() + y_stride = w * 2 + uv_off = off + y_stride * h + rgb = [] + for y in range(h): + yb = off + y * y_stride + uvrow = uv_off + (y // 2) * (w // 2) * 4 + row = bytearray() + for x in range(w): + Y = ((d[yb + x * 2] | (d[yb + x * 2 + 1] << 8)) >> 6) >> 2 + p = uvrow + (x // 2) * 4 + U = ((d[p] | (d[p + 1] << 8)) >> 6) >> 2 + V = ((d[p + 2] | (d[p + 3] << 8)) >> 6) >> 2 + c = Y - 16 + U -= 128 + V -= 128 + R = clamp((298 * c + 409 * V + 128) >> 8) + G = clamp((298 * c - 100 * U - 208 * V + 128) >> 8) + B = clamp((298 * c + 516 * U + 128) >> 8) + row += bytes((R, G, B)) + rgb.append(bytes(row)) + png_rgb(out, w, h, rgb) + print('scritto ' + out) + + +if __name__ == '__main__': + main() diff --git a/tools/scan_refs.py b/tools/scan_refs.py new file mode 100644 index 0000000..33b4c85 --- /dev/null +++ b/tools/scan_refs.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: latin-1 -*- +# Scanner grezzo di riferimenti in un ELF64 LE x86-64: trova chi materializza un +# indirizzo (stringa/rodata/vtable) via: +# - lea reg,[rip+disp32] (48/4C 8d, o 8d senza REX) +# - mov reg,[rip+disp32] (48 8b / 48 8d) +# - valore assoluto u64 nei dati (tabelle/vtable) +# +# python -I scan_refs.py [ ...] +# +import struct +import sys + + +def load(path): + data = open(path, 'rb').read() + e_phoff = struct.unpack_from('= n: + break + op = buf[j] + if op not in (0x8d, 0x8b, 0x89): + continue + if j + 6 > n: + continue + modrm = buf[j + 1] + if (modrm & 0xC7) != 0x05: # mod=00, rm=101 -> RIP+disp32 + continue + # con 0x41/0x44 ecc. (REX.R/X/B) e' comunque RIP-rel se mod=00 rm=101 + disp = struct.unpack_from('> 3) & 7 + if rex & 4: + reg += 8 + kind = {0x8d: 'lea', 0x8b: 'mov-r', 0x89: 'mov-w'}.get(op, hex(op)) + print('CODE %s @ %x (r%d) -> %x' % (kind, base + i, reg, tgt)) + hits.add(base + i) + print('abs hits: %d code hits: %d' % (len(abs_hits), len(hits))) + return hits + + +if __name__ == '__main__': + elf = sys.argv[1] + tgts = [int(x, 16) for x in sys.argv[2:]] + print('targets: %s' % [hex(t) for t in tgts]) + scan(elf, tgts) diff --git a/tools/sprx.py b/tools/sprx.py new file mode 100644 index 0000000..d1868ba --- /dev/null +++ b/tools/sprx.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Parser per .sprx PS4/PS5 (ET_SCE_DYNAMIC): export/import con nomi risolti via aerolib.csv.""" +import struct, sys, os + +def load_nid_db(p=None): + p = p or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'aerolib.csv') + m={} + with open(p, encoding='utf-8', errors='replace') as f: + for line in f: + line=line.strip() + if not line: continue + parts=line.split(' ',1) + if len(parts)==2: m[parts[0]]=parts[1] + return m + +def parse(path): + d=open(path,'rb').read() + phoff=struct.unpack_from('>48; off=v&0xffffffff + if off and off<0x1000000: + try: s=nm(off) + except Exception: continue + if s and idx not in libs: libs[idx]=s + nchain=struct.unpack_from(' nome import, per un .sprx.""" +import sys, os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import callsites, sprx +db = sprx.load_nid_db() +r = callsites.parse_full(sys.argv[1]) +stub2nid, calls, got2nid = callsites.scan(r) +for a, nid in sorted(stub2nid.items()): + print('0x%-6x %-14s %s' % (a, nid, db.get(nid, '?'))) diff --git a/tools/whoimports.py b/tools/whoimports.py new file mode 100644 index 0000000..a5ba4d8 --- /dev/null +++ b/tools/whoimports.py @@ -0,0 +1,23 @@ +import sys, glob, os +sys.path.insert(0, os.path.expanduser('~/ps5-research/tools')) +import sprx +TARGETS = { + 'svzPXluOz8U':'sceAvcap2Initialize','gojqghDU+1Y':'sceAvcap2Terminate', + '4jwUgirLFrA':'sceAvcap2OpenVideo','8CGNCwBsItI':'sceAvcap2Start', + 'pN0hIQZsMzc':'sceAvcap2ReadVideo','jkv9MlN7gvs':'sceAvcap2ReadVideo2', + 'tR7gPe1i8hw':'sceAvcap2Close','YaXDckFygvw':'sceAvcap2RequestKeyFrame', + 'Dht3tcbRoEY':'sceAvcap2QueryVideoEncoderMemorySize','X8c-f8LgzHo':'sceAvcap2MapMemory', + '2GC2UnBJHIA':'sceAvcap2GetFramePitch','RW88AiwmL+Y':'sceAvcap2GetSupportInformation', + '-NotZDC2pQc':'sceAvcap2SetVideoEncodeConfig','NeKlrUv+lbY':'sceAvcap2GetNonVclNalUnitStream', + '0zsTiDhM0nU':'IPMI::Client::create','svzPXluOz8U':'sceAvcap2Initialize', +} +for p in sorted(glob.glob(os.path.join(os.path.expanduser('~/ps5-research/disklibs'),'*.sprx'))): + try: r = sprx.parse(p) + except Exception as e: continue + if not r: continue + hits=[] + for s in r['syms']: + if s['shndx']==0 and s['nid'] in TARGETS: + hits.append(TARGETS[s['nid']]) + if hits: + print('%-42s importa: %s' % (os.path.basename(p), ', '.join(sorted(set(hits)))))