Yet another Ren'Py PlayStation exploit
Important
This exploit is userland exploit. Don't expect homebrew enabler(HEN) level of access.
- Supported games
- How to use
- "Pickling" the save data (Can be skipped if you download the pre-made save file from releases)
- Pack into single save file (Optional if you cannot copy all files)
- Changing the save data on PS4/PS4 Slim/PS4 Pro/
- Changing the save data on PS5/PS5 Slim/PS5 Pro (Only works with PS4 games)
- Run custom code on the game
- Updating yarpe
- Auto loader
- Python API
- Credits
- Disclaimer
- A YEAR OF SPRINGS PS4 (CUSA30428, CUSA30429, CUSA30430, CUSA30431)
- Arcade Spirits: The New Challengers PS4 (CUSA32096, CUSA32097)
Thanks https://github.com/shahrilnet/remote_lua_loader/blob/main/SETUP.md for the base of this guide.
- Run
python3 pack_savegame.pyto generatesave.zip.- You can either use
updater.pyor Apollo Save Tool to apply the save data.
- You can either use
Important
You need to run pack_savegame.py first to generate save.zip before doing this step.
- Run
python3 pack_unzipper.pyto generate1-1-LT1_unzipper.save.- Rename
1-1-LT1_unzipper.saveto1-1-LT1.saveand copy it to the save data folder on your console.
- Rename
Warning
Doing this will certainly deletes your existing save data for that game. Make sure to back it up first!
Note
Guide below assumes you already made a save file in the game you want to modify.
Note
Due to Discord's file count limit, when you use the Discord bot to encrypt the save, you might not be able to upload all the files. Use 1-1-LT1.save(rename from 1-1-LT1_unzipper.save if you pickled it yourself by running pack_unzipper.py) to create one unified save containing all the files.
When you run the game, it will extract the files automatically.
- Download and extract
save.zipfile to your PC. - Use Apollo Save Tool to export decrypted save data to USB drive by using the "Copy save game to USB" option.
- Go to (/PS4/APOLLO/id_{YOUR_GAME_CUSA_ID}_savedata) and copy all the content in
save.zipto that folder, replacing the existing file. - Use Apollo Save Tool to import the new save data from USB drive with "Copy save game to HDD".
- Run the game and see if the save data is changed(by looking at the save image).
- Download and extract
save.zipfile to your PC. - Make sure you're logged-in to the PSN(or fake)-activated user.
- Connect your USB drive to the PS4/PS4 Slim/PS4 Pro.
- Use the PS4 settings menu to export the save data to USB. (
Settings -> Application Saved Data Management -> Saved Data in System Storage -> Copy to USB Storage Device -> Select your game and copy) - You should have
SAVEDATA00andSAVEDATA00.binfiles in/PS4/SAVEDATA/(hash)/CUSA(your game id)/on the USB drive. Use either Save Wizard or Discord bot to decrypt the save data. - Go to the decrypted save data folder and copy all the content in
save.zipto that folder, replacing the existing file. - Use either Save Wizard or Discord bot to encrypt the modified save data again.
- Put the encrypted
SAVEDATA00andSAVEDATA00.binfiles back to/PS4/SAVEDATA/(hash)/CUSA(your game id)/on the USB drive. - Connect the USB drive to the PS4/PS4 Slim/PS4 Pro.
- Use the PS4 settings menu to import the modified save data from USB. (
Settings -> Application Saved Data Management -> Saved Data on USB Storage Device -> Copy to System Storage -> Select your game and copy) - Run the game and see if the save data is changed(by looking at the save image).
- Requirements:
- PSN-activated PS5/PS5 Slim/PS5 Pro. Can be non-recent offline firmware if was activated in the past.
- A PSN(or fake)-activated PS4 on a firmware version that is earlier or equivilant to the PS5/PS5 Slim/PS5 Pro. Refer to this table. For example, PS4 9.00 can be used to create save game for PS5 >=4.00 but not below that.
- Find your logged-in PSN account id on the PS5/PS5 Slim/PS5 Pro. Either by going to the PlayStation settings or by using this website.
- Take your account ID number (~19 characters long, for PSPlay) and convert it to hex using this website.
- Follow the "PSN-Activated" PS4/PS4 Slim/PS4 Pro guide above until step 7 to export the save data to USB drive.
- Make sure you're logged-in to the PSN-activated user.
- Connect your USB drive to the PS5/PS5 Slim/PS5 Pro.
- Use the PS5 settings menu to import the encrypted save data from the USB drive. (
Saved Data and Game/App Settings -> Saved Data (PS4) -> Copy or Delete from USB Drive -> Select your game and import) - Run the game and see if the save data is changed(by looking at the save image).
- Get any TCP socket client(e.g. nc, hermes-link) on your PC.
- Prepare a python script that you want to run on the game.
- Send the script data to the console on port 9025.
- The script will be executed on the game.
- Download
save.zipto your PC. - Run
updater.py(updater_for_up_to_2.x.x.pyfor versions up to 2.x.x) on the console. (Check the above for how to run custom code on the game) - Send the
save.zipfile to the console using the same method as above. - Press X(or O) to exit the game when prompted.
Note
This requires making your own save file using the "Pickling" method above.
- Edit
yarpe_autoload/autoload.example.txtaccording to your needs and save it asyarpe_autoload/autoload.txt. - Copy necessary scripts/elfs/bins to
yarpe_autoload/folder. - "Pickle" the save data using
pack_savegame.py(andpack_unzipper.pyif needed). - Copy the generated save file to your console using the above method.
- Use
payloads/force_persistent.pyto load the save file automatically on game start. - To ignore the auto loader and go straight to socket listening part, hold triangle button while loading the save.
utils: Various utility modulesutils.ref: Functions to get references to bytes/bytearray objects.utils.ref.refbytes(data): Returns a pointer to the content of bytes objectdatathat can then be passed to functions.utils.ref.refbytearray(data): Returns a pointer to the content of bytearray objectdatathat can then be passed to functions.utils.ref.get_ref_addr(data): Returns the address of the content of bytes/bytearray/str/Structure objectdata.
utils.pack: Functions to pack/unpack data.utils.pack.p64(value_list): Packsvalue_listas 8-byte little-endian bytes.utils.pack.p64a(*value): Packsvalueas 8-byte little-endian bytes.utils.pack.p32(value_list): Packsvalue_listas 4-byte little-endian bytes.utils.pack.p32a(*value): Packsvalueas 4-byte little-endian bytes.utils.pack.p16(value_list): Packsvalue_listas 2-byte little-endian bytes.utils.pack.p16a(*value): Packsvalueas 2-byte little-endian bytes.utils.pack.unpack(data): Unpacks 8-byte little-endian bytesdatato integer.
utils.conversion: Functions to convert between different data types.utils.conversion.u64(value): Convertsvalueto unsigned 64-bit integer.utils.conversion.u64_to_i64(value): Converts unsigned 64-bit integervalueto signed 64-bit integer.utils.conversion.u32_to_i32(value): Converts unsigned 32-bit integervalueto signed 32-bit integer.utils.conversion.get_cstring(data): Gets the C-style null-terminated string fromdata.
utils.etc: Miscellaneous utility functions.utils.etc.sizeof(data): Returns the size of bytes/bytearray/Structure objectdata.utils.etc.flat(list_of_data): Flattens a list of objects into a single list.utils.etc.addrof(data): Returns the address of the objectdata.utils.etc.to_hex(data): Convertsdatato a hex string.utils.etc.alloc(size): Allocatessizebytes in the game's memory and returns the bytearray.utils.etc.bytes(arr): Converts a list of integersarrto bytes object.
utils.unsafe: Unsafe memory access functions.utils.unsafe.readbuf(addr, length): Readslengthbytes fromaddr.utils.unsafe.writebuf(addr, data): Writesdatatoaddr.utils.unsafe.readuint(addr, size): Reads an unsigned integer ofsizebytes fromaddr.utils.unsafe.writeuint(addr, value, size): Writes unsigned integervalueofsizebytes toaddr.utils.unsafe.fakeobj(addr): Returns a fake python object ataddr.
utils.rp: Logging to screen using Ren'Py functions.utils.rp.log(*args): Logsargsto screen.utils.rp.log_exc(string): Logs thestringas an exception to screen.
utils.tcp: TCP socket functions.utils.tcp.ip_to_int(ip_string): Convertsip_stringto integer.utils.tcp.htonl(value): Convertsvalueto network byte order.utils.tcp.create_tcp_socket(): Creates a TCP socket and returns its file descriptor.utils.tcp.get_socket_name(socket_fd): Gets the socket ip and port ofsocket_fd.utils.tcp.create_tcp_server(port, ip='0.0.0.0'): Creates a TCP server listening onip:portand returns its file descriptor and sockaddr_in struct.utils.tcp.create_tcp_client(ip, port): Creates a TCP client connected toip:portand returns its file descriptor.utils.tcp.accept_client(socket_fd): Accepts a client connection onsocket_fdand returns the client socket fd and sockaddr_in struct.utils.tcp.read_from_socket(socket_fd, size=4096): Reads up tosizebytes fromsocket_fdand returns the data.utils.tcp.read_all_from_socket(socket_fd): Reads all available data fromsocket_fduntil no more data is available and returns the data.utils.tcp.write_to_socket(socket_fd, data): Writesdatatosocket_fd.utils.tcp.close_socket(socket_fd): Closes the socketsocket_fd.
utils.fs: File system functions.utils.fs.stat_file(path): Gets the stat struct of the file atpath.utils.fs.file_exists(path): Returns whether the file atpathexists.utils.fs.read_file_data(path): Reads the file atpathand returns its data.utils.fs.write_file_data(path, data): Writesdatato the file atpath.
sc.sc: SploitCore instance- I will shorten
sc.sctoscfor easier reading. sc.mem: bytearray representing the game's memory.sc.functions: Known functions that you can access likesc.functions.function_name(arg1, arg2, ...). Arguments will be automatically converted to integers usingget_ref_addr().sc.syscalls: Known syscalls that you can access likesc.syscalls.syscall_name(arg1, arg2, ...). Arguments will be automatically converted to integers usingget_ref_addr().sc.errno: Last error number.sc.exec_addr: Base address of the game's executable in memory.sc.libc_addr: Base address of libc in the game's memory.sc.libkernel_addr: Base address of libkernel in the game's memory.sc.platform: The console platform(does not depend on game edition) (e.g., 'ps4', 'ps5').- Do note that these are in lowercase.
sc.version: The console firmware version (e.g., '9.00', '10.03').sc.is_jailbroken: Returns whether the console is jailbroken.sc.make_function_if_needed(name, addr): Creates a function entry insc.functionsif it does not already exist, and returns it.sc.make_syscall_if_needed(name, num): Creates a syscall entry insc.syscallsif it does not already exist, and returns it.sc.send_notification(message): Sends a notification to the PS4/PS5.sc.get_sysctl_int(name): Gets the integer value of the sysctl variablename.sc.set_sysctl_int(name, value): Sets the integer value of the sysctl variablenametovalue.sc.kill_game(): Kills the current game process.
- I will shorten
ropchain: ROPChain moduleropchain.ROPChain(sc, size=variable_per_console): Creates a ROP chain builder.chain.chain: The bytearray representing the ROP chain.chain.index: Current index in the ROP chain.chain.return_value:- If
push_get_return_value()was used, this will contain the return value of the function call after execution.
- If
chain.errno:- If
push_get_errno()was used, this will contain the errno value after execution.
- If
chain.addr: The address of the ROP chain in memory.chain.reset(): Resets the ROP chain to empty.chain.append(value): Appends the 8-bytevalueto the ROP chain.chain.extend(buf): Extends the ROP chain with the bytes inbuf.chain.push_gadget(gadget_name): Appends the gadget with namegadget_nameto the ROP chain.chain.push_value(value): Same asappend(value).chain.push_syscall(syscall_number, arg1, arg2, ...): Appends the syscall with numbersyscall_numberand its arguments to the ROP chain. Arguments will be automatically converted to integers usingget_ref_addr().chain.push_call(addr, arg1, arg2, ...): Appends the function call toaddrwith its arguments to the ROP chain. Arguments will be automatically converted to integers usingget_ref_addr().chain.push_get_return_value(): Appends the necessary gadgets to get the return value of the last function/syscall called.chain.push_get_errno(): Appends the necessary gadgets to get the errno value after the last function/syscall called.chain.push_write_into_memory(addr, data): Appends the necessary gadgets to writedatainto memory ataddr.chain.push_store_rax_into_memory(addr): Appends the necessary gadgets to store the value inRAXinto memory ataddr.chain.push_store_rdx_into_memory(addr): Appends the necessary gadgets to store the value inRDXinto memory ataddr.
ropchain.Executable(sc, size=variable_per_console): Creates an executable memory region.- When you create an
Executableinstance, you need to create 4 ROP chains:- Front chain
- Call/syscall chain
- Post chain
- Back chain
executable.chain: TheROPChaininstance representing the executable code.executable.errno: The errno value after execution.executable.setup_front_chain(): Sets up the front chain for execution.executable.setup_call_chain(func_addr, arg1, arg2, ...): Sets up the call chain to callfunc_addrwith its arguments. Arguments will be automatically converted to integers usingget_ref_addr().executable.setup_syscall_chain(syscall_number, arg1, arg2, ...): Sets up the syscall chain to callsyscall_numberwith its arguments. Arguments will be automatically converted to integers usingget_ref_addr().executable.setup_post_chain(): Sets up the post chain for execution.executable.setup_back_chain(): Sets up the back chain for execution.executable.execute(): Executes the code in the executable memory region.
- When you create an
structure: Structure modulestructure.Structure(structure_pair): Creates a structure definition fromstructure_pair.structure_pair = [(field_name1, field_size1), (field_name2, field_size2), ...]struct.size: Total size of the structure in bytes.struct.offsets: A dictionary mapping field names to their offsets in the structure.struct.create(defaults=None): Creates an instance of the structure with optional defaults(defaults[field] = value).struct.from_bytes(data): Creates an instance of the structure from the given bytesdata.- Do note that
datawill be copied into a new bytearray, so modifying the instance will not modifydata.
- Do note that
struct.from_bytearray(data): Creates an instance of the structure from the given bytearraydata.- Do note that modifying the instance will modify
dataas well.
- Do note that modifying the instance will modify
struct.from_address(addr): Creates an instance of the structure from the memory ataddr.- Do note that modifying the instance will modify the memory at
addras well.
- Do note that modifying the instance will modify the memory at
structure.StructureInstance(structure, defaults=None): Creates an instance ofstructure.- You can provide optional defaults(
defaults[field] = value) to initialize fields. - When you access fields as attributes(e.g.,
instance.field_name), it will get/set the matching field value automatically. instance.structure: The structure definition used to create this instance.instance.buf: The bytearray representing the structure's data.instance.addr: The address of the structure's data in memory.instance.get_field(field_name): Gets the value of the fieldfield_name.instance.set_field(field_name, value): Sets the value of the fieldfield_nametovalue.instance.get_field_raw(field_name, size): Gets the raw bytes of the fieldfield_namewith lengthsize.instance.set_field_raw(field_name, data): Sets the raw bytes of the fieldfield_nametodata.instance.reset(): Resets all fields to zero.
- You can provide optional defaults(
offsets: Offsets list moduleoffsets.GADGET_OFFSETS: A dictionary containing known gadget offsets that can be used in ROP chains.- Example:
GADGET_OFFSETS[game_name][console_variation][gadget_name] = offset
- Example:
offsets.LIBC_GADGETS: A list that marks which gadgets are from libc.- For example, if you set
pop rax; retgadget as libc gadget, it will be resolved usingsc.libc_addr + offsetinstead ofsc.exec_addr + offset.
- For example, if you set
offsets.LIBC_OFFSETS: A dictionary containing known libc function offsets that can be used to create function entries insc.functions.- Example:
LIBC_OFFSETS[game_name][console_variation][function_name] = offset
- Example:
offsets.EXEC_OFFSETS: A dictionary containing known executable function offsets that can be used to create function entries insc.functions.- Example:
EXEC_OFFSETS[game_name][console_variation][function_name] = offset
- Example:
constants: Constants moduleVERSION: The version of yarpe.- If you built yarpe from source, this will be "custom build".
CONSOLE_KIND: Current game console edition(e.g., 'PS4', 'PS5') (does not depend on the console model itself).- Do note that these are in uppercase.
SELECTED_GADGETS: The gadget set selected for the current game and console.SELECTED_LIBC: The libc offsets selected for the current game and console.SELECTED_EXEC: The executable offsets selected for the current game and console.SYSCALL: A dictionary containing known syscall numbers that can be used to create syscall entries insc.syscalls.- Example:
SYSCALL[syscall_name] = number
- Example:
nogc: A list that holds references to objects that should not be garbage collected.rp:renpyvariable that can be used to access Ren'Py functions.SHARED_VARS: A dictionary that can be used to share data between multiple payloads.
- @DrYenyen - Testing with me
- @Gezine - For giving me some important clues
- remote_lua_loader - Being the reference for things like syscall
- unsafe-python - Inspiration for unsafe memory access in Python
- And anyone else who helped me!
This project is for educational purposes only. The author is not responsible for any damage caused by the use of this project.