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
36 lines
1.2 KiB
Python
36 lines
1.2 KiB
Python
"""Patch J: bypass the init-flag error gate at 0x020277.
|
|
|
|
The instruction at 0x020277 is JNZ +0x2e (75 2e). When the init flag at
|
|
VA 0x475425 is zero the fall-through path shows the 'corrupt files' error.
|
|
The jump target (0x0202a7) is the success return stub:
|
|
b0 01 MOV AL, 1
|
|
5d 5f 5e 5b POP EBP/EDI/ESI/EBX
|
|
83 c4 44 ADD ESP, 0x44
|
|
c2 0c 00 RET 0x0c
|
|
|
|
Changing 75->eb turns JNZ into JMP so execution always takes the success path.
|
|
"""
|
|
import struct
|
|
|
|
with open("HAVOC_NOCD.EXE", "rb") as f:
|
|
exe = bytearray(f.read())
|
|
|
|
PATCH_FO = 0x020277
|
|
# Verify expected bytes
|
|
expected = bytes([0x75, 0x2e])
|
|
actual = bytes(exe[PATCH_FO:PATCH_FO+2])
|
|
if actual != expected:
|
|
print(f"ERROR: expected {expected.hex(' ')} at 0x{PATCH_FO:06x}, got {actual.hex(' ')}")
|
|
raise SystemExit(1)
|
|
|
|
exe[PATCH_FO] = 0xeb # JNZ -> JMP
|
|
|
|
# Verify target bytes at 0x0202a7
|
|
target_bytes = bytes(exe[0x0202a7:0x0202a7+12])
|
|
print(f"Patch J target bytes at 0x0202a7: {target_bytes.hex(' ')}")
|
|
# should be: b0 01 5d 5f 5e 5b 83 c4 44 c2 0c 00
|
|
|
|
with open("HAVOC_NOCD.EXE", "wb") as f:
|
|
f.write(exe)
|
|
|
|
print(f"Patch J applied: 0x{PATCH_FO:06x}: 75 2e -> eb 2e (JNZ -> JMP, bypass init-flag check)")
|