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
116 lines
4.6 KiB
Python
116 lines
4.6 KiB
Python
"""Final analysis: trace paths around error gate, find D:\ usage, plan bypass patches."""
|
|
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]
|
|
|
|
def fo_to_va(fo):
|
|
num_secs = struct.unpack_from('<H', exe, pe_off+6)[0]
|
|
opt_sz = struct.unpack_from('<H', exe, pe_off+20)[0]
|
|
for i in range(num_secs):
|
|
s = pe_off + 24 + opt_sz + i*40
|
|
vsz = struct.unpack_from('<I', exe, s+8)[0]
|
|
vrva = struct.unpack_from('<I', exe, s+12)[0]
|
|
rsz = struct.unpack_from('<I', exe, s+16)[0]
|
|
roff = struct.unpack_from('<I', exe, s+20)[0]
|
|
if roff and roff <= fo < roff+rsz:
|
|
return img_base + vrva + (fo - roff)
|
|
return None
|
|
|
|
def call_target(call_fo):
|
|
rel = struct.unpack_from('<i', exe, call_fo+1)[0]
|
|
next_va = fo_to_va(call_fo + 5)
|
|
if next_va is None:
|
|
return None, None
|
|
target_va = next_va + rel
|
|
# Find file offset from VA
|
|
num_secs = struct.unpack_from('<H', exe, pe_off+6)[0]
|
|
opt_sz = struct.unpack_from('<H', exe, pe_off+20)[0]
|
|
rva = target_va - img_base
|
|
for i in range(num_secs):
|
|
s = pe_off + 24 + opt_sz + i*40
|
|
vsz = struct.unpack_from('<I', exe, s+8)[0]
|
|
vrva = struct.unpack_from('<I', exe, s+12)[0]
|
|
rsz = struct.unpack_from('<I', exe, s+16)[0]
|
|
roff = struct.unpack_from('<I', exe, s+20)[0]
|
|
if roff and vrva <= rva < vrva+rsz:
|
|
return roff + (rva - vrva), target_va
|
|
return None, target_va
|
|
|
|
# Dump exact bytes from file at key offsets
|
|
def hexdump(fo, n, label=""):
|
|
print(f"\n{label} [0x{fo:06x}..0x{fo+n-1:06x}]:")
|
|
for row in range(0, n, 16):
|
|
fo2 = fo + row
|
|
h = ' '.join(f'{b:02x}' for b in exe[fo2:fo2+16])
|
|
a = ''.join(chr(b) if 0x20<=b<0x7f else '.' for b in exe[fo2:fo2+16])
|
|
print(f" {fo2:06x}: {h:<48} {a}")
|
|
|
|
# Dump raw bytes around 0x020270
|
|
hexdump(0x020270, 80, "Error gate region")
|
|
|
|
# Decode JNZ manually
|
|
jnz_fo = 0x020277
|
|
jnz_imm = exe[jnz_fo + 1]
|
|
next_instr_fo = jnz_fo + 2
|
|
# JNZ target = next_instr_va + signed(imm)
|
|
next_va = fo_to_va(next_instr_fo)
|
|
target_va = next_va + (jnz_imm if jnz_imm < 128 else jnz_imm - 256)
|
|
print(f"\nJNZ at 0x{jnz_fo:06x}: imm={jnz_imm} (0x{jnz_imm:02x})")
|
|
print(f" Next instr fo=0x{next_instr_fo:06x} VA=0x{next_va:08x}")
|
|
print(f" Target VA=0x{target_va:08x} (+/- {jnz_imm if jnz_imm < 128 else jnz_imm-256})")
|
|
# Convert target VA to file offset
|
|
tfo, _ = call_target(jnz_fo - 4) # hacky: find target fo from VA
|
|
# Manual: target_fo = text_raw + (target_va - img_base - text_vRVA)
|
|
text_roff = 0x000400
|
|
text_vrva = 0x001000
|
|
tfo = text_roff + (target_va - img_base - text_vrva)
|
|
print(f" Target fo=0x{tfo:06x}")
|
|
hexdump(tfo, 24, "JNZ target")
|
|
|
|
# Also decode JZ at 0x02026A
|
|
jz_fo = 0x02026A
|
|
jz_imm = exe[jz_fo + 1]
|
|
jz_next_va = fo_to_va(jz_fo + 2)
|
|
jz_target_va = jz_next_va + (jz_imm if jz_imm < 128 else jz_imm - 256)
|
|
jz_tfo = text_roff + (jz_target_va - img_base - text_vrva)
|
|
print(f"\nJZ at 0x{jz_fo:06x}: imm={jz_imm} (0x{jz_imm:02x})")
|
|
print(f" Target VA=0x{jz_target_va:08x} fo=0x{jz_tfo:06x}")
|
|
hexdump(jz_tfo, 24, "JZ target")
|
|
|
|
# D:\ usage context
|
|
hexdump(0x029a90, 0x80, "D:\\ usage at 0x029ab0")
|
|
|
|
# Now let's check what HAVOC.INI read code does with drive letter
|
|
# Search for code that reads from HAVOC.INI section "SETUP" key "DRIVE"
|
|
setup_va = fo_to_va(0x06b2c4) # "SETUP"
|
|
drive_va = fo_to_va(0x06b2cc) # "DRIVE" (check actual offsets)
|
|
print(f"\n=== Looking for SETUP/DRIVE string VAs ===")
|
|
# Find "SETUP" and "DRIVE" strings
|
|
setup_off = exe.find(b'SETUP\x00', 0x069400)
|
|
drive_off = exe.find(b'DRIVE\x00', 0x069400)
|
|
havocini_off = exe.find(b'HAVOC.INI\x00', 0x069400)
|
|
print(f" SETUP at 0x{setup_off:06x}, VA=0x{fo_to_va(setup_off) or 0:08x}")
|
|
print(f" DRIVE at 0x{drive_off:06x}, VA=0x{fo_to_va(drive_off) or 0:08x}")
|
|
print(f" HAVOC.INI at 0x{havocini_off:06x}, VA=0x{fo_to_va(havocini_off) or 0:08x}")
|
|
|
|
# Find code refs to DRIVE key VA
|
|
if drive_off:
|
|
dva = fo_to_va(drive_off)
|
|
needle = struct.pack('<I', dva)
|
|
refs = [0x400 + m.start() for m in re.finditer(re.escape(needle), exe[0x400:0x067000])]
|
|
print(f" DRIVE refs={len(refs)}")
|
|
for r in refs:
|
|
ctx = exe[max(0,r-8):r+8]
|
|
print(f" 0x{r:06x}: {ctx.hex(' ')}")
|
|
|
|
# Find code refs to HAVOC.INI VA
|
|
if havocini_off:
|
|
hva = fo_to_va(havocini_off)
|
|
needle = struct.pack('<I', hva)
|
|
refs = [0x400 + m.start() for m in re.finditer(re.escape(needle), exe[0x400:0x067000])]
|
|
print(f" HAVOC.INI refs={len(refs)}")
|
|
for r in refs:
|
|
ctx = exe[max(0,r-8):r+12]
|
|
print(f" 0x{r:06x}: {ctx.hex(' ')}")
|