Last active
January 28, 2026 23:17
-
-
Save idlesauce/2ded24b7b5ff296f21792a8202542aaa to your computer and use it in GitHub Desktop.
Usage: `py ps5_elf_sdk_downgrade.py path_to_file_or_folder`, then it will prompt you to enter the desired SDK version 1-10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import os | |
| import struct | |
| import shutil | |
| import argparse | |
| # ANSI color codes | |
| GREEN = '\033[92m' | |
| YELLOW = '\033[93m' | |
| RED = '\033[91m' | |
| RESET = '\033[0m' | |
| PT_SCE_PROCPARAM = 0x61000001 | |
| PT_SCE_MODULE_PARAM = 0x61000002 | |
| SCE_PROCESS_PARAM_MAGIC = 0x4942524F | |
| SCE_MODULE_PARAM_MAGIC = 0x3C13F4BF | |
| SCE_PARAM_MAGIC_OFFSET = 0x8 | |
| SCE_PARAM_MAGIC_SIZE = 0x4 | |
| SCE_PARAM_PS4_SDK_OFFSET = 0x10 | |
| SCE_PARAM_PS5_SDK_OFFSET = 0x14 | |
| SCE_PARAM_PS_VERSION_SIZE = 0x4 | |
| PHT_OFFSET_OFFSET = 0x20 | |
| PHT_OFFSET_SIZE = 0x8 | |
| PHT_COUNT_OFFSET = 0x38 | |
| PHT_COUNT_SIZE = 0x2 | |
| PHDR_ENTRY_SIZE = 0x38 | |
| PHDR_TYPE_OFFSET = 0x0 | |
| PHDR_TYPE_SIZE = 0x4 | |
| PHDR_OFFSET_OFFSET = 0x8 | |
| PHDR_OFFSET_SIZE = 0x8 | |
| PHDR_FILESIZE_OFFSET = 0x20 | |
| PHDR_FILESIZE_SIZE = 0x8 | |
| ELF_MAGIC = b'\x7FELF' | |
| PS4_FSELF_MAGIC = b'\x4F\x15\x3D\x1D' | |
| PS5_FSELF_MAGIC = b'\x54\x14\xF5\xEE' | |
| executable_extensions = [".bin", ".elf", ".self", ".prx", ".sprx"] | |
| sdk_version_pairs = { | |
| 1: (0x01000050, 0x07590001), | |
| 2: (0x02000009, 0x08050001), | |
| 3: (0x03000027, 0x08540001), | |
| 4: (0x04000031, 0x09040001), | |
| 5: (0x05000033, 0x09590001), | |
| 6: (0x06000038, 0x10090001), | |
| 7: (0x07000038, 0x10590001), | |
| 8: (0x08000041, 0x11090001), | |
| 9: (0x09000040, 0x11590001), | |
| 10: (0x10000040, 0x12090001), | |
| } | |
| sdk_version_pairs_min = 1 | |
| sdk_version_pairs_max = 10 | |
| format_map = { 1: '<B', 2: '<H', 4: '<I', 8: '<Q' } | |
| def read_le_int(file, offset, size): | |
| file.seek(offset) | |
| data = file.read(size) | |
| if size not in format_map: | |
| raise ValueError(f"Unsupported size: {size}. Must be 1, 2, 4, or 8 bytes.") | |
| return struct.unpack(format_map[size], data)[0] | |
| def write_le_int(file, offset, size, value): | |
| if size not in format_map: | |
| raise ValueError(f"Unsupported size: {size}. Must be 1, 2, 4, or 8 bytes.") | |
| data = struct.pack(format_map[size], value) | |
| file.seek(offset) | |
| file.write(data) | |
| def patch_file(file, ps5_sdk_version, ps4_version): | |
| segment_count = read_le_int(file, PHT_COUNT_OFFSET, PHT_COUNT_SIZE) | |
| pht_offset = read_le_int(file, PHT_OFFSET_OFFSET, PHT_OFFSET_SIZE) | |
| for i in range(segment_count): | |
| segment_type = read_le_int(file, pht_offset + i * PHDR_ENTRY_SIZE + PHDR_TYPE_OFFSET, PHDR_TYPE_SIZE) | |
| if segment_type != PT_SCE_PROCPARAM and segment_type != PT_SCE_MODULE_PARAM: | |
| continue | |
| segment_offset = read_le_int(file, pht_offset + i * PHDR_ENTRY_SIZE + PHDR_OFFSET_OFFSET, PHDR_OFFSET_SIZE) | |
| param_struct_size = read_le_int(file, segment_offset, 4) | |
| if param_struct_size == 0 and segment_type == PT_SCE_MODULE_PARAM: | |
| print(f"{YELLOW}[?] Module file has no param, skipping '{file.name}'{RESET}") | |
| return True | |
| if param_struct_size < SCE_PARAM_PS5_SDK_OFFSET + SCE_PARAM_PS_VERSION_SIZE: | |
| print(f"{RED}[!] Unexpected param struct size 0x{param_struct_size:X} for file '{file.name}', aborting{RESET}") | |
| return False | |
| magic = read_le_int(file, segment_offset + SCE_PARAM_MAGIC_OFFSET, SCE_PARAM_MAGIC_SIZE) | |
| if (segment_type == PT_SCE_PROCPARAM and magic != SCE_PROCESS_PARAM_MAGIC) or (segment_type == PT_SCE_MODULE_PARAM and magic != SCE_MODULE_PARAM_MAGIC): | |
| print(f"{RED}[!] Invalid param magic 0x{magic:08X} for file '{file.name}', aborting{RESET}") | |
| return False | |
| og_ps5_sdk_version = read_le_int(file, segment_offset + SCE_PARAM_PS5_SDK_OFFSET, 4) | |
| og_ps4_sdk_version = read_le_int(file, segment_offset + SCE_PARAM_PS4_SDK_OFFSET, 4) | |
| write_le_int(file, segment_offset + SCE_PARAM_PS5_SDK_OFFSET, SCE_PARAM_PS_VERSION_SIZE, ps5_sdk_version) | |
| write_le_int(file, segment_offset + SCE_PARAM_PS4_SDK_OFFSET, SCE_PARAM_PS_VERSION_SIZE, ps4_version) | |
| print(f"{GREEN}[+] Patched file '{file.name}': PS5 SDK version 0x{og_ps5_sdk_version:08X} -> 0x{ps5_sdk_version:08X}, PS4 version 0x{og_ps4_sdk_version:08X} -> 0x{ps4_version:08X}{RESET}") | |
| return True | |
| # No process or module param segment found | |
| return False | |
| def process_file(file_path, create_backup, ps5_sdk_version, ps4_version): | |
| with open(file_path, 'r+b') as file: | |
| file.seek(0) | |
| magic = file.read(4) | |
| if magic != ELF_MAGIC: | |
| if magic == PS4_FSELF_MAGIC or magic == PS5_FSELF_MAGIC: | |
| print(f"{RED}[!] Aborting, file '{file_path}' is a signed file, this script expects unsigned ELF files{RESET}") | |
| exit(1) | |
| return | |
| file.seek(0) | |
| if create_backup and not os.path.exists(file_path + ".bak"): | |
| shutil.copyfile(file_path, file_path + ".bak") | |
| print(f"Backup created for '{file_path}'") | |
| if not patch_file(file, ps5_sdk_version, ps4_version): | |
| exit(1) | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="Patches the SDK version PS5 ELF files") | |
| parser.add_argument("input", help="Path to an ELF file or a folder which will be processed recursively") | |
| parser.add_argument("--ps5_ver", help="(optional) Custom PS5 SDK version to set (e.g. 0x04000031)", type=lambda x: int(x, 0)) | |
| parser.add_argument("--ps4_ver", help="(optional) Custom PS4 version to set (e.g. 0x09040001)", type=lambda x: int(x, 0)) | |
| parser.add_argument("--no-backup", action="store_true", help="(optional) Do not create backup .bak files") | |
| args = parser.parse_args() | |
| ps5_sdk_version = args.ps5_ver | |
| if ps5_sdk_version and ps5_sdk_version > 0xFFFFFFFF: | |
| print(f"{RED}[!] Invalid PS5 SDK version '{ps5_sdk_version}', must be a 32-bit value{RESET}") | |
| exit(1) | |
| ps4_version = args.ps4_ver | |
| if ps4_version and ps4_version > 0xFFFFFFFF: | |
| print(f"{RED}[!] Invalid PS4 version '{ps4_version}', must be a 32-bit value{RESET}") | |
| exit(1) | |
| if not ps5_sdk_version or not ps4_version: | |
| # Select from known pairs | |
| print(f"Enter target PS5 SDK version ({sdk_version_pairs_min}-{sdk_version_pairs_max}):") | |
| try: | |
| ver_input = int(input().strip()) | |
| if ver_input not in sdk_version_pairs: | |
| raise ValueError() | |
| except ValueError: | |
| print("Invalid or unsupported version") | |
| exit(1) | |
| selected = sdk_version_pairs[ver_input] | |
| if not ps5_sdk_version: | |
| ps5_sdk_version = selected[0] | |
| if not ps4_version: | |
| ps4_version = selected[1] | |
| print(f"Selected PS5 SDK version 0x{ps5_sdk_version:08X} and PS4 version 0x{ps4_version:08X}") | |
| input_path = args.input | |
| create_backup = not args.no_backup | |
| if os.path.isfile(input_path): | |
| process_file(input_path, create_backup, ps5_sdk_version, ps4_version) | |
| elif os.path.isdir(input_path): | |
| all_files = [ | |
| os.path.join(dp, f) | |
| for dp, dn, filenames in os.walk(input_path) | |
| for f in filenames if os.path.splitext(f)[1] in executable_extensions | |
| ] | |
| for file_path in all_files: | |
| process_file(file_path, create_backup, ps5_sdk_version, ps4_version) | |
| else: | |
| print(f"{RED}[!] Invalid input '{input_path}'{RESET}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment