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

137 lines
6.8 KiB
Python

import ctypes, sys, io
from ctypes import wintypes, byref, sizeof, c_char
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
k = ctypes.windll.kernel32
DEBUG_ONLY_THIS_PROCESS = 0x00000002
DBG_CONTINUE = 0x00010002
DBG_EXCEPTION_NOT_HANDLED = 0x80010001
EXCEPTION_BREAKPOINT = 0x80000003
EXCEPTION_ACCESS_VIOLATION = 0xC0000005
CREATE_PROCESS_DEBUG_EVENT = 3
EXCEPTION_DEBUG_EVENT = 1
LOAD_DLL_DEBUG_EVENT = 6
CREATE_THREAD_DEBUG_EVENT = 2
CONTEXT_FULL = 0x00010007 # WOW64 x86 CONTEXT_FULL (CONTROL|INTEGER|SEGMENTS)
BP_OPEN = 0x00439380 # stream open method entry (thiscall, ecx=this, path at [this+4])
class STARTUPINFO(ctypes.Structure):
_fields_=[("cb",wintypes.DWORD),("lpReserved",wintypes.LPWSTR),("lpDesktop",wintypes.LPWSTR),
("lpTitle",wintypes.LPWSTR),("dwX",wintypes.DWORD),("dwY",wintypes.DWORD),
("dwXSize",wintypes.DWORD),("dwYSize",wintypes.DWORD),("dwXCountChars",wintypes.DWORD),
("dwYCountChars",wintypes.DWORD),("dwFillAttribute",wintypes.DWORD),("dwFlags",wintypes.DWORD),
("wShowWindow",wintypes.WORD),("cbReserved2",wintypes.WORD),("lpReserved2",ctypes.c_void_p),
("hStdInput",wintypes.HANDLE),("hStdOutput",wintypes.HANDLE),("hStdError",wintypes.HANDLE)]
class PROCESS_INFORMATION(ctypes.Structure):
_fields_=[("hProcess",wintypes.HANDLE),("hThread",wintypes.HANDLE),
("dwProcessId",wintypes.DWORD),("dwThreadId",wintypes.DWORD)]
class EXCEPTION_RECORD(ctypes.Structure):
_fields_=[("ExceptionCode",wintypes.DWORD),("ExceptionFlags",wintypes.DWORD),
("ExceptionRecord",ctypes.c_void_p),("ExceptionAddress",ctypes.c_void_p),
("NumberParameters",wintypes.DWORD),("ExceptionInformation",ctypes.c_void_p*15)]
class EXCEPTION_DEBUG_INFO(ctypes.Structure):
_fields_=[("ExceptionRecord",EXCEPTION_RECORD),("dwFirstChance",wintypes.DWORD)]
class DEBUG_EVENT(ctypes.Structure):
class _U(ctypes.Union):
_fields_=[("Exception",EXCEPTION_DEBUG_INFO),("raw",c_char*160)]
_fields_=[("dwDebugEventCode",wintypes.DWORD),("dwProcessId",wintypes.DWORD),
("dwThreadId",wintypes.DWORD),("u",_U)]
# WOW64_CONTEXT x86
class WOW64_FLOATING_SAVE_AREA(ctypes.Structure):
_fields_=[("ControlWord",wintypes.DWORD),("StatusWord",wintypes.DWORD),("TagWord",wintypes.DWORD),
("ErrorOffset",wintypes.DWORD),("ErrorSelector",wintypes.DWORD),("DataOffset",wintypes.DWORD),
("DataSelector",wintypes.DWORD),("RegisterArea",c_char*80),("Cr0NpxState",wintypes.DWORD)]
class WOW64_CONTEXT(ctypes.Structure):
_fields_=[("ContextFlags",wintypes.DWORD),("Dr0",wintypes.DWORD),("Dr1",wintypes.DWORD),
("Dr2",wintypes.DWORD),("Dr3",wintypes.DWORD),("Dr6",wintypes.DWORD),("Dr7",wintypes.DWORD),
("FloatSave",WOW64_FLOATING_SAVE_AREA),("SegGs",wintypes.DWORD),("SegFs",wintypes.DWORD),
("SegEs",wintypes.DWORD),("SegDs",wintypes.DWORD),("Edi",wintypes.DWORD),("Esi",wintypes.DWORD),
("Ebx",wintypes.DWORD),("Edx",wintypes.DWORD),("Ecx",wintypes.DWORD),("Eax",wintypes.DWORD),
("Ebp",wintypes.DWORD),("Eip",wintypes.DWORD),("SegCs",wintypes.DWORD),("EFlags",wintypes.DWORD),
("Esp",wintypes.DWORD),("SegSs",wintypes.DWORD),("ExtendedRegisters",c_char*512)]
app = b"Z:\\Development\\devl\\Havoc\\HAVOC_NOCD.EXE"
cwd = b"Z:\\Development\\devl\\Havoc"
si=STARTUPINFO(); si.cb=sizeof(si)
pi=PROCESS_INFORMATION()
ok=k.CreateProcessA(app, None, None, None, False, DEBUG_ONLY_THIS_PROCESS, None, cwd, byref(si), byref(pi))
if not ok:
print("CreateProcess failed", k.GetLastError()); sys.exit(1)
print("launched pid", pi.dwProcessId)
hProc=None; orig=None; threads={}
def rpm(addr,n):
buf=(c_char*n)(); rd=ctypes.c_size_t(0)
k.ReadProcessMemory(hProc, ctypes.c_void_p(addr), buf, n, byref(rd))
return bytes(buf[:rd.value])
def wpm(addr,data):
wr=ctypes.c_size_t(0)
return k.WriteProcessMemory(hProc, ctypes.c_void_p(addr), data, len(data), byref(wr))
EXIT_PROCESS_DEBUG_EVENT=5; OUTPUT_DEBUG_STRING_EVENT=8
import time
evt=DEBUG_EVENT(); hit=0; done=False; dllcount=0
t0=time.time()
while not done:
if time.time()-t0 > 30:
print("=== 30s budget: dumping thread states ===")
for tid,hThr in threads.items():
ctx=WOW64_CONTEXT(); ctx.ContextFlags=CONTEXT_FULL
if k.Wow64GetThreadContext(hThr, byref(ctx)):
b=rpm(ctx.Eip,8)
print(" tid %d EIP=0x%08X bytes=%s"%(tid, ctx.Eip, b.hex()))
k.TerminateProcess(pi.hProcess,0); break
if not k.WaitForDebugEvent(byref(evt), 3000):
continue
code=evt.dwDebugEventCode; cont=DBG_CONTINUE
if code==CREATE_PROCESS_DEBUG_EVENT:
hProc=pi.hProcess
threads[evt.dwThreadId]=pi.hThread
orig=rpm(BP_OPEN,1)
okw=wpm(BP_OPEN, b"\xCC")
chk=rpm(BP_OPEN,1)
print("set BP at 0x%X (orig=%s wpm_ok=%s now=%s)"%(BP_OPEN, orig.hex(), bool(okw), chk.hex()))
elif code==EXIT_PROCESS_DEBUG_EVENT:
print("PROCESS EXITED code=0x%X"%(evt.u.Exception.ExceptionRecord.ExceptionCode)); done=True
elif code==LOAD_DLL_DEBUG_EVENT:
dllcount+=1
elif code==CREATE_THREAD_DEBUG_EVENT:
# track new thread handle: reopen by tid
h=k.OpenThread(0x1FFFFF, False, evt.dwThreadId)
if h: threads[evt.dwThreadId]=h
elif code==EXCEPTION_DEBUG_EVENT:
er=evt.u.Exception.ExceptionRecord
exc=er.ExceptionCode & 0xffffffff
addr=er.ExceptionAddress or 0
if exc==EXCEPTION_BREAKPOINT and addr==BP_OPEN:
hThr=threads.get(evt.dwThreadId)
ctx=WOW64_CONTEXT(); ctx.ContextFlags=CONTEXT_FULL
k.Wow64GetThreadContext(hThr, byref(ctx))
this=ctx.Ecx
pathptr=this+4
raw=rpm(pathptr,160)
s=raw.split(b"\x00")[0]
hit+=1
print("BP hit #%d: this=0x%08X path@0x%08X = %r"%(hit,this,pathptr,s.decode('latin1')))
# restore and step back so it re-executes original
wpm(BP_OPEN, orig)
ctx.Eip=BP_OPEN
ctx.ContextFlags=CONTEXT_FULL
k.Wow64SetThreadContext(hThr, byref(ctx))
if hit>=3:
print("captured enough; terminating child")
k.TerminateProcess(hProc,0); done=True
elif exc==EXCEPTION_ACCESS_VIOLATION:
hThr=threads.get(evt.dwThreadId)
ctx=WOW64_CONTEXT(); ctx.ContextFlags=CONTEXT_FULL
k.Wow64GetThreadContext(hThr, byref(ctx))
print("ACCESS_VIOLATION eip=0x%08X eax=0x%08X esi=0x%08X ecx=0x%08X (firstchance=%d)"%(
ctx.Eip, ctx.Eax, ctx.Esi, ctx.Ecx, evt.u.Exception.dwFirstChance))
k.TerminateProcess(hProc,0); done=True
elif exc==EXCEPTION_BREAKPOINT:
pass # initial ntdll bp
else:
cont=DBG_EXCEPTION_NOT_HANDLED
k.ContinueDebugEvent(evt.dwProcessId, evt.dwThreadId, cont)
print("debugger done")