mirror of
https://github.com/fstat13/PSCapture.git
synced 2026-10-17 06:00:37 +02:00
docs: full reverse-engineering writeup + RE toolbench
- docs/REVERSE_ENGINEERING.md: library extraction, scan-out ring, pan, chroma plane (V-first words), kernel-side notes, PLD delivery, timeline - tools/: dumplib payload + sprx/xref hunters + frame renderers (own code only)
This commit is contained in:
1 parent
76a6408586
commit
2a58cbcb43
14 files changed
+3076
No files matched your search
@@ -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
|
||||
|
||||
@@ -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**.*
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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('<Q', d, 32)[0]
|
||||
phes = struct.unpack_from('<H', d, 54)[0]
|
||||
phn = struct.unpack_from('<H', d, 56)[0]
|
||||
ph = []
|
||||
for i in range(phn):
|
||||
o = phoff + i * phes
|
||||
t, fl, off, va, pa, fsz, msz, al = struct.unpack_from('<IIQQQQQQ', d, o)
|
||||
ph.append((t, fl, off, va, fsz, msz))
|
||||
|
||||
def v2f(v):
|
||||
for t, fl, off, va, fsz, msz in ph:
|
||||
if t == 1 and va <= v < va + msz:
|
||||
delta = v - va
|
||||
if delta < fsz:
|
||||
return off + delta
|
||||
return None
|
||||
|
||||
dyn = [p for p in ph if p[0] == 2]
|
||||
if not dyn:
|
||||
return None
|
||||
_, _, doff, _, dsz, _ = dyn[0]
|
||||
|
||||
tag = {}
|
||||
ents = []
|
||||
for i in range(dsz // 16):
|
||||
t, v = struct.unpack_from('<QQ', d, doff + i * 16)
|
||||
ents.append((t, v))
|
||||
tag.setdefault(t, v)
|
||||
|
||||
strtab = v2f(tag.get(5, 0))
|
||||
symtab = v2f(tag.get(6, 0))
|
||||
hash_ = v2f(tag.get(4, 0))
|
||||
|
||||
def nm(off):
|
||||
e = d.find(b'\x00', strtab + off)
|
||||
return d[strtab + off:e].decode('latin1')
|
||||
|
||||
nchain = struct.unpack_from('<I', d, hash_ + 4)[0] if hash_ else 0
|
||||
symnames = []
|
||||
for i in range(nchain):
|
||||
o = symtab + i * 24
|
||||
st_name = struct.unpack_from('<I', d, o)[0]
|
||||
symnames.append(nm(st_name) if st_name else '')
|
||||
|
||||
return {'data': d, 'ph': ph, 'tag': tag, 'ents': ents, 'v2f': v2f,
|
||||
'symnames': symnames, 'nchain': nchain}
|
||||
|
||||
|
||||
def plt_map(r):
|
||||
"""slot GOT -> 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('<QQq', d, o)
|
||||
symidx = r_info >> 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('<i', code, i + 2)[0]
|
||||
a = va + i
|
||||
tgt = a + 6 + disp
|
||||
if tgt in got2nid:
|
||||
stub2nid[a] = got2nid[tgt]
|
||||
# call rel32 = e8 xx xx xx xx
|
||||
for i in range(len(code) - 5):
|
||||
if code[i] == 0xe8:
|
||||
rel = struct.unpack_from('<i', code, i + 1)[0]
|
||||
a = va + i
|
||||
call2stub.append((a, a + 5 + rel))
|
||||
|
||||
return stub2nid, call2stub, got2nid
|
||||
|
||||
|
||||
def report(path, want_nids, ctx=14):
|
||||
r = parse_full(path)
|
||||
if not r:
|
||||
print('%s: nessun PT_DYNAMIC' % path)
|
||||
return
|
||||
stub2nid, calls, got2nid = scan(r)
|
||||
d, v2f = r['data'], r['v2f']
|
||||
name = os.path.basename(path)
|
||||
print('=' * 78)
|
||||
print('%s stub PLT risolti: %d call site totali: %d' %
|
||||
(name, len(stub2nid), len(calls)))
|
||||
|
||||
# stub per nome
|
||||
print('--- stub PLT per le funzioni cercate ---')
|
||||
for a, nid in sorted(stub2nid.items()):
|
||||
if nid in want_nids:
|
||||
print(' 0x%-7x -> %-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)
|
||||
@@ -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
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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('<Q',d,32)[0]; phes=struct.unpack_from('<H',d,54)[0]; phn=struct.unpack_from('<H',d,56)[0]
|
||||
segs=[]
|
||||
for i in range(phn):
|
||||
o=phoff+i*phes
|
||||
t,fl,off,va,pa,fsz,msz,al=struct.unpack_from('<IIQQQQQQ',d,o)
|
||||
if t==1: segs.append((fl,off,va,fsz,msz))
|
||||
segs.sort(key=lambda s:s[2])
|
||||
if want=='exec':
|
||||
sel=[s for s in segs if s[0]&1]
|
||||
if not sel: print('nessun segmento eseguibile'); sys.exit(1)
|
||||
fl,off,va,fsz,msz=sel[0]
|
||||
open(dst,'wb').write(d[off:off+fsz])
|
||||
print('ADJUST_VMA=0x%x SIZE=0x%x' % (va,fsz))
|
||||
else:
|
||||
end=max(s[2]+s[4] for s in segs)
|
||||
base=min(s[2] for s in segs)
|
||||
buf=bytearray(end-base)
|
||||
for fl,off,va,fsz,msz in segs:
|
||||
buf[va-base:va-base+fsz]=d[off:off+fsz]
|
||||
open(dst,'wb').write(bytes(buf))
|
||||
print('ADJUST_VMA=0x%x SIZE=0x%x' % (base,len(buf)))
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: latin-1 -*-
|
||||
# Prova le geometrie possibili di un buffer NV12 e salva un PNG per ciascuna
|
||||
# (cosi' si guarda direttamente cos'e').
|
||||
#
|
||||
# python -I nv12png.py <file.bin> <out.png> [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()
|
||||
@@ -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 <file.bin> <off> <out.png> [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()
|
||||
@@ -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 <elf> <hexaddr> [<hexaddr> ...]
|
||||
#
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def load(path):
|
||||
data = open(path, 'rb').read()
|
||||
e_phoff = struct.unpack_from('<Q', data, 0x20)[0]
|
||||
e_phentsize = struct.unpack_from('<H', data, 0x36)[0]
|
||||
e_phnum = struct.unpack_from('<H', data, 0x38)[0]
|
||||
segs = []
|
||||
for i in range(e_phnum):
|
||||
off = e_phoff + i * e_phentsize
|
||||
p_type, p_flags = struct.unpack_from('<II', data, off)
|
||||
if p_type != 1:
|
||||
continue
|
||||
p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align = \
|
||||
struct.unpack_from('<QQQQQQ', data, off + 8)
|
||||
segs.append([p_vaddr, p_offset, p_filesz, p_flags])
|
||||
return data, segs
|
||||
|
||||
|
||||
def scan(elf, targets):
|
||||
data, segs = load(elf)
|
||||
tset = set(targets)
|
||||
|
||||
# pass 1: valori assoluti u64 nei dati (vtable/tabelle con reloc pre-applicate)
|
||||
abs_hits = set()
|
||||
for vaddr, off, sz, fl in segs:
|
||||
if fl & 1: # solo segmenti dati
|
||||
continue
|
||||
end = min(off + sz, len(data))
|
||||
for i in range(off, end - 8, 8):
|
||||
v64 = struct.unpack_from('<Q', data, i)[0]
|
||||
if v64 in tset:
|
||||
print('ABS64 @ %x = %x' % (vaddr + (i - off), v64))
|
||||
abs_hits.add(vaddr + (i - off))
|
||||
|
||||
# pass 2: istruzioni x86-64 con displacement RIP-relativo
|
||||
hits = set()
|
||||
for vaddr, off, sz, fl in segs:
|
||||
if not (fl & 1):
|
||||
continue
|
||||
end = min(off + sz, len(data))
|
||||
buf = data[off:end]
|
||||
base = vaddr
|
||||
n = len(buf)
|
||||
for i in range(n - 8):
|
||||
# candidati: [REX] [8d|8b] modrm(rip)
|
||||
j = i
|
||||
rex = 0
|
||||
if buf[j] in (0x48, 0x4c, 0x49, 0x4d, 0x44, 0x41, 0x40, 0x42, 0x43, 0x45, 0x46, 0x47):
|
||||
rex = buf[j]
|
||||
j += 1
|
||||
if j >= 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('<i', buf, j + 2)[0]
|
||||
ilen = (j - i) + 1 + 1 + 4
|
||||
tgt = base + i + ilen + disp
|
||||
if tgt in tset:
|
||||
reg = (modrm >> 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)
|
||||
+101
@@ -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('<Q',d,32)[0]
|
||||
phes=struct.unpack_from('<H',d,54)[0]
|
||||
phn=struct.unpack_from('<H',d,56)[0]
|
||||
ph=[]
|
||||
for i in range(phn):
|
||||
o=phoff+i*phes
|
||||
t,fl,off,va,pa,fsz,msz,al=struct.unpack_from('<IIQQQQQQ',d,o)
|
||||
ph.append((t,off,va,fsz,msz))
|
||||
def v2f(v):
|
||||
for t,off,va,fsz,msz in ph:
|
||||
if t==1 and va<=v<va+msz: return off+(v-va)
|
||||
dyn=[p for p in ph if p[0]==2]
|
||||
if not dyn: return None
|
||||
_,doff,_,dsz,_=dyn[0]
|
||||
strtab=symtab=hash_=None; ents=[]
|
||||
for i in range(dsz//16):
|
||||
t,v=struct.unpack_from('<QQ',d,doff+i*16)
|
||||
ents.append((t,v))
|
||||
if t==5: strtab=v2f(v)
|
||||
if t==6: symtab=v2f(v)
|
||||
if t==4: hash_=v2f(v)
|
||||
if not strtab or not symtab: return None
|
||||
def nm(off):
|
||||
e=d.find(b'\x00',strtab+off)
|
||||
return d[strtab+off:e].decode('latin1')
|
||||
# nomi delle librerie: DT_NEEDED + tag SCE con indice
|
||||
needed=[nm(v) for t,v in ents if t==1 and v<0x1000000]
|
||||
libs={}
|
||||
for t,v in ents:
|
||||
if 0x61000000<=t<=0x61ffffff:
|
||||
idx=v>>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('<I',d,hash_+4)[0] if hash_ else 0
|
||||
syms=[]
|
||||
for i in range(nchain):
|
||||
o=symtab+i*24
|
||||
st_name,st_info,st_other,st_shndx,st_value,st_size=struct.unpack_from('<IBBHQQ',d,o)
|
||||
if not st_name: continue
|
||||
s=nm(st_name)
|
||||
if '#' in s:
|
||||
nid,lid,mid=s.split('#',2)
|
||||
else:
|
||||
nid,lid,mid=s,'',''
|
||||
syms.append({'nid':nid,'lid':lid,'mid':mid,'info':st_info,'type':st_info&0xf,
|
||||
'shndx':st_shndx,'value':st_value,'size':st_size})
|
||||
return {'data':d,'phdrs':ph,'needed':needed,'libs':libs,'syms':syms,'strtab':strtab}
|
||||
|
||||
def report(path, db):
|
||||
r=parse(path)
|
||||
if not r:
|
||||
print('%s: non e un .sprx valido' % path); return
|
||||
name=os.path.basename(path)
|
||||
print('='*78)
|
||||
print('FILE: %s' % name)
|
||||
print(' NEEDED: %s' % ', '.join(r['needed']))
|
||||
exp=[s for s in r['syms'] if s['shndx']!=0 and s['value']!=0 and s['type']==2]
|
||||
obj=[s for s in r['syms'] if s['shndx']!=0 and s['value']!=0 and s['type']==1]
|
||||
imp=[s for s in r['syms'] if s['shndx']==0]
|
||||
print(' EXPORT funzioni: %d EXPORT oggetti: %d IMPORT: %d' % (len(exp),len(obj),len(imp)))
|
||||
print(' --- EXPORT (funzioni) ---')
|
||||
for s in sorted(exp,key=lambda x:x['value']):
|
||||
n=db.get(s['nid'],'??')
|
||||
print(' %-12s 0x%-7x %s' % (s['nid'], s['value'], n))
|
||||
if obj:
|
||||
print(' --- EXPORT (oggetti) ---')
|
||||
for s in sorted(obj,key=lambda x:x['value']):
|
||||
print(' %-12s 0x%-7x %s' % (s['nid'], s['value'], db.get(s['nid'],'??')))
|
||||
print(' --- IMPORT ---')
|
||||
seen=set()
|
||||
for s in sorted(imp,key=lambda x:db.get(x['nid'],'zz')):
|
||||
n=db.get(s['nid'],'??')
|
||||
if (n,s['lid']) in seen: continue
|
||||
seen.add((n,s['lid']))
|
||||
print(' %-12s #%s#%s %s' % (s['nid'], s['lid'], s['mid'], n))
|
||||
print()
|
||||
|
||||
if __name__=='__main__':
|
||||
db=load_nid_db()
|
||||
print('DB NID: %d voci' % len(db))
|
||||
for p in sys.argv[1:]:
|
||||
report(p, db)
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stampa la mappa completa stub PLT -> 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, '?')))
|
||||
@@ -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)))))
|
||||
Reference in new issue
Block a user