mirror of
https://github.com/deepkicks/TitaniaPy.git
synced 2026-10-17 16:00:31 +02:00
Add ioctl map from ps5-pup-decrypt source, improve approach 3 and auto-analysis - Mapped all 6 ioctl codes for /dev/pup_update0 from the on-console ps5-pup-decrypt encryptsrv.c source (VerifyBlsHeader through DecryptSegmentBlock) - Discovered type dispatch: DecryptHeader takes a type arg (0/1/2) that selects which key to use. PS5UPDATE1.PUP = type 1 (system update key) - analyze_dump.py now detects all 6 ioctl codes and type dispatch patterns (cmp 0/1/2) in handler disassembly, labels them in output - ps5_key_extractV2.c approach 3 now actually invokes DecryptHeader ioctl with type=1 between LSB snapshots instead of skipping the call - Added automated capstone disassembly engine to analyze_dump.py with --save-handlers flag for manual Ghidra/IDA fallback - Updated RESEARCH_POST.md with ioctl table and type dispatch finding - Updated README.md and EXTRACTION_GUIDE.md for automated analysis flow
220 lines
16 KiB
Markdown
220 lines
16 KiB
Markdown
# I Built a PC-Based PS5 PUP Decrypt Tool — Need Someone to Dump the EncryptService Key So It Works Forever
|
||
|
||
**TL;DR:** I built a Python tool that can parse and partially decrypt PS5 PUP files on PC — no console needed. It handles BLS containers, PUP structure analysis, and inner firmware decryption with known keys. I confirmed PBKDF2-HMAC-SHA256 as the Layer 0 derivation algorithm (exact match to Titania Rev 2 master key). Then I exhaustively tested every known key and derivation chain against real PUPs from **two different firmware versions (14.00 and 14.10)** and hit a wall: **the PUP container encryption key is not in the public key dump.** It lives inside the Titania Security Processor, used by `encsrv_decrypt_header()`. The key is almost certainly **burned into ROM or silicon** — it's universal across all consoles and has been there since FW 1.0. That means extracting it once = permanent solution for every firmware version, past and future. **I need someone with console access to either dump the EncryptService software or peek inside the Security Processor to find what key `encsrv_decrypt_header()` actually uses.**
|
||
|
||
---
|
||
|
||
## Background
|
||
|
||
After the PS5 bootrom key leak (Dec 31, 2025), a significant amount of key material became publicly documented — ROM key blocks, keyseeds (Key 2-9), OTP/KDF parameters, Titania master keys, and inner firmware AES keys. These are cataloged on psdevwiki.
|
||
|
||
The existing on-console tool (`ps5-pup-decrypt`) works by calling the PS5's EncryptService via ioctl, meaning it requires a jailbroken console. No PC-based PUP decryption tool existed publicly. I don't have a PS5, so I set out to build a tool that works entirely on PC — something designed to last. The architecture is modular: once the missing key is found, it drops into the tool and every PUP from every firmware version becomes decryptable without a console.
|
||
|
||
## What I Built
|
||
|
||
A Python CLI tool (`pup-decrypt-tool`) that handles the layers we have keys for:
|
||
|
||
- **BLS Parser** — Parses the outer SLB2 container (magic `0x32424C53`), extracts entry metadata
|
||
- **PUP Parser** — Reads the inner PUP structure (magic `0xEEF51454`), segment table, flags
|
||
- **Layer 3 Decryptor** — AES-128-CBC decryption of inner firmware blobs using known static keys (EMC IPL header/body, EAP KBL, EAP Kernel, EAP Kernel SELF, Titania Rev 2)
|
||
- **Key Derivation Module** — PBKDF2 + KMB_AES chain replication for research
|
||
- **Bruteforce Mode** — Tries all known keys against unknown blobs, uses Shannon entropy to score results
|
||
|
||
Requires only Python 3 + pycryptodome. No console needed.
|
||
|
||
## Finding 1: PBKDF2 Layer 0 Derivation Confirmed
|
||
|
||
The psdevwiki documents OTP_AES_ROOT, KDF_SALT1, and 1000 iterations as KDF parameters, but doesn't specify the hash function or output length.
|
||
|
||
I tested PBKDF2-HMAC with SHA-256, SHA-1, and SHA-512 at 16-byte and 32-byte output lengths:
|
||
|
||
| Hash | dklen | Match? |
|
||
|------|-------|--------|
|
||
| SHA-256 | 16 | **YES — exact match to Titania Rev 2 master key** |
|
||
| SHA-256 | 32 | First 16 bytes match (as expected) |
|
||
| SHA-1 | 16 | No |
|
||
| SHA-512 | 16 | No |
|
||
|
||
**Conclusion:** Layer 0 is PBKDF2-HMAC-SHA256 with 16-byte output. This is the algorithm that derives the Titania Rev 2 master key from OTP_AES_ROOT + KDF_SALT1.
|
||
|
||
## Finding 2: Real PUP Structure (14.00 and 14.10)
|
||
|
||
Tested against two retail PS5UPDATE.PUP files:
|
||
- **FW 14.00** — 1,260,600,832 bytes, BLS entry size 0x4B2339D5
|
||
- **FW 14.10** — 1,261,408,256 bytes, BLS entry size 0x4B2F8A3D
|
||
|
||
**BLS Container (plaintext):**
|
||
- Both: Version 3, 1 inner entry (`PS5UPDATE1.PUP`), offset 0x400
|
||
|
||
**PUP File Header (first 16 bytes — plaintext):**
|
||
- Both firmwares have **identical** plaintext headers:
|
||
- Magic: `0xEEF51454` (valid)
|
||
- Field at offset 0x04: `0x32010110` (likely format version, not firmware version)
|
||
- Header size: `0x5B0` (1456 bytes) — implies ~44 segments at 32 bytes each
|
||
- Hash/signature table size: `0xDE0` (3552 bytes)
|
||
|
||
**Encrypted content comparison (14.00 vs 14.10):**
|
||
- 256 out of 256 bytes differ in the encrypted extended header — completely different ciphertext
|
||
- Confirms: different firmware content, but same encryption scheme and key hierarchy
|
||
- All encrypted regions: ~7.15-7.29 bits/byte entropy (near max 8.0) — confirmed encryption, not compression
|
||
|
||
## Finding 3: Layer 1 Gap — Exhaustive Negative Result (Both Firmwares)
|
||
|
||
This is the main research contribution. I replicated the KMB_AES derivation chain documented on psdevwiki and tested every resulting key against the encrypted PUP headers **from both 14.00 and 14.10** simultaneously.
|
||
|
||
**Derivation chain tested:**
|
||
|
||
```
|
||
PBKDF2 master (slot 0) ──┬── CBC(KEY_4_KEYSEEDS) → slot 4
|
||
├── CBC(KEY_5_KEYSEEDS) → slot 5
|
||
├── CBC(KEY_7_KEYSEEDS) → slot 7
|
||
│ ├── ECB(KEY_8_KEYSEEDS) → slot 8
|
||
│ └── ECB(KEY_9_KEYSEEDS) → slot 9
|
||
└── CBC/ECB(KEY_2_KEYSEEDS) → slot 2
|
||
```
|
||
|
||
For each derived key, tested both first-16 and last-16 bytes of 32-byte results, in CBC, ECB, and CTR modes, with zeroed IV and all known Titania IVs.
|
||
|
||
**Full 8-phase scan (all negative on BOTH firmwares):**
|
||
1. All 17 known static keys × 6 IV variants × 3 modes (CBC/ECB/CTR)
|
||
2. KMB_AES derivation chain: slots 4, 5, 7, 8, 9 (both halves, key+IV combos)
|
||
3. ROM key blocks: every 16-byte window + derived chains through key7 → key8/key9
|
||
4. XOR combinations: master^OTP, master^SALT, OTP^SALT, master^ROM, master^K8/K9
|
||
5. AES-256 with all 32-byte keys (ROM windows, PBKDF2-256, keyseeds raw)
|
||
6. PBKDF2 variants: SHA-256/SHA-1/SHA-512, 16+32 byte output, header-mixed salts
|
||
7. Sliding window: every possible 16-byte key from all keyseed material concatenated (257 windows × 2 PUPs × 2 modes)
|
||
8. AES encrypt (reversed direction) with master, EMC header key, and K8 keyseed
|
||
|
||
**Validation method:** Decrypted the first 32 bytes of each PUP's extended header and checked if `file_size` falls within ~1MB of the known BLS entry size and `segment_count` is between 1-99.
|
||
|
||
**Result: Zero matches across hundreds of combinations on both firmware versions.**
|
||
|
||
## What This Means
|
||
|
||
The PS5 crypto architecture has (at least) these distinct layers:
|
||
|
||
| Layer | Purpose | Status |
|
||
|-------|---------|--------|
|
||
| 0 | PBKDF2 master key derivation | **Solved** — SHA-256, 16-byte output |
|
||
| 1 | PUP container encryption (header + segments) | **Blocked** — key not in public dump |
|
||
| 2 | PUP segment-level encryption | **Blocked** — depends on Layer 1 |
|
||
| 3 | Inner firmware encryption (EMC, EAP, etc.) | **Solved** — static keys known |
|
||
|
||
The bootrom key leak provided Layer 0 parameters and Layer 3 static keys. The KMB_AES keyseeds (Key 2-9) appear to be for Layer 3 inner firmware operations, **not** for Layer 1 PUP container crypto.
|
||
|
||
The `encsrv_decrypt_header()` ioctl (0xC0184402) used by the on-console tool accesses a key that is:
|
||
- Universal (all consoles decrypt the same PUP, so it's not per-console)
|
||
- Not in the public key dump
|
||
- Likely locked in the security processor's CCP hardware key slots
|
||
- Separate from the bootrom key hierarchy
|
||
|
||
## Why This Key Is the Nail in the Coffin
|
||
|
||
Here's why finding this one key is worth the effort:
|
||
|
||
**The PUP container key is almost certainly burned into ROM or silicon.** Every PS5 ever made — launch day consoles running FW 1.0 through the latest — must be able to decrypt every PUP Sony distributes. There's no per-console provisioning for system updates. The key has to be:
|
||
- **Universal** — identical on every console
|
||
- **Permanent** — present since the very first firmware, can't be rotated without bricking existing consoles
|
||
- **Unpatchable** — if it's in ROM/silicon (which the evidence strongly suggests), Sony can't change it even if they wanted to
|
||
|
||
Testing across two firmware versions (14.00 and 14.10) confirms this: both use identical plaintext header structures (`unknown_04=0x32010110`, `header_size=0x5B0`, `hash_size=0xDE0`) despite having completely different encrypted content. Same key hierarchy, same format — just different payloads.
|
||
|
||
**Once extracted, this key works forever.** It decrypts every past PUP and every future PUP. It's not like a signing key that can be rotated — it's a decryption key baked into hardware that already shipped. A PC tool with this key makes on-console decryption unnecessary for all time.
|
||
|
||
## What I Need — The Ask
|
||
|
||
The tool is one key away from being a complete, permanent PC-based PUP decryptor. That key lives inside the Titania Security Processor and is used by `encsrv_decrypt_header()` (ioctl `0xC0184402`). It's universal — every PS5 uses the same one — so it only needs to be extracted once.
|
||
|
||
**If you have a jailbroken PS5, here's what would crack this open:**
|
||
|
||
1. **Dump the EncryptService** — Extract the code behind the `/dev/pup_update0` ioctls so we can see how it selects and uses the PUP header key. Even a disassembly of the handler for `0xC0184402` would tell us which key slot it reads from.
|
||
|
||
2. **Peek inside the Security Processor during a PUP update** — Dump the CCP LSB slot contents while `encsrv_decrypt_header()` is running. The key is loaded into a hardware slot before the AES operation — if we can read the slots at that moment, we have it.
|
||
|
||
3. **Reverse engineer the Titania firmware** — The security processor firmware contains the key hierarchy. Deeper RE of how it handles PUP operations would reveal where the container key sits relative to the keys we already know.
|
||
|
||
Any one of these gives us the missing piece. Once that key is in the tool, it works for every PUP on every firmware — no console needed, forever.
|
||
|
||
## I Already Built the Extraction Payload
|
||
|
||
To make this as easy as possible for anyone with a jailbroken 13.60 PS5, I wrote a ready-to-compile C payload (`ps5_key_extract.c` v2) that implements a **chained extraction strategy**. The key insight: Byepervisor Reborn gives kernel + HV R/W, but **not** direct PSP memory access. So we can't just dump the security processor — we need to be smarter.
|
||
|
||
### The Chained Strategy: Approach 2 → Approach 1
|
||
|
||
**Step 1 — Dump the handler code (approach 2, needs only kernel R/W):**
|
||
The payload finds the `pup_update0` device driver's `cdevsw` struct in kernel memory and dumps 16KB each of:
|
||
- `d_ioctl` handler — the function behind `DecryptPupHeader` (ioctl `0xC0184402`)
|
||
- `d_open` handler — key loading may happen when the device is opened
|
||
- `d_close` handler — cleanup code that may reference the key
|
||
|
||
Transfer the dump to PC and run `analyze_dump.py`. It **automatically disassembles** the handler code using capstone, searches for kernel address references, CCP slot numbers, AES key size constants, and type-dispatch patterns, then outputs a ranked list of key source candidates with the exact `-a` command for a second payload run. No manual Ghidra work needed in most cases.
|
||
|
||
**Step 2 — Target what you found:**
|
||
Based on what the handler reveals:
|
||
|
||
| Handler shows | Next step |
|
||
|---|---|
|
||
| Key loaded from a kernel memory address | Re-run payload with `-a 0xADDRESS` to read it directly |
|
||
| Key loaded via CCP unwrap operation | Read the wrapped blob + wrapping key |
|
||
| Key pre-loaded in CCP slot at boot | Check approach 1 results for that slot |
|
||
| Key fetched from PSP via mailbox | Now you know the exact PSP address to target |
|
||
|
||
### All Five Approaches
|
||
|
||
1. **CCP LSB Key Slot Dump + Queue Registers** — Maps the AMD CCP v5 MMIO region, reads all 128 hardware key slots AND all queue configuration registers. The queue registers tell us which slots are CPU-readable (public mask) vs hardware-protected (private mask) — so we know immediately if direct reads are possible.
|
||
|
||
2. **Full EncryptService Handler Dump** — Dumps the complete `cdevsw` struct (all function pointers) plus 16KB of `d_ioctl`, `d_open`, and `d_close` handler code. This is the most reliable approach — it only needs kernel R/W, which Byepervisor Reborn provides.
|
||
|
||
3. **Instrumented PUP Decrypt** — Snapshots all CCP LSB slots before and after invoking the `DecryptHeader` ioctl (`0xC0184402`) with type=1 (system update key). Even if the decrypt fails on a dummy header, the security processor may still load the key into CCP slots — the pre/post diff catches it. Note: PSP may lock out after boot on newer firmware, making this less reliable.
|
||
|
||
4. **Kernel Memory Scan** — Searches near strings like `encsrv`, `decrypt_header`, `sceSblSsDecryptPupHeader`, `Titania` and dumps surrounding context.
|
||
|
||
5. **Targeted Address Read** — Re-run mode for after you've analyzed the handler dump on PC. Pass `-a 0xADDRESS` to read from a specific kernel or physical address. Automatically detects whether the address is virtual (uses `kernel_copyout`) or physical (uses MMIO mapping).
|
||
|
||
All operations are **read-only** — nothing is written to kernel or CCP state. Output goes to `/data/pup_key_dump.bin`, which you transfer to PC and run through the companion `analyze_dump.py` script. That script automatically parses all section types, displays CCP queue register masks, shows which slots are CPU-readable, tests every captured key candidate against encrypted PUP headers from FW 14.00 and 14.10, and then **auto-disassembles the handler code** to find where the key comes from.
|
||
|
||
### Key Discovery: Full Ioctl Map and Type Dispatch
|
||
|
||
By studying the `ps5-pup-decrypt` on-console tool source, I mapped the complete ioctl interface for `/dev/pup_update0`:
|
||
|
||
| Ioctl Code | Name | Args |
|
||
|---|---|---|
|
||
| `0xC0104401` | VerifyBlsHeader | buffer, length, unknown |
|
||
| `0xC0184402` | **DecryptHeader** | buffer, length, **type** |
|
||
| `0xC0184403` | VerifyPupAdditionalSign | index, buffer, length |
|
||
| `0xC0184404` | VerifyPupWatermark | index, buffer, length |
|
||
| `0xC0184405` | DecryptSegment | index, buffer, length |
|
||
| `0xC0284406` | DecryptSegmentBlock | entry_idx, block_idx, block_buf, block_len, table_buf, table_len |
|
||
|
||
The critical insight: `DecryptHeader` takes a **type** parameter (0, 1, or 2) that selects which key to use. `PS5UPDATE1.PUP` (system updates) = type 1, `PS5UPDATE2.PUP` (recovery) = type 0. This means the `d_ioctl` handler has a **dispatch branch** — `cmp type, 1` followed by a branch to the code that loads the system update key. Our automated disassembly specifically tracks this pattern and highlights it in the output.
|
||
|
||
**Requirements:** A PS5 on FW 13.60 or below with kernel R/W access (Byepervisor Reborn + Relapse + etaHEN or similar). Compile for FreeBSD 11 amd64 and load via payload loader. You'll need to plug in your framework's `kernel_read64` / `kernel_copyout` functions.
|
||
|
||
**Usage:**
|
||
```bash
|
||
# Full scan — run all approaches
|
||
./ps5_key_extract
|
||
|
||
# Targeted read — after RE on PC reveals the key address
|
||
./ps5_key_extract -a 0xFFFFFFFF82345678
|
||
./ps5_key_extract -a 0xFFFFFFFF82345678 -s 256
|
||
```
|
||
|
||
## Tool Availability
|
||
|
||
The tool works today for everything except the one missing key:
|
||
- Parse any PS5 PUP file's BLS container and show structure
|
||
- Estimate segment count from the plaintext header
|
||
- Decrypt inner firmware blobs if you have them extracted by other means
|
||
- Run the PBKDF2 derivation to verify/experiment with Layer 0
|
||
- Bruteforce-test unknown blobs against all known keys with entropy scoring
|
||
- Tested against real retail PUPs from FW 14.00 and FW 14.10
|
||
- **Ready to accept the Layer 1 key** — the crypto module just needs one more key added and the full pipeline connects
|
||
- **Extraction payload ready** — `ps5_key_extract.c` + `analyze_dump.py` for someone with console access
|
||
|
||
Source is published: **[GitHub repo link]** — drop in your PUP files and go. DMs open if you have console access and want to collaborate — this is a one-key problem with a permanent payoff.
|
||
|
||
---
|
||
|
||
*All research conducted on personally owned hardware for educational purposes. No proprietary key values are reproduced in this post — all referenced keys are already publicly documented on psdevwiki. This work builds on research by the PS5 security community, the dayzerosec CCP architecture writeup, and fail0verflow's PS4 Aeolia documentation.*
|