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
This commit is contained in:
parent
534c41af73
commit
27174a1c79
51 changed files with 3140 additions and 2 deletions
18
.gitignore
vendored
18
.gitignore
vendored
|
|
@ -9,12 +9,30 @@
|
|||
*.MAP
|
||||
*.ICO
|
||||
*.INF
|
||||
*.INI
|
||||
*.ini
|
||||
.windows-serial
|
||||
BIGFILE.DAT
|
||||
WORLDS/
|
||||
DIRECTX/
|
||||
LICENSE.TXT
|
||||
|
||||
# --- CD images (copyright) ---
|
||||
*.cue
|
||||
*.bin
|
||||
tmp_cd/
|
||||
|
||||
# --- Third-party (dgVoodoo2 DirectDraw wrapper) - not ours to redistribute ---
|
||||
dgVoodoo.conf
|
||||
dgVoodooCpl.exe
|
||||
ddraw.dll
|
||||
DSETUP*.DLL
|
||||
|
||||
# --- Ghidra: install is third-party; project + decompile output embed the binary ---
|
||||
ghidra/
|
||||
tools/ghidra_*/
|
||||
tools/ghidra_decompile.txt
|
||||
|
||||
# --- Derived audio / extracted assets (also copyrighted) ---
|
||||
tools/out_*/
|
||||
tools/Havoc_*/
|
||||
|
|
|
|||
|
|
@ -86,6 +86,15 @@ salmon background) revealed only 3 differing entries out of 256 -- indices 0, 1,
|
|||
Length-prefixed strings for the screen's UI labels. First byte = string length, then UTF-8
|
||||
text. Example: `STR#797C` contains all main menu button labels and the copyright notice.
|
||||
|
||||
Two special-purpose string tables live in `INTRFACE.FF` at fixed IDs:
|
||||
|
||||
| ID | Content |
|
||||
|---|---|
|
||||
| `7D02` | Pickup banner text -- all item and weapon pickup notification strings |
|
||||
| `7D03` | HUD active weapon name display strings |
|
||||
|
||||
See `docs/WEAPONS.md` for the complete weapon list derived from these tables.
|
||||
|
||||
### Identified screens (640x480 full-screen bitmaps)
|
||||
|
||||
| ID | Content (from STR# strings) |
|
||||
|
|
@ -117,7 +126,21 @@ text. Example: `STR#797C` contains all main menu button labels and the copyright
|
|||
|
||||
## World data (`WORLDS/`) — TODO (3D terrain)
|
||||
|
||||
Per-level sets `<NN>` = 01..06 plus `N`/`N2` variants (biomes: desert, snow/ice, lava, ocean).
|
||||
Per-level sets `<NN>` = 01..06. Six worlds confirmed from EXE string table + player memory:
|
||||
|
||||
| File prefix | World name | Biome |
|
||||
|---|---|---|
|
||||
| `LAND01xx` | Badlands | Desert |
|
||||
| `LAND02xx` | Fallout | Ice/Snow |
|
||||
| `LAND03xx` | Tyrak | Alien |
|
||||
| `LAND04xx` | Wasteland | Desert (harder) |
|
||||
| `LAND05xx` | Malterra | Ice (harder) |
|
||||
| `LAND06xx` | Overlord | Alien (harder) |
|
||||
|
||||
Level suffix codes within each world: `01`=Level 1, `02`=Level 2, `03`=Level 3, `04`=Boss, `0B`=Bonus.
|
||||
Network variants use `0N`, `1N`, `2N` prefixes for the three biomes at Easy/Medium/Hard.
|
||||
|
||||
Music track ID is stored at **offset 18 as big-endian uint16** in each `LAND*.DAT` file.
|
||||
|
||||
| Pattern | Guess | Status |
|
||||
|---|---|---|
|
||||
|
|
|
|||
|
|
@ -180,7 +180,63 @@ an extraction accuracy issue.
|
|||
|
||||
---
|
||||
|
||||
## 7. What's next
|
||||
## 7. Music-to-level mapping (from LAND headers)
|
||||
|
||||
Each `LAND<world><level>.DAT` file stores the music track ID at **offset 18 as a
|
||||
big-endian uint16** (the same big-endian convention as the PLST payloads -- a pattern
|
||||
throughout this codebase).
|
||||
|
||||
Discovery process: searched `HAVOC.EXE` for all `MOV WORD PTR [addr], <plst_id>` instructions
|
||||
(`66 C7 05` prefix). Found 9 assignments, all writing to address `0x00475C58` (the current
|
||||
music variable), but only covering 4 unique track IDs. The remaining 13 tracks had to come
|
||||
from data. Scanning the LAND headers for known PLST IDs at each uint16-aligned offset revealed
|
||||
offset 18 as the music field -- every level matched a known PLST ID.
|
||||
|
||||
Two tracks (`1771`, `1B59`) have zero references in either `HAVOC.EXE` or `LAUNCHER.EXE`
|
||||
and are used by no level file. They are almost certainly cut/multiplayer-lobby tracks.
|
||||
Track `4269` is hardcoded in `HAVOC.EXE` near the intro sequence. Track `4651` (Credits)
|
||||
and `0BB9` (Badlands1) appear in the launcher/menu code paths.
|
||||
|
||||
### Complete level music table
|
||||
|
||||
| World | Level 1 | Level 2 | Level 3 | Boss | Bonus |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 Badlands | Badlands1 (0BB9) | Badlands2 (1F41) | Badlands3 (1389) | BossBadlands (07D1) | Bonus (0FA1) |
|
||||
| 2 Fallout | Fallout1 (32C9) | Fallout2 (3A99) | Fallout3 (36B1) | BossFallout (3E81) | Badlands3 (1389) |
|
||||
| 3 Tyrak | Tyrak1 (2329) | Tyrak2 (2AF9) | Tyrak3 (2EE1) | BossTyrak (2711) | Bonus (0FA1) |
|
||||
| 4 Wasteland | Badlands3 (1389) | Badlands2 (1F41) | BossBadlands (07D1) | Badlands1 (0BB9) | Badlands3 (1389) |
|
||||
| 5 Malterra | Fallout1 (32C9) | Fallout2 (3A99) | Fallout3 (36B1) | BossFallout (3E81) | Bonus (0FA1) |
|
||||
| 6 Overlord | Tyrak3 (2EE1) | Tyrak2 (2AF9) | Tyrak1 (2329) | BossTyrak (2711) | Badlands3 (1389) |
|
||||
|
||||
Other tracks: Intro (4269, EXE intro), Credits (4651), Multiplayer1 (1771, unused),
|
||||
Multiplayer2 (1B59, unused).
|
||||
|
||||
**Verification note:** Badlands3 (1389) assembles correctly from its 5 segments (52.5s total,
|
||||
20-step PLST) but is notably shorter than other tracks. Tyrak1 is a similar length (53.3s) so
|
||||
this may be intentional. Cross-reference against an emulated playthrough (dgVoodoo2 or Win98 VM)
|
||||
to confirm in-level behavior and loop point.
|
||||
|
||||
Worlds 2 and 5 share identical music. Worlds 3 and 6 use the same tracks in reversed order.
|
||||
|
||||
### World names (from EXE string table + player memory)
|
||||
|
||||
Found at offset `0x06A7C4` in `HAVOC.EXE`, six NUL-terminated world names stored in reverse world order
|
||||
(`Overlord, Wasteland, Malterra, Tyrak, Fallout, Badlands`):
|
||||
|
||||
| World | Name | Biome |
|
||||
|---|---|---|
|
||||
| 1 | Badlands | Desert |
|
||||
| 2 | Fallout | Ice/Snow |
|
||||
| 3 | Tyrak | Alien |
|
||||
| 4 | Wasteland | Desert (harder) |
|
||||
| 5 | Malterra | Ice (harder) |
|
||||
| 6 | Overlord | Alien (harder) |
|
||||
|
||||
The reverse storage order is consistent with a reverse-indexed lookup table (index 0 = highest world).
|
||||
Network play confirms the three biome names separately: `Badlands Net`, `Fallout Net`, `Tyrak Net`.
|
||||
The ice/snow biome is named "Fallout" in the EXE despite its visual aesthetic.
|
||||
|
||||
## 8. What's next
|
||||
|
||||
- `SOUND.FF`: same format as MUSIC.FF, just raw SFX WAVs (no playlists)
|
||||
- `WORLDS/`: terrain data -- `MAP*.MAP`, `LAND*.DAT`, `GRAFIX*.DAT`, `STUF*.DAT`
|
||||
|
|
|
|||
94
docs/PATCHES.md
Normal file
94
docs/PATCHES.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# 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.
|
||||
94
docs/WEAPONS.md
Normal file
94
docs/WEAPONS.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Havoc weapon system
|
||||
|
||||
Source: `STR#7D02.dat` (pickup banner text) and `STR#7D03.dat` (HUD active weapon names),
|
||||
extracted from `INTRFACE.FF`. True class structure and game logic to be confirmed via Ghidra
|
||||
decompilation of `HAVOC.EXE`.
|
||||
|
||||
Player memory: 6 weapon classes each for primary (infinite ammo) and secondary (limited ammo).
|
||||
|
||||
---
|
||||
|
||||
## Primary weapons (infinite ammo)
|
||||
|
||||
Weapons not appearing in the pickup table are assumed to be starting/default weapons.
|
||||
Upgrades within a class are listed tier by tier.
|
||||
|
||||
| Class | Tier 1 (start) | Tier 2 | Tier 3 | Tier 4 |
|
||||
|---|---|---|---|---|
|
||||
| Laser | Laser | Improved Laser | ElectroLaser | High-Power ElectroLaser |
|
||||
| Disc | Disc | Improved Disc | Disc Gun Doubler | High-Power Disc / Fin-Stabilized Disc |
|
||||
| Shockwave | Shockwave | Radial Shockwave | | |
|
||||
| Fireball | Fireball | High-Velocity Fireball | | |
|
||||
| Plasma | Plasma | High-Energy Plasma | | |
|
||||
| VARG | VARG Omicron | VARG Omicron High-Power | VARG Omicron Focused | VARG Photon / VARG Photon High-Energy |
|
||||
|
||||
Notes:
|
||||
- "Laser Doubler" pickup grants a second laser barrel (doubles fire rate/output, not a separate class).
|
||||
- "Disc" and "Improved Disc" do not appear as pickups -- probably starting weapons.
|
||||
- "Plasma" does not appear as a pickup -- probably starting or granted via progression.
|
||||
- VARG (Variable Armament Resonance Gun?) is the highest-tier class, with two sub-branches (Omicron / Photon).
|
||||
|
||||
---
|
||||
|
||||
## Secondary weapons (limited ammo)
|
||||
|
||||
Cheat code `mmm` gives the best Heatseeker class weapon. Cheat `aaa` = max ammo for all secondaries.
|
||||
|
||||
| Class | Base weapon | Upgrade(s) | Ammo pickup |
|
||||
|---|---|---|---|
|
||||
| Rocket | Rocket | Rocket Repeater, Rocket Doubler | Rocket Ammo +5 / +10 |
|
||||
| Cruise Missile | Cruise Missile | Afterburner Cruise | Cruise Missile Ammo +5 |
|
||||
| Mortar | Mortar | High-Impact Mortar | Mortar Ammo +5 |
|
||||
| Mine | Mine Dropper | Thermite Mine Dropper, Proximity Mine Dropper | Mines +5 |
|
||||
| Heatseeker | Heatseeker | Afterburner Heatseeker | Heatseeker Ammo +5 |
|
||||
| EMP | EMP Cannon | Improved EMP Cannon | EMP Cannon Ammo +5 |
|
||||
| Fusion | Fusion Cannon | Catalyst-Aug Fusion Cannon | Fusion Cannon Ammo +5 |
|
||||
| LGM | Laser Guided Missile | Thermal LGM, ElectroCharge LGM | LGM Ammo +5 |
|
||||
| Warhead | Warhead | Multi-Warhead | Warhead Ammo +5 |
|
||||
| Sweep Dropper | Sweep Dropper - Laser | Sweep Dropper - Photon, Sweep Dropper - Flame | Sweep Dropper Ammo +5 |
|
||||
| Terraformer | Terraformer | Enhanced Terraformer | Terraformer Ammo +5 |
|
||||
| Phase Missile | Phase Missile | Dual-Phase Missile | Phase Missile Ammo +5 |
|
||||
| Catapult | Ion Catapult | Meson Catapult | Catapult Ammo +5 |
|
||||
| Disruptor | Disruptor Cannon | High-Phase Disruptor Cannon | Disruptor Cannon Ammo +5 |
|
||||
|
||||
Note: The data shows 14 distinct secondary weapon lines, but player memory is 6 classes.
|
||||
Likely some of these are grouped into the same class slot in the UI (e.g. Rocket + Cruise Missile
|
||||
may share one slot, LGM + Phase Missile another). Ghidra will resolve the actual slot count.
|
||||
|
||||
Unaccounted-in-pickups (appear only in HUD list, may be starting secondaries):
|
||||
- `Pulse`
|
||||
- `Frequency-Modulated Pulse`
|
||||
- `Particle Accelerator`
|
||||
- `High-Velocity Particle`
|
||||
|
||||
---
|
||||
|
||||
## Special pickups (non-weapon)
|
||||
|
||||
| Item | Effect |
|
||||
|---|---|
|
||||
| Gate Key | Required to open the exit gate and complete the level |
|
||||
| Shields +5 / +10 / +20 / +30 / +50 / +100 | Restores shield hitpoints |
|
||||
| Shields Restored | Full shield restore |
|
||||
| Ultrashield | Temporary invulnerability (or large instant shield restore) |
|
||||
| Shield Max Increased +10 | Permanently raises shield capacity |
|
||||
| Freeze Enemies | Temporarily freezes all enemies on screen |
|
||||
| Invisibility | Temporary cloaking |
|
||||
| Targeting Enhancement | Improves lock-on/targeting |
|
||||
| Bonus Vehicle | Extra life |
|
||||
| Installing Mass-Energy Weapon | Special weapon install event |
|
||||
| Upgrading Mass-Energy Weapon | Upgrade event for the above |
|
||||
|
||||
---
|
||||
|
||||
## Cheat codes
|
||||
|
||||
All confirmed working in demo and retail:
|
||||
|
||||
| Code | Effect |
|
||||
|---|---|
|
||||
| `mmm` | Best Heatseeker class weapon (Afterburner Heatseeker) |
|
||||
| `vvv` | Bonus Vehicle / extra life |
|
||||
| `sss` | Shield restore |
|
||||
| `hhh` | Health restore |
|
||||
| `aaa` | Max ammo for all secondaries |
|
||||
4
run_and_log.bat
Normal file
4
run_and_log.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
@echo off
|
||||
cd /d Z:\Development\devl\Havoc
|
||||
HAVOC_NOCD.EXE
|
||||
echo %ERRORLEVEL% > exit_code.txt
|
||||
6
run_probe.bat
Normal file
6
run_probe.bat
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
@echo off
|
||||
cd /d Z:\Development\devl\Havoc
|
||||
echo cwd=%CD% > probe.txt
|
||||
.\HAVOC_NOCD.EXE
|
||||
echo exitcode=%ERRORLEVEL% >> probe.txt
|
||||
echo done >> probe.txt
|
||||
49
tools/analyze2.py
Normal file
49
tools/analyze2.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
def show(start, length):
|
||||
for row in range(start, start + length, 16):
|
||||
chunk = data[row:row+16]
|
||||
ann = []
|
||||
for j in range(len(chunk)):
|
||||
f = row + j
|
||||
try:
|
||||
if data[f] == 0xe8 and f+5 <= len(data):
|
||||
rel = struct.unpack_from('<i', data, f+1)[0]
|
||||
tgt = (FO2VA(f+5) + rel) & 0xFFFFFFFF
|
||||
ann.append('+%d:CALL_0x%x' % (j, tgt))
|
||||
if data[f] == 0xff and data[f+1] == 0x15 and f+6 <= len(data):
|
||||
iat = struct.unpack_from('<I', data, f+2)[0]
|
||||
d = {
|
||||
0x4822f4:'DirectDrawCreate', 0x4822fc:'DirectSoundCreate',
|
||||
0x48240c:'ExitProcess', 0x48248c:'MessageBoxA',
|
||||
0x4823cc:'LocalAlloc', 0x482370:'VirtualAlloc',
|
||||
0x482390:'CreateFileA', 0x4824bc:'PeekMessageA',
|
||||
0x482460:'CreateWindowExA', 0x48246c:'ShowWindow',
|
||||
0x482498:'RegisterClassA',
|
||||
}
|
||||
ann.append('+%d:[%s]' % (j, d.get(iat, 'iat_0x%x' % iat)))
|
||||
if data[f] == 0xc3: ann.append('+%d:RET' % j)
|
||||
if data[f] == 0xc2 and f+3 <= len(data):
|
||||
n = struct.unpack_from('<H', data, f+1)[0]
|
||||
ann.append('+%d:RET%d' % (j, n))
|
||||
if data[f] == 0xc7 and data[f+1] in (0x01,0x06,0x07) and f+6 <= len(data):
|
||||
val = struct.unpack_from('<I', data, f+2)[0]
|
||||
if 0x479000 <= val <= 0x47BFFF:
|
||||
ann.append('+%d:VTABLE=0x%x' % (j, val))
|
||||
except Exception:
|
||||
pass
|
||||
line = 'FO 0x%05x VA 0x%07x: %s' % (row, FO2VA(row), ' '.join('%02x' % b for b in chunk))
|
||||
if ann:
|
||||
line += ' [%s]' % ','.join(ann)
|
||||
print(line)
|
||||
|
||||
fn_fo = VA2FO(0x43A960)
|
||||
print("=== function 0x43A960 (FO 0x%05x) ===" % fn_fo)
|
||||
show(fn_fo, 128)
|
||||
print()
|
||||
print("=== function 0x40ADF0 (called AFTER pool allocator, FO 0x%05x) ===" % VA2FO(0x40ADF0))
|
||||
show(VA2FO(0x40ADF0), 80)
|
||||
66
tools/analyze3.py
Normal file
66
tools/analyze3.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
def show(start, length):
|
||||
for row in range(start, start + length, 16):
|
||||
chunk = data[row:row+16]
|
||||
ann = []
|
||||
for j in range(len(chunk)):
|
||||
f = row + j
|
||||
try:
|
||||
if data[f] == 0xe8 and f+5 <= len(data):
|
||||
rel = struct.unpack_from('<i', data, f+1)[0]
|
||||
tgt = (FO2VA(f+5) + rel) & 0xFFFFFFFF
|
||||
ann.append('+%d:CALL_0x%x' % (j, tgt))
|
||||
if data[f] == 0xff and data[f+1] == 0x15 and f+6 <= len(data):
|
||||
iat = struct.unpack_from('<I', data, f+2)[0]
|
||||
d = {
|
||||
0x4822f4:'DirectDrawCreate', 0x4822fc:'DirectSoundCreate',
|
||||
0x48240c:'ExitProcess', 0x48248c:'MessageBoxA',
|
||||
0x4823cc:'LocalAlloc', 0x482370:'VirtualAlloc',
|
||||
0x482390:'CreateFileA', 0x4823B0:'HeapAlloc',
|
||||
0x482438:'HeapCreate',
|
||||
}
|
||||
ann.append('+%d:[%s]' % (j, d.get(iat, 'iat_0x%x' % iat)))
|
||||
if data[f] == 0xa3 and f+5 <= len(data):
|
||||
va = struct.unpack_from('<I', data, f+1)[0]
|
||||
if va == 0x480964:
|
||||
ann.append('+%d:MOV[0x480964],EAX' % j)
|
||||
if data[f] == 0xc3: ann.append('+%d:RET' % j)
|
||||
if data[f] == 0xc2 and f+3 <= len(data):
|
||||
n = struct.unpack_from('<H', data, f+1)[0]
|
||||
ann.append('+%d:RET%d' % (j, n))
|
||||
except Exception:
|
||||
pass
|
||||
line = 'FO 0x%05x VA 0x%07x: %s' % (row, FO2VA(row), ' '.join('%02x' % b for b in chunk))
|
||||
if ann:
|
||||
line += ' [%s]' % ','.join(ann)
|
||||
print(line)
|
||||
|
||||
# Search for all writes to [0x480964]
|
||||
print("=== All writes to [0x480964] (heap handle) ===")
|
||||
for fo in range(0, 0x70000):
|
||||
# MOV [0x480964], EAX
|
||||
if data[fo] == 0xa3 and fo+5 <= len(data):
|
||||
va = struct.unpack_from('<I', data, fo+1)[0]
|
||||
if va == 0x480964:
|
||||
ctx = data[fo-16:fo+16]
|
||||
ctx_hex = ' '.join('%02x'%b for b in ctx)
|
||||
print(' FO 0x%05x VA 0x%07x: %s' % (fo, FO2VA(fo), ctx_hex))
|
||||
# MOV [0x480964], r32 (89 05 ...)
|
||||
if data[fo] == 0x89 and data[fo+1] == 0x05 and fo+6 <= len(data):
|
||||
va = struct.unpack_from('<I', data, fo+2)[0]
|
||||
if va == 0x480964:
|
||||
ctx = data[fo-8:fo+16]
|
||||
print(' FO 0x%05x (89 05): %s' % (fo, ' '.join('%02x'%b for b in ctx)))
|
||||
|
||||
print()
|
||||
print("=== function 0x4633C7 (underlying alloc, FO 0x%05x) ===" % VA2FO(0x4633C7))
|
||||
show(VA2FO(0x4633C7), 80)
|
||||
|
||||
print()
|
||||
print("=== function 0x43A960: EDX vs ECX setup (what initializes them) ===")
|
||||
show(VA2FO(0x43A960), 96)
|
||||
60
tools/analyze4.py
Normal file
60
tools/analyze4.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO_data = lambda va: va - 0x47C000 + 0x69400 # .data section
|
||||
|
||||
# Check the value at [0x480964]
|
||||
fo = VA2FO_data(0x480964)
|
||||
val = struct.unpack_from('<I', data, fo)[0]
|
||||
print("[0x480964] at FO 0x%05x = 0x%08x" % (fo, val))
|
||||
|
||||
# Check surrounding .data at 0x480960
|
||||
print("Data at 0x480960 area (FO 0x%05x):" % VA2FO_data(0x480960))
|
||||
for i in range(0, 32, 4):
|
||||
fo2 = VA2FO_data(0x480960 + i)
|
||||
v = struct.unpack_from('<I', data, fo2)[0]
|
||||
print(" [0x%x] at FO 0x%05x = 0x%08x" % (0x480960 + i, fo2, v))
|
||||
|
||||
# Also check what callers of 0x4633BE pass as the heap handle
|
||||
print()
|
||||
print("=== 0x4633BE wrapper (FO 0x627be): reads [0x480964] ===")
|
||||
fo = 0x627be
|
||||
chunk = data[fo:fo+16]
|
||||
print("FO 0x%05x: %s" % (fo, ' '.join('%02x'%b for b in chunk)))
|
||||
|
||||
# Find callers of 0x4633BE
|
||||
print()
|
||||
print("=== All callers of 0x4633BE ===")
|
||||
tgt_va = 0x4633BE
|
||||
for fo in range(0, 0x67000):
|
||||
if data[fo] == 0xe8 and fo+5 <= len(data):
|
||||
rel = struct.unpack_from('<i', data, fo+1)[0]
|
||||
call_tgt = (FO2VA(fo+5) + rel) & 0xFFFFFFFF
|
||||
if call_tgt == tgt_va:
|
||||
ctx = data[fo-8:fo+12]
|
||||
print(" FO 0x%05x VA 0x%07x: ...%s..." % (fo, FO2VA(fo), ' '.join('%02x'%b for b in ctx)))
|
||||
|
||||
# Also search for what calls 0x4633D1 (the actual impl)
|
||||
print()
|
||||
print("=== All callers of 0x4633D1 (real alloc impl) ===")
|
||||
tgt_va2 = 0x4633D1
|
||||
for fo in range(0, 0x67000):
|
||||
if data[fo] == 0xe8 and fo+5 <= len(data):
|
||||
rel = struct.unpack_from('<i', data, fo+1)[0]
|
||||
call_tgt = (FO2VA(fo+5) + rel) & 0xFFFFFFFF
|
||||
if call_tgt == tgt_va2:
|
||||
ctx = data[fo-8:fo+12]
|
||||
print(" FO 0x%05x VA 0x%07x: ...%s..." % (fo, FO2VA(fo), ' '.join('%02x'%b for b in ctx)))
|
||||
|
||||
# Check what functions initialize the allocator - search for CALL 0x464E77 (used inside 0x4633C7)
|
||||
print()
|
||||
print("=== Callers of 0x464E77 (alloc helper) ===")
|
||||
tgt_va3 = 0x464E77
|
||||
for fo in range(0, 0x67000):
|
||||
if data[fo] == 0xe8 and fo+5 <= len(data):
|
||||
rel = struct.unpack_from('<i', data, fo+1)[0]
|
||||
call_tgt = (FO2VA(fo+5) + rel) & 0xFFFFFFFF
|
||||
if call_tgt == tgt_va3:
|
||||
ctx = data[fo-4:fo+8]
|
||||
print(" FO 0x%05x VA 0x%07x: %s" % (fo, FO2VA(fo), ' '.join('%02x'%b for b in ctx)))
|
||||
58
tools/analyze5.py
Normal file
58
tools/analyze5.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
# Read PE entry point
|
||||
e_lfanew = struct.unpack_from('<I', data, 0x3c)[0]
|
||||
ep_rva = struct.unpack_from('<I', data, e_lfanew + 0x28)[0]
|
||||
ep_va = 0x400000 + ep_rva
|
||||
ep_fo = ep_rva - 0xC00 # .text: rawoff=0x400, VirtualAddress=0x1000, so FO = VA - 0x400C00
|
||||
print("Entry point: RVA 0x%x VA 0x%x FO 0x%x" % (ep_rva, ep_va, ep_fo))
|
||||
|
||||
def show(start, length):
|
||||
for row in range(start, start + length, 16):
|
||||
chunk = data[row:row+16]
|
||||
ann = []
|
||||
for j in range(len(chunk)):
|
||||
f = row + j
|
||||
try:
|
||||
if data[f] == 0xe8 and f+5 <= len(data):
|
||||
rel = struct.unpack_from('<i', data, f+1)[0]
|
||||
tgt = (FO2VA(f+5) + rel) & 0xFFFFFFFF
|
||||
ann.append('+%d:CALL_0x%x' % (j, tgt))
|
||||
if data[f] == 0xff and data[f+1] == 0x15 and f+6 <= len(data):
|
||||
iat = struct.unpack_from('<I', data, f+2)[0]
|
||||
d = {
|
||||
0x4822f4:'DirectDrawCreate', 0x4822fc:'DirectSoundCreate',
|
||||
0x48240c:'ExitProcess', 0x48248c:'MessageBoxA',
|
||||
0x4823cc:'LocalAlloc', 0x482370:'VirtualAlloc',
|
||||
0x482390:'CreateFileA', 0x482398:'GetModuleHandleA',
|
||||
0x48249c:'GetCommandLineA', 0x48238c:'GetStartupInfoA',
|
||||
0x4823a8:'SetFileAttributesA',
|
||||
0x482438:'HeapCreate', 0x4824c8:'InitializeCriticalSection',
|
||||
}
|
||||
ann.append('+%d:[%s]' % (j, d.get(iat, 'iat_0x%x' % iat)))
|
||||
if data[f] == 0xa3 and f+5 <= len(data):
|
||||
va = struct.unpack_from('<I', data, f+1)[0]
|
||||
if 0x47C000 <= va <= 0x482000:
|
||||
ann.append('+%d:MOV[0x%x],EAX' % (j, va))
|
||||
if data[f] == 0xc3: ann.append('+%d:RET' % j)
|
||||
if data[f] == 0xc2 and f+3 <= len(data):
|
||||
n = struct.unpack_from('<H', data, f+1)[0]
|
||||
ann.append('+%d:RET%d' % (j, n))
|
||||
except Exception:
|
||||
pass
|
||||
line = 'FO 0x%05x VA 0x%07x: %s' % (row, FO2VA(row), ' '.join('%02x' % b for b in chunk))
|
||||
if ann:
|
||||
line += ' [%s]' % ','.join(ann)
|
||||
print(line)
|
||||
|
||||
print()
|
||||
print("=== Entry point ===")
|
||||
show(ep_fo, 128)
|
||||
|
||||
print()
|
||||
print("=== function 0x4283F0 (first call in init 0x418600) at FO 0x%05x ===" % VA2FO(0x4283F0))
|
||||
show(VA2FO(0x4283F0), 96)
|
||||
59
tools/analyze6.py
Normal file
59
tools/analyze6.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
def show(start, length, label=''):
|
||||
if label: print("=== %s ===" % label)
|
||||
for row in range(start, start + length, 16):
|
||||
chunk = data[row:row+16]
|
||||
ann = []
|
||||
for j in range(len(chunk)):
|
||||
f = row + j
|
||||
try:
|
||||
if data[f] == 0xe8 and f+5 <= len(data):
|
||||
rel = struct.unpack_from('<i', data, f+1)[0]
|
||||
tgt = (FO2VA(f+5) + rel) & 0xFFFFFFFF
|
||||
ann.append('+%d:CALL_0x%x' % (j, tgt))
|
||||
if data[f] == 0xff and data[f+1] == 0x15 and f+6 <= len(data):
|
||||
iat = struct.unpack_from('<I', data, f+2)[0]
|
||||
ann.append('+%d:[iat_0x%x]' % (j, iat))
|
||||
if data[f] == 0xa3 and f+5 <= len(data):
|
||||
va = struct.unpack_from('<I', data, f+1)[0]
|
||||
if 0x47C000 <= va <= 0x482000:
|
||||
ann.append('+%d:MOV[0x%x],EAX' % (j, va))
|
||||
if data[f] == 0xc3: ann.append('+%d:RET' % j)
|
||||
if data[f] == 0xc2 and f+3 <= len(data):
|
||||
n = struct.unpack_from('<H', data, f+1)[0]
|
||||
ann.append('+%d:RET%d' % (j, n))
|
||||
except Exception:
|
||||
pass
|
||||
line = 'FO 0x%05x VA 0x%07x: %s' % (row, FO2VA(row), ' '.join('%02x' % b for b in chunk))
|
||||
if ann:
|
||||
line += ' [%s]' % ','.join(ann)
|
||||
print(line)
|
||||
|
||||
# 0x464DCA - called from entry point
|
||||
show(VA2FO(0x464DCA), 80, "0x464DCA (CRT init?)")
|
||||
print()
|
||||
# 0x465BD3 - called next
|
||||
show(VA2FO(0x465BD3), 96, "0x465BD3 (global ctors?)")
|
||||
print()
|
||||
# 0x465BC8 - called next
|
||||
show(VA2FO(0x465BC8), 32, "0x465BC8")
|
||||
print()
|
||||
# Check IAT entries near 0x482434 and 0x48243c
|
||||
print("=== IAT around 0x482430 ===")
|
||||
for va_iat in range(0x482430, 0x482470, 4):
|
||||
fo = va_iat - 0x47C000 + 0x69400 # .data section
|
||||
if fo < 0 or fo+4 > len(data): continue
|
||||
val = struct.unpack_from('<I', data, fo)[0]
|
||||
# Look for the import name by checking .idata
|
||||
# .idata: VA 0x00482000, rawoff=0x6ec00 rawsiz=0x00e00
|
||||
fo_idata = 0x6ec00 + (va_iat - 0x482000)
|
||||
print(" [0x%x] = 0x%08x" % (va_iat, val))
|
||||
|
||||
# Also look at function 0x464F67 (called in alloc chain) to understand what
|
||||
# the heap needs
|
||||
show(VA2FO(0x464F67), 48, "0x464F67 (alloc helper 2)")
|
||||
51
tools/analyze7.py
Normal file
51
tools/analyze7.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
IAT = {
|
||||
0x482370:'VirtualAlloc', 0x4823cc:'LocalAlloc', 0x48240c:'ExitProcess',
|
||||
0x482390:'CreateFileA', 0x4822f4:'DirectDrawCreate', 0x4822fc:'DirectSoundCreate',
|
||||
0x48248c:'MessageBoxA', 0x48243c:'GetVersion', 0x48242c:'GetStartupInfoA',
|
||||
}
|
||||
|
||||
def show(start, length, label=''):
|
||||
if label: print("=== %s ===" % label)
|
||||
for row in range(start, start + length, 16):
|
||||
chunk = data[row:row+16]
|
||||
ann = []
|
||||
for j in range(len(chunk)):
|
||||
f = row + j
|
||||
try:
|
||||
if data[f] == 0xe8 and f+5 <= len(data):
|
||||
rel = struct.unpack_from('<i', data, f+1)[0]
|
||||
tgt = (FO2VA(f+5) + rel) & 0xFFFFFFFF
|
||||
ann.append('+%d:CALL_0x%x' % (j, tgt))
|
||||
if data[f] == 0xff and data[f+1] == 0x15 and f+6 <= len(data):
|
||||
iat = struct.unpack_from('<I', data, f+2)[0]
|
||||
ann.append('+%d:[%s]' % (j, IAT.get(iat, 'iat_0x%x' % iat)))
|
||||
if data[f] == 0xa3 and f+5 <= len(data):
|
||||
va = struct.unpack_from('<I', data, f+1)[0]
|
||||
if 0x47C000 <= va <= 0x482000:
|
||||
ann.append('+%d:MOV[0x%x],EAX' % (j, va))
|
||||
if data[f] == 0xc3: ann.append('+%d:RET' % j)
|
||||
if data[f] == 0xc2 and f+3 <= len(data):
|
||||
ann.append('+%d:RET%d' % (j, struct.unpack_from('<H', data, f+1)[0]))
|
||||
if data[f] == 0xe9 and f+5 <= len(data):
|
||||
rel = struct.unpack_from('<i', data, f+1)[0]
|
||||
tgt = (FO2VA(f+5) + rel) & 0xFFFFFFFF
|
||||
ann.append('+%d:JMP_0x%x' % (j, tgt))
|
||||
except Exception: pass
|
||||
line = 'FO 0x%05x VA 0x%07x: %s' % (row, FO2VA(row), ' '.join('%02x' % b for b in chunk))
|
||||
if ann: line += ' [%s]' % ','.join(ann)
|
||||
print(line)
|
||||
|
||||
# 0x464F52 = the "both free lists at sentinel" path in 0x464E77
|
||||
show(VA2FO(0x464F52), 64, "0x464F52 (empty-lists fallback in 0x464E77)")
|
||||
print()
|
||||
# 0x464F67 = second alloc helper called from 0x4633D1 when 0x464E77 returns NULL
|
||||
show(VA2FO(0x464F67), 96, "0x464F67 (second alloc helper)")
|
||||
print()
|
||||
# 0x4633D1 full - 0x60 bytes to see what it does after 0x464E77 returns
|
||||
show(VA2FO(0x4633D1), 96, "0x4633D1 (real allocator, full body)")
|
||||
50
tools/analyze_crash.py
Normal file
50
tools/analyze_crash.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
|
||||
def show(start, length):
|
||||
for row in range(start, start + length, 16):
|
||||
chunk = data[row:row+16]
|
||||
ann = []
|
||||
for j in range(len(chunk)):
|
||||
f = row + j
|
||||
try:
|
||||
if data[f] == 0xe8 and f+5 <= len(data):
|
||||
rel = struct.unpack_from('<i', data, f+1)[0]
|
||||
tgt = (FO2VA(f+5) + rel) & 0xFFFFFFFF
|
||||
ann.append('+%d:CALL_0x%x' % (j, tgt))
|
||||
if data[f] == 0xff and data[f+1] == 0x15 and f+6 <= len(data):
|
||||
iat = struct.unpack_from('<I', data, f+2)[0]
|
||||
d = {
|
||||
0x4822f4:'DirectDrawCreate', 0x4822fc:'DirectSoundCreate',
|
||||
0x48240c:'ExitProcess', 0x48248c:'MessageBoxA',
|
||||
0x4823cc:'LocalAlloc', 0x482370:'VirtualAlloc',
|
||||
0x482390:'CreateFileA',
|
||||
}
|
||||
ann.append('+%d:[%s]' % (j, d.get(iat, 'iat_0x%x' % iat)))
|
||||
if data[f] == 0xc3: ann.append('+%d:RET' % j)
|
||||
if data[f] == 0xc2 and f+3 <= len(data):
|
||||
n = struct.unpack_from('<H', data, f+1)[0]
|
||||
ann.append('+%d:RET%d' % (j, n))
|
||||
if data[f] == 0xc7 and data[f+1] in (0x01,0x06,0x07) and f+6 <= len(data):
|
||||
val = struct.unpack_from('<I', data, f+2)[0]
|
||||
if 0x479000 <= val <= 0x47BFFF:
|
||||
ann.append('+%d:VTABLE=0x%x' % (j, val))
|
||||
except Exception:
|
||||
pass
|
||||
line = 'FO 0x%05x VA 0x%07x: %s' % (row, FO2VA(row), ' '.join('%02x' % b for b in chunk))
|
||||
if ann:
|
||||
line += ' [%s]' % ','.join(ann)
|
||||
print(line)
|
||||
|
||||
print("=== context around caller FO 0x17A8A (pool allocator caller) ===")
|
||||
show(0x17A60, 192)
|
||||
print()
|
||||
print("=== what's at FO 0x17A60 parent function start ===")
|
||||
# find function start
|
||||
fo = 0x17A60
|
||||
while fo > 0 and data[fo-1] != 0xCC:
|
||||
fo -= 1
|
||||
print("Function start: FO 0x%05x VA 0x%07x" % (fo, FO2VA(fo)))
|
||||
show(fo, min(32, 0x17A60 - fo + 32))
|
||||
69
tools/check_files.py
Normal file
69
tools/check_files.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"""Check what files the game needs and whether they exist, then show more code context."""
|
||||
import re, struct, os
|
||||
|
||||
print("=== Files present in game directory ===")
|
||||
game_dir = "."
|
||||
for f in sorted(os.listdir(game_dir)):
|
||||
full = os.path.join(game_dir, f)
|
||||
if os.path.isfile(full):
|
||||
print(f" {f:30s} {os.path.getsize(full):>10d} bytes")
|
||||
elif os.path.isdir(full):
|
||||
print(f" {f}/ [dir]")
|
||||
|
||||
print("\n=== Checking expected files ===")
|
||||
needed = ['MUSIC.FF','INTRFACE.FF','BIGFILE.DAT','STRDATA.DAT','HAVOC.INI','WORLDS']
|
||||
for n in needed:
|
||||
exists = os.path.exists(n)
|
||||
info = ""
|
||||
if exists:
|
||||
if os.path.isdir(n):
|
||||
contents = os.listdir(n)
|
||||
info = f" [dir, {len(contents)} files]"
|
||||
else:
|
||||
info = f" [{os.path.getsize(n)} bytes]"
|
||||
print(f" {'OK' if exists else 'MISSING':<6} {n}{info}")
|
||||
|
||||
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]
|
||||
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
|
||||
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((roff, rsz, vrva, vsz))
|
||||
|
||||
# Search for ALL C:\ paths and Z:\ paths still in the binary
|
||||
print("\n=== Remaining hardcoded paths (C: or Z:) in binary ===")
|
||||
data = exe[0x000400:0x070000]
|
||||
for m in re.finditer(rb'[CcDdZz]:\\[A-Za-z0-9_.\\]{3,40}', data):
|
||||
fo = 0x000400 + m.start()
|
||||
print(f" 0x{fo:06x}: {m.group().decode('latin1', errors='replace')!r}")
|
||||
|
||||
# Show more code around the flag check at 0x020270 - look at 0x020200-0x020280
|
||||
# Disassemble-like dump with computed CALL targets
|
||||
print("\n=== Call targets in 0x020200-0x020280 ===")
|
||||
for fo in range(0x020200, 0x020280):
|
||||
b = exe[fo]
|
||||
if b == 0xe8: # CALL rel32
|
||||
rel = struct.unpack_from('<i', exe, fo+1)[0]
|
||||
next_fo = fo + 5
|
||||
# VA of next instruction
|
||||
next_va = img_base + 0x001000 + (next_fo - 0x000400)
|
||||
target_va = next_va + rel
|
||||
target_fo = 0x000400 + (target_va - img_base - 0x001000)
|
||||
ctx = exe[max(0,fo-8):fo+8]
|
||||
print(f" CALL at 0x{fo:06x} -> fo=0x{target_fo:06x} (VA=0x{target_va:08x}): ...{exe[fo-3:fo+6].hex()}...")
|
||||
|
||||
# Also look for ALL the path strings still in the binary
|
||||
print("\n=== All path/filename strings in data section (0x069400-0x06EC00) ===")
|
||||
data_sec = exe[0x069400:0x06ec00]
|
||||
for m in re.finditer(rb'[A-Za-z0-9][A-Za-z0-9_. \\:]{2,39}(?=\x00)', data_sec):
|
||||
s = m.group().decode('latin1', errors='replace')
|
||||
if any(c in s for c in ['.', '\\', ':']):
|
||||
fo = 0x069400 + m.start()
|
||||
print(f" 0x{fo:06x}: {s!r}")
|
||||
45
tools/check_strings.py
Normal file
45
tools/check_strings.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
# Show context around the "missing from the HAVOC directory" string
|
||||
fo = 0x6bec5
|
||||
print("=== Context around FO 0x%05x ===" % fo)
|
||||
start = fo - 0x80
|
||||
end = fo + 0x80
|
||||
chunk = data[start:end]
|
||||
for i in range(0, len(chunk), 16):
|
||||
row = chunk[i:i+16]
|
||||
hex_part = ' '.join('%02x' % b for b in row)
|
||||
asc_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in row)
|
||||
print("FO 0x%05x: %-47s %s" % (start+i, hex_part, asc_part))
|
||||
|
||||
print()
|
||||
print("=== Callers of 'missing' string at FO 0x6BEC5 VA 0x46CAC5 ===")
|
||||
target = 0x46cac5
|
||||
for fo2 in range(0x400, 0x67000):
|
||||
if data[fo2] == 0x68: # PUSH imm32
|
||||
imm = struct.unpack_from('<I', data, fo2+1)[0]
|
||||
if imm == target:
|
||||
print(" PUSH 0x46CAC5 at FO 0x%05x VA 0x%08x" % (fo2, FO2VA(fo2)))
|
||||
|
||||
# Also look for what calls the string display - search for any CALL near those PUSHes
|
||||
print()
|
||||
print("=== Reading STRDATA.DAT ===")
|
||||
try:
|
||||
sd = open('STRDATA.DAT','rb').read()
|
||||
print("Size: %d bytes" % len(sd))
|
||||
# Try to find "corrupt" in it
|
||||
for needle in [b'corrupt', b'unavail', b'Corrupt', b'Unavail']:
|
||||
idx = sd.find(needle)
|
||||
if idx != -1:
|
||||
print("Found '%s' at offset 0x%x: %r" % (needle.decode(), idx, sd[max(0,idx-20):idx+60]))
|
||||
# Show first 256 bytes as text
|
||||
print("First 200 bytes:")
|
||||
printable = ''.join(chr(b) if 32 <= b < 127 else ('\\x%02x' % b) for b in sd[:200])
|
||||
print(printable)
|
||||
except Exception as e:
|
||||
print("Error: %s" % e)
|
||||
88
tools/createfile_trace.py
Normal file
88
tools/createfile_trace.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import struct, re
|
||||
|
||||
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
||||
base = 0x00400000
|
||||
|
||||
sections = [
|
||||
(".text", 0x00001000, 0x00066bbd, 0x00000400, 0x00066c00),
|
||||
(".rdata", 0x00079000, 0x000023c0, 0x00067000, 0x00002400),
|
||||
(".data", 0x0007c000, 0x00005650, 0x00069400, 0x00005800),
|
||||
(".idata", 0x00082000, 0x00000d5a, 0x0006ec00, 0x00000e00),
|
||||
]
|
||||
def rva2fo(rva):
|
||||
for (n, vrva, vsz, raw, rsz) in sections:
|
||||
if vrva <= rva < vrva + vsz:
|
||||
return raw + (rva - vrva)
|
||||
return None
|
||||
|
||||
def va2str(va):
|
||||
fo = rva2fo(va - base)
|
||||
if fo is None or fo >= len(exe):
|
||||
return None
|
||||
try:
|
||||
end = exe.index(b'\x00', fo)
|
||||
return exe[fo:end].decode('latin1', errors='replace')
|
||||
except:
|
||||
return None
|
||||
|
||||
def find_call_sites(iat_va):
|
||||
needle = b'\xff\x15' + struct.pack('<I', iat_va)
|
||||
sites = []
|
||||
for m in re.finditer(re.escape(needle), exe[0x400:0x067000]):
|
||||
sites.append(0x400 + m.start())
|
||||
return sites
|
||||
|
||||
# IAT slot VAs from parse_iat.py output
|
||||
iat = {
|
||||
'CreateFileA': 0x00482390,
|
||||
'GetModuleFileNameA': 0x00482364,
|
||||
'GetPrivateProfileIntA':0x004823b4,
|
||||
'ReadFile': 0x00482398,
|
||||
'CreateFileMappingA': 0x004823bc,
|
||||
'MapViewOfFile': 0x004823b8,
|
||||
'DeleteFileA': 0x00482388,
|
||||
'WriteFile': 0x004823a0,
|
||||
'WritePrivateProfileStringA': 0x0048238c,
|
||||
}
|
||||
|
||||
def extract_string_pushes(call_fo, lookback=160):
|
||||
ctx_start = max(0x400, call_fo - lookback)
|
||||
ctx = exe[ctx_start:call_fo]
|
||||
pushes = []
|
||||
pos = 0
|
||||
while pos < len(ctx):
|
||||
b = ctx[pos]
|
||||
if b == 0x68 and pos + 5 <= len(ctx):
|
||||
val = struct.unpack_from('<I', ctx, pos+1)[0]
|
||||
s = va2str(val)
|
||||
if s is not None and len(s) >= 2:
|
||||
pushes.append((ctx_start + pos, s))
|
||||
pos += 5
|
||||
else:
|
||||
pos += 1
|
||||
return pushes
|
||||
|
||||
print("=== CreateFileA call sites ===")
|
||||
for cfo in find_call_sites(iat['CreateFileA']):
|
||||
pushes = extract_string_pushes(cfo)
|
||||
print(f" CALL @ 0x{cfo:06x} strings: {[s for _, s in pushes]}")
|
||||
|
||||
print("\n=== DeleteFileA call sites ===")
|
||||
for cfo in find_call_sites(iat['DeleteFileA']):
|
||||
pushes = extract_string_pushes(cfo)
|
||||
print(f" CALL @ 0x{cfo:06x} strings: {[s for _, s in pushes]}")
|
||||
|
||||
print("\n=== GetModuleFileNameA call sites ===")
|
||||
for cfo in find_call_sites(iat['GetModuleFileNameA']):
|
||||
ctx = exe[cfo:cfo+80]
|
||||
print(f" CALL @ 0x{cfo:06x}: {ctx.hex(' ')}")
|
||||
|
||||
print("\n=== GetPrivateProfileIntA call sites ===")
|
||||
for cfo in find_call_sites(iat['GetPrivateProfileIntA']):
|
||||
pushes = extract_string_pushes(cfo)
|
||||
print(f" CALL @ 0x{cfo:06x} strings: {[s for _, s in pushes]}")
|
||||
|
||||
print("\n=== WritePrivateProfileStringA call sites ===")
|
||||
for cfo in find_call_sites(iat['WritePrivateProfileStringA']):
|
||||
pushes = extract_string_pushes(cfo)
|
||||
print(f" CALL @ 0x{cfo:06x} strings: {[s for _, s in pushes]}")
|
||||
137
tools/dbg_path.py
Normal file
137
tools/dbg_path.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
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")
|
||||
103
tools/deep_audit.py
Normal file
103
tools/deep_audit.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import re, struct, os
|
||||
|
||||
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
||||
|
||||
# ── 1. Verify raw bytes at known path locations ──
|
||||
print("=== Raw bytes at path string locations ===")
|
||||
spots = [
|
||||
(0x06b27c, 16, "MUSIC.FF area"),
|
||||
(0x06b288, 16, "INTRFACE.FF area"),
|
||||
(0x06b2b0, 24, "WORLDS\\ area"),
|
||||
(0x06b390, 30, "C:\\WORLDS\\LAND0101 area"),
|
||||
(0x06b3a8, 20, "HAVOCDAT area"),
|
||||
(0x06b3b8, 16, "BIGFILE area"),
|
||||
(0x06b978, 24, "GAMIMAGE area"),
|
||||
]
|
||||
for fo, length, label in spots:
|
||||
raw = exe[fo:fo+length]
|
||||
asc = ''.join(chr(b) if 0x20 <= b < 0x7f else '.' for b in raw)
|
||||
print(f" 0x{fo:06x} {label}: {raw.hex(' ')}")
|
||||
print(f" ascii: {asc!r}")
|
||||
|
||||
# ── 2. Check which expected files exist ──
|
||||
print("\n=== File existence check in game dir ===")
|
||||
game_files = [
|
||||
"BIGFILE.DAT", "MUSIC.FF", "INTRFACE.FF", "OBJECTS.FF", "SOUND.FF",
|
||||
"STRDATA.DAT", "HAVOC.INI", "GAMIMAGE.DAT", "KEYSET.DAT",
|
||||
"WORLDS/LAND0101.DAT", "WORLDS/GRAFIX00.DAT",
|
||||
]
|
||||
for f in game_files:
|
||||
exists = os.path.exists(f)
|
||||
print(f" {'OK' if exists else 'MISSING':8s} {f}")
|
||||
|
||||
# ── 3. Find SetCurrentDirectory / GetModuleFileName in imports ──
|
||||
print("\n=== SetCurrentDirectory / GetModuleFileName call sites ===")
|
||||
for api_bytes in [b'SetCurrentDirectoryA', b'GetModuleFileNameA', b'_chdir']:
|
||||
idx = exe.find(api_bytes)
|
||||
if idx == -1:
|
||||
print(f" NOT IN EXE: {api_bytes.decode()}")
|
||||
continue
|
||||
print(f" {api_bytes.decode()} name at 0x{idx:06x}")
|
||||
# find IAT VA for this
|
||||
hint_fo = idx - 2
|
||||
int_rva = hint_fo - 0x000400 + 0x001000
|
||||
iat_needle = struct.pack('<I', 0x400000 + int_rva)
|
||||
# search for CALL [mem] referencing this
|
||||
for m in re.finditer(b'\xff\x15' + re.escape(iat_needle), exe[0x400:0x067000]):
|
||||
call_fo = 0x400 + m.start()
|
||||
print(f" CALL @ 0x{call_fo:06x}: {exe[max(0,call_fo-8):call_fo+12].hex(' ')}")
|
||||
|
||||
# ── 4. Find GAMIMAGE.DAT context and its callers ──
|
||||
print("\n=== GAMIMAGE.DAT context ===")
|
||||
idx = exe.find(b'GAMIMAGE.DAT')
|
||||
if idx != -1:
|
||||
region = exe[max(0, idx-64):idx+80]
|
||||
for row in range(0, len(region), 16):
|
||||
hex_part = ' '.join(f'{b:02x}' for b in region[row:row+16])
|
||||
asc_part = ''.join(chr(b) if 0x20 <= b < 0x7f else '.' for b in region[row:row+16])
|
||||
print(f" {max(0,idx-64)+row:06x}: {hex_part:<48} {asc_part}")
|
||||
# find its VA and look for PUSH imm32 references in .text
|
||||
img_va = 0x400000 + idx - 0x000400 + 0x001000
|
||||
needle = struct.pack('<I', img_va)
|
||||
for m in re.finditer(re.escape(needle), exe[0x400:0x067000]):
|
||||
ref_fo = 0x400 + m.start()
|
||||
print(f" ref in .text @ 0x{ref_fo:06x}: {exe[ref_fo-4:ref_fo+8].hex(' ')}")
|
||||
|
||||
# ── 5. Find the error-display function: search for code that opens STRDATA.DAT
|
||||
# then calls MessageBox with a string index ──
|
||||
print("\n=== STRDATA.DAT open call site ===")
|
||||
strdata_va = 0x400000 + exe.find(b'STRDATA.DAT\x00') - 0x000400 + 0x001000
|
||||
needle = struct.pack('<I', strdata_va)
|
||||
print(f" STRDATA.DAT string VA: 0x{strdata_va:08x}")
|
||||
for m in re.finditer(re.escape(needle), exe[0x400:0x067000]):
|
||||
ref_fo = 0x400 + m.start()
|
||||
ctx = exe[max(0x400, ref_fo-32):ref_fo+32]
|
||||
print(f" ref @ 0x{ref_fo:06x}: {ctx.hex(' ')}")
|
||||
|
||||
# ── 6. Find all CreateFileA call sites and what string they push ──
|
||||
print("\n=== CreateFileA / _lopen call sites (first 20) ──")
|
||||
for api_bytes in [b'CreateFileA\x00', b'_lopen\x00', b'OpenFile\x00']:
|
||||
idx = exe.find(api_bytes)
|
||||
if idx == -1:
|
||||
continue
|
||||
hint_fo = idx - 2
|
||||
int_rva = hint_fo - 0x000400 + 0x001000
|
||||
iat_needle = struct.pack('<I', 0x400000 + int_rva)
|
||||
sites = []
|
||||
for m in re.finditer(b'\xff\x15' + re.escape(iat_needle), exe[0x400:0x067000]):
|
||||
sites.append(0x400 + m.start())
|
||||
print(f" {api_bytes.rstrip(b'\\x00').decode()}: {len(sites)} call sites")
|
||||
for call_fo in sites[:6]:
|
||||
# look backward for PUSH imm32 (string arg)
|
||||
ctx = exe[max(0x400, call_fo-64):call_fo]
|
||||
pushes = []
|
||||
for pm in re.finditer(b'\x68', ctx):
|
||||
va = struct.unpack_from('<I', ctx, pm.start()+1)[0]
|
||||
fo = va - 0x400000 - 0x001000 + 0x000400
|
||||
if 0x067000 <= fo < len(exe):
|
||||
try:
|
||||
s = exe[fo:exe.index(b'\x00', fo)].decode('latin1', errors='replace')[:30]
|
||||
pushes.append(repr(s))
|
||||
except:
|
||||
pass
|
||||
print(f" call @ 0x{call_fo:06x}: pushed={pushes}")
|
||||
93
tools/deep_trace.py
Normal file
93
tools/deep_trace.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
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(' ')}")
|
||||
106
tools/drive_trace.py
Normal file
106
tools/drive_trace.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"""Find all code references to D:\ buffer and the file-open call chain."""
|
||||
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]
|
||||
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
|
||||
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((roff, rsz, vrva, vsz))
|
||||
|
||||
def fo_to_va(fo):
|
||||
for 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 ro, rs, vr, vs in secs:
|
||||
if ro and vr <= rva < vr+rs:
|
||||
return ro + (rva - vr)
|
||||
return None
|
||||
|
||||
def search_refs(fo, label=""):
|
||||
va = fo_to_va(fo)
|
||||
if not va:
|
||||
print(f" {label}: fo 0x{fo:06x} -> no VA")
|
||||
return
|
||||
needle = struct.pack('<I', va)
|
||||
refs = [0x400 + m.start() for m in re.finditer(re.escape(needle), exe[0x400:0x067000])]
|
||||
print(f"\n {label} fo=0x{fo:06x} VA=0x{va:08x} refs={len(refs)}")
|
||||
for r in refs[:8]:
|
||||
ctx = exe[max(0x400,r-8):r+8]
|
||||
print(f" 0x{r:06x}: {ctx.hex(' ')}")
|
||||
|
||||
# D:\ buffer and neighboring strings
|
||||
print("=== D:\\ and neighboring strings ===")
|
||||
for fo in [0x06b2b0, 0x06b2b8, 0x06b2d4, 0x06b390, 0x06b3a8, 0x06b3b8, 0x06b2d0]:
|
||||
s_bytes = exe[fo:fo+24]
|
||||
s = s_bytes.rstrip(b'\x00').decode('latin1', errors='replace')
|
||||
search_refs(fo, repr(s))
|
||||
|
||||
# What's around D:\ in .data?
|
||||
print("\n=== Hex context around D:\\ (0x06b2c0-0x06b420) ===")
|
||||
for row in range(0, 0x160, 16):
|
||||
fo = 0x06b2c0 + row
|
||||
h = ' '.join(f'{b:02x}' for b in exe[fo:fo+16])
|
||||
a = ''.join(chr(b) if 0x20<=b<0x7f else '.' for b in exe[fo:fo+16])
|
||||
print(f" {fo:06x}: {h:<48} {a}")
|
||||
|
||||
# Dump the init function at 0x009C60
|
||||
print("\n=== Init function at 0x009C60 ===")
|
||||
for row in range(0, 0xc0, 16):
|
||||
fo = 0x009c60 + row
|
||||
h = ' '.join(f'{b:02x}' for b in exe[fo:fo+16])
|
||||
a = ''.join(chr(b) if 0x20<=b<0x7f else '.' for b in exe[fo:fo+16])
|
||||
ann = ''
|
||||
for off in range(13):
|
||||
if fo+off+4 <= fo+16:
|
||||
dw = struct.unpack_from('<I', exe, fo+off)[0]
|
||||
if dw == fo_to_va(0x06b2d4): ann = ' <-D:\\'
|
||||
elif dw == fo_to_va(0x06b27c): ann = ' <-MUSIC.FF'
|
||||
elif dw == fo_to_va(0x06b2b8): ann = ' <-WORLDS\\'
|
||||
elif dw == fo_to_va(0x06b3b8): ann = ' <-BIGFILE.DAT'
|
||||
elif dw == fo_to_va(0x06bee8): ann = ' <-STRDATA.DAT'
|
||||
# Also annotate CALL targets
|
||||
if exe[fo] == 0xe8:
|
||||
rel = struct.unpack_from('<i', exe, fo+1)[0]
|
||||
tva = img_base + 0x001000 + (fo+5 - 0x000400) + rel
|
||||
tfo = 0x000400 + (tva - img_base - 0x001000)
|
||||
ann += f' CALL->0x{tfo:06x}'
|
||||
print(f" {fo:06x}: {h:<48} {a}{ann}")
|
||||
|
||||
# Context around CALL at 0x020253 -> 0x009C60
|
||||
print("\n=== Context before error check (0x020230-0x020285) ===")
|
||||
for row in range(0, 0x60, 16):
|
||||
fo = 0x020230 + row
|
||||
h = ' '.join(f'{b:02x}' for b in exe[fo:fo+16])
|
||||
a = ''.join(chr(b) if 0x20<=b<0x7f else '.' for b in exe[fo:fo+16])
|
||||
ann = ''
|
||||
if exe[fo] == 0xe8:
|
||||
rel = struct.unpack_from('<i', exe, fo+1)[0]
|
||||
tva = img_base + 0x001000 + (fo+5 - 0x000400) + rel
|
||||
tfo = 0x000400 + (tva - img_base - 0x001000)
|
||||
ann += f' CALL->0x{tfo:06x}'
|
||||
print(f" {fo:06x}: {h:<48} {a}{ann}")
|
||||
|
||||
# Check the flag check, and suggest bypass bytes
|
||||
print("\n=== Error gate at 0x020270 (current bytes) ===")
|
||||
ctx = exe[0x020270:0x020282]
|
||||
print(f" {ctx.hex(' ')}")
|
||||
print(f" Byte at 0x020277: 0x{ctx[7]:02x} (should be 75=JNZ for unfixed, or eb=JMP for bypass)")
|
||||
|
||||
# Check if there's a HAVOC.INI read code that stores the drive to 0x06b2d4
|
||||
drive_va = fo_to_va(0x06b2d4)
|
||||
print(f"\n=== All writes to D:\\ buffer area VA=0x{drive_va:08x} ===")
|
||||
# Look for MOV BYTE/DWORD PTR [0x47DED4] patterns
|
||||
# Could be: a2, c6 05, a3, c7 05 ...
|
||||
# Also look for LEA/MOV reg, addr then strcpy/memcpy
|
||||
109
tools/dump_region.py
Normal file
109
tools/dump_region.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
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]
|
||||
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
|
||||
|
||||
LABELS = {
|
||||
fo_to_va(0x06b27c): 'MUSIC.FF',
|
||||
fo_to_va(0x06b288): 'INTRFACE.FF',
|
||||
fo_to_va(0x06b2b8): 'WORLDS',
|
||||
fo_to_va(0x06b3b8): 'BIGFILE.DAT',
|
||||
fo_to_va(0x06bee8): 'STRDATA.DAT',
|
||||
}
|
||||
|
||||
def dump(start_fo, end_fo, title):
|
||||
print(f"\n=== {title} ===")
|
||||
row = start_fo & ~0xF
|
||||
while row < end_fo:
|
||||
h = ' '.join(f'{b:02x}' for b in exe[row:row+16])
|
||||
a = ''.join(chr(b) if 0x20<=b<0x7f else '.' for b in exe[row:row+16])
|
||||
ann = ''
|
||||
for off in range(13):
|
||||
if row+off+4 <= row+16:
|
||||
dw = struct.unpack_from('<I', exe, row+off)[0]
|
||||
if dw in LABELS:
|
||||
ann = f' <-{LABELS[dw]}'
|
||||
break
|
||||
print(f" {row:06x}: {h:<48} {a}{ann}")
|
||||
row += 16
|
||||
|
||||
# Show the file-loading init region
|
||||
dump(0x029550, 0x029600, "0x029550-0x029600 (INTRFACE.FF area)")
|
||||
dump(0x0295c0, 0x029620, "0x0295c0-0x029620 (MUSIC.FF area)")
|
||||
dump(0x029870, 0x0298c0, "0x029870-0x0298c0 (WORLDS area)")
|
||||
dump(0x029900, 0x029960, "0x029900-0x029960 (after WORLDS)")
|
||||
dump(0x033de0, 0x033e60, "0x033de0-0x033e60 (flag-setter context)")
|
||||
|
||||
# Find the CALL target at 0x033df7
|
||||
call_site = 0x033df7
|
||||
rel32 = struct.unpack_from('<i', exe, call_site+1)[0]
|
||||
target = call_site + 5 + rel32
|
||||
print(f"\nCALL at 0x{call_site:06x}: target = 0x{target:06x}")
|
||||
# section boundary check
|
||||
for n, ro, rs, vr, vs in secs:
|
||||
if ro and ro <= target < ro+rs:
|
||||
print(f" In section {n} (raw 0x{ro:06x})")
|
||||
break
|
||||
|
||||
# Dump start of that target function
|
||||
dump(target, target+0x80, f"Target fn at 0x{target:06x}")
|
||||
|
||||
# Now find MessageBoxA IAT using correct section math
|
||||
print("\n=== Finding MessageBoxA IAT ===")
|
||||
mbox_name_off = exe.find(b'MessageBoxA\x00')
|
||||
print(f" 'MessageBoxA' string at 0x{mbox_name_off:06x}")
|
||||
# Search .idata for a thunk pointing near that name
|
||||
idata_start = 0x06ec00
|
||||
idata_end = 0x06fa00
|
||||
for fo in range(idata_start, idata_end, 4):
|
||||
val = struct.unpack_from('<I', exe, fo)[0]
|
||||
if val == 0:
|
||||
continue
|
||||
# Try to dereference as a VA pointing to a hint+name
|
||||
rva = val - img_base
|
||||
candidate_fo = None
|
||||
for n, ro, rs, vr, vs in secs:
|
||||
if ro and vr <= rva < vr+rs:
|
||||
candidate_fo = ro + (rva - vr)
|
||||
break
|
||||
if candidate_fo and 0 < candidate_fo < len(exe) - 20:
|
||||
try:
|
||||
name_start = candidate_fo + 2
|
||||
end = exe.index(b'\x00', name_start)
|
||||
nm = exe[name_start:end].decode('latin1', errors='replace')
|
||||
if 'MessageBox' in nm:
|
||||
iat_va = fo_to_va(fo)
|
||||
print(f" IAT entry at fo=0x{fo:06x} VA=0x{iat_va:08x} fn='{nm}'")
|
||||
# Find all call sites: ff 15 <le32 iat_va>
|
||||
needle = b'\xff\x15' + struct.pack('<I', iat_va)
|
||||
for m in re.finditer(re.escape(needle), exe[0x400:0x067000]):
|
||||
cfo = 0x400 + m.start()
|
||||
ctx = exe[max(0x400,cfo-20):cfo+12]
|
||||
print(f" call site 0x{cfo:06x}: {ctx.hex(' ')}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Also search for error-string "corrupt" via STRDATA.DAT open in code
|
||||
print("\n=== STRDATA.DAT open call at 0x038c8e ===")
|
||||
dump(0x038c6e, 0x038cd0, "STRDATA.DAT open region")
|
||||
|
||||
# Look at what function contains 0x020270 (flag check before error)
|
||||
dump(0x020200, 0x020300, "0x020200-0x020300 (error gate function)")
|
||||
116
tools/final_analysis.py
Normal file
116
tools/final_analysis.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""Final analysis: trace paths around error gate, find D:\ usage, plan bypass patches."""
|
||||
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]
|
||||
|
||||
def fo_to_va(fo):
|
||||
num_secs = struct.unpack_from('<H', exe, pe_off+6)[0]
|
||||
opt_sz = struct.unpack_from('<H', exe, pe_off+20)[0]
|
||||
for i in range(num_secs):
|
||||
s = pe_off + 24 + opt_sz + i*40
|
||||
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]
|
||||
if roff and roff <= fo < roff+rsz:
|
||||
return img_base + vrva + (fo - roff)
|
||||
return None
|
||||
|
||||
def call_target(call_fo):
|
||||
rel = struct.unpack_from('<i', exe, call_fo+1)[0]
|
||||
next_va = fo_to_va(call_fo + 5)
|
||||
if next_va is None:
|
||||
return None, None
|
||||
target_va = next_va + rel
|
||||
# Find file offset from VA
|
||||
num_secs = struct.unpack_from('<H', exe, pe_off+6)[0]
|
||||
opt_sz = struct.unpack_from('<H', exe, pe_off+20)[0]
|
||||
rva = target_va - img_base
|
||||
for i in range(num_secs):
|
||||
s = pe_off + 24 + opt_sz + i*40
|
||||
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]
|
||||
if roff and vrva <= rva < vrva+rsz:
|
||||
return roff + (rva - vrva), target_va
|
||||
return None, target_va
|
||||
|
||||
# Dump exact bytes from file at key offsets
|
||||
def hexdump(fo, n, label=""):
|
||||
print(f"\n{label} [0x{fo:06x}..0x{fo+n-1:06x}]:")
|
||||
for row in range(0, n, 16):
|
||||
fo2 = fo + row
|
||||
h = ' '.join(f'{b:02x}' for b in exe[fo2:fo2+16])
|
||||
a = ''.join(chr(b) if 0x20<=b<0x7f else '.' for b in exe[fo2:fo2+16])
|
||||
print(f" {fo2:06x}: {h:<48} {a}")
|
||||
|
||||
# Dump raw bytes around 0x020270
|
||||
hexdump(0x020270, 80, "Error gate region")
|
||||
|
||||
# Decode JNZ manually
|
||||
jnz_fo = 0x020277
|
||||
jnz_imm = exe[jnz_fo + 1]
|
||||
next_instr_fo = jnz_fo + 2
|
||||
# JNZ target = next_instr_va + signed(imm)
|
||||
next_va = fo_to_va(next_instr_fo)
|
||||
target_va = next_va + (jnz_imm if jnz_imm < 128 else jnz_imm - 256)
|
||||
print(f"\nJNZ at 0x{jnz_fo:06x}: imm={jnz_imm} (0x{jnz_imm:02x})")
|
||||
print(f" Next instr fo=0x{next_instr_fo:06x} VA=0x{next_va:08x}")
|
||||
print(f" Target VA=0x{target_va:08x} (+/- {jnz_imm if jnz_imm < 128 else jnz_imm-256})")
|
||||
# Convert target VA to file offset
|
||||
tfo, _ = call_target(jnz_fo - 4) # hacky: find target fo from VA
|
||||
# Manual: target_fo = text_raw + (target_va - img_base - text_vRVA)
|
||||
text_roff = 0x000400
|
||||
text_vrva = 0x001000
|
||||
tfo = text_roff + (target_va - img_base - text_vrva)
|
||||
print(f" Target fo=0x{tfo:06x}")
|
||||
hexdump(tfo, 24, "JNZ target")
|
||||
|
||||
# Also decode JZ at 0x02026A
|
||||
jz_fo = 0x02026A
|
||||
jz_imm = exe[jz_fo + 1]
|
||||
jz_next_va = fo_to_va(jz_fo + 2)
|
||||
jz_target_va = jz_next_va + (jz_imm if jz_imm < 128 else jz_imm - 256)
|
||||
jz_tfo = text_roff + (jz_target_va - img_base - text_vrva)
|
||||
print(f"\nJZ at 0x{jz_fo:06x}: imm={jz_imm} (0x{jz_imm:02x})")
|
||||
print(f" Target VA=0x{jz_target_va:08x} fo=0x{jz_tfo:06x}")
|
||||
hexdump(jz_tfo, 24, "JZ target")
|
||||
|
||||
# D:\ usage context
|
||||
hexdump(0x029a90, 0x80, "D:\\ usage at 0x029ab0")
|
||||
|
||||
# Now let's check what HAVOC.INI read code does with drive letter
|
||||
# Search for code that reads from HAVOC.INI section "SETUP" key "DRIVE"
|
||||
setup_va = fo_to_va(0x06b2c4) # "SETUP"
|
||||
drive_va = fo_to_va(0x06b2cc) # "DRIVE" (check actual offsets)
|
||||
print(f"\n=== Looking for SETUP/DRIVE string VAs ===")
|
||||
# Find "SETUP" and "DRIVE" strings
|
||||
setup_off = exe.find(b'SETUP\x00', 0x069400)
|
||||
drive_off = exe.find(b'DRIVE\x00', 0x069400)
|
||||
havocini_off = exe.find(b'HAVOC.INI\x00', 0x069400)
|
||||
print(f" SETUP at 0x{setup_off:06x}, VA=0x{fo_to_va(setup_off) or 0:08x}")
|
||||
print(f" DRIVE at 0x{drive_off:06x}, VA=0x{fo_to_va(drive_off) or 0:08x}")
|
||||
print(f" HAVOC.INI at 0x{havocini_off:06x}, VA=0x{fo_to_va(havocini_off) or 0:08x}")
|
||||
|
||||
# Find code refs to DRIVE key VA
|
||||
if drive_off:
|
||||
dva = fo_to_va(drive_off)
|
||||
needle = struct.pack('<I', dva)
|
||||
refs = [0x400 + m.start() for m in re.finditer(re.escape(needle), exe[0x400:0x067000])]
|
||||
print(f" DRIVE refs={len(refs)}")
|
||||
for r in refs:
|
||||
ctx = exe[max(0,r-8):r+8]
|
||||
print(f" 0x{r:06x}: {ctx.hex(' ')}")
|
||||
|
||||
# Find code refs to HAVOC.INI VA
|
||||
if havocini_off:
|
||||
hva = fo_to_va(havocini_off)
|
||||
needle = struct.pack('<I', hva)
|
||||
refs = [0x400 + m.start() for m in re.finditer(re.escape(needle), exe[0x400:0x067000])]
|
||||
print(f" HAVOC.INI refs={len(refs)}")
|
||||
for r in refs:
|
||||
ctx = exe[max(0,r-8):r+12]
|
||||
print(f" 0x{r:06x}: {ctx.hex(' ')}")
|
||||
37
tools/find_callers.py
Normal file
37
tools/find_callers.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
target_va = 0x40AB90 # crash_stub
|
||||
|
||||
print("=== All CALL instructions targeting 0x40AB90 (crash_stub) ===")
|
||||
for fo in range(0x400, len(data)-4):
|
||||
if data[fo] == 0xe8:
|
||||
rel = struct.unpack_from('<i', data, fo+1)[0]
|
||||
tgt = (FO2VA(fo+5) + rel) & 0xFFFFFFFF
|
||||
if tgt == target_va:
|
||||
caller_va = FO2VA(fo)
|
||||
print(" Caller at FO 0x%05x VA 0x%08x" % (fo, caller_va))
|
||||
|
||||
print()
|
||||
print("=== All CALL instructions targeting 0x40AAF0 (pool_alloc) ===")
|
||||
target2 = 0x40AAF0
|
||||
for fo in range(0x400, len(data)-4):
|
||||
if data[fo] == 0xe8:
|
||||
rel = struct.unpack_from('<i', data, fo+1)[0]
|
||||
tgt = (FO2VA(fo+5) + rel) & 0xFFFFFFFF
|
||||
if tgt == target2:
|
||||
print(" Caller at FO 0x%05x VA 0x%08x" % (fo, FO2VA(fo)))
|
||||
|
||||
print()
|
||||
print("=== Checking callers of 0x429CA0 ===")
|
||||
target3 = 0x429CA0
|
||||
for fo in range(0x400, len(data)-4):
|
||||
if data[fo] == 0xe8:
|
||||
rel = struct.unpack_from('<i', data, fo+1)[0]
|
||||
tgt = (FO2VA(fo+5) + rel) & 0xFFFFFFFF
|
||||
if tgt == target3:
|
||||
print(" Caller at FO 0x%05x VA 0x%08x" % (fo, FO2VA(fo)))
|
||||
39
tools/find_cdcheck.py
Normal file
39
tools/find_cdcheck.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import struct, sys, io, r2pipe
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
# The string at STRDATA offset 0x8C is string index ~7 (counting from 0)
|
||||
# Parse STRDATA.DAT to find the index of the "corrupt or unavailable" string
|
||||
sd = open('STRDATA.DAT','rb').read()
|
||||
idx = 0
|
||||
str_idx = 0
|
||||
while idx < len(sd):
|
||||
length = sd[idx]
|
||||
if length == 0:
|
||||
idx += 1
|
||||
continue
|
||||
s = sd[idx+1:idx+1+length]
|
||||
printable = ''.join(chr(b) if 32 <= b < 127 else '?' for b in s)
|
||||
print("String[%d] len=%d: %s" % (str_idx, length, printable[:80]))
|
||||
if b'corrupt' in s or b'unavail' in s:
|
||||
print(" ^^^ THIS IS THE TARGET string index %d" % str_idx)
|
||||
str_idx += 1
|
||||
idx += 1 + length
|
||||
|
||||
print()
|
||||
print("=== All hardcoded paths/filenames in binary ===")
|
||||
# Search for drive letters and path patterns
|
||||
for needle in [b'C:\\', b'D:\\', b'.FF', b'.DAT', b'WORLD', b'BIGFILE',
|
||||
b'OBJECTS', b'SOUND.FF', b'CD_DATA', b'cd_data']:
|
||||
fo = 0
|
||||
while True:
|
||||
pos = data.find(needle, fo)
|
||||
if pos == -1: break
|
||||
start = max(0, pos-8)
|
||||
end = min(len(data), pos+40)
|
||||
ctx = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data[start:end])
|
||||
print("FO 0x%05x VA 0x%08x: %s" % (pos, FO2VA(pos), ctx))
|
||||
fo = pos + 1
|
||||
48
tools/find_cdpath.py
Normal file
48
tools/find_cdpath.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import struct, sys, io, r2pipe
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
r2 = r2pipe.open("Z:/Development/devl/Havoc/HAVOC_NOCD.EXE",
|
||||
["-e", "bin.relocs.apply=true"])
|
||||
|
||||
# String at FO 0x6B2D4 = VA 0x46BED4: "DRIVE\0\0\0"
|
||||
# String at FO 0x6B2DC = VA 0x46BEDC: "D:\"
|
||||
# String at FO 0x6B2E0 = VA 0x46BEE0: "HAVOC"
|
||||
drive_va = 0x46bed4
|
||||
havoc_va = 0x46bee0
|
||||
dcolon_va = 0x46bedc
|
||||
|
||||
print("=== Callers that push DRIVE VA 0x46BED4 or D:\\ VA 0x46BEDC ===")
|
||||
for target in [drive_va, havoc_va, dcolon_va]:
|
||||
for fo in range(0x400, 0x67000):
|
||||
if data[fo] == 0x68:
|
||||
imm = struct.unpack_from('<I', data, fo+1)[0]
|
||||
if imm == target:
|
||||
print(" PUSH 0x%08x at FO 0x%05x VA 0x%08x" % (target, fo, FO2VA(fo)))
|
||||
|
||||
# Also search for the string "HAVOC" as a MOV target (could be hard-coded path)
|
||||
print()
|
||||
|
||||
# Show the full data block at 0x6B270-0x6B320
|
||||
print("=== Data at 0x46BE70-0x46BF40 (file list area) ===")
|
||||
start = VA2FO(0x46be70)
|
||||
for fo2 in range(start, start + 0xD0, 16):
|
||||
chunk = data[fo2:fo2+16]
|
||||
hex_part = ' '.join('%02x' % b for b in chunk)
|
||||
asc_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
|
||||
print("VA 0x%08x: %-47s %s" % (FO2VA(fo2), hex_part, asc_part))
|
||||
|
||||
print()
|
||||
# Now find what code READS HAVOC.INI DRIVE key
|
||||
print("=== Callers of SETUP/DRIVE/D:\\ area ===")
|
||||
setup_va = None
|
||||
# Find "SETUP" string
|
||||
pos = data.find(b'SETUP\x00')
|
||||
while pos != -1:
|
||||
print(" 'SETUP' at FO 0x%05x VA 0x%08x" % (pos, FO2VA(pos)))
|
||||
pos = data.find(b'SETUP\x00', pos+1)
|
||||
|
||||
r2.quit()
|
||||
62
tools/find_drive_func.py
Normal file
62
tools/find_drive_func.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
# The strings: SETUP=0x46BEC4, DRIVE=0x46BECC, D:\=0x46BED4, HAVOC=0x46BED8
|
||||
setup_va = 0x46bec4
|
||||
drive_va = 0x46becc
|
||||
dcol_va = 0x46bed4
|
||||
havoc_va = 0x46bed8
|
||||
|
||||
# Search for any instruction that loads these addresses (LEA, MOV, PUSH, etc.)
|
||||
targets = {setup_va: 'SETUP', drive_va: 'DRIVE', dcol_va: 'D:\\\\', havoc_va: 'HAVOC'}
|
||||
|
||||
# Also look for GetPrivateProfileString / GetPrivateProfileInt in the IAT
|
||||
# These are Windows API calls
|
||||
print("=== Searching for IAT calls to GetPrivateProfile* ===")
|
||||
for fo in range(0x400, len(data)-5):
|
||||
if data[fo] == 0xff and data[fo+1] == 0x15:
|
||||
# CALL [IAT_addr]
|
||||
iat_addr = struct.unpack_from('<I', data, fo+2)[0]
|
||||
# Read what's at the IAT to see if it's GetPrivateProfileString
|
||||
# Can't run it, but we can look for strings in the import table
|
||||
iat_fo = VA2FO(iat_addr)
|
||||
if 0 <= iat_fo < len(data) - 4:
|
||||
pass # Would need to resolve IAT at runtime
|
||||
|
||||
# Better: search for the strings "GetPrivateProfile" in the binary (in import names)
|
||||
for needle in [b'GetPrivateProfile', b'GetFileAttributes', b'FindFirstFile',
|
||||
b'CreateDirectory', b'PathFileExists']:
|
||||
pos = data.find(needle)
|
||||
while pos != -1:
|
||||
ctx = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data[pos:pos+32])
|
||||
print(" FO 0x%05x: %s" % (pos, ctx))
|
||||
pos = data.find(needle, pos+1)
|
||||
|
||||
print()
|
||||
print("=== Code that references SETUP/DRIVE/HAVOC strings ===")
|
||||
for target_va, name in targets.items():
|
||||
print("-- Target: %s = VA 0x%08x --" % (name, target_va))
|
||||
for fo in range(0x400, 0x67000):
|
||||
b = data[fo]
|
||||
# PUSH imm32
|
||||
if b == 0x68:
|
||||
imm = struct.unpack_from('<I', data, fo+1)[0]
|
||||
if imm == target_va:
|
||||
print(" PUSH at FO 0x%05x VA 0x%08x" % (fo, FO2VA(fo)))
|
||||
# MOV reg, imm32 (b8-bf)
|
||||
elif 0xb8 <= b <= 0xbf:
|
||||
imm = struct.unpack_from('<I', data, fo+1)[0]
|
||||
if imm == target_va:
|
||||
print(" MOV reg,imm at FO 0x%05x VA 0x%08x" % (fo, FO2VA(fo)))
|
||||
# MOV [mem], imm32 (c7 05)
|
||||
elif b == 0xc7 and fo+9 < len(data) and data[fo+1] == 0x05:
|
||||
imm = struct.unpack_from('<I', data, fo+6)[0]
|
||||
if imm == target_va:
|
||||
print(" MOV [mem],imm at FO 0x%05x VA 0x%08x" % (fo, FO2VA(fo)))
|
||||
# LEA reg, [mem] (8d xx)
|
||||
elif b == 0x8d and fo+6 < len(data):
|
||||
pass # harder to parse
|
||||
82
tools/find_ini_calls.py
Normal file
82
tools/find_ini_calls.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
# Find IAT addresses for GetPrivateProfileStringA and GetPrivateProfileIntA
|
||||
# by scanning the import directory
|
||||
# The strings are at:
|
||||
# FO 0x6F1C8: GetPrivateProfileStringA
|
||||
# FO 0x6F2D4: GetPrivateProfileIntA
|
||||
|
||||
# Find the IAT thunks: look for FF 15 xx xx xx xx (CALL [IAT])
|
||||
# The IAT entry itself should be near the import names
|
||||
# Let's find all CALL [mem] patterns and check what they point to
|
||||
|
||||
print("=== All CALL [mem] (FF 15) in .text section (FO 0x400-0x67000) ===")
|
||||
gp_str_calls = []
|
||||
gp_int_calls = []
|
||||
|
||||
# First: find the IAT addresses by looking at the import tables
|
||||
# Scan for references to the string offsets
|
||||
str_fo = 0x6f1c8
|
||||
int_fo = 0x6f2d4
|
||||
|
||||
# The IAT contains pointers to the DLL functions.
|
||||
# We need to find what VA the IAT entry for GetPrivateProfileStringA has.
|
||||
# In a PE, the IAT is typically at a fixed address. Let's scan for CALL [addr]
|
||||
# and then check the immediate to see if it's in the IAT range.
|
||||
|
||||
# Actually: let's scan all FF 15 calls and find ones where nearby code pushes
|
||||
# strings from the 0x46BE70 data block area.
|
||||
|
||||
# IAT is likely in the 0x482000-0x483000 range based on prior analysis
|
||||
iat_start = 0x482000
|
||||
iat_end = 0x483000
|
||||
|
||||
for fo in range(0x400, 0x67000):
|
||||
if data[fo] == 0xff and data[fo+1] == 0x15:
|
||||
iat_addr = struct.unpack_from('<I', data, fo+2)[0]
|
||||
if iat_start <= iat_addr < iat_end:
|
||||
# This is an IAT call. Check if it's GetPrivateProfile*
|
||||
# The IAT at runtime points to the function. But statically we need
|
||||
# to check the import names.
|
||||
# Just collect them all and print context
|
||||
# Check what was pushed before this call (look back 80 bytes)
|
||||
context_start = max(0x400, fo - 80)
|
||||
context_bytes = data[context_start:fo+6]
|
||||
context_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in context_bytes)
|
||||
if 'SETUP' in context_str or 'DRIVE' in context_str or 'HAVOC' in context_str:
|
||||
print(" IAT call at FO 0x%05x VA 0x%08x -> [0x%08x]" % (fo, FO2VA(fo), iat_addr))
|
||||
print(" Context: %s" % context_str[-60:])
|
||||
|
||||
print()
|
||||
# Better approach: look for the function that opens the INI file
|
||||
# GetPrivateProfileString calls should push the INI filename as last arg
|
||||
# HAVOC.INI = b'HAVOC.INI'
|
||||
havoc_ini_fo = data.find(b'HAVOC.INI\x00')
|
||||
if havoc_ini_fo == -1:
|
||||
havoc_ini_fo = data.find(b'HAVOC.ini\x00')
|
||||
print("HAVOC.INI string at FO 0x%05x VA 0x%08x" % (havoc_ini_fo, FO2VA(havoc_ini_fo)))
|
||||
|
||||
# Now find all pushes of HAVOC.INI's VA
|
||||
ini_va = FO2VA(havoc_ini_fo)
|
||||
print("=== Code pushing HAVOC.INI VA 0x%08x ===" % ini_va)
|
||||
for fo in range(0x400, 0x67000):
|
||||
if data[fo] == 0x68:
|
||||
imm = struct.unpack_from('<I', data, fo+1)[0]
|
||||
if imm == ini_va:
|
||||
print(" PUSH at FO 0x%05x VA 0x%08x" % (fo, FO2VA(fo)))
|
||||
|
||||
# Also look for GetFileAttributes / FindFirst style CD check
|
||||
print()
|
||||
print("=== Searching for GetFileAttributes IAT call context ===")
|
||||
# GetFileAttributes string in imports
|
||||
gfa_fo = data.find(b'GetFileAttributesA')
|
||||
if gfa_fo != -1:
|
||||
print("GetFileAttributesA import at FO 0x%05x" % gfa_fo)
|
||||
gff_fo = data.find(b'FindFirstFileA')
|
||||
if gff_fo != -1:
|
||||
print("FindFirstFileA import at FO 0x%05x" % gff_fo)
|
||||
20
tools/find_string.py
Normal file
20
tools/find_string.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
|
||||
# Search for "corrupted" and "unavailable" strings
|
||||
for needle in [b'corrupt', b'unavail', b'CORRUPT', b'UNAVAIL', b'not found', b'NOT FOUND',
|
||||
b'cannot', b'CANNOT', b'missing', b'MISSING', b'invalid', b'INVALID']:
|
||||
idx = 0
|
||||
while True:
|
||||
pos = data.find(needle, idx)
|
||||
if pos == -1: break
|
||||
# Print surrounding context
|
||||
start = max(0, pos - 4)
|
||||
end = min(len(data), pos + 60)
|
||||
snippet = data[start:end]
|
||||
printable = ''.join(chr(b) if 32 <= b < 127 else '.' for b in snippet)
|
||||
print("FO 0x%05x VA 0x%08x: %s" % (pos, FO2VA(pos), printable))
|
||||
idx = pos + 1
|
||||
120
tools/full_audit.py
Normal file
120
tools/full_audit.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import re, struct, sys
|
||||
|
||||
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
||||
|
||||
# ── 1. All NUL-terminated strings in rdata that look like paths/filenames ──
|
||||
print("=== Path/filename strings in rdata (0x067000+) ===")
|
||||
rdata = exe[0x067000:]
|
||||
base = 0x067000
|
||||
seen = set()
|
||||
for m in re.finditer(rb'(?:[\x21-\x7e]{3,60})(?=\x00)', rdata):
|
||||
s = m.group().decode('latin1')
|
||||
fo = base + m.start()
|
||||
if s in seen:
|
||||
continue
|
||||
# keep only things that look like filenames or paths
|
||||
has_dot = '.' in s
|
||||
has_bs = chr(0x5c) in s # backslash
|
||||
has_colon = ':' in s
|
||||
if not (has_dot or has_bs or has_colon):
|
||||
continue
|
||||
# skip long prose strings
|
||||
if len(s) > 50:
|
||||
continue
|
||||
seen.add(s)
|
||||
print(f" 0x{fo:06x}: {s!r}")
|
||||
|
||||
# ── 2. Find the IAT VA for every imported function ──
|
||||
# Scan the import descriptor table to map function name -> IAT file offset
|
||||
print("\n=== Imports (name -> IAT file offset -> VA) ===")
|
||||
iat_map = {} # name -> iat_va
|
||||
|
||||
# The IAT in HAVOC.EXE is around 0x06ec00 based on prior analysis.
|
||||
# Scan 0x06ec00..0x06fd00 for hint+name entries and build a name->IAT map.
|
||||
# Actually easier: find all FF 15 (CALL [mem]) targets in .text, then resolve names.
|
||||
call_indirect = {} # iat_va -> [call_fo, ...]
|
||||
for m in re.finditer(b'\xff\x15', exe[0x400:0x067000]):
|
||||
call_fo = 0x400 + m.start()
|
||||
iat_va = struct.unpack_from('<I', exe, call_fo + 2)[0]
|
||||
call_indirect.setdefault(iat_va, []).append(call_fo)
|
||||
|
||||
# For each IAT VA, try to resolve the function name via the image import dir.
|
||||
# image_base=0x400000. IAT file offset = iat_va - 0x400000 - 0x07b800 + 0x06ac00 (approx)
|
||||
# Easier: just scan the file around the known IAT area for function names.
|
||||
# Known: function names are NUL-terminated, preceded by a 2-byte hint in the INT.
|
||||
# We'll just scan the rdata for known API names and match to nearby IAT entries.
|
||||
|
||||
apis_of_interest = [
|
||||
b'MessageBoxA', b'CreateFileA', b'OpenFile', b'FindFirstFileA',
|
||||
b'GetVolumeInformationA', b'GetPrivateProfileStringA',
|
||||
b'WritePrivateProfileStringA', b'RegOpenKeyExA', b'RegQueryValueExA',
|
||||
b'SetCurrentDirectoryA', b'GetCurrentDirectoryA', b'GetModuleFileNameA',
|
||||
b'DirectDrawCreate', b'GetFileAttributesA',
|
||||
]
|
||||
resolved = {} # name -> iat_va
|
||||
for api in apis_of_interest:
|
||||
idx = exe.find(api)
|
||||
while idx != -1:
|
||||
# the hint is 2 bytes before the name; the IAT entry points here via the INT
|
||||
# Find which IAT entry references this (INT entry = &hint+name)
|
||||
hint_fo = idx - 2
|
||||
int_rva = 0x400000 + hint_fo - 0x000400 + 0x001000 # VA of hint record
|
||||
needle = struct.pack('<I', int_rva)
|
||||
# INT entries are in the IDT (idata section); find the IAT address
|
||||
for m in re.finditer(re.escape(needle), exe[0x06ac00:0x06fc00]):
|
||||
int_fo_in_idata = 0x06ac00 + m.start()
|
||||
# The IAT entry is at the same index in the IAT array.
|
||||
# Distance from IDT import entry start tells us which DLL/ordinal.
|
||||
# Shortcut: find CALL [mem] in .text where the target dword equals
|
||||
# the runtime-filled IAT value. We can't do that statically, but
|
||||
# we know the IAT RVA = INT RVA (before loading). So IAT entry at
|
||||
# int_fo_in_idata contains the same pointer before load.
|
||||
iat_va = 0x400000 + int_fo_in_idata - 0x000400 + 0x001000
|
||||
name = api.rstrip(b'\x00').decode()
|
||||
resolved[name] = (int_fo_in_idata, iat_va)
|
||||
if iat_va in call_indirect:
|
||||
sites = call_indirect[iat_va]
|
||||
print(f" {name}: IAT 0x{iat_va:08x}, called from: {[hex(f) for f in sites[:6]]}")
|
||||
idx = exe.find(api, idx + 1)
|
||||
|
||||
# ── 3. Trace MessageBoxA call sites ──
|
||||
print("\n=== MessageBoxA call sites + pushed string VAs ===")
|
||||
if 'MessageBoxA' in resolved:
|
||||
_, mbox_iat_va = resolved['MessageBoxA']
|
||||
needle = b'\xff\x15' + struct.pack('<I', mbox_iat_va)
|
||||
for m in re.finditer(re.escape(needle), exe[0x400:0x067000]):
|
||||
call_fo = 0x400 + m.start()
|
||||
# look backward up to 64 bytes for PUSH imm32 (68 xx xx xx xx)
|
||||
ctx = exe[max(0x400, call_fo-64):call_fo+8]
|
||||
pushes = []
|
||||
for pm in re.finditer(b'\x68', ctx):
|
||||
va = struct.unpack_from('<I', ctx, pm.start()+1)[0]
|
||||
fo = va - 0x400000 - 0x001000 + 0x000400
|
||||
if 0x067000 <= fo < len(exe):
|
||||
try:
|
||||
s = exe[fo:exe.index(b'\x00', fo)].decode('latin1', errors='replace')
|
||||
pushes.append(repr(s[:40]))
|
||||
except:
|
||||
pass
|
||||
print(f" CALL @ 0x{call_fo:06x} pushed strings: {pushes}")
|
||||
|
||||
# ── 4. Find STRDATA error display: look for references to string-index 6 ──
|
||||
# The game likely calls a helper like ShowError(int index) or
|
||||
# uses STRDATA string pointers. Find code that pushes small integers (0-10)
|
||||
# before calling a common sub_ function.
|
||||
print("\n=== Functions called with small int arg (error index?) ===")
|
||||
# Find all PUSH 0x06 (error string 6) = 6a 06, followed within 20 bytes by a CALL
|
||||
for m in re.finditer(b'\x6a\x06', exe[0x400:0x067000]):
|
||||
fo = 0x400 + m.start()
|
||||
ctx = exe[fo:fo+30]
|
||||
if b'\xe8' in ctx[2:22]: # there's a CALL nearby
|
||||
print(f" PUSH 6; near 0x{fo:06x}: {ctx.hex(' ')}")
|
||||
|
||||
print("\n=== All remaining C:\\ byte sequences ===")
|
||||
c_colon_bs = bytes([0x43, 0x3a, 0x5c])
|
||||
for m in re.finditer(re.escape(c_colon_bs), exe):
|
||||
fo = m.start()
|
||||
ctx = exe[fo:fo+30]
|
||||
print(f" 0x{fo:06x}: {ctx!r}")
|
||||
|
||||
print("\nDone.")
|
||||
110
tools/ini_audit.py
Normal file
110
tools/ini_audit.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import re, struct
|
||||
|
||||
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
||||
|
||||
# ── Step 1: Find GetPrivateProfileStringA in the import name table ──
|
||||
# Import names are stored as: hint(2 bytes) + NUL-terminated name
|
||||
# Scan ALL occurrences of this name in the file
|
||||
gpps_positions = []
|
||||
for m in re.finditer(b'GetPrivateProfileStringA\x00', exe):
|
||||
gpps_positions.append(m.start())
|
||||
print(f"GetPrivateProfileStringA name at file offset 0x{m.start():06x}")
|
||||
|
||||
if not gpps_positions:
|
||||
print("NOT FOUND in EXE")
|
||||
import sys; sys.exit(1)
|
||||
|
||||
# The IAT entry for this function is at a dword that originally holds the INT pointer.
|
||||
# Find that dword by scanning for the VA of each occurrence as an INT entry.
|
||||
# Then find CALL [mem] (FF 15 xx xx xx xx) instructions referencing the IAT slot.
|
||||
|
||||
print("\n=== Scanning for CALL GetPrivateProfileStringA ===")
|
||||
call_sites = []
|
||||
for name_fo in gpps_positions:
|
||||
# The INT entry value = VA of hint+name = image_base + RVA of (name_fo - 2)
|
||||
hint_fo = name_fo - 2
|
||||
# This is in a different section (idata), compute its VA
|
||||
# idata is usually mapped near rdata; let's scan for its VA as a dword anywhere
|
||||
# Actually: try both rdata and idata VA mappings
|
||||
for section_offset, section_rva in [(0x000400, 0x001000), (0x06ac00, 0x07bc00)]:
|
||||
rva_of_hint = (hint_fo - section_offset + section_rva)
|
||||
va_of_hint = 0x400000 + rva_of_hint
|
||||
needle = struct.pack('<I', va_of_hint)
|
||||
# Find this VA in the IAT (it's in an IAT array somewhere before loading)
|
||||
for m in re.finditer(re.escape(needle), exe[0x06ac00:0x06fc00]):
|
||||
iat_fo = 0x06ac00 + m.start()
|
||||
iat_rva = iat_fo - 0x000400 + 0x001000
|
||||
iat_va = 0x400000 + iat_rva
|
||||
# Find CALL [iat_va] in .text
|
||||
call_needle = b'\xff\x15' + struct.pack('<I', iat_va)
|
||||
for cm in re.finditer(re.escape(call_needle), exe[0x400:0x067000]):
|
||||
cfo = 0x400 + cm.start()
|
||||
if cfo not in call_sites:
|
||||
call_sites.append(cfo)
|
||||
print(f" CALL @ 0x{cfo:06x} (via IAT @ 0x{iat_fo:06x})")
|
||||
|
||||
if not call_sites:
|
||||
print(" None found via IAT scan -- trying direct search")
|
||||
# Fallback: search for CALL DWORD PTR (FF 15) followed by any dword in idata range
|
||||
# and check if it resolves to GetPrivateProfileStringA via hint table
|
||||
for m in re.finditer(b'\xff\x15', exe[0x400:0x067000]):
|
||||
cfo = 0x400 + m.start()
|
||||
iat_va = struct.unpack_from('<I', exe, cfo+2)[0]
|
||||
iat_fo = iat_va - 0x400000 - 0x001000 + 0x000400
|
||||
if 0x06ac00 <= iat_fo < 0x06fc00:
|
||||
# read the dword at iat_fo -- pre-load it's the INT pointer (hint+name VA)
|
||||
int_ptr = struct.unpack_from('<I', exe, iat_fo)[0]
|
||||
hint_name_fo = int_ptr - 0x400000 - 0x001000 + 0x000400
|
||||
if 0 < hint_name_fo < len(exe) - 30:
|
||||
try:
|
||||
name = exe[hint_name_fo+2 : exe.index(b'\x00', hint_name_fo+2)].decode('latin1')
|
||||
if name == 'GetPrivateProfileStringA':
|
||||
call_sites.append(cfo)
|
||||
print(f" CALL @ 0x{cfo:06x}")
|
||||
except:
|
||||
pass
|
||||
|
||||
# ── Step 2: For each call site, walk backward to find pushed args ──
|
||||
# GetPrivateProfileStringA(lpAppName, lpKeyName, lpDefault, lpReturnedString, nSize, lpFileName)
|
||||
# That's 6 args. In stdcall, they're pushed right-to-left.
|
||||
# Push order: lpFileName, nSize, lpReturnedString, lpDefault, lpKeyName, lpAppName
|
||||
|
||||
def read_string(exe, va):
|
||||
fo = va - 0x400000 - 0x001000 + 0x000400
|
||||
if fo < 0 or fo >= len(exe):
|
||||
return f"<VA 0x{va:08x}>"
|
||||
try:
|
||||
end = exe.index(b'\x00', fo)
|
||||
return exe[fo:end].decode('latin1', errors='replace')
|
||||
except:
|
||||
return f"<err@0x{fo:06x}>"
|
||||
|
||||
print("\n=== GetPrivateProfileStringA parameters at each call site ===")
|
||||
for cfo in sorted(call_sites):
|
||||
# Scan backward up to 128 bytes for PUSH imm32 (0x68) instructions
|
||||
ctx_start = max(0x400, cfo - 128)
|
||||
ctx = exe[ctx_start:cfo]
|
||||
pushes = [] # list of (offset_in_ctx, value)
|
||||
pos = 0
|
||||
while pos < len(ctx):
|
||||
b = ctx[pos]
|
||||
if b == 0x68: # PUSH imm32
|
||||
if pos + 5 <= len(ctx):
|
||||
val = struct.unpack_from('<I', ctx, pos+1)[0]
|
||||
pushes.append((ctx_start + pos, val))
|
||||
pos += 5
|
||||
elif b == 0x6a: # PUSH imm8
|
||||
pushes += [] # ignore small int pushes for now
|
||||
pos += 2
|
||||
else:
|
||||
pos += 1
|
||||
# Last 3-4 imm32 pushes before the call are likely lpDefault, lpKeyName, lpAppName
|
||||
# (lpFileName is typically pushed early, nSize is a small constant)
|
||||
imm_pushes = [v for _, v in pushes[-6:]]
|
||||
args = []
|
||||
for v in imm_pushes:
|
||||
if 0x400000 <= v <= 0x500000:
|
||||
args.append(repr(read_string(exe, v)))
|
||||
else:
|
||||
args.append(hex(v))
|
||||
print(f" 0x{cfo:06x}: {' | '.join(args)}")
|
||||
57
tools/io_audit.py
Normal file
57
tools/io_audit.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import re, struct
|
||||
|
||||
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
||||
|
||||
# ── 1. Dump ALL imported function names ──
|
||||
# Scan the file for the import name table: look for blocks of hint(2) + name(NUL-terminated)
|
||||
# that look like Win32 API names
|
||||
print("=== All imported function names ===")
|
||||
seen = set()
|
||||
for m in re.finditer(rb'\x00\x00([A-Za-z_][A-Za-z0-9_]{3,40})\x00', exe[0x06ac00:0x06fc00]):
|
||||
name = m.group(1).decode('latin1')
|
||||
if name not in seen:
|
||||
seen.add(name)
|
||||
print(f" {name}")
|
||||
|
||||
# ── 2. Find GetModuleFileNameA call sites ──
|
||||
print("\n=== GetModuleFileNameA call sites ===")
|
||||
idx = exe.find(b'GetModuleFileNameA\x00')
|
||||
if idx != -1:
|
||||
hint_fo = idx - 2
|
||||
int_rva = hint_fo - 0x000400 + 0x001000
|
||||
iat_needle = struct.pack('<I', 0x400000 + int_rva)
|
||||
for m in re.finditer(b'\xff\x15' + re.escape(iat_needle), exe[0x400:0x067000]):
|
||||
call_fo = 0x400 + m.start()
|
||||
ctx = exe[call_fo:call_fo+64]
|
||||
print(f" @ 0x{call_fo:06x}: {ctx.hex(' ')}")
|
||||
|
||||
# ── 3. Scan for all string refs in .text that point to rdata strings ──
|
||||
# Push imm32 (68 xx xx xx xx) where VA points into rdata
|
||||
print("\n=== PUSH <rdata string VA> in .text (file load refs) ──")
|
||||
# Rdata VA range: roughly 0x400000 + (0x067000 - 0x400 + 0x1000) = 0x400000 + 0x067c00 = 0x467c00
|
||||
# to end of file
|
||||
rdata_va_start = 0x400000 + 0x067000 - 0x000400 + 0x001000
|
||||
rdata_va_end = 0x400000 + len(exe) - 0x000400 + 0x001000
|
||||
interesting_strings = {}
|
||||
# Build map of rdata VA -> string
|
||||
rdata = exe[0x067000:]
|
||||
for m in re.finditer(rb'[A-Za-z][A-Za-z0-9_.\\]{3,30}\x00', rdata):
|
||||
s = m.group()[:-1].decode('latin1')
|
||||
if '.' in s or '\\' in s:
|
||||
va = rdata_va_start - 0x067000 + 0x067000 + m.start()
|
||||
# recalc: rdata file offset = 0x067000 + m.start()
|
||||
fo = 0x067000 + m.start()
|
||||
va = 0x400000 + fo - 0x000400 + 0x001000
|
||||
interesting_strings[va] = s
|
||||
|
||||
# Now scan .text for pushes of these VAs
|
||||
for m in re.finditer(b'\x68', exe[0x400:0x067000]):
|
||||
call_fo = 0x400 + m.start()
|
||||
if call_fo + 5 > 0x067000:
|
||||
continue
|
||||
va = struct.unpack_from('<I', exe, call_fo + 1)[0]
|
||||
if va in interesting_strings:
|
||||
s = interesting_strings[va]
|
||||
# Only show file-related strings
|
||||
ctx_after = exe[call_fo + 5: call_fo + 15]
|
||||
print(f" PUSH {s!r:30s} @ 0x{call_fo:06x} next: {ctx_after.hex(' ')}")
|
||||
112
tools/parse_iat.py
Normal file
112
tools/parse_iat.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import struct, re
|
||||
|
||||
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
||||
base = 0x00400000
|
||||
|
||||
def rva2fo(rva):
|
||||
# Sections from pe_sections.py output
|
||||
sections = [
|
||||
(".text", 0x00001000, 0x00066bbd, 0x00000400, 0x00066c00),
|
||||
(".rdata", 0x00079000, 0x000023c0, 0x00067000, 0x00002400),
|
||||
(".data", 0x0007c000, 0x00005650, 0x00069400, 0x00005800),
|
||||
(".idata", 0x00082000, 0x00000d5a, 0x0006ec00, 0x00000e00),
|
||||
(".rsrc", 0x00083000, 0x0000080c, 0x0006fa00, 0x00000a00),
|
||||
(".reloc", 0x00084000, 0x000063ba, 0x00070400, 0x00006400),
|
||||
]
|
||||
for (name, vrva, vsz, raw, rsz) in sections:
|
||||
if vrva <= rva < vrva + vsz:
|
||||
return raw + (rva - vrva)
|
||||
return None
|
||||
|
||||
# Data directory entry 1 = import table
|
||||
pe_off = 0x80
|
||||
opt_off = pe_off + 24
|
||||
import_rva = struct.unpack_from('<I', exe, opt_off + 104)[0]
|
||||
import_size = struct.unpack_from('<I', exe, opt_off + 108)[0]
|
||||
print(f"Import directory RVA: 0x{import_rva:08x} size: 0x{import_size:08x}")
|
||||
import_fo = rva2fo(import_rva)
|
||||
print(f"Import directory file offset: 0x{import_fo:06x}")
|
||||
|
||||
# Parse IMAGE_IMPORT_DESCRIPTOR array (each 20 bytes, terminated by all-zeros)
|
||||
pos = import_fo
|
||||
gpps_iat_va = None
|
||||
all_funcs = {} # func_name -> iat_va
|
||||
|
||||
while True:
|
||||
orig_thunk_rva = struct.unpack_from('<I', exe, pos)[0]
|
||||
name_rva = struct.unpack_from('<I', exe, pos + 12)[0]
|
||||
first_thunk_rva= struct.unpack_from('<I', exe, pos + 16)[0]
|
||||
if orig_thunk_rva == 0 and name_rva == 0:
|
||||
break
|
||||
|
||||
dll_name_fo = rva2fo(name_rva)
|
||||
dll_name = exe[dll_name_fo : exe.index(b'\x00', dll_name_fo)].decode('latin1') if dll_name_fo else "?"
|
||||
print(f"\nDLL: {dll_name} INT_RVA=0x{orig_thunk_rva:08x} IAT_RVA=0x{first_thunk_rva:08x}")
|
||||
|
||||
# Walk INT (OriginalFirstThunk array)
|
||||
int_fo = rva2fo(orig_thunk_rva)
|
||||
iat_fo = rva2fo(first_thunk_rva)
|
||||
if not int_fo or not iat_fo:
|
||||
pos += 20
|
||||
continue
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
entry_rva = struct.unpack_from('<I', exe, int_fo + i*4)[0]
|
||||
if entry_rva == 0:
|
||||
break
|
||||
# High bit = import by ordinal
|
||||
if entry_rva & 0x80000000:
|
||||
ordinal = entry_rva & 0x7fff
|
||||
func_name = f"ord_{ordinal}"
|
||||
else:
|
||||
name_entry_fo = rva2fo(entry_rva)
|
||||
if name_entry_fo:
|
||||
func_name = exe[name_entry_fo+2 : exe.index(b'\x00', name_entry_fo+2)].decode('latin1', errors='replace')
|
||||
else:
|
||||
func_name = f"rva_{entry_rva:08x}"
|
||||
|
||||
iat_slot_fo = iat_fo + i*4
|
||||
iat_slot_va = base + first_thunk_rva + i*4
|
||||
all_funcs[func_name] = iat_slot_va
|
||||
if 'PrivateProfile' in func_name or 'CreateFile' in func_name or 'OpenFile' in func_name or 'GetModule' in func_name or 'lopen' in func_name.lower() or 'SetCurrentDir' in func_name:
|
||||
print(f" [{i:3d}] {func_name:<40} IAT slot VA: 0x{iat_slot_va:08x}")
|
||||
i += 1
|
||||
|
||||
pos += 20
|
||||
|
||||
# Now find CALL [GetPrivateProfileStringA_IAT_VA] in .text
|
||||
print("\n=== GetPrivateProfileStringA call sites ===")
|
||||
if 'GetPrivateProfileStringA' in all_funcs:
|
||||
iat_va = all_funcs['GetPrivateProfileStringA']
|
||||
print(f" IAT slot VA: 0x{iat_va:08x}")
|
||||
call_needle = b'\xff\x15' + struct.pack('<I', iat_va)
|
||||
for m in re.finditer(re.escape(call_needle), exe[0x400:0x067000]):
|
||||
cfo = 0x400 + m.start()
|
||||
print(f" CALL @ 0x{cfo:06x}")
|
||||
# extract pushed args (last 3 PUSH imm32 before the call)
|
||||
ctx = exe[max(0x400, cfo-128):cfo]
|
||||
pushes = []
|
||||
for pm in re.finditer(b'\x68....', ctx, re.DOTALL):
|
||||
val = struct.unpack_from('<I', ctx, pm.start()+1)[0]
|
||||
if 0x400000 <= val <= 0x500000:
|
||||
fo = rva2fo(val - 0x400000)
|
||||
if fo and fo < len(exe):
|
||||
try:
|
||||
s = exe[fo : exe.index(b'\x00', fo)].decode('latin1', errors='replace')[:30]
|
||||
pushes.append(repr(s))
|
||||
except:
|
||||
pushes.append(hex(val))
|
||||
else:
|
||||
pushes.append(hex(val))
|
||||
print(f" string args: {pushes}")
|
||||
else:
|
||||
print(" GetPrivateProfileStringA not found in resolved imports!")
|
||||
print(" Available funcs with 'Profile':", [k for k in all_funcs if 'Profile' in k])
|
||||
|
||||
# Also show all resolved file I/O functions
|
||||
print("\n=== File I/O related imports ===")
|
||||
io_keywords = ['File', 'Open', 'Read', 'Write', 'Create', 'Handle', 'lopen', 'Profile', 'Module']
|
||||
for name, va in sorted(all_funcs.items()):
|
||||
if any(kw.lower() in name.lower() for kw in io_keywords):
|
||||
print(f" {name:<45} IAT VA: 0x{va:08x}")
|
||||
80
tools/parse_imports.py
Normal file
80
tools/parse_imports.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
data = open('HAVOC_NOCD.EXE','rb').read()
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
VA2FO = lambda va: va - 0x400C00
|
||||
|
||||
# Parse PE import table to find GetPrivateProfileStringA IAT entry
|
||||
# PE header
|
||||
e_lfanew = struct.unpack_from('<I', data, 0x3c)[0]
|
||||
pe_off = e_lfanew
|
||||
sig = data[pe_off:pe_off+4]
|
||||
print("PE sig: %s" % sig)
|
||||
|
||||
# COFF header
|
||||
machine = struct.unpack_from('<H', data, pe_off+4)[0]
|
||||
num_sections = struct.unpack_from('<H', data, pe_off+6)[0]
|
||||
opt_hdr_size = struct.unpack_from('<H', data, pe_off+20)[0]
|
||||
|
||||
# Optional header
|
||||
opt_off = pe_off + 24
|
||||
imagebase = struct.unpack_from('<I', data, opt_off+28)[0]
|
||||
print("ImageBase: 0x%08x" % imagebase)
|
||||
|
||||
# Data directories
|
||||
dd_off = opt_off + 96 # offset to data directories in optional header
|
||||
import_rva = struct.unpack_from('<I', data, dd_off + 8)[0]
|
||||
import_size = struct.unpack_from('<I', data, dd_off + 12)[0]
|
||||
print("Import table RVA: 0x%08x, size: 0x%x" % (import_rva, import_size))
|
||||
|
||||
# Convert import RVA to file offset
|
||||
import_fo = import_rva # RVA = FO + 0x400 - 0x1000? Let's try VA2FO
|
||||
import_fo_calc = VA2FO(imagebase + import_rva)
|
||||
print("Import table FO (calc): 0x%05x" % import_fo_calc)
|
||||
|
||||
# Parse import descriptors
|
||||
fo = import_fo_calc
|
||||
while fo < len(data) - 20:
|
||||
orig_first_thunk = struct.unpack_from('<I', data, fo)[0]
|
||||
timedatestamp = struct.unpack_from('<I', data, fo+4)[0]
|
||||
forwarder_chain = struct.unpack_from('<I', data, fo+8)[0]
|
||||
name_rva = struct.unpack_from('<I', data, fo+12)[0]
|
||||
first_thunk = struct.unpack_from('<I', data, fo+16)[0]
|
||||
if orig_first_thunk == 0 and name_rva == 0:
|
||||
break
|
||||
name_fo = VA2FO(imagebase + name_rva)
|
||||
dll_name = b''
|
||||
nf = name_fo
|
||||
while nf < len(data) and data[nf] != 0:
|
||||
dll_name += bytes([data[nf]])
|
||||
nf += 1
|
||||
print("\nDLL: %s (FirstThunk RVA=0x%08x, FO=0x%05x)" %
|
||||
(dll_name.decode('ascii','replace'), first_thunk, VA2FO(imagebase + first_thunk)))
|
||||
|
||||
# Parse thunks to find GetPrivateProfile*
|
||||
thunk_fo = VA2FO(imagebase + first_thunk)
|
||||
thunk_idx = 0
|
||||
while thunk_fo < len(data) - 4:
|
||||
thunk_val = struct.unpack_from('<I', data, thunk_fo)[0]
|
||||
if thunk_val == 0:
|
||||
break
|
||||
# Check if it's an ordinal import
|
||||
if thunk_val & 0x80000000:
|
||||
pass
|
||||
else:
|
||||
hint_fo = VA2FO(imagebase + thunk_val)
|
||||
if 0 <= hint_fo < len(data) - 2:
|
||||
fn_name_fo = hint_fo + 2 # skip hint word
|
||||
fn_name = b''
|
||||
fn = fn_name_fo
|
||||
while fn < len(data) and data[fn] != 0:
|
||||
fn_name += bytes([data[fn]])
|
||||
fn += 1
|
||||
fn_str = fn_name.decode('ascii','replace')
|
||||
iat_va = imagebase + first_thunk + thunk_idx * 4
|
||||
if 'Private' in fn_str or 'File' in fn_str or 'Find' in fn_str:
|
||||
print(" [0x%08x] -> %s" % (iat_va, fn_str))
|
||||
thunk_fo += 4
|
||||
thunk_idx += 1
|
||||
fo += 20
|
||||
114
tools/patch_exappwindow.py
Normal file
114
tools/patch_exappwindow.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import struct, sys
|
||||
|
||||
# Set TEST_MODE=True to use dwExStyle=0 (original), False for WS_EX_APPWINDOW
|
||||
TEST_MODE = True
|
||||
|
||||
exe = bytearray(open("HAVOC_NOCD.EXE", "rb").read())
|
||||
BASE = 0x400000
|
||||
|
||||
# ---- Locate .text section header -------------------------------------------
|
||||
e_lfanew = struct.unpack_from('<I', exe, 0x3c)[0]
|
||||
coff_off = e_lfanew + 4
|
||||
num_sects = struct.unpack_from('<H', exe, coff_off + 2)[0]
|
||||
opt_size = struct.unpack_from('<H', exe, coff_off + 16)[0]
|
||||
sects_off = coff_off + 20 + opt_size
|
||||
|
||||
TEXT_SECHDOFF = None
|
||||
for i in range(num_sects):
|
||||
sh = sects_off + i * 40
|
||||
name = exe[sh:sh+8].rstrip(b'\x00')
|
||||
if name == b'.text':
|
||||
TEXT_SECHDOFF = sh
|
||||
break
|
||||
|
||||
if TEXT_SECHDOFF is None:
|
||||
print("ERROR: .text section not found"); sys.exit(1)
|
||||
|
||||
sh = TEXT_SECHDOFF
|
||||
TEXT_VRVA = struct.unpack_from('<I', exe, sh+12)[0]
|
||||
TEXT_VSIZ = struct.unpack_from('<I', exe, sh+8)[0]
|
||||
TEXT_RAWSIZ = struct.unpack_from('<I', exe, sh+16)[0]
|
||||
TEXT_RAWOFF = struct.unpack_from('<I', exe, sh+20)[0]
|
||||
|
||||
print(f".text: VirtRVA=0x{TEXT_VRVA:x} VirtSize=0x{TEXT_VSIZ:x} "
|
||||
f"RawOff=0x{TEXT_RAWOFF:x} RawSize=0x{TEXT_RAWSIZ:x}")
|
||||
|
||||
def va2fo(va):
|
||||
rva = va - BASE
|
||||
return TEXT_RAWOFF + (rva - TEXT_VRVA)
|
||||
|
||||
# ---- Cave: placed in raw padding beyond ORIGINAL VirtualSize ----------------
|
||||
# Hardcode original VirtualSize so this script is safe to re-run even after
|
||||
# VirtualSize has already been extended to RawSize.
|
||||
ORIG_TEXT_VSIZ = 0x66bbd
|
||||
cave_fo = TEXT_RAWOFF + ORIG_TEXT_VSIZ # FO 0x66fbd (raw padding start)
|
||||
cave_va = BASE + TEXT_VRVA + ORIG_TEXT_VSIZ # VA 0x467bbd
|
||||
PATCH_VA = 0x4260c7
|
||||
patch_fo = va2fo(PATCH_VA)
|
||||
BACK_VA = 0x4260d3
|
||||
|
||||
print(f"Cave FO=0x{cave_fo:05x} VA=0x{cave_va:07x}")
|
||||
|
||||
# Verify cave area is zero/CC/or previously-patched bytes (safe to overwrite)
|
||||
cave_bytes = exe[cave_fo:cave_fo+20]
|
||||
all_safe = all(b in (0x00, 0xCC) for b in cave_bytes)
|
||||
print(f"Cave current bytes: {cave_bytes.hex(' ')}")
|
||||
if not all_safe:
|
||||
print("(overwriting previous cave code)")
|
||||
|
||||
# ---- Verify patch site (may already be patched; accept either) -------------
|
||||
original_12 = bytes.fromhex("740051689cda47006a00ffd0")
|
||||
actual_12 = bytes(exe[patch_fo:patch_fo+12])
|
||||
if actual_12 == original_12:
|
||||
print("Patch site: original (unpatched)")
|
||||
elif actual_12[0] == 0x51 and actual_12[1] == 0xe9:
|
||||
print("Patch site: already patched -- overwriting")
|
||||
else:
|
||||
print(f"ERROR: unexpected bytes at patch site: {actual_12.hex(' ')}")
|
||||
sys.exit(1)
|
||||
|
||||
# ---- Cave code: explicit pushes (no LEA trick) to minimise risk -----------
|
||||
# Push className as immediate, not computed.
|
||||
# arg3 (title/lpWindowName) is pushed at patch site via 'push ecx'.
|
||||
# Cave pushes arg2 (className) and arg1 (dwExStyle), then calls CreateWindowExA.
|
||||
CLASSNAME_VA = 0x0047da9c
|
||||
EX_STYLE = 0 if TEST_MODE else 0x00040000
|
||||
print(f"dwExStyle = 0x{EX_STYLE:08x} ({'TEST: original style' if TEST_MODE else 'WS_EX_APPWINDOW'})")
|
||||
|
||||
cave_code = bytearray()
|
||||
cave_code += b'\x68' + struct.pack('<I', CLASSNAME_VA) # push className (5b)
|
||||
cave_code += b'\x68' + struct.pack('<I', EX_STYLE) # push dwExStyle (5b)
|
||||
cave_code += b'\xFF\xD0' # call eax (2b)
|
||||
jmp_back = BACK_VA - (cave_va + len(cave_code) + 5)
|
||||
cave_code += b'\xE9' + struct.pack('<i', jmp_back) # jmp BACK_VA (5b)
|
||||
|
||||
CAVE_SIZE = len(cave_code) # 17
|
||||
print(f"Cave ({CAVE_SIZE}b): {cave_code.hex(' ')}")
|
||||
print(f" jmp_back = {jmp_back:+d} -> IP after jmp = 0x{cave_va+CAVE_SIZE:07x}, "
|
||||
f"target = 0x{cave_va+CAVE_SIZE+jmp_back:07x}")
|
||||
|
||||
# ---- Patch (12 bytes at 0x4260c7) -----------------------------------------
|
||||
jmp_cave = cave_va - (PATCH_VA + 1 + 5)
|
||||
patch = bytearray()
|
||||
patch += b'\x51' # push ecx (title) (1b)
|
||||
patch += b'\xE9' + struct.pack('<i', jmp_cave) # jmp cave (5b)
|
||||
patch += b'\x90' * 6 # 6x NOP (6b)
|
||||
print(f"Patch (12b): {patch.hex(' ')}")
|
||||
print(f" jmp_cave = {jmp_cave:+d} -> IP after jmp = 0x{PATCH_VA+6:07x}, "
|
||||
f"target = 0x{PATCH_VA+6+jmp_cave:07x}")
|
||||
|
||||
# Confirm jump targets
|
||||
assert PATCH_VA + 6 + jmp_cave == cave_va, "jmp_cave target mismatch"
|
||||
assert cave_va + CAVE_SIZE + jmp_back == BACK_VA, "jmp_back target mismatch"
|
||||
print("Jump target assertions: OK")
|
||||
|
||||
# ---- Apply -----------------------------------------------------------------
|
||||
new_vsiz = TEXT_RAWSIZ # expose all raw data as executable
|
||||
struct.pack_into('<I', exe, sh + 8, new_vsiz)
|
||||
print(f".text VirtualSize: 0x{TEXT_VSIZ:x} -> 0x{new_vsiz:x}")
|
||||
|
||||
exe[cave_fo:cave_fo+CAVE_SIZE] = cave_code
|
||||
exe[patch_fo:patch_fo+12] = patch
|
||||
|
||||
open("HAVOC_NOCD.EXE", "wb").write(exe)
|
||||
print("Patch R applied.")
|
||||
36
tools/patch_flagcheck.py
Normal file
36
tools/patch_flagcheck.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"""Patch J: bypass the init-flag error gate at 0x020277.
|
||||
|
||||
The instruction at 0x020277 is JNZ +0x2e (75 2e). When the init flag at
|
||||
VA 0x475425 is zero the fall-through path shows the 'corrupt files' error.
|
||||
The jump target (0x0202a7) is the success return stub:
|
||||
b0 01 MOV AL, 1
|
||||
5d 5f 5e 5b POP EBP/EDI/ESI/EBX
|
||||
83 c4 44 ADD ESP, 0x44
|
||||
c2 0c 00 RET 0x0c
|
||||
|
||||
Changing 75->eb turns JNZ into JMP so execution always takes the success path.
|
||||
"""
|
||||
import struct
|
||||
|
||||
with open("HAVOC_NOCD.EXE", "rb") as f:
|
||||
exe = bytearray(f.read())
|
||||
|
||||
PATCH_FO = 0x020277
|
||||
# Verify expected bytes
|
||||
expected = bytes([0x75, 0x2e])
|
||||
actual = bytes(exe[PATCH_FO:PATCH_FO+2])
|
||||
if actual != expected:
|
||||
print(f"ERROR: expected {expected.hex(' ')} at 0x{PATCH_FO:06x}, got {actual.hex(' ')}")
|
||||
raise SystemExit(1)
|
||||
|
||||
exe[PATCH_FO] = 0xeb # JNZ -> JMP
|
||||
|
||||
# Verify target bytes at 0x0202a7
|
||||
target_bytes = bytes(exe[0x0202a7:0x0202a7+12])
|
||||
print(f"Patch J target bytes at 0x0202a7: {target_bytes.hex(' ')}")
|
||||
# should be: b0 01 5d 5f 5e 5b 83 c4 44 c2 0c 00
|
||||
|
||||
with open("HAVOC_NOCD.EXE", "wb") as f:
|
||||
f.write(exe)
|
||||
|
||||
print(f"Patch J applied: 0x{PATCH_FO:06x}: 75 2e -> eb 2e (JNZ -> JMP, bypass init-flag check)")
|
||||
51
tools/patch_paths.py
Normal file
51
tools/patch_paths.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"""Patch hardcoded C:\ path prefixes out of HAVOC_NOCD.EXE so it loads files
|
||||
from the current directory instead of C:\ root."""
|
||||
|
||||
exe = bytearray(open("HAVOC_NOCD.EXE", "rb").read())
|
||||
|
||||
# Each tuple: (bytes_to_find, bytes_to_replace, description)
|
||||
# Lengths must match -- we strip "C:\" (3 bytes) and pad with nulls at the end.
|
||||
patches = [
|
||||
# C:\MUSIC.FF\0 (12 bytes) -> MUSIC.FF\0 + 3 nulls
|
||||
(
|
||||
bytes([0x43,0x3a,0x5c,0x4d,0x55,0x53,0x49,0x43,0x2e,0x46,0x46,0x00]),
|
||||
bytes([ 0x4d,0x55,0x53,0x49,0x43,0x2e,0x46,0x46,0x00,0x00,0x00,0x00]),
|
||||
"C:\\MUSIC.FF -> MUSIC.FF",
|
||||
),
|
||||
# C:\INTRFACE.FF\0\0 (16 bytes) -> INTRFACE.FF\0 + 4 nulls
|
||||
(
|
||||
bytes([0x43,0x3a,0x5c,0x49,0x4e,0x54,0x52,0x46,0x41,0x43,0x45,0x2e,0x46,0x46,0x00,0x00]),
|
||||
bytes([ 0x49,0x4e,0x54,0x52,0x46,0x41,0x43,0x45,0x2e,0x46,0x46,0x00,0x00,0x00,0x00,0x00]),
|
||||
"C:\\INTRFACE.FF -> INTRFACE.FF",
|
||||
),
|
||||
# C:\WORLDS\ \0\0 (12 bytes) -> WORLDS\ \0 + 4 nulls
|
||||
(
|
||||
bytes([0x43,0x3a,0x5c,0x57,0x4f,0x52,0x4c,0x44,0x53,0x5c,0x00,0x00]),
|
||||
bytes([ 0x57,0x4f,0x52,0x4c,0x44,0x53,0x5c,0x00,0x00,0x00,0x00,0x00]),
|
||||
"C:\\WORLDS\\ -> WORLDS\\",
|
||||
),
|
||||
# C:\BIGFILE.DAT\0\0 (16 bytes) -> BIGFILE.DAT\0 + 4 nulls
|
||||
(
|
||||
bytes([0x43,0x3a,0x5c,0x42,0x49,0x47,0x46,0x49,0x4c,0x45,0x2e,0x44,0x41,0x54,0x00,0x00]),
|
||||
bytes([ 0x42,0x49,0x47,0x46,0x49,0x4c,0x45,0x2e,0x44,0x41,0x54,0x00,0x00,0x00,0x00,0x00]),
|
||||
"C:\\BIGFILE.DAT -> BIGFILE.DAT",
|
||||
),
|
||||
]
|
||||
|
||||
for search, replace, desc in patches:
|
||||
assert len(search) == len(replace), f"Length mismatch for {desc}: {len(search)} vs {len(replace)}"
|
||||
idx = bytes(exe).find(search)
|
||||
if idx == -1:
|
||||
print(f" NOT FOUND (check bytes): {desc}")
|
||||
# print surrounding bytes for debugging
|
||||
# try to find just the ascii part
|
||||
ascii_part = bytes(b for b in search if 0x20 <= b < 0x7f)
|
||||
idx2 = bytes(exe).find(ascii_part)
|
||||
if idx2 != -1:
|
||||
print(f" ASCII match at 0x{idx2:06x}: {bytes(exe[idx2-4:idx2+len(ascii_part)+4]).hex(' ')}")
|
||||
continue
|
||||
exe[idx:idx+len(replace)] = replace
|
||||
print(f" OK 0x{idx:06x}: {desc}")
|
||||
|
||||
open("HAVOC_NOCD.EXE", "wb").write(exe)
|
||||
print("\nDone. HAVOC_NOCD.EXE no longer needs C:\\ symlinks for file loading.")
|
||||
112
tools/patch_vtable.py
Normal file
112
tools/patch_vtable.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
"""
|
||||
Patch V: vtable-setter in init_archive (0x443010)
|
||||
|
||||
Root cause: init_archive explicitly clears vtable ([ESI+0]=0) and never sets it.
|
||||
load_archives later calls 0x443140 (cleanup) for each archive object; with vtable=0,
|
||||
0x443140 unmaps the file, closes handles, and zeroes field_0xC (directory ptr).
|
||||
open_archive_file checks field_0xC != 0, so it fails, and get_archive returns 0 =>
|
||||
"corrupt or unavailable game file" error fires.
|
||||
|
||||
Fix: insert MOV [ESI+0], 1 at the init_archive success return path. With vtable=1,
|
||||
0x443140's vtable-check (TEST EAX; JNZ) skips cleanup, field_0xC is preserved, and
|
||||
get_archive succeeds.
|
||||
|
||||
Mechanism:
|
||||
FO 0x42514 (VA 0x443114): replace 5-byte MOV EAX,1 with JMP to cave at FO 0x66FCE
|
||||
Cave (FO 0x66FCE, VA 0x467BCE):
|
||||
c7 46 00 01 00 00 00 MOV [ESI+0], 1 <- set vtable sentinel
|
||||
b8 01 00 00 00 MOV EAX, 1 <- return value = success
|
||||
5e 5b c2 08 00 POP ESI; POP EBX; RET 8
|
||||
"""
|
||||
import struct, sys
|
||||
|
||||
EXE = "HAVOC_NOCD.EXE"
|
||||
exe = bytearray(open(EXE, "rb").read())
|
||||
BASE = 0x400000
|
||||
FO2VA = lambda fo: fo + 0x400C00
|
||||
|
||||
# ---- locate .text section header ----
|
||||
e_lfanew = struct.unpack_from('<I', exe, 0x3c)[0]
|
||||
coff_off = e_lfanew + 4
|
||||
num_sects = struct.unpack_from('<H', exe, coff_off + 2)[0]
|
||||
opt_size = struct.unpack_from('<H', exe, coff_off + 16)[0]
|
||||
sects_off = coff_off + 20 + opt_size
|
||||
for i in range(num_sects):
|
||||
sh = sects_off + i * 40
|
||||
if exe[sh:sh+8].rstrip(b'\x00') == b'.text':
|
||||
text_sh = sh
|
||||
text_rawoff = struct.unpack_from('<I', exe, sh+20)[0]
|
||||
text_rawsiz = struct.unpack_from('<I', exe, sh+16)[0]
|
||||
text_vrva = struct.unpack_from('<I', exe, sh+12)[0]
|
||||
text_vsiz = struct.unpack_from('<I', exe, sh+8)[0]
|
||||
break
|
||||
print(".text: rawoff=0x%x rawsiz=0x%x vrva=0x%x vsiz=0x%x" %
|
||||
(text_rawoff, text_rawsiz, text_vrva, text_vsiz))
|
||||
|
||||
ORIG_TEXT_VSIZ = 0x66bbd
|
||||
cave_fo = text_rawoff + ORIG_TEXT_VSIZ # FO 0x66fbd (Patch R area)
|
||||
patch_v_cave_fo = cave_fo + 17 # FO 0x66fce (Patch V area)
|
||||
patch_v_cave_va = FO2VA(patch_v_cave_fo)
|
||||
|
||||
# ---- Verify cave is clean ----
|
||||
cave_bytes = bytes(exe[patch_v_cave_fo:patch_v_cave_fo+17])
|
||||
assert all(b in (0x00, 0xCC) for b in cave_bytes), \
|
||||
"Cave area not clean: %s" % cave_bytes.hex(' ')
|
||||
print("Cave at FO 0x%05x VA 0x%07x: clean" % (patch_v_cave_fo, patch_v_cave_va))
|
||||
|
||||
# ---- Verify patch site ----
|
||||
PATCH_FO = 0x42514
|
||||
expected_at_site = bytes.fromhex('b801000000') # MOV EAX, 1
|
||||
actual_at_site = bytes(exe[PATCH_FO:PATCH_FO+5])
|
||||
if actual_at_site == expected_at_site:
|
||||
print("Patch site FO 0x%05x: original (unpatched)" % PATCH_FO)
|
||||
elif actual_at_site[0] == 0xe9:
|
||||
print("Patch site FO 0x%05x: already patched (JMP present)" % PATCH_FO)
|
||||
# verify it points to our cave
|
||||
rel = struct.unpack_from('<i', actual_at_site, 1)[0]
|
||||
tgt = FO2VA(PATCH_FO + 5) + rel
|
||||
print(" Existing JMP -> 0x%07x (cave=0x%07x)" % (tgt, patch_v_cave_va))
|
||||
if tgt == patch_v_cave_va:
|
||||
print(" -> points to our cave; verifying cave code...")
|
||||
cave_expected = bytes.fromhex('c746000100000' + '0b801000000' + '5e5bc20800')
|
||||
cave_actual = bytes(exe[patch_v_cave_fo:patch_v_cave_fo+17])
|
||||
if cave_actual == cave_expected:
|
||||
print(" Cave code matches. Patch already applied.")
|
||||
sys.exit(0)
|
||||
print(" -> unexpected; re-applying")
|
||||
else:
|
||||
print("ERROR: unexpected bytes at patch site: %s" % actual_at_site.hex(' '))
|
||||
sys.exit(1)
|
||||
|
||||
# ---- Build cave code ----
|
||||
cave_code = bytearray()
|
||||
cave_code += b'\xc7\x46\x00\x01\x00\x00\x00' # MOV [ESI+0], 1 (7 bytes)
|
||||
cave_code += b'\xb8\x01\x00\x00\x00' # MOV EAX, 1 (5 bytes)
|
||||
cave_code += b'\x5e\x5b\xc2\x08\x00' # POP ESI; POP EBX; RET 8 (5 bytes)
|
||||
assert len(cave_code) == 17, "cave size %d != 17" % len(cave_code)
|
||||
print("Cave code (%d bytes): %s" % (len(cave_code), cave_code.hex(' ')))
|
||||
|
||||
# ---- Build JMP at patch site ----
|
||||
next_instr_va = FO2VA(PATCH_FO + 5)
|
||||
rel32 = (patch_v_cave_va - next_instr_va) & 0xFFFFFFFF
|
||||
jmp_bytes = b'\xe9' + struct.pack('<I', rel32)
|
||||
print("JMP bytes: %s (rel=0x%08x -> cave 0x%07x)" % (
|
||||
jmp_bytes.hex(' '), rel32, patch_v_cave_va))
|
||||
# Verify jump lands on cave
|
||||
assert (next_instr_va + struct.unpack_from('<i', jmp_bytes, 1)[0]) & 0xFFFFFFFF == patch_v_cave_va, \
|
||||
"JMP target mismatch"
|
||||
print("JMP target assertion: OK")
|
||||
|
||||
# ---- Extend VirtualSize to RawSize (makes caves executable) ----
|
||||
new_vsiz = text_rawsiz
|
||||
print(".text VirtualSize: 0x%x -> 0x%x" % (text_vsiz, new_vsiz))
|
||||
|
||||
# ---- Apply ----
|
||||
exe[patch_v_cave_fo:patch_v_cave_fo+17] = cave_code
|
||||
exe[PATCH_FO:PATCH_FO+5] = jmp_bytes
|
||||
struct.pack_into('<I', exe, text_sh + 8, new_vsiz)
|
||||
|
||||
open(EXE, "wb").write(exe)
|
||||
print("Patch V applied successfully.")
|
||||
print("init_archive now sets vtable=1 at success return.")
|
||||
print("0x443140 will skip cleanup for initialized archives, preserving field_0xC.")
|
||||
52
tools/path_audit.py
Normal file
52
tools/path_audit.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import re, struct
|
||||
|
||||
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
||||
rdata = exe[0x067000:]
|
||||
base = 0x067000
|
||||
|
||||
print("=== ALL filename/path strings in rdata ===")
|
||||
for m in re.finditer(rb'[A-Za-z0-9][A-Za-z0-9_.\\: ]{3,39}(?=\x00)', rdata):
|
||||
s = m.group().decode('latin1')
|
||||
fo = base + m.start()
|
||||
if '.' in s or chr(0x5c) in s or ':' in s:
|
||||
print(f" 0x{fo:06x}: {s!r}")
|
||||
|
||||
# Find ALL MessageBoxA / error-display calls in .text
|
||||
print("\n=== MessageBoxA calls in .text ===")
|
||||
# Find MessageBoxA IAT entry
|
||||
msgbox_name = b'MessageBoxA\x00'
|
||||
idx = exe.find(msgbox_name)
|
||||
print(f" MessageBoxA name at: 0x{idx:06x}")
|
||||
|
||||
# Find all CALL DWORD PTR [mem] (ff 15 xx xx xx xx) in .text 0x000400-0x067000
|
||||
print("\n=== All CALL [mem] in .text (indirect calls, includes IAT) ===")
|
||||
call_targets = {}
|
||||
for m in re.finditer(b'\xff\x15', exe[0x400:0x067000]):
|
||||
off = 0x400 + m.start()
|
||||
va = struct.unpack_from('<I', exe, off+2)[0]
|
||||
fo_target = va - 0x400000 - 0x001000 + 0x000400
|
||||
if 0x067000 <= fo_target < 0x080000:
|
||||
# read the import name near there
|
||||
name_off = fo_target + 2 # skip hint
|
||||
name = exe[name_off: exe.index(b'\x00', name_off)].decode('latin1', errors='replace')
|
||||
call_targets[name] = call_targets.get(name, 0) + 1
|
||||
|
||||
for name, count in sorted(call_targets.items()):
|
||||
print(f" {name}: {count} call(s)")
|
||||
|
||||
# Now find all calls to MessageBoxA specifically
|
||||
print("\n=== MessageBoxA call sites ===")
|
||||
for fo_iat in range(0x067000, 0x080000, 4):
|
||||
hint_fo = struct.unpack_from('<I', exe, fo_iat)[0] - 0x400000 - 0x001000 + 0x000400
|
||||
if 0 < hint_fo < len(exe):
|
||||
try:
|
||||
name = exe[hint_fo+2 : exe.index(b'\x00', hint_fo+2)].decode('latin1', errors='replace')
|
||||
except:
|
||||
continue
|
||||
if 'MessageBox' in name:
|
||||
iat_va = 0x400000 + 0x001000 + (fo_iat - 0x000400)
|
||||
needle = struct.pack('<I', iat_va)
|
||||
for m in re.finditer(b'\xff\x15' + re.escape(needle), exe[0x400:0x067000]):
|
||||
call_fo = 0x400 + m.start()
|
||||
ctx = exe[max(0x400, call_fo-16):call_fo+20]
|
||||
print(f" CALL MessageBox @ 0x{call_fo:06x}: {ctx.hex(' ')}")
|
||||
67
tools/pe_sections.py
Normal file
67
tools/pe_sections.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import struct
|
||||
|
||||
exe = open("HAVOC_NOCD.EXE", "rb").read()
|
||||
|
||||
# Read PE header
|
||||
pe_off = struct.unpack_from('<I', exe, 0x3c)[0]
|
||||
print(f"PE offset: 0x{pe_off:04x}")
|
||||
print(f"Machine: 0x{struct.unpack_from('<H', exe, pe_off+4)[0]:04x}")
|
||||
|
||||
# Optional header
|
||||
opt_off = pe_off + 24
|
||||
image_base = struct.unpack_from('<I', exe, opt_off + 28)[0]
|
||||
print(f"ImageBase: 0x{image_base:08x}")
|
||||
|
||||
# Number of sections
|
||||
num_sections = struct.unpack_from('<H', exe, pe_off + 6)[0]
|
||||
size_of_opt = struct.unpack_from('<H', exe, pe_off + 20)[0]
|
||||
sections_off = pe_off + 24 + size_of_opt
|
||||
|
||||
print(f"\n{'Section':<12} {'VirtAddr':>10} {'VirtSize':>10} {'RawOff':>10} {'RawSize':>10}")
|
||||
sections = []
|
||||
for i in range(num_sections):
|
||||
s = sections_off + i * 40
|
||||
name = exe[s:s+8].rstrip(b'\x00').decode('latin1', errors='replace')
|
||||
virt_rva = struct.unpack_from('<I', exe, s+12)[0]
|
||||
virt_size = struct.unpack_from('<I', exe, s+8)[0]
|
||||
raw_off = struct.unpack_from('<I', exe, s+20)[0]
|
||||
raw_size = struct.unpack_from('<I', exe, s+16)[0]
|
||||
sections.append((name, virt_rva, virt_size, raw_off, raw_size))
|
||||
print(f" {name:<10} 0x{virt_rva:08x} 0x{virt_size:08x} 0x{raw_off:08x} 0x{raw_size:08x}")
|
||||
|
||||
# Find which section GetPrivateProfileStringA name (at 0x06f1c8) falls in
|
||||
name_fo = 0x06f1c8
|
||||
print(f"\nGetPrivateProfileStringA name at file offset 0x{name_fo:06x}")
|
||||
for (sname, vrva, vsz, raw, rsz) in sections:
|
||||
if raw <= name_fo < raw + rsz:
|
||||
rva = vrva + (name_fo - raw)
|
||||
va = image_base + rva
|
||||
print(f" In section [{sname}]: RVA=0x{rva:08x} VA=0x{va:08x}")
|
||||
hint_fo = name_fo - 2
|
||||
hint_rva = vrva + (hint_fo - raw)
|
||||
hint_va = image_base + hint_rva
|
||||
print(f" Hint+name VA: 0x{hint_va:08x}")
|
||||
|
||||
# Search for this VA in the IAT/INT (which lives in idata too)
|
||||
import re
|
||||
needle = struct.pack('<I', hint_va)
|
||||
print(f"\n Searching for INT entry 0x{hint_va:08x}...")
|
||||
for m in re.finditer(re.escape(needle), exe):
|
||||
ref_fo = m.start()
|
||||
ref_in_sec = [(sn, vr, vs, ro, rs) for (sn, vr, vs, ro, rs) in sections if ro <= ref_fo < ro + rs]
|
||||
sec_label = ref_in_sec[0][0] if ref_in_sec else "?"
|
||||
print(f" Found at file offset 0x{ref_fo:06x} in [{sec_label}]")
|
||||
# This is the INT entry. The IAT entry is at the same index in the IAT array.
|
||||
iat_fo = ref_fo
|
||||
iat_rva = None
|
||||
for (sn, vr, vs, ro, rs) in sections:
|
||||
if ro <= iat_fo < ro + rs:
|
||||
iat_rva = vr + (iat_fo - ro)
|
||||
if iat_rva:
|
||||
iat_va = image_base + iat_rva
|
||||
print(f" IAT entry VA (pre-load): 0x{iat_va:08x}")
|
||||
# Find CALL [iat_va] in .text
|
||||
call_needle = b'\xff\x15' + struct.pack('<I', iat_va)
|
||||
for cm in re.finditer(re.escape(call_needle), exe[0x400:0x067000]):
|
||||
cfo = 0x400 + cm.start()
|
||||
print(f" CALL @ 0x{cfo:06x}")
|
||||
22
tools/r2ac00.py
Normal file
22
tools/r2ac00.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import r2pipe, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
r2 = r2pipe.open("Z:/Development/devl/Havoc/HAVOC_NOCD.EXE",
|
||||
["-e", "bin.relocs.apply=true"])
|
||||
|
||||
print("=== 0x40AC00 (object creator - does it short-circuit?) ===")
|
||||
print(r2.cmd("pd 30 @ 0x40ac00"))
|
||||
|
||||
print("\n=== 0x40ADF0 (second pool alloc, called after 0x429CA0) ===")
|
||||
print(r2.cmd("pd 40 @ 0x40adf0"))
|
||||
|
||||
# Also: read the full 0x429CA0 past the loop to see if there's another crash_stub call
|
||||
print("\n=== 0x429CA0 continued (after offset 0x429d57) ===")
|
||||
print(r2.cmd("pd 40 @ 0x429d57"))
|
||||
|
||||
# Run the game and capture the new crash address
|
||||
print("\n=== current patch state at crash site FO 0x290A0 area ===")
|
||||
# Check if there's anything patched around FO 0x290A0 = VA 0x429CA0
|
||||
print(r2.cmd("pd 5 @ 0x429ca0"))
|
||||
|
||||
r2.quit()
|
||||
26
tools/r2alloc.py
Normal file
26
tools/r2alloc.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import r2pipe, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
r2 = r2pipe.open("Z:/Development/devl/Havoc/HAVOC_NOCD.EXE",
|
||||
["-e", "bin.relocs.apply=true"])
|
||||
|
||||
print("=== 0x464DF1 (heap init / VirtualAlloc path) - full function ===")
|
||||
print(r2.cmd("pdr @ 0x464df1"))
|
||||
|
||||
print("\n=== 0x464E77 (alloc helper - called when free list empty) ===")
|
||||
print(r2.cmd("pdr @ 0x464e77"))
|
||||
|
||||
print("\n=== 0x4633D1 (real allocator impl) ===")
|
||||
print(r2.cmd("pdr @ 0x4633d1"))
|
||||
|
||||
print("\n=== What calls 0x464DF1? ===")
|
||||
print(r2.cmd("axt @ 0x464df1"))
|
||||
|
||||
print("\n=== What calls 0x464E77? ===")
|
||||
print(r2.cmd("axt @ 0x464e77"))
|
||||
|
||||
# Also: data layout around 0x480960
|
||||
print("\n=== Data at 0x480960 (heap struct) ===")
|
||||
print(r2.cmd("pxw 32 @ 0x480960"))
|
||||
|
||||
r2.quit()
|
||||
29
tools/r2alloc2.py
Normal file
29
tools/r2alloc2.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import r2pipe, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
r2 = r2pipe.open("Z:/Development/devl/Havoc/HAVOC_NOCD.EXE",
|
||||
["-e", "bin.relocs.apply=true"])
|
||||
|
||||
print("=== 0x464DF1 (heap init) ===")
|
||||
print(r2.cmd("pd 60 @ 0x464df1"))
|
||||
|
||||
print("\n=== 0x464E77 (alloc helper) ===")
|
||||
print(r2.cmd("pd 60 @ 0x464e77"))
|
||||
|
||||
print("\n=== callers of 0x464DF1 (search all CALL rel32) ===")
|
||||
# search for e8 bytes that target 0x464df1
|
||||
hits = r2.cmd("/x e8")
|
||||
# Use r2 search for the exact call bytes
|
||||
rel = (0x464df1 - 5) & 0xFFFFFFFF # call displacement = target - (callsite+5)
|
||||
# search for call targeting 0x464df1: e8 XX XX XX XX where XX = rel32
|
||||
print(r2.cmd("/ \\xe8\\xf1")) # rough search prefix
|
||||
# Better: use radare2's cross-reference after deeper analysis
|
||||
r2.cmd("aaaa") # deeper analysis
|
||||
print("\nxrefs to 0x464df1 (after deeper analysis):")
|
||||
print(r2.cmd("axt @ 0x464df1"))
|
||||
print("xrefs to 0x464e77:")
|
||||
print(r2.cmd("axt @ 0x464e77"))
|
||||
print("xrefs to 0x4633d1:")
|
||||
print(r2.cmd("axt @ 0x4633d1"))
|
||||
|
||||
r2.quit()
|
||||
19
tools/r2alloc3.py
Normal file
19
tools/r2alloc3.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import r2pipe, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
r2 = r2pipe.open("Z:/Development/devl/Havoc/HAVOC_NOCD.EXE",
|
||||
["-e", "bin.relocs.apply=true"])
|
||||
|
||||
print("=== 0x464F52 (empty free list path in 0x464E77) ===")
|
||||
print(r2.cmd("pd 40 @ 0x464f52"))
|
||||
|
||||
print("\n=== 0x464F67 (second alloc helper called from 0x4633D1) ===")
|
||||
print(r2.cmd("pd 60 @ 0x464f67"))
|
||||
|
||||
print("\n=== 0x4633D1 full: from where 0x464E77 returns NULL ===")
|
||||
print(r2.cmd("pd 80 @ 0x4633d1"))
|
||||
|
||||
print("\n=== 0x4647BD (called from 0x464E6C) ===")
|
||||
print(r2.cmd("pd 20 @ 0x4647bd"))
|
||||
|
||||
r2.quit()
|
||||
36
tools/r2analyze.py
Normal file
36
tools/r2analyze.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import r2pipe, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
r2 = r2pipe.open("HAVOC_NOCD.EXE", ["-e", "anal.strings=false"])
|
||||
r2.cmd("aaa") # analyze all
|
||||
|
||||
def disasm(addr, n=20):
|
||||
return r2.cmd("pd %d @ 0x%x" % (n, addr))
|
||||
|
||||
def xrefs_to(addr):
|
||||
return r2.cmd("axt @ 0x%x" % addr)
|
||||
|
||||
def xrefs_from(addr):
|
||||
return r2.cmd("axf @ 0x%x" % addr)
|
||||
|
||||
print("=== Entry point function ===")
|
||||
ep_info = r2.cmd("ie")
|
||||
print(ep_info)
|
||||
|
||||
print("=== 0x464DCA (first call from entry, CRT init?) ===")
|
||||
print(disasm(0x464DCA, 40))
|
||||
|
||||
print("=== Search for writes to [0x480964] (heap handle init) ===")
|
||||
# Search for MOV [0x480964], something
|
||||
hits = r2.cmd("/ \\xa3\\x64\\x09\\x48\\x00") # MOV [0x480964],EAX
|
||||
print("MOV [0x480964],EAX hits:", hits)
|
||||
hits2 = r2.cmd("/ \\x89\\x05\\x64\\x09\\x48\\x00") # MOV [0x480964],r32
|
||||
print("MOV [0x480964],r32 hits:", hits2)
|
||||
|
||||
print("=== 0x465BD3 (global ctor runner?) ===")
|
||||
print(disasm(0x465BD3, 40))
|
||||
|
||||
print("=== 0x465BC8 ===")
|
||||
print(disasm(0x465BC8, 20))
|
||||
|
||||
r2.quit()
|
||||
18
tools/r2cdcheck.py
Normal file
18
tools/r2cdcheck.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import r2pipe, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
r2 = r2pipe.open("Z:/Development/devl/Havoc/HAVOC_NOCD.EXE",
|
||||
["-e", "bin.relocs.apply=true"])
|
||||
|
||||
# CD check function around 0x42A6BE
|
||||
print("=== CD check function area 0x42A680 - 0x42A800 ===")
|
||||
print(r2.cmd("pd 100 @ 0x42a680"))
|
||||
|
||||
print("\n=== NOCD stub at 0x42A7B0 (existing patch) ===")
|
||||
print(r2.cmd("pd 8 @ 0x42a7b0"))
|
||||
|
||||
# Also check the caller of this function
|
||||
print("\n=== Looking backwards: what calls 0x42A6BE area? ===")
|
||||
print(r2.cmd("pd 5 @ 0x42a67e")) # entry point of the function
|
||||
|
||||
r2.quit()
|
||||
23
tools/r2crash.py
Normal file
23
tools/r2crash.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import r2pipe, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
r2 = r2pipe.open("Z:/Development/devl/Havoc/HAVOC_NOCD.EXE",
|
||||
["-e", "bin.relocs.apply=true"])
|
||||
# No aaa - just raw disassembly
|
||||
|
||||
print("=== crash_stub at 0x40AB90 (crash site) ===")
|
||||
print(r2.cmd("pd 4 @ 0x40ab90"))
|
||||
|
||||
print("\n=== pool allocator 0x40AAF0 (what calls crash_stub?) ===")
|
||||
print(r2.cmd("pd 50 @ 0x40aaf0"))
|
||||
|
||||
print("\n=== function 0x429CA0 = caller FO 0x290A0 ===")
|
||||
print(r2.cmd("pd 60 @ 0x429ca0"))
|
||||
|
||||
print("\n=== 0x40ABD0: zeroed alloc (does it check for NULL?) ===")
|
||||
print(r2.cmd("pd 20 @ 0x40abd0"))
|
||||
|
||||
print("\n=== 0x465050: VirtualAlloc path ===")
|
||||
print(r2.cmd("pd 30 @ 0x465050"))
|
||||
|
||||
r2.quit()
|
||||
47
tools/r2heap.py
Normal file
47
tools/r2heap.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
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)
|
||||
22
tools/r2init.py
Normal file
22
tools/r2init.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import r2pipe, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
r2 = r2pipe.open("Z:/Development/devl/Havoc/HAVOC_NOCD.EXE",
|
||||
["-e", "bin.relocs.apply=true"])
|
||||
|
||||
# 0x41868A is the only caller of 0x429CA0
|
||||
# Let's see the context there and 0x418600 (init function)
|
||||
print("=== 0x418600 context (init function, caller of pool init) ===")
|
||||
print(r2.cmd("pd 80 @ 0x418600"))
|
||||
|
||||
print("\n=== 0x43A960 (gating function: arg2==0 -> run pool init) ===")
|
||||
print(r2.cmd("pd 30 @ 0x43a960"))
|
||||
|
||||
# Also check what's at VA 0x429EC1 and VA 0x429F6C (other crash_stub callers in 0x429CA0 area)
|
||||
print("\n=== 0x429EB3 (pool_alloc call near 0x429EC1) ===")
|
||||
print(r2.cmd("pd 25 @ 0x429eb3"))
|
||||
|
||||
print("\n=== 0x429F5E (pool_alloc call near 0x429F6C) ===")
|
||||
print(r2.cmd("pd 25 @ 0x429f5e"))
|
||||
|
||||
r2.quit()
|
||||
95
tools/read_bin_iso.py
Normal file
95
tools/read_bin_iso.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
"""
|
||||
Parse ISO 9660 from a MODE1/2352 raw BIN file.
|
||||
Each sector = 2352 bytes; user data starts at byte 16 (after sync+header).
|
||||
"""
|
||||
import struct, sys, os
|
||||
|
||||
BIN = sys.argv[1] if len(sys.argv) > 1 else r"Z:\Development\devl\Havoc\tmp_cd\Havoc (USA) (Game Disk).bin"
|
||||
SECTOR = 2352
|
||||
DATA_OFF = 16 # within each sector: sync(12) + header(4)
|
||||
DATA_LEN = 2048
|
||||
|
||||
def read_sector(f, lba):
|
||||
f.seek(lba * SECTOR + DATA_OFF)
|
||||
return f.read(DATA_LEN)
|
||||
|
||||
def read_u16_le(b, off): return struct.unpack_from('<H', b, off)[0]
|
||||
def read_u32_le(b, off): return struct.unpack_from('<I', b, off)[0]
|
||||
def read_u32_both(b, off): return struct.unpack_from('<I', b, off)[0] # LE half
|
||||
|
||||
def parse_dir(f, lba, size, indent=0, entries=None):
|
||||
if entries is None:
|
||||
entries = []
|
||||
data = b''
|
||||
remaining = size
|
||||
cur_lba = lba
|
||||
while remaining > 0:
|
||||
chunk = read_sector(f, cur_lba)
|
||||
data += chunk[:min(DATA_LEN, remaining)]
|
||||
remaining -= DATA_LEN
|
||||
cur_lba += 1
|
||||
|
||||
off = 0
|
||||
while off < len(data):
|
||||
reclen = data[off]
|
||||
if reclen == 0:
|
||||
off = (off + 1 + 1) & ~1 # skip padding; advance
|
||||
if off >= len(data): break
|
||||
continue
|
||||
flags = data[off + 25]
|
||||
name_len = data[off + 32]
|
||||
name_raw = data[off+33:off+33+name_len]
|
||||
# Strip version suffix ;1
|
||||
name = name_raw.decode('ascii', errors='replace').split(';')[0].rstrip('\x00')
|
||||
file_lba = read_u32_both(data, off + 2)
|
||||
file_size = read_u32_both(data, off + 10)
|
||||
is_dir = bool(flags & 0x02)
|
||||
if name not in ('', '\x01'):
|
||||
entries.append({'name': name, 'lba': file_lba, 'size': file_size,
|
||||
'is_dir': is_dir, 'indent': indent})
|
||||
if is_dir:
|
||||
parse_dir(f, file_lba, file_size, indent+1, entries)
|
||||
off += reclen
|
||||
return entries
|
||||
|
||||
with open(BIN, 'rb') as f:
|
||||
# Check for ISO 9660 PVD at sector 16
|
||||
pvd = read_sector(f, 16)
|
||||
if pvd[1:6] != b'CD001':
|
||||
print(f"No ISO 9660 PVD at sector 16. Got: {pvd[0:8].hex(' ')}")
|
||||
# Try sector 16 with different offset (some TOAST BINs have ISO 9660 elsewhere)
|
||||
# Check volume descriptor sequence starting at 16
|
||||
for lba in [16, 17, 18, 32, 64]:
|
||||
s = read_sector(f, lba)
|
||||
print(f" Sector {lba}: type={s[0]} sig={s[1:6]} system_id={s[8:40].rstrip()}")
|
||||
sys.exit(1)
|
||||
|
||||
vol_type = pvd[0] # 1 = PVD
|
||||
system_id = pvd[8:40].decode('ascii', errors='replace').strip()
|
||||
vol_id = pvd[40:72].decode('ascii', errors='replace').strip()
|
||||
vol_space = read_u32_both(pvd, 80)
|
||||
print(f"ISO 9660 PVD found:")
|
||||
print(f" System ID: {system_id!r}")
|
||||
print(f" Volume ID: {vol_id!r}")
|
||||
print(f" Vol sectors:{vol_space}")
|
||||
|
||||
root_dir_lba = read_u32_both(pvd, 156 + 2)
|
||||
root_dir_size = read_u32_both(pvd, 156 + 10)
|
||||
print(f" Root dir LBA={root_dir_lba} size={root_dir_size}")
|
||||
print()
|
||||
|
||||
entries = parse_dir(f, root_dir_lba, root_dir_size)
|
||||
|
||||
print(f"Files ({len(entries)} entries):")
|
||||
for e in entries:
|
||||
prefix = ' ' * e['indent']
|
||||
tag = '[DIR]' if e['is_dir'] else f"{e['size']:>10}"
|
||||
print(f" {prefix}{e['name']:<30} {tag}")
|
||||
|
||||
print()
|
||||
# Look for specific files we need
|
||||
targets = ['HAVOCDAT.DAT', 'KEYSET.DAT', 'BIGFILE.DAT', 'GAMIMAGE.DAT']
|
||||
print("Target file search:")
|
||||
for e in entries:
|
||||
if e['name'].upper() in targets:
|
||||
print(f" FOUND: {e['name']} at LBA {e['lba']} size {e['size']}")
|
||||
48
tools/trace_init.py
Normal file
48
tools/trace_init.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
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]
|
||||
print(f"ImageBase: 0x{img_base:08x}")
|
||||
|
||||
# The global byte tested before 'PUSH 6 -> corrupt error'
|
||||
# Instruction at 0x020270: a0 25 54 47 00 = MOV AL, [0x00475425]
|
||||
flag_va = 0x00475425
|
||||
flag_needle = struct.pack('<I', flag_va)
|
||||
|
||||
print(f"\n=== All accesses to flag byte at VA=0x{flag_va:08x} ===")
|
||||
for m in re.finditer(re.escape(flag_needle), exe[0x400:0x067000]):
|
||||
off = 0x400 + m.start()
|
||||
ctx = exe[max(0x400, off-4):off+6]
|
||||
print(f" 0x{off-4:06x}: {ctx.hex(' ')}")
|
||||
|
||||
# String VAs with correct ImageBase
|
||||
rdata_vRVA = 0x079000
|
||||
rdata_raw = 0x067000
|
||||
|
||||
def rdata_va(fo):
|
||||
return img_base + rdata_vRVA + (fo - rdata_raw)
|
||||
|
||||
string_fos = {
|
||||
'MUSIC.FF': 0x06b27c,
|
||||
'INTRFACE.FF': 0x06b288,
|
||||
'OBJECTS.FF': 0x06b298,
|
||||
'SOUND.FF': 0x06b2a4,
|
||||
'BIGFILE.DAT': 0x06b3b8,
|
||||
'STRDATA.DAT': 0x06bee8,
|
||||
'WORLDS_rel': 0x06b2b0,
|
||||
'WORLDS_abs': 0x06b2b8,
|
||||
'HAVOCDAT': 0x06b3a8,
|
||||
'GAMIMAGE': 0x06b980,
|
||||
}
|
||||
|
||||
print(f"\n=== String VAs and code references ===")
|
||||
for label, fo in string_fos.items():
|
||||
va = rdata_va(fo)
|
||||
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[:3]:
|
||||
ctx = exe[max(0x400,r-6):r+8]
|
||||
print(f" 0x{r:06x}: {ctx.hex(' ')}")
|
||||
Loading…
Reference in a new issue