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
137 lines
6.8 KiB
Python
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")
|