"""find_palette_cache.py Find the live Palette CLOCache instance in a process. Strategy: 1. Scan all committed RW memory for DWORD == 0x007c6a98 (DBOCacheVtbl) 2. For each hit, treat (hit - 0) as start of a CLOCache object 3. Search the next 0x114 bytes for the Palette::Allocator pointer (0x004f7b70) 4. If found, that CLOCache is Palette's. Print its address + relevant fields. Fields we care about (from FUN_00416b50 / FreelistAdd): +0xf1: byte, cache config flag 1 (freelist enabled?) +0xf2: byte, cache config flag 2 +0xfc: freelist MAX size (uint32) +0x100: freelist head ptr +0x104: freelist tail ptr +0x108: freelist current count """ import ctypes, ctypes.wintypes as wt, struct, sys VTABLE_DBOCACHE = 0x007c6a98 ALLOCATOR_PALETTE = 0x004f7b70 PROCESS_VM_READ = 0x0010 PROCESS_QUERY_INFORMATION = 0x0400 MEM_COMMIT = 0x1000 MEM_PRIVATE = 0x20000 MEM_IMAGE = 0x1000000 PAGE_READWRITE = 0x04 PAGE_EXECUTE_READWRITE = 0x40 class MEMORY_BASIC_INFORMATION(ctypes.Structure): _fields_ = [ ("BaseAddress", ctypes.c_void_p), ("AllocationBase", ctypes.c_void_p), ("AllocationProtect", wt.DWORD), ("PartitionId", wt.WORD), ("RegionSize", ctypes.c_size_t), ("State", wt.DWORD), ("Protect", wt.DWORD), ("Type", wt.DWORD), ] 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 VirtualQueryEx = k32.VirtualQueryEx VirtualQueryEx.argtypes = [wt.HANDLE, ctypes.c_void_p, ctypes.POINTER(MEMORY_BASIC_INFORMATION), ctypes.c_size_t] VirtualQueryEx.restype = ctypes.c_size_t def read(h, addr, n): buf = (ctypes.c_ubyte * n)() sz = ctypes.c_size_t(0) if not ReadProcessMemory(h, addr, buf, n, ctypes.byref(sz)): return None return bytes(buf[:sz.value]) def main(): pid = int(sys.argv[1]) h = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) if not h: print(f"OpenProcess({pid}) err={ctypes.get_last_error()}"); return mbi = MEMORY_BASIC_INFORMATION() addr = 0 candidates = [] while VirtualQueryEx(h, addr, ctypes.byref(mbi), ctypes.sizeof(mbi)): base = mbi.BaseAddress or 0 size = mbi.RegionSize st = mbi.State ty = mbi.Type pr = mbi.Protect & 0xFF if st == MEM_COMMIT and ty == MEM_PRIVATE and pr in (PAGE_READWRITE, PAGE_EXECUTE_READWRITE): data = read(h, base, size) if data: for off in range(0, len(data) - 4, 4): v = struct.unpack_from("= 0x80000000: break print(f"DBOCache vtable hits: {len(candidates)}") palette_cache = None for addr, blob in candidates: # Search the first 0x114 bytes for Palette::Allocator pointer for o in range(0, min(len(blob), 0x114) - 4, 4): if struct.unpack_from("