Five bugs identified and patched in retail Asheron's Call client: - v3b: palette refcount over-increment (3-byte NOP at two sites) - v5: RenderSurface PurgeResource no-op stub (vtable slot 2 thunk) - v11: two dangling-pointer crash guards (NULL-check + reorder) - v14: CEnvCell::Destroy ClipPlaneList leak (18-byte JMP to cleanup thunk) - v22: unpacker stale-pointer SEH guard (whole-function __try/__except) All five ship in leakfix.dll (117 KB, SHA d282f23c…) which is loaded by acclient.exe at process start via PE import table patching by tools/install_leakfix.py. Controlled 15-client fleet soak: unpatched control died at 26h with palette exhaustion; all 14 patched clients survived past that point and reached ≥5-day uptime. Residual ~15 MB/h growth traced to d3d9.dll's internal slab allocator (260KB surface backing buffers retained after Release). See REPORT.md §10 for the full investigation; conclusion is that it's unfixable from outside d3d9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
3.9 KiB
Python
103 lines
3.9 KiB
Python
"""build_patched_binary.py
|
|
|
|
Build acclient.eor.patched.exe by applying the v3b byte patches to a
|
|
backed-up copy of the original EoR acclient.exe.
|
|
|
|
Patches (file offsets = VA - 0x400 since base=0x400000 and .text RVA is +0x400):
|
|
- VA 0x0053effe ff 40 24 -> 90 90 90 ; NOP makeModifiedPalette() over-increment
|
|
- VA 0x0053f19c ff 46 24 -> 90 90 90 ; NOP makeModifiedPalette(id,sub) over-increment
|
|
|
|
For 32-bit PE with base=0x00400000 and .text starting at RVA 0x1000
|
|
mapped to file offset 0x400 (standard MSVC layout), file_offset =
|
|
VA - 0x00400000 - 0x1000 + 0x400 = VA - 0x00400C00.
|
|
"""
|
|
import hashlib, os, shutil, struct, sys
|
|
|
|
|
|
SRC = r"C:\Turbine\Asheron's Call\acclient.exe"
|
|
ORIG = r"C:\Turbine\Asheron's Call\acclient.eor.orig.exe"
|
|
PATCHED = r"C:\Turbine\Asheron's Call\acclient.eor.patched.exe"
|
|
|
|
# VA-based patches (we resolve file offsets via PE parsing)
|
|
PATCHES = [
|
|
(0x0053effe, bytes([0xff, 0x40, 0x24]), bytes([0x90, 0x90, 0x90])),
|
|
(0x0053f19c, bytes([0xff, 0x46, 0x24]), bytes([0x90, 0x90, 0x90])),
|
|
]
|
|
|
|
|
|
def parse_pe_sections(data):
|
|
"""Return list of (rva_start, rva_size, file_offset) tuples."""
|
|
e_lfanew = struct.unpack_from("<I", data, 0x3c)[0]
|
|
sig = data[e_lfanew:e_lfanew+4]
|
|
assert sig == b"PE\0\0", f"bad PE sig: {sig}"
|
|
# COFF header at e_lfanew + 4
|
|
num_sections = struct.unpack_from("<H", data, e_lfanew + 6)[0]
|
|
size_opt_hdr = struct.unpack_from("<H", data, e_lfanew + 0x14)[0]
|
|
sections_off = e_lfanew + 0x18 + size_opt_hdr
|
|
image_base = struct.unpack_from("<I", data, e_lfanew + 0x34)[0] # PE32 ImageBase
|
|
out = []
|
|
for i in range(num_sections):
|
|
sh = sections_off + i * 0x28
|
|
virtual_size = struct.unpack_from("<I", data, sh + 0x08)[0]
|
|
virtual_addr = struct.unpack_from("<I", data, sh + 0x0c)[0]
|
|
raw_size = struct.unpack_from("<I", data, sh + 0x10)[0]
|
|
raw_off = struct.unpack_from("<I", data, sh + 0x14)[0]
|
|
name = data[sh:sh+8].rstrip(b"\0").decode("ascii", "replace")
|
|
out.append((name, image_base + virtual_addr, virtual_size, raw_off, raw_size))
|
|
return image_base, out
|
|
|
|
|
|
def va_to_file_offset(va, sections):
|
|
for name, va_start, vsize, raw_off, raw_size in sections:
|
|
if va_start <= va < va_start + max(vsize, raw_size):
|
|
return raw_off + (va - va_start)
|
|
raise ValueError(f"VA 0x{va:08x} not in any section")
|
|
|
|
|
|
def main():
|
|
if not os.path.exists(SRC):
|
|
print(f"src not found: {SRC}"); sys.exit(1)
|
|
|
|
# Backup
|
|
if not os.path.exists(ORIG):
|
|
shutil.copy2(SRC, ORIG)
|
|
print(f"backup written: {ORIG}")
|
|
else:
|
|
print(f"backup already exists: {ORIG}")
|
|
|
|
with open(ORIG, "rb") as f:
|
|
data = bytearray(f.read())
|
|
|
|
sha_orig = hashlib.sha256(data).hexdigest()
|
|
print(f"orig sha256: {sha_orig}")
|
|
print(f"orig size: {len(data)}")
|
|
|
|
image_base, sections = parse_pe_sections(data)
|
|
print(f"image base: 0x{image_base:08x}")
|
|
for s in sections[:3]:
|
|
print(f" section {s[0]:<8} va=0x{s[1]:08x} vsize=0x{s[2]:x} raw=0x{s[3]:08x} rsize=0x{s[4]:x}")
|
|
|
|
# Apply patches
|
|
for va, orig_bytes, patched_bytes in PATCHES:
|
|
off = va_to_file_offset(va, sections)
|
|
actual = bytes(data[off:off+len(orig_bytes)])
|
|
if actual == orig_bytes:
|
|
data[off:off+len(patched_bytes)] = patched_bytes
|
|
print(f" patched VA 0x{va:08x} (file off 0x{off:x}): "
|
|
f"{' '.join(f'{b:02x}' for b in orig_bytes)} -> "
|
|
f"{' '.join(f'{b:02x}' for b in patched_bytes)}")
|
|
elif actual == patched_bytes:
|
|
print(f" VA 0x{va:08x} already patched, skipping")
|
|
else:
|
|
print(f" VA 0x{va:08x} UNEXPECTED bytes: {' '.join(f'{b:02x}' for b in actual)}")
|
|
sys.exit(2)
|
|
|
|
with open(PATCHED, "wb") as f:
|
|
f.write(data)
|
|
sha_new = hashlib.sha256(bytes(data)).hexdigest()
|
|
print(f"\npatched sha256: {sha_new}")
|
|
print(f"written: {PATCHED}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|