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.5 KiB
Python
48 lines
1.5 KiB
Python
import re, struct
|
|
|
|
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
|
|
|
pe_off = struct.unpack_from('<I', exe, 0x3c)[0]
|
|
img_base = struct.unpack_from('<I', exe, pe_off + 52)[0]
|
|
print(f"ImageBase: 0x{img_base:08x}")
|
|
|
|
# The global byte tested before 'PUSH 6 -> corrupt error'
|
|
# Instruction at 0x020270: a0 25 54 47 00 = MOV AL, [0x00475425]
|
|
flag_va = 0x00475425
|
|
flag_needle = struct.pack('<I', flag_va)
|
|
|
|
print(f"\n=== All accesses to flag byte at VA=0x{flag_va:08x} ===")
|
|
for m in re.finditer(re.escape(flag_needle), exe[0x400:0x067000]):
|
|
off = 0x400 + m.start()
|
|
ctx = exe[max(0x400, off-4):off+6]
|
|
print(f" 0x{off-4:06x}: {ctx.hex(' ')}")
|
|
|
|
# String VAs with correct ImageBase
|
|
rdata_vRVA = 0x079000
|
|
rdata_raw = 0x067000
|
|
|
|
def rdata_va(fo):
|
|
return img_base + rdata_vRVA + (fo - rdata_raw)
|
|
|
|
string_fos = {
|
|
'MUSIC.FF': 0x06b27c,
|
|
'INTRFACE.FF': 0x06b288,
|
|
'OBJECTS.FF': 0x06b298,
|
|
'SOUND.FF': 0x06b2a4,
|
|
'BIGFILE.DAT': 0x06b3b8,
|
|
'STRDATA.DAT': 0x06bee8,
|
|
'WORLDS_rel': 0x06b2b0,
|
|
'WORLDS_abs': 0x06b2b8,
|
|
'HAVOCDAT': 0x06b3a8,
|
|
'GAMIMAGE': 0x06b980,
|
|
}
|
|
|
|
print(f"\n=== String VAs and code references ===")
|
|
for label, fo in string_fos.items():
|
|
va = rdata_va(fo)
|
|
needle = struct.pack('<I', va)
|
|
refs = [0x400 + m.start() for m in re.finditer(re.escape(needle), exe[0x400:0x067000])]
|
|
print(f" {label:<15} VA=0x{va:08x} refs={len(refs)}")
|
|
for r in refs[:3]:
|
|
ctx = exe[max(0x400,r-6):r+8]
|
|
print(f" 0x{r:06x}: {ctx.hex(' ')}")
|