From 60eab267dbaa7f40dabaec47d658cb72269c39d8 Mon Sep 17 00:00:00 2001 From: deepkicks <77412763+deepkicks@users.noreply.github.com> Date: Thu, 15 Oct 2026 03:11:14 -0500 Subject: [PATCH] Add files via upload ioctl handler dump 4KB 16KB cdevsw handlers dumped d_ioctl only d_open, d_close, d_read, d_ioctl CCP queue descriptors not dumped dumped (shows what ops are queued) Targeted address read not possible --addr flag for pass 2 Run order all 4 parallel approach 2 first (priority) Approach 3 (pre/post) included demoted to optional (PSP lockout concern) --- README.md | 27 +- RESEARCH_POST.md | 48 ++- analyze_dump.py | 102 ++++++- ps5_key_extractV2.c | 691 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 847 insertions(+), 21 deletions(-) create mode 100644 ps5_key_extractV2.c diff --git a/README.md b/README.md index 95fa197..4e6a88d 100644 --- a/README.md +++ b/README.md @@ -69,18 +69,31 @@ python main.py decompress segment.bin -o decompressed.bin |------|-------------| | `RESEARCH_POST.md` | Full technical write-up of findings | -## Key Extraction Payload +## Key Extraction Payload (v2) -If you have a jailbroken PS5 on FW 13.60 or below, `ps5_key_extract.c` implements four extraction approaches: +If you have a jailbroken PS5 on FW 13.60 or below, `ps5_key_extract.c` implements a **chained extraction strategy**: -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 +**The chain: Approach 2 → Approach 1/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 -All read-only. Output goes to `/data/pup_key_dump.bin`. Transfer to PC and run: +**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 +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 ```bash +# Full scan — run all approaches +./ps5_key_extract + +# Targeted read — after disassembling the handler dump +./ps5_key_extract -a 0xFFFFFFFF82345678 +./ps5_key_extract -a 0xFFFFFFFF82345678 -s 256 + +# Analyze on PC python analyze_dump.py pup_key_dump.bin --pup14 PS5UPDATE14.00.PUP --pup14_1 PS5UPDATE14.10.PUP ``` diff --git a/RESEARCH_POST.md b/RESEARCH_POST.md index 75d140b..ad53e2d 100644 --- a/RESEARCH_POST.md +++ b/RESEARCH_POST.md @@ -137,20 +137,54 @@ Any one of these gives us the missing piece. Once that key is in the tool, it wo ## 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: +To make this as easy as possible for anyone with a jailbroken 13.60 PS5, I wrote a ready-to-compile C payload (`ps5_key_extract.c` v2) that implements a **chained extraction strategy**. The key insight: Byepervisor Reborn gives kernel + HV R/W, but **not** direct PSP memory access. So we can't just dump the security processor — we need to be smarter. -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. +### The Chained Strategy: Approach 2 → Approach 1 -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. +**Step 1 — Dump the handler code (approach 2, needs only kernel R/W):** +The payload finds the `pup_update0` device driver's `cdevsw` struct in kernel memory and dumps 16KB each of: +- `d_ioctl` handler — the function behind `DecryptPupHeader` (ioctl `0xC0184402`) +- `d_open` handler — key loading may happen when the device is opened +- `d_close` handler — cleanup code that may reference the key -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. +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. -4. **Kernel Memory Scan** — Searches near strings like `encsrv`, `decrypt_header`, `Titania`, `ccp_key` and dumps surrounding context. +**Step 2 — Target what you found:** +Based on what the handler reveals: -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. +| Handler shows | Next step | +|---|---| +| Key loaded from a kernel memory address | Re-run payload with `-a 0xADDRESS` to read it directly | +| Key loaded via CCP unwrap operation | Read the wrapped blob + wrapping key | +| Key pre-loaded in CCP slot at boot | Check approach 1 results for that slot | +| Key fetched from PSP via mailbox | Now you know the exact PSP address to target | + +### All Five Approaches + +1. **CCP LSB Key Slot Dump + Queue Registers** — Maps the AMD CCP v5 MMIO region, reads all 128 hardware key slots AND all queue configuration registers. The queue registers tell us which slots are CPU-readable (public mask) vs hardware-protected (private mask) — so we know immediately if direct reads are possible. + +2. **Full EncryptService Handler Dump** — Dumps the complete `cdevsw` struct (all function pointers) plus 16KB of `d_ioctl`, `d_open`, and `d_close` handler code. This is the most reliable approach — it only needs kernel R/W, which Byepervisor Reborn provides. + +3. **Instrumented PUP Decrypt** — Snapshots all CCP LSB slots before and after a decrypt ioctl. 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. **Requirements:** A PS5 on FW 13.60 or below with kernel R/W access (Byepervisor Reborn + Relapse + etaHEN or similar). Compile for FreeBSD 11 amd64 and load via payload loader. You'll need to plug in your framework's `kernel_read64` / `kernel_copyout` functions. +**Usage:** +```bash +# Full scan — run all approaches +./ps5_key_extract + +# Targeted read — after RE on PC reveals the key address +./ps5_key_extract -a 0xFFFFFFFF82345678 +./ps5_key_extract -a 0xFFFFFFFF82345678 -s 256 +``` + ## Tool Availability The tool works today for everything except the one missing key: @@ -163,7 +197,7 @@ The tool works today for everything except the one missing key: - **Ready to accept the Layer 1 key** — the crypto module just needs one more key added and the full pipeline connects - **Extraction payload ready** — `ps5_key_extract.c` + `analyze_dump.py` for someone with console access -Source is published: **https://github.com/deepkicks/TitaniaPy** — drop in your PUP files and go. DMs open if you have console access and want to collaborate — this is a one-key problem with a permanent payoff. +Source is published: **[GitHub repo link]** — drop in your PUP files and go. DMs open if you have console access and want to collaborate — this is a one-key problem with a permanent payoff. --- diff --git a/analyze_dump.py b/analyze_dump.py index 977f082..ee5291d 100644 --- a/analyze_dump.py +++ b/analyze_dump.py @@ -21,22 +21,40 @@ except ImportError: print("ERROR: pycryptodome required. Install with: pip install pycryptodome") sys.exit(1) -# --- Dump format constants (must match ps5_key_extract.c) --- +# --- Dump format constants (must match ps5_key_extract.c v2) --- 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_CDEVSW = 0x06 +SECTION_CCP_REGS = 0x07 +SECTION_ADDR_READ = 0x08 +SECTION_HANDLER_OPEN = 0x09 +SECTION_HANDLER_CLOSE = 0x0A SECTION_NAMES = { SECTION_LSB: "CCP LSB Key Slots", - SECTION_IOCTL: "ioctl Handler Code", + SECTION_IOCTL: "d_ioctl Handler Code", SECTION_SNAP_PRE: "Pre-Decrypt LSB Snapshot", SECTION_SNAP_POST: "Post-Decrypt LSB Snapshot", SECTION_MEMSCAN: "Kernel Memory Scan", + SECTION_CDEVSW: "cdevsw Struct", + SECTION_CCP_REGS: "CCP Queue Registers", + SECTION_ADDR_READ: "Targeted Address Read", + SECTION_HANDLER_OPEN: "d_open Handler Code", + SECTION_HANDLER_CLOSE: "d_close Handler Code", } +# CCP queue register names (11 regs x 4 bytes each, per queue) +CCP_QUEUE_REG_NAMES = [ + "CONTROL", "TAIL_LO", "HEAD_LO", "INT_ENABLE", "INT_STATUS", + "STATUS", "PUBLIC_MASK_LO", "PUBLIC_MASK_HI", + "PRIVATE_MASK_LO", "PRIVATE_MASK_HI", "STATUS2", +] +CCP_NUM_QUEUES = 5 + # LSB geometry LSB_COUNT = 8 SLOTS_PER_LSB = 16 @@ -72,7 +90,7 @@ def parse_dump(data: bytes) -> dict: header_size = 16 # dump_header_t section_hdr_size = 16 # section_header_t - max_sections = 32 + max_sections = 64 section_table_offset = header_size sections = [] @@ -105,7 +123,7 @@ def extract_key_candidates(sections: list) -> list: seen = set() for sec in sections: - if sec["type"] not in (SECTION_LSB, SECTION_SNAP_PRE, SECTION_SNAP_POST): + if sec["type"] not in (SECTION_LSB, SECTION_SNAP_PRE, SECTION_SNAP_POST, SECTION_ADDR_READ): continue data = sec["data"] @@ -289,6 +307,64 @@ def hexdump_section(data: bytes, label: str, max_bytes: int = 256): print(f" ... ({len(data) - show} more bytes)") +def display_cdevsw(data: bytes): + """Display the pup_update0 cdevsw struct with labeled function pointers.""" + print(f"\n--- cdevsw Struct (pup_update0) ---") + if len(data) < 0x58: + print(f" [!] Too small ({len(data)} bytes)") + return + 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"), + ] + for off, name in fields: + if off + 8 <= len(data): + val = struct.unpack_from("+#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 psdevwiki) ---------- */ +#define PUP_DEVICE_PATH "/dev/pup_update0" +#define PUP_IOCTL_DECRYPT_HEADER 0xC0184402 + +/* ---------- 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); + + printf("[*] To invoke the ioctl, provide valid src/dst physical addresses.\n"); + printf("[*] Skipping actual ioctl call (needs DMA-able memory with PUP header).\n"); + + 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; +}