bruvzg / gdsdecomp

Godot reverse engineering tools
MIT License
1.47k stars 147 forks source link

Recovering save files #91

Closed rbhorse closed 1 year ago

rbhorse commented 1 year ago

Hello; I have recovered the cfb key and it appears to be working well with the pck. However, my aim is to decrypt the save/achievements file that the game uses (custom file extension: .dv and .dvb). It seems to use the same godot encryption as it has the GDEC header. All methods I've tried so far have been futile and have lead to processing errors from gdsdecomp. What else can I try?

image

rbhorse commented 1 year ago

I ended up writing a decryptor in python. But still kind of interested.

nikitalita commented 1 year ago

I ended up writing a decryptor in python. But still kind of interested.

wait, what? can you post that somewhere?

rbhorse commented 1 year ago

@nikitalita It's for 3.5.1. Apparently the engine used ECB but later switched to CFB

import sys

#pip install pycryptodome
from Crypto.Cipher import AES

save_str = b'password string here'

def encrypt(key, filepath):
    print('---enc---')
    with open(filepath, 'rb+') as f:
        pt_dat = f.read()

    enc = b''

    #write magic
    magic = 'GDEC'.encode()[:4]
    enc += magic
    print(f"magic: {magic.hex()}")

    #write mode
    mode = int(1).to_bytes(4, 'little')
    enc += mode
    print(f"mode: {mode.hex()}")

    #md5 digest of plaintext
    md5d = hashlib.md5(pt_dat).digest()
    assert len(md5d) == 16
    enc += md5d
    print(f"md5d: {md5d.hex()}")

    #pt data size
    ptsz = int(len(pt_dat)).to_bytes(8, 'little')
    enc += ptsz
    print(f"ptsz: {ptsz.hex()} ({int.from_bytes(ptsz, 'little')})")

    #ciphertext
    cipher = AES.new(key=key, 
        mode=AES.MODE_ECB
    )
    ds = len(pt_dat)
    if ds % 16 != 0:
        pt_dat += b'\0'* (16 - (ds % 16))
    ct_dat = cipher.encrypt(pt_dat)
    enc += ct_dat
    print(f"sz ct_dat: {len(ct_dat)}")

    return enc

def decrypt(key, filepath):
    print('---dec---')
    with open(filepath, 'rb+') as f:
        magic = f.read(4)
        assert magic.decode() == 'GDEC'
        print(f"magic: {magic.hex()}")

        mode = f.read(4)
        print(f"mode: {mode.hex()}")

        #md5 digest of plaintext
        md5d = f.read(16)
        print(f"md5d: {md5d.hex()}")

        lb = f.read(8)
        length = int.from_bytes(lb, 'little')
        print(f"pt sz (exp): {lb.hex()} ({length})")

        ds = length
        if ds % 16 != 0:
            ds += 16 - (ds % 16)
        print(f"ds: {ds}")

        ct_dat = f.read(ds)
        print(f"ct sz: {len(ct_dat)}")

        cipher = AES.new(key=key, 
            mode=AES.MODE_ECB
        )

        pt_dat = cipher.decrypt(ct_dat)[:length]
        print(f"pt sz (act): {len(pt_dat)}")

        #make sure pt hash matches expected from header
        assert md5d == hashlib.md5(pt_dat).digest()

    return pt_dat

def main():

    if len(sys.argv) != 3:
        print('invalid options')
        return 

    key = hashlib.md5(save_str).hexdigest().lower().encode()
    print(f'key: {key}')

    fp = sys.argv[1]
    op = sys.argv[2]

    if(op == 'd'):
        with open(fp+".dec", 'wb+') as f:
            f.write(decrypt(key, fp))

    elif (op == 'e'):
        with open(fp+".enc", 'wb+') as f:
            f.write(encrypt(key, fp))

    return

if __name__ == '__main__':
    main()
nikitalita commented 1 year ago

Thank you for this. It seems that it's using the same encryption scheme as Godot 3.x scripts, which we have support for.

I am probably not going to add a feature to decrypt non-script files like savegames, however, as this can mess with Steam achievements. That is, you'd be messing with an external system, not just reverse engineering what's on yours. I'd rather not encourage that.