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
59 lines
2.4 KiB
Python
59 lines
2.4 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 = lambda va: va - 0x400C00
|
|
|
|
def show(start, length, label=''):
|
|
if label: print("=== %s ===" % label)
|
|
for row in range(start, start + length, 16):
|
|
chunk = data[row:row+16]
|
|
ann = []
|
|
for j in range(len(chunk)):
|
|
f = row + j
|
|
try:
|
|
if data[f] == 0xe8 and f+5 <= len(data):
|
|
rel = struct.unpack_from('<i', data, f+1)[0]
|
|
tgt = (FO2VA(f+5) + rel) & 0xFFFFFFFF
|
|
ann.append('+%d:CALL_0x%x' % (j, tgt))
|
|
if data[f] == 0xff and data[f+1] == 0x15 and f+6 <= len(data):
|
|
iat = struct.unpack_from('<I', data, f+2)[0]
|
|
ann.append('+%d:[iat_0x%x]' % (j, iat))
|
|
if data[f] == 0xa3 and f+5 <= len(data):
|
|
va = struct.unpack_from('<I', data, f+1)[0]
|
|
if 0x47C000 <= va <= 0x482000:
|
|
ann.append('+%d:MOV[0x%x],EAX' % (j, va))
|
|
if data[f] == 0xc3: ann.append('+%d:RET' % j)
|
|
if data[f] == 0xc2 and f+3 <= len(data):
|
|
n = struct.unpack_from('<H', data, f+1)[0]
|
|
ann.append('+%d:RET%d' % (j, n))
|
|
except Exception:
|
|
pass
|
|
line = 'FO 0x%05x VA 0x%07x: %s' % (row, FO2VA(row), ' '.join('%02x' % b for b in chunk))
|
|
if ann:
|
|
line += ' [%s]' % ','.join(ann)
|
|
print(line)
|
|
|
|
# 0x464DCA - called from entry point
|
|
show(VA2FO(0x464DCA), 80, "0x464DCA (CRT init?)")
|
|
print()
|
|
# 0x465BD3 - called next
|
|
show(VA2FO(0x465BD3), 96, "0x465BD3 (global ctors?)")
|
|
print()
|
|
# 0x465BC8 - called next
|
|
show(VA2FO(0x465BC8), 32, "0x465BC8")
|
|
print()
|
|
# Check IAT entries near 0x482434 and 0x48243c
|
|
print("=== IAT around 0x482430 ===")
|
|
for va_iat in range(0x482430, 0x482470, 4):
|
|
fo = va_iat - 0x47C000 + 0x69400 # .data section
|
|
if fo < 0 or fo+4 > len(data): continue
|
|
val = struct.unpack_from('<I', data, fo)[0]
|
|
# Look for the import name by checking .idata
|
|
# .idata: VA 0x00482000, rawoff=0x6ec00 rawsiz=0x00e00
|
|
fo_idata = 0x6ec00 + (va_iat - 0x482000)
|
|
print(" [0x%x] = 0x%08x" % (va_iat, val))
|
|
|
|
# Also look at function 0x464F67 (called in alloc chain) to understand what
|
|
# the heap needs
|
|
show(VA2FO(0x464F67), 48, "0x464F67 (alloc helper 2)")
|