""" runtime_patch_v3.py [--dry-run] [--revert] Per-vtable thunks with class-specific field offsets, derived from find_rendersurfaces.py diagnostic across two dumps: vtable A 0x007caa08 (base RenderSurface): m_pSurfaceBits +0x58, sourceBits +0x40 vtable B 0x007ca0d8 (RenderSurfaceD3D-ish): buffers at +0x90 and +0x94 vtable C 0x007961e0 (another variant): buffers at +0x90 and +0x94 The v2 patch used +0x58 for ALL vtables; that was wrong for B/C and crashed PID 17252 reading pixel data instead of a buffer pointer. This v3 emits a separate thunk per vtable with the correct offsets. Each thunk: - Reads each buffer field - Null-check, skip if zero - Calls operator delete[] at EoR 0x005df164 (one buffer at a time) - Nulls the field - Returns 1 (uint8_t) """ import argparse, ctypes, ctypes.wintypes as wt, json, os, struct, sys PROCESS_VM_READ = 0x0010 PROCESS_VM_WRITE = 0x0020 PROCESS_VM_OPERATION = 0x0008 PROCESS_QUERY_INFORMATION = 0x0400 MEM_COMMIT = 0x1000 MEM_RESERVE = 0x2000 MEM_RELEASE = 0x8000 PAGE_EXECUTE_READWRITE = 0x40 PAGE_READWRITE = 0x04 # Per-vtable patch metadata: (name, vtable_addr, [field_offsets_to_free]) VTABLES = [ ("RS vtable A (base RenderSurface)", 0x007caa08, [0x40, 0x58]), ("RS vtable B (D3D variant)", 0x007ca0d8, [0x90, 0x94]), ("RS vtable C (other variant)", 0x007961e0, [0x90, 0x94]), ] RSO_SLOT = 0x2c OP_DELETE_ARR_EOR = 0x005df164 NO_OP_STUB = 0x004154a0 k32 = ctypes.windll.kernel32 def _setup_apis(): for fn, argt, rest in ( ('OpenProcess', [wt.DWORD, wt.BOOL, wt.DWORD], wt.HANDLE), ('CloseHandle', [wt.HANDLE], wt.BOOL), ('ReadProcessMemory', [wt.HANDLE, wt.LPCVOID, wt.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)], wt.BOOL), ('WriteProcessMemory', [wt.HANDLE, wt.LPVOID, wt.LPCVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)], wt.BOOL), ('VirtualAllocEx', [wt.HANDLE, wt.LPVOID, ctypes.c_size_t, wt.DWORD, wt.DWORD], wt.LPVOID), ('VirtualFreeEx', [wt.HANDLE, wt.LPVOID, ctypes.c_size_t, wt.DWORD], wt.BOOL), ('VirtualProtectEx', [wt.HANDLE, wt.LPVOID, ctypes.c_size_t, wt.DWORD, ctypes.POINTER(wt.DWORD)], wt.BOOL), ): f = getattr(k32, fn); f.argtypes = argt; f.restype = rest _setup_apis() def read_uint32(h, addr): b = ctypes.c_uint32(0); n = ctypes.c_size_t(0) if not k32.ReadProcessMemory(h, addr, ctypes.byref(b), 4, ctypes.byref(n)) or n.value != 4: raise OSError(f"read 0x{addr:x} err={ctypes.get_last_error()}") return b.value def write_bytes(h, addr, data): n = ctypes.c_size_t(0); old = wt.DWORD(0) if not k32.VirtualProtectEx(h, addr, len(data), PAGE_READWRITE, ctypes.byref(old)): raise OSError(f"protect 0x{addr:x} err={ctypes.get_last_error()}") try: buf = (ctypes.c_ubyte * len(data))(*data) if not k32.WriteProcessMemory(h, addr, buf, len(data), ctypes.byref(n)) or n.value != len(data): raise OSError(f"write 0x{addr:x} err={ctypes.get_last_error()}") finally: k32.VirtualProtectEx(h, addr, len(data), old.value, ctypes.byref(old)) def build_thunk(remote_addr, offsets): """Build x86 __fastcall thunk that frees `[esi+offset]` for each offset. Body: push esi mov esi, ecx FOR each offset: mov eax, [esi + offset] test eax, eax jz skip push eax call add esp, 4 mov dword ptr [esi + offset], 0 skip: mov al, 1 pop esi ret """ code = bytearray() code += b"\x56" # push esi code += b"\x8b\xf1" # mov esi, ecx for off in offsets: # mov eax, [esi + off] if off < 0x80: code += b"\x8b\x46" + bytes([off & 0xff]) else: code += b"\x8b\x86" + struct.pack(" (rel32) call_idx = len(code); code += b"\xe8\x00\x00\x00\x00" next_after = remote_addr + call_idx + 5 rel = (OP_DELETE_ARR_EOR - next_after) & 0xffffffff code[call_idx+1:call_idx+5] = struct.pack(" 0x{entry['pre']:08x}") write_bytes(h, entry["vtable"] + RSO_SLOT, struct.pack(" {cur_addr:#x}") applied.append(dict(name=name, vtable=vtbl, offsets=offs, pre=cur_slot, post=cur_addr, thunk_addr=cur_addr, thunk_size=len(tk))) cur_addr += len(tk) + 4 # padding if applied: with open(backup_file, "w") as f: json.dump({"pid": pid, "thunk_base": remote, "slots": applied}, f, indent=2) print(f"backup saved to {backup_file}") else: print("NO slots patched") k32.VirtualFreeEx(h, remote, 0, MEM_RELEASE) finally: k32.CloseHandle(h) def main(): ap = argparse.ArgumentParser() ap.add_argument("pid", type=int) ap.add_argument("--dry-run", action="store_true") ap.add_argument("--revert", action="store_true") args = ap.parse_args() patch_process(args.pid, dry_run=args.dry_run, revert=args.revert) if __name__ == "__main__": main()