"""patch_palette_v1.py [--revert] Apply (or revert) the Palette leak fix to a running acclient.exe. Mechanism: Overwrite Palette's primary vtable slot +0x2c (which is the DBObj::ReleaseSubObjects no-op stub) to point at Palette's existing PurgeResource function (slot +0x3c) instead. This causes DBOCache::FreeObject's ReleaseSubObjects call to actually free the ARGB buffer when the palette goes to the freelist. Target: EoR vtable 0x007caa08, slot +0x2c Replace value: 0x004154a0 (no-op stub, returns 1) With value: 0x0053ecf0 (PurgeResource — frees +0x40 buffer) Safety: - Idempotent: only patches if current value matches the no-op stub - Revert via --revert flag - Saves before-image to backup - The dtor at 0x0053f0d0 guards against NULL — no double-free risk """ import ctypes, ctypes.wintypes as wt, sys, struct VT_PRIMARY = 0x007caa08 SLOT_OFFSET = 0x2c TARGET_ADDR = VT_PRIMARY + SLOT_OFFSET # 0x007caa34 EXPECTED_VAL = 0x004154a0 # no-op ReleaseSubObjects stub PATCHED_VAL = 0x0053ecf0 # Palette::PurgeResource PROCESS_VM_READ = 0x0010 PROCESS_VM_WRITE = 0x0020 PROCESS_VM_OPERATION = 0x0008 PROCESS_QUERY_INFORMATION = 0x0400 PAGE_READWRITE = 0x04 k32 = ctypes.windll.kernel32 OpenProcess = k32.OpenProcess OpenProcess.argtypes = [wt.DWORD, wt.BOOL, wt.DWORD]; OpenProcess.restype = wt.HANDLE CloseHandle = k32.CloseHandle CloseHandle.argtypes = [wt.HANDLE]; CloseHandle.restype = wt.BOOL ReadProcessMemory = k32.ReadProcessMemory ReadProcessMemory.argtypes = [wt.HANDLE, wt.LPCVOID, wt.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)] ReadProcessMemory.restype = wt.BOOL WriteProcessMemory = k32.WriteProcessMemory WriteProcessMemory.argtypes = [wt.HANDLE, wt.LPVOID, wt.LPCVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)] WriteProcessMemory.restype = wt.BOOL VirtualProtectEx = k32.VirtualProtectEx VirtualProtectEx.argtypes = [wt.HANDLE, wt.LPVOID, ctypes.c_size_t, wt.DWORD, ctypes.POINTER(wt.DWORD)] VirtualProtectEx.restype = wt.BOOL def main(): if len(sys.argv) < 2: print("usage: patch_palette_v1.py [--revert]"); sys.exit(1) pid = int(sys.argv[1]) revert = "--revert" in sys.argv h = OpenProcess(PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_VM_OPERATION|PROCESS_QUERY_INFORMATION, False, pid) if not h: print(f"OpenProcess({pid}) failed: err={ctypes.get_last_error()}"); sys.exit(2) # Read current value cur = ctypes.c_uint(0); sz = ctypes.c_size_t(0) if not ReadProcessMemory(h, TARGET_ADDR, ctypes.byref(cur), 4, ctypes.byref(sz)): print(f"read failed: err={ctypes.get_last_error()}"); CloseHandle(h); sys.exit(3) print(f"PID {pid} slot @ 0x{TARGET_ADDR:08x} current: 0x{cur.value:08x}") target = EXPECTED_VAL if revert else PATCHED_VAL expect_before = PATCHED_VAL if revert else EXPECTED_VAL if cur.value == target: print(f" already {'reverted' if revert else 'patched'} — nothing to do") CloseHandle(h); return if cur.value != expect_before: print(f" UNEXPECTED current value (expected 0x{expect_before:08x}) — aborting to avoid corruption") CloseHandle(h); sys.exit(4) # Make page writable old_prot = wt.DWORD(0) if not VirtualProtectEx(h, TARGET_ADDR, 4, PAGE_READWRITE, ctypes.byref(old_prot)): print(f"VirtualProtectEx failed: err={ctypes.get_last_error()}"); CloseHandle(h); sys.exit(5) print(f" old protect: 0x{old_prot.value:x}") # Write new value new = ctypes.c_uint(target) if not WriteProcessMemory(h, TARGET_ADDR, ctypes.byref(new), 4, ctypes.byref(sz)): print(f"write failed: err={ctypes.get_last_error()}"); CloseHandle(h); sys.exit(6) # Restore original protection restored = wt.DWORD(0) VirtualProtectEx(h, TARGET_ADDR, 4, old_prot.value, ctypes.byref(restored)) # Verify ReadProcessMemory(h, TARGET_ADDR, ctypes.byref(cur), 4, ctypes.byref(sz)) print(f" new value: 0x{cur.value:08x} ({'reverted' if revert else 'patched'} successfully)") CloseHandle(h) if __name__ == "__main__": main()