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
48 lines
1.7 KiB
Python
48 lines
1.7 KiB
Python
import struct, sys, io, r2pipe
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
|
|
data = open('HAVOC_NOCD.EXE','rb').read()
|
|
FO2VA = lambda fo: fo + 0x400C00
|
|
VA2FO = lambda va: va - 0x400C00
|
|
|
|
r2 = r2pipe.open("Z:/Development/devl/Havoc/HAVOC_NOCD.EXE",
|
|
["-e", "bin.relocs.apply=true"])
|
|
|
|
# String at FO 0x6B2D4 = VA 0x46BED4: "DRIVE\0\0\0"
|
|
# String at FO 0x6B2DC = VA 0x46BEDC: "D:\"
|
|
# String at FO 0x6B2E0 = VA 0x46BEE0: "HAVOC"
|
|
drive_va = 0x46bed4
|
|
havoc_va = 0x46bee0
|
|
dcolon_va = 0x46bedc
|
|
|
|
print("=== Callers that push DRIVE VA 0x46BED4 or D:\\ VA 0x46BEDC ===")
|
|
for target in [drive_va, havoc_va, dcolon_va]:
|
|
for fo in range(0x400, 0x67000):
|
|
if data[fo] == 0x68:
|
|
imm = struct.unpack_from('<I', data, fo+1)[0]
|
|
if imm == target:
|
|
print(" PUSH 0x%08x at FO 0x%05x VA 0x%08x" % (target, fo, FO2VA(fo)))
|
|
|
|
# Also search for the string "HAVOC" as a MOV target (could be hard-coded path)
|
|
print()
|
|
|
|
# Show the full data block at 0x6B270-0x6B320
|
|
print("=== Data at 0x46BE70-0x46BF40 (file list area) ===")
|
|
start = VA2FO(0x46be70)
|
|
for fo2 in range(start, start + 0xD0, 16):
|
|
chunk = data[fo2:fo2+16]
|
|
hex_part = ' '.join('%02x' % b for b in chunk)
|
|
asc_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
|
|
print("VA 0x%08x: %-47s %s" % (FO2VA(fo2), hex_part, asc_part))
|
|
|
|
print()
|
|
# Now find what code READS HAVOC.INI DRIVE key
|
|
print("=== Callers of SETUP/DRIVE/D:\\ area ===")
|
|
setup_va = None
|
|
# Find "SETUP" string
|
|
pos = data.find(b'SETUP\x00')
|
|
while pos != -1:
|
|
print(" 'SETUP' at FO 0x%05x VA 0x%08x" % (pos, FO2VA(pos)))
|
|
pos = data.find(b'SETUP\x00', pos+1)
|
|
|
|
r2.quit()
|