"""probe_iter3_orphans.py Walk a live AC client looking for CPhysicsObj instances (vfptr 0x007C78EC) matching the iter-3-triple predicate (parent=NULL, cell=NULL, hash_next=NULL). For each, dump key fields: id, state, transient_state, movement_manager, weenie_obj. Aggregate by weenie-NULL vs weenie-non-NULL to test the v23 classification assumption.""" import ctypes, ctypes.wintypes as wt, sys, struct PROCESS_VM_READ = 0x10 PROCESS_QUERY_INFORMATION = 0x400 k = ctypes.windll.kernel32 k.OpenProcess.argtypes = [wt.DWORD, wt.BOOL, wt.DWORD]; k.OpenProcess.restype = wt.HANDLE k.ReadProcessMemory.argtypes = [wt.HANDLE, wt.LPCVOID, wt.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)] k.ReadProcessMemory.restype = wt.BOOL k.VirtualQueryEx.argtypes = [wt.HANDLE, wt.LPCVOID, ctypes.c_void_p, ctypes.c_size_t] k.VirtualQueryEx.restype = ctypes.c_size_t class MBI(ctypes.Structure): _fields_ = [ ("BaseAddress", ctypes.c_void_p), ("AllocationBase", ctypes.c_void_p), ("AllocationProtect", wt.DWORD), ("RegionSize", ctypes.c_size_t), ("State", wt.DWORD), ("Protect", wt.DWORD), ("Type", wt.DWORD), ] MEM_COMMIT = 0x1000 MEM_PRIVATE = 0x20000 PAGE_READWRITE = 0x04 PAGE_EXECUTE_READWRITE = 0x40 CPHYS_VTABLE = 0x007C78EC # Field offsets (from instr.cpp constants) OFF_HASH_NEXT = 0x04 OFF_ID = 0x08 OFF_PARENT = 0x40 OFF_CELL = 0x90 OFF_STATE = 0xA8 OFF_TRANSTATE = 0xAC OFF_MOVMGR = 0xC4 OFF_WEENIE = 0x12C def rd(h, va, n): buf = (ctypes.c_ubyte * n)(); sz = ctypes.c_size_t(0) if not k.ReadProcessMemory(h, va, buf, n, ctypes.byref(sz)): return None return bytes(buf[:sz.value]) pid = int(sys.argv[1]) h = k.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) if not h: print(f"OpenProcess err={ctypes.get_last_error()}"); sys.exit(2) n_total = 0 n_triple = 0 buckets = { "weenie=NULL movmgr=NULL state=0": 0, "weenie=NULL movmgr=NULL state!=0": 0, "weenie=NULL movmgr!=NULL": 0, "weenie!=NULL state=0": 0, "weenie!=NULL state!=0": 0, } sample_each = {k_: [] for k_ in buckets} mbi = MBI() addr = 0 while k.VirtualQueryEx(h, addr, ctypes.byref(mbi), ctypes.sizeof(mbi)): region_base = mbi.BaseAddress or 0 region_size = mbi.RegionSize if (mbi.State == MEM_COMMIT and mbi.Type == MEM_PRIVATE and (mbi.Protect & 0xFF) in (PAGE_READWRITE, PAGE_EXECUTE_READWRITE)): data = rd(h, region_base, region_size) if data: n = len(data) // 4 for i in range(n - (OFF_WEENIE // 4) - 1): v = struct.unpack_from('= 0x80000000: break k.CloseHandle(h) print(f"pid {pid}: total CPhysicsObj={n_total} iter-3-triple={n_triple}") print() print(f" {'bucket':45s} {'count':>6}") for label, count in buckets.items(): print(f" {label:45s} {count:>6}") print() print("Sample dumps (up to 3 per bucket):") for label, samples in sample_each.items(): if not samples: continue print(f" [{label}]") for obj_va, obj_id, state, transtate, movmgr, weenie in samples: print(f" @0x{obj_va:08x} id=0x{obj_id:08x} state=0x{state:08x} ts=0x{transtate:08x} movmgr=0x{movmgr:08x} weenie=0x{weenie:08x}")