"""install_leakfix.py — Install leakfix.dll into an Asheron's Call install. Patches acclient.exe to statically import leakfix.dll. Adds a new PE section ".limport" containing a rebuilt import table that includes leakfix.dll alongside the original imports. Usage: python install_leakfix.py [ac_dir] Defaults ac_dir to "C:\\Turbine\\Asheron's Call". The script: 1. Locates acclient.exe and leakfix.dll in ac_dir 2. Checks whether acclient.exe already imports leakfix.dll (idempotent — exits cleanly if already patched) 3. Saves a backup of the original acclient.exe (if no backup exists) 4. Builds a new import table with leakfix.dll added at the end 5. Appends a new PE section ".limport" containing the new import table 6. Updates the PE Optional Header DataDirectory[1] (Import Table) to point at the new section 7. Updates NumberOfSections in the COFF header acclient.exe section table has 1 unused slot already (verified — it has 7 sections but room for 8 in headers), so we can append cleanly without needing to move .text. """ import struct import sys import os import shutil import hashlib DEFAULT_AC_DIR = r"C:\Turbine\Asheron's Call" # leakfix.dll exports — leakfix.dll has only DllMain (no exported functions # we need to call by name), so the import name table just needs ONE entry # with a fake function name or an ordinal import. Easiest: import by ordinal # 1 if the DLL exports anything, or use a forwarder. But leakfix.dll has no # exports, so we need to add one — see leakfix DLL build for a stub export. # # For a DLL that has *zero* exports, an empty IAT entry (just the terminator) # in the import descriptor would mean "load the DLL but don't bind anything". # Windows still calls DllMain on load. That's what we want. # # Required structure per import descriptor: # OriginalFirstThunk (RVA to import lookup table) - terminator entry only # TimeDateStamp = 0 # ForwarderChain = 0 # Name (RVA to DLL name string) # FirstThunk (RVA to import address table) - terminator entry only def hexdump_at(data, off, n=64): chunk = data[off:off+n] return ' '.join(f'{b:02x}' for b in chunk) def find_section(data, pe_off, name): num_sections = struct.unpack_from(' 0) if next_sect_entry + 40 > first_section_raw: print(f"\nERROR: no room in section table at file 0x{next_sect_entry:x} " f"(first section raw 0x{first_section_raw:x}). Would need to expand headers.") return 1 print(f"section table free slot at 0x{next_sect_entry:x}, first section raw 0x{first_section_raw:x}: OK") # Read original import table to copy as-is dd_off = opt_off + 96 old_imp_rva = struct.unpack_from(' len(data): data += b'\0' * (new_rawoff - len(data)) # pad to alignment data += blob # === Write new section header === new_sect_hdr = bytearray(40) new_sect_hdr[0:8] = b'.limport' # name struct.pack_into(' 1 else DEFAULT_AC_DIR cmd = sys.argv[2] if len(sys.argv) > 2 else 'patch' print(f"AC directory: {ac_dir}\nCommand: {cmd}\n") if cmd == 'patch': return patch(ac_dir) elif cmd == 'verify': return verify(ac_dir) else: print(f"unknown command: {cmd}") return 1 if __name__ == '__main__': sys.exit(main())