"""patch_v9_test.py [--revert] v9: hook CPhysicsObj::Destroy to call unparent_children(this) first. Mechanism: Insert a 5-byte JMP at CPhysicsObj::Destroy entry (0x005145D0) that redirects to a 17-byte thunk in the .text cave at 0x00792CE0. The thunk: 1. Saves this (ecx) 2. Calls CPhysicsObj::unparent_children(this) at 0x00513FE0 3. Restores this 4. Re-executes the 5 displaced bytes (push ebx; push esi; mov esi,ecx; push edi) 5. JMPs back to 0x005145D5 (continuation of Destroy) This fixes the orphan-children leak: parents being destroyed without calling unparent_children leave children with parent pointers to freed memory, which causes UAF crashes in CObjectMaint cleanup chains. Risk: - unparent_children is idempotent (safe to call multiple times) - Both patch site and thunk are in .text - Single chokepoint: only ~CPhysicsObj reaches Destroy - Stack alignment preserved (push ecx + pop ecx) """ import argparse import ctypes import ctypes.wintypes as wt import struct import sys PATCH_SITE_VA = 0x005145D0 THUNK_VA = 0x00792CE0 UNPARENT_VA = 0x00513FE0 RESUME_VA = 0x005145D5 ORIG_BYTES = bytes([0x53, 0x56, 0x8B, 0xF1, 0x57]) # push ebx; push esi; mov esi,ecx; push edi # Replacement is computed at runtime (E9 + rel32 to thunk) def build_thunk(thunk_base: int) -> bytes: """17 bytes: save ecx, call unparent_children, restore, displaced, jmp back.""" out = bytearray() out += bytes([0x51]) # push ecx # call rel32 unparent_children rel_call = UNPARENT_VA - (thunk_base + len(out) + 5) out += bytes([0xE8]) + struct.pack("