diff --git a/extract_dlls.py b/extract_dlls.py new file mode 100644 index 0000000..620e746 --- /dev/null +++ b/extract_dlls.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +""" +Extract embedded .NET assemblies from the game executable, +name them by their module/assembly name, and copy to GameDll/. + +Usage: + python3 csharp/extract_dlls.py [--exe PATH] [--out DIR] [--dry-run] + +Relies on the Module table (always table 0x00 in #~ metadata stream) +to get the module name, strips .dll/.exe suffix for assembly name. +""" + +import os, sys, struct, shutil + +PE32_MAGIC = 0x10B +PE32_PLUS_MAGIC = 0x20B +CLI_HDR_IDX = 14 + +# Assemblies that ship with .NET Framework / Mono runtime and +# need NOT be copied to GameDll (resolved from system at build time). +SYSTEM_PREFIXES = ('System.', 'Microsoft.Bcl.') +SYSTEM_NAMES = frozenset({ + 'mscorlib', 'netstandard', + 'Microsoft.CSharp', + 'System', # bare System.dll +}) + + +def is_system_assembly(name): + """Return True if the assembly is a system/Framework DLL.""" + if name in SYSTEM_NAMES: + return True + for p in SYSTEM_PREFIXES: + if name.startswith(p): + return True + return False + + +def rva_to_offset(sections, rva): + for va, vs, ra, rs in sections: + if va <= rva < va + vs: + return rva - va + ra + return None + + +def parse_sections(data, pe_sig_off): + num = struct.unpack_from('= num_dir: + return False + return struct.unpack_from('= num_dir: + return None + cli_rva = struct.unpack_from('= str_sz: + return None + + # Read null-terminated string from #Strings + addr = str_base + name_idx + if addr >= len(pe_data): + return None + end = addr + while end < len(pe_data) and pe_data[end] != 0: + end += 1 + module_name = pe_data[addr:end].decode('utf-8', errors='replace').strip() + if not module_name: + return None + + # Strip .dll or .exe extension + for ext in ('.dll', '.exe', '.DLL', '.EXE'): + if module_name.lower().endswith(ext.lower()): + module_name = module_name[:-len(ext)] + break + + return module_name + + +def find_embedded_pe(data): + """Find all embedded PE files in the binary.""" + results = [] + pos = 0 + while True: + idx = data.find(b'MZ', pos) + if idx == -1: + break + if idx + 0x40 >= len(data): + pos = idx + 2; continue + pe_off = struct.unpack_from('= len(data) or data[pe_addr:pe_addr + 4] != b'PE\x00\x00': + pos = idx + 2; continue + magic = struct.unpack_from(' deduped[name][1]: + deduped[name] = (off, sz) + found = [(n, o, s) for n, (o, s) in deduped.items()] + + # Split into game vs system + game = [(n, o, s) for n, o, s in found if not is_system_assembly(n)] + system = [(n, o, s) for n, o, s in found if is_system_assembly(n)] + + # Print game assemblies + print(f"Game assemblies to copy ({len(game)}):") + print(f"{'Assembly':<45} {'Offset':>12} {'Size':>10}") + print("-" * 69) + for name, off, sz in sorted(game, key=lambda x: x[0].lower()): + print(f"{name:<45} 0x{off:08x} {sz:>10,}") + if not args.include_system: + print(f"\nFiltered out system assemblies ({len(system)}):") + for name, off, sz in sorted(system, key=lambda x: x[0].lower()): + print(f" SKIP {name:45s} (system)") + print(f"\nTotal: {len(game)} game + {len(system)} system = {len(found)} unique .NET assemblies") + + if args.dry_run: + return + + # Decide which list to copy + to_copy = found if args.include_system else game + + os.makedirs(out_dir, exist_ok=True) + copied = skipped = filtered = 0 + for name, offset, sz in sorted(to_copy, key=lambda x: x[0].lower()): + src = os.path.join(tmp, f"{name}.dll") + dst = os.path.join(out_dir, f"{name}.dll") + if os.path.exists(dst): + if os.path.getsize(dst) == os.path.getsize(src): + skipped += 1 + continue + shutil.copyfile(src, dst) + print(f" COPY {name}.dll") + copied += 1 + + if not args.include_system: + filtered = len(found) - len(to_copy) + + print(f"\nDone: {copied} copied, {skipped} skipped") + if filtered: + print(f" {filtered} system assemblies filtered out (use --include-system to copy)") + if copied > 0: + print(f"Output: {out_dir}") + + +if __name__ == '__main__': + main()