havoc-remaster/tools/r2heap.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

47 lines
1.4 KiB
Python

import r2pipe, sys, io, struct
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
BINARIES = [
("HAVOC_NOCD.EXE", "patched"),
("HAVOC.EXE", "original"),
]
for fname, label in BINARIES:
print("\n" + "="*60)
print(" %s [%s]" % (fname, label))
print("="*60)
try:
r2 = r2pipe.open("Z:/Development/devl/Havoc/" + fname,
["-e", "bin.relocs.apply=true"])
# Binary info
info = r2.cmd("ij").strip()[:80]
print("Binary: %s..." % info[:120])
# Check value at 0x480964 (heap handle in .data)
v = r2.cmd("p4 @ 0x480964").strip()
print("\n[0x480964] (heap handle, raw bytes): %s" % v)
# Entry point
ep = r2.cmd("ie~[2]").strip()
print("Entry point VA: %s" % ep)
# Disassemble entry point
print("\n--- Entry point ---")
print(r2.cmd("pd 20 @ entry0"))
# Disassemble 0x464DCA (first non-trivial call in startup)
print("\n--- 0x464DCA (CRT startup call) ---")
print(r2.cmd("pd 30 @ 0x464DCA"))
# Disassemble 0x465BD3 (global ctor runner candidate)
print("\n--- 0x465BD3 ---")
print(r2.cmd("pd 30 @ 0x465BD3"))
# Search for writes to 0x480964
print("\n--- xrefs to 0x480964 ---")
print(r2.cmd("axt @ 0x480964"))
r2.quit()
except Exception as e:
print("ERROR: %s" % e)