Asynchronous ps4debug implementation in Python.
Project description
PyPS4debug
A modern, asynchronous Python client for interacting with a PlayStation 4 running a compatible debug payload.
This library provides high-level APIs for process inspection, memory access, debugging, and remote code execution over a TCP interface.
Tested with ps4debug v1.0.15 and v1.1.19.
⚠️ Disclaimer
Interacting with system memory and debugging processes can damage your device, cause instability, or lead to permanent hardware failure.
This project does not implement the payload or server running on the target device—it only provides a client for communicating with it.
You assume all responsibility for any damage, data loss, or unintended behavior resulting from the use of this library.
⚠️ Important Notes
-
Code examples in this documentation are not directly runnable.
-
Values such as:
- IP addresses
- Process IDs (PID)
- Memory addresses will differ depending on your environment and target system.
You are expected to adapt examples to your specific setup.
Features
- Automatic device discovery on local network
- Async TCP client with connection pooling
- Process enumeration and inspection
- Memory read/write and allocation
- Kernel memory access
- Remote procedure calls (RPC)
- ELF and raw payload injection
- Debugging session management
- Memory scanning utilities
- Console output and notifications
Installation
pip install ps4debug
Quick Start
import asyncio
from ps4debug import PS4Debug
async def main():
# Discover a PS4 running the debug payload
ps4 = await PS4Debug.discover()
# Get version info
version = await ps4.get_version()
print("Version:", version)
# List processes
processes = await ps4.get_processes()
for proc in processes:
print(proc)
asyncio.run(main())
Core Concepts
Client
The PS4Debug class is the main entry point. It manages connections and exposes all high-level functionality.
from ps4debug import PS4Debug
ps4 = PS4Debug("192.168.0.10")
Processes
Retrieve and inspect running processes:
processes = await ps4.get_processes()
info = await ps4.get_process_info(pid)
maps = await ps4.get_process_maps(pid)
Payloads
Raw Payload
await ps4.send_payload(payload_bytes)
ELF Injection
await ps4.send_elf(pid, elf_bytes)
Console Interaction
await ps4.print("Hello from Python")
await ps4.notify("Done")
System Control
await ps4.reboot()
kernel_base = await ps4.get_kernel_base()
data = await ps4.read_kernel_memory(address, length)
Memory Access
Raw Memory
data = await ps4.read_memory(pid, address, length)
await ps4.write_memory(pid, address, b"\x90\x90")
Allocated Memory (Recommended)
The preferred way to work with memory is through MemoryContext, which manages allocation and cleanup automatically.
async with ps4.memory(pid, length=1024) as mem:
await mem.write(b"hello")
data = await mem.read(5)
Key properties:
- Memory is allocated on
__aenter__ - Memory is always freed on
__aexit__ - Safe bounds checking is enforced for reads/writes
- Prevents leaks and invalid memory reuse
Important: Once the context exits, the memory is no longer valid on the target system.
Structured Memory Access
MemoryContext supports structured data using ConstructModel:
value = await mem.read_model(MyModel)
await mem.write_model(my_model_instance)
Memory Views (Typed Access)
MemoryView provides a type-safe interface over memory.
Creating a View
view = ps4.view(pid, address)
Or from an allocation:
view = mem.view()
Reading Values
value = await view.uint32(offset=0x10).get()
Shorthand:
flag = await view.boolean(offset=0x20)
Writing Values
await view.uint32(offset=0x10).set(1337)
Shorthand:
await view.uint32(offset=0x10)(1337)
Offsetting Views
sub = view.offset(by=0x100)
value = await sub.uint16()
Supported Types
- Integers:
int8,uint8,int16,uint16,int32,uint32,int64,uint64 - Floating point:
floating,double - Boolean:
boolean - Raw bytes:
bytes(size) - Strings:
string(length) - Structured models:
model(MyModel)
Strings
text = await view.string(length=32, offset=0x0)
Or null-terminated:
text = await view.read_variable_text()
Memory Protection
await mem.change_protection(prot)
Executing Code from Allocated Memory
Allocated memory can be used as an execution target:
result = await mem.call(params=my_model, result_model=MyReturnModel)
This is a convenience wrapper around PS4Debug.call() using the allocated address.
Remote Procedure Calls (RPC)
Execute functions inside a target process:
result = await ps4.call(
pid=pid,
address=0x12345678,
params=my_model,
return_model=MyReturnModel
)
There is a CallRegisters model that can be used to get started.
Its values are serialized to 8-byte unsigned integers each.
from ps4debug import CallRegisters
registers = CallRegisters(
rdi=1,
rsi=2,
rdx=3,
rcx=4,
r8=5,
r9=6,
)
Return Model Requirements
Return values can be parsed into a model using a custom base class:
from typing import Annotated
from construct import Int32ul
from pydantic_construct import ConstructModel
class MyReturnModel(ConstructModel):
number: Annotated[int, Int32ul]
Constraints:
- The model must inherit from
ConstructModel - The total size must not exceed 8 bytes (size of the
RAXregister) - If no return model is provided, raw bytes are returned
Debugging
Debugging is exposed through an async context manager returning a DebuggingContext.
async with ps4.debugger(pid) as dbg:
await dbg.resume_process()
Only one debugging session can be active at a time.
Breakpoints
Breakpoints are managed using indexed slots.
index = await dbg.add_breakpoint(address=0x12345678, callback=lambda event: ...)
You can also configure them manually:
await dbg.set_breakpoint(index, address, callback, enabled=True)
- Limited number of breakpoint slots
- Managed internally via index and address mapping
Breakpoint Callbacks
Callbacks are async functions triggered when a breakpoint is hit:
async def on_break(event):
print("Breakpoint hit at", hex(event.interrupt.regs.rip))
event.resume = True # resume execution automatically
await dbg.add_breakpoint(address, on_break)
You can also register a global callback:
dbg.register_callback(global_handler)
Execution order:
- Global callback
- Breakpoint-specific callback
Watchpoints
Hardware watchpoints can be configured:
await dbg.set_watchpoint(index, address, enabled=True)
- Supports read/write monitoring
- Limited hardware slots
Process Control
await dbg.stop_process()
await dbg.resume_process()
await dbg.kill_process()
Threads
threads = await dbg.get_threads()
info = await dbg.get_thread_info(thread_id)
Thread-level control exists but may depend on server-side support:
await dbg.stop_thread(thread_id)
await dbg.resume_thread(thread_id)
Registers
General Purpose
regs = await dbg.get_registers(thread_id)
await dbg.set_registers(thread_id, regs)
Floating Point
fp = await dbg.get_fp_registers(thread_id)
Debug Registers
dbg_regs = await dbg.get_debug_registers(thread_id)
Single Stepping
await dbg.single_step()
Executes exactly one instruction.
Event Handling Model
- Debug events are received over a local TCP server
- Events are processed asynchronously
- Callbacks are awaited, the debugger will not continue until all callbacks ran
- Execution can optionally resume automatically via
event.resume = True(default behavior)
Memory Scanning
Memory scanning is performed using a builder-based query system.
PS4Debug.scan returns a local scanner, meaning:
- Memory is downloaded from the target
- Scanning happens on the client side
Basic Usage
There are three ways to run scan queries.
Bounded queries
scanner.query() returns a builder object that can run itself.
scanner = ps4.scan(pid)
results = await (
scanner
.query()
.int32()
.exact(100)
.execute()
)
Context builder
async with scanner.executor() as q:
q.int32()
q.exact(100)
The query will execute when the context is exited.
Manual
If the above two methods don't suit your needs you can simply create a builder.
builder = ScanBuilder()
builder.int32().exact(100)
scanner.execute(builder)
Builder Pattern
The ScanBuilder allows incremental query construction:
builder = scanner.query()
builder.int32().bigger(100).aligned(True)
results = await builder.execute()
Common Scan Types
Exact Match
.int32().exact(1337)
Range
.int32().between(low=100, high=200)
Increased / Decreased
.increased(to=150)
.decreased(to=50)
.changed(None)
Unknown Initial Value
.unknown_initial_value()
Filtering
Memory Bounds
.bounds(start, end)
Module Filtering
.only_module("libSce.*")
Pause Target
.pause(True)
Iterative Scanning
async for addr, value in scanner.query().int32().exact(100).execute_iter():
print(hex(addr), value)
- Supports large result sets
- Updates internal scan state automatically
Scan Lifecycle
Each session tracks:
initialvaluespreviousvalues
Subsequent scans operate as refinements rather than full scans.
Error Handling
Operations may raise multiple exception types depending on the failure source:
PS4DebugException– protocol-level or server-side failuresValueError,RuntimeError– invalid usage or stateasyncioexceptions – timeouts, cancellations- Parsing/validation errors from underlying libraries (e.g. model decoding from
Pydanticorconstruct)
Example:
from ps4debug.exceptions import PS4DebugException
try:
await ps4.reboot()
except PS4DebugException as e:
print("Operation failed:", e)
License
This project is licensed under the 0BSD License.
You are free to use, modify, and distribute this software with minimal restrictions.
Contributing
The project is considered functionally complete, but improvements and refinements are welcome. Focus areas:
- Improving API ergonomics
- Expanding documentation and examples
- Adding test coverage
Acknowledgements
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ps4debug-1.0.0.tar.gz.
File metadata
- Download URL: ps4debug-1.0.0.tar.gz
- Upload date:
- Size: 30.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cd68bd88a8bfebb8e573cc675d542c32d0442c01bc4c62320f0b43f02d07f094
|
|
| MD5 |
65ea879e9116bff4d1668a73266f46bc
|
|
| BLAKE2b-256 |
1cf955128a98b034a8f969c1ac83b8ffbfb3e3d2db581a6e17b56b883b02b590
|
File details
Details for the file ps4debug-1.0.0-py3-none-any.whl.
File metadata
- Download URL: ps4debug-1.0.0-py3-none-any.whl
- Upload date:
- Size: 40.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1c827d823d1fa38c50c88285ee4e6e773ab589a2b1c67ee0e177bbfe4c34a41e
|
|
| MD5 |
59d500f3425577aefeb1d458f1cba773
|
|
| BLAKE2b-256 |
cbffd4bd687741acfab435a86db32a077e713a8ae847e3572abfadbf9a45c941
|