mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-11 08:39:28 +01:00
Merge pull request #22124 from hrydgard/psar-unpack
PSAR file format support: Automatic unpacking of firmware updates
This commit is contained in:
16 files changed
+1488
-2
No files matched your search
@@ -169,6 +169,8 @@ codebase first - PPSSPP already has implementations of many formats (CSO, LZRC,
|
||||
handlers, PBP, SevenZip, etc.), possibly in several places. Reuse or extend an existing one instead of
|
||||
writing a new one (e.g. there is an LZRC decompressor in Core/FileSystems/tlzrc.cpp).
|
||||
|
||||
For string sanitation, we already have SanitizeString in StringUtils.cpp - add new modes if needed.
|
||||
|
||||
## Headless and unittest builds
|
||||
|
||||
We have additional PPSSPPHeadless and unit test builds (/headless and /unittest), that have their own separate
|
||||
@@ -537,6 +539,12 @@ Concretely, as measured against the headless build (2026-08-16), per CPU backend
|
||||
| `memory.breakpoint.*` (memchecks) | works | **only for constant addresses** - see below |
|
||||
| `cpu.regBreakpoint.*` | works | never trips (as documented) |
|
||||
|
||||
## Commit message style
|
||||
|
||||
Keep commit messages focused, not overly long (although sometimes it's motivated if a single commit
|
||||
is super complex). Do not report things like 100/100 tests passed - that's a given, if tests break
|
||||
you aren't supposed to make a commit.
|
||||
|
||||
## Quick rebuild on Linux
|
||||
|
||||
You don't need to do ./b.sh --debug to verify every single little change, instead use this shortcut:
|
||||
|
||||
@@ -705,6 +705,8 @@ add_library(Core STATIC
|
||||
Util/BlockAllocator.h
|
||||
Util/PPGeDraw.cpp
|
||||
Util/PPGeDraw.h
|
||||
Util/PSARUnpack.cpp
|
||||
Util/PSARUnpack.h
|
||||
Util/RecentFiles.cpp
|
||||
Util/RecentFiles.h
|
||||
${CMAKE_SOURCE_DIR}/ext/disarm.cpp
|
||||
|
||||
@@ -192,6 +192,8 @@ static const CommandLineParam g_autoParams[] = {
|
||||
{POFF(timeout), CmdParamType::Double, "timeout", '\0', "Set the timeout value", CmdLineMode::Headless},
|
||||
{POFF(maxScreenshotError), CmdParamType::Double, "max-mse", '\0', "Maximum allowed MSE error for screenshot comparison", CmdLineMode::Headless},
|
||||
{POFF(mountIso), CmdParamType::String, "mount", 'm', "Mount ISO/CSO on umd1:", CmdLineMode::Headless},
|
||||
{POFF(unpackUpdater), CmdParamType::String, "unpack-updater", '\0', "Unpack the firmware in an updater EBOOT.PBP into DIR and exit", CmdLineMode::Headless},
|
||||
{POFF(unpackUpdaterModel), CmdParamType::String, "unpack-updater-model", '\0', "PSP model to unpack for (01g..12g, default any)", CmdLineMode::Headless},
|
||||
{POFF(odsLog), CmdParamType::Bool, "odslog", 'o', "Also log through OutputDebugString (Windows)", CmdLineMode::Headless},
|
||||
{POFF(generateInterpreterDispatch), CmdParamType::Bool, "generate-interpreter-dispatch", '\0', "Generate C++ interpreter dispatch code (ExecInstruction) to stdout and exit", CmdLineMode::Headless},
|
||||
{POFF(resolutionScale), CmdParamType::Int, "resolution-scale", '\0', "Set the resolution scale factor"},
|
||||
|
||||
@@ -64,6 +64,15 @@ struct CommandLineOptions {
|
||||
std::optional<std::string> memStick;
|
||||
std::optional<std::string> stateToLoad;
|
||||
|
||||
// Headless: unpack the firmware inside an official updater EBOOT.PBP (given as the boot
|
||||
// filename) into this directory, then exit without booting anything.
|
||||
// See Core/Util/PSARUnpack.h - the API can also filter to a subfolder, which the command line
|
||||
// deliberately doesn't expose since the in-app use is specifically flash0:/font.
|
||||
std::optional<std::string> unpackUpdater;
|
||||
// Headless: which PSP model the unpacker resolves names against - "01g".."12g", or "any"
|
||||
// (the default) to take whatever file list names each file first.
|
||||
std::optional<std::string> unpackUpdaterModel;
|
||||
|
||||
std::optional<int> memReadAction;
|
||||
std::optional<int> memWriteAction;
|
||||
std::optional<int> breakAction;
|
||||
|
||||
@@ -917,6 +917,7 @@
|
||||
<ClCompile Include="Util\PathUtil.cpp" />
|
||||
<ClCompile Include="Util\PortManager.cpp" />
|
||||
<ClCompile Include="Util\PPGeDraw.cpp" />
|
||||
<ClCompile Include="Util\PSARUnpack.cpp" />
|
||||
<ClCompile Include="..\ext\xxhash.c">
|
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">MaxSpeed</Optimization>
|
||||
<IntrinsicFunctions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</IntrinsicFunctions>
|
||||
@@ -1280,6 +1281,7 @@
|
||||
<ClInclude Include="Util\PathUtil.h" />
|
||||
<ClInclude Include="Util\PortManager.h" />
|
||||
<ClInclude Include="Util\PPGeDraw.h" />
|
||||
<ClInclude Include="Util\PSARUnpack.h" />
|
||||
<ClInclude Include="..\ext\xxhash.h" />
|
||||
<ClInclude Include="Util\RecentFiles.h" />
|
||||
<ClInclude Include="Util\VideoPlayer.h" />
|
||||
|
||||
@@ -363,6 +363,9 @@
|
||||
<ClCompile Include="Util\PPGeDraw.cpp">
|
||||
<Filter>Util</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Util\PSARUnpack.cpp">
|
||||
<Filter>Util</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="HLE\sceFont.cpp">
|
||||
<Filter>HLE\Libraries</Filter>
|
||||
</ClCompile>
|
||||
@@ -1674,6 +1677,9 @@
|
||||
<ClInclude Include="Util\PPGeDraw.h">
|
||||
<Filter>Util</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Util\PSARUnpack.h">
|
||||
<Filter>Util</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="HLE\sceFont.h">
|
||||
<Filter>HLE\Libraries</Filter>
|
||||
</ClInclude>
|
||||
|
||||
@@ -284,12 +284,24 @@ static const u32 g_key_INDEXDAT1xx[] = {
|
||||
0xA34D8C80, 0x962B235D, 0x3E420548, 0x09CF9FFE, 0xD4883F5C, 0xD90E9CB5,
|
||||
0x00AEF4E9, 0xF0886DE9, 0x62A58A5B, 0x52A55546, 0x971941B5, 0xF5B79FAC};
|
||||
|
||||
// The blocks inside an official updater's DATA.PSAR carry this tag. See Core/Util/PSARUnpack.cpp.
|
||||
static const u32 g_keyUPDATER_PSAR[] = {
|
||||
0x77B757DE, 0xEE62DD17, 0x5D03787B, 0x59CA8644, 0x93F68D20, 0x21819328,
|
||||
0x86A74E71, 0x1B2482CA, 0x5F74AE58, 0x568D016C, 0x9A4D8832, 0x2EA24372,
|
||||
0x820CF484, 0xFCFC06B9, 0x8A5BFB6A, 0xBF9F9CD7, 0x15850D01, 0x39ED5FBA,
|
||||
0x4CC38393, 0xED3ADEAF, 0x1AA768BF, 0x89BD8A77, 0x46564165, 0x7333DBD9,
|
||||
0x62E86C81, 0x03299B96, 0x73AFAE5A, 0x40A05320, 0x10664BE8, 0xE5B76A99,
|
||||
0x29E0DD70, 0xEA602428, 0x2042AE30, 0x946F8D32, 0xA29E5F71, 0x7C0C7FD5};
|
||||
|
||||
struct TAG_INFO
|
||||
{
|
||||
u32 tag; // 4 byte value at offset 0xD0 in the PRX file
|
||||
const u32 *key; // "step1_result" use for XOR step
|
||||
u8 code;
|
||||
u8 codeExtra;
|
||||
// Most keys here are stored already scrambled, so the kirk7 pass the hardware would do is
|
||||
// skipped. Set this for a key that's stored raw and needs that pass applied first.
|
||||
bool scrambleKey;
|
||||
};
|
||||
|
||||
static const TAG_INFO g_tagInfo[] =
|
||||
@@ -310,7 +322,8 @@ static const TAG_INFO g_tagInfo[] =
|
||||
{ 0x862648D1, g_keyMEIMG260, 0x52, 0x52 },
|
||||
{ 0x207BBF2F, g_keyUNK1, 0x5A, 0x5A },
|
||||
{ 0x09000000, g_key_GAMESHARE1xx, 0x4C },
|
||||
{ 0xBB67C59F, g_key_GAMESHARE2xx, 0x5E, 0x5E }
|
||||
{ 0xBB67C59F, g_key_GAMESHARE2xx, 0x5E, 0x5E },
|
||||
{ 0x0E000000, g_keyUPDATER_PSAR, 0x51, 0x00, true }
|
||||
};
|
||||
|
||||
static const TAG_INFO *GetTagInfo(u32 tagFind)
|
||||
@@ -726,6 +739,11 @@ static int pspDecryptType0(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz
|
||||
// normally this would be a kirk7 op, but we have the seed pre-decrypted
|
||||
std::array<u8, 0x90> xorbuf;
|
||||
memcpy(xorbuf.data(), reinterpret_cast<const u8 *>(pti->key), xorbuf.size());
|
||||
if (pti->scrambleKey)
|
||||
{
|
||||
// ...except for keys we have in their raw form, which need that pass after all.
|
||||
kirk7(xorbuf.data(), xorbuf.data(), xorbuf.size(), pti->code);
|
||||
}
|
||||
|
||||
// construct the header format for a type 0 prx
|
||||
PRXType0 type0(inbuf);
|
||||
|
||||
+4
-1
@@ -221,7 +221,10 @@ IdentifiedFileType Identify_File(FileLoader *fileLoader, std::string *errorStrin
|
||||
return IdentifiedFileType::ARCHIVE_7Z;
|
||||
}
|
||||
|
||||
if (id == 'FLE\x7F') {
|
||||
// "~PSP" is an encrypted PRX. The module loader decrypts those on the way in, so as far as
|
||||
// identification goes it's the same thing as a plain ELF - which is how the modules in a
|
||||
// firmware unpacked straight from an updater arrive (see Core/Util/PSARUnpack.cpp).
|
||||
if (id == 'FLE\x7F' || id == 'PSP~') {
|
||||
Path filename = fileLoader->GetPath();
|
||||
// There are a few elfs misnamed as pbp (like Trig Wars), accept that. Also accept extension-less paths.
|
||||
if (extension == ".plf" || strstr(filename.GetFilename().c_str(), "BOOT.BIN") ||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2026- PPSSPP Project.
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, version 2.0 or later versions.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License 2.0 for more details.
|
||||
|
||||
// A copy of the GPL 2.0 should have been included with the program.
|
||||
// If not, see http://www.gnu.org/licenses/
|
||||
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
class Path;
|
||||
|
||||
// Unpacks the firmware image inside an official PSP updater (PSP/GAME/UPDATE/EBOOT.PBP).
|
||||
//
|
||||
// The updater's DATA.PSAR is a flat sequence of encrypted, compressed file records. The real
|
||||
// updater installs them by executing on the PSP, but the archive can be walked directly - each
|
||||
// record carries its own name, sizes and a standard PRX-style encryption header, so all it needs
|
||||
// is the KIRK engine and the PRX decrypter we already have.
|
||||
//
|
||||
// The main use is pulling flash0:/font out of an updater the user supplies, which is why a
|
||||
// prefix filter is part of the API rather than something the caller filters afterwards.
|
||||
|
||||
enum class PSARCompression {
|
||||
None,
|
||||
Zlib,
|
||||
KL4E,
|
||||
KL3E,
|
||||
LZR,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
const char *PSARCompressionToString(PSARCompression c);
|
||||
|
||||
// PSP hardware revisions, as the updater numbers them. An updater carries one file list per
|
||||
// model, so which one you resolve names against decides both what a file is called and whether
|
||||
// it's part of that model's firmware at all.
|
||||
enum class PSPModelGeneration {
|
||||
Any = 0, // Whichever list names a file first. Use this to extract everything.
|
||||
PSP_1000 = 1,
|
||||
PSP_2000 = 2,
|
||||
PSP_3000 = 3,
|
||||
PSP_4000 = 4,
|
||||
PSP_N1000 = 5, // PSP Go
|
||||
PSP_6000 = 6,
|
||||
PSP_7000 = 7,
|
||||
PSP_9000 = 9,
|
||||
PSP_11000 = 11,
|
||||
MAX = 12,
|
||||
};
|
||||
|
||||
const char *PSPModelGenerationToString(PSPModelGeneration generation);
|
||||
// Accepts "01g".."12g", a bare number, or "any". Returns false if it's none of those.
|
||||
bool PSPModelGenerationFromString(std::string_view name, PSPModelGeneration *generation);
|
||||
|
||||
struct PSARUnpackOptions {
|
||||
// Only unpack entries whose name starts with this, e.g. "flash0:/font/". Case insensitive.
|
||||
// Empty means everything. Entries whose real name we can't recover are never matched by a
|
||||
// non-empty filter.
|
||||
std::string prefixFilter;
|
||||
// Which model's file list to resolve names against. Anything that model's list doesn't name
|
||||
// isn't part of its firmware, and is skipped.
|
||||
PSPModelGeneration model = PSPModelGeneration::Any;
|
||||
// Walk and report, but don't write any files.
|
||||
bool listOnly = false;
|
||||
// Log a line per entry. Off by default - an updater holds well over a thousand of them.
|
||||
bool verbose = false;
|
||||
};
|
||||
|
||||
struct PSARUnpackStats {
|
||||
std::string firmwareVersion;
|
||||
int entries = 0;
|
||||
int directories = 0;
|
||||
int written = 0;
|
||||
int skippedByFilter = 0;
|
||||
int nameTables = 0; // Entries that were file lists rather than files.
|
||||
int unnamed = 0; // Short names no file list claimed.
|
||||
int otherModel = 0; // Files that belong to a model other than the requested one.
|
||||
int failed = 0;
|
||||
// How many entries used each compression, indexed by PSARCompression.
|
||||
int compressionCounts[6]{};
|
||||
};
|
||||
|
||||
// psar points at the DATA.PSAR contents, starting with the "PSAR" magic.
|
||||
bool UnpackPSAR(const u8 *psar, size_t psarSize, const Path &outputDir, const PSARUnpackOptions &options, PSARUnpackStats *stats, std::string *error);
|
||||
|
||||
// Finds the firmware image in whatever an updater arrives as and unpacks it:
|
||||
// - a downloaded updater EBOOT.PBP, where the archive is its DATA.PSAR
|
||||
// - a disc updater's DATA.BIN, which is the same archive without the PBP wrapper
|
||||
// - a game ISO/CSO/CHD, which is searched for PSP_GAME/SYSDIR/UPDATE/DATA.BIN
|
||||
// The last one is the interesting case: most UMDs carry a firmware updater, so the fonts can come
|
||||
// from whatever game the user already has rather than a separate download.
|
||||
bool UnpackUpdater(const Path &filename, const Path &outputDir, const PSARUnpackOptions &options, PSARUnpackStats *stats, std::string *error);
|
||||
|
||||
// The version string an updater advertises ("6.61"), read from the PARAM.SFO next to it - no
|
||||
// decryption needed, so it's cheap enough to check every disc with. Empty if there's no updater.
|
||||
std::string ReadUpdaterVersion(const Path &filename);
|
||||
|
||||
// The same three, for the disc mounted as disc0: - i.e. the game that's running. These read
|
||||
// through the mounted filesystem instead of opening the image a second time, which also means
|
||||
// they work for the shapes that aren't an image at all, like a folder-based "disc".
|
||||
//
|
||||
// This is the path the font extraction is meant to take: while a game is running, ask whether its
|
||||
// disc carries an updater, and if so unpack just flash0:/font out of it.
|
||||
bool MountedDiscHasUpdater();
|
||||
std::string ReadMountedDiscUpdaterVersion();
|
||||
bool UnpackUpdaterFromMountedDisc(const Path &outputDir, const PSARUnpackOptions &options, PSARUnpackStats *stats, std::string *error);
|
||||
@@ -332,6 +332,7 @@
|
||||
<ClInclude Include="..\..\Core\Util\DisArm64.h" />
|
||||
<ClInclude Include="..\..\Core\Util\GameManager.h" />
|
||||
<ClInclude Include="..\..\Core\Util\PPGeDraw.h" />
|
||||
<ClInclude Include="..\..\Core\Util\PSARUnpack.h" />
|
||||
<ClInclude Include="..\..\Core\WaveFile.h" />
|
||||
<ClInclude Include="..\..\ext\cityhash\city.h" />
|
||||
<ClInclude Include="..\..\ext\cityhash\citycrc.h" />
|
||||
@@ -646,6 +647,7 @@
|
||||
<ClCompile Include="..\..\Core\Util\DisArm64.cpp" />
|
||||
<ClCompile Include="..\..\Core\Util\GameManager.cpp" />
|
||||
<ClCompile Include="..\..\Core\Util\PPGeDraw.cpp" />
|
||||
<ClCompile Include="..\..\Core\Util\PSARUnpack.cpp" />
|
||||
<ClCompile Include="..\..\Core\WaveFile.cpp" />
|
||||
<ClCompile Include="..\..\ext\cityhash\city.cpp" />
|
||||
<ClCompile Include="..\..\ext\disarm.cpp" />
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
<ClCompile Include="..\..\Core\Util\DisArm64.cpp" />
|
||||
<ClCompile Include="..\..\Core\Util\GameManager.cpp" />
|
||||
<ClCompile Include="..\..\Core\Util\PPGeDraw.cpp" />
|
||||
<ClCompile Include="..\..\Core\Util\PSARUnpack.cpp" />
|
||||
<ClCompile Include="..\..\Core\WaveFile.cpp" />
|
||||
<ClCompile Include="..\..\ext\cityhash\city.cpp" />
|
||||
<ClCompile Include="..\..\ext\disarm.cpp" />
|
||||
@@ -662,6 +663,7 @@
|
||||
<ClInclude Include="..\..\Core\Util\DisArm64.h" />
|
||||
<ClInclude Include="..\..\Core\Util\GameManager.h" />
|
||||
<ClInclude Include="..\..\Core\Util\PPGeDraw.h" />
|
||||
<ClInclude Include="..\..\Core\Util\PSARUnpack.h" />
|
||||
<ClInclude Include="..\..\Core\WaveFile.h" />
|
||||
<ClInclude Include="..\..\ext\cityhash\city.h" />
|
||||
<ClInclude Include="..\..\ext\cityhash\citycrc.h" />
|
||||
|
||||
@@ -805,6 +805,7 @@ EXEC_AND_LIB_FILES := \
|
||||
$(SRC)/Core/Util/GameManager.cpp \
|
||||
$(SRC)/Core/Util/BlockAllocator.cpp \
|
||||
$(SRC)/Core/Util/PPGeDraw.cpp \
|
||||
$(SRC)/Core/Util/PSARUnpack.cpp \
|
||||
$(SRC)/Core/Util/RecentFiles.cpp \
|
||||
$(SRC)/Core/Util/VideoPlayer.cpp \
|
||||
$(SRC)/git-version.cpp
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
# The PSAR archive format (PSP firmware updaters)
|
||||
|
||||
An official PSP firmware updater carries the whole firmware image in a PSAR archive. PPSSPP can
|
||||
walk one and extract the files out of it without emulating the updater, which is what
|
||||
`Core/Util/PSARUnpack.cpp` does. The main use is getting `flash0:/font` - the PGF system fonts -
|
||||
out of an updater the user already has, since most UMDs carry one.
|
||||
|
||||
This document is what was learned reading 43 different firmware versions, from 1.50 to 6.61. It's
|
||||
a description of the format rather than of the code; the code is the same shape but has the error
|
||||
handling.
|
||||
|
||||
## Where an updater lives
|
||||
|
||||
The archive turns up in three shapes, all of them the same bytes once you find them:
|
||||
|
||||
| Shape | Where | Notes |
|
||||
| --- | --- | --- |
|
||||
| Downloaded updater | `EBOOT.PBP`, subfile 7 (`DATA.PSAR`) | The one you get from Sony's site |
|
||||
| Disc updater | `PSP_GAME/SYSDIR/UPDATE/DATA.BIN` | Not wrapped in a PBP - the bare archive |
|
||||
| Loose | any file starting with `PSAR` | Whatever someone already pulled out |
|
||||
|
||||
A disc updater directory also holds `EBOOT.BIN` (the updater executable, a bare `~PSP` PRX, of no
|
||||
use to us) and `PARAM.SFO`. That SFO is worth knowing about: its `TITLE` reads
|
||||
`"PSP™ Update ver 3.95"`, so **the firmware version can be read without decrypting anything**,
|
||||
which is cheap enough to check a whole game library with. `DISC_ID` is `MSTKUPDATE` and `CATEGORY`
|
||||
is `MG`.
|
||||
|
||||
Of 589 discs scanned across two libraries, 473 carried an updater.
|
||||
|
||||
## Archive header
|
||||
|
||||
```
|
||||
+0x00 u32 "PSAR" magic (0x52415350)
|
||||
+0x04 u8 version. 1 = "oldschool" (see below), 2 and 3 are the common ones
|
||||
+0x08 u32 total length of the records
|
||||
+0x20 u32 0x2C333333 if this archive is already decrypted, otherwise part of the ciphertext
|
||||
```
|
||||
|
||||
The length at +0x08 matters: archives have a few bytes of padding after the last record (16 and 32
|
||||
bytes in the two checked), and a walk that goes by file size instead will try to decode that
|
||||
padding as a record and report a spurious failure at the very end.
|
||||
|
||||
The marker at +0x20 only appears in archives some other tool has already decrypted. In an original
|
||||
updater those bytes are ciphertext and will not match.
|
||||
|
||||
## Records
|
||||
|
||||
The archive is a flat sequence of records starting at +0x10. There is no index - you find entry
|
||||
*n+1* by decoding entry *n* and adding up its sizes. Each record is:
|
||||
|
||||
```
|
||||
[ 0x150 bytes encryption header ] <- absent if the archive is pre-decrypted
|
||||
[ 0x110 bytes entry description ]
|
||||
[ variable file contents ] <- has its own 0x150 header, same as above
|
||||
```
|
||||
|
||||
Call the header size *overhead*: 0x150 normally, 0 for a pre-decrypted archive.
|
||||
|
||||
### Decoding a block
|
||||
|
||||
Every encrypted block - entry descriptions and file contents alike - is decoded the same way:
|
||||
|
||||
1. **Demangle.** The 0x130 bytes at block+0x20 are AES-128-CBC encrypted on top of everything
|
||||
else. Decrypt them with KIRK command 7 (`KIRK_CMD_DECRYPT_IV_0`), key seed **0x55**, IV zero,
|
||||
writing the result back over block+0x20. This step is skipped for version 1 archives.
|
||||
|
||||
The point of it is that it hides the PRX tag: you cannot tell how to decrypt the block until
|
||||
you have done this.
|
||||
|
||||
2. **PRX decrypt.** The block is now an ordinary PRX blob, with its tag at block+0xD0. Run it
|
||||
through the normal PRX decrypter (`pspDecryptPRX`). The tag official updaters use is
|
||||
**0x0E000000**; `0x06000000` also appears in the wild.
|
||||
|
||||
Note that `0x0E000000`'s key is stored *unscrambled*, unlike almost every other key in
|
||||
PPSSPP's tag table, so it needs the `kirk7` pass applied before use - that's what the
|
||||
`scrambleKey` flag on `TAG_INFO` is for.
|
||||
|
||||
One wrinkle worth knowing: the decrypter reads a little past the end of the block and **uses what
|
||||
it finds there**, so a decoder should hand it about 16 bytes of slack from the archive. The last
|
||||
record ends flush with the end of the archive and has no slack to give; zero-filling it there is
|
||||
fine, but zero-filling it everywhere breaks every archive that does have those bytes.
|
||||
|
||||
### Entry description
|
||||
|
||||
0x110 bytes once decoded:
|
||||
|
||||
```
|
||||
+0x004 char[] entry name, NUL padded (see "Naming" below)
|
||||
+0x100 u32 always zero. If it isn't, you've lost your place in the archive
|
||||
+0x104 u32 size of the contents block that follows, including its 0x150 header
|
||||
+0x108 u32 uncompressed size of the file. 0 means this entry is a directory
|
||||
```
|
||||
|
||||
A directory entry still has a contents block to skip over - advance by `+0x104` either way.
|
||||
|
||||
### The first two records
|
||||
|
||||
The first record (at +0x10) is not a file. Decoded, it holds a text string at +0x10 whose last
|
||||
comma-separated field is the firmware version, e.g. `...,6.61`. That's the authoritative version
|
||||
for choosing decryption keys further down.
|
||||
|
||||
There is a second record after it whose length isn't recorded anywhere. In practice it is
|
||||
`overhead + 100`, or `overhead + 144` on 2.7x, or `overhead + u16 at +0x90 of the first record`.
|
||||
Try them in that order and take the first that decodes. Round the result up to a multiple of 16
|
||||
before advancing. Version 1 archives don't have this record at all.
|
||||
|
||||
## Contents
|
||||
|
||||
The contents block decodes to a compressed stream. Sniff the first bytes:
|
||||
|
||||
| Magic | Format |
|
||||
| --- | --- |
|
||||
| `78 9C` | zlib |
|
||||
| `KL4E` | KL4E - Sony's own, not implemented in PPSSPP |
|
||||
| `KL3E` | KL3E - likewise |
|
||||
| `2RLZ` | LZR |
|
||||
|
||||
Across all 43 firmware versions examined, **every file in every archive was zlib**. KL4E does turn
|
||||
up on the PSP, but a layer further in: two kernel modules (`memlmd_01g.prx` and `loadexec_01g.prx`)
|
||||
are themselves KL4E-compressed *inside* their own PRX, which is a separate problem from this one.
|
||||
|
||||
## Naming
|
||||
|
||||
This is the part that changed most across firmware generations, and the part most likely to catch
|
||||
out an implementation tested against only one updater.
|
||||
|
||||
### 1.x and 2.x - real paths
|
||||
|
||||
Entries are named outright: `flash0:/vsh/resource/opening_plugin.rco`, `flash1:/registry`,
|
||||
`ipl:/psp_nandipl.bin`. Nothing else is needed. Note `ipl:` - it is not enough to recognise
|
||||
`flash0:` and `flash1:`; better to treat any `<device>:/` shape as a real path.
|
||||
|
||||
### 3.x - grouped short names
|
||||
|
||||
Entries are named `<group>:<5 digits>`, where the group is `com` for files every model gets, or
|
||||
`01g`, `02g`, ... for one model's. `<group>:00000` is that group's **file list**, and the rest of
|
||||
that group's entries are looked up in it by their number alone (entry `com:00004` is key `00004`).
|
||||
|
||||
### 5.x and 6.x - flat short names with per-model lists
|
||||
|
||||
Entries are a bare 5-digit number. The low numbers are file lists, one per PSP model
|
||||
(`00001` = 01g, `00002` = 02g, ...) and the rest are files that any of the lists may name.
|
||||
|
||||
**Which numbers are lists is not fixed.** 6.61 uses 1-11; 6.00 has real files at `00010`-`00012`.
|
||||
Since a list always decrypts successfully (the PRX layer inside validates a hash), the robust rule
|
||||
is: try to decrypt a candidate as a list, and if that fails, treat it as an ordinary file.
|
||||
|
||||
### File lists
|
||||
|
||||
A list decodes to plain text, one `shortname<separator>path` per line. Both the separator and the
|
||||
path style depend on the generation:
|
||||
|
||||
| Generation | Separator | Path written as |
|
||||
| --- | --- | --- |
|
||||
| 3.x | `\|` | `flash0/font/ltn0.pgf` |
|
||||
| 5.x, 6.x | `,` | `flash0:/font/ltn0.pgf` |
|
||||
|
||||
Worth normalising to one form early - otherwise a filter like `flash0:/font/` silently matches
|
||||
nothing on a 3.x archive.
|
||||
|
||||
### List encryption
|
||||
|
||||
The lists are encrypted on top of everything else, with **DES** - the tables in the reference
|
||||
implementations are DES's own: a 56-entry PC-1, a 48-entry PC-2, eight 64-entry 4-bit S-boxes, a
|
||||
32-entry P permutation, and textbook IP/FP bit-shuffling constants.
|
||||
|
||||
It is DES-CBC decrypt, with the key assembled from two 32-bit words as `(high << 32) | low` in
|
||||
big-endian byte order, and an IV alongside them. There is one such set per firmware series, and
|
||||
which one applies is chosen from the version string in the archive's first record:
|
||||
|
||||
| Firmware | Key set |
|
||||
| --- | --- |
|
||||
| 1.x, 2.x, 3.0 - 3.7 | 0 |
|
||||
| 3.8, 3.9 | 1 |
|
||||
| 4.x | 2 |
|
||||
| 5.x | 3 |
|
||||
| 6.x | 4 |
|
||||
|
||||
A sixth set exists that nothing selects by version. The values themselves are in `kTableKeys` in
|
||||
`Core/Util/PSARUnpack.cpp` and aren't repeated here.
|
||||
|
||||
Underneath the DES layer is an ordinary PRX blob, so run the result through `pspDecryptPRX` as
|
||||
well; what comes out of *that* is the text.
|
||||
|
||||
A good check while implementing: after the DES pass but before the PRX pass, the u32 at +0xD0
|
||||
should be a recognisable PRX tag. A wrong key gives a random value there, so that single word
|
||||
tells you whether the cipher is right before anything else has to work.
|
||||
|
||||
## What the archives look like in practice
|
||||
|
||||
Three structural eras, visible in the shape of the output:
|
||||
|
||||
| Firmware | Naming | Directory entries | Fonts |
|
||||
| --- | --- | --- | --- |
|
||||
| 1.50 - 3.52 | real paths | 0-16 | 17-20 |
|
||||
| 3.71 - 4.05 | `com:`/`NNg:` groups | 50 | 21 |
|
||||
| 5.01 - 6.61 | flat + per-model lists | 16-17 | 21 |
|
||||
|
||||
A 6.61 archive holds 435 records: 411 files, 17 directories and 7 file lists. Among the files are
|
||||
295 `~PSP` modules, 61 PRF files, 7 encrypted XMB indices (`PSPsysGP`), and 21 files in
|
||||
`flash0:/font` - 19 of them PGF, plus `gb3s1518.bwfon` and `imagefont.bin`.
|
||||
|
||||
The "Fonts" column above counts files in `flash0:/font`, not PGFs specifically.
|
||||
|
||||
## Gotchas, collected
|
||||
|
||||
- Walk by the length at +0x08, not the file size, or the trailing padding decodes as a bad record.
|
||||
- Stop when fewer than `overhead + 0x110` bytes remain rather than when the position passes the
|
||||
end - the two differ by exactly one spurious record.
|
||||
- Give the block decrypter its 16 bytes of slack, but don't require them for the last record.
|
||||
- Don't assume which entry numbers are file lists; find out by trying.
|
||||
- Don't assume there are file lists at all - 1.x and 2.x have none.
|
||||
- `ipl:` is a device too.
|
||||
- A 3.x path has no colon; a 6.x path does.
|
||||
|
||||
## See also
|
||||
|
||||
- `Core/Util/PSARUnpack.{cpp,h}` - the implementation, and the only consumer of most of this.
|
||||
- `Core/ELF/PrxDecrypter.cpp` - the PRX layer every block goes through.
|
||||
- `ext/libkirk` - `kirk7()` is the demangle step.
|
||||
- `PPSSPPHeadless --unpack-updater=DIR <updater|disc|PSAR>` unpacks one from the command line;
|
||||
`--unpack-updater-model=01g..12g` picks a model.
|
||||
@@ -57,6 +57,7 @@
|
||||
#include "Core/EmuThread.h"
|
||||
#include "Core/MIPS/MIPSTables.h"
|
||||
#include "Core/System.h"
|
||||
#include "Core/Util/PSARUnpack.h"
|
||||
#include "Core/WebServer.h"
|
||||
#include "Core/HLE/sceUtility.h"
|
||||
#include "Core/SaveState.h"
|
||||
@@ -612,6 +613,40 @@ int main(int argc, const char* argv[]) {
|
||||
g_logManager.EnableOutput(LogOutput::Printf);
|
||||
}
|
||||
|
||||
// Unpacking an updater is a standalone action - no emulation involved, so do it here and exit
|
||||
// before any of the setup below.
|
||||
if (cmdLineOptions.unpackUpdater.has_value()) {
|
||||
if (cmdLineOptions.bootFilenames.size() != 1) {
|
||||
fprintf(stderr, "--unpack-updater takes exactly one updater, disc image or PSAR\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
PSARUnpackOptions unpackOptions;
|
||||
unpackOptions.verbose = testOptions.verbose;
|
||||
if (cmdLineOptions.unpackUpdaterModel.has_value() &&
|
||||
!PSPModelGenerationFromString(cmdLineOptions.unpackUpdaterModel.value(), &unpackOptions.model)) {
|
||||
fprintf(stderr, "Unknown PSP model '%s' - expected 01g..12g or any\n", cmdLineOptions.unpackUpdaterModel.value().c_str());
|
||||
return 1;
|
||||
}
|
||||
PSARUnpackStats stats;
|
||||
std::string unpackError;
|
||||
const bool ok = UnpackUpdater(Path(cmdLineOptions.bootFilenames[0]), Path(cmdLineOptions.unpackUpdater.value()), unpackOptions, &stats, &unpackError);
|
||||
if (!ok) {
|
||||
fprintf(stderr, "Unpacking failed: %s\n", unpackError.c_str());
|
||||
}
|
||||
printf("Firmware %s (model %s): %d entries, %d files written, %d directories, %d unresolved names, %d for other models, %d failed\n",
|
||||
stats.firmwareVersion.c_str(), PSPModelGenerationToString(unpackOptions.model), stats.entries, stats.written,
|
||||
stats.directories, stats.unnamed, stats.otherModel, stats.failed);
|
||||
printf("Compression: none=%d zlib=%d KL4E=%d KL3E=%d LZR=%d unknown=%d\n",
|
||||
stats.compressionCounts[(int)PSARCompression::None],
|
||||
stats.compressionCounts[(int)PSARCompression::Zlib],
|
||||
stats.compressionCounts[(int)PSARCompression::KL4E],
|
||||
stats.compressionCounts[(int)PSARCompression::KL3E],
|
||||
stats.compressionCounts[(int)PSARCompression::LZR],
|
||||
stats.compressionCounts[(int)PSARCompression::Unknown]);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
g_Config.RestoreDefaults(RestoreSettingsBits::SETTINGS | RestoreSettingsBits::CONTROLS | RestoreSettingsBits::RECENT, false);
|
||||
|
||||
Core_RegisterDebugOutputListeners(&SendDebugOutput, &SendDebugScreenshot);
|
||||
|
||||
@@ -895,6 +895,7 @@ SOURCES_CXX += \
|
||||
$(COREDIR)/Util/BlockAllocator.cpp \
|
||||
$(COREDIR)/Util/MemStick.cpp \
|
||||
$(COREDIR)/Util/PPGeDraw.cpp \
|
||||
$(COREDIR)/Util/PSARUnpack.cpp \
|
||||
$(COREDIR)/Util/RecentFiles.cpp \
|
||||
$(COREDIR)/Util/AudioFormat.cpp \
|
||||
$(COREDIR)/Util/PathUtil.cpp \
|
||||
|
||||
Reference in new issue
Block a user