Package: ps4debug
PyPS4debugFully asynchronous ps4debug implementation written in python.Install (pip)pip install ps4debugUsageA full example can be found at the end of this section!BasicsRunning asynchronous codeimportasyncioasyncdefmain():# Your asynchronous code goes here!...if__name__=='__main__':loop=asyncio.new_event_loop()loop.run_until_complete(main())Sending the PS4Debugfromps4debugimportPS4DebugifPS4Debug.find_ps4()isNone:hostname='ip address or hostname'awaitPS4Debug.send_ps4debug(hostname,port=9020)Creating a PS4Debug instanceCreating a PS4Debug instance doesnotconnect to the PlayStation 4 yet.fromps4debugimportPS4Debug,PS4DebugExceptiontry:# Omitting the first parameter will cause it to search the network for a PlayStation 4 system.ps4=PS4Debug()exceptPS4DebugException:# No PlayStation 4 running ps4debug was found.hostname='ip address or hostname'# Either ps4debug is not running.awaitPS4Debug.send_ps4debug('ip address or hostname',port=9020)# Or it was not reachable in this network. You should pass a hostname in that case.ps4=PS4Debug('ip address or hostname')Use the process listfromps4debugimportPS4Debugps4=PS4Debug()processes=awaitps4.get_processes()forpinprocesses:print(p.name,p.pid)# You may search for a specific name and get its process id (pid):process_name='eboot.bin'pid=next((p.pidforpinprocessesifp.name==process_name),None)ifpidisNone:print(process_name,'is not running!')Using a factory functionUsing the above snippets we can create a nice factory function to handle creating new PS4Debug objects.importasynciofromps4debugimportPS4Debugasyncdefget_ps4(hostname:str|None)->tuple[PS4Debug,int]:ps4=PS4Debug(hostname)processes=awaitps4.get_processes()pid=next((p.pidforpinprocessesifp.name=='eboot.bin'),None)returnps4,pidasyncdefmain():ps4,eboot_pid=awaitget_ps4('10.0.0.0')print('hostname:',ps4.pool.host,'eboot: ',eboot_pid)if__name__=='__main__':loop=asyncio.new_event_loop()loop.run_until_complete(main())Reading memoryYou can use theread_*functions to read memory from a process' memory.Raw memoryRaw memory is returned as a mutablebytearray.This way you can manipulate it directly like you would an array and parse it yourself.data=awaitps4.read_memory(pid,0xCA88888,length=32)Primitive typesPrimitive data types can be easily read just by passing the process id (pid) and the address.data=awaitps4.read_bool(pid,address)data=awaitps4.read_char(pid,address)data=awaitps4.read_byte(pid,address)data=awaitps4.read_ubyte(pid,address)data=awaitps4.read_int16(pid,address)data=awaitps4.read_uint16(pid,address)data=awaitps4.read_int32(pid,address)data=awaitps4.read_uint32(pid,address)data=awaitps4.read_int64(pid,address)data=awaitps4.read_uint64(pid,address)data=awaitps4.read_float(pid,address)data=awaitps4.read_double(pid,address)StructureFor structures you can pass either a format string, a struct.Struct instance or use construct's Struct.Here we are retrieving two floats. See thePython docsfor more information on format strings.f1,f2=awaitps4.read_struct(pid,0xCC001234,structure='<2f')StringYou can specify the encoding by passing the python charset name in the call.By defaultasciiis used and the string is null-terminated.data=awaitps4.read_text(pid,0xABCDEF,encoding='utf8')If a null-terminated string is not what you are looking for, you can also pass alengthparameter to set a fix length to be read.data=awaitps4.read_text(pid,0xABCDEF,length=32)Writing to memoryYou can use thewrite_*functions to write to the process' memory.Raw memoryYou can pass anybytesorbytearraysequence as parameter.status=awaitps4.write_memory(pid,0xCA88888,b'\xC3\xCC\x90')Primitive typesPrimitive data types can be easily written just by passing the process id (pid), the address and value.status=awaitps4.write_bool(pid,address,True)status=awaitps4.write_char(pid,address,'C')status=awaitps4.write_byte(pid,address,-128)status=awaitps4.write_ubyte(pid,address,255)status=awaitps4.write_int16(pid,address,-32000)status=awaitps4.write_uint16(pid,address,65000)status=awaitps4.write_int32(pid,address,-1000000)status=awaitps4.write_uint32(pid,address,1000000)status=awaitps4.write_int64(pid,address,-2**42)status=awaitps4.write_uint64(pid,address,2**42)status=awaitps4.write_float(pid,address,-128e-2)status=awaitps4.write_double(pid,address,128e5)StructureFor structures you can pass either a format string, a struct.Struct instance or use construct's Struct.Here we are writing two floats. See thePython docsfor more information on format strings.status=awaitps4.write_struct(pid,0xCC001234,structure='<2f',2.5,-1.0)StringYou can specify the encoding by passing the python charset name in the call.The null character is appended automatically if not provided.By defaultasciiis used.status=awaitps4.write_text(pid,0xABCDEF,'We are injecting text here!',encoding='ascii')AdvancedGetting process informationUsingget_process_infoYou can find out what exact CUSA is running.info=awaitps4.get_process_info(pid)ifinfo.title_id!='CUSA012345':print('This program only works with CUSA012345!')returnget_process_mapsis helpful for getting an overview of the memory layout and find the base address.maps=awaitps4.get_process_maps(pid)# Filter for 'executable'maps=[mforminmapsif'executable'inm.name]maps.sort(key=lambdam:m.start)base_address=maps[0].startiflen(maps)elseNoneAllocating MemoryAllocating memory works by using the two methodsallocate_memoryandfree_memory.
For the length, try to use multiples of 4096, the default page size.length=4096address=awaitps4.allocate_memory(pid,length)# Do something with your own memory section...awaitps4.free_memory(pid,address,length)You can easily wrap these methods to create a memory manager using closures.ps4=...pid=...allocated={}asyncdefallocate(id_:int=None,length:int=4096)->int:address=awaitps4.allocate_memory(pid,length)ifid_inallocated:awaitfree_memory(id_)allocated[id_]=(address,length)asyncdeffree(id_:int):ifid_inallocated:address,length=allocated[id_]awaitps4.free_memory(pid,address,length)addr1=allocate(100)addr2=allocate(101)...addr3=allocate(100)# Frees old #100free(100)free(101)Using the allocation contextThe aboveallocate_memory,free_memorycombination works fine but preferably you should stick to using an allocation context.asyncwithps4.memory(pid)asmemory:...When this with-block is entered, memory will be allocated, and will be automatically freed when the block is exited.You can use thememoryvariable to operate on the memory section assigned to you.asyncwithps4.memory(pid)asmemory:awaitps4.write_int32(pid,memory.address,42)# The context variable offers some useful methods tooawaitmemory.write(b'\x90'*100)Remote code executionExecuting by addressExecuting code is as simple as callingcalland telling it at which address to start executing a new thread.awaitps4.call(pid,0x8475610)The executing thread will have all its registers set to 0 and start with a bare minimum stack so be cautious when calling random functions in your debugging applications.Injecting assembly and executing itBy allocating memory as above and executing the data we write in our memory section as assembly code we are able to execute code remotely.asyncwithps4.memory(pid)asmemory:assembly=b'\x90\x90\x90\xC3'awaitmemory.write(assembly)awaitmemory.call()await memory.call()is a shortcut forawait ps4.call(pid, memory.address)Passing parametersPS4Debug allows 6 quadwords (more specifically 48 bytes) to be passed as parameters.By default you can pass up to 6 integers to the call that are serialized to 1 quadword each.awaitmemory.call(1,2,3,4,5,6)or outside of a contextawaitps4.call(pid,address,1,2,3,4,5,6)This will cause PS4Debug to fill the registers before starting execution in reverse order:rdi=1rsi=2rdx=3rcx=4rbx=5rax=6In case you want to pass for example a float, you may use the keyword argumentparameter_format.See thePython docsfor more information on format strings.awaitmemory.call(1.0,2.0,3,4,5,6,parameter_format='<2f4Q')The registers will still be filled the same way, so the remote code has to parse it (In this case by using bitwise operations).rdi=1.0|2.0rsi=3rdx=4rcx=5rbx=6rax=0🠔 Because we are using 8 bytes less than before, we could even pass one more value!Retrieving return valuesReturn values are taken from theraxregister when remote execution ends.To get your desired value back, make sure tomovit to theraxregister.Seestdcallfor more information.movrax,rdi; rax <- rdiretWith the above injected assembly code, we can use the following call to echo its parameter back to us.rax=awaitmemory.call(1,2,parameter_format='<2i')print(rax)# Will print '8589934593' (0x200000001)As with passing the parameters, this will return the two integers (4 bytes each) packed in onelong(8 bytes).You can either parse the data yourself usingint.from_bytes(rax[0:4], 'little')or use theoutput_formatparameter.ret1,ret2=awaitmemory.call(50,-100,parameter_format='<2i',output_format='<2i')print(ret1,ret2)# Will correctly print '1 2'See thePython docsfor more information on format strings.DebuggingSimilarly to allocating memory, a debugging context can be used to fire up the PS4Debug server and notify the PlayStation 4 system to connect to it.asyncwithps4.debugger(pid,resume=True)asdebugger:...Entering this context will start a server on port 755, so make sure it is reachable for the PlayStation 4.
When python exits this with-block the debugger and all breakpoints are stopped.Inside this with-block you are able to register breakpoints and do other relevant debugging operations like setting a breakpoint.asyncdefcallback(event:ps4debug.BreakpointEvent):thread_id=event.interrupt.lwpidregisters=event.interrupt.regsregisters.rax=42event.debugger.set_registers(thread_id,registers)asyncwithps4.debugger(pid,resume=True)asdebugger:debugger.set_breakpoint(0,True,address,on_hit=callback)Using async featuresBeing completely asynchronous, it makes sense to use the full advantage of it.tasks=[asyncio.create_task(ps4.write_int32(pid,0x123456,1000)),asyncio.create_task(ps4.write_int32(pid,0x789ABC,2000)),asyncio.create_task(ps4.write_int32(pid,0x654210,3000)),]pending=taskswhilelen(pending):done,pending=awaitasyncio.wait(pending,return_when=asyncio.FIRST_COMPLETED)fortaskindone:response_code=awaittiftistasks[0]:print('0x123456:',response_code)else:print(response_code)The above code creates 3 tasks that each write an integer to a different address.We then loop until all tasks are done (while len(pending)) and retrieve all completed tasks.You can then iterate all completed tasks (indone) andawaitthem individiually to get their return value.Theisoperator is ideal to check which task completed.Usingfunctoolsto make code cleanerSometimes always passing the same ps4debug object, the pid and the address gets quite tedious and unclean.You can wrap it in a function, like so:asyncdefget_score():returnawaitps4.read_int32(pid=pid,address=0xABCDEF)score=awaitget_score()Or even build a repository/service class for your game:classGameRepository(object):def__init__(self,ps4,pid):super(GameRepository,self).__init__()self.ps4=ps4self.pid=pidasyncdefget_score(self):returnawaitself.ps4.read_int32(pid=self.pid,address=0xABCDEF)repository=GameRepository(ps4,pid)score=awaitrepository.get_score()But that is very verbose. A more lightweight solution is to use the functools module.importfunctoolsget_score=functools.partial(ps4.read_int32,pid=pid,address=0xABCDEF)score=awaitget_score()This would even work for calls!injected_function=functools.partial(ps4.call,pid=pid,address=address,parameter_format='<2I')rax=awaitinjected_function(42,10)Full exampleimportsysimportfunctoolsimportasyncioimportps4debugasyncdefmain(ip_address):# You may also retrieve the IP address using the find_ps4() functionip_address=ip_addressorps4debug.PS4Debug.find_ps4()ps4=ps4debug.PS4Debug(ip_address)# Get processesprocesses=awaitps4.get_processes()# Find specific process idpid=next((p.pidforpinprocessesifp.name=='eboot.bin'),None)# Read memorygold=awaitps4.read_int32(pid,0xCA88888)# Write memorystatus=awaitps4.write_int32(pid,0xCA44444,9999)ifstatus!=ps4debug.ResponseCode.SUCCESS:print('There was an error!')# Let's do something where the async features shinestasks=[asyncio.create_task(ps4.write_int32(pid,0x123456,1000)),asyncio.create_task(ps4.write_int32(pid,0x789ABC,2000)),asyncio.create_task(ps4.write_int32(pid,0x654210,3000)),]pending=taskswhilelen(pending):# We iterate until all tasks are done but we stop waiting and handle already finished tasks.done,pending=awaitasyncio.wait(pending,return_when=asyncio.FIRST_COMPLETED)fortindone:# Let's say the first task was something special and want its result, we can use 'is' for that:# Note: If you use Coroutines, which are wrapped into tasks by asyncio. The following will not work.response_code=awaittiftistasks[0]:print('0x123456:',response_code)else:print(response_code)# Remotely execute code (Code injection)asyncwithps4.memory(pid,4096)asmemory:# Write your own assembly code to the systemassembly=b'\x90\x90\x90\x90\xC3\x90'awaitmemory.write(assembly)# And call it. Parameters are limited to 48 bytes or 6 values.# See https://docs.python.org/3/library/struct.html#format-strings for more information on the '<6Q' part if you're confused.rpc_stub=awaitps4.install_rpc(pid)rax=awaitmemory.call(1,2,3,4,5,6,parameter_format='<6Q')print(f'Thread returned with rax ={rax}')# You may also use functools.partial for cleaner calls:get_gold=functools.partial(ps4.read_int32,pid=pid,address=0xCA88888)set_gold=functools.partial(ps4.write_int32,pid=pid,address=0xCA88888)injected_function=functools.partial(ps4.call,pid=pid,rpc_stub=rpc_stub,address=memory,parameter_format='<6Q')gold=awaitget_gold()awaitset_gold(gold+10)awaitinjected_function(1,2,3,4,5,6)# Attaching the debugger works similarlyasyncwithps4.debugger(pid,resume=True)asdebugger:# Inside this context, a server on port 755 is being run to listen for debugger events.asyncdefbreakpoint_hit(event:ps4debug.BreakpointEvent):...# Do something with your breakpoint here!awaitdebugger.set_breakpoint(0,True,0x444111,on_hit=breakpoint_hit)# Alternatively to 'on_hit=...' you can use register_callbackdebugger.register_callback(breakpoint_hit)# Note! When the with block is exited, the debugger stops and all breakpoints will be disabled.# You can use asyncio.Event for example to keep the with block spinning.# Wait for everything to finishawaitasyncio.gather(*asyncio.all_tasks()-{asyncio.current_task()})if__name__=='__main__':# Normally you would use something like Typer for thisargs=sys.argv[1:]address=args[0]iflen(args)elseinput('Enter the IP address of your PS4: ')# asyncio.run(main()) might throw an exception because of the ProactorEventLoop closing on Windowsloop=asyncio.new_event_loop()loop.run_until_complete(main(address))# If you insist on using asyncio.run on Windows try to set the following snippetifsys.platform:asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())asyncio.run(main(address))Note: Do NOT run the above code as is. Depending on what game is running your system or the game might crash