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
60 lines
2.3 KiB
Python
60 lines
2.3 KiB
Python
import struct, sys, io
|
|
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_data = lambda va: va - 0x47C000 + 0x69400 # .data section
|
|
|
|
# Check the value at [0x480964]
|
|
fo = VA2FO_data(0x480964)
|
|
val = struct.unpack_from('<I', data, fo)[0]
|
|
print("[0x480964] at FO 0x%05x = 0x%08x" % (fo, val))
|
|
|
|
# Check surrounding .data at 0x480960
|
|
print("Data at 0x480960 area (FO 0x%05x):" % VA2FO_data(0x480960))
|
|
for i in range(0, 32, 4):
|
|
fo2 = VA2FO_data(0x480960 + i)
|
|
v = struct.unpack_from('<I', data, fo2)[0]
|
|
print(" [0x%x] at FO 0x%05x = 0x%08x" % (0x480960 + i, fo2, v))
|
|
|
|
# Also check what callers of 0x4633BE pass as the heap handle
|
|
print()
|
|
print("=== 0x4633BE wrapper (FO 0x627be): reads [0x480964] ===")
|
|
fo = 0x627be
|
|
chunk = data[fo:fo+16]
|
|
print("FO 0x%05x: %s" % (fo, ' '.join('%02x'%b for b in chunk)))
|
|
|
|
# Find callers of 0x4633BE
|
|
print()
|
|
print("=== All callers of 0x4633BE ===")
|
|
tgt_va = 0x4633BE
|
|
for fo in range(0, 0x67000):
|
|
if data[fo] == 0xe8 and fo+5 <= len(data):
|
|
rel = struct.unpack_from('<i', data, fo+1)[0]
|
|
call_tgt = (FO2VA(fo+5) + rel) & 0xFFFFFFFF
|
|
if call_tgt == tgt_va:
|
|
ctx = data[fo-8:fo+12]
|
|
print(" FO 0x%05x VA 0x%07x: ...%s..." % (fo, FO2VA(fo), ' '.join('%02x'%b for b in ctx)))
|
|
|
|
# Also search for what calls 0x4633D1 (the actual impl)
|
|
print()
|
|
print("=== All callers of 0x4633D1 (real alloc impl) ===")
|
|
tgt_va2 = 0x4633D1
|
|
for fo in range(0, 0x67000):
|
|
if data[fo] == 0xe8 and fo+5 <= len(data):
|
|
rel = struct.unpack_from('<i', data, fo+1)[0]
|
|
call_tgt = (FO2VA(fo+5) + rel) & 0xFFFFFFFF
|
|
if call_tgt == tgt_va2:
|
|
ctx = data[fo-8:fo+12]
|
|
print(" FO 0x%05x VA 0x%07x: ...%s..." % (fo, FO2VA(fo), ' '.join('%02x'%b for b in ctx)))
|
|
|
|
# Check what functions initialize the allocator - search for CALL 0x464E77 (used inside 0x4633C7)
|
|
print()
|
|
print("=== Callers of 0x464E77 (alloc helper) ===")
|
|
tgt_va3 = 0x464E77
|
|
for fo in range(0, 0x67000):
|
|
if data[fo] == 0xe8 and fo+5 <= len(data):
|
|
rel = struct.unpack_from('<i', data, fo+1)[0]
|
|
call_tgt = (FO2VA(fo+5) + rel) & 0xFFFFFFFF
|
|
if call_tgt == tgt_va3:
|
|
ctx = data[fo-4:fo+8]
|
|
print(" FO 0x%05x VA 0x%07x: %s" % (fo, FO2VA(fo), ' '.join('%02x'%b for b in ctx)))
|