mirror of
https://github.com/deepkicks/TitaniaPy.git
synced 2026-10-17 15:00:31 +02:00
Add files via upload
This commit is contained in:
1 parent
43726fbbe4
commit
aec31c7b18
11 files changed
+2510
-2
No files matched your search
@@ -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.*
|
||||
@@ -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
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
@@ -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)",
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
pycryptodome
|
||||
Reference in new issue
Block a user