Diagnosed and fixed the crash that killed HAVOC_NOCD.EXE ~1s after launch
(dgVoodoo window appears black, then the process exits). It was not a clean
exit: an ACCESS_VIOLATION (null-pointer read at VA 0x444E6A) during init.
Root cause is a copy-protection check in the WORLDS\ data-file loader
(0x42a480). It opens each file (GRAFIX/LAND/MAP/STUF) from both a local
"WORLDS\<name>" path and a CD-drive path (HAVOC.INI [SETUP] DRIVE=D:\), and
returns a valid stream only when the local open fails and the CD-drive open
succeeds. With files present locally and no D: drive it returned NULL, and the
GRAFIX loader (0x444db0) dereferenced that NULL without checking.
Fix: 14-byte patch at FO 0x29a2b rewrites the return decision to hand back the
successfully-opened local object. The game now boots to the title screen
("HAVOC(tm) by Reality Bytes"), responsive, main loop running.
- docs/PATCHES.md: full patch table (28 patches) + crash write-up
- tools/: RE + patching scripts (r2pipe disasm, minidump parser, ctypes
debugger, PE/IAT/import analysis)
- run_probe.bat / run_and_log.bat: reliable native launch for repro
- .gitignore: exclude CD images, Ghidra install/project, dgVoodoo, *.ini
Diagnosed via WER minidumps (%LOCALAPPDATA%\CrashDumps) parsed in pure Python
(no cdb/windbg available).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
67 lines
2.9 KiB
Python
67 lines
2.9 KiB
Python
import struct
|
|
|
|
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
|
|
|
# Read PE header
|
|
pe_off = struct.unpack_from('<I', exe, 0x3c)[0]
|
|
print(f"PE offset: 0x{pe_off:04x}")
|
|
print(f"Machine: 0x{struct.unpack_from('<H', exe, pe_off+4)[0]:04x}")
|
|
|
|
# Optional header
|
|
opt_off = pe_off + 24
|
|
image_base = struct.unpack_from('<I', exe, opt_off + 28)[0]
|
|
print(f"ImageBase: 0x{image_base:08x}")
|
|
|
|
# Number of sections
|
|
num_sections = struct.unpack_from('<H', exe, pe_off + 6)[0]
|
|
size_of_opt = struct.unpack_from('<H', exe, pe_off + 20)[0]
|
|
sections_off = pe_off + 24 + size_of_opt
|
|
|
|
print(f"\n{'Section':<12} {'VirtAddr':>10} {'VirtSize':>10} {'RawOff':>10} {'RawSize':>10}")
|
|
sections = []
|
|
for i in range(num_sections):
|
|
s = sections_off + i * 40
|
|
name = exe[s:s+8].rstrip(b'\x00').decode('latin1', errors='replace')
|
|
virt_rva = struct.unpack_from('<I', exe, s+12)[0]
|
|
virt_size = struct.unpack_from('<I', exe, s+8)[0]
|
|
raw_off = struct.unpack_from('<I', exe, s+20)[0]
|
|
raw_size = struct.unpack_from('<I', exe, s+16)[0]
|
|
sections.append((name, virt_rva, virt_size, raw_off, raw_size))
|
|
print(f" {name:<10} 0x{virt_rva:08x} 0x{virt_size:08x} 0x{raw_off:08x} 0x{raw_size:08x}")
|
|
|
|
# Find which section GetPrivateProfileStringA name (at 0x06f1c8) falls in
|
|
name_fo = 0x06f1c8
|
|
print(f"\nGetPrivateProfileStringA name at file offset 0x{name_fo:06x}")
|
|
for (sname, vrva, vsz, raw, rsz) in sections:
|
|
if raw <= name_fo < raw + rsz:
|
|
rva = vrva + (name_fo - raw)
|
|
va = image_base + rva
|
|
print(f" In section [{sname}]: RVA=0x{rva:08x} VA=0x{va:08x}")
|
|
hint_fo = name_fo - 2
|
|
hint_rva = vrva + (hint_fo - raw)
|
|
hint_va = image_base + hint_rva
|
|
print(f" Hint+name VA: 0x{hint_va:08x}")
|
|
|
|
# Search for this VA in the IAT/INT (which lives in idata too)
|
|
import re
|
|
needle = struct.pack('<I', hint_va)
|
|
print(f"\n Searching for INT entry 0x{hint_va:08x}...")
|
|
for m in re.finditer(re.escape(needle), exe):
|
|
ref_fo = m.start()
|
|
ref_in_sec = [(sn, vr, vs, ro, rs) for (sn, vr, vs, ro, rs) in sections if ro <= ref_fo < ro + rs]
|
|
sec_label = ref_in_sec[0][0] if ref_in_sec else "?"
|
|
print(f" Found at file offset 0x{ref_fo:06x} in [{sec_label}]")
|
|
# This is the INT entry. The IAT entry is at the same index in the IAT array.
|
|
iat_fo = ref_fo
|
|
iat_rva = None
|
|
for (sn, vr, vs, ro, rs) in sections:
|
|
if ro <= iat_fo < ro + rs:
|
|
iat_rva = vr + (iat_fo - ro)
|
|
if iat_rva:
|
|
iat_va = image_base + iat_rva
|
|
print(f" IAT entry VA (pre-load): 0x{iat_va:08x}")
|
|
# Find CALL [iat_va] in .text
|
|
call_needle = b'\xff\x15' + struct.pack('<I', iat_va)
|
|
for cm in re.finditer(re.escape(call_needle), exe[0x400:0x067000]):
|
|
cfo = 0x400 + cm.start()
|
|
print(f" CALL @ 0x{cfo:06x}")
|