Skip to content

Commit

Permalink
RDR2 decrypter
Browse files Browse the repository at this point in the history
PS4 rdr2 + checksums
  • Loading branch information
bucanero committed Sep 5, 2023
1 parent 38716d5 commit 0869485
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 0 deletions.
10 changes: 10 additions & 0 deletions ps4-rdr2-decrypter/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# RDR2 PS4 Save Decrypter
# (c)2023 Damian Parrino

all: decrypter

decrypter:
gcc -D _GNU_SOURCE -o rdr2-ps4save-decrypter main.c

clean:
-rm -f rdr2-ps4save-decrypter
19 changes: 19 additions & 0 deletions ps4-rdr2-decrypter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Red Dead Redemption 2 PS4 Save Decrypter

A tool to decrypt RDR2 (Red Dead Redemption 2) PS4 save-games

```
USAGE: ./rdr2-ps4save-decrypter [option] filename
OPTIONS Explanation:
-d Decrypt File
-e Encrypt File
```

### Hashes

**Note:** this tool also updates the `CHKS` custom integrity hashes.

### Credits

The custom checksums is based on [rdr2_enc_dec](https://github.com/Zhaxxy/rdr2_enc_dec) by Zhaxxy
157 changes: 157 additions & 0 deletions ps4-rdr2-decrypter/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
*
* RDR2 PS4 Save Decrypter - (c) 2023 by Bucanero - www.bucanero.com.ar
*
*/

#include "../common/iofile.c"
#include "../common/aes.c"

// same as GTA5 PS3 Key
const u8 RDR2_PS4_KEY[32] = {
0x16, 0x85, 0xFF, 0xA3, 0x8D, 0x01, 0x0F, 0x0D, 0xFE, 0x66, 0x1C, 0xF9, 0xB5, 0x57, 0x2C, 0x50,
0x0D, 0x80, 0x26, 0x48, 0xDB, 0x37, 0xB9, 0xED, 0x0F, 0x48, 0xC5, 0x73, 0x42, 0xC0, 0x22, 0xF5
};

int search_data(const u8* data, size_t size, int start, const char* search, int len)
{
for (size_t i = start; i <= (size-len); i++)
if (memcmp(data + i, search, len) == 0)
return i;

return -1;
}

// https://github.com/Zhaxxy/rdr2_enc_dec/blob/main/rdr2_enc_dec.py#L10
uint32_t rockstar_chks(const char* data, int len)
{
uint32_t checksum = 0x3FAC7125;

while (len--)
{
checksum = ((checksum + (signed char) *data++) * 0x401) & 0xFFFFFFFF;
checksum = (checksum >> 6 ^ checksum) & 0xFFFFFFFF;
}
checksum = (checksum*9) & 0xFFFFFFFF;

return (((checksum >> 11 ^ checksum) * 0x8001) & 0xFFFFFFFF);
}

void decrypt_data(u8* data, u32 size)
{
struct AES_ctx ctx;
AES_init_ctx(&ctx, RDR2_PS4_KEY);

printf("[*] Total Decrypted Size Is 0x%X (%d bytes)\n", size, size);

for (int i = 0; i < size; i+= AES_BLOCKLEN)
{
AES_ECB_decrypt(&ctx, data + i);
}

printf("[*] Decrypted File Successfully!\n\n");
return;
}

void encrypt_data(u8* data, u32 size)
{
struct AES_ctx ctx;
AES_init_ctx(&ctx, RDR2_PS4_KEY);

printf("[*] Total Encrypted Size Is 0x%X (%d bytes)\n", size, size);

for (int i = 0; i < size; i+= AES_BLOCKLEN)
{
AES_ECB_encrypt(&ctx, data + i);
}

printf("[*] Encrypted File Successfully!\n\n");
return;
}

void print_usage(const char* argv0)
{
printf("USAGE: %s [option] filename\n\n", argv0);
printf("OPTIONS Explanation:\n");
printf(" -d Decrypt File\n");
printf(" -e Encrypt File\n\n");
return;
}

int main(int argc, char **argv)
{
size_t len;
u8* data;
char *opt, *bak;

printf("\nRDR2 PS4 Save Decrypter 0.1.0 - (c) 2023 by Bucanero\n\n");

if (--argc < 2)
{
print_usage(argv[0]);
return -1;
}

opt = argv[1];
if (*opt++ != '-' || (*opt != 'd' && *opt != 'e'))
{
print_usage(argv[0]);
return -1;
}

if (read_buffer(argv[2], &data, &len) != 0)
{
printf("[*] Could Not Access The File (%s)\n", argv[2]);
return -1;
}
// Save a file backup
asprintf(&bak, "%s.bak", argv[2]);
write_buffer(bak, data, len);

if (*opt == 'd')
decrypt_data(data + 0x120, len - 0x120);
else
{
uint32_t chks, chks_len;
int chks_off = search_data(data, len, 0, "CHKS", 5);

if (chks_off < 0)
{
printf("[!] CHKS Header Not Found!\n>>> Aborting... (Not a decrypted RDR2 save?)\n\n");
return -1;
}

while (chks_off > 0)
{
chks = ES32(*(uint32_t*)(data + chks_off + 4));
chks_len = ES32(*(uint32_t*)(data + chks_off + 8));

if (chks != 0x14)
printf(" ! CHKS Header Mismatch!\n > Expected: %08X\n > Detected: %08X\n\n", 0x14, chks);

printf(" - CHKS Offset : 0x%X\n", chks_off);
printf(" - CHKS Size : 0x%X (%d bytes)\n", chks_len, chks_len);
printf(" - Old Checksum: %08X\n", ES32(*(uint32_t*)(data + chks_off + 0xC)));

memset(data + chks_off + 8, 0, 8);
chks = rockstar_chks((char*) data + (chks_off - chks_len + 0x14), chks_len);
printf(" + New Checksum: %08X\n\n", chks);

chks = ES32(chks);
chks_len = ES32(chks_len);
memcpy(data + chks_off + 0xC, &chks, sizeof(uint32_t));
memcpy(data + chks_off + 0x8, &chks_len, sizeof(uint32_t));

chks_off = search_data(data, len, chks_off+1, "CHKS", 5);
}

encrypt_data(data + 0x120, len - 0x120);
}

write_buffer(argv[2], data, len);

free(bak);
free(data);

return 0;
}
Binary file added ps4-rdr2-decrypter/samples/SRDR30000.dec
Binary file not shown.
Binary file added ps4-rdr2-decrypter/samples/SRDR30000.enc
Binary file not shown.
113 changes: 113 additions & 0 deletions ps4-rdr2-decrypter/samples/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from io import BytesIO
import struct
import argparse
import re

from Crypto.Cipher import AES

GTA5_AND_REDDEAD_KEY = AES.new(b'\x16\x85\xff\xa3\x8d\x01\x0f\r\xfef\x1c\xf9\xb5W,P\r\x80&H\xdb7\xb9\xed\x0fH\xc5sB\xc0"\xf5', AES.MODE_ECB)

def rdr2_checksum(data: bytes, /) -> bytes:
checksum = 0x3fac7125

for char in data:
char = (char + 128) % 256 - 128 # casting to signed char
checksum = ((char + checksum) * 0x401) & 0xFFFFFFFF
checksum = (checksum >> 6 ^ checksum) & 0xFFFFFFFF
checksum = (checksum*9) & 0xFFFFFFFF

return struct.pack('>I',((checksum >> 11 ^ checksum) * 0x8001) & 0xFFFFFFFF)

def _crypt_rdr2_ps4_save(rdr2_save: BytesIO,/,*,enc_data_offset: int,do_enc: bool = True) -> bytes:
previous_spot = rdr2_save.tell()
rdr2_save.seek(0)
rdr2_save.seek(enc_data_offset)

crypted_data = rdr2_save.read()

rdr2_save.seek(0)
if enc_data_offset == 0x120 and do_enc:
for chunk in [m.start() for m in re.finditer(b'CHKS\x00', crypted_data)]: # calculate checksums for each chunk
rdr2_save.seek(enc_data_offset + chunk + 4,0) # 4 bytes for the magic CHKS
header_size = struct.unpack('>I',rdr2_save.read(4))[0]
data_length = struct.unpack('>I',rdr2_save.read(4))[0]
rdr2_save.seek(header_size - 4 - 4 - 4,1) # 4 for the header size num, 4 for the data length num AND 4 for the checksum

rdr2_save.seek(-data_length,1)
data_to_be_hashed = bytearray(rdr2_save.read(data_length))

chks_offset = len(data_to_be_hashed)-header_size + 4 + 4
data_to_be_hashed[chks_offset:chks_offset + 8] = b'\x00' * 8 # remove the length and hash
new_hash = rdr2_checksum(data_to_be_hashed)

rdr2_save.seek(enc_data_offset + chunk + 12,0)
rdr2_save.write(new_hash)
rdr2_save.seek(0)
rdr2_save.seek(enc_data_offset)
crypted_data = rdr2_save.read()
rdr2_save.seek(0)
if do_enc:
dec_data = GTA5_AND_REDDEAD_KEY.encrypt(crypted_data)
else:
dec_data = GTA5_AND_REDDEAD_KEY.decrypt(crypted_data)

result = rdr2_save.read(enc_data_offset) + dec_data
rdr2_save.seek(previous_spot)

return result


def encrypt_rdr2_ps4_save(rdr2_save: BytesIO,/,*,enc_data_offset: int) -> bytes:
return _crypt_rdr2_ps4_save(rdr2_save,enc_data_offset=enc_data_offset,do_enc=True)


def decrypt_rdr2_ps4_save(rdr2_save: BytesIO,/,*,enc_data_offset: int) -> bytes:
return _crypt_rdr2_ps4_save(rdr2_save,enc_data_offset=enc_data_offset,do_enc=False)


def auto_encrypt_decrypt(rdr2_save: BytesIO,/) -> bytes:
previous_spot = rdr2_save.tell()
rdr2_save.seek(0)
rdr2_save.seek(0x114)
if rdr2_save.read(4) == b'\x00\x00\x00\x00':
ENC_DATA_OFFSET = 0x120
DEC_SAVE_HEADER = b'RSAV'
else:
ENC_DATA_OFFSET = 0x114
DEC_SAVE_HEADER = b'PSIN'

rdr2_save.seek(0)
rdr2_save.seek(ENC_DATA_OFFSET)

header = rdr2_save.read(4)
rdr2_save.seek(previous_spot)

if header == DEC_SAVE_HEADER:
return encrypt_rdr2_ps4_save(rdr2_save,enc_data_offset=ENC_DATA_OFFSET)
else:
return decrypt_rdr2_ps4_save(rdr2_save,enc_data_offset=ENC_DATA_OFFSET)

def main():

# from zlib import crc32
# from pathlib import Path

# print(hex(crc32(Path('Untitled1.bin').read_bytes())))


# return
parser = argparse.ArgumentParser(description='Simple tool to encrypt and decrypt ps4 saves for Red Dead Redemption 2 and GTAV, detects if its encrypted or decrypted automatically')

parser.add_argument("input_save", help="Path to the save file")
parser.add_argument("output_save", help="Path to write the output save")

args = parser.parse_args()

with open(args.input_save,'rb') as f:
old_data = BytesIO(f.read())
with open(args.output_save,'wb') as f:
f.write(auto_encrypt_decrypt(old_data))


if __name__ == '__main__':
main()

0 comments on commit 0869485

Please sign in to comment.