"""check_patch_state.py [pid1 pid2 ...] Check which patches are applied to each AC client. If no PIDs given, scans all acclient processes. """ import argparse, ctypes, ctypes.wintypes as wt, subprocess, sys SITES = { "v3b-1": (0x0053effe, bytes([0xff, 0x40, 0x24]), bytes([0x90, 0x90, 0x90])), "v3b-2": (0x0053f19c, bytes([0xff, 0x46, 0x24]), bytes([0x90, 0x90, 0x90])), "v4": (0x007ca444, bytes([0xa0, 0x54, 0x41, 0x00]), None), # CGfxObj vtable slot 11 "v5-RS": (0x0079a684, bytes([0xa0, 0x54, 0x41, 0x00]), None), # RenderSurface vtable slot 2 "v5-RT": (0x0079c1a0, bytes([0xa0, 0x54, 0x41, 0x00]), None), # RenderTexture vtable slot 2 "v8-1": (0x004e439d, bytes([0x0f, 0x84, 0xf3, 0x01, 0x00, 0x00]), bytes([0x90, 0x90, 0x90, 0x90, 0x90, 0x90])), "v8-2": (0x004e43c0, bytes([0x0f, 0x85, 0xcd, 0x01, 0x00, 0x00]), bytes([0x90, 0x90, 0x90, 0x90, 0x90, 0x90])), "v8-3": (0x004e4496, bytes([0x75, 0x0d]), bytes([0x75, 0x08])), } 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.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 def read_bytes(h, addr, n): buf = (ctypes.c_ubyte * n)() sz = ctypes.c_size_t(0) if not k.ReadProcessMemory(h, addr, buf, n, ctypes.byref(sz)): return None return bytes(buf[:sz.value]) def check_pid(pid): h = k.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) if not h: return None result = {} for name, (addr, orig, patched) in SITES.items(): cur = read_bytes(h, addr, len(orig)) if cur is None: result[name] = "?" elif cur == orig: result[name] = "." # unpatched / original elif patched is not None and cur == patched: result[name] = "P" else: result[name] = "X" # different (could be runtime-allocated thunk addr for v4/v5) k.CloseHandle(h) return result def get_window_title(pid): try: out = subprocess.check_output( ["powershell.exe", "-NoProfile", "-Command", f"(Get-Process -Id {pid} -ErrorAction SilentlyContinue).MainWindowTitle"], text=True, stderr=subprocess.DEVNULL).strip() return out.split("-")[-1].strip() if out else "" except Exception: return "" def list_acclient_pids(): try: out = subprocess.check_output( ["powershell.exe", "-NoProfile", "-Command", "(Get-Process acclient -ErrorAction SilentlyContinue).Id"], text=True, stderr=subprocess.DEVNULL).strip() return sorted(int(line) for line in out.splitlines() if line.strip()) except Exception: return [] ap = argparse.ArgumentParser() ap.add_argument("pids", type=int, nargs="*") args = ap.parse_args() pids = args.pids if args.pids else list_acclient_pids() cols = list(SITES.keys()) print(f"{'pid':>6} {' '.join(c[:6] for c in cols)} character") print(f"{'---':>6} {' '.join('------' for c in cols)} -----------") for pid in pids: r = check_pid(pid) if r is None: print(f"{pid:>6} ") continue name = get_window_title(pid) row = " ".join(f"{r[c]:>6}" for c in cols) print(f"{pid:>6} {row} {name}") print() print("Legend: . = unpatched/original P = patched (NOP/byte change)") print(" X = different (runtime-allocated thunk, e.g. v4/v5 -> custom code page)")