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
82 lines
3.4 KiB
Python
82 lines
3.4 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
|
|
|
|
# Find IAT addresses for GetPrivateProfileStringA and GetPrivateProfileIntA
|
|
# by scanning the import directory
|
|
# The strings are at:
|
|
# FO 0x6F1C8: GetPrivateProfileStringA
|
|
# FO 0x6F2D4: GetPrivateProfileIntA
|
|
|
|
# Find the IAT thunks: look for FF 15 xx xx xx xx (CALL [IAT])
|
|
# The IAT entry itself should be near the import names
|
|
# Let's find all CALL [mem] patterns and check what they point to
|
|
|
|
print("=== All CALL [mem] (FF 15) in .text section (FO 0x400-0x67000) ===")
|
|
gp_str_calls = []
|
|
gp_int_calls = []
|
|
|
|
# First: find the IAT addresses by looking at the import tables
|
|
# Scan for references to the string offsets
|
|
str_fo = 0x6f1c8
|
|
int_fo = 0x6f2d4
|
|
|
|
# The IAT contains pointers to the DLL functions.
|
|
# We need to find what VA the IAT entry for GetPrivateProfileStringA has.
|
|
# In a PE, the IAT is typically at a fixed address. Let's scan for CALL [addr]
|
|
# and then check the immediate to see if it's in the IAT range.
|
|
|
|
# Actually: let's scan all FF 15 calls and find ones where nearby code pushes
|
|
# strings from the 0x46BE70 data block area.
|
|
|
|
# IAT is likely in the 0x482000-0x483000 range based on prior analysis
|
|
iat_start = 0x482000
|
|
iat_end = 0x483000
|
|
|
|
for fo in range(0x400, 0x67000):
|
|
if data[fo] == 0xff and data[fo+1] == 0x15:
|
|
iat_addr = struct.unpack_from('<I', data, fo+2)[0]
|
|
if iat_start <= iat_addr < iat_end:
|
|
# This is an IAT call. Check if it's GetPrivateProfile*
|
|
# The IAT at runtime points to the function. But statically we need
|
|
# to check the import names.
|
|
# Just collect them all and print context
|
|
# Check what was pushed before this call (look back 80 bytes)
|
|
context_start = max(0x400, fo - 80)
|
|
context_bytes = data[context_start:fo+6]
|
|
context_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in context_bytes)
|
|
if 'SETUP' in context_str or 'DRIVE' in context_str or 'HAVOC' in context_str:
|
|
print(" IAT call at FO 0x%05x VA 0x%08x -> [0x%08x]" % (fo, FO2VA(fo), iat_addr))
|
|
print(" Context: %s" % context_str[-60:])
|
|
|
|
print()
|
|
# Better approach: look for the function that opens the INI file
|
|
# GetPrivateProfileString calls should push the INI filename as last arg
|
|
# HAVOC.INI = b'HAVOC.INI'
|
|
havoc_ini_fo = data.find(b'HAVOC.INI\x00')
|
|
if havoc_ini_fo == -1:
|
|
havoc_ini_fo = data.find(b'HAVOC.ini\x00')
|
|
print("HAVOC.INI string at FO 0x%05x VA 0x%08x" % (havoc_ini_fo, FO2VA(havoc_ini_fo)))
|
|
|
|
# Now find all pushes of HAVOC.INI's VA
|
|
ini_va = FO2VA(havoc_ini_fo)
|
|
print("=== Code pushing HAVOC.INI VA 0x%08x ===" % ini_va)
|
|
for fo in range(0x400, 0x67000):
|
|
if data[fo] == 0x68:
|
|
imm = struct.unpack_from('<I', data, fo+1)[0]
|
|
if imm == ini_va:
|
|
print(" PUSH at FO 0x%05x VA 0x%08x" % (fo, FO2VA(fo)))
|
|
|
|
# Also look for GetFileAttributes / FindFirst style CD check
|
|
print()
|
|
print("=== Searching for GetFileAttributes IAT call context ===")
|
|
# GetFileAttributes string in imports
|
|
gfa_fo = data.find(b'GetFileAttributesA')
|
|
if gfa_fo != -1:
|
|
print("GetFileAttributesA import at FO 0x%05x" % gfa_fo)
|
|
gff_fo = data.find(b'FindFirstFileA')
|
|
if gff_fo != -1:
|
|
print("FindFirstFileA import at FO 0x%05x" % gff_fo)
|