Reverse Engineering Call of Duty: Ghosts PS3 FastFiles
Introduction
After years of reverse engineering Call of Duty FastFiles from older titles like Modern Warfare 2 and Black Ops 1, I decided to tackle something more challenging Call of Duty: Ghosts on PlayStation 3. Unlike the earlier games where the encryption keys were eventually discovered by simply dumping from memory, Ghosts presented a unique challenge. The PS3 version uses a signed FastFile format with what appeared to be an entirely different protection scheme.
What followed was a deep dive into hex editors, PS3 emulators, memory dumps, RSA signatures, and ultimately discovering things that challenged my initial assumptions. This post documents my journey from start to finish, the tools I used, the discoveries I made, and why I eventually decided to shelve my efforts on this project.
The information provided in this post is for educational and research purposes only, specifically focusing on reverse engineering game files and understanding their internal structures. No malicious intent is implied or encouraged. All examples and technical details are shared under the principles of fair use, to foster learning and advance the understanding of legacy game formats.
Please be aware that reverse engineering and modifying game files may violate the terms of service, end-user license agreements, or other legal agreements. If you choose to explore or replicate any of these topics, you do so at your own risk.
Background: What Are FastFiles?
For those unfamiliar, FastFiles (or FFs) are the container format that Call of Duty games use to store virtually all game assets: models, textures, sounds, scripts (GSC files), and more. The format evolved significantly over the years:
- CoD4 - MW2: Relatively simple zlib compression, unsigned on most platforms
- Black Ops 1: Introduced Salsa20 encryption on console versions
- Ghosts: RSA-2048 signatures, new compression, and unknown encryption on PS3
I had previously written tooling to handle the older formats (you can read about that in my FastFile Reverse Engineering post), but Ghosts was a different beast entirely.
Initial Analysis: The PS3 FastFile Header
I started with a PS3 FastFile called ghosts_patch_common_mp.ff (525,354 bytes). Opening it
in a hex editor revealed a familiar magic signature: IWff0100. But immediately after came
something new: IWffS100 - the "S" indicating a signed FastFile.
Here's the header structure I mapped out:
| Offset | Size | Value | Description |
|---|---|---|---|
| 0x000 | 8 | IWff0100 | Magic signature (IW = Infinity Ward) |
| 0x008 | 4 | 0x0000022E | Version (558) |
| 0x00C | 4 | 0x01000404 | Flags |
| 0x010 | 12 | 00... | Reserved/Padding |
| 0x01C | 4 | 0x0008042A | File size (Big Endian) |
| 0x020 | 4 | 0x0008042A | File size (duplicate) |
The signed header that followed was even more interesting:
| Offset | Size | Description |
|---|---|---|
| 0x024 | 8 | IWffS100 - Signed fastfile marker |
| 0x030 | 20 | Master SHA-1 hash |
| 0x050 | 256 | RSA-2048 signature |
| 0x150 | 32 | Zone name (null-padded) |
| 0x170 | 7808 | Block hash table (244 entries × 32 bytes) |
The block hash table was particularly interesting: 244 entries, each containing a SHA-1 hash.
These hashes are computed over the decrypted data blocks for integrity verification.
The master hash at 0x030 is a hash of all the block hashes, and the RSA signature
signs this master hash.
The Encrypted Data: First Surprises
The actual compressed/encrypted data begins at offset 0x1FF4. My first instinct
was to assume Salsa20 encryption (Given activisions past use of it), so I started looking for keys.
But then I noticed something strange when analyzing the byte distribution:
First 32 bytes of "encrypted" data:
70 2f 2f 15 1b 69 03 7c 09 50 72 19 64 54 22 65
44 68 50 1f 3c 1d 14 63 27 25 6b 3f 6e 34 1b 71 I wrote a quick script to analyze the byte distribution across the first 64KB of encrypted data. The results were interesting:
Byte distribution analysis (first 64KB):
Bytes 0x00-0x7F: 57,393 occurrences (100%)
Bytes 0x80-0xFF: 0 occurrences (0%)
First byte with high bit set: offset 0x10025 Every single byte in the first 56KB had a value between 0x00 and 0x7F... No byte had its high bit set. This is absolutely NOT what Salsa20 encrypted data looks like. Salsa20 is a stream cipher that XORs plaintext with a pseudorandom keystream, producing output that should be uniformly distributed across all 256 possible byte values. This was clearly some kind of 7-bit encoding scheme.
Decrypting the EBOOT: Looking for Clues
To understand how the game processes FastFiles, I needed to look at the game's executable. PS3 games ship with encrypted EBOOT.BIN files that need to be decrypted before analysis. I extracted the EBOOT from an install of the game on my PlayStation and used the OSCE tool to obtain a decrypted version of the executable.
Loading the decrypted ELF into IDA Pro, I searched for relevant strings and found some interesting function names:
Key functions found in EBOOT:
DB_AuthLoad_Inflate
DB_AuthLoad_InflateInit
DB_AuthLoad_InflateEnd
DBX_AuthLoad_ProcessDataBlock
DBX_AuthLoad_ProcessHeader
DBX_AuthLoad_ProcessMasterBlock
DBX_AuthLoad_ValidateSignature
I also discovered references to libtomcrypt 1.17, a well known cryptographic library.
The game uses AES via rijndael_ecb_encrypt and rijndael_ecb_decrypt,
along with SHA-1 for integrity verification through a DemonWare crypto wrapper class
called bdCypherAES.
Finding the RSA Public Key
One of my more successful finds was locating the RSA public key used to verify FastFile
signatures. At file offset 0x693760 in the decrypted EBOOT, I found an
ASN.1 DER-encoded RSA public key:
30 82 01 0A 02 82 01 01 00 DC 05 AB 26 D5 3E BD
DB CA 7D 62 C0 0F 71 5C 92 23 0C 11 67 1A 7E 87
D9 4C 6B BD BE 36 8C CD E8 5B 51 F5 21 58 19 94
23 87 44 6D 5E 1E E4 E1 CD 04 18 F0 31 45 59 A6
46 74 19 D6 56 56 05 27 16 39 E6 30 4E CF 59 86
C9 42 27 7C F1 C0 B8 EE 97 DB 39 75 79 F6 EE 0D
A9 7D 7A E3 E9 27 00 5E EF 38 FD 30 B5 00 F5 43
6A A7 F1 D1 CB 9A 71 EC D9 A5 8B 32 70 33 BC C7
64 58 F1 D5 10 24 24 4B 5D 23 7C 96 49 31 33 CE
64 2C 16 35 DD E5 C7 80 05 7B 16 62 71 06 7D A3
F4 78 37 2E 6D CD 6D 55 57 E1 14 A9 C7 AD 4A 13
C5 A0 0E 26 72 AE B5 E4 DF F1 59 48 35 DB A8 BB
22 4E CC A4 06 62 A9 83 25 11 DA 40 3B F6 5C 38
7D D4 41 21 41 8D 48 AB AD AF 55 67 68 61 27 5C
F2 55 7B 6E 92 C3 2E 8C 44 60 A2 6E DE 22 0B 8B
89 DD 1E 3A D0 91 60 92 0B 25 1E 68 67 0C B4 9F
EA AD 0B 8A 1F B0 0B 0E 67 02 03 01 00 01 <- Exponent: 65537 This is a 2048-bit RSA key with the standard exponent of 65537. However, I quickly realized this key is only for signature verification, not encryption. The FastFile data isn't RSA-encrypted (that would be incredibly slow for multi megabyte files). RSA is only used to sign the master hash, ensuring the file hasn't been tampered with.
A Lucky Break: The DEV Build
While searching for more information, I came across something invaluable... a development build of Call of Duty: Ghosts from May 9, 2013. This build included PDB debug symbols and, crucially, unsigned debug FastFiles (.ffd files).
Opening one of these FFD files (dev_mp_snow.ffd), I immediately noticed the magic
was IWffu100 - the "u" indicating unsigned. Even better, the data inside
was simply zlib compressed with no encryption.
// Successfully decompressed GSC from dev_mp_snow.ffd
#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\gametypes\_hostmigration;
main()
{
maps\mp\mp_snow_precache::main();
maps\createart\mp_snow_art::main();
maps\mp\mp_snow_fx::main();
...
} This confirmed that at its core, Ghosts FastFiles contain the same kind of data as previous games. The protection is purely a layer on top.
Xbox 360 DEV Build: LZX Compression Discovery
The DEV build also contained Xbox 360 FastFiles. Analyzing common_mp.ff from
the Xbox version, I found something crucial in the accompanying MAP file:
Symbols found in 2-iw6mp.map:
DB_AuthLoad_InflateLZX
XCOMPRESS::LzxCreate
XCOMPRESS::LzxDecompress
XCOMPRESS::LzxDestroy The Xbox 360 version uses Microsoft's LZX compression algorithm - the same algorithm used in Xbox CAB files and XNA Game Studio. This made me wonder... could the PS3 version also be using LZX?
Looking at the LZX specification, the first 3 bits of a compressed block indicate the block type:
001= Verbatim block010= Aligned offset block011= Uncompressed block
The first byte of my PS3 encrypted data was 0x70, which in binary is 01110000.
Reading the first 3 bits MSB-first gives 011, an uncompressed block in LZX!
This seemed promising.
Testing the LZX Theory
Excited by this discovery, I added the lzxd crate to my Rust project and wrote a
test harness to try decompressing the PS3 data with various LZX window sizes:
=== Testing LZX Decompression ===
Trying window size: 32KB
Failed: DecompressError(InvalidPathLengths)
Trying window size: 64KB
Failed: DecompressError(InvalidPathLengths)
Trying window size: 128KB
Failed: DecompressError(InvalidPathLengths)
... (all window sizes failed)
Every attempt failed with "InvalidPathLengths" - the data wasn't valid LZX. The 7-bit encoding
was interfering. The data might be LZX underneath, but there's clearly some transformation
or encryption happening first. Interestingly, Splinter Cell: Conviction uses a similar approach...
LZX compression via Xbox 360's XCOMPRESS library combined with simple single byte XOR encryption. The
7-bit pattern in Ghosts could be explained by an XOR key that consistently clears the high bit, something
like data ^ 0x80 or a similar transformation. Without runtime analysis to confirm the exact
key, this remains a working theory.
Plan B: Memory Dumping with RPCS3
Since I couldn't crack the encoding directly, I decided to let the game do the work for me. Using RPCS3's debugging capabilities, I set up the game and dumped memory while it was running. The theory: somewhere in RAM, the decompressed zone data must exist after the game loads it.
After some searching, I found it at offset 0x981AD4 in the memory dump - 65,792 bytes
of beautiful, decompressed zone data!
Zone entries found in memory dump:
0x0060: mp_prisonbreak_path
0x0100: patch_common_mp
0x01A0: eng_patch_common_mp
0x0240: eng_common_mp
0x02E0: common_mp
0x0380: eng_mp_prisonbreak
0x0420: mp_prisonbreak
0x04C0: mp_viewhands_devgru_tr
0x0560: mp_head_k_helmet_t_tr
... The zone structure uses 160-byte (0xA0) fixed size entries, with the asset name stored at offset 0x60 within each entry. This matched the format I was expecting from my analysis of previous CoD games.
Understanding the Zone Structure
With the decompressed data in hand, I could finally map out the zone header:
pub struct GhostsZoneEntry {
/// Entry flags (8 bytes)
pub flags: u64,
/// Unknown counts (4 bytes each)
pub count1: u32,
pub count2: u32,
/// Size information
pub size1: u32,
pub size2: u32,
pub size3: u32,
/// Asset name at offset 0x60
pub name: String,
} The zone header indicated an uncompressed size of approximately 872 MB(a massive amount of data packed into that 525KB FastFile!)
DEV vs. PS3 Retail: The Full Picture
After all my analysis, here's how the two formats compare:
| Feature | DEV (Xbox 360) | PS3 Retail |
|---|---|---|
| Magic | IWff0100 | IWff0100 + IWffS100 |
| Compression | LZX (Xbox native) | Unknown encoding |
| Encryption | None | Yes (unknown method) |
| Block count | 968 | 244 (hash entries) |
| Signing | None | RSA-2048 |
What I Learned (And Didn't Crack)
Despite all this work, I never fully solved the PS3 encoding scheme. Here's what I know for certain:
- The PS3 format uses RSA-2048 signatures for integrity (found the public key)
- The data undergoes some 7-bit encoding transformation (only bytes 0x00-0x7F in first 56KB)
- The underlying compression is likely LZX or zlib, but encrypted/encoded first
- The game uses libtomcrypt and DemonWare's bdCypherAES for crypto operations
- Zone data follows the same general structure as other CoD games
What I didn't figure out:
- The exact encoding/encryption algorithm transforming the data
- What or how exactly the game is parsing the fastfile data
- The relationship between the 7-bit encoding and the actual decryption process
Tools I Built Along the Way
Even though I didn't fully crack the format, I built some useful Rust tooling:
// ghosts_zone.rs - Zone parser for decompressed data
pub struct GhostsZone {
pub entries: Vec<GhostsZoneEntry>,
pub header: GhostsZoneHeader,
}
impl GhostsZone {
pub fn parse(data: &[u8]) -> io::Result<Self> {
// Parse 160-byte entries, extract names at offset 0x60
...
}
}
// lzx_helper.rs - LZX decompression utilities
pub struct GhostsLzxDecompressor {
window_size: WindowSize,
}
impl GhostsLzxDecompressor {
pub fn decompress_cod_format(&self, data: &[u8]) -> io::Result<Vec<u8>> {
// Try various CoD block formats
...
}
} The zone parser works perfectly on memory dumped data, so if anyone ever cracks the encryption, the rest of the pipeline is ready.
Conclusion: Knowing When to Stop
At this point, I've decided to shelve this project. The time investment required to fully reverse engineer the PS3 encoding scheme doesn't seem worth it for what would ultimately be a niche tool for a decade old game on a dead platform. The PS3 modding scene has largely moved on, and the people who really need to modify Ghosts FastFiles can work with the PC version or use memory dumping techniques like I did.
That said, I had a lot of fun with this project. There's something deeply satisfying about digging through hex dumps, finding patterns, discovering that first readable string in a sea of encoded bytes. The journey taught and refreshed me a lot about:
- PS3 architecture and RPCS3 debugging
- RSA signatures and how games use them for integrity
- Microsoft's LZX compression algorithm
- The evolution of CoD's protection schemes across generations
Maybe someday I'll come back to this, or maybe someone else will pick up where I left off.
If you're interested in continuing this research, please contact me about aquiring my work.
All my notes are in the GHOSTS_FF_FORMAT.md
file in my repository, and the Rust code is functional for everything except the actual decryption.
Sometimes the best part of reverse engineering isn't the destination... it's the rabbit holes you fall down along the way.