Files
chimera-gfx-Public/tools/audit_ps5_loader_runtime.py
Chimera GFX release export a6037502d7
phase0-ci / build-and-audit (push) Successful in 2m14s
Publish Chimera GFX source
2026-09-03 03:27:14 +02:00

574 lines
19 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Generate the exact Phase-0.6 PS5 loader/runtime audit without execution."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import subprocess
from pathlib import Path
from typing import Any
ELFLDR_COMMIT = "699e8bcff03e91e8d6ca6eba281af25c5a58d8c2"
ELFLDR_SHA256 = "092d16ee0ede0c494947efd38d1a17bbd7cc4b022d3858ea898833c188c703e8"
ELFLDR_SIZE = 397000
PLDMGR_COMMIT = "cfbc70f30f419b09bf2b52283f7409e2d3117ee1"
PLDMGR_SHA256 = "518740adbacccb9094fadb07dd424c53ee290f38306449ccc9d6957fdf813c0b"
SDK_COMMIT = "d2e2e585740362976a39fdd5ccf390f199a7bc37"
def digest_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def digest_file(path: Path) -> str:
return digest_bytes(path.read_bytes())
def run(command: list[str]) -> str:
result = subprocess.run(
command,
check=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
return result.stdout.replace("\r\n", "\n")
def git_identity(path: Path, expected: str) -> dict[str, Any]:
git = ["git", "-c", "core.autocrlf=true", "-C", str(path)]
commit = run([*git, "rev-parse", "HEAD"]).strip()
if commit != expected:
raise ValueError(f"{path}: expected commit {expected}, got {commit}")
status = run([*git, "status", "--porcelain"])
if status:
raise ValueError(f"{path}: source checkout is dirty")
return {"commit": commit, "dirty": False}
def source_record(root: Path, path: Path) -> dict[str, Any]:
data = path.read_bytes().replace(b"\r\n", b"\n")
return {
"normalization": "lf",
"path": path.relative_to(root).as_posix(),
"sha256": digest_bytes(data),
"size": len(data),
}
def require_tokens(path: Path, tokens: list[str]) -> None:
text = path.read_text(encoding="utf-8")
missing = [token for token in tokens if token not in text]
if missing:
raise ValueError(f"{path}: required evidence missing: {missing}")
def parse_binary(readelf_output: str, disassembly: str) -> dict[str, Any]:
entry_match = re.search(r"Entry point address:\s+(0x[0-9a-f]+)", readelf_output)
relocation_match = re.search(
r"Relocation section '\.rela\.dyn'.*contains (\d+) entries",
readelf_output,
)
relative_match = re.search(r"\(RELACOUNT\)\s+(\d+)", readelf_output)
if entry_match is None or relocation_match is None or relative_match is None:
raise ValueError("readelf output lacks required header/relocation evidence")
needed = sorted(re.findall(r"\(NEEDED\).*\[([^\]]+)\]", readelf_output))
dynsym_match = re.search(
r"Symbol table '\.dynsym'.*?\n(?P<body>.*?)(?:\nSymbol table|\Z)",
readelf_output,
flags=re.DOTALL,
)
if dynsym_match is None:
raise ValueError("readelf output lacks .dynsym")
undefined: list[str] = []
for line in dynsym_match.group("body").splitlines():
match = re.search(r"\bUND\s+(\S+)\s*$", line)
if match and match.group(1):
undefined.append(match.group(1))
array_sizes: dict[str, int] = {}
for name in ("PREINIT_ARRAY", "INIT_ARRAY", "FINI_ARRAY"):
match = re.search(rf"\({name}SZ\)\s+(\d+)", readelf_output)
if match is None:
raise ValueError(f"readelf output lacks {name}SZ")
array_sizes[name.lower()] = int(match.group(1))
load_segments = []
for line in readelf_output.splitlines():
match = re.match(
r"\s*LOAD\s+\S+\s+\S+\s+\S+\s+(\S+)\s+(\S+)\s+([RWE ]+)\s+\S+",
line,
)
if match:
load_segments.append(
{
"file_size": int(match.group(1), 16),
"memory_size": int(match.group(2), 16),
"permissions": match.group(3).replace(" ", ""),
}
)
return {
"disassembly_sha256": digest_bytes(disassembly.encode("utf-8")),
"dt_needed": needed,
"entry_point": entry_match.group(1),
"init_fini_array_sizes": array_sizes,
"load_segments": load_segments,
"readelf_report_sha256": digest_bytes(readelf_output.encode("utf-8")),
"relocations": {
"relative": int(relative_match.group(1)),
"total": int(relocation_match.group(1)),
},
"tls_present": bool(
re.search(r"^\s*TLS\s", readelf_output, flags=re.MULTILINE)
or re.search(r"\.(?:tdata|tbss)\b", readelf_output)
),
"undefined_dynamic_symbols": sorted(undefined),
}
def effect(
effect_id: str,
classification: str,
scope: str,
evidence: list[str],
blocker: bool,
) -> dict[str, Any]:
return {
"blocker": blocker,
"classification": classification,
"evidence": evidence,
"id": effect_id,
"scope": scope,
}
def build_audit(args: argparse.Namespace) -> dict[str, Any]:
root = args.root.resolve()
loader_source = args.loader_source.resolve()
manager_source = args.payload_manager_source.resolve()
sdk_source = args.sdk_source.resolve()
asset = args.loader_asset.resolve()
loader_git = git_identity(loader_source, ELFLDR_COMMIT)
manager_git = git_identity(manager_source, PLDMGR_COMMIT)
sdk_git = git_identity(sdk_source, SDK_COMMIT)
if asset.stat().st_size != ELFLDR_SIZE or digest_file(asset) != ELFLDR_SHA256:
raise ValueError("elfldr release asset identity mismatch")
loader_files = [
loader_source / name
for name in ("main.c", "elfldr.c", "elfldr.h", "pt.c", "pt.h", "socksrv.c", "Makefile")
]
manager_files = [
manager_source / name
for name in (
"src/ps5_launcher.c",
"src/http_server.c",
"src/autoload.c",
"src/main.c",
"src/payload_mgr.c",
"include/pldmgr.h",
"Makefile",
)
]
sdk_files = [
sdk_source / name
for name in (
"crt/crt.c",
"crt/patch.c",
"crt/kernel.c",
"include/ps5/payload.h",
"crt/Makefile",
)
]
require_tokens(
loader_source / "elfldr.c",
[
"rfork_thread(RFPROC | RFCFDG | RFMEM",
"execve(SceSpZeroConf, argv, 0)",
"pt_setlong(pid, r.r_rsp-8, r.r_rip)",
"r.r_rip = entry",
"r.r_rdi = args",
"kernel_overlap_sockets",
"kernel_set_ucred_uid(pid, 0)",
"pt_detach(pid, 0)",
],
)
require_tokens(
loader_source / "pt.c",
[
"kernel_set_ucred_authid(mypid, 0x4800000000010003l)",
"kernel_set_ucred_authid(mypid, authid)",
"while(jmp_reg.r_rsp <= bak_reg.r_rsp)",
],
)
require_tokens(
loader_source / "main.c",
["kernel_set_qaflags(qa_flags)", "elfldr_raise_privileges(mypid)"],
)
require_tokens(
loader_source / "socksrv.c",
["signal(SIGCHLD, SIG_IGN)", "while(1)", "serve_elfldr(port)"],
)
require_tokens(
manager_source / "src/ps5_launcher.c",
['server_addr.sin_addr.s_addr = inet_addr("127.0.0.1")', "send(sock"],
)
require_tokens(
manager_source / "src/http_server.c",
[
"ps5_launch_elf(final_path)",
"fopen(path, \"wb\")",
"payload_mgr_import_to_storage",
],
)
require_tokens(
sdk_source / "crt/crt.c",
["__patch_init()", "payload_terminate(void)", "_start(payload_args_t *args)"],
)
readelf_output = run(
[
str(args.readelf),
"-h",
"-l",
"-S",
"-d",
"-r",
"-s",
"-W",
str(asset),
]
)
disassembly = run(
[str(args.objdump), "-d", "--no-show-raw-insn", str(asset)]
)
binary = parse_binary(readelf_output, disassembly)
effects = [
effect(
"elfldr_first_stage_qaflags_enable",
"EXPECTED_VOLATILE_RUNTIME_EFFECT",
"existing_loader_bootstrap",
["elfldr/main.c:48-59"],
False,
),
effect(
"elfldr_first_stage_privilege_restore",
"UNBOUNDED_OR_UNKNOWN",
"existing_exploit_host_process",
[
"elfldr/main.c:61-105 restores jail/root/caps/authid",
"UID is changed by elfldr_raise_privileges but is not backed up or restored",
],
True,
),
effect(
"payload_process_creation",
"PAYLOAD_PROCESS_LOCAL",
"new_SceSpZeroConf_child",
["elfldr/elfldr.c:570-710"],
False,
),
effect(
"ptrace_authid_restore_success_path",
"RESTORED_BY_LOADER",
"elfldr_service_process",
["elfldr/pt.c:35-54"],
False,
),
effect(
"ptrace_authid_restore_failure_path",
"UNBOUNDED_OR_UNKNOWN",
"elfldr_service_process",
[
"elfldr/pt.c:50-51 returns after failed restoration",
"no second restoration or process shutdown is present",
],
True,
),
effect(
"ptrace_single_step_completion",
"UNBOUNDED_OR_UNKNOWN",
"loader_control_path",
["elfldr/pt.c:238-246", "elfldr/pt.c:291-299"],
True,
),
effect(
"breakpoint_byte",
"RESTORED_BY_LOADER",
"payload_child",
["elfldr/elfldr.c:675-700"],
False,
),
effect(
"breakpoint_page_permissions",
"PAYLOAD_PROCESS_LOCAL",
"payload_child",
[
"elfldr/elfldr.c:669 changes page to RWX",
"no source edge restores the original protection",
],
False,
),
effect(
"payload_credentials",
"UNBOUNDED_OR_UNKNOWN",
"payload_child",
[
"elfldr/elfldr.c:447-513 restores jail/root/caps/authid",
"UID is set to zero and is not restored",
],
True,
),
effect(
"payload_mapping_args_sockets_pipes",
"UNBOUNDED_OR_UNKNOWN",
"payload_child",
[
"elfldr/elfldr.c:143-343",
"successful detach has no explicit unmap/close cleanup",
"cleanup depends on an unproven child termination path",
],
True,
),
effect(
"sdk_patch_init",
"PAYLOAD_PROCESS_LOCAL",
"payload_child",
["sdk/crt/crt.c", "sdk/crt/patch.c"],
False,
),
effect(
"sdk_termination_branch",
"UNBOUNDED_OR_UNKNOWN",
"payload_child",
[
"sdk/crt/crt.c payload_terminate may return, call exit, or trap",
"the exact branch for the injected SceSpZeroConf child is not proven",
],
True,
),
effect(
"payload_runtime_limit",
"UNBOUNDED_OR_UNKNOWN",
"detached_payload_child",
[
"elfldr/elfldr.c:703-710 detaches and returns the PID",
"no 2000 ms watchdog, wait, kill, or retry budget is present",
],
True,
),
effect(
"child_reaping",
"UNBOUNDED_OR_UNKNOWN",
"elfldr_service_process",
[
"elfldr/socksrv.c:400 ignores SIGCHLD",
"post-detach exit/resource cleanup semantics are not documented",
],
True,
),
effect(
"payload_manager_launch_hash_binding",
"UNBOUNDED_OR_UNKNOWN",
"payload_manager_to_elfldr",
[
"pldmgr/http_server.c resolves a path then calls ps5_launch_elf",
"pldmgr/ps5_launcher.c streams bytes without calculating or checking SHA-256",
],
True,
),
effect(
"payload_manager_upload",
"PERSISTENT_WRITE",
"payload_manager_storage",
[
"pldmgr/http_server.c writes /data/pldmgr/payloads/<name>.tmp",
"the upload is committed into payload storage",
],
True,
),
]
source_files = {
"elfldr": [source_record(root, path) for path in loader_files],
"payload_manager": [source_record(root, path) for path in manager_files],
"sdk": [source_record(root, path) for path in sdk_files],
}
hard_blockers = [
item["id"]
for item in effects
if item["classification"] in ("PERSISTENT_WRITE", "UNBOUNDED_OR_UNKNOWN")
and item["blocker"]
]
hard_blockers.extend(
[
"exact_exploit_and_autoloader_identity_unproven",
"firmware_9_60_not_independently_device_attested",
"return_continuation_after_payload_start_unproven",
]
)
return {
"artifact": {
"built": False,
"execution_eligible": False,
"filename": None,
"sha256": None,
"size": None,
},
"binary_evidence": binary,
"callgraph": {
"entry": "Payload Manager /loadpayload:<path>",
"edges": [
["Payload Manager /loadpayload:<path>", "ps5_launch_elf"],
["ps5_launch_elf", "connect 127.0.0.1:9021"],
["ps5_launch_elf", "send ELF bytes"],
["serve_elfldr", "elfldr_spawn"],
["elfldr_spawn", "rfork_thread"],
["rfork_thread child", "elfldr_rfork_entry"],
["elfldr_rfork_entry", "ptrace PT_TRACE_ME"],
["elfldr_rfork_entry", "execve SceSpZeroConf"],
["elfldr_spawn parent", "pt_syscall 599"],
["elfldr_spawn parent", "install then restore INT3 byte"],
["elfldr_spawn parent", "elfldr_exec"],
["elfldr_exec", "elfldr_raise_privileges"],
["elfldr_exec", "elfldr_prepare_exec"],
["elfldr_prepare_exec", "elfldr_load"],
["elfldr_prepare_exec", "elfldr_payload_args"],
["elfldr_prepare_exec", "push observed RIP at RSP-8"],
["elfldr_prepare_exec", "set RIP=payload entry"],
["elfldr_prepare_exec", "set RDI=payload_args"],
["elfldr_exec", "restore subset of credentials"],
["elfldr_exec", "ptrace PT_DETACH"],
["payload _start", "SDK __patch_init"],
["payload _start", "payload main"],
["payload _start", "payload_terminate"],
["payload_terminate", "return or exit or trap"],
],
"extraction": "reviewed source edges with required-token assertions",
},
"decision": "BLOCKED_VERSION_OR_UNBOUNDED_EFFECT",
"effects": effects,
"firmware": {
"device_attested": False,
"exact": "9.60",
"evidence": "user_provided_only",
},
"hard_blockers": sorted(hard_blockers),
"identity": {
"elfldr": {
**loader_git,
"installed_asset_hash_match": True,
"observed_inventory_path": (
"/data/pldmgr/payloads/elfldr/elfldr_v0.23.elf"
),
"observed_inventory_sha256": ELFLDR_SHA256,
"observed_inventory_version": "v0.23",
"release": "v0.23",
"release_asset_sha256": ELFLDR_SHA256,
"release_asset_size": ELFLDR_SIZE,
"repository": "https://github.com/ps5-payload-dev/elfldr.git",
},
"exact_exploit_autoloader": {
"identified": False,
"local_candidate": {
"filename": "Y2JB-Autoloader-403-1240.zip",
"sha256": (
"805e3f87f0c371223619ffc7d3a7b3c0d41a1fae8a8b1171d9e2f162659e8291"
),
"size": 504159435,
"status": "local_backup_candidate_not_installed_identity_proof",
},
"status": "UNPROVEN",
},
"payload_manager": {
**manager_git,
"installed_asset_hash_match": True,
"observed_inventory_path": (
"/data/pldmgr/payloads/pldmgr/pldmgr_v0.3.1.elf"
),
"observed_inventory_sha256": PLDMGR_SHA256,
"observed_inventory_version": "v0.3.1",
"observed_version_endpoint": "0.3.1",
"release": "v0.3.1",
"release_asset_sha256": PLDMGR_SHA256,
"repository": "https://github.com/itsPLK/ps5-payload-manager.git",
},
"sdk": {
**sdk_git,
"release": "v0.41",
"repository": "https://github.com/ps5-payload-dev/sdk.git",
},
},
"no_console_actions": {
"elf_executed": False,
"elf_transferred": False,
"gnm": False,
"raw_port_9021_contacted": False,
"rendering": False,
"videoout": False,
},
"observation_scope": {
"date": "2026-07-17",
"payload_manager_routes": [
"/autoload_status",
"/get_config",
"/list_payloads",
"/log",
"/sources_list",
"/version",
],
"payload_manager_routes_read_only": True,
"strict_status_port_744_result": "ECONNREFUSED",
},
"phase": "0.6",
"schema_version": 1,
"source_evidence": source_files,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", type=Path, required=True)
parser.add_argument("--loader-source", type=Path, required=True)
parser.add_argument("--loader-asset", type=Path, required=True)
parser.add_argument("--payload-manager-source", type=Path, required=True)
parser.add_argument("--sdk-source", type=Path, required=True)
parser.add_argument("--readelf", type=Path, required=True)
parser.add_argument("--objdump", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
try:
document = build_audit(args)
output = args.output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(
json.dumps(document, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
newline="\n",
)
print(
"Phase-0.6 loader audit: BLOCKED_VERSION_OR_UNBOUNDED_EFFECT; "
f"{len(document['hard_blockers'])} hard blockers"
)
return 0
except (OSError, subprocess.CalledProcessError, ValueError) as error:
print(f"Phase-0.6 loader audit failed: {error}")
return 1
if __name__ == "__main__":
raise SystemExit(main())