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
57 lines
2.3 KiB
Python
57 lines
2.3 KiB
Python
import re, struct
|
|
|
|
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
|
|
|
# ── 1. Dump ALL imported function names ──
|
|
# Scan the file for the import name table: look for blocks of hint(2) + name(NUL-terminated)
|
|
# that look like Win32 API names
|
|
print("=== All imported function names ===")
|
|
seen = set()
|
|
for m in re.finditer(rb'\x00\x00([A-Za-z_][A-Za-z0-9_]{3,40})\x00', exe[0x06ac00:0x06fc00]):
|
|
name = m.group(1).decode('latin1')
|
|
if name not in seen:
|
|
seen.add(name)
|
|
print(f" {name}")
|
|
|
|
# ── 2. Find GetModuleFileNameA call sites ──
|
|
print("\n=== GetModuleFileNameA call sites ===")
|
|
idx = exe.find(b'GetModuleFileNameA\x00')
|
|
if idx != -1:
|
|
hint_fo = idx - 2
|
|
int_rva = hint_fo - 0x000400 + 0x001000
|
|
iat_needle = struct.pack('<I', 0x400000 + int_rva)
|
|
for m in re.finditer(b'\xff\x15' + re.escape(iat_needle), exe[0x400:0x067000]):
|
|
call_fo = 0x400 + m.start()
|
|
ctx = exe[call_fo:call_fo+64]
|
|
print(f" @ 0x{call_fo:06x}: {ctx.hex(' ')}")
|
|
|
|
# ── 3. Scan for all string refs in .text that point to rdata strings ──
|
|
# Push imm32 (68 xx xx xx xx) where VA points into rdata
|
|
print("\n=== PUSH <rdata string VA> in .text (file load refs) ──")
|
|
# Rdata VA range: roughly 0x400000 + (0x067000 - 0x400 + 0x1000) = 0x400000 + 0x067c00 = 0x467c00
|
|
# to end of file
|
|
rdata_va_start = 0x400000 + 0x067000 - 0x000400 + 0x001000
|
|
rdata_va_end = 0x400000 + len(exe) - 0x000400 + 0x001000
|
|
interesting_strings = {}
|
|
# Build map of rdata VA -> string
|
|
rdata = exe[0x067000:]
|
|
for m in re.finditer(rb'[A-Za-z][A-Za-z0-9_.\\]{3,30}\x00', rdata):
|
|
s = m.group()[:-1].decode('latin1')
|
|
if '.' in s or '\\' in s:
|
|
va = rdata_va_start - 0x067000 + 0x067000 + m.start()
|
|
# recalc: rdata file offset = 0x067000 + m.start()
|
|
fo = 0x067000 + m.start()
|
|
va = 0x400000 + fo - 0x000400 + 0x001000
|
|
interesting_strings[va] = s
|
|
|
|
# Now scan .text for pushes of these VAs
|
|
for m in re.finditer(b'\x68', exe[0x400:0x067000]):
|
|
call_fo = 0x400 + m.start()
|
|
if call_fo + 5 > 0x067000:
|
|
continue
|
|
va = struct.unpack_from('<I', exe, call_fo + 1)[0]
|
|
if va in interesting_strings:
|
|
s = interesting_strings[va]
|
|
# Only show file-related strings
|
|
ctx_after = exe[call_fo + 5: call_fo + 15]
|
|
print(f" PUSH {s!r:30s} @ 0x{call_fo:06x} next: {ctx_after.hex(' ')}")
|