havoc-remaster/tools/deep_trace.py
pyr0ball 27174a1c79 feat: NOCD patch boots Havoc on modern Windows (defeats CD copy protection)
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
2026-07-03 09:29:59 -07:00

93 lines
3.2 KiB
Python

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]
# Correct section table
num_secs = struct.unpack_from('<H', exe, pe_off+6)[0]
opt_sz = struct.unpack_from('<H', exe, pe_off+20)[0]
secs = []
for i in range(num_secs):
s = pe_off + 24 + opt_sz + i*40
name = exe[s:s+8].rstrip(b'\x00').decode()
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]
secs.append((name, roff, rsz, vrva, vsz))
def fo_to_va(fo):
for n, ro, rs, vr, vs in secs:
if ro and ro <= fo < ro+rs:
return img_base + vr + (fo - ro)
return None
def va_to_fo(va):
rva = va - img_base
for n, ro, rs, vr, vs in secs:
if vr <= rva < vr+max(rs,vs):
return ro + (rva - vr) if ro else None
return None
print("Sections:")
for n, ro, rs, vr, vs in secs:
print(f" {n:<8} raw=0x{ro:06x} sz=0x{rs:05x} vRVA=0x{vr:06x} vsz=0x{vs:05x} VA=0x{img_base+vr:08x}")
# Verify a known string
music_va = fo_to_va(0x06b27c)
print(f"\nMUSIC.FF fo=0x06b27c -> VA=0x{music_va:08x}")
# Find MessageBoxA IAT entry
print("\n=== MessageBoxA IAT ===")
mbox_name_off = exe.find(b'MessageBoxA\x00')
print(f" name at 0x{mbox_name_off:06x}")
mbox_iat_fo = None
for i in range(0x06ec00, 0x06fa00, 4):
thunk_va = struct.unpack_from('<I', exe, i)[0]
if thunk_va == 0:
continue
fo = va_to_fo(thunk_va)
if fo and 0 < fo < len(exe):
try:
nm = exe[fo+2:exe.index(b'\x00', fo+2)].decode('latin1', errors='replace')
if 'MessageBox' in nm:
mbox_iat_fo = i
iat_va = fo_to_va(i)
print(f" IAT thunk at fo=0x{i:06x} VA=0x{iat_va:08x}")
except:
pass
# Find all CALL [MessageBoxA]
if mbox_iat_fo:
iat_va = fo_to_va(mbox_iat_fo)
needle = b'\xff\x15' + struct.pack('<I', iat_va)
print(f"\n=== All CALL MessageBoxA sites ===")
for m in re.finditer(re.escape(needle), exe[0x400:0x067000]):
fo = 0x400 + m.start()
ctx = exe[max(0x400,fo-24):fo+8]
print(f" CALL @ 0x{fo:06x}: {ctx.hex(' ')}")
# Look at context around the flag-setter at 0x033dfc
print("\n=== Context around flag-setter at 0x033de0-0x033e40 ===")
for row in range(0, 0x70, 16):
h = ' '.join(f'{b:02x}' for b in exe[0x033de0+row:0x033de0+row+16])
a = ''.join(chr(b) if 0x20<=b<0x7f else '.' for b in exe[0x033de0+row:0x033de0+row+16])
print(f" {0x033de0+row:06x}: {h:<48} {a}")
# Find correct string VAs and search code
print("\n=== Correct string VAs and code refs ===")
fos = {'MUSIC.FF':0x06b27c,'INTRFACE.FF':0x06b288,'BIGFILE.DAT':0x06b3b8,
'STRDATA.DAT':0x06bee8,'WORLDS\\':0x06b2b8}
for label, fo in fos.items():
va = fo_to_va(fo)
if not va:
print(f" {label}: can't get VA")
continue
needle = struct.pack('<I', va)
refs = [0x400+m.start() for m in re.finditer(re.escape(needle), exe[0x400:0x067000])]
print(f" {label:<15} VA=0x{va:08x} refs={len(refs)}")
for r in refs[:4]:
ctx = exe[max(0x400,r-6):r+8]
print(f" 0x{r:06x}: {ctx.hex(' ')}")