Apollo PS4 param.sfo File Structure: How It Modifies User ID and Account ID Fields
Apollo PS4 parses the binary SFO container format to locate the ACCOUNT_ID and PARAMS keys, then overwrites the 8-byte Account ID and the 32-bit User ID embedded in the PARAMS structure using direct memory manipulation.
The param.sfo file is a critical metadata container in every PS4 save game, storing the Account ID and User ID that tie save data to specific PlayStation Network profiles. Understanding the internal param.sfo file structure is essential for developers working with save game tools like Apollo PS4, which modifies these identifiers to enable save sharing across different accounts.
Binary Layout of the param.sfo File Structure
The param.sfo file follows the generic PS4 SFO container format, identified by the magic value 0x46535000. According to the Apollo PS4 source code in source/sfo.c, the binary layout consists of a fixed header followed by three variable-length tables.
SFO Header Format
The 20-byte header structure defines the offsets for the remaining tables:
| Offset | Size | Field |
|---|---|---|
| 0x00 | 4 bytes | Magic – 0x46535000 (SFO_MAGIC) |
| 0x04 | 4 bytes | Version – 0x0101 (SFO_VERSION) |
| 0x08 | 4 bytes | Key-table offset – Byte offset to the null-terminated key strings |
| 0x0C | 4 bytes | Data-table offset – Byte offset to the raw binary values |
| 0x10 | 4 bytes | Number of entries – Count of key/value pairs |
Index Table and Entry Structure
Immediately following the header is the index table, containing one entry per key/value pair. Each entry is defined by the sfo_index_table_t structure in source/sfo.c (lines 17-23):
typedef struct sfo_index_table_s {
u16 key_offset; // offset into the key table
u16 param_format; // data type (e.g. UTF-8, binary)
u32 param_length; // actual length of the value
u32 param_max_length;// allocated size in the data table
u32 data_offset; // offset into the data table
} sfo_index_table_t;
Apollo uses this index to locate specific keys without scanning the entire file linearly.
Key Data Structures: ACCOUNT_ID and PARAMS
Within the param.sfo data table, Apollo specifically targets two keys to modify ownership metadata: ACCOUNT_ID and PARAMS.
ACCOUNT_ID Field
The ACCOUNT_ID key stores the 64-bit PlayStation Network identifier as raw binary data. According to source/sfo.c, the expected size is defined by SFO_ACCOUNT_ID_SIZE (8 bytes). The index table entry for this key points to an 8-byte region in the data table containing the little-endian account identifier.
PARAMS Field and User ID Location
The PARAMS key contains a larger binary structure of type sfo_param_params_t, defined in source/sfo.c (lines 25-34):
typedef struct sfo_param_params_s {
u32 unk1; // usually zero
u32 user_id; // <-- the User ID we want to change
u8 unk2[32]; // padding / unknown purpose
u32 unk3; // usually zero
char title_id_1[0x10]; // primary title ID
char title_id_2[0x10]; // secondary title ID (mirrored)
u32 unk4; // usually zero
u8 chunk[0x3B0]; // additional save-metadata
} sfo_param_params_t;
The User ID is stored as a 32-bit unsigned integer at offset 0x04 within this structure (immediately following the unk1 field).
How Apollo Modifies User ID and Account ID Fields
Apollo implements targeted patching through two internal helper functions in source/sfo.c, which are orchestrated by the public patch_sfo function.
Patching the Account ID
The sfo_patch_account function (lines 63-71 in source/sfo.c) overwrites the 8-byte ACCOUNT_ID value:
static void sfo_patch_account(sfo_context_t *inout, u64 account) {
sfo_context_param_t *p;
if (!account) return;
p = sfo_context_get_param(inout, "ACCOUNT_ID");
if (p != NULL && p->actual_length == SFO_ACCOUNT_ID_SIZE) {
memcpy(p->value, &account, SFO_ACCOUNT_ID_SIZE);
}
}
This function validates that the parameter exists and has the expected 8-byte length before using memcpy to replace the raw binary data.
Patching the User ID
The sfo_patch_user_id function (lines 82-92 in source/sfo.c) updates the 32-bit field within the PARAMS structure:
static void sfo_patch_user_id(sfo_context_t *inout, u32 userid) {
sfo_context_param_t *p;
if (userid == 0) return;
p = sfo_context_get_param(inout, "PARAMS");
if (p != NULL) {
sfo_param_params_t *params = (sfo_param_params_t *)p->value;
params->user_id = userid;
}
}
This function casts the parameter's value buffer to the sfo_param_params_t structure type and directly assigns the new User ID to the user_id field.
Orchestrating the Patch Process
Both helpers are called by patch_sfo (implemented in source/sfo.c), which provides the high-level interface used by the Apollo UI:
int patch_sfo(const char *in_file_path, sfo_patch_t* patches) {
sfo_context_t *sfo = sfo_alloc();
if (sfo_read(sfo, in_file_path) < 0) { /* error handling */ }
sfo_patch_titleid(sfo); // copies title_id_1 → title_id_2
sfo_patch_account(sfo, patches->account_id);
sfo_patch_user_id(sfo, patches->user_id);
// … optional PSID / directory patches …
if (sfo_write(sfo, in_file_path) < 0) { /* error handling */ }
sfo_free(sfo);
return 0;
}
The sfo_patch_t structure (defined in include/sfo.h lines 22-28) carries the new identifiers:
typedef struct {
u32 flags;
u32 user_id; // <-- value passed to sfo_patch_user_id()
u64 account_id; // <-- value passed to sfo_patch_account()
u8* psid;
char* directory;
} sfo_patch_t;
When a user selects "Change Account ID" or "Change User ID" in the Apollo interface, the application populates this structure and invokes patch_sfo on the target PARAM.SFO file located in the save data directory.
Summary
-
param.sfo Structure: The file follows the PS4 SFO container format with a 20-byte header, an index table mapping keys to data offsets, and separate key and data tables. The
ACCOUNT_IDkey stores an 8-byte binary PSN identifier, while thePARAMSkey contains a 0x3F8-byte structure holding the 32-bit User ID at offset 0x04. -
Modification Method: Apollo parses the SFO into an in-memory
sfo_context_t, locates the target entries usingsfo_context_get_param(), and overwrites the raw binary data. Thesfo_patch_account()function usesmemcpyto replace the 8-byte Account ID, whilesfo_patch_user_id()casts the PARAMS buffer tosfo_param_params_t*and assigns the new User ID directly. -
Implementation Path: The high-level
patch_sfo()function insource/sfo.corchestrates the read-modify-write cycle, utilizing thesfo_patch_tstructure defined ininclude/sfo.hto transport the new identifiers from the UI (implemented insource/saves.c) to the binary patching layer.
Frequently Asked Questions
How does Apollo PS4 locate the User ID and Account ID inside param.sfo without scanning the entire file?
Apollo leverages the SFO index table defined in the file header. After parsing the header in source/sfo.c, it uses sfo_context_get_param() to perform a lookup by key name (e.g., "ACCOUNT_ID" or "PARAMS"). The index table entry provides the exact byte offset into the data table, allowing direct access without linear scanning.
What data types are used for the Account ID and User ID in the PS4 param.sfo format?
The Account ID is stored as a 64-bit unsigned integer (little-endian binary) under the ACCOUNT_ID key, occupying exactly 8 bytes (SFO_ACCOUNT_ID_SIZE). The User ID is a 32-bit unsigned integer located at offset 0x04 within the sfo_param_params_t structure stored under the PARAMS key.
Can Apollo modify other fields in param.sfo besides the User ID and Account ID?
Yes. The patch_sfo() function in source/sfo.c also calls sfo_patch_titleid(), which synchronizes the title_id_2 field with title_id_1 inside the PARAMS structure. Additionally, the sfo_patch_t structure supports patching the PSID and directory paths, though these require separate handling in the patching logic.
Is it safe to manually edit param.sfo with a hex editor instead of using Apollo?
While possible, manual hex editing is risky because the SFO format requires consistent offsets between the index table, key table, and data table. Apollo ensures integrity by parsing the index table in source/sfo.c and using sfo_write() to recalculate offsets and maintain the correct binary layout. Direct hex editing without updating index entries can corrupt the file and make the save unreadable on PS4.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →