"""patch_palette_v3.py [--revert] v3 patch: NOP the extra refcount increment in Palette::makeModifiedPalette (EoR FUN_0053efe0). This stops the +1 over-increment that leaves every new "modified palette" at refcount=2 instead of refcount=1. Background: FUN_0053efe0 = Palette::makeModifiedPalette(): new(0x48); Palette::Palette(this, 0x800); this->refcount++; return; The "this->refcount++" makes the returned palette have refcount=2. After caller's single Release, refcount=1, and the palette becomes unreachable (m_pMaintainer is NULL — no cache reference) but alive. 56,664 leaked palettes in dump_9156 are all this shape: rc=1, m_pMaintainer=0, m_numLinks=0. Target bytes: 0x0053effe (3 bytes inside FUN_0053efe0) Original: ff 40 24 ; inc dword [eax+0x24] ; refcount++ Patched: 90 90 90 ; nop nop nop The 2013 build has the same +1 pattern at 0x0053e29e — leak likely exists there too, just less noticeable. Risk: some caller may assume refcount=2 and release twice; after this patch, the second release would crash on already-freed memory. Test on one client and watch closely. """ import ctypes, ctypes.wintypes as wt, sys TARGET_ADDR = 0x0053effe ORIGINAL_BYTES = bytes([0xff, 0x40, 0x24]) PATCHED_BYTES = bytes([0x90, 0x90, 0x90]) 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(): 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 * 3)(); sz = ctypes.c_size_t(0) if not ReadProcessMemory(h, TARGET_ADDR, cur, 3, ctypes.byref(sz)): print(f"read failed: err={ctypes.get_last_error()}"); CloseHandle(h); sys.exit(3) cur_b = bytes(cur) print(f"PID {pid} bytes @ 0x{TARGET_ADDR:08x}: {' '.join(f'{b:02x}' for b in cur_b)}") target = ORIGINAL_BYTES if revert else PATCHED_BYTES expect_before = PATCHED_BYTES if revert else ORIGINAL_BYTES if cur_b == target: print(f" already {'reverted' if revert else 'patched'}"); CloseHandle(h); return if cur_b != expect_before: print(f" UNEXPECTED current bytes (wanted {' '.join(f'{b:02x}' for b in expect_before)}) — aborting") CloseHandle(h); sys.exit(4) old_prot = wt.DWORD(0) if not VirtualProtectEx(h, TARGET_ADDR, 3, 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 * 3)(*target) if not WriteProcessMemory(h, TARGET_ADDR, new, 3, 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, 3, old_prot.value, ctypes.byref(restored)) ReadProcessMemory(h, TARGET_ADDR, cur, 3, ctypes.byref(sz)) print(f" new bytes: {' '.join(f'{b:02x}' for b in bytes(cur))} ({'reverted' if revert else 'patched'})") CloseHandle(h) if __name__ == "__main__": main()