mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
Decrypt the NPDRM modules a PKG game update installs
A .sprx from one of these packages is an NPDRM "\0PSPEDAT" container: a 0x90-byte header, then an ordinary ~PSP PRX. The loader only ever saw the EDAT magic and gave up with SCE_KERNEL_ERROR_UNSUPPORTED_PRX_TYPE. Step over the header, then derive the key the PRX inside is really encrypted against: sceNpDrmGetFixedKey() over the content ID, XOR in the licensee key the game handed us through sceNpDrmSetLicenseeKey(), then AES under a module key that had to be added. Both halves of that were already lying around unused - sceNpDrmGetFixedKey() had no callers at all, and the licensee key was being kept and never read. The rest of it is a fixed XOR that the PRX header's decrypt_mode selects rather than its tag, so it's applied on the mode the way JPCSP does it and the tag table is left alone - tag 0x407810F0 carries no seed of its own there either, so ours was never wrong about it. pspDecryptType5() already had a slot for both XORs; no new decryption logic was needed. Decryption is only half of it: these modules are KL4E-compressed rather than gzipped, so they also need Core/Util/KL4E.cpp, which is already there for the firmware modules that use the same compression. With both halves Shiren 4 Plus loads its one big .sprx and runs. God Eater 2 needed one further fix that isn't in this commit - the type-B relocation bug in ElfReader::LoadRelocations2, issue #8075 - and then plays. docs/pkg_notes.md has the container layout and the key derivation. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
f0dafdee10
commit
19723f59eb
5 files changed
+199
-33
No files matched your search
@@ -14,6 +14,20 @@ extern "C"
|
||||
|
||||
#define ROUNDUP16(x) (((x)+15)&~15)
|
||||
|
||||
// PSP_Header::decrypt_mode, the byte at 0x7C. Only the one we act on is named; the rest select
|
||||
// which decryption variant a real PSP would use, which we don't need since we try them in turn.
|
||||
enum {
|
||||
PRX_DECRYPT_MODE_SPRX = 23,
|
||||
};
|
||||
|
||||
// A module that arrived inside an NPDRM EDAT carries this fixed XOR on top of its tag's key. It
|
||||
// goes with the decrypt_mode above rather than with any particular tag - tag 0x407810F0's table
|
||||
// entry has no seed of its own, in JPCSP's tables as well as ours, so keying this on the tag
|
||||
// would be wrong for a 0x407810F0 module that arrived some other way.
|
||||
static const u8 xor_91E0A9AD[16] = {
|
||||
0x84, 0x7B, 0xF5, 0xFE, 0xE8, 0x4D, 0xAD, 0x7A, 0xB5, 0x06, 0x28, 0x0E, 0x09, 0xFA, 0x81, 0xE1,
|
||||
};
|
||||
|
||||
// Thank you PSARDUMPER & JPCSP keys
|
||||
|
||||
// PRXDecrypter 16-byte tag keys.
|
||||
@@ -966,9 +980,13 @@ static int pspDecryptType5(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz
|
||||
// expand the seed into a xor buffer
|
||||
auto xorbuf = expandSeed(pti->key, pti->code, seed);
|
||||
|
||||
// The XOR the decrypt_mode implies wins over the tag table's, which is only the fallback -
|
||||
// same precedence as JPCSP, and it leaves every tag that has a seed of its own alone.
|
||||
const u8 *xor1 = inbuf[0x7C] == PRX_DECRYPT_MODE_SPRX ? xor_91E0A9AD : pti->seed;
|
||||
|
||||
// construct the header format for a type 2 prx
|
||||
PRXType5 type5(inbuf);
|
||||
type5.decrypt(pti->code, pti->seed, seed);
|
||||
type5.decrypt(pti->code, xor1, seed);
|
||||
|
||||
SHA_CTX ctx;
|
||||
SHAInit(&ctx);
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#include "Core/ELF/ElfReader.h"
|
||||
#include "Core/ELF/PBPReader.h"
|
||||
#include "Core/ELF/PrxDecrypter.h"
|
||||
#include "Core/HLE/scePspNpDrm_user.h"
|
||||
#include "Core/Util/KL4E.h"
|
||||
#include "Core/FileSystems/FileSystem.h"
|
||||
#include "Core/FileSystems/MetaFileSystem.h"
|
||||
@@ -1216,7 +1217,9 @@ static void LoadAndStartVshKernelModules() {
|
||||
}
|
||||
|
||||
// filename is only used for dumping/metadata.
|
||||
static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 loadAddress, bool fromTop, std::string *error_string, u32 *magic, std::string_view filename, u32 &error) {
|
||||
// prxSeed is the extra key a module that came out of an NPDRM container needs to decrypt - see
|
||||
// NpDrmDeriveModuleKey(). Null for everything else, which is the overwhelming majority.
|
||||
static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 loadAddress, bool fromTop, std::string *error_string, u32 *magic, std::string_view filename, u32 &error, const u8 *prxSeed = nullptr) {
|
||||
// The magic reads below need four bytes, and the ~SCE branch another four after that. Everything
|
||||
// downstream checks its own sizes; this is just so we can look at the magic at all. The PBP path
|
||||
// in __KernelLoadModule computes elfSize from two offsets in the file and doesn't floor it.
|
||||
@@ -1289,7 +1292,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
|
||||
newptr = new u8[maxElfSize];
|
||||
elfSize = maxElfSize;
|
||||
ptr = newptr;
|
||||
int decryptedSize = pspDecryptPRX(in, (u8*)ptr, head->psp_size);
|
||||
int decryptedSize = pspDecryptPRX(in, (u8*)ptr, head->psp_size, prxSeed);
|
||||
// If decryption got us nowhere, the PRX may simply not be encrypted - in which case the ELF
|
||||
// starts right after the header. Check the source buffer, not the destination: on the paths
|
||||
// where decryption bails early nothing has been written to newptr yet, so this used to read
|
||||
@@ -2304,6 +2307,37 @@ u32 sceKernelLoadModule(const char *name, u32 flags, u32 optionAddr) {
|
||||
return hleDelayResult(error, "module loaded", 500);
|
||||
}
|
||||
|
||||
// A .sprx installed by a PKG game update comes wrapped in an NPDRM "\0PSPEDAT" container: a
|
||||
// 0x90-byte header naming the content ID, then the payload at the offset in its u16 at 0x0C.
|
||||
// The payload is an ordinary ~PSP PRX, so stepping over the header is enough to get it to the
|
||||
// decrypter - otherwise the ELF check sees the EDAT magic and the load fails with
|
||||
// SCE_KERNEL_ERROR_UNSUPPORTED_PRX_TYPE. It doesn't decrypt with the tag's key alone though;
|
||||
// the header also yields the seed it's really encrypted against.
|
||||
//
|
||||
// Data EDATs put a PGD at the payload offset instead (0x0003 rather than 0x0101 at 0x0E), and
|
||||
// those aren't loaded as modules - they go through sceNpDrmEdataSetupKey and the io layer.
|
||||
//
|
||||
// Hardware only unwraps this for sceKernelLoadModuleNpDrm, but keying off the file's own magic
|
||||
// costs nothing: an unwrapped module never has it. See docs/pkg_notes.md.
|
||||
u8 prxSeed[16];
|
||||
bool havePrxSeed = false;
|
||||
if (fileData.size() > 0x90 && !memcmp(fileData.data(), "\0PSPEDAT", 8)) {
|
||||
const size_t payloadOffset = fileData[0x0C] | (fileData[0x0D] << 8);
|
||||
if (payloadOffset >= 0x90 && payloadOffset < fileData.size()) {
|
||||
havePrxSeed = NpDrmDeriveModuleKey(fileData.data(), prxSeed);
|
||||
if (!havePrxSeed) {
|
||||
// Not fatal on its own - a module that needs no seed decrypts without one, and one
|
||||
// that does will fail below with the same error as any other undecryptable module.
|
||||
WARN_LOG(Log::Loader, "Couldn't derive the NPDRM key for '%s'", name);
|
||||
}
|
||||
DEBUG_LOG(Log::Loader, "Unwrapping NPDRM module '%s' (%d bytes of EDAT header)", name, (int)payloadOffset);
|
||||
fileData.erase(fileData.begin(), fileData.begin() + payloadOffset);
|
||||
} else {
|
||||
// Fall through - the magic check further down reports it like any other bad module.
|
||||
WARN_LOG(Log::Loader, "'%s' has an EDAT header with a bad payload offset %d", name, (int)payloadOffset);
|
||||
}
|
||||
}
|
||||
|
||||
// We log before hand because ELF loading logs a bunch.
|
||||
DEBUG_LOG(Log::Loader, "sceKernelLoadModule(%s, %08x)", name, flags);
|
||||
|
||||
@@ -2332,7 +2366,7 @@ u32 sceKernelLoadModule(const char *name, u32 flags, u32 optionAddr) {
|
||||
u32 magic;
|
||||
u32 error;
|
||||
std::string error_string;
|
||||
module = __KernelLoadELFFromPtr(fileData.data(), fileData.size(), 0, lmoption ? lmoption->position == PSP_SMEM_High : false, &error_string, &magic, name, error);
|
||||
module = __KernelLoadELFFromPtr(fileData.data(), fileData.size(), 0, lmoption ? lmoption->position == PSP_SMEM_High : false, &error_string, &magic, name, error, havePrxSeed ? prxSeed : nullptr);
|
||||
|
||||
if (!module) {
|
||||
if (magic == 0x46535000) {
|
||||
@@ -2368,7 +2402,8 @@ u32 sceKernelLoadModule(const char *name, u32 flags, u32 optionAddr) {
|
||||
}
|
||||
|
||||
static u32 sceKernelLoadModuleNpDrm(const char *name, u32 flags, u32 optionAddr) {
|
||||
// Just forward it, same parameters so the logging will make sense.
|
||||
// Just forward it, same parameters so the logging will make sense. The NPDRM EDAT wrapper these
|
||||
// modules carry is stepped over in there, since that's keyed off the file's own magic.
|
||||
return sceKernelLoadModule(name, flags, optionAddr);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
// kirk_engine.h includes AES.h from outside its own extern "C" block, so whoever pulls it in first
|
||||
// decides the linkage the AES_* functions get. Do it here, the way PrxDecrypter.cpp does, or they
|
||||
// come through sceChnnlsv.h below with C++ linkage and fail to link.
|
||||
extern "C" {
|
||||
#include "ext/libkirk/kirk_engine.h"
|
||||
#include "ext/libkirk/amctrl.h"
|
||||
}
|
||||
|
||||
#include "Core/HLE/scePspNpDrm_user.h"
|
||||
#include "Core/MemMapHelpers.h"
|
||||
#include "Core/HLE/HLE.h"
|
||||
#include "Core/HLE/FunctionWrappers.h"
|
||||
#include "Core/HLE/sceChnnlsv.h"
|
||||
#include "Core/HLE/sceIo.h"
|
||||
#include "Core/FileSystems/MetaFileSystem.h"
|
||||
|
||||
@@ -132,6 +141,45 @@ static int sceNpDrmOpen() {
|
||||
return hleLogError(Log::sceIo, 0, "UNIMPL: sceNpDrmOpen()");
|
||||
}
|
||||
|
||||
// The last pass over a module key. This one belongs to npdrm.prx's module path rather than to the
|
||||
// PGD/amctrl side, so it isn't among the keys in ext/libkirk.
|
||||
static const u8 drmModuleKey[16] = {
|
||||
0xBA, 0x87, 0xE4, 0xAB, 0x2C, 0x60, 0x5F, 0x59, 0xB8, 0x3B, 0xDB, 0xA6, 0x82, 0xFD, 0xAE, 0x14,
|
||||
};
|
||||
|
||||
bool NpDrmDeriveModuleKey(const u8 *edatHeader, u8 *keyOut) {
|
||||
// The low byte of the u32 at 0x08 picks which fixed key to derive from the content ID at 0x10.
|
||||
char contentId[0x31]{};
|
||||
memcpy(contentId, edatHeader + 0x10, 0x30);
|
||||
const int keyMode = 0x01000000 | edatHeader[0x08];
|
||||
const int result = sceNpDrmGetFixedKey(__ChnnlsvKirkState(), keyOut, contentId, keyMode);
|
||||
if (result != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const u8 flags = edatHeader[0x0F];
|
||||
if (flags & 1) {
|
||||
// The game is meant to hand the licensee key over before it loads the module.
|
||||
if (!isLicenseeKeySet) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < PSP_NPDRM_LICENSEE_KEY_LENGTH; i++) {
|
||||
keyOut[i] ^= licenseeKey[i];
|
||||
}
|
||||
}
|
||||
if (flags & 2) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
keyOut[i] ^= edatHeader[0x40 + i];
|
||||
}
|
||||
}
|
||||
|
||||
// JPCSP does this as CBC with an all-zero IV, which over a single block is a plain decrypt.
|
||||
AES_ctx ctx;
|
||||
AES_set_key(&ctx, drmModuleKey, 128);
|
||||
AES_decrypt(&ctx, keyOut, keyOut);
|
||||
return true;
|
||||
}
|
||||
|
||||
const HLEFunction sceNpDrm[] = {
|
||||
{0XA1336091, &WrapI_U<sceNpDrmSetLicenseeKey>, "sceNpDrmSetLicenseeKey", 'i', "x"},
|
||||
{0X9B745542, &WrapI_V<sceNpDrmClearLicenseeKey>, "sceNpDrmClearLicenseeKey", 'i', "" },
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
class PointerWrap;
|
||||
|
||||
void Register_sceNpDrm();
|
||||
|
||||
// A module wrapped in an NPDRM "\0PSPEDAT" container has its PRX encrypted against a key built
|
||||
// from that container's header and, usually, the licensee key the game handed over through
|
||||
// sceNpDrmSetLicenseeKey - which is why this lives here rather than with the PRX decrypter.
|
||||
// Writes 16 bytes to keyOut, to be passed to pspDecryptPRX() as the seed.
|
||||
//
|
||||
// edatHeader must point at the 0x90 readable bytes of the container header. Returns false if the
|
||||
// header asks for something we can't build, notably a licensee key the game never set.
|
||||
// See docs/pkg_notes.md for the layout and where this came from.
|
||||
bool NpDrmDeriveModuleKey(const u8 *edatHeader, u8 *keyOut);
|
||||
+81
-28
@@ -115,8 +115,11 @@ encrypted - a decoder can tell what it is holding before looking at it:
|
||||
| 8 | PSP EDAT (`.sprx` modules) | `\0PSPEDAT` |
|
||||
| 11 | PBP | `\0PBP` |
|
||||
|
||||
The `\0PSPEDAT` files are the PGD-wrapped kind PPSSPP already decrypts at runtime, via
|
||||
`sceNpDrmEdataSetupKey` in `Core/HLE/scePspNpDrm_user.cpp`.
|
||||
Both flags 5 and 8 give you a `\0PSPEDAT`, but they are not the same thing inside. A flags-5 data
|
||||
file wraps a PGD, which PPSSPP decrypts at runtime through `sceNpDrmEdataSetupKey()` in
|
||||
`Core/HLE/scePspNpDrm_user.cpp`. A flags-8 `.sprx` wraps an encrypted PRX instead, and goes through
|
||||
the module loader - see "NPDRM `.sprx` modules" below. The byte at 0x0E of the header tells them apart
|
||||
(3 for the PGD kind, 1 for the PRX kind).
|
||||
|
||||
## What an update package contains
|
||||
|
||||
@@ -253,43 +256,93 @@ because the dumps in circulation are later disc revisions than the updates were
|
||||
Refusing outright would make most of them unusable. Elminage Original was the one clean
|
||||
exact-match case, disc 1.01 against an update for 1.01, and it boots without a warning.
|
||||
|
||||
### Known limitation: PGD-wrapped `.sprx` modules don't load
|
||||
### NPDRM `.sprx` modules
|
||||
|
||||
Package payloads are full of `\0PSPEDAT` files. That's fine for
|
||||
*data*: `sceNpDrmEdataSetupKey()` in `Core/HLE/scePspNpDrm_user.cpp` wraps an open file descriptor
|
||||
with the `0x04100002`/`0x04100001` ioctl pair, and the game reads plaintext.
|
||||
|
||||
A few packages ship `\0PSPEDAT` **executables** - `.sprx` modules - and that path is not
|
||||
implemented. God Eater 2 (NPJH50832, 45 of them) installs cleanly, boots its `PBOOT.PBP`, and then
|
||||
loops forever on:
|
||||
A few packages wrap **executables** that way - `.sprx` modules the game loads with
|
||||
`sceKernelLoadModuleNpDrm`. Those need more than the data path does. Until they were handled, God
|
||||
Eater 2 (NPJH50832) installed cleanly, booted its `PBOOT.PBP`, and then looped forever failing to
|
||||
load `system.sprx`; Shiren 4 Plus (NPJH50698), which keeps the whole game in one `.sprx` behind a
|
||||
small loader, failed the same way.
|
||||
|
||||
An NPDRM module is two layers, and the loader originally saw only the outer one:
|
||||
|
||||
```
|
||||
E Loader: SCE_KERNEL_ERROR_UNSUPPORTED_PRX_TYPE=sceKernelLoadModuleNpDrm(
|
||||
ms0:/PSP/GAME/NPJH50832/system.sprx, 00000000, 00000000): failed to load
|
||||
E sceModule: Wrong magic number 50535000
|
||||
+0x00 "\0PSPEDAT" header, 0x90 bytes
|
||||
+0x08 u32 key mode; low byte is what sceNpDrmGetFixedKey takes (3 in all of these)
|
||||
+0x0C u16 payload offset (0x90 in everything seen)
|
||||
+0x0F u8 flag bits: 1 = XOR in the licensee key, 2 = XOR in the 16 bytes at 0x40
|
||||
+0x10 char[0x30] content ID, "JP0365-NPJH50698_00-SIREN4PLUS2012MA"
|
||||
+0x90 a normal "~PSP" PRX: tag 0x407810F0 at 0xD0, decrypt_mode 23 at 0x7C
|
||||
```
|
||||
|
||||
Shiren 4 Plus (NPJH50698, one `.sprx` holding the whole game behind a 62 KB loader) fails
|
||||
identically. Tales of the World Radiant Mythology 3 (NPJH50353, two) doesn't reach its modules
|
||||
within 100 seconds of headless boot, so it gets past startup - it presumably hits the same wall
|
||||
whenever it does load them.
|
||||
`sceKernelLoadModule()` steps over the EDAT header, and the PRX inside then decrypts. Two keys go
|
||||
into that, both worked out from JPCSP (`ModuleMgrForUser.sceKernelLoadModuleNpDrm`, `crypto/DRM.java`,
|
||||
`crypto/PRX.java`, `crypto/KeyVault.java`), and both feed `pspDecryptType5()`, which already had a
|
||||
slot for each.
|
||||
|
||||
`sceKernelLoadModuleNpDrm()` in `Core/HLE/sceKernelModule.cpp` is a one-line forward to
|
||||
`sceKernelLoadModule()`, which sees something that isn't ELF magic and gives up. Fixing it means
|
||||
unwrapping the EDAT before handing the bytes to the ELF loader, using the licensee key the game has
|
||||
already set via `sceNpDrmSetLicenseeKey` - the same material the data path uses.
|
||||
**xor2, the per-content key** - `NpDrmDeriveModuleKey()` in `Core/HLE/scePspNpDrm_user.cpp`, four
|
||||
steps in order:
|
||||
|
||||
This is a pre-existing emulator gap rather than something the installer gets wrong: until now
|
||||
nothing put such a file on the memory stick, so it was unreachable. The unpatched God Eater 2 never
|
||||
calls `sceKernelLoadModuleNpDrm` at all - the update is what introduces the modules - so for that
|
||||
title the update is currently better not installed, since the unpatched game boots. It's also why a
|
||||
"God Eater 2 DLC Update v1.40 *Decrypted for PPSSPP Emulator*" folder circulates: the `PBOOT.PBP` in
|
||||
it is byte-for-byte what `InstallPkg()` writes, and the only difference is that its `.sprx` files
|
||||
have been unwrapped to plain ELF by hand.
|
||||
1. `sceNpDrmGetFixedKey(kirk, key, edat+0x10, 0x01000000 | edat[0x08])` - already in
|
||||
`ext/libkirk/amctrl.c`, and identical to JPCSP's `hleNpDrmGetFixedKey`: our `key_363C` is its
|
||||
`drmFixedKey`, our `key_357C[0/1/2]` are its `drmEncKey1/2/3`, and its AES-CBC under an all-zero
|
||||
IV over one block is our `AES_encrypt`. Nothing had called this function before.
|
||||
2. If `edat[0x0F] & 1`: XOR the licensee key the game passed to `sceNpDrmSetLicenseeKey()`, which
|
||||
this file already kept but never used. The game sets it before it loads the module.
|
||||
3. If `edat[0x0F] & 2`: XOR the 16 bytes at `edat+0x40`. None seen here use it.
|
||||
4. AES-128 decrypt the result under `drmModuleKey`, the one constant that had to be added here.
|
||||
JPCSP does CBC with a zero IV; over a single block that is a plain `AES_decrypt`.
|
||||
|
||||
**xor1, a static key picked by `decrypt_mode`** - the PRX header byte at 0x7C being 23
|
||||
(`DECRYPT_MODE_SPRX`), which is what these payloads are. JPCSP keys this on the mode rather than on
|
||||
the tag, and so do we: tag 0x407810F0's table entry has no seed of its own in JPCSP's tables either,
|
||||
so our table was never wrong, it just had nothing to say about a case selected somewhere else.
|
||||
`pspDecryptType5()` takes the mode-derived XOR when the mode calls for it and falls back to the tag
|
||||
table's otherwise - the same precedence JPCSP uses, which leaves every tag that does carry a seed
|
||||
(the `pauth` ones) exactly as it was.
|
||||
|
||||
No new decryption logic was needed. `pspDecryptType5()` is structurally identical to JPCSP's "new
|
||||
method" for type 5: `expandSeed(pti->key, pti->code, seed)` XORs xor2 over the 0x90-byte scrambled
|
||||
key buffer as `RoundXOR(buf2, 0, 0x90, xor2, null)` does; `PRXType5::decrypt()` XORs both over the
|
||||
0x50-byte kirk header and SHA1 as `RoundXOR(buf2, 0x14, 0x50, xor1, xor2)` does, and then xor1 alone
|
||||
over the 0x60 bytes at `id` as `RoundXOR(buf4, 0x14, 0x60, xor1, null)` does. JPCSP's `RoundXOR` is
|
||||
`buf[i] ^= key[i & 0xF]`, the same repeating XOR as our `xor[i % 0x10]`.
|
||||
|
||||
### The other half of it: KL4E
|
||||
|
||||
Decrypting is only half the job. Shiren 4 Plus's `f5psp.sprx` decrypts to bytes that start with
|
||||
`KL4E`: every one of these modules has `comp_attribute = 0x0201`, i.e. compressed, and
|
||||
`(comp_attribute & 0xF00) != 0` means KL4E/KL3E rather than gzip. So decryption alone would just
|
||||
move the failure from "unsupported PRX type" to "decompression failed".
|
||||
|
||||
`Core/Util/KL4E.cpp` already handles that - it went in for firmware modules that use the same
|
||||
compression, and these get it for free. It's worth knowing the two halves are independent, because
|
||||
each one on its own leaves the module unloadable and the error doesn't say which is missing.
|
||||
|
||||
Note that none of this can be checked offline against the hand-decrypted `.sprx` files that
|
||||
circulate for God Eater 2: their flag byte says the licensee key is part of the derivation, and that
|
||||
key only exists while the game is running. Those files are still useful as a cross-check of the
|
||||
installer itself - the `PBOOT.PBP` in such a folder is byte-for-byte what `InstallPkg()` writes, and
|
||||
the only difference is that the `.sprx` files beside it were decrypted by hand.
|
||||
|
||||
### Where that leaves the three module titles
|
||||
|
||||
- **God Eater 2 (NPJH50832)** installs, boots, loads its modules and plays. Decryption alone wasn't
|
||||
enough for it: it also needed the type-B relocation fix in `ElfReader::LoadRelocations2` (issue
|
||||
#8075), where two `lui`s sharing one `addiu` got different high halves, so a callback pointer
|
||||
landed 0x48 bytes inside a function.
|
||||
- **Shiren 4 Plus (NPJH50698)** loads `f5psp.sprx` - the log says `'FDS3PSP' is KL4E-compressed,
|
||||
decompressing` - and runs.
|
||||
- **Tales of the World Radiant Mythology 3 (NPJH50353)** still doesn't reach its modules inside a
|
||||
headless boot, so it remains untested rather than known-good.
|
||||
|
||||
### Still not tested
|
||||
|
||||
- Nothing here plays past a title screen. The runs are 25-second headless boots, so "the update is
|
||||
in use" means the patched executable is what loaded and ran - not that a patched *asset* was read.
|
||||
LittleBigPlanet's `PATCH.ARC` covers that for a UMD title; there's no equivalent observation for a
|
||||
digital one yet.
|
||||
- Only God Eater 2 has been played past a title screen. For the rest the runs are short headless
|
||||
boots, so "the update is in use" means the patched executable is what loaded and ran - not that a
|
||||
patched *asset* was read. LittleBigPlanet's `PATCH.ARC` covers that for a UMD title; there's no
|
||||
equivalent observation for a digital one yet.
|
||||
Reference in new issue
Block a user