leakhunt/tools/va_to_raw.py
acbot 57b5e43d0e Initial commit — leak-hunt project complete
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>
2026-05-23 21:07:58 +02:00

52 lines
1.9 KiB
Python

"""
va_to_raw.py <exe> <va_hex> [count]
Maps a virtual address to file offset using the PE section table, and
optionally dumps `count` bytes from the file at that offset.
"""
import struct, sys
path = sys.argv[1]
va = int(sys.argv[2], 0)
n = int(sys.argv[3]) if len(sys.argv) > 3 else 64
with open(path, "rb") as f:
d = f.read()
# DOS header
e_lfanew = struct.unpack_from("<I", d, 0x3C)[0]
# PE signature + COFF header
assert d[e_lfanew:e_lfanew+4] == b"PE\x00\x00"
coff = e_lfanew + 4
num_sections = struct.unpack_from("<H", d, coff + 2)[0]
opt_hdr_size = struct.unpack_from("<H", d, coff + 16)[0]
opt = coff + 20
magic = struct.unpack_from("<H", d, opt)[0]
assert magic == 0x10b, f"only PE32 supported, got {magic:#x}"
image_base = struct.unpack_from("<I", d, opt + 28)[0]
sect_tbl = opt + opt_hdr_size
print(f"image_base = 0x{image_base:08x}, num_sections = {num_sections}")
rva = va - image_base
print(f"RVA = 0x{rva:08x}")
for i in range(num_sections):
off = sect_tbl + i * 40
name = d[off:off+8].rstrip(b"\x00").decode("latin1")
virt_size = struct.unpack_from("<I", d, off + 8)[0]
virt_addr = struct.unpack_from("<I", d, off + 12)[0]
raw_size = struct.unpack_from("<I", d, off + 16)[0]
raw_ptr = struct.unpack_from("<I", d, off + 20)[0]
if virt_addr <= rva < virt_addr + max(virt_size, raw_size):
delta = rva - virt_addr
file_off = raw_ptr + delta
print(f"section {name!r}: VA=0x{image_base+virt_addr:08x} raw=0x{raw_ptr:08x}")
print(f"file offset = 0x{file_off:08x}")
chunk = d[file_off:file_off+n]
for j in range(0, len(chunk), 16):
row = chunk[j:j+16]
hexs = " ".join(f"{b:02x}" for b in row)
asci = "".join(chr(b) if 32 <= b < 127 else "." for b in row)
print(f" 0x{va+j:08x} {hexs:<47} {asci}")
break
else:
print("VA not in any section")