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
88 lines
2.9 KiB
Python
88 lines
2.9 KiB
Python
import struct, re
|
|
|
|
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
|
base = 0x00400000
|
|
|
|
sections = [
|
|
(".text", 0x00001000, 0x00066bbd, 0x00000400, 0x00066c00),
|
|
(".rdata", 0x00079000, 0x000023c0, 0x00067000, 0x00002400),
|
|
(".data", 0x0007c000, 0x00005650, 0x00069400, 0x00005800),
|
|
(".idata", 0x00082000, 0x00000d5a, 0x0006ec00, 0x00000e00),
|
|
]
|
|
def rva2fo(rva):
|
|
for (n, vrva, vsz, raw, rsz) in sections:
|
|
if vrva <= rva < vrva + vsz:
|
|
return raw + (rva - vrva)
|
|
return None
|
|
|
|
def va2str(va):
|
|
fo = rva2fo(va - base)
|
|
if fo is None or fo >= len(exe):
|
|
return None
|
|
try:
|
|
end = exe.index(b'\x00', fo)
|
|
return exe[fo:end].decode('latin1', errors='replace')
|
|
except:
|
|
return None
|
|
|
|
def find_call_sites(iat_va):
|
|
needle = b'\xff\x15' + struct.pack('<I', iat_va)
|
|
sites = []
|
|
for m in re.finditer(re.escape(needle), exe[0x400:0x067000]):
|
|
sites.append(0x400 + m.start())
|
|
return sites
|
|
|
|
# IAT slot VAs from parse_iat.py output
|
|
iat = {
|
|
'CreateFileA': 0x00482390,
|
|
'GetModuleFileNameA': 0x00482364,
|
|
'GetPrivateProfileIntA':0x004823b4,
|
|
'ReadFile': 0x00482398,
|
|
'CreateFileMappingA': 0x004823bc,
|
|
'MapViewOfFile': 0x004823b8,
|
|
'DeleteFileA': 0x00482388,
|
|
'WriteFile': 0x004823a0,
|
|
'WritePrivateProfileStringA': 0x0048238c,
|
|
}
|
|
|
|
def extract_string_pushes(call_fo, lookback=160):
|
|
ctx_start = max(0x400, call_fo - lookback)
|
|
ctx = exe[ctx_start:call_fo]
|
|
pushes = []
|
|
pos = 0
|
|
while pos < len(ctx):
|
|
b = ctx[pos]
|
|
if b == 0x68 and pos + 5 <= len(ctx):
|
|
val = struct.unpack_from('<I', ctx, pos+1)[0]
|
|
s = va2str(val)
|
|
if s is not None and len(s) >= 2:
|
|
pushes.append((ctx_start + pos, s))
|
|
pos += 5
|
|
else:
|
|
pos += 1
|
|
return pushes
|
|
|
|
print("=== CreateFileA call sites ===")
|
|
for cfo in find_call_sites(iat['CreateFileA']):
|
|
pushes = extract_string_pushes(cfo)
|
|
print(f" CALL @ 0x{cfo:06x} strings: {[s for _, s in pushes]}")
|
|
|
|
print("\n=== DeleteFileA call sites ===")
|
|
for cfo in find_call_sites(iat['DeleteFileA']):
|
|
pushes = extract_string_pushes(cfo)
|
|
print(f" CALL @ 0x{cfo:06x} strings: {[s for _, s in pushes]}")
|
|
|
|
print("\n=== GetModuleFileNameA call sites ===")
|
|
for cfo in find_call_sites(iat['GetModuleFileNameA']):
|
|
ctx = exe[cfo:cfo+80]
|
|
print(f" CALL @ 0x{cfo:06x}: {ctx.hex(' ')}")
|
|
|
|
print("\n=== GetPrivateProfileIntA call sites ===")
|
|
for cfo in find_call_sites(iat['GetPrivateProfileIntA']):
|
|
pushes = extract_string_pushes(cfo)
|
|
print(f" CALL @ 0x{cfo:06x} strings: {[s for _, s in pushes]}")
|
|
|
|
print("\n=== WritePrivateProfileStringA call sites ===")
|
|
for cfo in find_call_sites(iat['WritePrivateProfileStringA']):
|
|
pushes = extract_string_pushes(cfo)
|
|
print(f" CALL @ 0x{cfo:06x} strings: {[s for _, s in pushes]}")
|