/* * ps5_key_extractV4.c — Enhanced PUP key extractor with CCP intercept * * V4 adds to V3: * Approach 6: CCP Queue Snoop — monitor CCP command submissions during decrypt * Approach 7: Symbol Resolve — find crypto functions via kernel_dynlib_dlsym * Approach 8: CCP PASSTHROUGH — copy key from private SB slot to system memory * Approach 9: Decrypt Oracle — feed real PUP data and capture decrypted output * * Build: prospero-clang from ps5-payload-dev SDK * export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk * $PS5_PAYLOAD_SDK/bin/prospero-clang -o ps5_key_extract.elf ps5_key_extractV4.c * * Usage: * ./ps5_key_extract — full scan (approaches 1-7) * ./ps5_key_extract -a 0xFFFF... [-s N] — targeted address read (approach 5) * ./ps5_key_extract -p [-q ] — PASSTHROUGH extract from SB slot * ./ps5_key_extract -d — decrypt oracle (/data/pup_header.bin) * * Approaches 1-7 are READ-ONLY (safe). * Approach 8 WRITES to CCP hardware (aggressive). Approach 9 is read-only. */ #include #include #include #include #include #include #include #include #include /* ---------- kernel R/W primitives (ps5-payload-dev SDK) ---------- */ #include static inline uint64_t kread64(uint64_t kaddr) { return kernel_getlong((intptr_t)kaddr); } static inline uint32_t kread32(uint64_t kaddr) { return kernel_getint((intptr_t)kaddr); } static inline void kwrite64(uint64_t kaddr, uint64_t val) { kernel_setlong((intptr_t)kaddr, val); } static inline void kwrite32(uint64_t kaddr, uint32_t val) { kernel_setint((intptr_t)kaddr, val); } /* ---------- output format (v3) ---------- */ #define DUMP_MAGIC 0x505550444D50ULL /* "PUPDMP\0\0" */ #define DUMP_VERSION 3 #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 #define SECTION_CCP_CMD 0x0B #define SECTION_SYMRESOLVE 0x0C #define SECTION_PASSTHROUGH 0x0D #define SECTION_ORACLE_IN 0x0E #define SECTION_ORACLE_OUT 0x0F 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 #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) /* CCP v5 command descriptor */ #define CCP_DESC_SIZE 32 typedef struct { uint32_t dw[8]; } __attribute__((packed)) ccp5_desc_t; /* CCP v5 descriptor field extraction */ #define CCP_DESC_ENGINE(d) (((d)->dw[0] >> 20) & 0xF) #define CCP_DESC_FUNCTION(d) (((d)->dw[0] >> 5) & 0x7FFF) #define CCP_DESC_EOM(d) (((d)->dw[0] >> 4) & 1) #define CCP_DESC_INIT(d) (((d)->dw[0] >> 3) & 1) #define CCP_DESC_LENGTH(d) ((d)->dw[1]) #define CCP_DESC_SRC_LO(d) ((d)->dw[2]) #define CCP_DESC_SRC_MEM(d) (((d)->dw[3] >> 16) & 3) #define CCP_DESC_DST_LO(d) ((d)->dw[4]) #define CCP_DESC_DST_MEM(d) (((d)->dw[5] >> 16) & 3) #define CCP_DESC_KEY_LO(d) ((d)->dw[6]) #define CCP_DESC_KEY_MEM(d) (((d)->dw[7] >> 16) & 3) /* Engine IDs */ #define CCP_ENGINE_AES 0 #define CCP_ENGINE_XTS_AES 1 #define CCP_ENGINE_SHA 2 #define CCP_ENGINE_RSA 3 #define CCP_ENGINE_PASSTHROUGH 5 #define CCP_ENGINE_ZLIB 6 /* Memory types */ #define CCP_MEMTYPE_SYSTEM 0 #define CCP_MEMTYPE_SB 1 #define CCP_MEMTYPE_LOCAL 2 /* AES function field bits (within the 15-bit function) */ #define CCP_AES_MODE_MASK 0x000F /* bits [3:0]: ECB=0,CBC=1,OFB=2,CFB=3,CTR=4 */ #define CCP_AES_SIZE_MASK 0x0030 /* bits [5:4]: 128=0,192=1,256=2 */ #define CCP_AES_DECRYPT 0x0040 /* bit 6: decrypt */ #define CCP_AES_KEY_SB 0x0080 /* bit 7: key from SB (vs from memory) */ /* LSB geometry */ #define LSB_OFFSET_A 0x8000 #define LSB_OFFSET_B 0x10000 #define LSB_OFFSET_C 0x4000 #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 ---------- */ #define PUP_DEVICE_PATH "/dev/pup_update0" #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 /* ---------- kernel constants ---------- */ #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 #define HANDLER_DUMP_SIZE (16 * 1024) /* ---------- globals ---------- */ #define OUTPUT_MAX (1024 * 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]; /* CCP BAR candidates — tried by multiple approaches */ static const uint64_t g_bar_candidates[] = { 0xE0100000ULL, 0xFED80000ULL, 0xFEC10000ULL, 0x03000000ULL }; #define NUM_BAR_CANDIDATES (sizeof(g_bar_candidates)/sizeof(g_bar_candidates[0])) /* ---------- 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); } /* ---------- helper: open dmem ---------- */ static int open_dmem(void) { int fd = open("/dev/dmem0", O_RDWR); if (fd < 0) fd = open("/dev/mem", O_RDWR); return fd; } /* ---------- helper: get kernel scan base ---------- */ static uint64_t get_kernel_base(void) { uint64_t base = (uint64_t)KERNEL_ADDRESS_TEXT_BASE; if (base >= 0xFFFFFFFF80000000ULL) { printf("[+] SDK kernel text base: 0x%llx\n", (unsigned long long)base); return base; } printf("[!] SDK kernel base invalid (0x%llx), using fallback\n", (unsigned long long)base); return 0xFFFFFFFF82200000ULL; } /* ================================================================ * APPROACH 1: CCP LSB Key Slot Dump + Queue Register Dump * (unchanged from V3) * ================================================================ */ static int approach1_ccp_lsb_dump(void) { printf("\n=== APPROACH 1: CCP LSB Key Slot Dump + Queue Registers ===\n"); int dmem_fd = open_dmem(); if (dmem_fd < 0) { printf("[!] Cannot open /dev/dmem0 or /dev/mem\n"); return -1; } 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)NUM_BAR_CANDIDATES; b++) { uint64_t bar = g_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) 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; } } 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); return found_any ? 0 : -1; } /* ================================================================ * APPROACH 2: EncryptService Full Handler Dump * IMPROVED: uses SDK KERNEL_ADDRESS_TEXT_BASE * ================================================================ */ static int approach2_ioctl_handler_scan(void) { printf("\n=== APPROACH 2: EncryptService Full Handler Dump ===\n"); uint64_t kbase = get_kernel_base(); 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; } /* Try SDK base first, then data segment, then fallback */ uint64_t bases[] = { kbase, (uint64_t)KERNEL_ADDRESS_DATA_BASE, 0xFFFFFFFF80000000ULL }; for (int kb = 0; kb < 3; kb++) { uint64_t base = bases[kb]; if (base < 0xFFFFFFFF80000000ULL) continue; printf("[*] Scanning kernel at base 0x%llx...\n", (unsigned long long)base); if (kernel_copyout(base, kbuf, scan_size) != 0) { printf("[!] kernel_copyout failed for base 0x%llx\n", (unsigned long long)base); continue; } for (size_t i = 0; i < scan_size - search_len; i++) { if (memcmp(&kbuf[i], search_str, search_len) != 0) continue; if (kbuf[i + search_len] != '\0') continue; uint64_t str_kaddr = base + 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) continue; uint64_t ptr_kaddr = base + j; uint64_t cdevsw_base = ptr_kaddr - CDEVSW_D_NAME; printf("[+] Candidate cdevsw at 0x%llx\n", (unsigned long long)cdevsw_base); uint64_t d_open = kread64(cdevsw_base + CDEVSW_D_OPEN); uint64_t d_close = kread64(cdevsw_base + CDEVSW_D_CLOSE); uint64_t d_ioctl = kread64(cdevsw_base + CDEVSW_D_IOCTL); printf("[+] d_open=0x%llx d_close=0x%llx d_ioctl=0x%llx\n", (unsigned long long)d_open, (unsigned long long)d_close, (unsigned long long)d_ioctl); if (d_ioctl < 0xFFFFFFFF80000000ULL) { printf("[!] d_ioctl looks invalid, skipping\n"); continue; } uint8_t cdevsw_raw[CDEVSW_SIZE]; if (kernel_copyout(cdevsw_base, cdevsw_raw, sizeof(cdevsw_raw)) == 0) output_add_section(SECTION_CDEVSW, cdevsw_raw, sizeof(cdevsw_raw)); 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); output_add_section(SECTION_IOCTL, handler_code, sizeof(handler_code)); } 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); output_add_section(SECTION_HANDLER_OPEN, open_code, sizeof(open_code)); } } 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)); } } free(kbuf); return 0; } } } free(kbuf); printf("[!] Could not locate pup_update cdevsw\n"); return -1; } /* ================================================================ * APPROACH 3: Instrumented PUP Decrypt (LSB snapshot) * (unchanged from V3) * ================================================================ */ 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\n", PUP_DEVICE_PATH); return -1; } int dmem_fd = open_dmem(); if (dmem_fd < 0) { close(pup_fd); return -1; } uint64_t lsb_phys = 0x03000000ULL + LSB_OFFSET_A; uint8_t pre_lsb[LSB_TOTAL_SIZE], 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\n"); close(dmem_fd); close(pup_fd); return -1; } memcpy(pre_lsb, mapped, LSB_TOTAL_SIZE); output_add_section(SECTION_SNAP_PRE, pre_lsb, LSB_TOTAL_SIZE); struct { void *buffer; size_t length; int type; } dh_args; uint8_t dummy_header[0x600]; memset(dummy_header, 0, sizeof(dummy_header)); 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; printf("[*] Triggering DecryptHeader ioctl (type=1)...\n"); int ret = ioctl(pup_fd, PUP_IOCTL_DECRYPT_HEADER, &dh_args); printf("[*] ioctl returned %d\n", ret); memcpy(post_lsb, mapped, LSB_TOTAL_SIZE); munmap(mapped, LSB_TOTAL_SIZE); 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++) { if (memcmp(&pre_lsb[slot * SLOT_SIZE], &post_lsb[slot * SLOT_SIZE], SLOT_SIZE) != 0) { printf("[+] LSB slot %d CHANGED during decrypt!\n", slot); hexdump(" after", &post_lsb[slot * SLOT_SIZE], 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 * IMPROVED: uses SDK KERNEL_ADDRESS_TEXT_BASE * ================================================================ */ 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 kbase = get_kernel_base(); size_t scan_size = 32 * 1024 * 1024; uint8_t *kbuf = (uint8_t *)malloc(scan_size); if (!kbuf) return -1; printf("[*] Scanning from 0x%llx...\n", (unsigned long long)kbase); if (kernel_copyout(kbase, kbuf, scan_size) != 0) { free(kbuf); return -1; } int total_found = 0; 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) continue; 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 < scan_size) ? (i + 256) : scan_size; output_add_section(SECTION_MEMSCAN, &kbuf[ctx_start], (uint32_t)(ctx_end - ctx_start)); total_found++; i += nlen; } } free(kbuf); return (total_found > 0) ? 0 : -1; } /* ================================================================ * APPROACH 5: Targeted Address Read (unchanged from V3) * ================================================================ */ 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) return -1; if (addr < 0x100000000ULL) { int dmem_fd = open_dmem(); if (dmem_fd < 0) { free(buf); return -1; } void *mapped = mmap(NULL, size, PROT_READ, MAP_SHARED, dmem_fd, (off_t)addr); if (mapped == MAP_FAILED) { close(dmem_fd); free(buf); return -1; } memcpy(buf, mapped, size); munmap(mapped, size); close(dmem_fd); } else { if (kernel_copyout(addr, buf, size) != 0) { free(buf); return -1; } } hexdump("address read", buf, size); output_add_section(SECTION_ADDR_READ, buf, size); free(buf); return 0; } /* ================================================================ * APPROACH 6: CCP Command Queue Snoop (NEW — read-only) * * Record CCP queue tail pointers BEFORE and AFTER triggering the * DecryptHeader ioctl. If any queue's tail advances, the commands * submitted between old and new tail are the CCP operations used * for PUP decryption. Dump them raw for PC-side analysis. * * The command descriptors reveal: * - Which CCP engine is used (AES, PASSTHROUGH, etc.) * - Which SB slot holds the key (for AES with SB key source) * - The key memory address (for AES with memory key source) * - The exact AES mode and key size * ================================================================ */ #define CCP_SNOOP_MAX_DESCS 64 static const char *ccp_engine_name(uint32_t id) { switch (id) { case 0: return "AES"; case 1: return "XTS-AES"; case 2: return "SHA"; case 3: return "RSA"; case 4: return "ECC"; case 5: return "PASSTHROUGH"; case 6: return "ZLIB"; default: return "UNKNOWN"; } } static const char *ccp_mem_name(uint32_t id) { switch (id) { case 0: return "system"; case 1: return "SB"; case 2: return "local"; default: return "?"; } } static int approach6_ccp_queue_snoop(void) { printf("\n=== APPROACH 6: CCP Command Queue Snoop ===\n"); int dmem_fd = open_dmem(); if (dmem_fd < 0) return -1; int pup_fd = open(PUP_DEVICE_PATH, O_RDWR); if (pup_fd < 0) { printf("[!] Cannot open %s — skipping queue snoop\n", PUP_DEVICE_PATH); close(dmem_fd); return -1; } int found_cmds = 0; for (int b = 0; b < (int)NUM_BAR_CANDIDATES; b++) { uint64_t bar = g_bar_candidates[b]; /* Step 1: snapshot tail pointers for all queues */ uint32_t pre_tail[CCP_NUM_QUEUES]; uint32_t pre_head[CCP_NUM_QUEUES]; int accessible[CCP_NUM_QUEUES]; for (int q = 0; q < CCP_NUM_QUEUES; q++) { accessible[q] = 0; uint64_t qaddr = bar + CCP_QUEUE_MMIO_BASE + (q * CCP_QUEUE_STRIDE); void *qmap = mmap(NULL, 0x100, PROT_READ, MAP_SHARED, dmem_fd, (off_t)qaddr); if (qmap == MAP_FAILED) continue; pre_tail[q] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_TAIL_LO); pre_head[q] = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_HEAD_LO); munmap(qmap, 0x100); accessible[q] = 1; } /* Step 2: trigger DecryptHeader ioctl */ struct { void *buffer; size_t length; int type; } dh_args; uint8_t dummy_header[0x600]; memset(dummy_header, 0, sizeof(dummy_header)); 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; printf("[*] Triggering DecryptHeader at BAR 0x%llx...\n", (unsigned long long)bar); ioctl(pup_fd, PUP_IOCTL_DECRYPT_HEADER, &dh_args); /* Step 3: read tail pointers again and diff */ for (int q = 0; q < CCP_NUM_QUEUES; q++) { if (!accessible[q]) continue; uint64_t qaddr = bar + CCP_QUEUE_MMIO_BASE + (q * CCP_QUEUE_STRIDE); void *qmap = mmap(NULL, 0x100, PROT_READ, MAP_SHARED, dmem_fd, (off_t)qaddr); if (qmap == MAP_FAILED) continue; uint32_t post_tail = *(volatile uint32_t *) ((uint8_t *)qmap + CCP_REG_TAIL_LO); uint32_t post_head = *(volatile uint32_t *) ((uint8_t *)qmap + CCP_REG_HEAD_LO); munmap(qmap, 0x100); if (post_tail == pre_tail[q]) continue; uint32_t new_bytes; if (post_tail > pre_tail[q]) new_bytes = post_tail - pre_tail[q]; else new_bytes = post_tail + 8192 - pre_tail[q]; uint32_t ndesc = new_bytes / CCP_DESC_SIZE; if (ndesc > CCP_SNOOP_MAX_DESCS) ndesc = CCP_SNOOP_MAX_DESCS; printf("[+] Queue %d: tail 0x%08x -> 0x%08x (%u new commands)\n", q, pre_tail[q], post_tail, ndesc); /* Step 4: map the descriptor ring and read the commands */ uint64_t desc_phys = (uint64_t)pre_tail[q]; uint32_t map_off = (uint32_t)(desc_phys & 0xFFF); void *ring_map = mmap(NULL, map_off + ndesc * CCP_DESC_SIZE + 0x1000, PROT_READ, MAP_SHARED, dmem_fd, (off_t)(desc_phys & ~0xFFFULL)); if (ring_map == MAP_FAILED) { printf("[!] Cannot map descriptor ring at 0x%08x\n", (uint32_t)desc_phys); continue; } uint8_t *desc_base = (uint8_t *)ring_map + map_off; for (uint32_t d = 0; d < ndesc; d++) { ccp5_desc_t *cmd = (ccp5_desc_t *)(desc_base + d * CCP_DESC_SIZE); uint32_t eng = CCP_DESC_ENGINE(cmd); uint32_t func = CCP_DESC_FUNCTION(cmd); uint32_t len = CCP_DESC_LENGTH(cmd); uint32_t key_mem = CCP_DESC_KEY_MEM(cmd); uint32_t key_lo = CCP_DESC_KEY_LO(cmd); printf(" [%u] %s func=0x%04x len=%u\n", d, ccp_engine_name(eng), func, len); printf(" src=%s@0x%08x dst=%s@0x%08x\n", ccp_mem_name(CCP_DESC_SRC_MEM(cmd)), CCP_DESC_SRC_LO(cmd), ccp_mem_name(CCP_DESC_DST_MEM(cmd)), CCP_DESC_DST_LO(cmd)); if (eng == CCP_ENGINE_AES) { int is_decrypt = (func & CCP_AES_DECRYPT) ? 1 : 0; int mode = func & CCP_AES_MODE_MASK; int ksize = (func & CCP_AES_SIZE_MASK) >> 4; int key_sb = (func & CCP_AES_KEY_SB) ? 1 : 0; const char *modes[] = {"ECB","CBC","OFB","CFB","CTR","CMAC"}; const char *sizes[] = {"128","192","256"}; printf(" >>> AES-%s-%s %s", ksize < 3 ? sizes[ksize] : "?", mode < 6 ? modes[mode] : "?", is_decrypt ? "DECRYPT" : "ENCRYPT"); if (key_sb) { printf(", KEY FROM SB SLOT %u", key_lo); } else { printf(", KEY FROM %s@0x%08x", ccp_mem_name(key_mem), key_lo); } printf("\n"); } } /* Save raw descriptors to dump */ uint32_t sec_size = 8 + ndesc * CCP_DESC_SIZE; uint8_t *sec_buf = (uint8_t *)malloc(sec_size); if (sec_buf) { uint32_t queue_id = q; uint32_t count = ndesc; memcpy(sec_buf, &queue_id, 4); memcpy(sec_buf + 4, &count, 4); memcpy(sec_buf + 8, desc_base, ndesc * CCP_DESC_SIZE); output_add_section(SECTION_CCP_CMD, sec_buf, sec_size); free(sec_buf); } munmap(ring_map, map_off + ndesc * CCP_DESC_SIZE + 0x1000); found_cmds = 1; } if (found_cmds) break; } close(pup_fd); close(dmem_fd); return found_cmds ? 0 : -1; } /* ================================================================ * APPROACH 7: Kernel Symbol Resolve (NEW — read-only) * * Use the SDK's kernel_dynlib_dlsym / kernel_dynlib_resolve to find * Sony crypto functions by symbol name. Dump 4KB of code at each * resolved address. More reliable than string scanning when the * kernel exports these symbols. * ================================================================ */ typedef struct { uint64_t addr; uint32_t code_dumped; char name[60]; } __attribute__((packed)) sym_entry_t; static int approach7_symbol_resolve(void) { printf("\n=== APPROACH 7: Kernel Symbol Resolve ===\n"); pid_t pid = getpid(); static const char *sym_names[] = { "sceSblServiceCrypt", "sceSblSsDecryptPupHeader", "sceSblSsDecryptPupSegment", "sceSblServiceMailbox", "sceSblDriverMapPages", "sceSblKeymgrSmCallfunc", "sceSblPupUpdateSetKey", "sceSblAuthMgrSmStart", NULL }; /* Common module handle IDs: 0 = kernel base, 1 = libkernel, 0x2001 = libSceSbl */ static const uint32_t handles[] = { 0, 1, 0x2001, 0x2002, 0x13 }; int num_handles = sizeof(handles) / sizeof(handles[0]); sym_entry_t entries[16]; int entry_count = 0; int total_resolved = 0; for (int s = 0; sym_names[s] && entry_count < 16; s++) { intptr_t addr = 0; for (int h = 0; h < num_handles && addr == 0; h++) { addr = kernel_dynlib_dlsym(pid, handles[h], sym_names[s]); if (addr != 0) { printf("[+] %s = 0x%llx (handle=%u)\n", sym_names[s], (unsigned long long)addr, handles[h]); } } if (addr == 0) { addr = kernel_dynlib_resolve(pid, 0, sym_names[s]); if (addr != 0) { printf("[+] %s = 0x%llx (via NID resolve)\n", sym_names[s], (unsigned long long)addr); } } if (addr != 0) { sym_entry_t *e = &entries[entry_count]; e->addr = (uint64_t)addr; strncpy(e->name, sym_names[s], sizeof(e->name) - 1); e->name[sizeof(e->name) - 1] = '\0'; uint8_t code[4096]; if (kernel_copyout(addr, code, sizeof(code)) == 0) { e->code_dumped = sizeof(code); hexdump(sym_names[s], code, 128); output_add_section(SECTION_SYMRESOLVE, code, sizeof(code)); } else { e->code_dumped = 0; printf("[!] Failed to dump code at 0x%llx\n", (unsigned long long)addr); } entry_count++; total_resolved++; } else { printf("[-] %s — not found\n", sym_names[s]); } } if (entry_count > 0) { output_add_section(SECTION_SYMRESOLVE, entries, entry_count * sizeof(sym_entry_t)); } printf("[*] Resolved %d / %d symbols\n", total_resolved, (int)(sizeof(sym_names)/sizeof(sym_names[0])) - 1); return total_resolved > 0 ? 0 : -1; } /* ================================================================ * APPROACH 8: CCP PASSTHROUGH Key Extract (NEW — writes to CCP) * * Submit a CCP PASSTHROUGH command that copies data from an SB * (Secure Buffer / LSB) slot into system memory we control. * * The private mask only blocks direct CPU MMIO reads of SB slots. * A CCP-internal PASSTHROUGH operation copies between any source * and destination the CCP itself can access — including private * slots the CPU can't read via MMIO. * * Requires: -p [-q ] * WARNING: Writes to CCP command queue. Non-destructive but * aggressive. May hang CCP if parameters are wrong. * ================================================================ */ static int approach8_passthrough(int sb_slot, int queue_id) { printf("\n=== APPROACH 8: CCP PASSTHROUGH Extract ===\n"); printf("[*] Target: SB slot %d via queue %d\n", sb_slot, queue_id); int dmem_fd = open_dmem(); if (dmem_fd < 0) return -1; int found = 0; for (int b = 0; b < (int)NUM_BAR_CANDIDATES; b++) { uint64_t bar = g_bar_candidates[b]; uint64_t qaddr = bar + CCP_QUEUE_MMIO_BASE + (queue_id * CCP_QUEUE_STRIDE); /* Map queue registers with read+write */ void *qmap = mmap(NULL, 0x100, PROT_READ | PROT_WRITE, MAP_SHARED, dmem_fd, (off_t)qaddr); if (qmap == MAP_FAILED) continue; uint32_t ctrl = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_CONTROL); uint32_t status = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_STATUS); uint32_t tail = *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_TAIL_LO); printf("[+] BAR 0x%llx queue %d: ctrl=0x%x status=0x%x tail=0x%08x\n", (unsigned long long)bar, queue_id, ctrl, status, tail); if (tail == 0) { munmap(qmap, 0x100); continue; } /* Allocate output buffer: use a physical page via dmem0. * Pick an address 4KB past the current tail — this is in the * queue's DMA region and should be valid system RAM. */ uint64_t out_phys = ((uint64_t)tail & ~0xFFFULL) + 0x2000; void *out_map = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, dmem_fd, (off_t)out_phys); if (out_map == MAP_FAILED) { printf("[!] Cannot map output page at 0x%llx, trying 0x80000\n", (unsigned long long)out_phys); out_phys = 0x80000ULL; out_map = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, dmem_fd, (off_t)out_phys); if (out_map == MAP_FAILED) { printf("[!] Cannot map any output page\n"); munmap(qmap, 0x100); continue; } } memset(out_map, 0xCC, SLOT_SIZE); printf("[+] Output buffer at phys 0x%llx\n", (unsigned long long)out_phys); /* Build PASSTHROUGH descriptor: SB slot → system memory */ ccp5_desc_t pt_desc; memset(&pt_desc, 0, sizeof(pt_desc)); pt_desc.dw[0] = (CCP_ENGINE_PASSTHROUGH << 20) | (1 << 4) /* EOM */ | (1 << 3) /* INIT */ | (1 << 1) /* SOC */ | (1 << 0); /* IOC */ pt_desc.dw[1] = SLOT_SIZE; pt_desc.dw[2] = (uint32_t)sb_slot; pt_desc.dw[3] = (CCP_MEMTYPE_SB << 16); pt_desc.dw[4] = (uint32_t)(out_phys & 0xFFFFFFFF); pt_desc.dw[5] = (uint32_t)((out_phys >> 32) & 0xFFFF) | (CCP_MEMTYPE_SYSTEM << 16); pt_desc.dw[6] = 0; pt_desc.dw[7] = 0; printf("[*] Descriptor built:\n"); for (int i = 0; i < 8; i++) printf(" dw[%d] = 0x%08x\n", i, pt_desc.dw[i]); /* Write descriptor to queue ring buffer at current tail */ uint64_t tail_phys = (uint64_t)tail; uint32_t tail_off = (uint32_t)(tail_phys & 0xFFF); void *tail_map = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, dmem_fd, (off_t)(tail_phys & ~0xFFFULL)); if (tail_map == MAP_FAILED) { printf("[!] Cannot map queue ring at tail 0x%08x\n", tail); munmap(out_map, 0x1000); munmap(qmap, 0x100); continue; } memcpy((uint8_t *)tail_map + tail_off, &pt_desc, CCP_DESC_SIZE); printf("[+] Descriptor written to queue ring at 0x%08x\n", tail); munmap(tail_map, 0x1000); /* Advance tail pointer to submit the command */ uint32_t new_tail = tail + CCP_DESC_SIZE; printf("[*] Advancing tail: 0x%08x -> 0x%08x\n", tail, new_tail); *(volatile uint32_t *)((uint8_t *)qmap + CCP_REG_TAIL_LO) = new_tail; /* Poll for completion: wait for head to catch up */ printf("[*] Waiting for CCP to process...\n"); for (int poll = 0; poll < 10000; poll++) { uint32_t head = *(volatile uint32_t *) ((uint8_t *)qmap + CCP_REG_HEAD_LO); if (head >= new_tail) break; usleep(10); } uint32_t final_head = *(volatile uint32_t *) ((uint8_t *)qmap + CCP_REG_HEAD_LO); uint32_t final_status = *(volatile uint32_t *) ((uint8_t *)qmap + CCP_REG_STATUS); printf("[*] head=0x%08x status=0x%08x\n", final_head, final_status); munmap(qmap, 0x100); /* Read the result */ uint8_t result[SLOT_SIZE]; memcpy(result, out_map, SLOT_SIZE); munmap(out_map, 0x1000); /* Check if the output changed from our 0xCC fill */ int changed = 0; for (int i = 0; i < SLOT_SIZE; i++) { if (result[i] != 0xCC) { changed = 1; break; } } if (changed) { printf("[+] PASSTHROUGH succeeded! SB slot %d data:\n", sb_slot); hexdump("SB slot data", result, SLOT_SIZE); output_add_section(SECTION_PASSTHROUGH, result, SLOT_SIZE); found = 1; } else { printf("[!] Output unchanged — PASSTHROUGH may have failed or " "slot is empty\n"); printf("[*] Try a different BAR, queue, or verify slot number " "from Approach 6 output\n"); } if (found) break; } close(dmem_fd); return found ? 0 : -1; } /* ================================================================ * APPROACH 9: Decrypt Oracle (NEW — uses real PUP data) * * Feed a real encrypted PUP header from /data/pup_header.bin to * the DecryptHeader ioctl and capture the result. If the ioctl * decrypts in-place, we get the plaintext back. * * Upload the encrypted PUP header via FTP before running: * python -c " * with open('PS5UPDATE14.00.PUP','rb') as f: * f.seek(0x200) # skip BLS outer, adjust offset as needed * data = f.read(0x1000) * with open('pup_header.bin','wb') as f: * f.write(data) * " * ftp> put pup_header.bin /data/pup_header.bin * * Then run: ./ps5_key_extract -d * ================================================================ */ #define ORACLE_HEADER_PATH "/data/pup_header.bin" #define ORACLE_MAX_SIZE 0x10000 /* 64KB max */ static int approach9_decrypt_oracle(void) { printf("\n=== APPROACH 9: Decrypt Oracle ===\n"); int hdr_fd = open(ORACLE_HEADER_PATH, O_RDONLY); if (hdr_fd < 0) { printf("[!] Cannot open %s\n", ORACLE_HEADER_PATH); printf("[*] Upload the encrypted PUP header via FTP first.\n"); printf("[*] On PC, extract it from the BLS container:\n"); printf(" python pup.py extract-header PS5UPDATE14.00.PUP " "pup_header.bin\n"); printf(" ftp> put pup_header.bin /data/pup_header.bin\n"); return -1; } /* Read the encrypted header */ uint8_t *enc_buf = (uint8_t *)malloc(ORACLE_MAX_SIZE); uint8_t *dec_buf = (uint8_t *)malloc(ORACLE_MAX_SIZE); if (!enc_buf || !dec_buf) { printf("[!] malloc failed\n"); if (enc_buf) free(enc_buf); if (dec_buf) free(dec_buf); close(hdr_fd); return -1; } ssize_t hdr_size = read(hdr_fd, enc_buf, ORACLE_MAX_SIZE); close(hdr_fd); if (hdr_size <= 0) { printf("[!] Failed to read header file (size=%zd)\n", hdr_size); free(enc_buf); free(dec_buf); return -1; } printf("[+] Read %zd bytes from %s\n", hdr_size, ORACLE_HEADER_PATH); hexdump("encrypted header (first bytes)", enc_buf, hdr_size > 64 ? 64 : hdr_size); output_add_section(SECTION_ORACLE_IN, enc_buf, (uint32_t)hdr_size); /* Copy to decrypt buffer (ioctl may modify in-place) */ memcpy(dec_buf, enc_buf, hdr_size); int pup_fd = open(PUP_DEVICE_PATH, O_RDWR); if (pup_fd < 0) { printf("[!] Cannot open %s\n", PUP_DEVICE_PATH); free(enc_buf); free(dec_buf); return -1; } /* Try all three type values */ int types[] = { 1, 0, 2 }; const char *type_names[] = { "system update", "recovery", "type 2" }; for (int t = 0; t < 3; t++) { memcpy(dec_buf, enc_buf, hdr_size); struct { void *buffer; size_t length; int type; } dh_args; dh_args.buffer = dec_buf; dh_args.length = (size_t)hdr_size; dh_args.type = types[t]; printf("[*] DecryptHeader type=%d (%s)...\n", types[t], type_names[t]); int ret = ioctl(pup_fd, PUP_IOCTL_DECRYPT_HEADER, &dh_args); printf("[*] ioctl returned %d\n", ret); /* Check if buffer changed */ int changed = 0; for (ssize_t i = 0; i < hdr_size; i++) { if (dec_buf[i] != enc_buf[i]) { changed = 1; break; } } if (changed) { printf("[+] Buffer CHANGED after decrypt (type=%d)!\n", types[t]); hexdump("decrypted header (first bytes)", dec_buf, hdr_size > 128 ? 128 : hdr_size); output_add_section(SECTION_ORACLE_OUT, dec_buf, (uint32_t)hdr_size); /* Check for PUP magic in decrypted output */ if (hdr_size >= 4) { uint32_t magic; memcpy(&magic, dec_buf, 4); if (magic == 0xEEF51454) { printf("[!!!] Decrypted PUP magic found! Oracle SUCCESS!\n"); printf("[!!!] The PS5 decrypted the header correctly.\n"); } } } else { printf("[*] Buffer unchanged for type=%d\n", types[t]); } } close(pup_fd); free(enc_buf); free(dec_buf); return 0; } /* ---------- main ---------- */ int main(int argc, char **argv) { printf("=== PS5 PUP Key Extractor v4 ===\n"); printf("FW version: 0x%08x\n", kernel_get_fw_version()); printf("Kernel text base: 0x%llx\n", (unsigned long long)KERNEL_ADDRESS_TEXT_BASE); 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; int passthrough_mode = 0; int passthrough_slot = -1; int passthrough_queue = 0; int oracle_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; } else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) { passthrough_slot = (int)strtol(argv[++i], NULL, 0); passthrough_mode = 1; } else if (strcmp(argv[i], "-q") == 0 && i + 1 < argc) { passthrough_queue = (int)strtol(argv[++i], NULL, 0); } else if (strcmp(argv[i], "-d") == 0) { oracle_mode = 1; } } output_begin(); if (addr_mode) { approach5_read_address(target_addr, target_size); } else if (passthrough_mode) { if (passthrough_slot < 0 || passthrough_slot > 127) { printf("[!] Invalid slot number %d (must be 0-127)\n", passthrough_slot); return 1; } approach8_passthrough(passthrough_slot, passthrough_queue); } else if (oracle_mode) { approach9_decrypt_oracle(); } else { /* Full scan — run approaches 1-7 (all read-only) */ int r1 = approach1_ccp_lsb_dump(); int r2 = approach2_ioctl_handler_scan(); int r3 = approach3_instrumented_decrypt(); int r4 = approach4_kernel_memscan(); int r6 = approach6_ccp_queue_snoop(); int r7 = approach7_symbol_resolve(); 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("Approach 6 (CCP queue snoop): %s\n", r6 == 0 ? "OK" : "FAILED"); printf("Approach 7 (symbol resolve): %s\n", r7 == 0 ? "OK" : "FAILED"); printf("\n--- Next steps ---\n"); printf("1. Transfer dump to PC: python analyze_dump.py pup_key_dump.bin\n"); printf("2. If CCP snoop found AES with SB key:\n"); printf(" ./ps5_key_extract -p [-q ]\n"); printf("3. If handler/symbol analysis found key address:\n"); printf(" ./ps5_key_extract -a
-s 256\n"); printf("4. Decrypt oracle (upload PUP header first):\n"); printf(" ./ps5_key_extract -d\n"); } output_write("/data/pup_key_dump.bin"); return 0; }