-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PS4 rdr2 + checksums
- Loading branch information
Showing
6 changed files
with
299 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |