"""patch_freeobject_v2.py [--revert] v2 patch: force DBOCache::FreeObject to always take the DELETE path (never add to freelist). This causes every Release-to-refcount-1 to invoke the proper destructor, which calls operator delete[] on the Palette ARGB buffer (and similar for other DBObj-derived classes). Target byte: EoR 0x004169bf Original: 0x74 (jz delete_path) Patched: 0xeb (jmp delete_path) — unconditional Effect: - DBOCache::FreeObject(this, obj) now always: obj->ReleaseSubObjects(); obj->in_use = 0; this->vt[0x44](obj); - Skips the "if freelist enabled, push to freelist" branch - Affects ALL DBObj-derived classes (not just Palette) Risk: - Performance: cache misses now require full alloc + load instead of reuse from freelist. For frequently-used classes, may add latency. - Correctness: should be safe — delete path uses proper destructor that already handles all field cleanup. """ import ctypes, ctypes.wintypes as wt, sys TARGET_ADDR = 0x004169bf EXPECTED_VAL = 0x74 # jz PATCHED_VAL = 0xeb # jmp PROCESS_VM_READ = 0x0010 PROCESS_VM_WRITE = 0x0020 PROCESS_VM_OPERATION = 0x0008 PROCESS_QUERY_INFORMATION = 0x0400 PAGE_EXECUTE_READWRITE = 0x40 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_freeobject_v2.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) cur = ctypes.c_ubyte(0); sz = ctypes.c_size_t(0) if not ReadProcessMemory(h, TARGET_ADDR, ctypes.byref(cur), 1, ctypes.byref(sz)): print(f"read failed: err={ctypes.get_last_error()}"); CloseHandle(h); sys.exit(3) print(f"PID {pid} byte @ 0x{TARGET_ADDR:08x} current: 0x{cur.value:02x}") 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'}") CloseHandle(h); return if cur.value != expect_before: print(f" UNEXPECTED current value (wanted 0x{expect_before:02x}) — aborting") CloseHandle(h); sys.exit(4) old_prot = wt.DWORD(0) if not VirtualProtectEx(h, TARGET_ADDR, 1, PAGE_EXECUTE_READWRITE, ctypes.byref(old_prot)): print(f"VirtualProtectEx failed: err={ctypes.get_last_error()}"); CloseHandle(h); sys.exit(5) new = ctypes.c_ubyte(target) if not WriteProcessMemory(h, TARGET_ADDR, ctypes.byref(new), 1, ctypes.byref(sz)): print(f"write failed: err={ctypes.get_last_error()}"); CloseHandle(h); sys.exit(6) restored = wt.DWORD(0) VirtualProtectEx(h, TARGET_ADDR, 1, old_prot.value, ctypes.byref(restored)) ReadProcessMemory(h, TARGET_ADDR, ctypes.byref(cur), 1, ctypes.byref(sz)) print(f" new value: 0x{cur.value:02x} ({'reverted' if revert else 'patched'})") CloseHandle(h) if __name__ == "__main__": main()