"""position_host_scan.py For every Position instance found in a live PID: - Read 64 bytes before and 128 bytes after the vtable pointer. - Look at offsets -32, -28, -24, -20, -16, -12, -8, -4 (might be vtables of host containers / property wrappers / list nodes). - Look at offsets +96 (the byte right after Position ends), +100, +104, +108 (vtables of "next instance in the same container"). - For any DWORD in those slots that falls in acclient.exe code/.rdata range (0x00400000..0x007FFFFF), bucket it. - Print top vtables seen at each offset. This is the diagnostic that breaks open which OWNER class is heap-allocating Positions but never freeing them. """ import ctypes import ctypes.wintypes as wt import struct import sys from collections import Counter, defaultdict POSITION_VT = 0x00797910 ACCLIENT_MIN = 0x00400000 ACCLIENT_MAX = 0x00900000 # generous upper bound for .text/.rdata PROCESS_VM_READ = 0x10 PROCESS_QUERY_INFORMATION = 0x400 class MBI(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)] k = ctypes.windll.kernel32 k.OpenProcess.argtypes = [wt.DWORD, wt.BOOL, wt.DWORD] k.OpenProcess.restype = wt.HANDLE k.CloseHandle.argtypes = [wt.HANDLE] k.CloseHandle.restype = wt.BOOL 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, ctypes.c_void_p, ctypes.POINTER(MBI), ctypes.c_size_t] k.VirtualQueryEx.restype = ctypes.c_size_t # Offsets relative to Position vtable address. Positive = into Position; # negative = backwards into containing header. OFFSETS = [-32, -28, -24, -20, -16, -12, -8, -4, +96, +100, +104, +108, +112, +116, +120, +124] def scan(pid): h = k.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) if not h: print(f"OpenProcess err={ctypes.get_last_error()}"); return 1 # Histogram offset -> Counter(value) hist = defaultdict(Counter) total = 0 # Track Position-at-N density: how many Positions hit at exact # stride 96 from a prior Position (suggests array/list of Positions). stride_hits = Counter() mbi = MBI() addr = 0 while k.VirtualQueryEx(h, addr, ctypes.byref(mbi), ctypes.sizeof(mbi)): pr = mbi.Protect & 0xff if (mbi.State == 0x1000 and mbi.Type == 0x20000 and pr in (0x04, 0x40)): buf = (ctypes.c_ubyte * mbi.RegionSize)() sz = ctypes.c_size_t(0) if k.ReadProcessMemory(h, mbi.BaseAddress, buf, mbi.RegionSize, ctypes.byref(sz)): data = bytes(buf[:sz.value]) end = (len(data) // 4) * 4 # Find all Position vtable hits in this region first pos_offs_in_region = [] for off in range(0, end, 4): if struct.unpack_from("= 0x80000000: break print(f"PID {pid}: {total} Position instances scanned") print() print("Top non-zero vtables at each offset relative to Position vt:") for delta in OFFSETS: c = hist[delta] non_null = [(v, n) for v, n in c.items() if v != 0] non_null.sort(key=lambda x: -x[1]) null_n = c.get(0, 0) if not non_null and null_n == 0: continue print(f"\n offset {delta:+4d}: (null={null_n}/{total} = " f"{100.0*null_n/max(1,total):.1f}%)") for v, n in non_null[:6]: pct = 100.0 * n / total print(f" 0x{v:08x} count={n:>6} ({pct:.1f}%)") print() print("Position-at-stride hits (suggests array/list of Positions):") for s, n in stride_hits.most_common(): print(f" stride {s}B: {n} adjacent Position pairs") k.CloseHandle(h) return 0 if __name__ == "__main__": sys.exit(scan(int(sys.argv[1])))