""" Patch V: vtable-setter in init_archive (0x443010) Root cause: init_archive explicitly clears vtable ([ESI+0]=0) and never sets it. load_archives later calls 0x443140 (cleanup) for each archive object; with vtable=0, 0x443140 unmaps the file, closes handles, and zeroes field_0xC (directory ptr). open_archive_file checks field_0xC != 0, so it fails, and get_archive returns 0 => "corrupt or unavailable game file" error fires. Fix: insert MOV [ESI+0], 1 at the init_archive success return path. With vtable=1, 0x443140's vtable-check (TEST EAX; JNZ) skips cleanup, field_0xC is preserved, and get_archive succeeds. Mechanism: FO 0x42514 (VA 0x443114): replace 5-byte MOV EAX,1 with JMP to cave at FO 0x66FCE Cave (FO 0x66FCE, VA 0x467BCE): c7 46 00 01 00 00 00 MOV [ESI+0], 1 <- set vtable sentinel b8 01 00 00 00 MOV EAX, 1 <- return value = success 5e 5b c2 08 00 POP ESI; POP EBX; RET 8 """ import struct, sys EXE = "HAVOC_NOCD.EXE" exe = bytearray(open(EXE, "rb").read()) BASE = 0x400000 FO2VA = lambda fo: fo + 0x400C00 # ---- locate .text section header ---- e_lfanew = struct.unpack_from(' 0x%07x (cave=0x%07x)" % (tgt, patch_v_cave_va)) if tgt == patch_v_cave_va: print(" -> points to our cave; verifying cave code...") cave_expected = bytes.fromhex('c746000100000' + '0b801000000' + '5e5bc20800') cave_actual = bytes(exe[patch_v_cave_fo:patch_v_cave_fo+17]) if cave_actual == cave_expected: print(" Cave code matches. Patch already applied.") sys.exit(0) print(" -> unexpected; re-applying") else: print("ERROR: unexpected bytes at patch site: %s" % actual_at_site.hex(' ')) sys.exit(1) # ---- Build cave code ---- cave_code = bytearray() cave_code += b'\xc7\x46\x00\x01\x00\x00\x00' # MOV [ESI+0], 1 (7 bytes) cave_code += b'\xb8\x01\x00\x00\x00' # MOV EAX, 1 (5 bytes) cave_code += b'\x5e\x5b\xc2\x08\x00' # POP ESI; POP EBX; RET 8 (5 bytes) assert len(cave_code) == 17, "cave size %d != 17" % len(cave_code) print("Cave code (%d bytes): %s" % (len(cave_code), cave_code.hex(' '))) # ---- Build JMP at patch site ---- next_instr_va = FO2VA(PATCH_FO + 5) rel32 = (patch_v_cave_va - next_instr_va) & 0xFFFFFFFF jmp_bytes = b'\xe9' + struct.pack(' cave 0x%07x)" % ( jmp_bytes.hex(' '), rel32, patch_v_cave_va)) # Verify jump lands on cave assert (next_instr_va + struct.unpack_from(' 0x%x" % (text_vsiz, new_vsiz)) # ---- Apply ---- exe[patch_v_cave_fo:patch_v_cave_fo+17] = cave_code exe[PATCH_FO:PATCH_FO+5] = jmp_bytes struct.pack_into('