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
62 lines
2.6 KiB
Python
62 lines
2.6 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
|
|
|
|
# The strings: SETUP=0x46BEC4, DRIVE=0x46BECC, D:\=0x46BED4, HAVOC=0x46BED8
|
|
setup_va = 0x46bec4
|
|
drive_va = 0x46becc
|
|
dcol_va = 0x46bed4
|
|
havoc_va = 0x46bed8
|
|
|
|
# Search for any instruction that loads these addresses (LEA, MOV, PUSH, etc.)
|
|
targets = {setup_va: 'SETUP', drive_va: 'DRIVE', dcol_va: 'D:\\\\', havoc_va: 'HAVOC'}
|
|
|
|
# Also look for GetPrivateProfileString / GetPrivateProfileInt in the IAT
|
|
# These are Windows API calls
|
|
print("=== Searching for IAT calls to GetPrivateProfile* ===")
|
|
for fo in range(0x400, len(data)-5):
|
|
if data[fo] == 0xff and data[fo+1] == 0x15:
|
|
# CALL [IAT_addr]
|
|
iat_addr = struct.unpack_from('<I', data, fo+2)[0]
|
|
# Read what's at the IAT to see if it's GetPrivateProfileString
|
|
# Can't run it, but we can look for strings in the import table
|
|
iat_fo = VA2FO(iat_addr)
|
|
if 0 <= iat_fo < len(data) - 4:
|
|
pass # Would need to resolve IAT at runtime
|
|
|
|
# Better: search for the strings "GetPrivateProfile" in the binary (in import names)
|
|
for needle in [b'GetPrivateProfile', b'GetFileAttributes', b'FindFirstFile',
|
|
b'CreateDirectory', b'PathFileExists']:
|
|
pos = data.find(needle)
|
|
while pos != -1:
|
|
ctx = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data[pos:pos+32])
|
|
print(" FO 0x%05x: %s" % (pos, ctx))
|
|
pos = data.find(needle, pos+1)
|
|
|
|
print()
|
|
print("=== Code that references SETUP/DRIVE/HAVOC strings ===")
|
|
for target_va, name in targets.items():
|
|
print("-- Target: %s = VA 0x%08x --" % (name, target_va))
|
|
for fo in range(0x400, 0x67000):
|
|
b = data[fo]
|
|
# PUSH imm32
|
|
if b == 0x68:
|
|
imm = struct.unpack_from('<I', data, fo+1)[0]
|
|
if imm == target_va:
|
|
print(" PUSH at FO 0x%05x VA 0x%08x" % (fo, FO2VA(fo)))
|
|
# MOV reg, imm32 (b8-bf)
|
|
elif 0xb8 <= b <= 0xbf:
|
|
imm = struct.unpack_from('<I', data, fo+1)[0]
|
|
if imm == target_va:
|
|
print(" MOV reg,imm at FO 0x%05x VA 0x%08x" % (fo, FO2VA(fo)))
|
|
# MOV [mem], imm32 (c7 05)
|
|
elif b == 0xc7 and fo+9 < len(data) and data[fo+1] == 0x05:
|
|
imm = struct.unpack_from('<I', data, fo+6)[0]
|
|
if imm == target_va:
|
|
print(" MOV [mem],imm at FO 0x%05x VA 0x%08x" % (fo, FO2VA(fo)))
|
|
# LEA reg, [mem] (8d xx)
|
|
elif b == 0x8d and fo+6 < len(data):
|
|
pass # harder to parse
|