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
93 lines
3.2 KiB
Python
93 lines
3.2 KiB
Python
import re, struct
|
|
|
|
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
|
|
|
pe_off = struct.unpack_from('<I', exe, 0x3c)[0]
|
|
img_base = struct.unpack_from('<I', exe, pe_off + 52)[0]
|
|
|
|
# Correct section table
|
|
num_secs = struct.unpack_from('<H', exe, pe_off+6)[0]
|
|
opt_sz = struct.unpack_from('<H', exe, pe_off+20)[0]
|
|
secs = []
|
|
for i in range(num_secs):
|
|
s = pe_off + 24 + opt_sz + i*40
|
|
name = exe[s:s+8].rstrip(b'\x00').decode()
|
|
vsz = struct.unpack_from('<I', exe, s+8)[0]
|
|
vrva = struct.unpack_from('<I', exe, s+12)[0]
|
|
rsz = struct.unpack_from('<I', exe, s+16)[0]
|
|
roff = struct.unpack_from('<I', exe, s+20)[0]
|
|
secs.append((name, roff, rsz, vrva, vsz))
|
|
|
|
def fo_to_va(fo):
|
|
for n, ro, rs, vr, vs in secs:
|
|
if ro and ro <= fo < ro+rs:
|
|
return img_base + vr + (fo - ro)
|
|
return None
|
|
|
|
def va_to_fo(va):
|
|
rva = va - img_base
|
|
for n, ro, rs, vr, vs in secs:
|
|
if vr <= rva < vr+max(rs,vs):
|
|
return ro + (rva - vr) if ro else None
|
|
return None
|
|
|
|
print("Sections:")
|
|
for n, ro, rs, vr, vs in secs:
|
|
print(f" {n:<8} raw=0x{ro:06x} sz=0x{rs:05x} vRVA=0x{vr:06x} vsz=0x{vs:05x} VA=0x{img_base+vr:08x}")
|
|
|
|
# Verify a known string
|
|
music_va = fo_to_va(0x06b27c)
|
|
print(f"\nMUSIC.FF fo=0x06b27c -> VA=0x{music_va:08x}")
|
|
|
|
# Find MessageBoxA IAT entry
|
|
print("\n=== MessageBoxA IAT ===")
|
|
mbox_name_off = exe.find(b'MessageBoxA\x00')
|
|
print(f" name at 0x{mbox_name_off:06x}")
|
|
mbox_iat_fo = None
|
|
for i in range(0x06ec00, 0x06fa00, 4):
|
|
thunk_va = struct.unpack_from('<I', exe, i)[0]
|
|
if thunk_va == 0:
|
|
continue
|
|
fo = va_to_fo(thunk_va)
|
|
if fo and 0 < fo < len(exe):
|
|
try:
|
|
nm = exe[fo+2:exe.index(b'\x00', fo+2)].decode('latin1', errors='replace')
|
|
if 'MessageBox' in nm:
|
|
mbox_iat_fo = i
|
|
iat_va = fo_to_va(i)
|
|
print(f" IAT thunk at fo=0x{i:06x} VA=0x{iat_va:08x}")
|
|
except:
|
|
pass
|
|
|
|
# Find all CALL [MessageBoxA]
|
|
if mbox_iat_fo:
|
|
iat_va = fo_to_va(mbox_iat_fo)
|
|
needle = b'\xff\x15' + struct.pack('<I', iat_va)
|
|
print(f"\n=== All CALL MessageBoxA sites ===")
|
|
for m in re.finditer(re.escape(needle), exe[0x400:0x067000]):
|
|
fo = 0x400 + m.start()
|
|
ctx = exe[max(0x400,fo-24):fo+8]
|
|
print(f" CALL @ 0x{fo:06x}: {ctx.hex(' ')}")
|
|
|
|
# Look at context around the flag-setter at 0x033dfc
|
|
print("\n=== Context around flag-setter at 0x033de0-0x033e40 ===")
|
|
for row in range(0, 0x70, 16):
|
|
h = ' '.join(f'{b:02x}' for b in exe[0x033de0+row:0x033de0+row+16])
|
|
a = ''.join(chr(b) if 0x20<=b<0x7f else '.' for b in exe[0x033de0+row:0x033de0+row+16])
|
|
print(f" {0x033de0+row:06x}: {h:<48} {a}")
|
|
|
|
# Find correct string VAs and search code
|
|
print("\n=== Correct string VAs and code refs ===")
|
|
fos = {'MUSIC.FF':0x06b27c,'INTRFACE.FF':0x06b288,'BIGFILE.DAT':0x06b3b8,
|
|
'STRDATA.DAT':0x06bee8,'WORLDS\\':0x06b2b8}
|
|
for label, fo in fos.items():
|
|
va = fo_to_va(fo)
|
|
if not va:
|
|
print(f" {label}: can't get VA")
|
|
continue
|
|
needle = struct.pack('<I', va)
|
|
refs = [0x400+m.start() for m in re.finditer(re.escape(needle), exe[0x400:0x067000])]
|
|
print(f" {label:<15} VA=0x{va:08x} refs={len(refs)}")
|
|
for r in refs[:4]:
|
|
ctx = exe[max(0x400,r-6):r+8]
|
|
print(f" 0x{r:06x}: {ctx.hex(' ')}")
|