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
52 lines
2.1 KiB
Python
52 lines
2.1 KiB
Python
import re, struct
|
|
|
|
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
|
rdata = exe[0x067000:]
|
|
base = 0x067000
|
|
|
|
print("=== ALL filename/path strings in rdata ===")
|
|
for m in re.finditer(rb'[A-Za-z0-9][A-Za-z0-9_.\\: ]{3,39}(?=\x00)', rdata):
|
|
s = m.group().decode('latin1')
|
|
fo = base + m.start()
|
|
if '.' in s or chr(0x5c) in s or ':' in s:
|
|
print(f" 0x{fo:06x}: {s!r}")
|
|
|
|
# Find ALL MessageBoxA / error-display calls in .text
|
|
print("\n=== MessageBoxA calls in .text ===")
|
|
# Find MessageBoxA IAT entry
|
|
msgbox_name = b'MessageBoxA\x00'
|
|
idx = exe.find(msgbox_name)
|
|
print(f" MessageBoxA name at: 0x{idx:06x}")
|
|
|
|
# Find all CALL DWORD PTR [mem] (ff 15 xx xx xx xx) in .text 0x000400-0x067000
|
|
print("\n=== All CALL [mem] in .text (indirect calls, includes IAT) ===")
|
|
call_targets = {}
|
|
for m in re.finditer(b'\xff\x15', exe[0x400:0x067000]):
|
|
off = 0x400 + m.start()
|
|
va = struct.unpack_from('<I', exe, off+2)[0]
|
|
fo_target = va - 0x400000 - 0x001000 + 0x000400
|
|
if 0x067000 <= fo_target < 0x080000:
|
|
# read the import name near there
|
|
name_off = fo_target + 2 # skip hint
|
|
name = exe[name_off: exe.index(b'\x00', name_off)].decode('latin1', errors='replace')
|
|
call_targets[name] = call_targets.get(name, 0) + 1
|
|
|
|
for name, count in sorted(call_targets.items()):
|
|
print(f" {name}: {count} call(s)")
|
|
|
|
# Now find all calls to MessageBoxA specifically
|
|
print("\n=== MessageBoxA call sites ===")
|
|
for fo_iat in range(0x067000, 0x080000, 4):
|
|
hint_fo = struct.unpack_from('<I', exe, fo_iat)[0] - 0x400000 - 0x001000 + 0x000400
|
|
if 0 < hint_fo < len(exe):
|
|
try:
|
|
name = exe[hint_fo+2 : exe.index(b'\x00', hint_fo+2)].decode('latin1', errors='replace')
|
|
except:
|
|
continue
|
|
if 'MessageBox' in name:
|
|
iat_va = 0x400000 + 0x001000 + (fo_iat - 0x000400)
|
|
needle = struct.pack('<I', iat_va)
|
|
for m in re.finditer(b'\xff\x15' + re.escape(needle), exe[0x400:0x067000]):
|
|
call_fo = 0x400 + m.start()
|
|
ctx = exe[max(0x400, call_fo-16):call_fo+20]
|
|
print(f" CALL MessageBox @ 0x{call_fo:06x}: {ctx.hex(' ')}")
|