Created
April 24, 2026 14:57
-
-
Save leegao/bb2a87cca36c39052e8ef3ca9fb890cb to your computer and use it in GitHub Desktop.
Unpacking gamblers-table.pck (reverse engineering)
This file contains hidden or 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
| import hashlib | |
| import io | |
| import os | |
| import struct | |
| from Crypto.Cipher import AES | |
| def unpack_and_decrypt_pck(pck_path, hex_key): | |
| key = bytes.fromhex(hex_key) | |
| # key = 'generate_aes_key(b"\0" * 16)' | |
| print("key", key) | |
| output_dir = "decrypted_assets" | |
| if not os.path.exists(output_dir): | |
| os.makedirs(output_dir) | |
| with open(pck_path, "rb") as f: | |
| f.seek(0x18) | |
| file_offset = struct.unpack("<I", f.read(4))[0] | |
| f.seek(0x20) | |
| dir_offset = struct.unpack("<Q", f.read(8))[0] | |
| total_size = os.path.getsize(pck_path) | |
| f.seek(dir_offset) | |
| entries = [] | |
| for _ in range(10000): | |
| if f.tell() + 4 >= total_size: | |
| break | |
| file_id = struct.unpack("<I", f.read(4))[0] | |
| path_len = struct.unpack("<I", f.read(4))[0] | |
| path = f.read(path_len).decode("utf-8", errors="ignore").strip("\x00") | |
| offset, size = struct.unpack("<QQ", f.read(16)) | |
| f.seek(16, 1) | |
| entries.append((path, offset, size)) | |
| entries.sort(key=lambda x: x[1]) | |
| for i, (path, offset, size) in enumerate(entries): | |
| print(path, offset + file_offset) | |
| f.seek(offset + file_offset) | |
| end = dir_offset if i + 1 >= len(entries) else entries[i + 1][1] | |
| raw_data = f.read(end - offset) | |
| decrypted_data = decrypt_file_payload(raw_data, key) | |
| clean_path = path.replace("res://", "").replace("/", os.sep) | |
| full_out_path = os.path.join(output_dir, clean_path) | |
| os.makedirs(os.path.dirname(full_out_path), exist_ok=True) | |
| print( | |
| "At: ", | |
| hex(offset + file_offset), | |
| "rs", | |
| size, | |
| "ds", | |
| len(decrypted_data), | |
| full_out_path, | |
| ) | |
| with open(full_out_path, "wb") as out_f: | |
| out_f.write(decrypted_data) | |
| print(f"[+] Extracted: {path}") | |
| # break | |
| def decrypt_file_payload(raw_payload, key): | |
| iv = raw_payload[0:16] | |
| print(iv) | |
| length = int.from_bytes(raw_payload[16:24], "little") | |
| print(length) | |
| ciphertext = raw_payload[ | |
| 24: # 24 + 16 + length | |
| ] | |
| cipher = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128) | |
| plaintext = cipher.decrypt(ciphertext) | |
| print(len(plaintext), len(ciphertext)) | |
| return plaintext[16 : length + 16] | |
| MY_PCK_FILE = "sample.pck" | |
| MY_SECRET_KEY = "10 8A C8 C5 A2 93 6A 41 0C 92 67 DE D4 24 0D BB 62 5E 1B 52 3C A9 9A 5E F7 B2 E7 41 33 B1 2F 1D".replace( | |
| " ", "" | |
| ) | |
| # Look for 0x64646464 followed by 0x47474747 in the disassembly | |
| unpack_and_decrypt_pck(MY_PCK_FILE, MY_SECRET_KEY) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Changes to the .pck:
There's a way to just port the algorithm to calculate the key over, but it's far easier to just dump it from memory.