Files
deepkicks 9bc84bf840 Add files via upload
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
2026-10-15 04:01:25 -05:00

220 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.*