"""patch_v12_test.py [--revert] v12: NULL/range validator for the unpacker at 0x00526a50. This function (presumably AnimSequenceNode UnPack overload, or similar 4-dword stream reader) is called via vtable slot at 0x007c92c8. It dereferences `*(ctx)` without validating the cursor pointer. Two confirmed crashes here (Shadow 20:28, Frank 23:22), both with bad cursor pointers from leak-corrupted contexts. Mechanism (2 sites): 1. Write 29-byte validator at 0x00526a45 (uses 11-byte NOP pad + overwrites first 18 bytes of original function). Validator: size check + EAX load + cursor non-NULL + cursor >= 0x00400000 → success path jumps to 0x00526a62 (body unchanged); failure path returns 0 (same as original size-check-fail). 2. Redirect vtable slot at 0x007c92c8 from 0x00526a50 → 0x00526a45. After patch, all dispatched calls hit the validator first. The body from 0x00526a62 onward is untouched. Risk: - If a direct caller of 0x00526a50 exists (not via vtable), it'd land mid-validator at MOV EDX,[EAX] with EAX uninitialized. Ghidra found only the vtable xref, so unlikely. - Writing to a vtable in process memory is fine (we VirtualProtectEx). - 11 NOPs + 18 original-entry bytes = 29 bytes total replacement. No overlap into the body at 0x00526a62. """ import argparse, ctypes, ctypes.wintypes as wt, struct, sys VALIDATOR_VA = 0x00526a45 DISPATCH_VA = 0x007c92c8 OLD_FUNC_VA = 0x00526a50 VALIDATOR_BYTES = bytes([ # 0x00526a45 CMP DWORD [ESP+8], 0x10 0x83, 0x7C, 0x24, 0x08, 0x10, # 0x00526a4a JB +0x11 → 0x00526a5d (fail) 0x72, 0x11, # 0x00526a4c MOV EAX, [ESP+4] (EAX = ctx) 0x8B, 0x44, 0x24, 0x04, # 0x00526a50 MOV EDX, [EAX] (EDX = cursor) 0x8B, 0x10, # 0x00526a52 CMP EDX, 0x00400000 (cursor >= image base?) 0x81, 0xFA, 0x00, 0x00, 0x40, 0x00, # 0x00526a58 JB +0x03 → 0x00526a5d (fail) 0x72, 0x03, # 0x00526a5a JMP +0x06 → 0x00526a62 (body) 0xEB, 0x06, # 0x00526a5c NOP (filler) 0x90, # 0x00526a5d XOR EAX, EAX 0x33, 0xC0, # 0x00526a5f RET 8 0xC2, 0x08, 0x00, ]) assert len(VALIDATOR_BYTES) == 29 # Original bytes at validator site: 11 NOPs + 18 bytes of original function entry ORIG_VALIDATOR = bytes([0x90] * 11) + bytes([ 0x83, 0x7C, 0x24, 0x08, 0x10, # CMP [ESP+8], 0x10 0x73, 0x05, # JAE +5 0x33, 0xC0, # XOR EAX, EAX 0xC2, 0x08, 0x00, # RET 8 0x8B, 0x44, 0x24, 0x04, # MOV EAX, [ESP+4] 0x8B, 0x10 # MOV EDX, [EAX] ]) assert len(ORIG_VALIDATOR) == 29 OLD_DISPATCH = struct.pack("