From 19723f59ebfe90cde575faeee9a1d6878e93a365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 24 Aug 2026 14:39:59 +0200 Subject: [PATCH] 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 --- Core/ELF/PrxDecrypter.cpp | 20 ++++++- Core/HLE/sceKernelModule.cpp | 43 ++++++++++++-- Core/HLE/scePspNpDrm_user.cpp | 48 +++++++++++++++ Core/HLE/scePspNpDrm_user.h | 12 ++++ docs/pkg_notes.md | 109 +++++++++++++++++++++++++--------- 5 files changed, 199 insertions(+), 33 deletions(-) diff --git a/Core/ELF/PrxDecrypter.cpp b/Core/ELF/PrxDecrypter.cpp index 2ae7b4633f..cf0e980e37 100644 --- a/Core/ELF/PrxDecrypter.cpp +++ b/Core/ELF/PrxDecrypter.cpp @@ -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); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 8abad4708b..ae0ed6a032 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -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); } diff --git a/Core/HLE/scePspNpDrm_user.cpp b/Core/HLE/scePspNpDrm_user.cpp index c81acd7a51..37c35b9386 100644 --- a/Core/HLE/scePspNpDrm_user.cpp +++ b/Core/HLE/scePspNpDrm_user.cpp @@ -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", 'i', "x"}, {0X9B745542, &WrapI_V, "sceNpDrmClearLicenseeKey", 'i', "" }, diff --git a/Core/HLE/scePspNpDrm_user.h b/Core/HLE/scePspNpDrm_user.h index 3e59c6aa39..35af832eee 100644 --- a/Core/HLE/scePspNpDrm_user.h +++ b/Core/HLE/scePspNpDrm_user.h @@ -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); diff --git a/docs/pkg_notes.md b/docs/pkg_notes.md index c41a2837d7..720e35ff08 100644 --- a/docs/pkg_notes.md +++ b/docs/pkg_notes.md @@ -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.