From 9bc84bf840954a85986e6af3725e8ce4a5a1c571 Mon Sep 17 00:00:00 2001 From: deepkicks <77412763+deepkicks@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:01:25 -0500 Subject: [PATCH] Add files via upload Add ioctl map from ps5-pup-decrypt source, improve approach 3 and auto-analysis - Mapped all 6 ioctl codes for /dev/pup_update0 from the on-console ps5-pup-decrypt encryptsrv.c source (VerifyBlsHeader through DecryptSegmentBlock) - Discovered type dispatch: DecryptHeader takes a type arg (0/1/2) that selects which key to use. PS5UPDATE1.PUP = type 1 (system update key) - analyze_dump.py now detects all 6 ioctl codes and type dispatch patterns (cmp 0/1/2) in handler disassembly, labels them in output - ps5_key_extractV2.c approach 3 now actually invokes DecryptHeader ioctl with type=1 between LSB snapshots instead of skipping the call - Added automated capstone disassembly engine to analyze_dump.py with --save-handlers flag for manual Ghidra/IDA fallback - Updated RESEARCH_POST.md with ioctl table and type dispatch finding - Updated README.md and EXTRACTION_GUIDE.md for automated analysis flow --- README.md | 22 +- RESEARCH_POST.md | 21 +- analyze_dump.py | 813 ++++++++++++++++++++++++++++++++++---------- ps5_key_extractV3.c | 738 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1400 insertions(+), 194 deletions(-) create mode 100644 ps5_key_extractV3.c diff --git a/README.md b/README.md index 4e6a88d..2f0d515 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The tool handles 3 of 4 crypto layers. **Layer 1 (PUP container encryption) is t pip install -r requirements.txt ``` -Requires Python 3.10+ and [pycryptodome](https://pypi.org/project/pycryptodome/). +Requires Python 3.10+, [pycryptodome](https://pypi.org/project/pycryptodome/), and [capstone](https://pypi.org/project/capstone/) (for automated handler disassembly). ## Usage @@ -61,27 +61,28 @@ python main.py decompress segment.bin -o decompressed.bin ### 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 | +| `ps5_key_extract.c` | C payload — 5 approaches to extract the missing PUP container key | +| `analyze_dump.py` | Parses the dump, tests key candidates, and auto-disassembles handlers to find the key source | ### Documentation | File | Description | |------|-------------| | `RESEARCH_POST.md` | Full technical write-up of findings | +| `EXTRACTION_GUIDE.md` | Step-by-step walkthrough for running the extraction | ## Key Extraction Payload (v2) If you have a jailbroken PS5 on FW 13.60 or below, `ps5_key_extract.c` implements a **chained extraction strategy**: -**The chain: Approach 2 → Approach 1/5** +**The chain: Approach 2 → automated RE → Approach 5** 1. Dump the `pup_update0` ioctl/open/close handler code (only needs kernel R/W) -2. Disassemble on PC to find where the key comes from -3. Re-run the payload with `-a
` to read that specific location +2. Run `analyze_dump.py` on PC — it auto-disassembles the handlers and finds the key source +3. Re-run the payload with the `-a
` command it gives you **All five approaches:** 1. **CCP LSB Key Slot Dump + Queue Registers** — Reads all 128 hardware key slots AND queue config registers (shows which slots are CPU-readable) 2. **Full EncryptService Handler Dump** — Dumps 16KB each of `d_ioctl`, `d_open`, and `d_close` handlers plus the full `cdevsw` struct -3. **Instrumented PUP Decrypt** — Pre/post LSB snapshots around a decrypt call +3. **Instrumented PUP Decrypt** — Invokes `DecryptHeader` ioctl with type=1 (system update key) between pre/post LSB snapshots to catch key loading 4. **Kernel Memory Scan** — Searches near crypto-related strings 5. **Targeted Address Read** — Re-run mode: reads a specific kernel or physical address after RE reveals the key location @@ -89,12 +90,15 @@ If you have a jailbroken PS5 on FW 13.60 or below, `ps5_key_extract.c` implement # Full scan — run all approaches ./ps5_key_extract -# Targeted read — after disassembling the handler dump +# Targeted read — using the address from analyze_dump.py's recommendations ./ps5_key_extract -a 0xFFFFFFFF82345678 ./ps5_key_extract -a 0xFFFFFFFF82345678 -s 256 -# Analyze on PC +# Analyze on PC (tests keys + auto-disassembles handlers) python analyze_dump.py pup_key_dump.bin --pup14 PS5UPDATE14.00.PUP --pup14_1 PS5UPDATE14.10.PUP + +# Save handler .bin files for manual Ghidra/IDA analysis +python analyze_dump.py pup_key_dump.bin --save-handlers ``` **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. diff --git a/RESEARCH_POST.md b/RESEARCH_POST.md index ad53e2d..95df572 100644 --- a/RESEARCH_POST.md +++ b/RESEARCH_POST.md @@ -147,7 +147,7 @@ The payload finds the `pup_update0` device driver's `cdevsw` struct in kernel me - `d_open` handler — key loading may happen when the device is opened - `d_close` handler — cleanup code that may reference the key -Transfer the dump to PC, load the handler bytes into Ghidra/IDA as x86-64 code. The disassembly shows exactly how the key gets loaded: which CCP slot, which kernel address, or which PSP mailbox command provides it. +Transfer the dump to PC and run `analyze_dump.py`. It **automatically disassembles** the handler code using capstone, searches for kernel address references, CCP slot numbers, AES key size constants, and type-dispatch patterns, then outputs a ranked list of key source candidates with the exact `-a` command for a second payload run. No manual Ghidra work needed in most cases. **Step 2 — Target what you found:** Based on what the handler reveals: @@ -165,13 +165,28 @@ Based on what the handler reveals: 2. **Full EncryptService Handler Dump** — Dumps the complete `cdevsw` struct (all function pointers) plus 16KB of `d_ioctl`, `d_open`, and `d_close` handler code. This is the most reliable approach — it only needs kernel R/W, which Byepervisor Reborn provides. -3. **Instrumented PUP Decrypt** — Snapshots all CCP LSB slots before and after a decrypt ioctl. Note: PSP may lock out after boot on newer firmware, making this less reliable. +3. **Instrumented PUP Decrypt** — Snapshots all CCP LSB slots before and after invoking the `DecryptHeader` ioctl (`0xC0184402`) with type=1 (system update key). Even if the decrypt fails on a dummy header, the security processor may still load the key into CCP slots — the pre/post diff catches it. Note: PSP may lock out after boot on newer firmware, making this less reliable. 4. **Kernel Memory Scan** — Searches near strings like `encsrv`, `decrypt_header`, `sceSblSsDecryptPupHeader`, `Titania` and dumps surrounding context. 5. **Targeted Address Read** — Re-run mode for after you've analyzed the handler dump on PC. Pass `-a 0xADDRESS` to read from a specific kernel or physical address. Automatically detects whether the address is virtual (uses `kernel_copyout`) or physical (uses MMIO mapping). -All operations are **read-only** — nothing is written to kernel or CCP state. Output goes to `/data/pup_key_dump.bin`, which you transfer to PC and run through the companion `analyze_dump.py` script. That script automatically parses all section types, displays CCP queue register masks, shows which slots are CPU-readable, and tests every captured key candidate against encrypted PUP headers from FW 14.00 and 14.10. +All operations are **read-only** — nothing is written to kernel or CCP state. Output goes to `/data/pup_key_dump.bin`, which you transfer to PC and run through the companion `analyze_dump.py` script. That script automatically parses all section types, displays CCP queue register masks, shows which slots are CPU-readable, tests every captured key candidate against encrypted PUP headers from FW 14.00 and 14.10, and then **auto-disassembles the handler code** to find where the key comes from. + +### Key Discovery: Full Ioctl Map and Type Dispatch + +By studying the `ps5-pup-decrypt` on-console tool source, I mapped the complete ioctl interface for `/dev/pup_update0`: + +| Ioctl Code | Name | Args | +|---|---|---| +| `0xC0104401` | VerifyBlsHeader | buffer, length, unknown | +| `0xC0184402` | **DecryptHeader** | buffer, length, **type** | +| `0xC0184403` | VerifyPupAdditionalSign | index, buffer, length | +| `0xC0184404` | VerifyPupWatermark | index, buffer, length | +| `0xC0184405` | DecryptSegment | index, buffer, length | +| `0xC0284406` | DecryptSegmentBlock | entry_idx, block_idx, block_buf, block_len, table_buf, table_len | + +The critical insight: `DecryptHeader` takes a **type** parameter (0, 1, or 2) that selects which key to use. `PS5UPDATE1.PUP` (system updates) = type 1, `PS5UPDATE2.PUP` (recovery) = type 0. This means the `d_ioctl` handler has a **dispatch branch** — `cmp type, 1` followed by a branch to the code that loads the system update key. Our automated disassembly specifically tracks this pattern and highlights it in the output. **Requirements:** A PS5 on FW 13.60 or below with kernel R/W access (Byepervisor Reborn + Relapse + etaHEN or similar). Compile for FreeBSD 11 amd64 and load via payload loader. You'll need to plug in your framework's `kernel_read64` / `kernel_copyout` functions. diff --git a/analyze_dump.py b/analyze_dump.py index ee5291d..43e3f15 100644 --- a/analyze_dump.py +++ b/analyze_dump.py @@ -1,16 +1,18 @@ """ -analyze_dump.py — Parse pup_key_dump.bin and test key candidates against PUP headers. +analyze_dump.py — Parse pup_key_dump.bin, test key candidates, and auto-disassemble handlers. -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. +Reads the structured dump from ps5_key_extract.c v2, extracts all key candidates +from CCP LSB slots, tries AES-128-CBC/ECB decryption against encrypted PUP headers, +and automatically disassembles handler code to find where the key comes from. 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 + python analyze_dump.py pup_key_dump.bin --save-handlers # also save .bin files for Ghidra """ import argparse +import re import struct import sys from pathlib import Path @@ -21,8 +23,14 @@ except ImportError: print("ERROR: pycryptodome required. Install with: pip install pycryptodome") sys.exit(1) +try: + from capstone import Cs, CS_ARCH_X86, CS_MODE_64 + HAS_CAPSTONE = True +except ImportError: + HAS_CAPSTONE = False + # --- Dump format constants (must match ps5_key_extract.c v2) --- -DUMP_MAGIC = 0x505550444D50 # "PUPDMP\0\0" as uint64 LE +DUMP_MAGIC = 0x505550444D50 SECTION_LSB = 0x01 SECTION_IOCTL = 0x02 SECTION_SNAP_PRE = 0x03 @@ -47,7 +55,8 @@ SECTION_NAMES = { SECTION_HANDLER_CLOSE: "d_close Handler Code", } -# CCP queue register names (11 regs x 4 bytes each, per queue) +HANDLER_SECTIONS = {SECTION_IOCTL, SECTION_HANDLER_OPEN, SECTION_HANDLER_CLOSE} + CCP_QUEUE_REG_NAMES = [ "CONTROL", "TAIL_LO", "HEAD_LO", "INT_ENABLE", "INT_STATUS", "STATUS", "PUBLIC_MASK_LO", "PUBLIC_MASK_HI", @@ -55,47 +64,63 @@ CCP_QUEUE_REG_NAMES = [ ] CCP_NUM_QUEUES = 5 -# LSB geometry LSB_COUNT = 8 SLOTS_PER_LSB = 16 SLOT_SIZE = 32 -LSB_TOTAL_SIZE = LSB_COUNT * SLOTS_PER_LSB * SLOT_SIZE # 4096 +LSB_TOTAL_SIZE = LSB_COUNT * SLOTS_PER_LSB * SLOT_SIZE -# PUP header constants PUP_HEADER_OFFSET = 0x410 -PUP_HEADER_READ_SIZE = 64 # read enough for testing +PUP_HEADER_READ_SIZE = 64 -# 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" +# Crypto-related strings to watch for in call target vicinity +CRYPTO_HINTS = [ + "ccp", "aes", "sbl", "encsrv", "crypto", "cipher", "decrypt", "encrypt", + "key", "lsb", "passthru", "sha", "hmac", +] -def parse_dump(data: bytes) -> dict: - """Parse the structured dump file.""" +# Known ioctl codes from ps5-pup-decrypt encryptsrv.c +IOCTL_MAP = { + 0xC0104401: "VerifyBlsHeader", + 0xC0184402: "DecryptHeader", + 0xC0184403: "VerifyPupAdditionalSign", + 0xC0184404: "VerifyPupWatermark", + 0xC0184405: "DecryptSegment", + 0xC0284406: "DecryptSegmentBlock", +} + +# decrypt_header_args struct: { void* buffer, size_t length, int type } +# type comes from translate_type(): 0,3->0 1,4->1 2,5->2 +# PS5UPDATE1.PUP = type 1, PS5UPDATE2.PUP = type 0 + + +# ===================================================================== +# Dump parsing +# ===================================================================== + +def parse_dump(data: bytes) -> dict | None: if len(data) < 16: print(f"[!] Dump file too small ({len(data)} bytes)") return None magic, version, section_count = struct.unpack_from(" dict: section_data = data[soffset:] if soffset < len(data) else b"" sections.append({ - "type": stype, - "size": ssize, - "offset": soffset, - "data": section_data, - "name": name, + "type": stype, "size": ssize, "offset": soffset, + "data": section_data, "name": name, }) return {"version": version, "sections": sections} +# ===================================================================== +# Key candidate extraction +# ===================================================================== + def extract_key_candidates(sections: list) -> list: - """Extract all possible 16-byte key candidates from LSB sections.""" candidates = [] seen = set() @@ -127,44 +152,35 @@ def extract_key_candidates(sections: list) -> list: 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, + "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, + "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 + for i in range(0, len(data) - 15, 4): window = data[i:i + 16] if window == b"\x00" * 16: continue @@ -174,18 +190,14 @@ def extract_key_candidates(sections: list) -> list: candidates.append({ "key": window, "source": f"{sec['name']} @ offset 0x{i:x}", - "slot": -1, - "full_slot": None, + "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 - + pre_data = post_data = None for sec in sections: if sec["type"] == SECTION_SNAP_PRE: pre_data = sec["data"] @@ -196,39 +208,33 @@ def find_lsb_diff(sections: list) -> list: return [] changed = [] - min_len = min(len(pre_data), len(post_data)) - total_slots = min_len // SLOT_SIZE - + total_slots = min(len(pre_data), len(post_data)) // 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, - }) + changed.append({"slot": slot_idx, "before": pre_slot, "after": post_slot}) return changed +# ===================================================================== +# PUP header testing +# ===================================================================== + 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) @@ -236,106 +242,79 @@ def read_pup_header(pup_path: str, offset: int = PUP_HEADER_OFFSET, 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(" 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 + enc_block = header_data[:32] try: - cipher = AES.new(key, AES.MODE_CBC, iv=b"\x00" * 16) - dec_cbc = cipher.decrypt(enc_block) + dec_cbc = AES.new(key, AES.MODE_CBC, iv=b"\x00" * 16).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) + dec_ecb = AES.new(key, AES.MODE_ECB).decrypt(enc_block) if validate_decrypted_header(dec_ecb, bls_size, f"{pup_name} (ECB)"): matched = True except (ValueError, KeyError): pass - return matched +# ===================================================================== +# Display helpers +# ===================================================================== + 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] - ) + 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 display_cdevsw(data: bytes): - """Display the pup_update0 cdevsw struct with labeled function pointers.""" +def display_cdevsw(data: bytes) -> dict: print(f"\n--- cdevsw Struct (pup_update0) ---") + ptrs = {} if len(data) < 0x58: print(f" [!] Too small ({len(data)} bytes)") - return + return ptrs fields = [ - (0x00, "d_version"), - (0x04, "d_flags"), - (0x08, "d_name (ptr)"), - (0x10, "d_open"), - (0x18, "d_close"), - (0x20, "d_read"), - (0x28, "d_write"), - (0x30, "d_ioctl"), - (0x38, "d_poll"), - (0x40, "d_mmap"), - (0x48, "d_strategy"), - (0x58, "d_kqfilter"), + (0x00, "d_version"), (0x04, "d_flags"), (0x08, "d_name (ptr)"), + (0x10, "d_open"), (0x18, "d_close"), (0x20, "d_read"), + (0x28, "d_write"), (0x30, "d_ioctl"), (0x38, "d_poll"), + (0x40, "d_mmap"), (0x48, "d_strategy"), (0x58, "d_kqfilter"), ] for off, name in fields: if off + 8 <= len(data): val = struct.unpack_from(" dict: print(f"\n--- CCP Queue Registers ---") + info = {"readable_slots": set(), "protected_slots": set()} regs_per_queue = len(CCP_QUEUE_REG_NAMES) bytes_per_queue = regs_per_queue * 4 num_queues = min(CCP_NUM_QUEUES, len(data) // bytes_per_queue) @@ -345,8 +324,7 @@ def display_ccp_regs(data: bytes): print(f" Queue {q}:") for r in range(regs_per_queue): val = struct.unpack_from(" dict | None: + """Disassemble handler code and find key-related patterns.""" + if not HAS_CAPSTONE: + return None + + md = Cs(CS_ARCH_X86, CS_MODE_64) + md.detail = True + + results = { + "label": label, + "base_addr": base_addr, + "kernel_addr_refs": [], + "rip_relative_refs": [], + "call_targets": [], + "slot_candidates": [], + "aes_size_refs": [], + "ioctl_cmd_refs": [], + "type_dispatch_refs": [], + "instruction_count": 0, + } + + for insn in md.disasm(code, base_addr): + results["instruction_count"] += 1 + mnem = insn.mnemonic + ops = insn.op_str + + # 1) Kernel address immediates (mov reg, 0xffffffff8...) + for match in re.finditer(r'0x(f{8,16}[0-9a-f]+)', ops, re.I): + val = int(match.group(), 16) + if val > 0xFFFFFFFF00000000: + val = val & 0xFFFFFFFFFFFFFFFF + if 0xFFFFFFFF80000000 <= val <= 0xFFFFFFFFFFFFFFFF: + results["kernel_addr_refs"].append({ + "addr": insn.address, + "target": val, + "insn": f"{mnem} {ops}", + }) + + # 2) RIP-relative LEA/MOV (data references) + if mnem in ("lea", "mov") and "rip" in ops.lower(): + rip_match = re.search(r'\[rip\s*([+-])\s*0x([0-9a-f]+)\]', ops, re.I) + if rip_match: + sign = 1 if rip_match.group(1) == '+' else -1 + disp = int(rip_match.group(2), 16) + target = insn.address + insn.size + sign * disp + results["rip_relative_refs"].append({ + "addr": insn.address, + "target": target & 0xFFFFFFFFFFFFFFFF, + "insn": f"{mnem} {ops}", + "type": mnem, + }) + + # 3) Call targets + if mnem == "call": + call_match = re.match(r'0x([0-9a-f]+)', ops, re.I) + if call_match: + target = int(call_match.group(), 16) & 0xFFFFFFFFFFFFFFFF + results["call_targets"].append({ + "addr": insn.address, + "target": target, + }) + + # 4) Small immediate values (CCP slot candidates) + # Only in mov/or instructions, values 0-127 + if mnem in ("mov", "or", "and", "cmp", "test"): + for match in re.finditer(r'(? TARGET: 0x{ref['target']:x}") + print(f" Try: ./ps5_key_extract -a 0x{ref['target']:x} -s 256") + else: + print(f"\n [HIGH] Kernel Address References: none found") + + # RIP-relative data references (HIGH priority) + rip_refs = results["rip_relative_refs"] + if rip_refs: + seen_targets = {} + for ref in rip_refs: + t = ref["target"] + if t not in seen_targets: + seen_targets[t] = ref + unique = list(seen_targets.values()) + + print(f"\n [HIGH] RIP-Relative Data References ({len(unique)} unique):") + print(f" These load data from nearby kernel memory — could be key tables.") + for ref in unique[:20]: + print(f" 0x{ref['addr']:x}: {ref['insn']}") + print(f" -> DATA AT: 0x{ref['target']:x}") + print(f" Try: ./ps5_key_extract -a 0x{ref['target']:x} -s 256") + else: + print(f"\n [HIGH] RIP-Relative Data References: none found") + + # Slot candidates (MEDIUM priority) + slots = results["slot_candidates"] + if slots: + # Count frequency of each value + freq = {} + for s in slots: + v = s["value"] + if v not in freq: + freq[v] = [] + freq[v].append(s) + + # Filter: values 0-15 used more than once are interesting (CCP slot numbers) + interesting = {v: refs for v, refs in freq.items() + if v <= 15 and len(refs) >= 1} + # Also flag anything 16-127 that appears 2+ times + interesting.update({v: refs for v, refs in freq.items() + if 16 <= v <= 127 and len(refs) >= 2}) + + if interesting: + print(f"\n [MED] Potential CCP Slot Numbers:") + for v in sorted(interesting.keys()): + refs = interesting[v] + print(f" Slot {v} — referenced {len(refs)} time(s):") + for ref in refs[:3]: + print(f" 0x{ref['addr']:x}: {ref['insn']}") + else: + print(f"\n [MED] Potential CCP Slot Numbers: none found") + + # AES key size references + aes_refs = results["aes_size_refs"] + if aes_refs: + print(f"\n [MED] AES Key Size References:") + for ref in aes_refs[:10]: + bits = ref["size"] * 8 + print(f" 0x{ref['addr']:x}: {ref['insn']} (AES-{bits})") + + # Call targets (LOW priority — for mapping the call graph) + calls = results["call_targets"] + if calls: + # Deduplicate + unique_targets = {} + for c in calls: + t = c["target"] + if t not in unique_targets: + unique_targets[t] = 0 + unique_targets[t] += 1 + + print(f"\n [LOW] Function Calls ({len(unique_targets)} unique targets):") + for target, count in sorted(unique_targets.items(), key=lambda x: -x[1])[:15]: + in_handler = base <= target < base + 16384 + loc = "(within this handler)" if in_handler else "(external function)" + print(f" 0x{target:x} — called {count}x {loc}") + + # Ioctl command references + if results["ioctl_cmd_refs"]: + print(f"\n [INFO] Ioctl Command References:") + for ref in results["ioctl_cmd_refs"]: + name = ref.get("ioctl", "unknown") + print(f" 0x{ref['addr']:x}: {ref['insn']} ({name})") + + # Type dispatch (key selection based on PUP type 0/1/2) + type_refs = results.get("type_dispatch_refs", []) + if type_refs: + print(f"\n [HIGH] Type Dispatch Detection ({len(type_refs)} cmp instructions):") + print(f" The handler selects a key based on a 'type' argument (0, 1, or 2).") + print(f" Trace the branch targets from these comparisons to find the key load.") + for ref in type_refs[:10]: + print(f" 0x{ref['addr']:x}: {ref['insn']}") + print(f" Context: PS5UPDATE1.PUP = type 1, PS5UPDATE2.PUP = type 0") + print(f" The code after 'cmp ..., 1' + branch leads to the key for system updates.") + + +def generate_recommendations(all_analysis: list, ccp_info: dict | None): + """Combine analysis from all handlers and generate actionable next steps.""" + print(f"\n{'=' * 70}") + print(f" AUTOMATED RECOMMENDATIONS") + print(f"{'=' * 70}") + + # Collect all findings across all handlers + all_kaddr_targets = [] + all_rip_targets = [] + all_slots = {} + all_ioctls = [] + all_type_dispatch = [] + has_aes128 = False + has_aes256 = False + + for analysis in all_analysis: + if analysis is None: + continue + for ref in analysis["kernel_addr_refs"]: + all_kaddr_targets.append(ref) + for ref in analysis["rip_relative_refs"]: + all_rip_targets.append(ref) + for ref in analysis["slot_candidates"]: + v = ref["value"] + if v not in all_slots: + all_slots[v] = 0 + all_slots[v] += 1 + for ref in analysis["aes_size_refs"]: + if ref["size"] == 0x10: + has_aes128 = True + elif ref["size"] == 0x20: + has_aes256 = True + for ref in analysis.get("ioctl_cmd_refs", []): + all_ioctls.append(ref) + for ref in analysis.get("type_dispatch_refs", []): + all_type_dispatch.append(ref) + + # Deduplicate kernel targets + unique_kaddrs = list({ref["target"]: ref for ref in all_kaddr_targets}.values()) + unique_rip = list({ref["target"]: ref for ref in all_rip_targets}.values()) + + # Score and rank targets + ranked_targets = [] + + for ref in unique_kaddrs: + ranked_targets.append({ + "type": "kernel_addr", + "target": ref["target"], + "confidence": "HIGH", + "reason": f"Direct kernel address reference at 0x{ref['addr']:x}", + "command": f"./ps5_key_extract -a 0x{ref['target']:x} -s 256", + }) + + for ref in unique_rip: + ranked_targets.append({ + "type": "rip_data", + "target": ref["target"], + "confidence": "HIGH", + "reason": f"RIP-relative data load at 0x{ref['addr']:x}", + "command": f"./ps5_key_extract -a 0x{ref['target']:x} -s 256", + }) + + # Slot-based targets + readable_slots = ccp_info.get("readable_slots", set()) if ccp_info else set() + protected_slots = ccp_info.get("protected_slots", set()) if ccp_info else set() + + frequent_slots = sorted( + [(v, c) for v, c in all_slots.items() if v <= 15 and c >= 2], + key=lambda x: -x[1] + ) + for slot_val, count in frequent_slots: + if slot_val in readable_slots: + ranked_targets.append({ + "type": "ccp_slot_readable", + "target": slot_val, + "confidence": "HIGH", + "reason": f"CCP slot {slot_val} referenced {count}x and IS CPU-readable", + "command": f"Check LSB dump section for slot {slot_val}", + }) + elif slot_val in protected_slots: + ranked_targets.append({ + "type": "ccp_slot_protected", + "target": slot_val, + "confidence": "MED", + "reason": f"CCP slot {slot_val} referenced {count}x but is hardware-PROTECTED", + "command": "Need to find where this slot is loaded from (trace the loading code)", + }) + else: + ranked_targets.append({ + "type": "ccp_slot_unknown", + "target": slot_val, + "confidence": "MED", + "reason": f"CCP slot {slot_val} referenced {count}x (protection status unknown)", + "command": f"./ps5_key_extract -a 0x{slot_val:x} # if physical CCP offset known", + }) + + # Print recommendations + if not ranked_targets: + print("\n No high-confidence key source candidates identified.") + print(" Manual Ghidra/IDA analysis of the handler .bin files is needed.") + print(" Save handlers with: python analyze_dump.py dump.bin --save-handlers") + return ranked_targets + + # Sort by confidence + conf_order = {"HIGH": 0, "MED": 1, "LOW": 2} + ranked_targets.sort(key=lambda x: conf_order.get(x["confidence"], 99)) + + print(f"\n Found {len(ranked_targets)} potential key source(s):\n") + + for i, t in enumerate(ranked_targets): + print(f" [{t['confidence']}] Candidate {i + 1}: ", end="") + if t["type"] in ("kernel_addr", "rip_data"): + print(f"Kernel address 0x{t['target']:x}") + elif t["type"].startswith("ccp_slot"): + print(f"CCP slot {t['target']}") + print(f" Reason: {t['reason']}") + print(f" Action: {t['command']}") + print() + + # Key size hint + if has_aes128 and not has_aes256: + print(" [INFO] AES-128 (16-byte) key size detected in handler code.") + elif has_aes256 and not has_aes128: + print(" [INFO] AES-256 (32-byte) key size detected — use -s 32 on reads.") + elif has_aes128 and has_aes256: + print(" [INFO] Both AES-128 and AES-256 references found — try both key sizes.") + + # Ioctl dispatch summary + if all_ioctls: + found_names = set(ref.get("ioctl", "") for ref in all_ioctls) + print(f"\n [INFO] Ioctl dispatch codes found: {', '.join(sorted(found_names))}") + if "DecryptHeader" in found_names: + print(" DecryptHeader (0xC0184402) is present — this handler does PUP decryption.") + + # Type dispatch summary + if all_type_dispatch: + type_vals = sorted(set(ref["value"] for ref in all_type_dispatch)) + print(f"\n [INFO] Type dispatch detected (cmp against: {type_vals})") + print(" The handler uses a 'type' arg to select different keys:") + print(" type 0 = PS5UPDATE2.PUP (recovery update)") + print(" type 1 = PS5UPDATE1.PUP (system update) <-- primary target") + print(" type 2 = unknown/reserved") + print(" Trace the branch after 'cmp ..., 1' to find the system update key.") + + # Summary action + high_targets = [t for t in ranked_targets if t["confidence"] == "HIGH"] + if high_targets: + best = high_targets[0] + print(f"\n >>> RECOMMENDED NEXT STEP:") + print(f" {best['command']}") + print(f" Then re-run: python analyze_dump.py pup_key_dump.bin") + + return ranked_targets + + +def save_handler_binaries(sections: list, cdevsw_ptrs: dict, output_dir: Path): + """Save handler sections as .bin files for Ghidra/IDA import.""" + output_dir.mkdir(parents=True, exist_ok=True) + + section_to_name = { + SECTION_IOCTL: ("d_ioctl", "d_ioctl"), + SECTION_HANDLER_OPEN: ("d_open", "d_open"), + SECTION_HANDLER_CLOSE: ("d_close", "d_close"), + SECTION_CDEVSW: ("cdevsw", None), + } + + for sec in sections: + if sec["type"] in section_to_name: + filename, ptr_key = section_to_name[sec["type"]] + outpath = output_dir / f"{filename}.bin" + outpath.write_bytes(sec["data"]) + addr = cdevsw_ptrs.get(ptr_key, 0) if ptr_key else 0 + print(f" Saved {outpath} ({len(sec['data'])} bytes)", end="") + if addr: + print(f" — load at base 0x{addr:x} in Ghidra") + else: + print() + + # Write a Ghidra loader script + script_path = output_dir / "ghidra_import.py" + lines = ["# Ghidra script: run in Ghidra's Script Manager after importing .bin files", + "# This sets the correct base addresses for each handler binary.", ""] + for sec in sections: + if sec["type"] in section_to_name: + filename, ptr_key = section_to_name[sec["type"]] + if ptr_key and ptr_key in cdevsw_ptrs: + addr = cdevsw_ptrs[ptr_key] + lines.append(f"# {filename}.bin: base address = 0x{addr:x}") + lines.append("") + lines.append("# Import each .bin as Raw Binary, x86:LE:64:default") + lines.append("# Set the base address listed above for each file") + script_path.write_text("\n".join(lines)) + print(f" Saved {script_path} — Ghidra base address reference") + + +# ===================================================================== +# Main +# ===================================================================== def main(): - parser = argparse.ArgumentParser(description="Analyze PS5 PUP key dump") + parser = argparse.ArgumentParser( + description="Analyze PS5 PUP key dump — test keys and auto-disassemble handlers") 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") + parser.add_argument("--save-handlers", action="store_true", + help="Save handler .bin files for Ghidra/IDA import") 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}") @@ -386,113 +819,129 @@ def main(): if parsed is None: sys.exit(1) - # Display handler and metadata sections + # ---- Phase 1: Display metadata sections ---- + cdevsw_ptrs = {} + ccp_info = None + for sec in parsed["sections"]: - if sec["type"] == SECTION_IOCTL: - hexdump_section(sec["data"], "d_ioctl Handler Code (DecryptPupHeader)") - print(f" Save this {len(sec['data'])}-byte blob and load into Ghidra/IDA as x86-64") - elif sec["type"] == SECTION_HANDLER_OPEN: - hexdump_section(sec["data"], "d_open Handler Code") - print(f" Key loading may happen here — disassemble alongside d_ioctl") - elif sec["type"] == SECTION_HANDLER_CLOSE: - hexdump_section(sec["data"], "d_close Handler Code") - elif sec["type"] == SECTION_CDEVSW: - display_cdevsw(sec["data"]) + if sec["type"] == SECTION_CDEVSW: + cdevsw_ptrs = display_cdevsw(sec["data"]) elif sec["type"] == SECTION_CCP_REGS: - display_ccp_regs(sec["data"]) + ccp_info = display_ccp_regs(sec["data"]) elif sec["type"] == SECTION_ADDR_READ: hexdump_section(sec["data"], "Targeted Address Read") elif sec["type"] == SECTION_MEMSCAN: hexdump_section(sec["data"], "Kernel Memory Scan Match") - # Check for LSB diff (approach 3) + # ---- Phase 2: Test LSB key candidates ---- 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 = [] + if candidates and pup_headers: + print(f"\n[*] Testing {len(candidates)} candidates against {len(pup_headers)} PUP files...") + 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']}") - for i, cand in enumerate(candidates): - if i > 0 and i % 100 == 0: - print(f" ... tested {i}/{len(candidates)}") + 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"], + }) + elif candidates and not pup_headers: + print("[!] No PUP files available for testing") + print("[*] Listing key candidates:\n") + for i, cand in enumerate(candidates[:30]): + print(f" {i}: {cand['key'].hex()} — {cand['source']}") - 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 + # ---- Phase 2 results ---- 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']}") + print(f"{'=' * 60}") + print("\n[+] Add this key to keys.py as PUP_CONTAINER_KEY and you're done!") + return 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("[*] No matching keys in LSB data — moving to handler analysis...") + print(f"{'=' * 60}") + + # ---- Phase 3: Automated handler disassembly ---- + if not HAS_CAPSTONE: + print("\n[!] capstone not installed — skipping automated disassembly") + print(" Install with: pip install capstone") + print(" Or manually extract handler .bin files with --save-handlers") + else: + print(f"\n[*] Automated handler disassembly (capstone)...") + + handler_map = { + SECTION_IOCTL: ("d_ioctl (DecryptPupHeader)", "d_ioctl"), + SECTION_HANDLER_OPEN: ("d_open (device open)", "d_open"), + SECTION_HANDLER_CLOSE: ("d_close (device close)", "d_close"), + } + + all_analysis = [] + for sec in parsed["sections"]: + if sec["type"] in handler_map: + label, ptr_key = handler_map[sec["type"]] + base_addr = cdevsw_ptrs.get(ptr_key, 0) + if base_addr == 0: + base_addr = 0xFFFFFFFF82200000 + print(f"\n[!] No cdevsw base for {label}, using default 0x{base_addr:x}") + + analysis = analyze_handler(sec["data"], base_addr, label) + if analysis: + all_analysis.append(analysis) + print_analysis(analysis) + + # Generate combined recommendations + if all_analysis: + generate_recommendations(all_analysis, ccp_info) + + # ---- Save handler binaries if requested ---- + if args.save_handlers: + out_dir = dump_path.parent / "handlers" + print(f"\n[*] Saving handler binaries to {out_dir}/") + save_handler_binaries(parsed["sections"], cdevsw_ptrs, out_dir) + + # ---- Final summary ---- + print(f"\n{'=' * 60}") + print("[*] Analysis complete.") + if not matches: + print("[*] No key found in direct LSB data.") + print("[*] Check the AUTOMATED RECOMMENDATIONS above for next steps.") + if not args.save_handlers: + print("[*] For manual Ghidra RE, re-run with --save-handlers") print(f"{'=' * 60}") diff --git a/ps5_key_extractV3.c b/ps5_key_extractV3.c new file mode 100644 index 0000000..2c50d76 --- /dev/null +++ b/ps5_key_extractV3.c @@ -0,0 +1,738 @@ +/* + * 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) + * + * Strategy: Chain approach 2 → approach 1. + * 1) Dump the ioctl handler code (approach 2) — needs only kernel R/W + * 2) RE the handler on PC to find which CCP slot / kernel address holds the key + * 3) Use approach 5 (read-address mode) to read that specific location + * 4) Approach 1 (CCP LSB dump) may catch it directly if slots are readable + * + * All operations are READ-ONLY. Nothing is written to kernel or hardware state. + * + * Usage: + * ./ps5_key_extract — run all approaches + * ./ps5_key_extract -a 0xFFFF... — read 4KB from a specific kernel address + * ./ps5_key_extract -a 0xFFFF... -s N — read N bytes from that address + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---------- 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 (v2) ---------- */ +#define DUMP_MAGIC 0x505550444D50ULL /* "PUPDMP\0\0" */ +#define DUMP_VERSION 2 + +#define SECTION_LSB 0x01 +#define SECTION_IOCTL 0x02 +#define SECTION_SNAP_PRE 0x03 +#define SECTION_SNAP_POST 0x04 +#define SECTION_MEMSCAN 0x05 +#define SECTION_CDEVSW 0x06 +#define SECTION_CCP_REGS 0x07 +#define SECTION_ADDR_READ 0x08 +#define SECTION_HANDLER_OPEN 0x09 +#define SECTION_HANDLER_CLOSE 0x0A + +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 CCP_QUEUE_MMIO_BASE 0x3001000 +#define CCP_QUEUE_STRIDE 0x1000 +#define CCP_NUM_QUEUES 5 + +/* Queue register offsets */ +#define CCP_REG_CONTROL 0x00 +#define CCP_REG_TAIL_LO 0x04 +#define CCP_REG_HEAD_LO 0x08 +#define CCP_REG_INT_ENABLE 0x0C +#define CCP_REG_INT_STATUS 0x10 +#define CCP_REG_STATUS 0x14 +#define CCP_PUBLIC_MASK_LO 0x18 +#define CCP_PUBLIC_MASK_HI 0x1C +#define CCP_PRIVATE_MASK_LO 0x20 +#define CCP_PRIVATE_MASK_HI 0x24 +#define CCP_REG_STATUS2 0x28 + +#define CCP_QUEUE_REG_COUNT 11 +#define CCP_QUEUE_REG_SIZE (CCP_QUEUE_REG_COUNT * 4) + +/* LSB region candidates 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) + +/* ---------- PUP ioctl constants (from ps5-pup-decrypt encryptsrv.c) ---------- */ +#define PUP_DEVICE_PATH "/dev/pup_update0" + +/* Complete ioctl map for /dev/pup_update0: + * 0xC0104401 VerifyBlsHeader { void* buf, u64 len, u64 unk } + * 0xC0184402 DecryptHeader { void* buf, u64 len, int type } + * 0xC0184403 VerifyPupAdditionalSign { u16 idx, void* buf, u64 len } + * 0xC0184404 VerifyPupWatermark { u16 idx, void* buf, u64 len } + * 0xC0184405 DecryptSegment { u16 idx, void* buf, u64 len } + * 0xC0284406 DecryptSegmentBlock { u16 entry_idx, u16 block_idx, + * void* block_buf, u64 block_len, + * void* table_buf, u64 table_len } + * + * DecryptHeader 'type' arg selects the key: + * translate_type(): 0,3->0 1,4->1 2,5->2 + * PS5UPDATE1.PUP (system update) = type 1 + * PS5UPDATE2.PUP (recovery) = type 0 + * + * The d_ioctl handler dispatches on type to load different keys. + * Our automated disassembly (analyze_dump.py) traces this dispatch. + */ +#define PUP_IOCTL_VERIFY_BLS 0xC0104401 +#define PUP_IOCTL_DECRYPT_HEADER 0xC0184402 +#define PUP_IOCTL_VERIFY_ADDSIGN 0xC0184403 +#define PUP_IOCTL_VERIFY_WATER 0xC0184404 +#define PUP_IOCTL_DECRYPT_SEG 0xC0184405 +#define PUP_IOCTL_DECRYPT_BLOCK 0xC0284406 + +/* ---------- 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_FLAGS 0x04 +#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 +#define CDEVSW_D_MMAP 0x40 +#define CDEVSW_D_STRATEGY 0x48 +#define CDEVSW_D_KQFILTER 0x58 +#define CDEVSW_SIZE 0x80 + +/* Handler dump size — 16KB gives enough room for full function + callees */ +#define HANDLER_DUMP_SIZE (16 * 1024) + +/* ---------- globals ---------- */ +#define OUTPUT_MAX (512 * 1024) +static uint8_t g_output[OUTPUT_MAX]; +static uint32_t g_output_pos = 0; +static uint32_t g_section_count = 0; + +#define MAX_SECTIONS 64 +static section_header_t g_sections[MAX_SECTIONS]; + +/* ---------- 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 >= MAX_SECTIONS) return; + if (g_output_pos + size > OUTPUT_MAX) 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 = DUMP_VERSION; + 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); + size_t show = len > 256 ? 256 : len; + for (size_t i = 0; i < show; i++) { + if (i > 0 && (i % 16) == 0) printf("\n"); + if ((i % 16) == 0) printf(" %04zx: ", i); + printf("%02x ", data[i]); + } + printf("\n"); + if (show < len) + printf(" ... (%zu more bytes in dump)\n", len - show); +} + +/* ================================================================ + * APPROACH 1: CCP LSB Key Slot Dump + Queue Register Dump + * + * Map the AMD CCP v5 MMIO region and read all 128 LSB key slots. + * Also dump all queue configuration registers (control, status, + * public/private masks) — these tell us which slots are CPU-readable. + * ================================================================ */ +static int approach1_ccp_lsb_dump(void) { + printf("\n=== APPROACH 1: CCP LSB Key Slot Dump + Queue Registers ===\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"); + } + + uint64_t bar_candidates[] = { + 0xE0100000ULL, + 0xFED80000ULL, + 0xFEC10000ULL, + 0x03000000ULL, + }; + + 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); + + 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; + } + } + + /* Dump ALL queue registers — not just masks */ + uint8_t queue_regs[CCP_NUM_QUEUES * CCP_QUEUE_REG_SIZE]; + int got_regs = 0; + memset(queue_regs, 0, sizeof(queue_regs)); + + for (int q = 0; q < CCP_NUM_QUEUES; 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 *dst = (uint32_t *)&queue_regs[q * CCP_QUEUE_REG_SIZE]; + dst[0] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_CONTROL); + dst[1] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_TAIL_LO); + dst[2] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_HEAD_LO); + dst[3] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_INT_ENABLE); + dst[4] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_INT_STATUS); + dst[5] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_STATUS); + dst[6] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_PUBLIC_MASK_LO); + dst[7] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_PUBLIC_MASK_HI); + dst[8] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_PRIVATE_MASK_LO); + dst[9] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_PRIVATE_MASK_HI); + dst[10] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_STATUS2); + munmap(qmap, 0x100); + + printf("[+] Queue %d: ctrl=0x%08x status=0x%08x " + "pub=0x%08x%08x priv=0x%08x%08x\n", + q, dst[0], dst[5], dst[7], dst[6], dst[9], dst[8]); + got_regs = 1; + } + + if (got_regs) { + output_add_section(SECTION_CCP_REGS, queue_regs, sizeof(queue_regs)); + } + } + + close(dmem_fd); + if (!found_any) { + printf("[!] No non-zero LSB data found via MMIO\n"); + } + return found_any ? 0 : -1; +} + +/* ================================================================ + * APPROACH 2: EncryptService Full Handler Dump + * + * Find the pup_update device's cdevsw in kernel memory, then dump: + * - The full cdevsw struct (all function pointers) + * - 16KB of d_ioctl handler code + * - 16KB of d_open handler code (key loading may happen here) + * - 16KB of d_close handler code + * + * Disassemble all three on PC — the key's journey from storage + * into the CCP slot is visible somewhere in these functions. + * ================================================================ */ +static int approach2_ioctl_handler_scan(void) { + printf("\n=== APPROACH 2: EncryptService Full Handler Dump ===\n"); + + uint64_t kernel_bases[] = { KERNEL_BASE_PRIMARY, KERNEL_BASE_FALLBACK }; + const char *search_str = "pup_update"; + size_t search_len = strlen(search_str); + + 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; + } + + 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); + + 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 all function pointers from cdevsw */ + uint64_t d_open = kernel_read64(cdevsw_base + CDEVSW_D_OPEN); + uint64_t d_close = kernel_read64(cdevsw_base + CDEVSW_D_CLOSE); + uint64_t d_read = kernel_read64(cdevsw_base + CDEVSW_D_READ); + uint64_t d_write = kernel_read64(cdevsw_base + CDEVSW_D_WRITE); + uint64_t d_ioctl = kernel_read64(cdevsw_base + CDEVSW_D_IOCTL); + uint64_t d_poll = kernel_read64(cdevsw_base + CDEVSW_D_POLL); + uint64_t d_mmap = kernel_read64(cdevsw_base + CDEVSW_D_MMAP); + + printf("[+] cdevsw function pointers:\n"); + printf(" d_open = 0x%llx\n", (unsigned long long)d_open); + printf(" d_close = 0x%llx\n", (unsigned long long)d_close); + printf(" d_read = 0x%llx\n", (unsigned long long)d_read); + printf(" d_write = 0x%llx\n", (unsigned long long)d_write); + printf(" d_ioctl = 0x%llx\n", (unsigned long long)d_ioctl); + printf(" d_poll = 0x%llx\n", (unsigned long long)d_poll); + printf(" d_mmap = 0x%llx\n", (unsigned long long)d_mmap); + + if (d_ioctl < 0xFFFFFFFF80000000ULL) { + printf("[!] d_ioctl address looks invalid, skipping\n"); + continue; + } + + /* Dump the raw cdevsw struct */ + uint8_t cdevsw_raw[CDEVSW_SIZE]; + if (kernel_copyout(cdevsw_base, cdevsw_raw, sizeof(cdevsw_raw)) == 0) { + printf("[+] Dumped cdevsw struct (%d bytes)\n", CDEVSW_SIZE); + output_add_section(SECTION_CDEVSW, cdevsw_raw, sizeof(cdevsw_raw)); + } + + /* Dump 16KB of d_ioctl handler */ + uint8_t handler_code[HANDLER_DUMP_SIZE]; + if (kernel_copyout(d_ioctl, handler_code, sizeof(handler_code)) == 0) { + printf("[+] Dumped d_ioctl handler (%d bytes)\n", HANDLER_DUMP_SIZE); + hexdump("d_ioctl first 128 bytes", handler_code, 128); + output_add_section(SECTION_IOCTL, handler_code, sizeof(handler_code)); + } else { + printf("[!] Failed to dump d_ioctl handler\n"); + } + + /* Dump 16KB of d_open handler — key loading may happen here */ + if (d_open >= 0xFFFFFFFF80000000ULL) { + uint8_t open_code[HANDLER_DUMP_SIZE]; + if (kernel_copyout(d_open, open_code, sizeof(open_code)) == 0) { + printf("[+] Dumped d_open handler (%d bytes)\n", HANDLER_DUMP_SIZE); + hexdump("d_open first 128 bytes", open_code, 128); + output_add_section(SECTION_HANDLER_OPEN, open_code, sizeof(open_code)); + } else { + printf("[!] Failed to dump d_open handler\n"); + } + } + + /* Dump 16KB of d_close handler */ + if (d_close >= 0xFFFFFFFF80000000ULL) { + uint8_t close_code[HANDLER_DUMP_SIZE]; + if (kernel_copyout(d_close, close_code, sizeof(close_code)) == 0) { + printf("[+] Dumped d_close handler (%d bytes)\n", HANDLER_DUMP_SIZE); + output_add_section(SECTION_HANDLER_CLOSE, close_code, sizeof(close_code)); + } else { + printf("[!] Failed to dump d_close handler\n"); + } + } + + free(kbuf); + return 0; + } + } + } + } + } + + free(kbuf); + printf("[!] Could not locate pup_update cdevsw\n"); + return -1; +} + +/* ================================================================ + * APPROACH 3: Instrumented PUP Decrypt (LSB snapshot) + * + * Snapshot all CCP LSB key slots BEFORE and AFTER issuing the + * DecryptPupHeader ioctl. Any slot that changed was loaded by + * the security processor for decryption. + * + * NOTE: PSP may lock out after boot, making this less reliable + * on newer firmware. Still worth running for the diff data. + * ================================================================ */ +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("[*] Skipping approach 3 (needs %s)\n", PUP_DEVICE_PATH); + return -1; + } + + printf("[+] Opened %s\n", PUP_DEVICE_PATH); + + 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); + + /* Try to invoke DecryptHeader ioctl to trigger key loading. + * struct decrypt_header_args { void* buffer; size_t length; int type; } + * type 1 = PS5UPDATE1.PUP (system update key — our target) + * Even if the decrypt fails (no valid PUP header in buffer), the + * security processor may still load the key into CCP slots. */ + { + struct { + void *buffer; + size_t length; + int type; + } dh_args; + uint8_t dummy_header[0x600]; + memset(dummy_header, 0, sizeof(dummy_header)); + /* Set PUP magic so the handler at least starts processing */ + dummy_header[0] = 0x54; dummy_header[1] = 0x14; + dummy_header[2] = 0xF5; dummy_header[3] = 0xEE; + + dh_args.buffer = dummy_header; + dh_args.length = sizeof(dummy_header); + dh_args.type = 1; /* system update key */ + + printf("[*] Invoking DecryptHeader ioctl (type=1) to trigger key load...\n"); + int ret = ioctl(pup_fd, PUP_IOCTL_DECRYPT_HEADER, &dh_args); + printf("[*] ioctl returned %d (key may have been loaded regardless of result)\n", ret); + } + + 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); + + 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. + * 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", + "sceSblSsDecryptPupHeader", + "sceSblServiceCrypt", + 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); + + 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++; + + i += nlen; + } + } + } + + if (total_found > 0) break; + } + + free(kbuf); + printf("[*] Total string matches: %d\n", total_found); + return (total_found > 0) ? 0 : -1; +} + +/* ================================================================ + * APPROACH 5: Targeted Address Read + * + * After disassembling the handler dump on PC, you know exactly + * which kernel address or CCP slot holds the key. Re-run the + * payload with -a
to read from that specific location. + * + * Usage: ./ps5_key_extract -a 0xFFFFFFFF82345678 + * ./ps5_key_extract -a 0xFFFFFFFF82345678 -s 256 + * ================================================================ */ +static int approach5_read_address(uint64_t addr, uint32_t size) { + printf("\n=== APPROACH 5: Targeted Address Read ===\n"); + printf("[*] Reading %u bytes from 0x%llx\n", size, (unsigned long long)addr); + + uint8_t *buf = (uint8_t *)malloc(size); + if (!buf) { + printf("[!] malloc failed\n"); + return -1; + } + + if (addr < 0x100000000ULL) { + /* Physical address — use MMIO mapping */ + printf("[*] Address looks physical, using mmap\n"); + int dmem_fd = open("/dev/dmem0", O_RDWR); + if (dmem_fd < 0) { + printf("[!] Cannot open /dev/dmem0\n"); + free(buf); + return -1; + } + + void *mapped = mmap(NULL, size, PROT_READ, MAP_SHARED, + dmem_fd, (off_t)addr); + if (mapped == MAP_FAILED) { + printf("[!] mmap failed\n"); + close(dmem_fd); + free(buf); + return -1; + } + + memcpy(buf, mapped, size); + munmap(mapped, size); + close(dmem_fd); + } else { + /* Kernel virtual address — use kernel_copyout */ + printf("[*] Address looks virtual, using kernel_copyout\n"); + if (kernel_copyout(addr, buf, size) != 0) { + printf("[!] kernel_copyout failed\n"); + free(buf); + return -1; + } + } + + hexdump("address read", buf, size); + output_add_section(SECTION_ADDR_READ, buf, size); + + free(buf); + printf("[+] Saved to dump file\n"); + return 0; +} + +/* ---------- main ---------- */ +int main(int argc, char **argv) { + printf("=== PS5 PUP Key Extractor v2 ===\n"); + printf("Target: PUP header decryption key\n"); + printf("Output: /data/pup_key_dump.bin\n\n"); + + /* Parse arguments */ + uint64_t target_addr = 0; + uint32_t target_size = 4096; + int addr_mode = 0; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-a") == 0 && i + 1 < argc) { + target_addr = strtoull(argv[++i], NULL, 0); + addr_mode = 1; + } else if (strcmp(argv[i], "-s") == 0 && i + 1 < argc) { + target_size = (uint32_t)strtoul(argv[++i], NULL, 0); + if (target_size > 65536) target_size = 65536; + } + } + + output_begin(); + + if (addr_mode) { + /* Targeted read mode — just read the specified address */ + approach5_read_address(target_addr, target_size); + } else { + /* Full scan mode — run all approaches */ + 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 + queue regs): %s\n", r1 == 0 ? "OK" : "FAILED"); + printf("Approach 2 (full handler dump): %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"); + printf("\nChained strategy:\n"); + printf(" 1. Load d_ioctl/d_open dumps into Ghidra/IDA on PC\n"); + printf(" 2. Find which CCP slot or kernel address the key comes from\n"); + printf(" 3. Re-run with: ./ps5_key_extract -a
-s \n"); + } + + output_write("/data/pup_key_dump.bin"); + + printf("\n[*] Done. Transfer /data/pup_key_dump.bin to PC and run:\n"); + printf(" python analyze_dump.py pup_key_dump.bin\n"); + return 0; +}