Created
March 30, 2026 00:55
-
-
Save behcet/7b43122bd16c7e3c27b7d564eff143e2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Patch cm0304.exe to add Sleep(1) when the message queue is empty. | |
| Injects a code cave at the end of the .text section that: | |
| 1. Resolves Sleep via GetProcAddress(GetModuleHandleA("kernel32.dll"), "Sleep") | |
| 2. Calls Sleep(1) | |
| 3. Returns | |
| The message pump's exit path is redirected through this cave. | |
| """ | |
| import shutil | |
| import struct | |
| import sys | |
| import os | |
| # Replace the path below before running!! | |
| GAME_DIR = "#### YOUR PATH HERE ###/Program Files (x86)/Eidos/CM 03-04" | |
| EXE_NAME = "cm0304.exe" | |
| BACKUP_NAME = "cm0304.exe.bak" | |
| # The PE image base for this 32-bit exe | |
| IMAGE_BASE = 0x400000 | |
| # The message pump exit at VMA 0x987145: | |
| # pop esi; add esp, 28; ret (5 bytes: 5e 83 c4 1c c3) | |
| # We replace these 5 bytes with a JMP to our code cave. | |
| PATCH_VMA = 0x987145 | |
| PATCH_ORIGINAL = bytes([0x5e, 0x83, 0xc4, 0x1c, 0xc3]) | |
| # Code cave location: end of .text section padding | |
| # .text ends at VMA 0xab4b4a, next section at 0xab5000 | |
| # We use 0xab4b50 (aligned) | |
| CAVE_VMA = 0xAB4B50 | |
| # IAT addresses (absolute VMAs) for functions we call | |
| IAT_GetModuleHandleA = 0xAB50C8 | |
| IAT_GetProcAddress = 0xAB50B0 | |
| # String addresses within our cave | |
| STR_KERNEL32_VMA = CAVE_VMA + 0x20 | |
| STR_SLEEP_VMA = CAVE_VMA + 0x2D | |
| def vma_to_file(vma): | |
| """Convert VMA to file offset (.text section).""" | |
| return vma - IMAGE_BASE | |
| def build_cave(): | |
| """Build the code cave bytes.""" | |
| code = bytearray() | |
| # pop esi | |
| code += b'\x5e' | |
| # add esp, 0x1c (28) | |
| code += b'\x83\xc4\x1c' | |
| # push STR_KERNEL32_VMA ("kernel32.dll") | |
| code += b'\x68' + struct.pack('<I', STR_KERNEL32_VMA) | |
| # call [IAT_GetModuleHandleA] | |
| code += b'\xff\x15' + struct.pack('<I', IAT_GetModuleHandleA) | |
| # push STR_SLEEP_VMA ("Sleep") | |
| code += b'\x68' + struct.pack('<I', STR_SLEEP_VMA) | |
| # push eax (hKernel32) | |
| code += b'\x50' | |
| # call [IAT_GetProcAddress] | |
| code += b'\xff\x15' + struct.pack('<I', IAT_GetProcAddress) | |
| # push 1 | |
| code += b'\x6a\x01' | |
| # call eax (Sleep) | |
| code += b'\xff\xd0' | |
| # ret | |
| code += b'\xc3' | |
| # Pad to offset 0x20 for string data | |
| code += b'\x00' * (0x20 - len(code)) | |
| # "kernel32.dll\0" at offset 0x20 | |
| code += b'kernel32.dll\x00' | |
| # "Sleep\0" at offset 0x2d | |
| assert len(code) == 0x2D | |
| code += b'Sleep\x00' | |
| return bytes(code) | |
| def build_jmp(from_vma, to_vma): | |
| """Build a 5-byte JMP rel32 instruction.""" | |
| displacement = to_vma - (from_vma + 5) | |
| return b'\xe9' + struct.pack('<i', displacement) | |
| def main(): | |
| exe_path = os.path.join(GAME_DIR, EXE_NAME) | |
| bak_path = os.path.join(GAME_DIR, BACKUP_NAME) | |
| if not os.path.exists(exe_path): | |
| print(f"Error: {exe_path} not found") | |
| sys.exit(1) | |
| # Read the exe | |
| with open(exe_path, 'rb') as f: | |
| data = bytearray(f.read()) | |
| # Verify the patch site has the expected bytes | |
| patch_offset = vma_to_file(PATCH_VMA) | |
| actual = bytes(data[patch_offset:patch_offset + len(PATCH_ORIGINAL)]) | |
| if actual != PATCH_ORIGINAL: | |
| if actual == build_jmp(PATCH_VMA, CAVE_VMA): | |
| print("Already patched!") | |
| sys.exit(0) | |
| print(f"Error: unexpected bytes at patch site 0x{PATCH_VMA:x}") | |
| print(f" Expected: {PATCH_ORIGINAL.hex()}") | |
| print(f" Got: {actual.hex()}") | |
| sys.exit(1) | |
| # Verify the cave area is empty (zeros) | |
| cave_offset = vma_to_file(CAVE_VMA) | |
| cave_bytes = build_cave() | |
| cave_area = bytes(data[cave_offset:cave_offset + len(cave_bytes)]) | |
| if cave_area != b'\x00' * len(cave_bytes): | |
| print(f"Error: code cave area at 0x{CAVE_VMA:x} is not empty") | |
| sys.exit(1) | |
| # Backup | |
| if not os.path.exists(bak_path): | |
| shutil.copy2(exe_path, bak_path) | |
| print(f"Backup: {bak_path}") | |
| else: | |
| print(f"Backup already exists: {bak_path}") | |
| # Write the code cave | |
| data[cave_offset:cave_offset + len(cave_bytes)] = cave_bytes | |
| print(f"Code cave: {len(cave_bytes)} bytes at VMA 0x{CAVE_VMA:x} (file 0x{cave_offset:x})") | |
| # Patch the message pump exit to jump to our cave | |
| jmp = build_jmp(PATCH_VMA, CAVE_VMA) | |
| data[patch_offset:patch_offset + len(jmp)] = jmp | |
| print(f"Patched: JMP at 0x{PATCH_VMA:x} -> 0x{CAVE_VMA:x}") | |
| # Write | |
| with open(exe_path, 'wb') as f: | |
| f.write(data) | |
| print("Done! Restart the game to test.") | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment