- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| loaders | ||
| plugins | ||
| README.md | ||
PS3 loaders for IDA Pro, in Python
A Python rewrite of the PlayStation 3 half of xorloser's IDA console stuff
(ps3.dll / ps364.dll, package dated 2021-03-19). The original is a
compiled SDK plugin tied to one IDA version; this one is plain IDAPython, so it
keeps working across IDA releases and can be edited when a file does something
unexpected.
loaders/
ps3.py the loader itself
ps3.xml NID database (xorloser's file, one typo fixed)
ps3lib/
sce.py SELF container: parse, decrypt, rebuild an ELF
keys.py 116 key sets extracted from ps3.dll
plugins/
ppc2c.py C comments for tricky PowerPC instructions
ppchelper.py names saved registers and argument slots (PPC)
spuhelper.py the same for SPU
Install
Copy ps3.py, ps3.xml and the ps3lib directory into <IDA>/loaders, and
the three files in plugins/ into <IDA>/plugins. Everything is already
installed in C:\ida94b1.
The helper modules live in ps3lib/ on purpose: IDA treats every .py file
directly inside loaders/ as a loader, a subdirectory is left alone.
What it handles
| file | what you get |
|---|---|
.self (PPU) |
decrypted in memory, no scetool step |
.sprx / .prx |
relocated to 0x10000, PT_SCE_PPURELA applied |
.elf (PPU) |
loaded, with a reminder that the SELF carries more |
SPU .self / .elf |
loaded with IDA's SPU processor module |
lv1.self, lv2_kernel.self |
64 bit kernels, syscall table named |
Per file it does:
-
Decryption. The metadata info is unwrapped with each key set in turn and the right one is recognised by its two padding fields being zero, so key selection does not depend on a self-type/revision table. That is why 4.93 files decrypt even though the key table stops at "4.20 - 4.81" - the same keys are still in use. Debug SELFs (
se_flags & 0x8000, nothing encrypted) and fake-signed SELFs work too; NPDRM SELFs work when the content is free (the klicensee is derived), otherwise you get a message naming the content id. -
Segments from the section headers when they survived, from the program headers otherwise. Overlapping sections are skipped rather than fought over.
-
Relocations for prx/sprx: ADDR32, ADDR16_LO/HI/HA, REL24, ADDR64, REL64, ADDR16_LO_DS.
-
.opd: every function descriptor becomes a real function. Sony strips the section name table out of most retail files, so when.opdis not named the loader finds it - the longest run of entries whose function points into an executable segment and whose rtoc is one constant value, starting on a section boundary. Entries are 8 bytes on user land modules and the standard ppc64 24 bytes on lv1/lv2. -
rtoc: set as the default value of
r2for the whole image, so TOC relative loads resolve. -
Module info, imports and exports:
sceModuleInfo,sys_proc_prx_param,sys_process_param, theSceLibStub/SceLibEnttables, and a name for every NID out ofps3.xml(cellAdecOpen,sys_prx_load_module, ...). Unknown NIDs get<library>_<NID>, the same convention xorloser used. The nameless library every module exports is namedmodule_start/module_stop/module_exit/module_info.Three things were needed to make this work on every file, not just on prx:
vsh.selfhas a full.lib.entand.lib.stubbut an emptysys_prx_param, and its section names are stripped, so nothing points at either table. They are found by scanning for runs of back to back library structures, and a run is classified as an export table when its pointers land in.opdand as an import table when they land on a trampoline.- a library that only deals in functions uses the short 0x1C form of the structure, not just the 0x2C one.
- an import's stub table points straight at the trampoline in
.sceStub.text, it is not an.opdentry, so that address is what gets the name - that is what turns a call site intobl cellFsOpen.
On 4.93's
vsh.selfthat is 194/194 import trampolines and 4415/4415 export functions named, of whichps3.xmlcan resolve 131 and 4274 to real names (the rest are libraries the 2.60/3.20-era NID dump never saw,_cellAudio,sceNpOauth,np_sns_plugin). When two NIDs resolve to the same code the first name wins and the second is added as a repeatable comment. -
Syscall tables: for lv1/lv2 the array of pointers into
.opdis located and each handler named from theSyscall_lv1/Syscall_lv2groups ofps3.xml.The layout was read off the dispatcher rather than guessed. The 0xC00 vector lands in
sys_syscall_entry, which doescmpldi r11, 0x400 ; syscall number, 1024 slots blt ok li r11, 0 ; out of range uses slot 0ok: sldi r11, r11, 3 add r13, r11, r13 ; table base comes out of the process ld r13, 0(r13) ; slot -> .opd entry ld r13, 0(r13) ; .opd entry -> function
so a slot index is the syscall number, with no bias, and the table is exactly 1024 entries. On 4.93 that is 632 live handlers, 435 of them named by
ps3.xml, the other 197 namedlv2_syscall_<n>. Verified slot by slot against ground truth read straight out of the file: 633 of 633 exact.The syscall numbers the firmware dropped all point at one shared stub, which is named
lv2_invalid_syscallonce; the 88 namesps3.xmlstill lists for them are left on the table slots as comments instead of being dumped on that one function.Cross-checked against the kernel itself: 10 handlers reference a
syscall_sys_*debug string naming them, and all 10 land on the function the table +ps3.xmlnamed. Two of those strings name syscallsps3.xmlcannot (860sys_ss_get_cache_of_analog_sunset_flag, which the xml has commented out, and 870sys_ss_get_console_id, which it never had).One data file bug turned up:
ps3.xmlnamed both syscall 802 and 803sys_fs_read- 803 is reallysys_fs_write. That one line is now fixed in the shippedps3.xml, with an XML comment next to it saying what was changed; it is the only edit to xorloser's file. Every other syscall group was checked for the same kind of duplicate and is clean (Syscall_prochasproc_map_pageson 28 and 35 andproc_unmap_pageson 29 and 36, but those look like deliberate aliases, so they were left alone).The loader still warns if a future edit reintroduces a duplicate name.
Using the SELF code on its own
ps3lib/sce.py has no IDA dependency, so it doubles as a decrypter:
python ps3lib/sce.py lv2_kernel.self lv2_kernel.elf
SELF: LV2, auth id 0x1050000003000001, vendor id 0x05000002, version 4.93
key revision 0x0000
key set: Retail LV2 Keys (4.00 - 4.11) (LV2400)
elf: 3641960 bytes, machine 21, type 0x0002, 2 phdrs, 11 shdrs
It uses pycryptodome or cryptography if either is importable and falls back
to a built-in AES otherwise (slow, but it keeps the loader working in a bare
IDA interpreter).
Where the keys came from
ps3.dll carries the key table at file offset 0x105940: 116 entries of 0xD0
bytes, laid out as
0x00 char name[0x40] "Retail LV2 Keys (3.55)"
0x40 char id[0x20] "LV2355"
0x60 u8 erk[0x20]
0x80 u8 riv[0x10]
0x90 u8 pub[0x28]
0xB8 u8 priv[0x14]
0xCC u32 ctype
keys.py is that table dumped verbatim. Only erk/riv are used for
loading; pub/priv/ctype are kept for anyone who wants to check or forge
signatures.
Tested on
The 4.93 firmware tree on the Desktop: all 473 SELFs under Desktop\493
decrypt and rebuild without a single failure (APP, ISO, IS1, LV1, LV2 key
sets), and lv2_kernel.self, lv1.self, vsh.self, ps1_netemu.self,
bdj.self, libsysmodule.sprx, aim_spu_module.self and a pre-extracted
ps3swu.self.elf all load through idat -A cleanly.
The naming is checked against tables read straight out of the file rather
than eyeballed: on vsh.self every one of the 194 import trampolines and 4415
export functions ends up with exactly the expected name, and on
lv2_kernel.self all 633 syscall handlers do.
(Names ending in _0 in a kernel database are IDA's own doing: a lot of lv2
syscall entry points are one-instruction thunks, so IDA propagates the name to
the real body and adds a suffix to keep it unique.)
The plugins
ppc2c.py - PPC to C
Not a decompiler: it writes a C-ish comment next to the instructions that are hard to read at a glance.
mflr r0 # r0 = lr
rlwinm r3, r3, 28,3,31 # r3 = ((r3 << 28) | (r3 >> 4)) & 0x1FFFFFFF
clrlwi r0, r0, 28 # r0 = r0 & 0x0000000F
sldi r11, r11, 3 # r11 = r11 << 3
srawi r0, r3, 0x1F # r0 = (signed)r3 >> 31 (sets carry)
subf r9, r0, r9 # r9 = r9 - r0
blt loc_275BEC # if(cr0 is less than) goto loc_275BEC
Instructions are decoded from the raw opcode word, not from what IDA prints, so every simplified mnemonic (sldi, srdi, clrlwi, rotlwi, extrwi, ...) is handled without knowing how a given IDA version spells it. Covers the rotate and mask family (rlwinm/rlwimi/rlwnm and all four rld forms), conditional branches, compares, the logic and shift instructions, addi/addis/li/lis and mfspr/mtspr.
The decoder is importable and has no IDA dependency, so it can be checked
outside IDA - python ppc2c.py --selftest runs 19 cases whose words were
taken out of a real 4.93 lv2_kernel.self and whose expected text was checked
against what IDA prints at that address.
Hotkeys: Ctrl-Alt-C current line, Ctrl-Alt-Shift-C whole function. The
plugins.cfg lines from xorloser's readme still work, run(0) is one line and
run(1) is the function.
ppchelper.py / spuhelper.py
Read the prologue and give the stack slots and registers useful names:
stqd lr, var_10(sp) -> stqd lr, save_lr(sp)
stqd r80, var_20(sp) -> stqd r80, save_r80(sp)
stw r0, 0x110(r31) -> stw r0, arg0(r31)
lr r84, r5 -> r84 becomes arg2 from here on
The rules come from the ABI: a store of a callee saved register (r14-r31 on
PPC, r80-r127 on SPU) before anything has written to it is that register's
save slot; r3-r10 (PPC) or r3-r74 (SPU) hold the incoming arguments, so those
registers, and any callee saved register the prologue copies them into, are
named arg0..argN.
Three details that matter:
- a register variable starts after the copy, not at the function start -
before that the register still holds the caller's value, and naming it
arg0there would be a lie - only the first register to receive a given argument gets its name, so two
registers never both show up as
arg0 - a displacement is only treated as a frame access when its base register is
really the stack pointer -
r1, or a register the prologue copiedr1into. Without that check any0x10(r31)would be misread as a stack slot
Only dummy names (var_8, arg_10, ...) are touched, so IDA's own
back_chain / saved_toc / sender_lr and anything you named survive. The
incoming parameter slots live above the frame and IDA does not create them by
itself, so those members are added.
Hotkeys: Ctrl-Alt-H current function, Ctrl-Alt-Shift-H every function in
the database. Each plugin only wakes up for its own processor, so the shared
hotkey is never ambiguous.
Unlike the original SPUHelper, spuhelper.py works with the SPU processor
module IDA ships - it reads registers by number rather than by xorloser's
names - so there is no need to install his SPU module or rename files.
Not carried over
- xorloser's own SPU processor module - IDA ships a perfectly good one, and the SPU helper here is written against it
- the Xbox 360 XEX loader
- automatic DWARF loading for debug builds - IDA's own ELF loader does that, and you can still point it at the extracted ELF
Gotcha worth remembering
Do not call ida_funcs.add_func() from inside load_file(). The analysis
engine is not up yet and IDA dies with no message at all. Queue the address
with ida_auto.auto_make_proc() instead; the function appears once loading
finishes.