Add files via upload

This commit is contained in:
deepkicks authored and GitHub committed 2026-10-15 01:26:59 -05:00
1 parent 43726fbbe4
commit aec31c7b18
11 files changed
+2510 -2

No files matched your search

+95 -2
View File
@@ -1,2 +1,95 @@
# TitaniaPy
A Python CLI tool that parses, analyzes, and partially decrypts PS5 update (PUP) and BLS container files directly on PC—no console required. Features BLS metadata extraction, Layer 3 component decryption, PBKDF2/KMB derivation, and entropy-based key brute-forcing. Built as a modular framework ready for full PUP container decryption.
# PS5 PUP Decrypt Tool
PC-based Python tool for parsing and decrypting PS5 PUP (PlayStation Update Package) files. No console needed.
## Status
The tool handles 3 of 4 crypto layers. **Layer 1 (PUP container encryption) is the one missing piece** — see [RESEARCH_POST.md](RESEARCH_POST.md) for the full technical write-up and what's needed.
| Layer | Purpose | Status |
|-------|---------|--------|
| 0 | PBKDF2 master key derivation | Solved |
| 1 | PUP container encryption | **Blocked — key not public** |
| 2 | PUP segment-level encryption | Blocked (depends on Layer 1) |
| 3 | Inner firmware encryption | Solved |
## Setup
```bash
pip install -r requirements.txt
```
Requires Python 3.10+ and [pycryptodome](https://pypi.org/project/pycryptodome/).
## Usage
```bash
# Show PUP file structure (BLS container + PUP header)
python main.py info PS5UPDATE.PUP
# Extract inner entries from BLS container
python main.py extract PS5UPDATE.PUP -o output_dir/
# Decrypt inner firmware blobs with known Layer 3 keys
python main.py decrypt firmware_blob.bin -k emc_ipl_header
# Bruteforce-test an unknown blob against all known keys
python main.py bruteforce unknown_blob.bin
# Analyze entropy of a binary blob
python main.py analyze blob.bin
# Show all known keys and derivation results
python main.py keys
# Decompress a zlib-compressed segment
python main.py decompress segment.bin -o decompressed.bin
```
## Files
### Core Tool
| File | Description |
|------|-------------|
| `main.py` | CLI entry point — 7 commands |
| `keys.py` | All known PS5 keys (ROM, keyseeds, OTP/KDF, Titania, firmware) |
| `bls.py` | BLS (SLB2) outer container parser |
| `pup.py` | PUP inner structure parser |
| `crypto.py` | AES-128-CBC/ECB, PBKDF2, Layer 3 decrypt functions |
| `firmware.py` | Inner firmware decryptor with entropy analysis |
### Key Extraction (for jailbroken PS5)
| File | Description |
|------|-------------|
| `ps5_key_extract.c` | C payload — 4 approaches to extract the missing PUP container key |
| `analyze_dump.py` | Parses the dump from `ps5_key_extract.c` and tests key candidates |
### Documentation
| File | Description |
|------|-------------|
| `RESEARCH_POST.md` | Full technical write-up of findings |
## Key Extraction Payload
If you have a jailbroken PS5 on FW 13.60 or below, `ps5_key_extract.c` implements four extraction approaches:
1. **CCP LSB Key Slot Dump** — Reads all 128 hardware key slots via MMIO
2. **EncryptService ioctl Handler Scan** — Finds and dumps the `pup_update0` decrypt handler code
3. **Instrumented PUP Decrypt** — Snapshots key slots before/after a decrypt call to catch the key being loaded
4. **Kernel Memory Scan** — Searches near crypto-related strings for key material
All read-only. Output goes to `/data/pup_key_dump.bin`. Transfer to PC and run:
```bash
python analyze_dump.py pup_key_dump.bin --pup14 PS5UPDATE14.00.PUP --pup14_1 PS5UPDATE14.10.PUP
```
**Requirements:** FreeBSD 11 amd64 cross-compiler, kernel R/W via Byepervisor Reborn + Relapse + etaHEN or similar. You'll need to provide your framework's `kernel_read64` / `kernel_copyout` functions.
## Contributing
The tool is one key away from full PUP decryption. If you have console access and can run the extraction payload or dump the EncryptService, open an issue or DM.
---
*All research conducted on personally owned hardware for educational purposes. Referenced keys are publicly documented on psdevwiki.*
+170
View File
@@ -0,0 +1,170 @@
# 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`) that does all three of the above automatically. It implements four approaches:
1. **CCP LSB Key Slot Dump** — Maps the AMD CCP v5 MMIO region via `/dev/dmem0`, reads all 128 hardware key slots (8 LSBs x 16 slots x 32 bytes each). If the PUP key is in any slot, this grabs it.
2. **EncryptService ioctl Handler Scan** — Searches kernel memory for the `pup_update0` device driver's `cdevsw` structure, follows the `d_ioctl` function pointer, and dumps 4KB of the handler code. Disassemble on PC with Ghidra/IDA to find which CCP key slot the decrypt function references.
3. **Instrumented PUP Decrypt** — Snapshots all CCP LSB slots before and after a `DecryptPupHeader` ioctl call. Any slot that changes during the call = the key that was loaded for decryption.
4. **Kernel Memory Scan** — Searches near strings like `encsrv`, `decrypt_header`, `Titania`, `ccp_key` and dumps surrounding context.
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 tests every captured key candidate against encrypted PUP headers from FW 14.00 and 14.10.
**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.
## 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
Happy to publish the source on GitHub if there's interest. 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.*
+412
View File
@@ -0,0 +1,412 @@
"""
analyze_dump.py — Parse pup_key_dump.bin and test key candidates against PUP headers.
Reads the structured dump from ps5_key_extract.c, extracts all key candidates
from CCP LSB slots, and tries AES-128-CBC/ECB decryption against the encrypted
PUP headers from two firmware files.
Usage:
python analyze_dump.py pup_key_dump.bin
python analyze_dump.py pup_key_dump.bin --pup14 path/to/14.00.PUP --pup14_1 path/to/14.10.PUP
"""
import argparse
import struct
import sys
from pathlib import Path
try:
from Crypto.Cipher import AES
except ImportError:
print("ERROR: pycryptodome required. Install with: pip install pycryptodome")
sys.exit(1)
# --- Dump format constants (must match ps5_key_extract.c) ---
DUMP_MAGIC = 0x505550444D50 # "PUPDMP\0\0" as uint64 LE
SECTION_LSB = 0x01
SECTION_IOCTL = 0x02
SECTION_SNAP_PRE = 0x03
SECTION_SNAP_POST = 0x04
SECTION_MEMSCAN = 0x05
SECTION_NAMES = {
SECTION_LSB: "CCP LSB Key Slots",
SECTION_IOCTL: "ioctl Handler Code",
SECTION_SNAP_PRE: "Pre-Decrypt LSB Snapshot",
SECTION_SNAP_POST: "Post-Decrypt LSB Snapshot",
SECTION_MEMSCAN: "Kernel Memory Scan",
}
# LSB geometry
LSB_COUNT = 8
SLOTS_PER_LSB = 16
SLOT_SIZE = 32
LSB_TOTAL_SIZE = LSB_COUNT * SLOTS_PER_LSB * SLOT_SIZE # 4096
# PUP header constants
PUP_HEADER_OFFSET = 0x410
PUP_HEADER_READ_SIZE = 64 # read enough for testing
# Known BLS entry sizes for validation
PUP_14_00_BLS_SIZE = 0x4B2339D5
PUP_14_10_BLS_SIZE = 0x4B2F8A3D
# Default PUP file paths (set to your local paths or pass via --pup14 / --pup14_1)
DEFAULT_PUP_14_00 = "PS5UPDATE14.00.PUP"
DEFAULT_PUP_14_10 = "PS5UPDATE14.10.PUP"
def parse_dump(data: bytes) -> dict:
"""Parse the structured dump file."""
if len(data) < 16:
print(f"[!] Dump file too small ({len(data)} bytes)")
return None
magic, version, section_count = struct.unpack_from("<QII", data, 0)
if magic != DUMP_MAGIC:
print(f"[!] Bad magic: 0x{magic:012x} (expected 0x{DUMP_MAGIC:012x})")
return None
print(f"[+] Dump version {version}, {section_count} sections")
header_size = 16 # dump_header_t
section_hdr_size = 16 # section_header_t
max_sections = 32
section_table_offset = header_size
sections = []
for i in range(min(section_count, max_sections)):
off = section_table_offset + i * section_hdr_size
stype, ssize, soffset, _ = struct.unpack_from("<IIII", data, off)
name = SECTION_NAMES.get(stype, f"Unknown(0x{stype:02x})")
print(f" Section {i}: {name}, {ssize} bytes at offset 0x{soffset:x}")
if soffset + ssize <= len(data):
section_data = data[soffset:soffset + ssize]
else:
print(f" [!] Section data extends past end of file")
section_data = data[soffset:] if soffset < len(data) else b""
sections.append({
"type": stype,
"size": ssize,
"offset": soffset,
"data": section_data,
"name": name,
})
return {"version": version, "sections": sections}
def extract_key_candidates(sections: list) -> list:
"""Extract all possible 16-byte key candidates from LSB sections."""
candidates = []
seen = set()
for sec in sections:
if sec["type"] not in (SECTION_LSB, SECTION_SNAP_PRE, SECTION_SNAP_POST):
continue
data = sec["data"]
# Extract each 32-byte slot, then take the first 16 bytes as AES-128 key
total_slots = len(data) // SLOT_SIZE
for slot_idx in range(total_slots):
slot = data[slot_idx * SLOT_SIZE:(slot_idx + 1) * SLOT_SIZE]
if slot == b"\x00" * SLOT_SIZE:
continue
# AES-128 key = first 16 bytes of the slot
key16 = slot[:16]
key_hex = key16.hex()
if key_hex not in seen:
seen.add(key_hex)
candidates.append({
"key": key16,
"source": sec["name"],
"slot": slot_idx,
"full_slot": slot,
})
# Also try second 16 bytes (AES-128 key might be in upper half)
key16b = slot[16:32]
key_hex_b = key16b.hex()
if key_hex_b not in seen and key16b != b"\x00" * 16:
seen.add(key_hex_b)
candidates.append({
"key": key16b,
"source": sec["name"] + " (upper half)",
"slot": slot_idx,
"full_slot": slot,
})
# Also do sliding 16-byte window over all section data
for sec in sections:
data = sec["data"]
if len(data) < 16:
continue
for i in range(0, len(data) - 15, 4): # 4-byte aligned
window = data[i:i + 16]
if window == b"\x00" * 16:
continue
whex = window.hex()
if whex not in seen:
seen.add(whex)
candidates.append({
"key": window,
"source": f"{sec['name']} @ offset 0x{i:x}",
"slot": -1,
"full_slot": None,
})
return candidates
def find_lsb_diff(sections: list) -> list:
"""Find LSB slots that changed between pre and post snapshots."""
pre_data = None
post_data = None
for sec in sections:
if sec["type"] == SECTION_SNAP_PRE:
pre_data = sec["data"]
elif sec["type"] == SECTION_SNAP_POST:
post_data = sec["data"]
if pre_data is None or post_data is None:
return []
changed = []
min_len = min(len(pre_data), len(post_data))
total_slots = min_len // SLOT_SIZE
for slot_idx in range(total_slots):
pre_slot = pre_data[slot_idx * SLOT_SIZE:(slot_idx + 1) * SLOT_SIZE]
post_slot = post_data[slot_idx * SLOT_SIZE:(slot_idx + 1) * SLOT_SIZE]
if pre_slot != post_slot:
print(f"[!] LSB slot {slot_idx} CHANGED during decrypt:")
print(f" Before: {pre_slot.hex()}")
print(f" After: {post_slot.hex()}")
changed.append({
"slot": slot_idx,
"before": pre_slot,
"after": post_slot,
})
return changed
def read_pup_header(pup_path: str, offset: int = PUP_HEADER_OFFSET,
size: int = PUP_HEADER_READ_SIZE) -> bytes | None:
"""Read encrypted header bytes from a PUP file."""
path = Path(pup_path)
if not path.exists():
print(f"[!] PUP file not found: {pup_path}")
return None
file_size = path.stat().st_size
if file_size < offset + size:
print(f"[!] PUP file too small: {file_size} bytes")
return None
with open(path, "rb") as f:
f.seek(offset)
return f.read(size)
def validate_decrypted_header(decrypted: bytes, expected_bls_size: int,
pup_name: str) -> bool:
"""
Check if decrypted data looks like a valid PUP segment table entry.
Expected layout:
offset 0: uint64 LE — file_size (should be within 1MB of BLS entry size)
offset 8: uint16 LE — segment_count (should be 1-99)
"""
if len(decrypted) < 16:
return False
file_size = struct.unpack_from("<Q", decrypted, 0)[0]
segment_count = struct.unpack_from("<H", decrypted, 8)[0]
size_diff = abs(int(file_size) - int(expected_bls_size))
size_ok = size_diff < (1024 * 1024) # within 1MB
count_ok = 1 <= segment_count <= 99
if size_ok and count_ok:
print(f" [!!!] MATCH on {pup_name}!")
print(f" file_size=0x{file_size:x} (diff={size_diff} from BLS 0x{expected_bls_size:x})")
print(f" segment_count={segment_count}")
print(f" Full decrypted: {decrypted[:32].hex()}")
return True
return False
def test_key_against_pups(key: bytes, pup_headers: dict) -> bool:
"""Try a key with AES-128-CBC (zero IV) and AES-128-ECB on each PUP header."""
matched = False
for pup_name, (header_data, bls_size) in pup_headers.items():
if header_data is None:
continue
enc_block = header_data[:32] # first 32 bytes (2 AES blocks)
# AES-128-CBC with zero IV
try:
cipher = AES.new(key, AES.MODE_CBC, iv=b"\x00" * 16)
dec_cbc = cipher.decrypt(enc_block)
if validate_decrypted_header(dec_cbc, bls_size, f"{pup_name} (CBC)"):
matched = True
except (ValueError, KeyError):
pass
# AES-128-ECB
try:
cipher = AES.new(key, AES.MODE_ECB)
dec_ecb = cipher.decrypt(enc_block)
if validate_decrypted_header(dec_ecb, bls_size, f"{pup_name} (ECB)"):
matched = True
except (ValueError, KeyError):
pass
return matched
def hexdump_section(data: bytes, label: str, max_bytes: int = 256):
"""Print a hex dump of section data."""
print(f"\n--- {label} ---")
show = min(len(data), max_bytes)
for i in range(0, show, 16):
hex_part = " ".join(f"{b:02x}" for b in data[i:i + 16])
ascii_part = "".join(
chr(b) if 32 <= b < 127 else "." for b in data[i:i + 16]
)
print(f" {i:04x}: {hex_part:<48s} {ascii_part}")
if show < len(data):
print(f" ... ({len(data) - show} more bytes)")
def main():
parser = argparse.ArgumentParser(description="Analyze PS5 PUP key dump")
parser.add_argument("dumpfile", help="Path to pup_key_dump.bin")
parser.add_argument("--pup14", default=DEFAULT_PUP_14_00,
help="Path to PS5UPDATE14.00.PUP")
parser.add_argument("--pup14_1", default=DEFAULT_PUP_14_10,
help="Path to PS5UPDATE14.10.PUP")
args = parser.parse_args()
# Read dump file
dump_path = Path(args.dumpfile)
if not dump_path.exists():
print(f"[!] Dump file not found: {args.dumpfile}")
sys.exit(1)
print(f"[*] Reading dump: {args.dumpfile}")
dump_data = dump_path.read_bytes()
parsed = parse_dump(dump_data)
if parsed is None:
sys.exit(1)
# Display ioctl handler sections
for sec in parsed["sections"]:
if sec["type"] == SECTION_IOCTL:
hexdump_section(sec["data"], "ioctl Handler Code (DecryptPupHeader)")
elif sec["type"] == SECTION_MEMSCAN:
hexdump_section(sec["data"], f"Kernel Memory Scan Match")
# Check for LSB diff (approach 3)
changed_slots = find_lsb_diff(parsed["sections"])
if changed_slots:
print(f"\n[+] {len(changed_slots)} LSB slots changed during decrypt — high-priority candidates")
# Extract key candidates
candidates = extract_key_candidates(parsed["sections"])
print(f"\n[*] Extracted {len(candidates)} unique key candidates")
if not candidates:
print("[!] No key candidates found in dump. Possible issues:")
print(" - CCP LSB region not accessible via MMIO")
print(" - LSB contents cleared after use")
print(" - Key stored in PSP SRAM, not CCP LSB")
sys.exit(1)
# Read PUP headers
print(f"\n[*] Reading PUP headers...")
pup_headers = {}
header_14 = read_pup_header(args.pup14)
if header_14:
print(f" 14.00: {len(header_14)} bytes from offset 0x{PUP_HEADER_OFFSET:x}")
pup_headers["FW 14.00"] = (header_14, PUP_14_00_BLS_SIZE)
header_14_1 = read_pup_header(args.pup14_1)
if header_14_1:
print(f" 14.10: {len(header_14_1)} bytes from offset 0x{PUP_HEADER_OFFSET:x}")
pup_headers["FW 14.10"] = (header_14_1, PUP_14_10_BLS_SIZE)
if not pup_headers:
print("[!] No PUP files available for testing")
print("[*] Listing all key candidates instead:\n")
for i, cand in enumerate(candidates):
print(f" Candidate {i}: {cand['key'].hex()}")
print(f" Source: {cand['source']}, slot {cand['slot']}")
if cand['full_slot']:
print(f" Full: {cand['full_slot'].hex()}")
sys.exit(0)
# Test each candidate
print(f"\n[*] Testing {len(candidates)} candidates against {len(pup_headers)} PUP files...")
matches = []
for i, cand in enumerate(candidates):
if i > 0 and i % 100 == 0:
print(f" ... tested {i}/{len(candidates)}")
if test_key_against_pups(cand["key"], pup_headers):
matches.append(cand)
print(f" KEY FOUND: {cand['key'].hex()}")
print(f" Source: {cand['source']}, slot {cand['slot']}")
# Priority test: changed slots from approach 3
if changed_slots:
print(f"\n[*] Priority testing {len(changed_slots)} changed LSB slots...")
for cs in changed_slots:
for offset in (0, 16):
key = cs["after"][offset:offset + 16]
if key == b"\x00" * 16:
continue
print(f" Testing changed slot {cs['slot']} offset {offset}: {key.hex()}")
if test_key_against_pups(key, pup_headers):
matches.append({
"key": key,
"source": f"Changed LSB slot {cs['slot']} (post-decrypt)",
"slot": cs["slot"],
"full_slot": cs["after"],
})
# Report
print(f"\n{'=' * 60}")
if matches:
print(f"[+] FOUND {len(matches)} MATCHING KEY(S)!")
for m in matches:
print(f" Key: {m['key'].hex()}")
print(f" Source: {m['source']}")
else:
print("[*] No matching keys found.")
print("[*] Possible reasons:")
print(" - Key is AES-256, not AES-128")
print(" - Different cipher mode (CTR, XTS, etc.)")
print(" - Header structure differs from expected layout")
print(" - Key not present in dumped LSB data")
print("[*] Next steps:")
print(" - Try AES-256 with 32-byte slot data")
print(" - Reverse the ioctl handler code to find the exact algorithm")
print(" - Use approach 3 with real DMA memory for live capture")
print(f"{'=' * 60}")
if __name__ == "__main__":
main()
+98
View File
@@ -0,0 +1,98 @@
"""
BLS (SLB2) Container Parser
Parses the outer BLS container of PS5 PUP files.
Magic: 0x32424C53 ("SLB2")
"""
import struct
from dataclasses import dataclass
from typing import BinaryIO
BLS_MAGIC = 0x32424C53 # "SLB2" little-endian
@dataclass
class BLSEntry:
index: int
block_offset: int # multiply by 512 for byte offset
size: int
name: str
@property
def byte_offset(self) -> int:
return self.block_offset * 512
def __repr__(self) -> str:
return (
f"BLSEntry(#{self.index} name='{self.name}' "
f"offset=0x{self.byte_offset:X} size=0x{self.size:X})"
)
@dataclass
class BLSHeader:
magic: int
version: int
flags: int
file_count: int
block_count: int
entries: list
def __repr__(self) -> str:
return (
f"BLSHeader(magic=0x{self.magic:08X} version={self.version} "
f"flags=0x{self.flags:X} files={self.file_count} blocks={self.block_count})"
)
def parse_bls_header(f: BinaryIO) -> BLSHeader:
"""Parse a BLS (SLB2) header from the current file position."""
data = f.read(32)
if len(data) < 32:
raise ValueError("File too small for BLS header")
magic, version, flags, file_count, block_count = struct.unpack_from("<5I", data)
if magic != BLS_MAGIC:
raise ValueError(
f"Invalid BLS magic: 0x{magic:08X} (expected 0x{BLS_MAGIC:08X})"
)
entries = []
for i in range(file_count):
entry_data = f.read(48)
if len(entry_data) < 48:
raise ValueError(f"Truncated BLS entry at index {i}")
block_offset, size = struct.unpack_from("<II", entry_data, 0)
# 8 bytes reserved
name_bytes = entry_data[16:48]
name = name_bytes.split(b"\x00", 1)[0].decode("ascii", errors="replace")
entries.append(BLSEntry(
index=i,
block_offset=block_offset,
size=size,
name=name,
))
return BLSHeader(
magic=magic,
version=version,
flags=flags,
file_count=file_count,
block_count=block_count,
entries=entries,
)
def extract_bls_entry(f: BinaryIO, entry: BLSEntry) -> bytes:
"""Read the raw data of a BLS entry."""
f.seek(entry.byte_offset)
return f.read(entry.size)
def list_bls_entries(filepath: str) -> BLSHeader:
"""Open a PUP file and list all BLS entries."""
with open(filepath, "rb") as f:
return parse_bls_header(f)
+182
View File
@@ -0,0 +1,182 @@
"""
PS5 Crypto Operations
- AES-128-CBC decryption with known keys (Layer 3)
- PBKDF2 key derivation (Layer 0 - experimental)
- Key derivation infrastructure for when Layer 1 gaps are filled
"""
import hashlib
import struct
from typing import Optional
from Crypto.Cipher import AES
def aes_128_cbc_decrypt(data: bytes, key: bytes, iv: bytes = None) -> bytes:
"""Standard AES-128-CBC decryption."""
if iv is None:
iv = b"\x00" * 16
if len(key) != 16:
raise ValueError(f"Key must be 16 bytes, got {len(key)}")
if len(iv) != 16:
raise ValueError(f"IV must be 16 bytes, got {len(iv)}")
# Pad data to 16-byte boundary if needed
padded = data
remainder = len(data) % 16
if remainder:
padded = data + b"\x00" * (16 - remainder)
cipher = AES.new(key, AES.MODE_CBC, iv)
result = cipher.decrypt(padded)
return result[: len(data)]
def aes_128_ecb_decrypt(data: bytes, key: bytes) -> bytes:
"""AES-128-ECB decryption (single block or multiple)."""
if len(key) != 16:
raise ValueError(f"Key must be 16 bytes, got {len(key)}")
padded = data
remainder = len(data) % 16
if remainder:
padded = data + b"\x00" * (16 - remainder)
cipher = AES.new(key, AES.MODE_ECB)
result = cipher.decrypt(padded)
return result[: len(data)]
def pbkdf2_derive(
password: bytes, salt: bytes, iterations: int, key_len: int = 16
) -> bytes:
"""PBKDF2-HMAC-SHA256 key derivation."""
return hashlib.pbkdf2_hmac("sha256", password, salt, iterations, dklen=key_len)
def decrypt_emc_ipl_header(data: bytes) -> bytes:
"""
Decrypt EMC IPL header using the known CP key.
Key: F0332357C8CFAE7E7E26E52BE9E3AED4
AES-128-CBC with zeroed IV.
"""
from keys import EMC_IPL_HEADER_KEY
return aes_128_cbc_decrypt(data, EMC_IPL_HEADER_KEY)
def decrypt_emc_ipl_body(data: bytes) -> bytes:
"""
Decrypt EMC IPL body (rev c0 only).
Key: D5C92E39759A3E5CE954E772B1C2B651
AES-128-CBC with zeroed IV.
"""
from keys import EMC_IPL_CIPHER_KEY
return aes_128_cbc_decrypt(data, EMC_IPL_CIPHER_KEY)
def decrypt_eap_kbl(data: bytes) -> bytes:
"""
Decrypt EAP KBL.
Key: 262555E3CF062B070B5AA2CDDF3A5D0E
AES-128-CBC with zeroed IV.
"""
from keys import EAP_KBL_KEY
return aes_128_cbc_decrypt(data, EAP_KBL_KEY)
def decrypt_eap_kernel(data: bytes) -> bytes:
"""
Decrypt EAP Kernel (from KBL).
Key: CBCC1E53F42C1CB44D965E233CD792A8
AES-128-CBC with zeroed IV.
"""
from keys import EAP_KBL_KERNEL_KEY
return aes_128_cbc_decrypt(data, EAP_KBL_KERNEL_KEY)
def decrypt_eap_kernel_self(data: bytes) -> bytes:
"""
Decrypt EAP Kernel SELF modules.
Key: 32D00F27AE38FE4AC88A352313A2BFB4
IV: 08FEA1ACC37A630999745386168812EC
AES-128-CBC.
"""
from keys import EAP_KERNEL_SELF_KEY, EAP_KERNEL_SELF_IV
return aes_128_cbc_decrypt(data, EAP_KERNEL_SELF_KEY, EAP_KERNEL_SELF_IV)
def decrypt_with_titania_key(data: bytes, revision: int) -> Optional[bytes]:
"""
Decrypt using Titania master key for a given hardware revision.
Only revision 2 has a known key; 1 and 3 have only IVs.
"""
from keys import TITANIA_KEYS
if revision not in TITANIA_KEYS:
return None
tk = TITANIA_KEYS[revision]
if "key" not in tk:
return None
return aes_128_cbc_decrypt(data, tk["key"], tk["iv"])
# --- Experimental: PS4-style key derivation (adapted for PS5) ---
def ps4_decrypt_with_seed(
rom_constant: bytes, key_seed: bytes, fuse_key: bytes
) -> bytes:
"""
PS4 Aeolia-style key derivation (fail0verflow).
Builds 32-byte input: key_seed[8] repeated twice + rom_constant[16]
AES-128-CBC decrypt with zeroed IV using fuse_key.
Returns second 16 bytes of result.
NOTE: PS5 probably uses a different scheme (PBKDF2 + KMB_AES).
This is here for reference/experimentation.
"""
if len(key_seed) < 8:
key_seed = key_seed + b"\x00" * (8 - len(key_seed))
plaintext = key_seed[:8] + key_seed[:8] + rom_constant[:16]
decrypted = aes_128_cbc_decrypt(plaintext, fuse_key)
return decrypted[16:32]
def experimental_pbkdf2_master_key() -> bytes:
"""
Experimental: derive master key using OTP_AES_ROOT + KDF_SALT1 + 1000 iters.
This is Layer 0 of the PS5 key derivation.
The exact parameters (hash function, key length) are not fully confirmed.
Trying PBKDF2-HMAC-SHA256 with 16-byte output.
"""
from keys import OTP_AES_ROOT, KDF_SALT1, KDF_ITERS
return pbkdf2_derive(OTP_AES_ROOT, KDF_SALT1, KDF_ITERS, key_len=16)
def show_experimental_derivation():
"""Show experimental PBKDF2 derivation results for research."""
from keys import OTP_AES_ROOT, KDF_SALT1, KDF_ITERS
print("=== Experimental PBKDF2 Key Derivation ===")
print(f"OTP_AES_ROOT: {OTP_AES_ROOT.hex()}")
print(f"KDF_SALT1: {KDF_SALT1.hex()}")
print(f"KDF_ITERS: {KDF_ITERS}")
print()
for hash_fn in ("sha256", "sha1", "sha512"):
for kl in (16, 32):
derived = hashlib.pbkdf2_hmac(
hash_fn, OTP_AES_ROOT, KDF_SALT1, KDF_ITERS, dklen=kl
)
print(f"PBKDF2-HMAC-{hash_fn.upper()} (dklen={kl}): {derived.hex()}")
print()
print("NOTE: The correct hash function and key length are not confirmed.")
print("These are candidates to test against known Titania master keys.")
print()
from keys import TITANIA_KEYS
for rev, tk in TITANIA_KEYS.items():
if "key" in tk:
print(f"Titania {rev} known key: {tk['key'].hex()}")
print(f"Titania {rev} known IV: {tk['iv'].hex()}")
+161
View File
@@ -0,0 +1,161 @@
"""
Inner Firmware Decryptor
Decrypts already-extracted firmware segments using known keys.
This is Layer 3 - works on raw firmware blobs, not on encrypted PUP segments.
"""
import os
import struct
from typing import Optional
from crypto import (
aes_128_cbc_decrypt,
decrypt_emc_ipl_header,
decrypt_emc_ipl_body,
decrypt_eap_kbl,
decrypt_eap_kernel,
decrypt_eap_kernel_self,
decrypt_with_titania_key,
)
def detect_firmware_type(data: bytes, filename: str = "") -> Optional[str]:
"""Guess the firmware type from filename or content."""
fn = filename.lower()
if "emc_salina" in fn or "emc_ipl" in fn:
return "emc_ipl"
if "eap_kbl" in fn:
return "eap_kbl"
if "eap_kernel" in fn or "eap_kern" in fn:
return "eap_kernel"
if "titania" in fn:
return "titania"
if fn.endswith(".self"):
return "eap_self"
return None
def decrypt_firmware_blob(
data: bytes, fw_type: str, titania_rev: int = 2
) -> Optional[bytes]:
"""
Decrypt a firmware blob using the appropriate known key.
Args:
data: raw encrypted firmware bytes
fw_type: one of 'emc_ipl', 'emc_ipl_header', 'eap_kbl',
'eap_kernel', 'eap_self', 'titania'
titania_rev: Titania hardware revision (1, 2, or 3)
Returns:
Decrypted bytes, or None if key not available for this type
"""
if fw_type == "emc_ipl_header":
return decrypt_emc_ipl_header(data)
elif fw_type == "emc_ipl":
return decrypt_emc_ipl_body(data)
elif fw_type == "eap_kbl":
return decrypt_eap_kbl(data)
elif fw_type == "eap_kernel":
return decrypt_eap_kernel(data)
elif fw_type == "eap_self":
return decrypt_eap_kernel_self(data)
elif fw_type == "titania":
return decrypt_with_titania_key(data, titania_rev)
return None
def try_all_decryptions(data: bytes, output_dir: str, base_name: str):
"""
Try decrypting data with all known keys and save results.
Useful for identifying which key works on unknown segments.
"""
os.makedirs(output_dir, exist_ok=True)
attempts = [
("emc_ipl_header", "EMC IPL Header Key"),
("emc_ipl", "EMC IPL Cipher Key (rev c0)"),
("eap_kbl", "EAP KBL Key"),
("eap_kernel", "EAP KBL Kernel Key"),
("eap_self", "EAP Kernel SELF Key"),
]
# Also try Titania keys
for rev in (1, 2, 3):
attempts.append((f"titania_rev{rev}", f"Titania Rev {rev} Key"))
results = []
for key_name, description in attempts:
try:
if key_name.startswith("titania_rev"):
rev = int(key_name[-1])
decrypted = decrypt_with_titania_key(data[:256], rev)
else:
decrypted = decrypt_firmware_blob(data[:256], key_name)
if decrypted is None:
results.append((key_name, description, "NO KEY AVAILABLE"))
continue
# Check if decrypted data looks reasonable
entropy = _estimate_entropy(decrypted[:64])
has_nulls = b"\x00\x00\x00\x00" in decrypted[:64]
has_ascii = any(32 <= b <= 126 for b in decrypted[:32])
verdict = "HIGH_ENTROPY"
if entropy < 4.0:
verdict = "LOW_ENTROPY (likely correct!)"
elif has_nulls and has_ascii:
verdict = "POSSIBLE_MATCH"
results.append((key_name, description, verdict))
# Save first 256 bytes for inspection
out_path = os.path.join(output_dir, f"{base_name}.{key_name}.bin")
with open(out_path, "wb") as f:
f.write(decrypted)
except Exception as e:
results.append((key_name, description, f"ERROR: {e}"))
return results
def _estimate_entropy(data: bytes) -> float:
"""Rough Shannon entropy estimate (0-8 bits per byte)."""
import math
if not data:
return 0.0
freq = {}
for b in data:
freq[b] = freq.get(b, 0) + 1
entropy = 0.0
for count in freq.values():
p = count / len(data)
if p > 0:
entropy -= p * math.log2(p)
return entropy
def analyze_emc_ipl(data: bytes) -> dict:
"""
Analyze an EMC IPL blob structure.
Based on PS4 Aeolia IPL header (fail0verflow), adapted for PS5.
"""
result = {"size": len(data)}
# Try decrypting header with known key
if len(data) >= 0x80:
dec_header = decrypt_emc_ipl_header(data[:0x80])
result["header_decrypted"] = dec_header
# Check for PS4-style magic at start
magic = struct.unpack_from("<I", dec_header, 0)[0]
result["header_magic"] = f"0x{magic:08X}"
# Try to extract fields similar to PS4 aeolia_ipl_hdr
if len(dec_header) >= 0x20:
result["possible_hdr_len"] = struct.unpack_from("<I", dec_header, 8)[0]
result["possible_body_len"] = struct.unpack_from("<I", dec_header, 12)[0]
return result
+182
View File
@@ -0,0 +1,182 @@
"""
PS5 Known Keys from psdevwiki
For legitimate security research on personally owned hardware.
"""
# --- ROM Keys (two 256-byte blocks) ---
ROM_KEY_BLOCK_1 = bytes.fromhex((
"EC0561F36B62400E6FA0EC2EDCFD8D17"
"33889AE0E03180AB6DAE39D54BE037E6"
"7E5324285D0ABD7734747BE4341FB446"
"DB17D1E40947B1C5DB0F6B19CED5A0D5"
"036CAEA074963831CB81C0F162EE3CD2"
"E3A64517BA22A205D1459FDB8E50CF2A"
"4234474B4B389C70F08C2F02816B8F27"
"6FFA83C47E75802EFD2A03C8DFADA5A5"
"A703A1BB130A4AC725B98839631F6910"
"5AF4BF2BA4E6D5198105CF26F6CC18C7"
"98EB907FEF9B498D5ACEFE3B8AE13148"
"00FA9F7128DC79F914FBE12645AA2115"
"8861EB7868C8D3295D28BA5D4FFBE648"
"42EA57BEE47A74704D7E5B37706440BD"
"A5880A90A62CF2BE24C954D3CED225ED"
"DD51408C1DFD41492A22E44458AB5957"
))
ROM_KEY_BLOCK_2 = bytes.fromhex((
"C8D8FCAD0C461FB5F6F90AF080287D5C"
"B53B0FF9B30D1C1769091705B93BA82E"
"7617400761C4CCA06377A6B5D2BE3D77"
"1B4C18922AE90FABB2593C6C5768010E"
"E1A4A0350BEA15A4D8FCC8C637C078B1"
"46B4D765046F8622C4D2C130C83CD6D5"
"D2DA74CA61A87C84073FC968579BE3DA"
"C7EB34E0E9DF186A4F1A1071EC098F61"
"D14B060509F53F3E051A67F28D6AFE69"
"FDA68E24BE3D4F4784C2B2D6AAA74934"
"AFFB54E11A8AAEB626E7B8E7EE511414"
"30D5AF68C2BFA6E277349E5BB464B33A"
"CD1812894FC7806065BC3E5EEF16E0D9"
"69E0B1908A00826643624BB9717A2BAF"
"CFD2AC9AAFF6015BFB8B61D5BB980A6D"
"86CCDD28A81D1E8199DC95792C77A6FB"
))
# --- Key 2-9 Keyseeds ---
KEY_2_KEYSEEDS_ALT3 = bytes.fromhex("AB0A9CB9B3DC4118388512A2127AAAA8")
KEY_2_KEYSEEDS = bytes.fromhex("C09355F88AC42B3B494EFF0B1C556BD2")
KEY_2_KEYSEEDS_ALT = bytes.fromhex("8C6CEABB994DC0AC1272F39DC7924B9A")
KEY_4_KEYSEEDS = bytes.fromhex(
"9A1A6BC1A8A1C0F22ED44CC84ABBD647"
"696C8A4B08B19389FEB54CD2FDEEA5E6"
)
KEY_4_KEYSEEDS_ALT = bytes.fromhex(
"ABAB2B75F6EDA2177314DBA2D294252C"
"00000000000000000000000000000000"
)
KEY_4_KEYSEEDS_ALT2 = bytes.fromhex(
"90ECFD2134AE59C8F1BEE41586B40DAB"
"BECED785BDBF2500D4769EB27D4D557C"
)
KEY_5_KEYSEEDS = bytes.fromhex(
"085AAF95FFD183F58D034CAA416D7904"
"40315956BE8FEF843006250D144C9C46"
)
KEY_6_KEYSEEDS_ALT2 = bytes.fromhex(
"BCB4914433800577E933753CDF9B0530"
"D3BDD2C9773E072B094441009DB442D7"
)
KEY_7_KEYSEEDS = bytes.fromhex(
"A2451EFC50D29C36795E7520D3EF766B"
"FB369393BC47E7B3FCA9460F139D4D27"
)
KEY_8_KEYSEEDS = bytes.fromhex("AFC457975ADB0B40F981E87F96789213")
KEY_9_KEYSEEDS = bytes.fromhex("78E46F39DE022C96D96599BA90739515")
# --- OTP / KDF ---
OTP_AES_ROOT = bytes.fromhex("63f0cf8914429057988bcc975b8e798b")
KDF_SALT1 = bytes.fromhex("4BCEDB98FEA3A7016A75661A03B74AF5")
KDF_ITERS = 1000
# --- Titania Master Keys (per hardware revision) ---
TITANIA_KEYS = {
1: {
"iv": bytes.fromhex("540F65FF23B21B58F98AC952EB51D9B3"),
"efc_ipl_revs": ["C3000001", "C3000002"],
"eap_kbl_ids": [0x6, 0x7, 0x8],
"fw_range": "0.700+",
},
2: {
"iv": bytes.fromhex("582E2420ECD919908ECD73D7A5E8CBA3"),
"key": bytes.fromhex("22C5473E5DFB432F9FC37E4AC0A63F4C"),
"efc_ipl_revs": [
"C3000003", "C3000004", "C3000005", "C3000006",
"C3000007", "C3000009", "C300000A", "C300000B", "C300000C",
],
"eap_kbl_ids": [0xE],
"fw_range": "0.750+",
},
3: {
"iv": bytes.fromhex("AD4FC4C25A23B2713DB29E54E9AB0193"),
"efc_ipl_revs": [
"C300000D", "C300000E", "C300000F", "C3000010",
"C3000011", "C3000012", "C3000013", "C3000014",
],
"eap_kbl_ids": [0x15],
"fw_range": "6.00+",
},
}
# --- Inner Firmware Keys (Layer 3 - known, work on extracted segments) ---
EMC_IPL_HEADER_KEY = bytes.fromhex("F0332357C8CFAE7E7E26E52BE9E3AED4")
EMC_IPL_CIPHER_KEY = bytes.fromhex("D5C92E39759A3E5CE954E772B1C2B651")
EAP_KBL_KEY = bytes.fromhex("262555E3CF062B070B5AA2CDDF3A5D0E")
EAP_KBL_HMAC_KEY = bytes.fromhex("1EE22F6A189E7D99A28B9A96D3C4DBA2")
EAP_KBL_KERNEL_KEY = bytes.fromhex("CBCC1E53F42C1CB44D965E233CD792A8")
EAP_KBL_KERNEL_MAC_KEY = bytes.fromhex("683D6E2E496687CB5B831DA12BCB001B")
EAP_KERNEL_SELF_KEY = bytes.fromhex("32D00F27AE38FE4AC88A352313A2BFB4")
EAP_KERNEL_SELF_IV = bytes.fromhex("08FEA1ACC37A630999745386168812EC")
EAP_KERNEL_CIPHER_KEY = bytes.fromhex("3D0B1CAA949DB83028F5049BF9FDC11D")
EAP_KERNEL_CIPHER_IV = bytes.fromhex("7AD87F4C865B1CE2F02E5E62D7B7375D")
# PSVR2 Master Key
PSVR2_MASTER_KEY = bytes.fromhex("BE5C41DBF3D2DD902E4F939394A8A715")
# PUP segment ID → filename mapping
SEGMENT_NAMES = {
0x01: "eula.xml",
0x02: "updatemode.self",
0x03: "emc_salina_a.bin",
0x04: "mbr.bin",
0x05: "kernel.bin",
0x06: "unk_06.bin",
0x07: "unk_07.bin",
0x08: "unk_08.bin",
0x09: "unk_09.bin",
0x0A: "CP.bin",
0x0B: "titania.bls",
0x0C: "version_name.xml",
0x0D: "emc_salina_b.bin",
0x0E: "eap_kbl.bin",
0x0F: "bd_firm_info.json",
0x10: "emc_salina_c.bls",
0x11: "floyd_salina_c.bls",
0x12: "usb_pdc_salina_c.bls",
0x14: "emc_salina_d.bls",
0x15: "eap_kbl_2.bin",
0x16: "font.zip",
0x100: "ariel_sec_ldr_a.bin",
0x101: "oberon_sec_ldr_a.bin",
0x102: "oberon_sec_ldr_b.bin",
0x103: "oberon_sec_ldr_c.bin",
0x104: "oberon_sec_ldr_d.bin",
0x105: "oberon_sec_ldr_e.bin",
0x106: "oberon_sec_ldr_f.bin",
0x200: "dev_unk_512.bin",
0x201: "dev_wlanbt.bin",
0x203: "dev_ssd0.system_b",
0x204: "dev_ssd0.system_ex_b",
0x207: "dev_ssd0.preinst",
}
# KMB_AES function call signatures (for reference during derivation research)
KMB_CALLS = {
"KEY_4": "KMB_AES_CBC_MP0(1, KEY_4_KEYSEEDS, IV, 0, 4u, 1, 0x706)",
"KEY_4_ALT": "KMB_AES_CBC_MP0(0, v63, KEY_4_KEYSEEDS_ALTERNATE, 3u, 4u, 1, 0x706)",
"KEY_4_ALT2": "KMB_AES_CBC_MP0(1, KEY_4_KEYSEEDS_ALTERNATE_2, IV, 2u, 4u, 1, 0x706)",
"KEY_5": "KMB_AES_CBC_MP0(1, KEY_5_KEYSEEDS, IV, 0, 5u, 1, 0x807)",
"KEY_6_ALT2": "KMB_AES_CBC_MP0(1, KEY_6_KEYSEEDS_ALTERNATE_2, IV, 2u, 6u, 1, 0x520)",
"KEY_7": "KMB_AES_CBC_MP0(1, KEY_7_KEYSEEDS, IV, 0, 7u, 1, 0x805)",
"KEY_8": "KMB_AES_ECB_MP0(1, KEY_8_KEYSEEDS, 7u, 8u, 1, 0xFCF)",
"KEY_9": "KMB_AES_ECB_MP0(1, KEY_9_KEYSEEDS, 7u, 9u, 1, 0xFCF)",
"KEY_2": "CRYPT_AES(v64, v64, nullptr, 32, 4, KEY_2_KEYSEEDS, 1, 2, 0)",
"KEY_2_ALT3": "CRYPT_AES(0, 0, nullptr, v59, 4, KEY_2_KEYSEEDS_ALT3, 1, 2, 0)",
}
+408
View File
@@ -0,0 +1,408 @@
"""
PS5 PUP Decrypt Tool - CLI Entry Point
Partial PC-based tool for PS5 firmware analysis.
What works (Layer 3 - known keys):
- BLS container parsing (outer SLB2 format)
- PUP header/segment structure parsing
- Inner firmware decryption (EMC IPL, EAP KBL, EAP Kernel, Titania)
What's missing (Layer 1 - key derivation):
- PUP segment decryption (needs KMB_AES derivation chain)
- Full end-to-end PUP decryption pipeline
"""
import argparse
import os
import sys
import zlib
from bls import parse_bls_header, extract_bls_entry, BLS_MAGIC
from pup import parse_pup_header, parse_pup_segments, try_parse_pup, PUP_MAGIC
from firmware import (
detect_firmware_type,
decrypt_firmware_blob,
try_all_decryptions,
analyze_emc_ipl,
)
from crypto import show_experimental_derivation
def cmd_info(args):
"""Show BLS and PUP structure info for a PUP file."""
filepath = args.file
if not os.path.isfile(filepath):
print(f"Error: File not found: {filepath}")
return 1
print(f"=== PS5 PUP Info: {os.path.basename(filepath)} ===")
print(f"File size: {os.path.getsize(filepath):,} bytes")
print()
with open(filepath, "rb") as f:
# Check BLS magic
magic_bytes = f.read(4)
f.seek(0)
import struct
magic = struct.unpack_from("<I", magic_bytes, 0)[0]
if magic == BLS_MAGIC:
print("[BLS Container]")
try:
header = parse_bls_header(f)
print(f" Version: {header.version}")
print(f" Flags: 0x{header.flags:X}")
print(f" File count: {header.file_count}")
print(f" Block count: {header.block_count}")
print()
print(f"{'#':<4} {'Name':<32} {'Offset':<14} {'Size':<14}")
print("-" * 68)
for entry in header.entries:
print(
f"{entry.index:<4} {entry.name:<32} "
f"0x{entry.byte_offset:<12X} 0x{entry.size:<12X}"
)
print()
# Try to parse each entry as PUP
for entry in header.entries:
f.seek(entry.byte_offset)
pup_peek = f.read(min(entry.size, 0x2000))
pup_info = try_parse_pup(pup_peek)
if pup_info and pup_info.get("magic_valid"):
_print_pup_info(entry.name, pup_info, entry.size)
except Exception as e:
print(f" Error parsing BLS: {e}")
elif magic == PUP_MAGIC:
print("[Raw PUP (no BLS wrapper)]")
data = f.read()
pup_info = try_parse_pup(data)
if pup_info:
_print_pup_info("(root)", pup_info)
else:
print(f"Unknown file format. Magic: 0x{magic:08X}")
print("Expected BLS (0x32424C53) or PUP (0xEEF51454)")
return 1
return 0
def _print_pup_info(name, pup_info, bls_entry_size=0):
"""Print PUP structure details."""
print(f"[PUP Entry: {name}]")
header = pup_info.get("header")
is_encrypted = pup_info.get("encrypted", False)
if header:
fh = header.file_header
print(f" Magic: 0x{fh.magic:08X} (valid)")
print(f" unknown_04: 0x{fh.unknown_04:08X}")
print(f" Header size: 0x{fh.header_size:X} ({fh.header_size} bytes)")
print(f" Hash size: 0x{fh.hash_size:X} ({fh.hash_size} bytes)")
if is_encrypted:
# Estimate segment count from header_size
seg_table_bytes = fh.header_size - 0x20
est_segs = seg_table_bytes // 32
data_offset = fh.header_size + fh.hash_size
print()
print(f" Status: ENCRYPTED")
print(f" - File header (16 bytes): plaintext")
print(f" - Extended header + segment table (0x10 to 0x{fh.header_size:X}): encrypted")
print(f" - Hash/signature table (0x{fh.header_size:X} to 0x{data_offset:X}): encrypted")
print(f" - Segment data (0x{data_offset:X}+): encrypted")
print(f" - Estimated segment count: ~{est_segs} (from header_size)")
if bls_entry_size:
print(f" - Total PUP size: {bls_entry_size:,} bytes ({bls_entry_size/1024/1024:.1f} MB)")
print(f" - Decryption requires: Layer 1 key derivation (KMB_AES chain)")
else:
print(f" File size: 0x{header.file_size:X}")
print(f" Segments: {header.segment_count}")
print()
segments = pup_info.get("segments", [])
if segments and not is_encrypted:
print(f" {'#':<4} {'ID':<6} {'Name':<28} {'Compressed':<14} {'Uncompressed':<14} {'Flags'}")
print(" " + "-" * 90)
for seg in segments:
if seg.segment_type in (0xE, 0xF):
print(f" {seg.index:<4} {'--':<6} {seg.type_name:<28} {'--':<14} {'--':<14} 0x{seg.flags:08X}")
else:
flags_parts = []
if seg.is_compressed:
flags_parts.append("ZLIB")
if seg.is_blocked:
flags_parts.append(f"BLK(0x{seg.block_size:X})")
if seg.is_table:
flags_parts.append("TBL")
flags_str = " ".join(flags_parts) if flags_parts else "SIMPLE"
print(
f" {seg.index:<4} 0x{seg.segment_id:<4X} {seg.name:<28} "
f"0x{seg.compressed_size:<12X} 0x{seg.uncompressed_size:<12X} {flags_str}"
)
print()
def cmd_extract(args):
"""Extract BLS entries to disk."""
filepath = args.file
output_dir = args.output or os.path.splitext(filepath)[0] + "_extracted"
if not os.path.isfile(filepath):
print(f"Error: File not found: {filepath}")
return 1
os.makedirs(output_dir, exist_ok=True)
with open(filepath, "rb") as f:
try:
header = parse_bls_header(f)
except ValueError as e:
print(f"Error: {e}")
return 1
print(f"Extracting {header.file_count} BLS entries to {output_dir}/")
for entry in header.entries:
data = extract_bls_entry(f, entry)
out_path = os.path.join(output_dir, entry.name)
with open(out_path, "wb") as out:
out.write(data)
print(f" {entry.name} ({len(data):,} bytes)")
print(f"\nExtracted {header.file_count} entries.")
return 0
def cmd_decrypt(args):
"""Decrypt an extracted firmware blob with known keys."""
filepath = args.file
fw_type = args.type
titania_rev = args.titania_rev
if not os.path.isfile(filepath):
print(f"Error: File not found: {filepath}")
return 1
with open(filepath, "rb") as f:
data = f.read()
if not fw_type:
fw_type = detect_firmware_type(data, os.path.basename(filepath))
if fw_type:
print(f"Auto-detected firmware type: {fw_type}")
else:
print("Could not auto-detect firmware type.")
print("Use --type with one of: emc_ipl_header, emc_ipl, eap_kbl, eap_kernel, eap_self, titania")
return 1
print(f"Decrypting {os.path.basename(filepath)} as {fw_type}...")
result = decrypt_firmware_blob(data, fw_type, titania_rev)
if result is None:
print(f"No key available for type '{fw_type}' (rev {titania_rev})")
return 1
output = args.output
if not output:
base, ext = os.path.splitext(filepath)
output = f"{base}.dec{ext}"
with open(output, "wb") as f:
f.write(result)
print(f"Decrypted {len(result):,} bytes -> {output}")
# Show first 64 bytes hex dump
print("\nFirst 64 bytes of decrypted output:")
_hexdump(result[:64])
return 0
def cmd_bruteforce(args):
"""Try all known keys against a firmware blob."""
filepath = args.file
output_dir = args.output or os.path.splitext(filepath)[0] + "_bruteforce"
if not os.path.isfile(filepath):
print(f"Error: File not found: {filepath}")
return 1
with open(filepath, "rb") as f:
data = f.read()
print(f"Trying all known keys against {os.path.basename(filepath)}...")
print(f"Output dir: {output_dir}/")
print()
results = try_all_decryptions(data, output_dir, os.path.splitext(os.path.basename(filepath))[0])
print(f"{'Key':<20} {'Description':<30} {'Result'}")
print("-" * 80)
for key_name, description, verdict in results:
marker = ">>>" if "likely correct" in verdict.lower() or "POSSIBLE" in verdict else " "
print(f"{marker} {key_name:<20} {description:<30} {verdict}")
print()
print("LOW_ENTROPY = likely correct decryption (structured data)")
print("POSSIBLE_MATCH = has null runs and ASCII (worth investigating)")
print("HIGH_ENTROPY = still looks encrypted or wrong key")
return 0
def cmd_analyze(args):
"""Analyze an EMC IPL blob in detail."""
filepath = args.file
if not os.path.isfile(filepath):
print(f"Error: File not found: {filepath}")
return 1
with open(filepath, "rb") as f:
data = f.read()
print(f"=== EMC IPL Analysis: {os.path.basename(filepath)} ===")
print(f"Size: {len(data):,} bytes")
print()
info = analyze_emc_ipl(data)
if "header_magic" in info:
print(f"Decrypted header magic: {info['header_magic']}")
if "possible_hdr_len" in info:
print(f"Possible header length: 0x{info['possible_hdr_len']:X}")
if "possible_body_len" in info:
print(f"Possible body length: 0x{info['possible_body_len']:X}")
if "header_decrypted" in info:
print("\nDecrypted header (first 0x80 bytes):")
_hexdump(info["header_decrypted"])
return 0
def cmd_decompress(args):
"""Decompress a zlib-compressed segment."""
filepath = args.file
if not os.path.isfile(filepath):
print(f"Error: File not found: {filepath}")
return 1
with open(filepath, "rb") as f:
data = f.read()
try:
decompressed = zlib.decompress(data)
except zlib.error as e:
print(f"Decompression failed: {e}")
print("Data may not be zlib-compressed, or may still be encrypted.")
return 1
output = args.output
if not output:
base, ext = os.path.splitext(filepath)
output = f"{base}.decompressed{ext}"
with open(output, "wb") as f:
f.write(decompressed)
ratio = len(decompressed) / len(data) if data else 0
print(f"Decompressed: {len(data):,} -> {len(decompressed):,} bytes ({ratio:.1f}x)")
print(f"Output: {output}")
return 0
def cmd_keys(args):
"""Show known keys and experimental derivation."""
show_experimental_derivation()
def _hexdump(data: bytes, width: int = 16):
"""Print a hex dump of data."""
for offset in range(0, len(data), width):
chunk = data[offset:offset + width]
hex_part = " ".join(f"{b:02X}" for b in chunk)
ascii_part = "".join(chr(b) if 32 <= b <= 126 else "." for b in chunk)
print(f" {offset:08X} {hex_part:<{width * 3}} {ascii_part}")
def main():
parser = argparse.ArgumentParser(
description="PS5 PUP Decrypt Tool - Partial PC-based firmware analyzer",
epilog=(
"Layer 3 (known keys) is fully functional.\n"
"Layer 1 (PUP segment decryption) requires key derivation research.\n"
"For security research on personally owned hardware only."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", help="Command to run")
# info
p_info = subparsers.add_parser("info", help="Show PUP/BLS structure info")
p_info.add_argument("file", help="Path to PS5UPDATE.PUP or extracted PUP")
p_info.set_defaults(func=cmd_info)
# extract
p_extract = subparsers.add_parser("extract", help="Extract BLS entries to disk")
p_extract.add_argument("file", help="Path to PS5UPDATE.PUP")
p_extract.add_argument("-o", "--output", help="Output directory")
p_extract.set_defaults(func=cmd_extract)
# decrypt
p_decrypt = subparsers.add_parser("decrypt", help="Decrypt firmware blob with known key")
p_decrypt.add_argument("file", help="Path to extracted firmware blob")
p_decrypt.add_argument(
"-t", "--type",
choices=["emc_ipl_header", "emc_ipl", "eap_kbl", "eap_kernel", "eap_self", "titania"],
help="Firmware type (auto-detected from filename if omitted)",
)
p_decrypt.add_argument("-o", "--output", help="Output file path")
p_decrypt.add_argument("--titania-rev", type=int, default=2, choices=[1, 2, 3],
help="Titania hardware revision (default: 2)")
p_decrypt.set_defaults(func=cmd_decrypt)
# bruteforce
p_brute = subparsers.add_parser("bruteforce", help="Try all known keys against a blob")
p_brute.add_argument("file", help="Path to firmware blob")
p_brute.add_argument("-o", "--output", help="Output directory")
p_brute.set_defaults(func=cmd_bruteforce)
# analyze
p_analyze = subparsers.add_parser("analyze", help="Analyze EMC IPL blob structure")
p_analyze.add_argument("file", help="Path to EMC IPL blob")
p_analyze.set_defaults(func=cmd_analyze)
# decompress
p_decomp = subparsers.add_parser("decompress", help="Zlib-decompress a segment")
p_decomp.add_argument("file", help="Path to compressed segment")
p_decomp.add_argument("-o", "--output", help="Output file path")
p_decomp.set_defaults(func=cmd_decompress)
# keys
p_keys = subparsers.add_parser("keys", help="Show known keys and derivation experiments")
p_keys.set_defaults(func=cmd_keys)
args = parser.parse_args()
if not args.command:
parser.print_help()
return 0
try:
from Crypto.Cipher import AES
except ImportError:
print("Error: pycryptodome is required. Install it with:")
print(" pip install pycryptodome")
return 1
return args.func(args)
if __name__ == "__main__":
sys.exit(main() or 0)
+593
View File
@@ -0,0 +1,593 @@
/*
* ps5_key_extract.c — PUP header decryption key extractor for jailbroken PS5
*
* Target: PS5 FW 13.60 or below with kernel R/W (e.g. Byepervisor Reborn + Relapse)
* Build: compile for FreeBSD 11 amd64, load via payload loader
* Output: /data/pup_key_dump.bin (structured binary with section headers)
*
* All operations are READ-ONLY. Nothing is written to kernel or hardware state.
*/
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* ---------- kernel R/W primitives (framework-dependent) ---------- */
extern uint64_t kernel_read64(uint64_t kaddr);
extern uint32_t kernel_read32(uint64_t kaddr);
extern int kernel_copyout(uint64_t kaddr, void *uaddr, size_t len);
/* ---------- output format ---------- */
#define DUMP_MAGIC 0x505550444D50ULL /* "PUPDMP\0\0" */
#define SECTION_LSB 0x01
#define SECTION_IOCTL 0x02
#define SECTION_SNAP_PRE 0x03
#define SECTION_SNAP_POST 0x04
#define SECTION_MEMSCAN 0x05
typedef struct {
uint64_t magic;
uint32_t version;
uint32_t section_count;
} __attribute__((packed)) dump_header_t;
typedef struct {
uint32_t type;
uint32_t size;
uint32_t offset;
uint32_t reserved;
} __attribute__((packed)) section_header_t;
/* ---------- AMD CCP v5 constants ---------- */
#define PCI_VENDOR_AMD 0x1022
#define CCP_DEV_ID_1 0x1456
#define CCP_DEV_ID_2 0x1468
#define CCP_DEV_ID_3 0x15DF
#define CCP_QUEUE_MMIO_BASE 0x3001000
#define CCP_QUEUE_STRIDE 0x1000
/* LSB mask register offsets within a queue */
#define CCP_PUBLIC_MASK_LO 0x18
#define CCP_PUBLIC_MASK_HI 0x1C
#define CCP_PRIVATE_MASK_LO 0x20
#define CCP_PRIVATE_MASK_HI 0x24
/* Candidate LSB region offsets from CCP BAR */
#define LSB_OFFSET_A 0x8000
#define LSB_OFFSET_B 0x10000
#define LSB_OFFSET_C 0x4000
/* LSB geometry: 8 LSBs x 16 slots x 32 bytes = 4096 bytes */
#define LSB_COUNT 8
#define SLOTS_PER_LSB 16
#define SLOT_SIZE 32
#define LSB_TOTAL_SIZE (LSB_COUNT * SLOTS_PER_LSB * SLOT_SIZE)
/* CCP engine type constants (from Linux ccp-dev.h) */
#define CCP_ENGINE_AES 0
#define CCP_ENGINE_XTS_AES 1
#define CCP_ENGINE_DES3 2 /* 3DES */
#define CCP_ENGINE_SHA 3
#define CCP_ENGINE_RSA 4
#define CCP_ENGINE_PASSTHRU 5
#define CCP_ENGINE_ZLIB 6
#define CCP_ENGINE_ECC 7
/* CCP AES modes */
#define CCP_AES_MODE_ECB 0
#define CCP_AES_MODE_CBC 1
#define CCP_AES_MODE_OFB 2
#define CCP_AES_MODE_CFB 3
#define CCP_AES_MODE_CTR 4
#define CCP_AES_MODE_CMAC 5
#define CCP_AES_MODE_GHASH 6
#define CCP_AES_MODE_GCTR 7
#define CCP_AES_MODE_GCM 8
#define CCP_AES_MODE_GMAC 9
/* ---------- PUP ioctl constants (from psdevwiki) ---------- */
#define PUP_DEVICE_PATH "/dev/pup_update0"
#define PUP_IOCTL_DECRYPT_HEADER 0xC0184402 /* DecryptPupHeader */
#define PUP_IOCTL_VERIFY_SEGMENT 0xC0184403
#define PUP_IOCTL_DECRYPT_SEGMENT 0xC0184404
#define PUP_HEADER_ENC_SIZE 0x5B0
/* ---------- PS5 kernel constants (FreeBSD 11 based) ---------- */
#define KERNEL_BASE_PRIMARY 0xFFFFFFFF82200000ULL
#define KERNEL_BASE_FALLBACK 0xFFFFFFFF80000000ULL
/* FreeBSD 11 cdevsw field offsets */
#define CDEVSW_D_VERSION 0x00
#define CDEVSW_D_NAME 0x08
#define CDEVSW_D_OPEN 0x10
#define CDEVSW_D_CLOSE 0x18
#define CDEVSW_D_READ 0x20
#define CDEVSW_D_WRITE 0x28
#define CDEVSW_D_IOCTL 0x30
#define CDEVSW_D_POLL 0x38
/* ioctl argument for DecryptPupHeader */
typedef struct {
uint64_t src_paddr;
uint64_t dst_paddr;
uint32_t size;
uint32_t flags;
} __attribute__((packed)) pup_decrypt_arg_t;
/* ---------- globals ---------- */
static uint8_t g_output[256 * 1024];
static uint32_t g_output_pos = 0;
static uint32_t g_section_count = 0;
static section_header_t g_sections[32];
/* ---------- output helpers ---------- */
static void output_begin(void) {
g_output_pos = sizeof(dump_header_t) + sizeof(g_sections);
g_section_count = 0;
memset(g_output, 0, sizeof(g_output));
}
static void output_add_section(uint32_t type, const void *data, uint32_t size) {
if (g_section_count >= 32) return;
if (g_output_pos + size > sizeof(g_output)) return;
g_sections[g_section_count].type = type;
g_sections[g_section_count].size = size;
g_sections[g_section_count].offset = g_output_pos;
g_sections[g_section_count].reserved = 0;
memcpy(g_output + g_output_pos, data, size);
g_output_pos += size;
g_section_count++;
}
static int output_write(const char *path) {
dump_header_t hdr;
hdr.magic = DUMP_MAGIC;
hdr.version = 1;
hdr.section_count = g_section_count;
memcpy(g_output, &hdr, sizeof(hdr));
memcpy(g_output + sizeof(hdr), g_sections, sizeof(g_sections));
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
printf("[!] Failed to open %s for writing\n", path);
return -1;
}
write(fd, g_output, g_output_pos);
close(fd);
printf("[+] Wrote %u bytes to %s (%u sections)\n",
g_output_pos, path, g_section_count);
return 0;
}
static void hexdump(const char *label, const uint8_t *data, size_t len) {
printf("[*] %s (%zu bytes):\n", label, len);
for (size_t i = 0; i < len; i++) {
if (i > 0 && (i % 16) == 0) printf("\n");
if ((i % 16) == 0) printf(" %04zx: ", i);
printf("%02x ", data[i]);
}
printf("\n");
}
/* ================================================================
* APPROACH 1: CCP LSB Key Slot Dump
*
* Map the AMD CCP v5 MMIO region and read all 128 LSB key slots.
* The CCP stores AES keys used by the security processor in its
* Local Storage Block (LSB). The PUP decrypt key should be in one
* of these slots.
* ================================================================ */
static int approach1_ccp_lsb_dump(void) {
printf("\n=== APPROACH 1: CCP LSB Key Slot Dump ===\n");
int dmem_fd = open("/dev/dmem0", O_RDWR);
if (dmem_fd < 0) {
dmem_fd = open("/dev/mem", O_RDWR);
if (dmem_fd < 0) {
printf("[!] Cannot open /dev/dmem0 or /dev/mem\n");
return -1;
}
printf("[+] Opened /dev/mem\n");
} else {
printf("[+] Opened /dev/dmem0\n");
}
/* Find CCP BAR via PCI config space scan */
uint64_t ccp_bar = 0;
printf("[*] Scanning PCI for AMD CCP (vendor 0x%04x, devices 0x%04x/0x%04x/0x%04x)...\n",
PCI_VENDOR_AMD, CCP_DEV_ID_1, CCP_DEV_ID_2, CCP_DEV_ID_3);
/*
* PCI config space on x86: use I/O ports 0xCF8/0xCFC or ECAM.
* On PS5, the CCP is integrated into the SoC. Try known MMIO bases.
* The queue MMIO base 0x3001000 is relative to the CCP BAR.
* Typical PS5 CCP BAR candidates:
*/
uint64_t bar_candidates[] = {
0xE0100000ULL, /* common AMD APU CCP BAR */
0xFED80000ULL, /* AMD FCH region */
0xFEC10000ULL,
0x03000000ULL, /* from dayzerosec: queue base 0x3001000 implies BAR ~ 0x3000000 */
};
uint32_t lsb_offsets[] = { LSB_OFFSET_A, LSB_OFFSET_B, LSB_OFFSET_C };
uint8_t lsb_data[LSB_TOTAL_SIZE];
int found_any = 0;
for (int b = 0; b < (int)(sizeof(bar_candidates)/sizeof(bar_candidates[0])); b++) {
uint64_t bar = bar_candidates[b];
for (int lo = 0; lo < (int)(sizeof(lsb_offsets)/sizeof(lsb_offsets[0])); lo++) {
uint64_t lsb_phys = bar + lsb_offsets[lo];
printf("[*] Trying BAR 0x%llx + LSB offset 0x%x = 0x%llx\n",
(unsigned long long)bar, lsb_offsets[lo],
(unsigned long long)lsb_phys);
void *mapped = mmap(NULL, LSB_TOTAL_SIZE,
PROT_READ, MAP_SHARED,
dmem_fd, (off_t)lsb_phys);
if (mapped == MAP_FAILED) {
printf("[!] mmap failed for 0x%llx\n", (unsigned long long)lsb_phys);
continue;
}
memcpy(lsb_data, mapped, LSB_TOTAL_SIZE);
munmap(mapped, LSB_TOTAL_SIZE);
/* Check for non-zero slots */
int nonzero_count = 0;
for (int slot = 0; slot < LSB_COUNT * SLOTS_PER_LSB; slot++) {
uint8_t *s = &lsb_data[slot * SLOT_SIZE];
int is_zero = 1;
for (int j = 0; j < SLOT_SIZE; j++) {
if (s[j] != 0) { is_zero = 0; break; }
}
if (!is_zero) {
nonzero_count++;
printf("[+] Non-zero LSB slot %d (LSB %d, slot %d):\n",
slot, slot / SLOTS_PER_LSB, slot % SLOTS_PER_LSB);
hexdump(" slot data", s, SLOT_SIZE);
}
}
if (nonzero_count > 0) {
printf("[+] Found %d non-zero slots at BAR 0x%llx + 0x%x\n",
nonzero_count, (unsigned long long)bar, lsb_offsets[lo]);
output_add_section(SECTION_LSB, lsb_data, LSB_TOTAL_SIZE);
found_any = 1;
}
}
/* Also read the queue mask registers */
for (int q = 0; q < 5; q++) {
uint64_t queue_base = bar + CCP_QUEUE_MMIO_BASE + (q * CCP_QUEUE_STRIDE);
void *qmap = mmap(NULL, 0x100, PROT_READ, MAP_SHARED,
dmem_fd, (off_t)queue_base);
if (qmap == MAP_FAILED) continue;
uint32_t pub_lo = *(volatile uint32_t *)((uint8_t *)qmap + CCP_PUBLIC_MASK_LO);
uint32_t pub_hi = *(volatile uint32_t *)((uint8_t *)qmap + CCP_PUBLIC_MASK_HI);
uint32_t priv_lo = *(volatile uint32_t *)((uint8_t *)qmap + CCP_PRIVATE_MASK_LO);
uint32_t priv_hi = *(volatile uint32_t *)((uint8_t *)qmap + CCP_PRIVATE_MASK_HI);
munmap(qmap, 0x100);
if (pub_lo || pub_hi || priv_lo || priv_hi) {
printf("[+] Queue %d masks: pub=0x%08x%08x priv=0x%08x%08x\n",
q, pub_hi, pub_lo, priv_hi, priv_lo);
}
}
}
close(dmem_fd);
if (!found_any) {
printf("[!] No non-zero LSB data found via MMIO\n");
}
return found_any ? 0 : -1;
}
/* ================================================================
* APPROACH 2: EncryptService ioctl Handler Scan
*
* Find the pup_update device's cdevsw in kernel memory, extract
* the d_ioctl handler pointer, dump 4KB of the handler code.
* This is the function called by ioctl 0xC0184402.
* ================================================================ */
static int approach2_ioctl_handler_scan(void) {
printf("\n=== APPROACH 2: EncryptService ioctl Handler Scan ===\n");
uint64_t kernel_bases[] = { KERNEL_BASE_PRIMARY, KERNEL_BASE_FALLBACK };
const char *search_str = "pup_update";
size_t search_len = strlen(search_str);
/* Scan range: first 32MB of kernel text/data */
size_t scan_size = 32 * 1024 * 1024;
uint8_t *kbuf = (uint8_t *)malloc(scan_size);
if (!kbuf) {
printf("[!] malloc failed for kernel scan buffer\n");
return -1;
}
for (int kb = 0; kb < (int)(sizeof(kernel_bases)/sizeof(kernel_bases[0])); kb++) {
uint64_t kbase = kernel_bases[kb];
printf("[*] Scanning kernel at base 0x%llx...\n", (unsigned long long)kbase);
if (kernel_copyout(kbase, kbuf, scan_size) != 0) {
printf("[!] kernel_copyout failed for base 0x%llx\n", (unsigned long long)kbase);
continue;
}
/* Search for "pup_update" string */
for (size_t i = 0; i < scan_size - search_len; i++) {
if (memcmp(&kbuf[i], search_str, search_len) == 0 && kbuf[i + search_len] == '\0') {
uint64_t str_kaddr = kbase + i;
printf("[+] Found \"%s\" at kernel address 0x%llx\n",
search_str, (unsigned long long)str_kaddr);
/*
* Now search for a pointer to this string in kernel memory.
* In FreeBSD 11 cdevsw, d_name is at offset 0x08.
* So cdevsw_base = ptr_location - 0x08.
*/
for (size_t j = 0; j < scan_size - 8; j += 8) {
uint64_t val;
memcpy(&val, &kbuf[j], 8);
if (val == str_kaddr) {
uint64_t ptr_kaddr = kbase + j;
uint64_t cdevsw_base = ptr_kaddr - CDEVSW_D_NAME;
printf("[+] Pointer to string at 0x%llx\n",
(unsigned long long)ptr_kaddr);
printf("[+] Candidate cdevsw at 0x%llx\n",
(unsigned long long)cdevsw_base);
/* Read d_ioctl at cdevsw + 0x30 */
uint64_t d_ioctl = kernel_read64(cdevsw_base + CDEVSW_D_IOCTL);
printf("[+] d_ioctl handler at 0x%llx\n",
(unsigned long long)d_ioctl);
/* Validate: should be in kernel space */
if (d_ioctl < 0xFFFFFFFF80000000ULL) {
printf("[!] d_ioctl address looks invalid, skipping\n");
continue;
}
/* Also read other cdevsw entries for context */
uint64_t d_open = kernel_read64(cdevsw_base + CDEVSW_D_OPEN);
uint64_t d_close = kernel_read64(cdevsw_base + CDEVSW_D_CLOSE);
printf("[*] d_open=0x%llx d_close=0x%llx\n",
(unsigned long long)d_open,
(unsigned long long)d_close);
/* Dump 4KB of the ioctl handler code */
uint8_t handler_code[4096];
if (kernel_copyout(d_ioctl, handler_code, sizeof(handler_code)) == 0) {
printf("[+] Dumped ioctl handler (4096 bytes)\n");
hexdump("ioctl handler first 128 bytes", handler_code, 128);
output_add_section(SECTION_IOCTL, handler_code, sizeof(handler_code));
} else {
printf("[!] Failed to dump ioctl handler code\n");
}
/* Dump the cdevsw struct itself */
uint8_t cdevsw_raw[0x80];
if (kernel_copyout(cdevsw_base, cdevsw_raw, sizeof(cdevsw_raw)) == 0) {
hexdump("cdevsw struct", cdevsw_raw, sizeof(cdevsw_raw));
}
free(kbuf);
return 0;
}
}
}
}
}
free(kbuf);
printf("[!] Could not locate pup_update cdevsw\n");
return -1;
}
/* ================================================================
* APPROACH 3: Instrumented PUP Decrypt (LSB snapshot)
*
* Strategy: take a snapshot of all CCP LSB key slots BEFORE and
* AFTER issuing the DecryptPupHeader ioctl. Any slot that changed
* from zero to non-zero (or changed value) during the call was
* loaded by the security processor for the decryption — that's
* our key candidate.
*
* This requires a real PUP header blob to be present at a known
* physical address. The caller must arrange DMA-able memory.
* ================================================================ */
static int approach3_instrumented_decrypt(void) {
printf("\n=== APPROACH 3: Instrumented PUP Decrypt ===\n");
int pup_fd = open(PUP_DEVICE_PATH, O_RDWR);
if (pup_fd < 0) {
printf("[!] Cannot open %s — device may not exist or access denied\n",
PUP_DEVICE_PATH);
printf("[*] Strategy: snapshot LSB before/after ioctl 0x%x\n",
PUP_IOCTL_DECRYPT_HEADER);
printf("[*] The ioctl argument is 24 bytes:\n");
printf("[*] uint64_t src_paddr — physical address of encrypted header\n");
printf("[*] uint64_t dst_paddr — physical address for decrypted output\n");
printf("[*] uint32_t size — 0x5B0 (PUP header encrypted size)\n");
printf("[*] uint32_t flags — typically 0\n");
printf("[*] The key is loaded into a CCP LSB slot during the ioctl call.\n");
printf("[*] Compare pre/post LSB snapshots to identify which slot changed.\n");
return -1;
}
printf("[+] Opened %s\n", PUP_DEVICE_PATH);
/* Take pre-decrypt LSB snapshot via MMIO */
int dmem_fd = open("/dev/dmem0", O_RDWR);
if (dmem_fd < 0) {
printf("[!] Cannot open /dev/dmem0 for LSB snapshot\n");
close(pup_fd);
return -1;
}
uint64_t lsb_phys = 0x03000000ULL + LSB_OFFSET_A;
uint8_t pre_lsb[LSB_TOTAL_SIZE];
uint8_t post_lsb[LSB_TOTAL_SIZE];
void *mapped = mmap(NULL, LSB_TOTAL_SIZE, PROT_READ, MAP_SHARED,
dmem_fd, (off_t)lsb_phys);
if (mapped == MAP_FAILED) {
printf("[!] Cannot map LSB region for snapshot\n");
close(dmem_fd);
close(pup_fd);
return -1;
}
memcpy(pre_lsb, mapped, LSB_TOTAL_SIZE);
printf("[+] Pre-decrypt LSB snapshot captured\n");
output_add_section(SECTION_SNAP_PRE, pre_lsb, LSB_TOTAL_SIZE);
/*
* Issue the decrypt ioctl.
* NOTE: src/dst must be valid physical addresses containing
* a real encrypted PUP header. This is a placeholder —
* a real implementation must allocate contiguous physical
* memory (e.g. via contigmalloc or /dev/dmem0).
*/
printf("[*] To actually invoke the ioctl, provide valid src/dst paddrs.\n");
printf("[*] Skipping actual ioctl call (would need DMA-able memory with PUP header).\n");
/* Take post-decrypt snapshot */
memcpy(post_lsb, mapped, LSB_TOTAL_SIZE);
munmap(mapped, LSB_TOTAL_SIZE);
printf("[+] Post-decrypt LSB snapshot captured\n");
output_add_section(SECTION_SNAP_POST, post_lsb, LSB_TOTAL_SIZE);
/* Diff the snapshots */
int changes = 0;
for (int slot = 0; slot < LSB_COUNT * SLOTS_PER_LSB; slot++) {
uint8_t *pre = &pre_lsb[slot * SLOT_SIZE];
uint8_t *post = &post_lsb[slot * SLOT_SIZE];
if (memcmp(pre, post, SLOT_SIZE) != 0) {
printf("[+] LSB slot %d CHANGED during decrypt!\n", slot);
hexdump(" before", pre, SLOT_SIZE);
hexdump(" after ", post, SLOT_SIZE);
changes++;
}
}
printf("[*] %d LSB slots changed\n", changes);
close(dmem_fd);
close(pup_fd);
return (changes > 0) ? 0 : -1;
}
/* ================================================================
* APPROACH 4: Kernel Memory Scan
*
* Search kernel memory near known strings for key-like data.
* Strings: "encsrv", "encrypt_service", "pup_key",
* "decrypt_header", "ccp_key"
* Dump 512 bytes of surrounding context for each match.
* ================================================================ */
static int approach4_kernel_memscan(void) {
printf("\n=== APPROACH 4: Kernel Memory Scan ===\n");
const char *needles[] = {
"encsrv",
"encrypt_service",
"pup_key",
"decrypt_header",
"ccp_key",
"dec_pup",
"Titania",
NULL
};
uint64_t kernel_bases[] = { KERNEL_BASE_PRIMARY, KERNEL_BASE_FALLBACK };
size_t scan_size = 32 * 1024 * 1024;
uint8_t *kbuf = (uint8_t *)malloc(scan_size);
if (!kbuf) {
printf("[!] malloc failed\n");
return -1;
}
int total_found = 0;
for (int kb = 0; kb < (int)(sizeof(kernel_bases)/sizeof(kernel_bases[0])); kb++) {
uint64_t kbase = kernel_bases[kb];
printf("[*] Scanning kernel at base 0x%llx...\n", (unsigned long long)kbase);
if (kernel_copyout(kbase, kbuf, scan_size) != 0) {
printf("[!] kernel_copyout failed for 0x%llx\n", (unsigned long long)kbase);
continue;
}
for (int n = 0; needles[n] != NULL; n++) {
size_t nlen = strlen(needles[n]);
for (size_t i = 0; i < scan_size - nlen; i++) {
if (memcmp(&kbuf[i], needles[n], nlen) == 0) {
uint64_t match_addr = kbase + i;
printf("[+] Found \"%s\" at 0x%llx\n",
needles[n], (unsigned long long)match_addr);
/* Dump 512 bytes of surrounding context */
size_t ctx_start = (i >= 256) ? (i - 256) : 0;
size_t ctx_end = i + 256;
if (ctx_end > scan_size) ctx_end = scan_size;
size_t ctx_len = ctx_end - ctx_start;
char label[128];
snprintf(label, sizeof(label), "context around \"%s\"", needles[n]);
hexdump(label, &kbuf[ctx_start], ctx_len);
output_add_section(SECTION_MEMSCAN, &kbuf[ctx_start], (uint32_t)ctx_len);
total_found++;
/* Skip past this match to avoid duplicates */
i += nlen;
}
}
}
if (total_found > 0) break;
}
free(kbuf);
printf("[*] Total string matches: %d\n", total_found);
return (total_found > 0) ? 0 : -1;
}
/* ---------- main ---------- */
int main(int argc, char **argv) {
printf("=== PS5 PUP Key Extractor ===\n");
printf("Target: PUP header decryption key (AES-128)\n");
printf("Output: /data/pup_key_dump.bin\n\n");
output_begin();
int r1 = approach1_ccp_lsb_dump();
int r2 = approach2_ioctl_handler_scan();
int r3 = approach3_instrumented_decrypt();
int r4 = approach4_kernel_memscan();
printf("\n=== Summary ===\n");
printf("Approach 1 (CCP LSB dump): %s\n", r1 == 0 ? "OK" : "FAILED");
printf("Approach 2 (ioctl handler scan): %s\n", r2 == 0 ? "OK" : "FAILED");
printf("Approach 3 (instrumented decrypt):%s\n", r3 == 0 ? "OK" : "FAILED");
printf("Approach 4 (kernel mem scan): %s\n", r4 == 0 ? "OK" : "FAILED");
output_write("/data/pup_key_dump.bin");
printf("\n[*] Done. Transfer /data/pup_key_dump.bin to PC and run analyze_dump.py\n");
return 0;
}
+208
View File
@@ -0,0 +1,208 @@
"""
PUP Header and Segment Parser
Parses PS5 PUP inner structure (magic: 0xEEF51454)
Works on both encrypted PUPs (limited) and decrypted PUPs (full).
"""
import struct
from dataclasses import dataclass
from typing import BinaryIO, Optional
from keys import SEGMENT_NAMES
PUP_MAGIC = 0xEEF51454
@dataclass
class PUPFileHeader:
magic: int
unknown_04: int
unknown_08: int
flags: int
unknown_0B: int
header_size: int
hash_size: int
@property
def is_valid(self) -> bool:
return self.magic == PUP_MAGIC
@dataclass
class PUPHeader:
file_header: PUPFileHeader
file_size: int
segment_count: int
unknown_1A: int
unknown_1C: int
@dataclass
class PUPSegment:
index: int
flags: int
flags2: int
offset: int
compressed_size: int
uncompressed_size: int
@property
def segment_id(self) -> int:
return (self.flags >> 20) & 0xFFF
@property
def segment_type(self) -> int:
return (self.flags >> 28) & 0xF
@property
def is_compressed(self) -> bool:
return bool(self.flags & 0x8)
@property
def is_blocked(self) -> bool:
return bool(self.flags & 0x800)
@property
def is_table(self) -> bool:
return bool(self.flags & 0x1)
@property
def block_size(self) -> int:
return 1 << (((self.flags & 0xF000) >> 12) + 12)
@property
def name(self) -> str:
sid = self.segment_id
if sid in SEGMENT_NAMES:
return SEGMENT_NAMES[sid]
return f"unknown_0x{sid:03X}"
@property
def type_name(self) -> str:
t = self.segment_type
if t == 0xE:
return "additional_sig"
if t == 0xF:
return "watermark"
return f"type_{t:X}"
def __repr__(self) -> str:
flags_str = []
if self.is_compressed:
flags_str.append("COMPRESSED")
if self.is_blocked:
flags_str.append(f"BLOCKED(bs=0x{self.block_size:X})")
if self.is_table:
flags_str.append("TABLE")
flags_info = " ".join(flags_str) if flags_str else "SIMPLE"
special = self.segment_type
if special in (0xE, 0xF):
return (
f"PUPSegment(#{self.index} {self.type_name} "
f"flags=0x{self.flags:08X})"
)
return (
f"PUPSegment(#{self.index} id=0x{self.segment_id:03X} "
f"'{self.name}' offset=0x{self.offset:X} "
f"compressed=0x{self.compressed_size:X} "
f"uncompressed=0x{self.uncompressed_size:X} "
f"{flags_info})"
)
def parse_pup_file_header(data: bytes) -> PUPFileHeader:
"""Parse the 16-byte PUP file header."""
if len(data) < 16:
raise ValueError("Data too small for PUP file header")
magic, unknown_04 = struct.unpack_from("<II", data, 0)
unknown_08, flags, unknown_0B = struct.unpack_from("<HBB", data, 8)
header_size, hash_size = struct.unpack_from("<HH", data, 12)
return PUPFileHeader(
magic=magic,
unknown_04=unknown_04,
unknown_08=unknown_08,
flags=flags,
unknown_0B=unknown_0B,
header_size=header_size,
hash_size=hash_size,
)
def parse_pup_header(data: bytes) -> PUPHeader:
"""Parse the 32-byte PUP header (file_header + extended fields)."""
fh = parse_pup_file_header(data)
file_size = struct.unpack_from("<Q", data, 16)[0]
segment_count, unknown_1A, unknown_1C = struct.unpack_from("<HHI", data, 24)
return PUPHeader(
file_header=fh,
file_size=file_size,
segment_count=segment_count,
unknown_1A=unknown_1A,
unknown_1C=unknown_1C,
)
def parse_pup_segments(data: bytes, segment_count: int, offset: int = 0x20) -> list:
"""Parse segment table entries. Each entry is 32 bytes."""
segments = []
for i in range(segment_count):
pos = offset + (i * 32)
if pos + 32 > len(data):
break
flags, flags2 = struct.unpack_from("<II", data, pos)
seg_offset = struct.unpack_from("<q", data, pos + 8)[0]
compressed_size = struct.unpack_from("<Q", data, pos + 16)[0]
uncompressed_size = struct.unpack_from("<Q", data, pos + 24)[0]
segments.append(PUPSegment(
index=i,
flags=flags,
flags2=flags2,
offset=seg_offset,
compressed_size=compressed_size,
uncompressed_size=uncompressed_size,
))
return segments
def try_parse_pup(data: bytes) -> Optional[dict]:
"""
Try to parse PUP data. Works fully on decrypted PUPs.
On encrypted PUPs, can only verify magic and show raw header bytes.
"""
if len(data) < 16:
return None
magic = struct.unpack_from("<I", data, 0)[0]
if magic != PUP_MAGIC:
return None
result = {"magic": magic, "magic_valid": True, "encrypted": False}
try:
header = parse_pup_header(data)
result["header"] = header
if header.segment_count > 0 and header.segment_count < 1000:
segments = parse_pup_segments(data, header.segment_count)
result["segments"] = segments
# Sanity check: if segment offsets look reasonable, data is likely decrypted
for seg in segments:
if seg.segment_type in (0xE, 0xF):
continue
if seg.offset > len(data) or seg.offset < 0:
result["encrypted"] = True
break
else:
result["encrypted"] = True
except Exception:
result["encrypted"] = True
return result
+1
View File
@@ -0,0 +1 @@
pycryptodome