Files
deepkicks--TitaniaPy/analyze_dump.py
T
deepkicks 60eab267db 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)
2026-10-15 03:11:14 -05:00

501 lines
17 KiB
Python

"""
analyze_dump.py — Parse pup_key_dump.bin and test key candidates against PUP headers.
Reads the structured dump from ps5_key_extract.c, extracts all key candidates
from CCP LSB slots, and tries AES-128-CBC/ECB decryption against the encrypted
PUP headers from two firmware files.
Usage:
python analyze_dump.py pup_key_dump.bin
python analyze_dump.py pup_key_dump.bin --pup14 path/to/14.00.PUP --pup14_1 path/to/14.10.PUP
"""
import argparse
import struct
import sys
from pathlib import Path
try:
from Crypto.Cipher import AES
except ImportError:
print("ERROR: pycryptodome required. Install with: pip install pycryptodome")
sys.exit(1)
# --- Dump format constants (must match ps5_key_extract.c 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: "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
SLOT_SIZE = 32
LSB_TOTAL_SIZE = LSB_COUNT * SLOTS_PER_LSB * SLOT_SIZE # 4096
# PUP header constants
PUP_HEADER_OFFSET = 0x410
PUP_HEADER_READ_SIZE = 64 # read enough for testing
# Known BLS entry sizes for validation
PUP_14_00_BLS_SIZE = 0x4B2339D5
PUP_14_10_BLS_SIZE = 0x4B2F8A3D
# Default PUP file paths (set to your local paths or pass via --pup14 / --pup14_1)
DEFAULT_PUP_14_00 = "PS5UPDATE14.00.PUP"
DEFAULT_PUP_14_10 = "PS5UPDATE14.10.PUP"
def parse_dump(data: bytes) -> dict:
"""Parse the structured dump file."""
if len(data) < 16:
print(f"[!] Dump file too small ({len(data)} bytes)")
return None
magic, version, section_count = struct.unpack_from("<QII", data, 0)
if magic != DUMP_MAGIC:
print(f"[!] Bad magic: 0x{magic:012x} (expected 0x{DUMP_MAGIC:012x})")
return None
print(f"[+] Dump version {version}, {section_count} sections")
header_size = 16 # dump_header_t
section_hdr_size = 16 # section_header_t
max_sections = 64
section_table_offset = header_size
sections = []
for i in range(min(section_count, max_sections)):
off = section_table_offset + i * section_hdr_size
stype, ssize, soffset, _ = struct.unpack_from("<IIII", data, off)
name = SECTION_NAMES.get(stype, f"Unknown(0x{stype:02x})")
print(f" Section {i}: {name}, {ssize} bytes at offset 0x{soffset:x}")
if soffset + ssize <= len(data):
section_data = data[soffset:soffset + ssize]
else:
print(f" [!] Section data extends past end of file")
section_data = data[soffset:] if soffset < len(data) else b""
sections.append({
"type": stype,
"size": ssize,
"offset": soffset,
"data": section_data,
"name": name,
})
return {"version": version, "sections": sections}
def extract_key_candidates(sections: list) -> list:
"""Extract all possible 16-byte key candidates from LSB sections."""
candidates = []
seen = set()
for sec in sections:
if sec["type"] not in (SECTION_LSB, SECTION_SNAP_PRE, SECTION_SNAP_POST, SECTION_ADDR_READ):
continue
data = sec["data"]
# Extract each 32-byte slot, then take the first 16 bytes as AES-128 key
total_slots = len(data) // SLOT_SIZE
for slot_idx in range(total_slots):
slot = data[slot_idx * SLOT_SIZE:(slot_idx + 1) * SLOT_SIZE]
if slot == b"\x00" * SLOT_SIZE:
continue
# AES-128 key = first 16 bytes of the slot
key16 = slot[:16]
key_hex = key16.hex()
if key_hex not in seen:
seen.add(key_hex)
candidates.append({
"key": key16,
"source": sec["name"],
"slot": slot_idx,
"full_slot": slot,
})
# Also try second 16 bytes (AES-128 key might be in upper half)
key16b = slot[16:32]
key_hex_b = key16b.hex()
if key_hex_b not in seen and key16b != b"\x00" * 16:
seen.add(key_hex_b)
candidates.append({
"key": key16b,
"source": sec["name"] + " (upper half)",
"slot": slot_idx,
"full_slot": slot,
})
# Also do sliding 16-byte window over all section data
for sec in sections:
data = sec["data"]
if len(data) < 16:
continue
for i in range(0, len(data) - 15, 4): # 4-byte aligned
window = data[i:i + 16]
if window == b"\x00" * 16:
continue
whex = window.hex()
if whex not in seen:
seen.add(whex)
candidates.append({
"key": window,
"source": f"{sec['name']} @ offset 0x{i:x}",
"slot": -1,
"full_slot": None,
})
return candidates
def find_lsb_diff(sections: list) -> list:
"""Find LSB slots that changed between pre and post snapshots."""
pre_data = None
post_data = None
for sec in sections:
if sec["type"] == SECTION_SNAP_PRE:
pre_data = sec["data"]
elif sec["type"] == SECTION_SNAP_POST:
post_data = sec["data"]
if pre_data is None or post_data is None:
return []
changed = []
min_len = min(len(pre_data), len(post_data))
total_slots = min_len // SLOT_SIZE
for slot_idx in range(total_slots):
pre_slot = pre_data[slot_idx * SLOT_SIZE:(slot_idx + 1) * SLOT_SIZE]
post_slot = post_data[slot_idx * SLOT_SIZE:(slot_idx + 1) * SLOT_SIZE]
if pre_slot != post_slot:
print(f"[!] LSB slot {slot_idx} CHANGED during decrypt:")
print(f" Before: {pre_slot.hex()}")
print(f" After: {post_slot.hex()}")
changed.append({
"slot": slot_idx,
"before": pre_slot,
"after": post_slot,
})
return changed
def read_pup_header(pup_path: str, offset: int = PUP_HEADER_OFFSET,
size: int = PUP_HEADER_READ_SIZE) -> bytes | None:
"""Read encrypted header bytes from a PUP file."""
path = Path(pup_path)
if not path.exists():
print(f"[!] PUP file not found: {pup_path}")
return None
file_size = path.stat().st_size
if file_size < offset + size:
print(f"[!] PUP file too small: {file_size} bytes")
return None
with open(path, "rb") as f:
f.seek(offset)
return f.read(size)
def validate_decrypted_header(decrypted: bytes, expected_bls_size: int,
pup_name: str) -> bool:
"""
Check if decrypted data looks like a valid PUP segment table entry.
Expected layout:
offset 0: uint64 LE — file_size (should be within 1MB of BLS entry size)
offset 8: uint16 LE — segment_count (should be 1-99)
"""
if len(decrypted) < 16:
return False
file_size = struct.unpack_from("<Q", decrypted, 0)[0]
segment_count = struct.unpack_from("<H", decrypted, 8)[0]
size_diff = abs(int(file_size) - int(expected_bls_size))
size_ok = size_diff < (1024 * 1024) # within 1MB
count_ok = 1 <= segment_count <= 99
if size_ok and count_ok:
print(f" [!!!] MATCH on {pup_name}!")
print(f" file_size=0x{file_size:x} (diff={size_diff} from BLS 0x{expected_bls_size:x})")
print(f" segment_count={segment_count}")
print(f" Full decrypted: {decrypted[:32].hex()}")
return True
return False
def test_key_against_pups(key: bytes, pup_headers: dict) -> bool:
"""Try a key with AES-128-CBC (zero IV) and AES-128-ECB on each PUP header."""
matched = False
for pup_name, (header_data, bls_size) in pup_headers.items():
if header_data is None:
continue
enc_block = header_data[:32] # first 32 bytes (2 AES blocks)
# AES-128-CBC with zero IV
try:
cipher = AES.new(key, AES.MODE_CBC, iv=b"\x00" * 16)
dec_cbc = cipher.decrypt(enc_block)
if validate_decrypted_header(dec_cbc, bls_size, f"{pup_name} (CBC)"):
matched = True
except (ValueError, KeyError):
pass
# AES-128-ECB
try:
cipher = AES.new(key, AES.MODE_ECB)
dec_ecb = cipher.decrypt(enc_block)
if validate_decrypted_header(dec_ecb, bls_size, f"{pup_name} (ECB)"):
matched = True
except (ValueError, KeyError):
pass
return matched
def hexdump_section(data: bytes, label: str, max_bytes: int = 256):
"""Print a hex dump of section data."""
print(f"\n--- {label} ---")
show = min(len(data), max_bytes)
for i in range(0, show, 16):
hex_part = " ".join(f"{b:02x}" for b in data[i:i + 16])
ascii_part = "".join(
chr(b) if 32 <= b < 127 else "." for b in data[i:i + 16]
)
print(f" {i:04x}: {hex_part:<48s} {ascii_part}")
if show < len(data):
print(f" ... ({len(data) - show} more bytes)")
def 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("<Q", data, off)[0]
print(f" +0x{off:02x} {name:16s} = 0x{val:016x}")
def display_ccp_regs(data: bytes):
"""Display CCP queue registers."""
print(f"\n--- CCP Queue Registers ---")
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)
for q in range(num_queues):
base = q * bytes_per_queue
print(f" Queue {q}:")
for r in range(regs_per_queue):
val = struct.unpack_from("<I", data, base + r * 4)[0]
name = CCP_QUEUE_REG_NAMES[r]
print(f" {name:20s} = 0x{val:08x}")
pub_lo = struct.unpack_from("<I", data, base + 6 * 4)[0]
pub_hi = struct.unpack_from("<I", data, base + 7 * 4)[0]
pub_mask = (pub_hi << 32) | pub_lo
priv_lo = struct.unpack_from("<I", data, base + 8 * 4)[0]
priv_hi = struct.unpack_from("<I", data, base + 9 * 4)[0]
priv_mask = (priv_hi << 32) | priv_lo
readable = pub_mask & ~priv_mask
print(f" CPU-readable slots mask: 0x{readable:016x}")
if readable == 0:
print(f" [!] No CPU-readable slots on this queue")
else:
for bit in range(64):
if readable & (1 << bit):
print(f" [+] Slot {bit} is CPU-readable")
def main():
parser = argparse.ArgumentParser(description="Analyze PS5 PUP key dump")
parser.add_argument("dumpfile", help="Path to pup_key_dump.bin")
parser.add_argument("--pup14", default=DEFAULT_PUP_14_00,
help="Path to PS5UPDATE14.00.PUP")
parser.add_argument("--pup14_1", default=DEFAULT_PUP_14_10,
help="Path to PS5UPDATE14.10.PUP")
args = parser.parse_args()
# Read dump file
dump_path = Path(args.dumpfile)
if not dump_path.exists():
print(f"[!] Dump file not found: {args.dumpfile}")
sys.exit(1)
print(f"[*] Reading dump: {args.dumpfile}")
dump_data = dump_path.read_bytes()
parsed = parse_dump(dump_data)
if parsed is None:
sys.exit(1)
# Display handler and metadata sections
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"])
elif sec["type"] == SECTION_CCP_REGS:
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)
changed_slots = find_lsb_diff(parsed["sections"])
if changed_slots:
print(f"\n[+] {len(changed_slots)} LSB slots changed during decrypt — high-priority candidates")
# Extract key candidates
candidates = extract_key_candidates(parsed["sections"])
print(f"\n[*] Extracted {len(candidates)} unique key candidates")
if not candidates:
print("[!] No key candidates found in dump. Possible issues:")
print(" - CCP LSB region not accessible via MMIO")
print(" - LSB contents cleared after use")
print(" - Key stored in PSP SRAM, not CCP LSB")
sys.exit(1)
# Read PUP headers
print(f"\n[*] Reading PUP headers...")
pup_headers = {}
header_14 = read_pup_header(args.pup14)
if header_14:
print(f" 14.00: {len(header_14)} bytes from offset 0x{PUP_HEADER_OFFSET:x}")
pup_headers["FW 14.00"] = (header_14, PUP_14_00_BLS_SIZE)
header_14_1 = read_pup_header(args.pup14_1)
if header_14_1:
print(f" 14.10: {len(header_14_1)} bytes from offset 0x{PUP_HEADER_OFFSET:x}")
pup_headers["FW 14.10"] = (header_14_1, PUP_14_10_BLS_SIZE)
if not pup_headers:
print("[!] No PUP files available for testing")
print("[*] Listing all key candidates instead:\n")
for i, cand in enumerate(candidates):
print(f" Candidate {i}: {cand['key'].hex()}")
print(f" Source: {cand['source']}, slot {cand['slot']}")
if cand['full_slot']:
print(f" Full: {cand['full_slot'].hex()}")
sys.exit(0)
# Test each candidate
print(f"\n[*] Testing {len(candidates)} candidates against {len(pup_headers)} PUP files...")
matches = []
for i, cand in enumerate(candidates):
if i > 0 and i % 100 == 0:
print(f" ... tested {i}/{len(candidates)}")
if test_key_against_pups(cand["key"], pup_headers):
matches.append(cand)
print(f" KEY FOUND: {cand['key'].hex()}")
print(f" Source: {cand['source']}, slot {cand['slot']}")
# Priority test: changed slots from approach 3
if changed_slots:
print(f"\n[*] Priority testing {len(changed_slots)} changed LSB slots...")
for cs in changed_slots:
for offset in (0, 16):
key = cs["after"][offset:offset + 16]
if key == b"\x00" * 16:
continue
print(f" Testing changed slot {cs['slot']} offset {offset}: {key.hex()}")
if test_key_against_pups(key, pup_headers):
matches.append({
"key": key,
"source": f"Changed LSB slot {cs['slot']} (post-decrypt)",
"slot": cs["slot"],
"full_slot": cs["after"],
})
# Report
print(f"\n{'=' * 60}")
if matches:
print(f"[+] FOUND {len(matches)} MATCHING KEY(S)!")
for m in matches:
print(f" Key: {m['key'].hex()}")
print(f" Source: {m['source']}")
else:
print("[*] No matching keys found.")
print("[*] Possible reasons:")
print(" - Key is AES-256, not AES-128")
print(" - Different cipher mode (CTR, XTS, etc.)")
print(" - Header structure differs from expected layout")
print(" - Key not present in dumped LSB data")
print("[*] Next steps:")
print(" - Try AES-256 with 32-byte slot data")
print(" - Reverse the ioctl handler code to find the exact algorithm")
print(" - Use approach 3 with real DMA memory for live capture")
print(f"{'=' * 60}")
if __name__ == "__main__":
main()