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
106 lines
4.1 KiB
Python
106 lines
4.1 KiB
Python
"""Find all code references to D:\ buffer and the file-open call chain."""
|
|
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]
|
|
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
|
|
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((roff, rsz, vrva, vsz))
|
|
|
|
def fo_to_va(fo):
|
|
for 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 ro, rs, vr, vs in secs:
|
|
if ro and vr <= rva < vr+rs:
|
|
return ro + (rva - vr)
|
|
return None
|
|
|
|
def search_refs(fo, label=""):
|
|
va = fo_to_va(fo)
|
|
if not va:
|
|
print(f" {label}: fo 0x{fo:06x} -> no VA")
|
|
return
|
|
needle = struct.pack('<I', va)
|
|
refs = [0x400 + m.start() for m in re.finditer(re.escape(needle), exe[0x400:0x067000])]
|
|
print(f"\n {label} fo=0x{fo:06x} VA=0x{va:08x} refs={len(refs)}")
|
|
for r in refs[:8]:
|
|
ctx = exe[max(0x400,r-8):r+8]
|
|
print(f" 0x{r:06x}: {ctx.hex(' ')}")
|
|
|
|
# D:\ buffer and neighboring strings
|
|
print("=== D:\\ and neighboring strings ===")
|
|
for fo in [0x06b2b0, 0x06b2b8, 0x06b2d4, 0x06b390, 0x06b3a8, 0x06b3b8, 0x06b2d0]:
|
|
s_bytes = exe[fo:fo+24]
|
|
s = s_bytes.rstrip(b'\x00').decode('latin1', errors='replace')
|
|
search_refs(fo, repr(s))
|
|
|
|
# What's around D:\ in .data?
|
|
print("\n=== Hex context around D:\\ (0x06b2c0-0x06b420) ===")
|
|
for row in range(0, 0x160, 16):
|
|
fo = 0x06b2c0 + row
|
|
h = ' '.join(f'{b:02x}' for b in exe[fo:fo+16])
|
|
a = ''.join(chr(b) if 0x20<=b<0x7f else '.' for b in exe[fo:fo+16])
|
|
print(f" {fo:06x}: {h:<48} {a}")
|
|
|
|
# Dump the init function at 0x009C60
|
|
print("\n=== Init function at 0x009C60 ===")
|
|
for row in range(0, 0xc0, 16):
|
|
fo = 0x009c60 + row
|
|
h = ' '.join(f'{b:02x}' for b in exe[fo:fo+16])
|
|
a = ''.join(chr(b) if 0x20<=b<0x7f else '.' for b in exe[fo:fo+16])
|
|
ann = ''
|
|
for off in range(13):
|
|
if fo+off+4 <= fo+16:
|
|
dw = struct.unpack_from('<I', exe, fo+off)[0]
|
|
if dw == fo_to_va(0x06b2d4): ann = ' <-D:\\'
|
|
elif dw == fo_to_va(0x06b27c): ann = ' <-MUSIC.FF'
|
|
elif dw == fo_to_va(0x06b2b8): ann = ' <-WORLDS\\'
|
|
elif dw == fo_to_va(0x06b3b8): ann = ' <-BIGFILE.DAT'
|
|
elif dw == fo_to_va(0x06bee8): ann = ' <-STRDATA.DAT'
|
|
# Also annotate CALL targets
|
|
if exe[fo] == 0xe8:
|
|
rel = struct.unpack_from('<i', exe, fo+1)[0]
|
|
tva = img_base + 0x001000 + (fo+5 - 0x000400) + rel
|
|
tfo = 0x000400 + (tva - img_base - 0x001000)
|
|
ann += f' CALL->0x{tfo:06x}'
|
|
print(f" {fo:06x}: {h:<48} {a}{ann}")
|
|
|
|
# Context around CALL at 0x020253 -> 0x009C60
|
|
print("\n=== Context before error check (0x020230-0x020285) ===")
|
|
for row in range(0, 0x60, 16):
|
|
fo = 0x020230 + row
|
|
h = ' '.join(f'{b:02x}' for b in exe[fo:fo+16])
|
|
a = ''.join(chr(b) if 0x20<=b<0x7f else '.' for b in exe[fo:fo+16])
|
|
ann = ''
|
|
if exe[fo] == 0xe8:
|
|
rel = struct.unpack_from('<i', exe, fo+1)[0]
|
|
tva = img_base + 0x001000 + (fo+5 - 0x000400) + rel
|
|
tfo = 0x000400 + (tva - img_base - 0x001000)
|
|
ann += f' CALL->0x{tfo:06x}'
|
|
print(f" {fo:06x}: {h:<48} {a}{ann}")
|
|
|
|
# Check the flag check, and suggest bypass bytes
|
|
print("\n=== Error gate at 0x020270 (current bytes) ===")
|
|
ctx = exe[0x020270:0x020282]
|
|
print(f" {ctx.hex(' ')}")
|
|
print(f" Byte at 0x020277: 0x{ctx[7]:02x} (should be 75=JNZ for unfixed, or eb=JMP for bypass)")
|
|
|
|
# Check if there's a HAVOC.INI read code that stores the drive to 0x06b2d4
|
|
drive_va = fo_to_va(0x06b2d4)
|
|
print(f"\n=== All writes to D:\\ buffer area VA=0x{drive_va:08x} ===")
|
|
# Look for MOV BYTE/DWORD PTR [0x47DED4] patterns
|
|
# Could be: a2, c6 05, a3, c7 05 ...
|
|
# Also look for LEA/MOV reg, addr then strcpy/memcpy
|