havoc-remaster/docs/PATCHES.md
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

94 lines
5 KiB
Markdown

# HAVOC_NOCD.EXE — Binary Patches (run on modern Windows without CD)
`HAVOC_NOCD.EXE` is a patched copy of the original `HAVOC.EXE` (485,376 bytes, 32-bit PE i386,
DirectDraw + DirectSound). The patches let it run from a hard-drive install on Windows 11 via
[dgVoodoo2](http://dege.freeweb.hu/) (DirectDraw wrapper, windowed mode), with no CD mounted
and no admin rights.
Do **not** commit `HAVOC_NOCD.EXE` or the original `HAVOC.EXE` (copyright).
## Section layout / file-offset (FO) to virtual-address (VA) mapping
| Section | Raw FO start | RVA | FO → VA formula |
|---|---|---|---|
| `.text` | 0x00400 | 0x01000 | `VA = FO + 0x400C00` |
| `.data` | 0x69400 | 0x7C000 | `VA = FO + 0x412C00` |
| `.rdata` | 0x67000 | 0x79000 | `VA = FO - 0x67000 + 0x79000 + 0x400000` |
| `.idata` | 0x6EC00 | 0x82000 | `VA = FO + 0x413400` |
`.text` VirtualSize was extended from 0x66bbd to 0x66c00 (PE header FO 0x180) so the Patch-V
code cave at VA 0x467bce is mapped into memory.
## Patch table
| FO | VA | Bytes (→) | Purpose |
|---|---|---|---|
| 0x00180 | (PE hdr) | `bd6b`→`006c` | Extend `.text` VirtualSize to cover Patch-V cave |
| 0x094fd | 0x40a0fd | `66 b8 01 00`→`66 33 c0 90` | **Patch S**: CreateSoundBuffer fail returns AX=0 (continue w/o sound) |
| 0x20277 | 0x420e77 | `75`→`eb` | Force a Jcc branch (skip) |
| 0x29613 | 0x42a213 | `8844242c`→`90909090` | **Patch D**: NOP drive-letter overwrite (INTRFACE) |
| 0x29623 | 0x42a223 | `88442410`→`90909090` | **Patch D**: NOP drive-letter overwrite (MUSIC) |
| 0x296cc | 0x42a2cc | `7c`→`eb` | **Patch Q**: skip check |
| 0x29b02 | 0x42a702 | 18 bytes → NOP | Remove drive-type compare/branches |
| 0x29b38 | 0x42a738 | `75`→`eb` | Drive-check skip |
| 0x29b80 | 0x42a780 | `74`→`eb` | CD-check skip |
| 0x29bb0 | 0x42a7b0 | `81ec6c010000`→`b001c39090 90` | NOCD stub = `MOV AL,1; RET` |
| 0x29d63 | 0x42a963 | `75`→`eb` | Extra CD skip |
| 0x29e10 | 0x42aa10 | `74`→`eb` | CD skip 2 |
| **0x29a2b** | **0x42a62b** | `8b442414 85c0 8b442410 752a 85c0``8b442410 85c0 754e 8b442414 eb48` | **Copy-protection / GRAFIX fix** (see below) |
| 0x2cd70 | 0x42d970 | `81`→`c3` | **Patch N3**: `show_error_dialog` returns immediately (silence popups) |
| 0x2cf2e | 0x42db2e | `74`→`eb 16` | Corrupt/unavailable bypass |
| 0x30393 | 0x430f93 | `e888c9ffff`→NOP | NOP a validation CALL |
| 0x42514 | 0x443114 | `b8010000`→`e9b54a02` | **Patch V**: JMP to vtable-setter code cave |
| 0x66fce | 0x467bce | cave | **Patch V** cave: sets vtable=1, returns EAX=1 |
| 0x6b27c | 0x47de7c | `C:\MUSIC.FF`→`MUSIC.FF` | Strip drive prefix |
| 0x6b288 | 0x47de88 | `C:\INTRFACE.FF`→`INTRFACE.FF` | Strip drive prefix |
| 0x6b2b8 | 0x47deb8 | `C:\WORLDS\`→`WORLDS\` | Strip drive prefix |
| 0x6b390 | 0x47df90 | `C:\WORLDS\LAND0101.DAT`→`WORLDS\...` | Strip drive prefix |
| 0x6b3a8 | 0x47dfa8 | `C:\HAVOCDAT.DAT`→`HAVOCDAT.DAT` | Strip drive prefix |
| 0x6b3b8 | 0x47dfb8 | `C:\BIGFILE.DAT`→`BIGFILE.DAT` | Strip drive prefix |
## The copy-protection / GRAFIX crash (the "black window then closes" bug)
**Symptom:** dgVoodoo window appears for ~1 second, all black, then the process disappears.
**Actual cause:** an ACCESS_VIOLATION (NULL-pointer read) at VA **0x444E6A** during init — found
by parsing the WER minidump in `%LOCALAPPDATA%\CrashDumps` (the crash call stack shows
`0x434073``0x407CCB`, i.e. inside `WinMain → 0x434040 init`).
The WORLDS\ data-file loader `0x42a480` opens each file (`GRAFIX00-03.DAT`, `LAND*`, `MAP*`,
`STUF*`) from **two** locations: the local `WORLDS\<name>` path and a path built from the CD
drive letter in `HAVOC.INI` (`[SETUP] DRIVE=D:\`). Its return logic returns a valid stream
**only when the local open fails and the CD-drive open succeeds** — a copy-protection check that
assumes data lives on the CD. With the files present locally and no `D:` drive, it returns NULL;
the caller `0x444db0` (GRAFIX loader) dereferences that NULL without checking → crash.
**Fix (FO 0x29a2b):** rewrite the return decision to prefer the successfully-opened local object:
```
0x42a62b: mov eax,[esp+0x10] ; local WORLDS\ stream object
0x42a62f: test eax,eax
0x42a631: jnz 0x42a681 ; if local open OK, return it
0x42a633: mov eax,[esp+0x14] ; else return the drive object
0x42a637: jmp 0x42a681
```
After this patch the game runs (window title "HAVOC(tm) by Reality Bytes", responsive, main
loop executing) instead of crashing.
## Reproducing / testing
The crash is launch-environment dependent (it reproduces when launched normally but not under a
debugger). Reliable native run:
```bat
:: run_probe.bat
cd /d Z:\Development\devl\Havoc
.\HAVOC_NOCD.EXE
```
Notes:
- `NoDefaultCurrentDirectoryInExePath` may be set — invoke the exe with a `.\` prefix.
- Launching via MSYS/Git-Bash segfaults (exit 139) and does not produce a WER dump; use `cmd`.
- Minidumps can be parsed with plain Python (no cdb/windbg needed) — see the exception stream
(type 6) for the faulting EIP and the memory streams (5/9) to walk the stack.