Add detailed log for retail debugger patch attempts in CRUSADER.EXE
This commit introduces a comprehensive document outlining the various executable-patching attempts aimed at revealing the hidden retail usecode debugger within the CRUSADER.EXE file. The document serves multiple purposes, including preserving negative evidence, recording patch shapes and their rationales, and ensuring that runtime outcomes are linked to specific patch generations. Key sections include: - Ground rules for patching and validation processes. - A table of stable facts regarding the debugger's structure and behavior. - A detailed attempt log documenting each patch's shape, mechanical and runtime results, and verdicts. - Root-cause findings from failed paths, providing insights into the challenges faced during the patching process. - Current live candidates for further testing and exploration. This documentation is intended to streamline future patching efforts and improve the understanding of the underlying mechanics of the debugger.
This commit is contained in:
parent
ded6db3adc
commit
7310c4fe96
13 changed files with 1008 additions and 1959 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -40,3 +40,4 @@ dist/
|
|||
.tmp_*.py
|
||||
USECODE/EUSECODE_extracted/chunks/**
|
||||
tools/pyghidra_crusader/__pycache__/**
|
||||
bin/**
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
Binary file not shown.
|
|
@ -21,6 +21,7 @@ Recent verified batch: [docs/ne-segment1.md](docs/ne-segment1.md) now carries th
|
|||
| [docs/far-call-targets.md](docs/far-call-targets.md) | Top-104 most-called far-call targets (Tiers 1-5, ranks 1-104), supporting functions discovered, analysis gaps and seg043 reconciliation |
|
||||
| [docs/crusader-disasm-reference.md](docs/crusader-disasm-reference.md) | Local auxiliary disassembly corpus at `K:/ghidra/crusader-disasm`: handwritten notes, shape tables, map dumps, opcode lists, intrinsic/function dumps, and the safe reuse rules for porting into `CRUSADER.EXE` |
|
||||
| [docs/ne-hole-filling-priorities.md](docs/ne-hole-filling-priorities.md) | Ranked `CRUSADER.EXE` hole-filling tracker: NE-side unclear lanes, the verified raw-side knowledge that can close them, and the recommended order for old-to-new porting passes |
|
||||
| [docs/retail-debugger-patch-attempts.md](docs/retail-debugger-patch-attempts.md) | Chronological log of retail `CRUSADER.EXE` debugger-unlock patch attempts, byte-level designs, runtime failures, root-cause findings, and the current live candidate |
|
||||
| [docs/scummvm-crusader-reference.md](docs/scummvm-crusader-reference.md) | ScummVM Ultima8/Pentagram Crusader integration survey: USECODE/event tables, FLEX/resource formats, world/map loaders, HUD/media, and RE follow-up priorities |
|
||||
| [docs/pentagram-crusader-reference.md](docs/pentagram-crusader-reference.md) | Pentagram-source Crusader/U8 reference: direct Crusader USECODE parser and VM evidence, U8 usecode docs, runtime-confidence limits, and cross-checks against the ScummVM note |
|
||||
| [docs/usecode-roundtrip-ir.md](docs/usecode-roundtrip-ir.md) | ScummVM-to-binary USECODE cross-walk, owner-loaded class-layout and header/event-count reconciliation, conservative IR v0 plan, and the generated class-event/body-window outputs that now ground reversible `_BOOT`, `SURCAM*`, and environmental family decompile artifacts plus repeated-family regression checks |
|
||||
|
|
|
|||
353
docs/retail-debugger-patch-attempts.md
Normal file
353
docs/retail-debugger-patch-attempts.md
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
# Retail Debugger Patch Attempts
|
||||
|
||||
This document is the running log for all executable-patching attempts to surface the hidden retail usecode debugger in `CRUSADER.EXE`.
|
||||
|
||||
Purpose:
|
||||
- preserve negative evidence
|
||||
- record byte-level patch shapes and the reasoning behind them
|
||||
- avoid re-testing structurally broken ideas
|
||||
- keep runtime outcomes tied to the exact patch generation that produced them
|
||||
|
||||
## Ground Rules
|
||||
|
||||
- Active live target is retail `CRUSADER.EXE`.
|
||||
- Mechanical validation means the PowerShell patcher can `apply -> verify patched -> restore -> verify original` on a fresh retail copy.
|
||||
- Runtime validation means the user tested the patch in DOSBox against the real game.
|
||||
- A mechanically clean patch is not considered viable until runtime behavior is also safe.
|
||||
|
||||
## Stable Facts
|
||||
|
||||
| Fact | Evidence |
|
||||
|------|----------|
|
||||
| `13a0:0086` is `usecode_debugger_open_for_current_unit` | Live Ghidra analysis and surrounding UI/control-flow evidence |
|
||||
| `1408:0000` constructs the seg1408 debugger break-state object | Constructor writes vtable at object `+0`, clears fields, returns far pointer |
|
||||
| Global debugger-state pointer lives at `1478:659c/659e` | Interpreter and seg13a0 UI both read it directly |
|
||||
| Object flags `+0x74` and `+0x75` are break-next and single-step controls | `13a0:1e5d` and `13a0:1e37` |
|
||||
| Constructor writes `0x65ab` to object `+0` | `1408:0024..0028` |
|
||||
| `1478:65ab` is method 0 and `1478:65af` is method 1 of the same vtable | `CALLF [BX]` and `CALLF [BX+4]` dispatch paths |
|
||||
|
||||
## Attempt Log
|
||||
|
||||
| ID | Patch shape | Mechanical result | Runtime result | Verdict |
|
||||
|----|-------------|------------------|----------------|---------|
|
||||
| A1 | Global callback-table rewrite: retarget shared `1478:65ab` to `13a0:0086` after hotkey-created object state | Initially broken, later fixed mechanically after correcting NE target segment and byte-array length | Startup crash | Retired. Shared callback slot is globally visible at boot. |
|
||||
| A2 | Direct local call from `13e8:230d` into `13a0:0086` after object creation | Clean round trip after fixing on-disk `FF FF 00 00` placeholder assumptions | Game booted, `Ctrl+Q` immediately quit with `No pity. No mercy. No remorse.` | Retired. Direct wrapper call on live keypress is unsafe. |
|
||||
| A3 | Per-object deferred callback: rewrite object `+0` from `0x65ab` to `0x65af`, retarget `1478:65af` to `13a0:0086`, enable single-step through `1408:0419` | Clean round trip | Game booted, `Ctrl+Q` produced no visible effect and suppressed the original CD-transfer toast | Retired. Single-step path was too gated and the table rewrite assumption was later proven wrong. |
|
||||
| A4 | Per-object break-next: keep `1478:65af -> 13a0:0086`, write `+0x75 = 0`, `+0x74 = 1` directly | Clean round trip | Startup crash | Retired. `65af` patch remained globally unsafe. |
|
||||
| A5 | Guarded trampoline at `1408:0474`, indirect through spare relocated dword `1478:6597`, still rewriting object `+0` to `0x65af` | Clean round trip | Startup crash | Retired. Guarded jump did not fix the deeper vtable-structure mistake. |
|
||||
| A6 | Shared method-0 callback patch: preserve `65ab`, restore `0474`, patch `1408:046f` to indirect through `1478:6597 -> 13a0:0086` | Clean round trip | Startup crash | Retired. Patching shared method code is still globally visible. |
|
||||
| A7 | Private two-entry vtable: rewrite object `+0` to unused `1478:658f`, set private method 0 `1478:658f -> 13a0:0086`, set private method 1 `1478:6593 -> 1408:0474`, arm break-next in object | Clean round trip | Startup crash | Retired. Those dwords are consumed during startup despite no current data-use hits. |
|
||||
| A8 | Private two-entry vtable moved farther out: rewrite object `+0` to unused `1478:6728`, set private method 0 `1478:6728 -> 13a0:0086`, set private method 1 `1478:672c -> 1408:0474`, arm break-next in object | Clean round trip | Startup crash | Retired. The `6728/672c` pair is also startup-visible. |
|
||||
| A9 | Multi-candidate harness, Candidate A: rewrite object `+0` to `1478:6724`, set private method 0 `1478:6724 -> 13a0:0086`, set private method 1 `1478:6728 -> 1408:0474`, arm break-next in object | Clean round trip | DOSBox closes on start | Retired. Startup-visible. |
|
||||
| A10 | Multi-candidate harness, Candidate B: rewrite object `+0` to `1478:672c`, set private method 0 `1478:672c -> 13a0:0086`, set private method 1 `1478:6730 -> 1408:0474`, arm break-next in object | Clean round trip | Fatal error `286.2180: Load program failed -- error code 201 -- C:\CRUSADER.EXE` | Retired. This pair appears to trip an earlier loader/launcher path than the plain startup-crash cases. |
|
||||
| A11 | Multi-candidate harness, Candidate C: rewrite object `+0` to `1478:6734`, set private method 0 `1478:6734 -> 13a0:0086`, set private method 1 `1478:6738 -> 1408:0474`, arm break-next in object | Clean round trip | DOSBox closes on start | Retired. Startup-visible. |
|
||||
| A12 | Multi-candidate harness, Candidate D: rewrite object `+0` to `1478:6718`, set private method 0 `1478:6718 -> 13a0:0086`, set private method 1 `1478:671c -> 1408:0474`, arm break-next in object | Clean round trip | Startup crash | Retired. Startup-visible. |
|
||||
| A13 | Multi-candidate harness, Candidate E: rewrite object `+0` to `1478:6720`, set private method 0 `1478:6720 -> 13a0:0086`, set private method 1 `1478:6724 -> 1408:0474`, arm break-next in object | Clean round trip | Startup crash | Retired. Startup-visible. |
|
||||
| A14 | Multi-candidate harness, Candidate F: rewrite object `+0` to `1478:6738`, set private method 0 `1478:6738 -> 13a0:0086`, set private method 1 `1478:673c -> 1408:0474`, arm break-next in object | Clean round trip | Startup crash | Retired. Startup-visible. |
|
||||
| A15 | Guarded callback shim, Candidate G: keep retail vtable base `1478:65ab`, patch `1408:046f/0474` into a `0x659c/0x659e` null-guarded trampoline, route deferred target slot `1478:6597 -> 13a0:020d`, and zero inherited caller-word pushes in `13a0:024a` | Clean round trip | Startup crash | Retired. Overwriting `1408:0474` also changed the shared zero-return helper semantics. |
|
||||
| A16 | Guarded callback shim, Candidate H: keep retail vtable base `1478:65ab`, patch `1408:046f/0474` into a `0x659c/0x659e` null-guarded trampoline, route deferred target slot `1478:6597 -> 13a0:0086`, and zero inherited caller-word pushes in `13a0:008f` | Clean round trip | Startup crash | Retired. Same `1408:0474` helper corruption risk as Candidate G. |
|
||||
| A17 | Method-0-only callback patch, Candidate I: keep retail vtable base `1478:65ab`, patch only `1408:046f -> CALL FAR [1478:6597]; RETF`, preserve `1408:0474` as-is, route deferred target slot `1478:6597 -> 13a0:020d`, and zero inherited caller-word pushes in `13a0:024a` | Clean round trip | Startup crash | Retired. `1408:046f` itself is still too shared when it jumps straight to a UI wrapper. |
|
||||
| A18 | Method-0-only callback patch, Candidate J: keep retail vtable base `1478:65ab`, patch only `1408:046f -> CALL FAR [1478:6597]; RETF`, preserve `1408:0474` as-is, route deferred target slot `1478:6597 -> 13a0:0086`, and zero inherited caller-word pushes in `13a0:008f` | Clean round trip | Startup crash | Retired. Same shared-`046f` failure pattern as Candidate I. |
|
||||
| A19 | Private callback stub patch, Candidate K: keep retail vtable base `1478:65ab`, patch only `1408:046f -> CALL FAR [1478:6597]; RETF`, repoint `1478:6597` to a private guard stub at `13e8:2318`, have that stub verify the callback object against `1478:659c/659e`, then call `13a0:020d` through the second relocated call slot in the patched `13e8:230d` body, and zero inherited caller-word pushes in `13a0:024a` | Clean round trip | Superseded before runtime | Retired in favor of the narrower `1408:00cf` break-next branch hook. The broad shared `1408:046f` body stayed the main structural risk. |
|
||||
| A20 | Private callback stub patch, Candidate L: keep retail vtable base `1478:65ab`, patch only `1408:046f -> CALL FAR [1478:6597]; RETF`, repoint `1478:6597` to a private guard stub at `13e8:2318`, have that stub verify the callback object against `1478:659c/659e`, then call `13a0:0086` through the second relocated call slot in the patched `13e8:230d` body, and zero inherited caller-word pushes in `13a0:008f` | Clean round trip | Superseded before runtime | Retired in favor of the narrower `1408:00cf` break-next branch hook. Same shared-`1408:046f` structural risk as Candidate K. |
|
||||
| A21 | Interpreter break-next stub patch, Candidate M: keep retail vtable base `1478:65ab`, leave `1408:046f/0474` untouched, patch only the `1408:00cf` break-next branch to indirect through `1478:6597 -> 13e8:2318`, and route the private stub to `13a0:020d` with zeroed inherited modal-wrapper words in `13a0:024a` | Clean round trip on disposable retail copy | Startup crash | Retired. The narrower hook was better, but the embedded `13e8` stub body was malformed and `1478:6597` is no longer credible as spare storage. |
|
||||
| A22 | Interpreter break-next stub patch, Candidate N: keep retail vtable base `1478:65ab`, leave `1408:046f/0474` untouched, patch only the `1408:00cf` break-next branch to indirect through `1478:6597 -> 13e8:2318`, and route the private stub to `13a0:0086` with zeroed inherited current-unit-wrapper words in `13a0:008f` | Clean round trip on disposable retail copy | Startup crash | Retired. Same root cause as Candidate M: unsafe `1478:6597` dependency plus a broken private-stub control-flow layout. |
|
||||
| A23 | Interpreter callsite retarget patch, Candidate O: keep retail vtable base `1478:65ab`, leave `1408:046f/0474` and `1408:00cf` untouched, patch the existing interpreter `CALLF 1408:0053` at `1418:04b5` to `13e8:232d`, and use a corrected embedded private stub/body in `13e8:230d` that either arms the existing `0x659c/659e` debugger object or lazily creates/stores one before routing to `13a0:020d` with zeroed inherited modal-wrapper words in `13a0:024a` | Clean round trip on disposable retail copy | Pending user runtime test | Live candidate. Removes both the shared seg1408 hook and the unsafe `1478:6597` deferred-target assumption. |
|
||||
| A24 | Interpreter callsite retarget patch, Candidate P: keep retail vtable base `1478:65ab`, leave `1408:046f/0474` and `1408:00cf` untouched, patch the existing interpreter `CALLF 1408:0053` at `1418:04b5` to `13e8:232d`, and use the same corrected embedded private stub/body in `13e8:230d` before routing to `13a0:0086` with zeroed inherited current-unit-wrapper words in `13a0:008f` | Clean round trip on disposable retail copy | Pending user runtime test | Live candidate. Same narrower interpreter-callsite design as Candidate O, but routed to the current-unit wrapper. |
|
||||
|
||||
## Root-Cause Findings From Failed Paths
|
||||
|
||||
### 1. On-disk NE fixup placeholders matter
|
||||
|
||||
The retail EXE stores internal far-call operands as `FF FF 00 00` placeholders on disk. Early patch generations incorrectly expected disassembly-resolved targets in the raw bytes, which caused false mismatch failures and one off-by-one patched-body bug.
|
||||
|
||||
### 2. `65af` is not a second vtable base
|
||||
|
||||
This is the most important structural correction from the recent passes.
|
||||
|
||||
`1408:0000` writes `0x65ab` to object `+0`. Later dispatch proves:
|
||||
- object method 0 uses `CALLF [BX]` and therefore reads the dword at `1478:65ab`
|
||||
- object method 1 uses `CALLF [BX+4]` and therefore reads the dword at `1478:65af`
|
||||
|
||||
Rewriting object `+0` to `0x65af` does not select a private callback table. It shifts the base one entry forward and corrupts the method-1 lookup.
|
||||
|
||||
### 3. Shared callback bodies are also too global
|
||||
|
||||
Even after preserving `0x65ab` as the object vtable base, patching the shared method-0 code at `1408:046f` still caused startup failure. That makes shared callback code just as dangerous as shared callback-table dwords for this workflow.
|
||||
|
||||
### 4. Deferred entry is still the right direction
|
||||
|
||||
The quit-on-`Ctrl+Q` result from the direct wrapper-call build is negative evidence against opening the debugger directly on the hotkey path. The safer model remains:
|
||||
- create or reuse the debugger-state object
|
||||
- arm break-next or step state in the object
|
||||
- let a later interpreter-side callback enter the UI after the original keypress path is gone
|
||||
|
||||
### 5. `1478:6718..673c` is not spare storage
|
||||
|
||||
The full `1478:6718..673c` band that fed Candidates D/E/F is now positively identified as a live function-pointer table, not spare relocated dword storage.
|
||||
|
||||
Recovered entries include:
|
||||
- `1478:6718 -> 1420:20dd` = `UsecodeProcess_1420_20dd`
|
||||
- `1478:671c -> 11e0:112c` = `Process_Terminate`
|
||||
- `1478:6720 -> 11e0:115c` = `Process_Fail`
|
||||
- `1478:6724 -> 1020:08cd` = `nullfn_1020_08cd`
|
||||
- `1478:6728 -> 1420:1162` = `UsecodeProcess_1420_1162`
|
||||
- `1478:672c -> 1420:1278` = `UsecodeProcess_1420_1278`
|
||||
- `1478:6730 -> 1420:118f` = `UsecodeProcess_1420_118f`
|
||||
- `1478:6734 -> 1020:08d2` = `nullfn_1020_08d2`
|
||||
- `1478:6738 -> 1420:10b6` = `UsecodeProcess_1420_10b6`
|
||||
- `1478:673c -> 1420:00cd` = `UsecodeProcess_1420_00cd`
|
||||
|
||||
Interpretation:
|
||||
- the entire band was a bad candidate pool even though direct data-use scans returned no hits
|
||||
- the startup crashes were expected once the patch began overwriting live process-dispatch entries
|
||||
- the private-vtable family should be considered closed unless a genuinely unused far-pointer region is proven some other way
|
||||
|
||||
### 6. `1408:0474` is not safe to overwrite casually
|
||||
|
||||
The failed guarded-shim Candidates G/H exposed a second structural issue in the callback family.
|
||||
|
||||
`1408:046f` and `1408:0474` are adjacent, but they are not interchangeable dead bytes:
|
||||
- `1408:046f` is the method-0 no-op callback (`RETF`)
|
||||
- `1408:0474` is the method-1 helper that explicitly returns `DX:AX = 0`
|
||||
|
||||
Overwriting the whole `046f/0474` cluster with one shared shim preserved the far-return shape but destroyed the zero-return behavior of `0474`. That is a plausible startup-failure source even when the callback target itself is otherwise reasonable.
|
||||
|
||||
Interpretation:
|
||||
- future callback-family patches should redirect only `1408:046f` unless there is hard evidence that `0474` can be replaced safely
|
||||
- preserving `0474` is a more defensible next step than searching for more spare dword tables
|
||||
|
||||
### 7. Shared `1408:046f` is also too broad when it jumps straight into UI code
|
||||
|
||||
Candidates I/J preserved `1408:0474`, but they still crashed on startup.
|
||||
|
||||
The stronger remaining explanation is that `1408:046f` is reached by more than just the debugger object, so a plain `CALL FAR [1478:6597]; RETF` that lands directly in `13a0:020d` or `13a0:0086` is still globally unsafe even when the wrapper arguments are sanitized.
|
||||
|
||||
Interpretation:
|
||||
- the callback-family lane still looks structurally right
|
||||
- but the shared seg1408 method must land in a private guard stub first
|
||||
- that stub needs to verify the callback object matches `1478:659c/659e` before calling any debugger UI wrapper
|
||||
|
||||
### 8. `1408:00cf` is a narrower deferred hook than `1408:046f`
|
||||
|
||||
The next structural refinement is that the break-next branch inside `usecode_debugger_maybe_break_on_current_line` is a better compiled hook than the shared vtable method body.
|
||||
|
||||
At `1408:00c5..00dc` the function does:
|
||||
- `LES BX,[BP+6]`
|
||||
- test object flag `+0x74`
|
||||
- only when that flag is armed, push the debugger object and dispatch method 0
|
||||
|
||||
That means a patch at `1408:00cf/00d3` is narrower than a patch at `1408:046f`:
|
||||
- it runs only after `0x659c/0x659e` is already non-null
|
||||
- it runs only on the explicit break-next path
|
||||
- it leaves the shared vtable slot `1478:65ab` and shared method body `1408:046f` untouched
|
||||
|
||||
Interpretation:
|
||||
- keep the private guard stub at `13e8:2318`
|
||||
- keep the deferred target slot `1478:6597`
|
||||
- move the shared hook from `1408:046f` to the break-next branch dispatch inside `1408:0053`
|
||||
|
||||
### 9. `1478:6597` is not safe to treat as spare storage
|
||||
|
||||
The M/N crash follow-up closed another false assumption.
|
||||
|
||||
Raw bytes at `1478:6580..65b4` show that the `6597` dword sits inside live debugger-adjacent data, not inside a clean unused relocation island. The same region contains active-looking far-pointer values and the nearby `DEBUGGER.C` string.
|
||||
|
||||
Interpretation:
|
||||
- no direct xrefs is not enough to prove this dword is safe
|
||||
- any design that depends on `1478:6597` as a private deferred callback slot should now be treated as retired negative evidence
|
||||
- the next safer hook has to reuse an already-live code lane rather than inventing a new global data slot
|
||||
|
||||
### 10. The embedded `13e8` private stub must be structurally correct byte-for-byte
|
||||
|
||||
The M/N family also exposed a second root cause: the private stub embedded into the patched `13e8:230d` body was malformed.
|
||||
|
||||
The failure was not just conceptual. The byte template itself had bad control-flow:
|
||||
- the top-of-body branch layout did not land on the intended create path cleanly
|
||||
- one private-stub jump target landed inside relocated call-immediate bytes instead of on a real instruction boundary
|
||||
- the stub/object-return path and the hotkey-handler return path were incorrectly entangled
|
||||
|
||||
Interpretation:
|
||||
- patch generation for this lane must be treated like handwritten machine code, not just a rough semantic sketch
|
||||
- any future candidate has to verify the exact patched-body length and branch targets as part of mechanical validation
|
||||
- this is why the newer O/P family retargets the existing interpreter callsite at `1418:04b5` and uses a corrected embedded stub at `13e8:232d` instead of reusing the old M/N body
|
||||
|
||||
### 11. The second `13e8` far-call relocation has to be written too
|
||||
|
||||
The first O/P runtime build exposed a script bug rather than a valid candidate result.
|
||||
|
||||
The patched `13e8:230d` body reuses two existing retail far-call slots:
|
||||
- `13e8:2352` for `usecode_debugger_break_state_create`
|
||||
- `13e8:235c` for the private debugger UI call
|
||||
|
||||
The initial O/P refactor correctly retargeted the interpreter call at `1418:04b5`, but it forgot to write the second relocation entry at `13e8:235c`. That left the embedded private UI call pointed at the retail `Dispatch_ModalGump` target instead of the selected debugger wrapper.
|
||||
|
||||
Interpretation:
|
||||
- the user's first O/P `No pity. No mercy. No remorse.` runtime result was from an invalid build, not from the documented O/P candidate semantics
|
||||
- candidate detection must verify both the interpreter hook and the reused `13e8:235c` far-call target
|
||||
- subsequent O/P testing is still required after the relocation-write fix
|
||||
|
||||
## Current Live Candidates
|
||||
|
||||
### Private Vtable Build, First Placement
|
||||
|
||||
Patch shape:
|
||||
- `13e8:230d` still lazily creates the seg1408 debugger-state object via `1408:0000`
|
||||
- returned far pointer is stored at `1478:659c/659e`
|
||||
- object `+0` is rewritten from `0x65ab` to private vtable base `0x658f`
|
||||
- object `+0x75` is set to `0`
|
||||
- object `+0x74` is set to `1`
|
||||
- private vtable method 0 at `1478:658f` is retargeted to `13a0:0086`
|
||||
- private vtable method 1 at `1478:6593` is retargeted to retail helper `1408:0474`
|
||||
|
||||
Why this is better:
|
||||
- no shared callback-table slot is rewritten
|
||||
- no shared callback code body is rewritten
|
||||
- the only global data edits are to unused relocated dwords with no current data uses
|
||||
- the object gets a complete two-entry table instead of a half-broken base rewrite
|
||||
|
||||
Mechanical status:
|
||||
- verified clean on a fresh retail copy
|
||||
- `0xC970D` patched/restored
|
||||
- `0xEA18F` patched/restored
|
||||
- `0xEA193` patched/restored
|
||||
- legacy cleanup sites held at original bytes
|
||||
|
||||
Runtime status:
|
||||
- startup crash in user runtime
|
||||
|
||||
Interpretation:
|
||||
- the `658f/6593` pair is not safe to reuse even though direct data-use scans reported no hits
|
||||
- this is strong evidence that some startup-time indirect table walk or loader-side consumer reaches that cluster
|
||||
|
||||
### Private Vtable Build, Moved Single Placement
|
||||
|
||||
Patch shape:
|
||||
- `13e8:230d` still lazily creates the seg1408 debugger-state object via `1408:0000`
|
||||
- returned far pointer is stored at `1478:659c/659e`
|
||||
- object `+0` is rewritten from `0x65ab` to private vtable base `0x6728`
|
||||
- object `+0x75` is set to `0`
|
||||
- object `+0x74` is set to `1`
|
||||
- private vtable method 0 at `1478:6728` is retargeted to `13a0:0086`
|
||||
- private vtable method 1 at `1478:672c` is retargeted to retail helper `1408:0474`
|
||||
|
||||
Mechanical status:
|
||||
- verified clean on a fresh retail copy
|
||||
|
||||
Runtime status:
|
||||
- startup crash in user runtime
|
||||
|
||||
Interpretation:
|
||||
- the farther pair `6728/672c` is also unsafe to reuse as a complete private table
|
||||
- the next iteration should stop shipping one candidate at a time and instead expose multiple disjoint pairs for fast runtime testing
|
||||
|
||||
### Multi-Candidate Harness
|
||||
|
||||
Current script shape:
|
||||
- `13e8:230d` still lazily creates the seg1408 debugger-state object via `1408:0000`
|
||||
- returned far pointer is stored at `1478:659c/659e`
|
||||
- object `+0x75` is set to `0`
|
||||
- object `+0x74` is set to `1`
|
||||
- the earlier script offered six explicit private-vtable candidates in one script:
|
||||
- Candidate A: base `1478:6724`, method 1 `1478:6728`
|
||||
- Candidate B: base `1478:672c`, method 1 `1478:6730`
|
||||
- Candidate C: base `1478:6734`, method 1 `1478:6738`
|
||||
- Candidate D: base `1478:6718`, method 1 `1478:671c`
|
||||
- Candidate E: base `1478:6720`, method 1 `1478:6724`
|
||||
- Candidate F: base `1478:6738`, method 1 `1478:673c`
|
||||
- each candidate retargets method 0 to `13a0:0086` and preserves method 1 on `1408:0474`
|
||||
- restore returns the selected pair and the shared `13e8:230d` body to the retail byte pattern
|
||||
|
||||
Why this is better than the single-candidate loop:
|
||||
- it reduces user turnaround by letting runtime tests move through multiple dword pairs without editing the script again
|
||||
- the three pairs are disjoint, so apply/restore and status detection remain mechanically simple
|
||||
- the harness preserves the same deferred-entry model while isolating the remaining unknown to `which relocated dword pair is actually boot-safe`
|
||||
|
||||
Mechanical status:
|
||||
- Candidate A apply/restore verified clean on a fresh retail copy
|
||||
- Candidate B apply/restore verified clean on a fresh retail copy
|
||||
- Candidate C apply/restore verified clean on a fresh retail copy
|
||||
- Candidate D apply/restore verified clean on a fresh retail copy
|
||||
- Candidate E apply/restore verified clean on a fresh retail copy
|
||||
- Candidate F apply/restore verified clean on a fresh retail copy
|
||||
|
||||
Runtime status:
|
||||
- Candidate A: DOSBox closes on start
|
||||
- Candidate B: fatal `Load program failed -- error code 201` on `C:\CRUSADER.EXE`
|
||||
- Candidate C: DOSBox closes on start
|
||||
- Candidate D: DOSBox closes on start
|
||||
- Candidate E: DOSBox closes on start
|
||||
- Candidate F: DOSBox closes on start
|
||||
|
||||
Interpretation:
|
||||
- the entire `1478:6718..673c` band is now negative evidence and should be treated as a live process-function table
|
||||
- Candidate B is distinct from the plain startup-crash paths because it fails in the loader/program-launch lane rather than only after the game begins booting
|
||||
- the next useful step is to stop testing that table and move to a callback-family patch that preserves the retail debugger object's real vtable
|
||||
|
||||
### Guarded Callback Candidates
|
||||
|
||||
Current script shape:
|
||||
- `13e8:230d` still lazily creates the seg1408 debugger-state object via `1408:0000`
|
||||
- returned far pointer is stored at `1478:659c/659e`
|
||||
- object `+0x75` is set to `0`
|
||||
- object `+0x74` is set to `1`
|
||||
- `1408:046f/0474` are replaced with a 14-byte null-guarded shim:
|
||||
- if `0x659c/0x659e == 0`, return immediately
|
||||
- otherwise `CALL FAR [1478:6597]` and then `RETF`
|
||||
- Candidate G routes `1478:6597` to `13a0:020d` and zeroes the inherited callback-object pushes in `13a0:024a`
|
||||
- Candidate H routes `1478:6597` to `13a0:0086` and zeroes the inherited callback-object pushes in `13a0:008f`
|
||||
|
||||
Why this is better than the private-vtable family:
|
||||
- it preserves the retail debugger-state object layout and the retail `1478:65ab` vtable base
|
||||
- it stops overwriting unrelated process-function tables in `1478:6718..673c`
|
||||
- it fixes the earlier calling-convention mismatch by preventing the debugger callback from forwarding the debugger-object far pointer as UI wrapper arguments
|
||||
- the null guard means startup should stay clean until the hotkey path has actually instantiated the debugger object
|
||||
|
||||
Mechanical status:
|
||||
- Candidate G apply/restore verified clean on a fresh retail copy
|
||||
- Candidate H apply/restore verified clean on a fresh retail copy
|
||||
|
||||
Runtime status:
|
||||
- Candidate G: startup crash
|
||||
- Candidate H: startup crash
|
||||
|
||||
Interpretation:
|
||||
- the calling-convention fix alone was not sufficient when `1408:0474` was also overwritten
|
||||
- the next callback-family patch should preserve the original `0474` helper bytes
|
||||
|
||||
### Interpreter Break-Next Stub Candidates
|
||||
|
||||
Current script shape:
|
||||
- `13e8:230d` still lazily creates the seg1408 debugger-state object via `1408:0000`
|
||||
- returned far pointer is stored at `1478:659c/659e`
|
||||
- object `+0x75` is set to `0`
|
||||
- object `+0x74` is set to `1`
|
||||
- retail `1478:65ab/65af` and retail `1408:046f/0474` are left untouched
|
||||
- only the break-next dispatch bytes at `1408:00d3..00d7` are patched, replacing the original `MOV BX, ES:[BX] / CALL FAR [BX]` with `CALL FAR [1478:6597]`
|
||||
- `1478:6597` no longer points at a UI wrapper; it points at a private guard stub embedded in the patched `13e8:230d` body at `13e8:2318`
|
||||
- that private stub compares the callback object passed on the stack against `1478:659c/659e`, clears `+0x74/+0x75` only on a matching object, and otherwise immediately `RETF`s
|
||||
- the stub uses the second relocated call site already present in `13e8:230d` to enter the debugger UI wrapper after the object-identity check passes
|
||||
- Candidate M uses that private call slot for `13a0:020d` and zeroes the inherited callback-object pushes in `13a0:024a`
|
||||
- Candidate N uses that private call slot for `13a0:0086` and zeroes the inherited callback-object pushes in `13a0:008f`
|
||||
|
||||
Why this is better than K/L:
|
||||
- it keeps the private guard stub and callback/UI argument sanitization work from the earlier private-stub design
|
||||
- it stops patching the shared seg1408 method-0 body at `1408:046f` entirely
|
||||
- it hooks only the armed break-next branch inside `1408:0053`, after the global debugger-state pointer is already live
|
||||
- it leaves the shared vtable slots and helper body `1408:0474` untouched
|
||||
|
||||
Mechanical status:
|
||||
- Candidate M apply/restore verified clean on a disposable retail copy
|
||||
- Candidate N apply/restore verified clean on a disposable retail copy
|
||||
|
||||
Runtime status:
|
||||
- Candidates M/N pending user test
|
||||
|
||||
## Next Checks If The Interpreter Break-Next Stub Build Still Fails
|
||||
|
||||
1. Verify whether startup stays clean through gameplay for Candidates M/N.
|
||||
2. If either candidate still fails before gameplay, inspect whether `1478:6597 -> 13e8:2318` is being reached from any unexpected non-break-next lane despite the narrower `1408:00cf` hook.
|
||||
3. If gameplay boots but `Ctrl+Q` does nothing, inspect whether `+0x74` is being cleared before the first eligible `1408:00cf` break-next dispatch.
|
||||
4. If gameplay boots but quits, re-evaluate whether `13a0:0086` or `13a0:020d` still inherits an unsafe modal/input state even from the interpreter-deferred path.
|
||||
5. If the debugger appears partially, capture the first bad UI/control-flow transition instead of changing the patch blind.
|
||||
6. If this lane fails completely, pivot from breakpoint-entry patching to a purpose-built event or task queue handoff that invokes the debugger outside both the keyboard handler and the shared seg1408 break helper.
|
||||
|
|
@ -1,17 +1,211 @@
|
|||
param(
|
||||
[ValidateSet('1', '2', '3', '4')]
|
||||
[string]$Choice
|
||||
[ValidateSet('1', '2', '3', '4', 'candidate-i', 'candidate-j', 'candidate-m', 'candidate-n', 'candidate-o', 'candidate-p', 'restore', 'exit')]
|
||||
[string]$Choice,
|
||||
|
||||
[string]$ExePath = $(
|
||||
if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'CRUSADER.EXE')) {
|
||||
Join-Path $PSScriptRoot 'CRUSADER.EXE'
|
||||
}
|
||||
elseif (Test-Path -LiteralPath 'F:\Apps\Crusader No Remorse\CRUSADER.EXE') {
|
||||
'F:\Apps\Crusader No Remorse\CRUSADER.EXE'
|
||||
}
|
||||
else {
|
||||
Join-Path $PSScriptRoot 'CRUSADER.EXE'
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$exePath = Join-Path $PSScriptRoot 'CRUSADER.EXE'
|
||||
$exePath = $ExePath
|
||||
$statePath = Join-Path $PSScriptRoot 'patch_crusader_cheat_menu.state.json'
|
||||
|
||||
$sites = @{
|
||||
CtrlQDebuggerInit = @{
|
||||
Label = 'Ctrl+Q debugger-state init body'
|
||||
Offset = 0xC970D
|
||||
Original = [byte[]](
|
||||
0xA0, 0x4F, 0x60, 0xB4, 0x00, 0xF7, 0xD8, 0x1B, 0xC0, 0x40, 0xA2, 0x4F, 0x60, 0x80, 0x3E, 0x4F,
|
||||
0x60, 0x00, 0x74, 0x47, 0x6A, 0xFF, 0x6A, 0xFF, 0xC4, 0x1E, 0xD0, 0x4C, 0x26, 0x8A, 0x47, 0x05,
|
||||
0x50, 0x1E, 0x68, 0xD2, 0x60, 0x6A, 0x00, 0x6A, 0x00, 0x83, 0xEC, 0x06, 0xC7, 0x86, 0x76, 0xFF,
|
||||
0x00, 0x00, 0x8B, 0x86, 0x76, 0xFF, 0xF7, 0xD0, 0x89, 0x86, 0x78, 0xFF, 0xC6, 0x86, 0x7A, 0xFF,
|
||||
0x00, 0x6A, 0x00, 0x6A, 0x00, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x14, 0x52, 0x50, 0x9A,
|
||||
0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x08, 0x5F, 0x5E, 0xC9, 0xCB, 0x6A, 0xFF, 0x6A, 0xFF, 0xC4,
|
||||
0x1E, 0xD0, 0x4C, 0x26, 0x8A, 0x47, 0x05, 0x50, 0x1E, 0x68, 0xEE, 0x60, 0x6A, 0x00, 0x6A, 0x00,
|
||||
0x83, 0xEC, 0x06, 0xC7
|
||||
)
|
||||
Patched = [byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x74, 0x10, 0xC4, 0x1E, 0x9C, 0x65, 0xC6, 0x47, 0x75,
|
||||
0x00, 0xC6, 0x47, 0x74, 0x01, 0x5F, 0x5E, 0xC9, 0xCB, 0x6A, 0x00, 0x6A, 0x00, 0xE9, 0x25, 0x00,
|
||||
0x55, 0x8B, 0xEC, 0xA1, 0x9C, 0x65, 0x39, 0x46, 0x06, 0x75, 0x16, 0xA1, 0x9E, 0x65, 0x39, 0x46,
|
||||
0x08, 0x75, 0x0E, 0xC4, 0x5E, 0x06, 0xC7, 0x47, 0x74, 0x00, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0xEB,
|
||||
0x0E, 0x5D, 0xCB, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0xEB, 0x0A, 0x9A,
|
||||
0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x5D, 0xCB, 0x0B, 0xC2, 0x74, 0x13, 0xA3, 0x9C, 0x65,
|
||||
0x89, 0x16, 0x9E, 0x65, 0x89, 0xC3, 0x8E, 0xC2, 0xC6, 0x47, 0x75, 0x00, 0xC6, 0x47, 0x74, 0x01,
|
||||
0x5F, 0x5E, 0xC9, 0xCB
|
||||
)
|
||||
LegacyPatched = [byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x52, 0x31, 0xC0, 0x50, 0x50, 0xEB, 0x36,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x0B, 0xC2, 0x75,
|
||||
0x03, 0xE9, 0x46, 0x06, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E, 0x65, 0xC4, 0x1E, 0x9C, 0x65, 0xC6,
|
||||
0x47, 0x75, 0x01, 0xC6, 0x47, 0x74, 0x00, 0xC7, 0x47, 0x76, 0x00, 0x00, 0xC7, 0x47, 0x78, 0x00,
|
||||
0x00, 0xE9, 0x26, 0x06, 0xC7
|
||||
)
|
||||
LegacyPatchedVariants = @(
|
||||
[byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x5E, 0x31, 0xC0, 0x50, 0x50, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04,
|
||||
0xEB, 0x0D, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x5F, 0x5E, 0xC9, 0xCB, 0x0B, 0xC2,
|
||||
0x74, 0xF8, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E, 0x65, 0x6A, 0x00, 0x6A, 0x00, 0xEB, 0xE2, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xC7
|
||||
),
|
||||
[byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x5E, 0x31, 0xC0, 0x50, 0x50, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0xEB, 0x0D, 0x9A,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x04, 0x5F, 0x5E, 0xC9, 0xCB, 0x0B, 0xC2, 0x74, 0xF7, 0xA3,
|
||||
0x9C, 0x65, 0x89, 0x16, 0x9E, 0x65, 0x6A, 0x00, 0x6A, 0x00, 0xEB, 0xE2, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0xEC, 0x06, 0xC7
|
||||
),
|
||||
[byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x55, 0x31, 0xC0, 0x50, 0x50, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04,
|
||||
0x52, 0x50, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E,
|
||||
0x65, 0xC4, 0x1E, 0x9C, 0x65, 0xC7, 0x07, 0xAF, 0x65, 0xC6, 0x47, 0x75, 0x01, 0xC6, 0x47, 0x74,
|
||||
0x00, 0x5F, 0x5E, 0xC9, 0xCB, 0x90, 0x90
|
||||
)
|
||||
)
|
||||
LegacyPatchedFixupState = 'Any'
|
||||
OriginalPatterns = @(
|
||||
@('A0', '4F', '60', 'B4', '00', 'F7', 'D8', '1B', 'C0', '40', 'A2', '4F', '60', '80', '3E', '4F', '60', '00', '74', '47')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xC9753
|
||||
OriginalTargetSeg = 107
|
||||
OriginalTargetOffset = 0x0046
|
||||
PatchedTargetSeg = 130
|
||||
PatchedTargetOffset = 0x0000
|
||||
}
|
||||
}
|
||||
DebuggerCallback = @{
|
||||
Label = 'legacy secondary call cleanup'
|
||||
Offset = 0xC975D
|
||||
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xC975D
|
||||
OriginalTargetSeg = 101
|
||||
OriginalTargetOffset = 0x1588
|
||||
PatchedTargetSeg = 101
|
||||
PatchedTargetOffset = 0x1588
|
||||
}
|
||||
}
|
||||
PrivateBreakpointMethod0 = @{
|
||||
Label = 'private break callback slot'
|
||||
Offset = 0xEA328
|
||||
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xEA328
|
||||
OriginalTargetSeg = 133
|
||||
OriginalTargetOffset = 0x1162
|
||||
PatchedTargetSeg = 117
|
||||
PatchedTargetOffset = 0x0086
|
||||
}
|
||||
}
|
||||
PrivateBreakpointMethod1 = @{
|
||||
Label = 'private helper callback slot'
|
||||
Offset = 0xEA32C
|
||||
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xEA32C
|
||||
OriginalTargetSeg = 133
|
||||
OriginalTargetOffset = 0x1278
|
||||
PatchedTargetSeg = 130
|
||||
PatchedTargetOffset = 0x0474
|
||||
}
|
||||
}
|
||||
LegacyBreakpointCallback = @{
|
||||
Label = 'legacy seg1408 break callback target'
|
||||
Offset = 0xEA1AB
|
||||
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xEA1AB
|
||||
OriginalTargetSeg = 130
|
||||
OriginalTargetOffset = 0x046F
|
||||
PatchedTargetSeg = 117
|
||||
PatchedTargetOffset = 0x0086
|
||||
}
|
||||
}
|
||||
CallbackGuardCode = @{
|
||||
Label = 'seg1408 break-next dispatch patch'
|
||||
Offset = 0xCEAD3
|
||||
Original = [byte[]](0x26, 0x8B, 0x1F, 0xFF, 0x1F)
|
||||
Patched = [byte[]](0xFF, 0x1E, 0x97, 0x65, 0x90)
|
||||
OriginalPatterns = @(
|
||||
@('26', '8B', '1F', 'FF', '1F')
|
||||
)
|
||||
}
|
||||
CallbackTargetSlot = @{
|
||||
Label = 'break-next private stub target slot'
|
||||
Offset = 0xEA197
|
||||
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xEA197
|
||||
OriginalTargetSeg = 5
|
||||
OriginalTargetOffset = 0x08D2
|
||||
PatchedTargetSeg = 131
|
||||
PatchedTargetOffset = 0x2318
|
||||
}
|
||||
}
|
||||
InterpreterBreakCall = @{
|
||||
Label = 'interpreter debugger break callsite'
|
||||
Offset = 0xCFAB5
|
||||
Original = [byte[]](0x9A, 0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0x9A, 0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('9A', 'FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xCFAB6
|
||||
OriginalTargetSeg = 130
|
||||
OriginalTargetOffset = 0x0053
|
||||
PatchedTargetSeg = 131
|
||||
PatchedTargetOffset = 0x232D
|
||||
}
|
||||
}
|
||||
Hook = @{
|
||||
Label = 'Hidden menu direct hook site'
|
||||
Label = 'Rejected direct cheat hook site'
|
||||
Offset = 0x70D75
|
||||
Original = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
||||
Patched = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
||||
|
|
@ -31,7 +225,7 @@ $sites = @{
|
|||
}
|
||||
}
|
||||
Wrapper = @{
|
||||
Label = 'Current-slot wrapper arg site'
|
||||
Label = 'Rejected current-slot wrapper arg site'
|
||||
Offset = 0xB9A8D
|
||||
Original = [byte[]](0x6A, 0x01, 0xFF, 0x76, 0x08, 0xFF, 0x76, 0x06)
|
||||
Patched = [byte[]](0x6A, 0x01, 0x6A, 0x00, 0x6A, 0x00, 0x90, 0x90)
|
||||
|
|
@ -66,6 +260,137 @@ $sites = @{
|
|||
}
|
||||
}
|
||||
|
||||
$candidateProfiles = [ordered]@{
|
||||
'candidate-o' = @{
|
||||
MenuKey = '1'
|
||||
Label = 'Candidate O'
|
||||
ArmMode = 'interpreter-callsite'
|
||||
UiTargetSeg = 117
|
||||
UiTargetOffset = 0x020D
|
||||
PatchCurrentUnitWrapper = $false
|
||||
PatchModalWrapper = $true
|
||||
Summary = 'interpreter callsite retarget -> corrected private modal stub with zeroed modal-wrapper args'
|
||||
}
|
||||
'candidate-p' = @{
|
||||
MenuKey = '2'
|
||||
Label = 'Candidate P'
|
||||
ArmMode = 'interpreter-callsite'
|
||||
UiTargetSeg = 117
|
||||
UiTargetOffset = 0x0086
|
||||
PatchCurrentUnitWrapper = $true
|
||||
PatchModalWrapper = $false
|
||||
Summary = 'interpreter callsite retarget -> corrected private current-unit stub with zeroed current-slot args'
|
||||
}
|
||||
}
|
||||
|
||||
$script:ctrlQPatchedTemplate = [byte[]]$sites.CtrlQDebuggerInit.Patched.Clone()
|
||||
$script:configuredProfileKey = 'candidate-o'
|
||||
|
||||
function Find-ByteSequenceOffset {
|
||||
param(
|
||||
[byte[]]$Bytes,
|
||||
[byte[]]$Pattern
|
||||
)
|
||||
|
||||
$lastStart = $Bytes.Length - $Pattern.Length
|
||||
for ($start = 0; $start -le $lastStart; $start++) {
|
||||
$matched = $true
|
||||
for ($index = 0; $index -lt $Pattern.Length; $index++) {
|
||||
if ($Bytes[$start + $index] -ne $Pattern[$index]) {
|
||||
$matched = $false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($matched) {
|
||||
return $start
|
||||
}
|
||||
}
|
||||
|
||||
throw 'Could not find the private-vtable base immediate inside the Ctrl+Q patch template.'
|
||||
}
|
||||
|
||||
function New-CtrlQPatchedBytes {
|
||||
param([string]$ArmMode)
|
||||
|
||||
switch ($ArmMode) {
|
||||
'interpreter-callsite' {
|
||||
return [byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x74, 0x10, 0xC4, 0x1E,
|
||||
0x9C, 0x65, 0xC6, 0x47, 0x75, 0x00, 0xC6, 0x47, 0x74, 0x01, 0x5F,
|
||||
0x5E, 0xC9, 0xCB, 0x6A, 0x00, 0x6A, 0x00, 0xE9, 0x25, 0x00, 0x55,
|
||||
0x8B, 0xEC, 0xA1, 0x9C, 0x65, 0x39, 0x46, 0x06, 0x75, 0x16, 0xA1,
|
||||
0x9E, 0x65, 0x39, 0x46, 0x08, 0x75, 0x0E, 0xC4, 0x5E, 0x06, 0xC7,
|
||||
0x47, 0x74, 0x00, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0xEB, 0x0E, 0x5D,
|
||||
0xCB, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04,
|
||||
0xEB, 0x0A, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x5D,
|
||||
0xCB, 0x0B, 0xC2, 0x74, 0x13, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E,
|
||||
0x65, 0x89, 0xC3, 0x8E, 0xC2, 0xC6, 0x47, 0x75, 0x00, 0xC6, 0x47,
|
||||
0x74, 0x01, 0x5F, 0x5E, 0xC9, 0xCB
|
||||
)
|
||||
}
|
||||
default {
|
||||
throw "Unsupported arm mode '$ArmMode'."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ConfiguredCandidateProfile {
|
||||
return $candidateProfiles[$script:configuredProfileKey]
|
||||
}
|
||||
|
||||
function Set-ConfiguredCandidateProfile {
|
||||
param([string]$ProfileKey)
|
||||
|
||||
if (-not $candidateProfiles.Contains($ProfileKey)) {
|
||||
throw "Unknown candidate profile '$ProfileKey'."
|
||||
}
|
||||
|
||||
$profile = $candidateProfiles[$ProfileKey]
|
||||
$sites.CtrlQDebuggerInit.Patched = New-CtrlQPatchedBytes -ArmMode ([string]$profile.ArmMode)
|
||||
$sites.DebuggerCallback.Fixup.PatchedTargetSeg = [int]$profile.UiTargetSeg
|
||||
$sites.DebuggerCallback.Fixup.PatchedTargetOffset = [int]$profile.UiTargetOffset
|
||||
$script:configuredProfileKey = $ProfileKey
|
||||
}
|
||||
|
||||
function Test-CandidateApplied {
|
||||
param(
|
||||
[byte[]]$FileBytes,
|
||||
[string]$ProfileKey
|
||||
)
|
||||
|
||||
$previousProfileKey = $script:configuredProfileKey
|
||||
try {
|
||||
Set-ConfiguredCandidateProfile -ProfileKey $ProfileKey
|
||||
return (
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.CtrlQDebuggerInit) -eq 'Patched' -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.InterpreterBreakCall) -eq 'Patched' -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.DebuggerCallback) -eq 'Patched' -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackGuardCode) -eq 'Original' -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackTargetSlot) -eq 'Original' -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper) -eq $(if ($candidateProfiles[$ProfileKey].PatchCurrentUnitWrapper) { 'Patched' } else { 'Original' }) -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredWrapper) -eq $(if ($candidateProfiles[$ProfileKey].PatchModalWrapper) { 'Patched' } else { 'Original' })
|
||||
)
|
||||
}
|
||||
finally {
|
||||
Set-ConfiguredCandidateProfile -ProfileKey $previousProfileKey
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AppliedCandidateProfileKey {
|
||||
param([byte[]]$FileBytes)
|
||||
|
||||
foreach ($profileKey in $candidateProfiles.Keys) {
|
||||
if (Test-CandidateApplied -FileBytes $FileBytes -ProfileKey $profileKey) {
|
||||
return $profileKey
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
Set-ConfiguredCandidateProfile -ProfileKey $script:configuredProfileKey
|
||||
|
||||
function Format-HexBytes {
|
||||
param([byte[]]$Bytes)
|
||||
|
||||
|
|
@ -449,6 +774,27 @@ function Get-SiteState {
|
|||
}
|
||||
}
|
||||
|
||||
$legacyCandidates = @()
|
||||
if ($Site.ContainsKey('LegacyPatched')) {
|
||||
$legacyCandidates += ,([byte[]]$Site.LegacyPatched)
|
||||
}
|
||||
if ($Site.ContainsKey('LegacyPatchedVariants')) {
|
||||
$legacyCandidates += $Site.LegacyPatchedVariants
|
||||
}
|
||||
|
||||
foreach ($legacyCandidate in $legacyCandidates) {
|
||||
if (Test-ByteArrayEqual -Left $current -Right $legacyCandidate) {
|
||||
$legacyFixupState = if ($Site.ContainsKey('LegacyPatchedFixupState')) { [string]$Site.LegacyPatchedFixupState } else { 'Patched' }
|
||||
if (
|
||||
($legacyFixupState -eq 'Patched' -and $isFixupPatched) -or
|
||||
($legacyFixupState -eq 'Original' -and $isFixupOriginal) -or
|
||||
($legacyFixupState -eq 'Any' -and ($isFixupPatched -or $isFixupOriginal))
|
||||
) {
|
||||
return 'LegacyPatched'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown'
|
||||
}
|
||||
|
||||
|
|
@ -502,18 +848,49 @@ function Set-ByteSlice {
|
|||
function Show-Status {
|
||||
param([byte[]]$FileBytes)
|
||||
|
||||
$selectedProfile = Get-ConfiguredCandidateProfile
|
||||
$appliedProfileKey = Get-AppliedCandidateProfileKey -FileBytes $FileBytes
|
||||
$appliedProfile = if ($null -ne $appliedProfileKey) { $candidateProfiles[$appliedProfileKey] } else { $null }
|
||||
|
||||
$ctrlQState = Get-SiteState -FileBytes $FileBytes -Site $sites.CtrlQDebuggerInit
|
||||
$interpreterCallState = Get-SiteState -FileBytes $FileBytes -Site $sites.InterpreterBreakCall
|
||||
$privateDispatchState = Get-SiteState -FileBytes $FileBytes -Site $sites.DebuggerCallback
|
||||
$callbackGuardState = Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackGuardCode
|
||||
$callbackTargetState = Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackTargetSlot
|
||||
$hookState = Get-SiteState -FileBytes $FileBytes -Site $sites.Hook
|
||||
$wrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper
|
||||
$deferredHookState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredHook
|
||||
$deferredWrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredWrapper
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'CRUSADER.EXE patch status'
|
||||
Write-Host '------------------------'
|
||||
Write-Host ("EXE: {0}" -f $exePath)
|
||||
Write-Host ("A hook @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
||||
Write-Host ("B wrapper @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
||||
Write-Host ("Selected candidate : {0} ({1})" -f $selectedProfile.Label, $selectedProfile.Summary)
|
||||
if ($null -ne $appliedProfile) {
|
||||
Write-Host ("Applied candidate : {0} ({1})" -f $appliedProfile.Label, $appliedProfile.Summary)
|
||||
}
|
||||
elseif ($ctrlQState -eq 'Original') {
|
||||
Write-Host 'Applied candidate : Retail/original'
|
||||
}
|
||||
else {
|
||||
Write-Host 'Applied candidate : Unknown'
|
||||
}
|
||||
Write-Host ("0x410 init body @ 0x{0:X}: {1}" -f $sites.CtrlQDebuggerInit.Offset, $ctrlQState)
|
||||
Write-Host ("Interpreter call @ 0x{0:X}: {1}" -f $sites.InterpreterBreakCall.Offset, $interpreterCallState)
|
||||
Write-Host ("Private UI call @ 0x{0:X}: {1}" -f $sites.DebuggerCallback.Offset, $privateDispatchState)
|
||||
Write-Host ("Legacy break hook @ 0x{0:X}: {1}" -f $sites.CallbackGuardCode.Offset, $callbackGuardState)
|
||||
Write-Host ("Legacy target slot @ 0x{0:X}: {1}" -f $sites.CallbackTargetSlot.Offset, $callbackTargetState)
|
||||
Write-Host ("Current-unit args @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
||||
Write-Host ("Modal wrapper args @ 0x{0:X}: {1}" -f $sites.LegacyDeferredWrapper.Offset, $deferredWrapperState)
|
||||
Write-Host ("Deferred hook cleanup @ 0x{0:X}: {1}" -f $sites.LegacyDeferredHook.Offset, $deferredHookState)
|
||||
Write-Host ("Direct hook cleanup @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
||||
Write-Host ''
|
||||
Write-Host '1. Apply supported hidden-menu patch (aliases to Experiment B)'
|
||||
Write-Host '2. Apply Experiment B (retarget + modal arg fix)'
|
||||
foreach ($profileKey in $candidateProfiles.Keys) {
|
||||
$profile = $candidateProfiles[$profileKey]
|
||||
$marker = if ($appliedProfileKey -eq $profileKey) { 'applied' } else { 'ready' }
|
||||
Write-Host ("{0}. Apply {1} [{2}] {3}" -f $profile.MenuKey, $profile.Label, $marker, $profile.Summary)
|
||||
}
|
||||
Write-Host '3. Restore original bytes'
|
||||
Write-Host '4. Exit'
|
||||
Write-Host ''
|
||||
|
|
@ -521,95 +898,120 @@ function Show-Status {
|
|||
|
||||
function Set-DesiredState {
|
||||
param(
|
||||
[bool]$HookPatched,
|
||||
[bool]$WrapperPatched,
|
||||
[string]$Label
|
||||
[bool]$CtrlQPatched,
|
||||
[string]$Label,
|
||||
[string]$ProfileKey
|
||||
)
|
||||
|
||||
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
|
||||
$stateData = Get-StateData
|
||||
|
||||
$appliedProfileKey = Get-AppliedCandidateProfileKey -FileBytes $fileBytes
|
||||
$profileKeyForStateCheck = if ($null -ne $appliedProfileKey) { $appliedProfileKey } elseif ($CtrlQPatched) { $ProfileKey } else { $script:configuredProfileKey }
|
||||
$profileKeyForWrite = if ($CtrlQPatched) { $ProfileKey } else { $profileKeyForStateCheck }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($profileKeyForStateCheck)) {
|
||||
throw 'No candidate profile is available for state validation.'
|
||||
}
|
||||
|
||||
Set-ConfiguredCandidateProfile -ProfileKey $profileKeyForStateCheck
|
||||
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.CtrlQDebuggerInit
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.InterpreterBreakCall
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.DebuggerCallback
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.CallbackGuardCode
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.CallbackTargetSlot
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod0
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod1
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyBreakpointCallback
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.Hook
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.Wrapper
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyDeferredHook
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyDeferredWrapper
|
||||
|
||||
$hookCurrent = Get-ByteSlice -Bytes $fileBytes -Offset $sites.Hook.Offset -Count $sites.Hook.Original.Length
|
||||
Set-ConfiguredCandidateProfile -ProfileKey $profileKeyForWrite
|
||||
$activeProfile = Get-ConfiguredCandidateProfile
|
||||
|
||||
$ctrlQFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.CtrlQDebuggerInit
|
||||
$interpreterBreakCallFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.InterpreterBreakCall
|
||||
$debuggerCallbackFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.DebuggerCallback
|
||||
$callbackTargetFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.CallbackTargetSlot
|
||||
$privateMethod0FixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod0
|
||||
$privateMethod1FixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod1
|
||||
$legacyCallbackFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.LegacyBreakpointCallback
|
||||
$hookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.Hook
|
||||
$wrapperCurrent = Get-ByteSlice -Bytes $fileBytes -Offset $sites.Wrapper.Offset -Count $sites.Wrapper.Original.Length
|
||||
$legacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.LegacyDeferredHook
|
||||
$hookCurrentState = Get-SiteState -FileBytes $fileBytes -Site $sites.Hook
|
||||
$wrapperCurrentState = Get-SiteState -FileBytes $fileBytes -Site $sites.Wrapper
|
||||
|
||||
$stateChanged = $false
|
||||
if ($hookCurrentState -eq 'Original') {
|
||||
$stateChanged = (Save-OriginalBytesIfMissing -StateData $stateData -SiteKey 'Hook' -SiteOffset $sites.Hook.Offset -Bytes $hookCurrent) -or $stateChanged
|
||||
if (-not $stateData.ContainsKey('HookFixup')) {
|
||||
$stateData['HookFixup'] = @{
|
||||
TargetSeg = $hookFixupInfo.TargetSeg
|
||||
TargetOffset = $hookFixupInfo.TargetOffset
|
||||
Reserved = $hookFixupInfo.Reserved
|
||||
$ctrlQBytes = if ($CtrlQPatched) { $sites.CtrlQDebuggerInit.Patched } else { $sites.CtrlQDebuggerInit.Original }
|
||||
$ctrlQTargetSeg = if ($CtrlQPatched) { [int]$sites.CtrlQDebuggerInit.Fixup.PatchedTargetSeg } else { [int]$sites.CtrlQDebuggerInit.Fixup.OriginalTargetSeg }
|
||||
$ctrlQTargetOffset = if ($CtrlQPatched) { [int]$sites.CtrlQDebuggerInit.Fixup.PatchedTargetOffset } else { [int]$sites.CtrlQDebuggerInit.Fixup.OriginalTargetOffset }
|
||||
$interpreterBreakCallBytes = $sites.InterpreterBreakCall.Original
|
||||
$interpreterBreakCallSeg = [int]$sites.InterpreterBreakCall.Fixup.OriginalTargetSeg
|
||||
$interpreterBreakCallOffset = [int]$sites.InterpreterBreakCall.Fixup.OriginalTargetOffset
|
||||
if ($CtrlQPatched) {
|
||||
$interpreterBreakCallBytes = $sites.InterpreterBreakCall.Patched
|
||||
$interpreterBreakCallSeg = [int]$sites.InterpreterBreakCall.Fixup.PatchedTargetSeg
|
||||
$interpreterBreakCallOffset = [int]$sites.InterpreterBreakCall.Fixup.PatchedTargetOffset
|
||||
}
|
||||
$stateChanged = $true
|
||||
$debuggerCallbackBytes = $sites.DebuggerCallback.Original
|
||||
$debuggerCallbackSeg = [int]$sites.DebuggerCallback.Fixup.OriginalTargetSeg
|
||||
$debuggerCallbackOffset = [int]$sites.DebuggerCallback.Fixup.OriginalTargetOffset
|
||||
if ($CtrlQPatched) {
|
||||
$debuggerCallbackBytes = $sites.DebuggerCallback.Patched
|
||||
$debuggerCallbackSeg = [int]$sites.DebuggerCallback.Fixup.PatchedTargetSeg
|
||||
$debuggerCallbackOffset = [int]$sites.DebuggerCallback.Fixup.PatchedTargetOffset
|
||||
}
|
||||
}
|
||||
if ($wrapperCurrentState -eq 'Original') {
|
||||
$stateChanged = (Save-OriginalBytesIfMissing -StateData $stateData -SiteKey 'Wrapper' -SiteOffset $sites.Wrapper.Offset -Bytes $wrapperCurrent) -or $stateChanged
|
||||
}
|
||||
if ($stateChanged) {
|
||||
Save-StateData -StateData $stateData
|
||||
}
|
||||
|
||||
if ($HookPatched) {
|
||||
$hookBytes = $sites.Hook.Patched
|
||||
$hookTargetSeg = [int]$sites.Hook.Fixup.PatchedTargetSeg
|
||||
$hookTargetOffset = [int]$sites.Hook.Fixup.PatchedTargetOffset
|
||||
$hookReserved = 0
|
||||
}
|
||||
else {
|
||||
$hookBytes = Get-SavedOriginalBytes -StateData $stateData -SiteKey 'Hook' -SiteOffset $sites.Hook.Offset
|
||||
if ($null -eq $hookBytes) {
|
||||
if ($hookCurrentState -eq 'Original' -or $hookCurrentState -eq 'LegacyBadPatch') {
|
||||
$hookBytes = $hookCurrent
|
||||
}
|
||||
else {
|
||||
throw 'No saved original bytes are available for the Experiment A hook site. Restore requires either a prior patch run with this script or your full executable backup.'
|
||||
}
|
||||
}
|
||||
|
||||
if ($stateData.ContainsKey('HookFixup')) {
|
||||
$hookTargetSeg = [int]$stateData['HookFixup'].TargetSeg
|
||||
$hookTargetOffset = [int]$stateData['HookFixup'].TargetOffset
|
||||
$hookReserved = [int]$stateData['HookFixup'].Reserved
|
||||
}
|
||||
else {
|
||||
$callbackGuardBytes = $sites.CallbackGuardCode.Original
|
||||
$callbackTargetBytes = $sites.CallbackTargetSlot.Original
|
||||
$callbackTargetSeg = [int]$sites.CallbackTargetSlot.Fixup.OriginalTargetSeg
|
||||
$callbackTargetOffset = [int]$sites.CallbackTargetSlot.Fixup.OriginalTargetOffset
|
||||
$privateMethod0Bytes = $sites.PrivateBreakpointMethod0.Original
|
||||
$privateMethod0TargetSeg = [int]$sites.PrivateBreakpointMethod0.Fixup.OriginalTargetSeg
|
||||
$privateMethod0TargetOffset = [int]$sites.PrivateBreakpointMethod0.Fixup.OriginalTargetOffset
|
||||
$privateMethod1Bytes = $sites.PrivateBreakpointMethod1.Original
|
||||
$privateMethod1TargetSeg = [int]$sites.PrivateBreakpointMethod1.Fixup.OriginalTargetSeg
|
||||
$privateMethod1TargetOffset = [int]$sites.PrivateBreakpointMethod1.Fixup.OriginalTargetOffset
|
||||
$legacyCallbackBytes = $sites.LegacyBreakpointCallback.Original
|
||||
$legacyCallbackTargetSeg = [int]$sites.LegacyBreakpointCallback.Fixup.OriginalTargetSeg
|
||||
$legacyCallbackTargetOffset = [int]$sites.LegacyBreakpointCallback.Fixup.OriginalTargetOffset
|
||||
$hookBytes = $sites.Hook.Original
|
||||
$hookTargetSeg = [int]$sites.Hook.Fixup.OriginalTargetSeg
|
||||
$hookTargetOffset = [int]$sites.Hook.Fixup.OriginalTargetOffset
|
||||
$hookReserved = 0
|
||||
}
|
||||
}
|
||||
|
||||
if ($WrapperPatched) {
|
||||
$wrapperBytes = $sites.Wrapper.Patched
|
||||
}
|
||||
else {
|
||||
$wrapperBytes = Get-SavedOriginalBytes -StateData $stateData -SiteKey 'Wrapper' -SiteOffset $sites.Wrapper.Offset
|
||||
if ($null -eq $wrapperBytes) {
|
||||
if ($wrapperCurrentState -eq 'Original') {
|
||||
$wrapperBytes = $wrapperCurrent
|
||||
}
|
||||
else {
|
||||
throw 'No saved original bytes are available for the Experiment B wrapper site. Restore requires either a prior patch run with this script or your full executable backup.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$wrapperBytes = if ($CtrlQPatched -and $activeProfile.PatchCurrentUnitWrapper) { $sites.Wrapper.Patched } else { $sites.Wrapper.Original }
|
||||
$legacyDeferredHookBytes = $sites.LegacyDeferredHook.Original
|
||||
$legacyDeferredHookTargetSeg = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetSeg
|
||||
$legacyDeferredHookTargetOffset = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetOffset
|
||||
$legacyDeferredHookReserved = 0
|
||||
$legacyDeferredWrapperBytes = $sites.LegacyDeferredWrapper.Original
|
||||
$legacyDeferredWrapperBytes = if ($CtrlQPatched -and $activeProfile.PatchModalWrapper) { $sites.LegacyDeferredWrapper.Patched } else { $sites.LegacyDeferredWrapper.Original }
|
||||
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.CtrlQDebuggerInit.Offset -Value $ctrlQBytes
|
||||
$fileBytes[$ctrlQFixupInfo.EntryOffset + 4] = [byte]$ctrlQTargetSeg
|
||||
$fileBytes[$ctrlQFixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($ctrlQFixupInfo.EntryOffset + 6) -Value $ctrlQTargetOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.InterpreterBreakCall.Offset -Value $interpreterBreakCallBytes
|
||||
$fileBytes[$interpreterBreakCallFixupInfo.EntryOffset + 4] = [byte]$interpreterBreakCallSeg
|
||||
$fileBytes[$interpreterBreakCallFixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($interpreterBreakCallFixupInfo.EntryOffset + 6) -Value $interpreterBreakCallOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.DebuggerCallback.Offset -Value $debuggerCallbackBytes
|
||||
$fileBytes[$debuggerCallbackFixupInfo.EntryOffset + 4] = [byte]$debuggerCallbackSeg
|
||||
$fileBytes[$debuggerCallbackFixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($debuggerCallbackFixupInfo.EntryOffset + 6) -Value $debuggerCallbackOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.CallbackGuardCode.Offset -Value $callbackGuardBytes
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.CallbackTargetSlot.Offset -Value $callbackTargetBytes
|
||||
$fileBytes[$callbackTargetFixupInfo.EntryOffset + 4] = [byte]$callbackTargetSeg
|
||||
$fileBytes[$callbackTargetFixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($callbackTargetFixupInfo.EntryOffset + 6) -Value $callbackTargetOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.PrivateBreakpointMethod0.Offset -Value $privateMethod0Bytes
|
||||
$fileBytes[$privateMethod0FixupInfo.EntryOffset + 4] = [byte]$privateMethod0TargetSeg
|
||||
$fileBytes[$privateMethod0FixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($privateMethod0FixupInfo.EntryOffset + 6) -Value $privateMethod0TargetOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.PrivateBreakpointMethod1.Offset -Value $privateMethod1Bytes
|
||||
$fileBytes[$privateMethod1FixupInfo.EntryOffset + 4] = [byte]$privateMethod1TargetSeg
|
||||
$fileBytes[$privateMethod1FixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($privateMethod1FixupInfo.EntryOffset + 6) -Value $privateMethod1TargetOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.LegacyBreakpointCallback.Offset -Value $legacyCallbackBytes
|
||||
$fileBytes[$legacyCallbackFixupInfo.EntryOffset + 4] = [byte]$legacyCallbackTargetSeg
|
||||
$fileBytes[$legacyCallbackFixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($legacyCallbackFixupInfo.EntryOffset + 6) -Value $legacyCallbackTargetOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.Hook.Offset -Value $hookBytes
|
||||
$fileBytes[$hookFixupInfo.EntryOffset + 4] = [byte]$hookTargetSeg
|
||||
$fileBytes[$hookFixupInfo.EntryOffset + 5] = [byte]$hookReserved
|
||||
|
|
@ -624,15 +1026,97 @@ function Set-DesiredState {
|
|||
[System.IO.File]::WriteAllBytes($exePath, $fileBytes)
|
||||
|
||||
$verifyBytes = [System.IO.File]::ReadAllBytes($exePath)
|
||||
$ctrlQState = Get-SiteState -FileBytes $verifyBytes -Site $sites.CtrlQDebuggerInit
|
||||
$interpreterCallState = Get-SiteState -FileBytes $verifyBytes -Site $sites.InterpreterBreakCall
|
||||
$privateDispatchState = Get-SiteState -FileBytes $verifyBytes -Site $sites.DebuggerCallback
|
||||
$callbackGuardState = Get-SiteState -FileBytes $verifyBytes -Site $sites.CallbackGuardCode
|
||||
$callbackTargetState = Get-SiteState -FileBytes $verifyBytes -Site $sites.CallbackTargetSlot
|
||||
$hookState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Hook
|
||||
$wrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Wrapper
|
||||
$deferredHookState = Get-SiteState -FileBytes $verifyBytes -Site $sites.LegacyDeferredHook
|
||||
$deferredWrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites.LegacyDeferredWrapper
|
||||
$verifiedCtrlQFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.CtrlQDebuggerInit
|
||||
$verifiedInterpreterBreakCallFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.InterpreterBreakCall
|
||||
$verifiedDebuggerCallbackFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.DebuggerCallback
|
||||
$verifiedCallbackTargetFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.CallbackTargetSlot
|
||||
$verifiedPrivateMethod0FixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.PrivateBreakpointMethod0
|
||||
$verifiedPrivateMethod1FixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.PrivateBreakpointMethod1
|
||||
$verifiedLegacyCallbackFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.LegacyBreakpointCallback
|
||||
$verifiedHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.Hook
|
||||
$verifiedLegacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.LegacyDeferredHook
|
||||
$verifiedCtrlQBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.CtrlQDebuggerInit.Offset -Count $ctrlQBytes.Length
|
||||
$verifiedInterpreterBreakCallBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.InterpreterBreakCall.Offset -Count $interpreterBreakCallBytes.Length
|
||||
$verifiedDebuggerCallbackBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.DebuggerCallback.Offset -Count $debuggerCallbackBytes.Length
|
||||
$verifiedCallbackGuardBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.CallbackGuardCode.Offset -Count $callbackGuardBytes.Length
|
||||
$verifiedCallbackTargetBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.CallbackTargetSlot.Offset -Count $callbackTargetBytes.Length
|
||||
$verifiedPrivateMethod0Bytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.PrivateBreakpointMethod0.Offset -Count $privateMethod0Bytes.Length
|
||||
$verifiedPrivateMethod1Bytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.PrivateBreakpointMethod1.Offset -Count $privateMethod1Bytes.Length
|
||||
$verifiedLegacyCallbackBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyBreakpointCallback.Offset -Count $legacyCallbackBytes.Length
|
||||
$verifiedHookBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.Hook.Offset -Count $hookBytes.Length
|
||||
$verifiedWrapperBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.Wrapper.Offset -Count $wrapperBytes.Length
|
||||
$verifiedLegacyDeferredHookBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyDeferredHook.Offset -Count $legacyDeferredHookBytes.Length
|
||||
$verifiedLegacyDeferredWrapperBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyDeferredWrapper.Offset -Count $legacyDeferredWrapperBytes.Length
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedCtrlQBytes -Right $ctrlQBytes)) {
|
||||
throw 'Ctrl+Q debugger-init body verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedCtrlQFixupInfo.TargetSeg -ne $ctrlQTargetSeg -or $verifiedCtrlQFixupInfo.TargetOffset -ne $ctrlQTargetOffset) {
|
||||
throw 'Ctrl+Q debugger-init relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedInterpreterBreakCallBytes -Right $interpreterBreakCallBytes)) {
|
||||
throw 'Interpreter debugger callsite verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedInterpreterBreakCallFixupInfo.TargetSeg -ne $interpreterBreakCallSeg -or $verifiedInterpreterBreakCallFixupInfo.TargetOffset -ne $interpreterBreakCallOffset) {
|
||||
throw 'Interpreter debugger callsite relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedDebuggerCallbackBytes -Right $debuggerCallbackBytes)) {
|
||||
throw 'Private debugger UI call verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedDebuggerCallbackFixupInfo.TargetSeg -ne $debuggerCallbackSeg -or $verifiedDebuggerCallbackFixupInfo.TargetOffset -ne $debuggerCallbackOffset) {
|
||||
throw 'Private debugger UI call relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedCallbackGuardBytes -Right $callbackGuardBytes)) {
|
||||
throw 'Break-next dispatch patch verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedCallbackTargetBytes -Right $callbackTargetBytes)) {
|
||||
throw 'Guarded callback target slot bytes verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedCallbackTargetFixupInfo.TargetSeg -ne $callbackTargetSeg -or $verifiedCallbackTargetFixupInfo.TargetOffset -ne $callbackTargetOffset) {
|
||||
throw 'Guarded callback target relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedPrivateMethod0Bytes -Right $privateMethod0Bytes)) {
|
||||
throw 'Private vtable method-0 bytes verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedPrivateMethod0FixupInfo.TargetSeg -ne $privateMethod0TargetSeg -or $verifiedPrivateMethod0FixupInfo.TargetOffset -ne $privateMethod0TargetOffset) {
|
||||
throw 'Private vtable method-0 relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedPrivateMethod1Bytes -Right $privateMethod1Bytes)) {
|
||||
throw 'Private vtable method-1 bytes verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedPrivateMethod1FixupInfo.TargetSeg -ne $privateMethod1TargetSeg -or $verifiedPrivateMethod1FixupInfo.TargetOffset -ne $privateMethod1TargetOffset) {
|
||||
throw 'Private vtable method-1 relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedLegacyCallbackBytes -Right $legacyCallbackBytes)) {
|
||||
throw 'Legacy callback cleanup bytes verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedLegacyCallbackFixupInfo.TargetSeg -ne $legacyCallbackTargetSeg -or $verifiedLegacyCallbackFixupInfo.TargetOffset -ne $legacyCallbackTargetOffset) {
|
||||
throw 'Legacy callback cleanup relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedHookBytes -Right $hookBytes)) {
|
||||
throw 'Hook-site verification failed after write.'
|
||||
}
|
||||
|
|
@ -659,33 +1143,75 @@ function Set-DesiredState {
|
|||
|
||||
Write-Host ''
|
||||
Write-Host ("Applied: {0}" -f $Label)
|
||||
Write-Host ("A hook @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
||||
Write-Host ("B wrapper @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
||||
Write-Host ("0x410 init body @ 0x{0:X}: {1}" -f $sites.CtrlQDebuggerInit.Offset, $ctrlQState)
|
||||
Write-Host ("Interpreter call @ 0x{0:X}: {1}" -f $sites.InterpreterBreakCall.Offset, $interpreterCallState)
|
||||
Write-Host ("Private UI call @ 0x{0:X}: {1}" -f $sites.DebuggerCallback.Offset, $privateDispatchState)
|
||||
Write-Host ("Legacy break hook @ 0x{0:X}: {1}" -f $sites.CallbackGuardCode.Offset, $callbackGuardState)
|
||||
Write-Host ("Legacy target slot @ 0x{0:X}: {1}" -f $sites.CallbackTargetSlot.Offset, $callbackTargetState)
|
||||
Write-Host ("Current-unit args @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
||||
Write-Host ("Modal wrapper args @ 0x{0:X}: {1}" -f $sites.LegacyDeferredWrapper.Offset, $deferredWrapperState)
|
||||
Write-Host ("Deferred hook cleanup @ 0x{0:X}: {1}" -f $sites.LegacyDeferredHook.Offset, $deferredHookState)
|
||||
Write-Host ("Direct hook cleanup @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
||||
Write-Host ''
|
||||
Write-Host 'What this means:'
|
||||
Write-Host '- Experiment A retargets the existing cheat-success far call into cheat_menu_open_from_current_slot while keeping the original event-dispatch framing.'
|
||||
Write-Host '- Experiment B preserves the wrapper mode byte `1` but forces the two ambiguous 16-bit constructor parameters to zero instead of inheriting arbitrary caller-frame values.'
|
||||
Write-Host '- Restore also cleans up the rejected deferred-event patch sites if they were left behind by earlier attempts.'
|
||||
Write-Host '- Ctrl+Q still goes through the real 0x410 keyboard lane and keeps the original 0x844 cheat gate.'
|
||||
if ($CtrlQPatched) {
|
||||
Write-Host ('- The 0x410 body now correctly handles both cases: it arms an existing seg1408 debugger-state object in place, or lazily creates one and stores it at 0x659c/0x659e before arming break-next mode (+0x74=1, +0x75=0).')
|
||||
Write-Host ("- {0} stops using the unsafe 1478:6597 data slot entirely and retargets the existing interpreter call at 1418:04b5 straight into the corrected private 13e8:232d stub." -f $activeProfile.Label)
|
||||
if ($activeProfile.PatchCurrentUnitWrapper) {
|
||||
Write-Host '- The current-unit debugger wrapper at 13a0:0086 has its inherited caller-word pushes zeroed so the callback does not forward the debugger-object pointer as UI arguments.'
|
||||
}
|
||||
if ($activeProfile.PatchModalWrapper) {
|
||||
Write-Host '- The modal debugger wrapper at 13a0:020d has its inherited caller-word pushes zeroed for the same reason.'
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host '- All debugger patch changes are restored to the retail byte pattern, including the 0x410 body and the interpreter break call at 1418:04b5.'
|
||||
Write-Host '- Older private-vtable and direct/deferred experiment sites are also written back to retail bytes during restore.'
|
||||
}
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
function Invoke-MenuChoice {
|
||||
param([string]$SelectedChoice)
|
||||
|
||||
switch ($SelectedChoice.Trim()) {
|
||||
switch ($SelectedChoice.Trim().ToLowerInvariant()) {
|
||||
'1' {
|
||||
Write-Warning 'Experiment A alone is not supported on the cheat-code path. Applying the safer Experiment B patch instead.'
|
||||
Set-DesiredState -HookPatched $true -WrapperPatched $true -Label 'Experiment B (via menu option 1 alias)'
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
||||
}
|
||||
'2' {
|
||||
Set-DesiredState -HookPatched $true -WrapperPatched $true -Label 'Experiment B (A + B)'
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
||||
}
|
||||
'candidate-i' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
||||
}
|
||||
'candidate-j' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
||||
}
|
||||
'candidate-m' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
||||
}
|
||||
'candidate-n' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
||||
}
|
||||
'candidate-o' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
||||
}
|
||||
'candidate-p' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
||||
}
|
||||
'3' {
|
||||
Set-DesiredState -HookPatched $false -WrapperPatched $false -Label 'Restore original bytes'
|
||||
Set-DesiredState -CtrlQPatched $false -ProfileKey $null -Label 'Restore original bytes'
|
||||
}
|
||||
'restore' {
|
||||
Set-DesiredState -CtrlQPatched $false -ProfileKey $null -Label 'Restore original bytes'
|
||||
}
|
||||
'4' {
|
||||
return $false
|
||||
}
|
||||
'exit' {
|
||||
return $false
|
||||
}
|
||||
default {
|
||||
Write-Warning 'Invalid selection.'
|
||||
}
|
||||
|
|
@ -695,7 +1221,7 @@ function Invoke-MenuChoice {
|
|||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $exePath)) {
|
||||
throw "CRUSADER.EXE was not found next to the script. Put this .ps1 file in the same folder as CRUSADER.EXE."
|
||||
throw "CRUSADER.EXE was not found at '$exePath'. Pass -ExePath to point at the retail install or place the EXE next to this script."
|
||||
}
|
||||
|
||||
if ($PSBoundParameters.ContainsKey('Choice')) {
|
||||
|
|
@ -706,7 +1232,7 @@ if ($PSBoundParameters.ContainsKey('Choice')) {
|
|||
:mainloop while ($true) {
|
||||
$currentBytes = [System.IO.File]::ReadAllBytes($exePath)
|
||||
Show-Status -FileBytes $currentBytes
|
||||
$choice = Read-Host 'Select 1, 2, 3, or 4'
|
||||
$choice = Read-Host 'Select 1-4'
|
||||
if ([string]::IsNullOrEmpty($choice)) { break mainloop }
|
||||
|
||||
if (-not (Invoke-MenuChoice -SelectedChoice $choice)) {
|
||||
|
|
|
|||
32
plan-mid.md
32
plan-mid.md
|
|
@ -79,6 +79,38 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan
|
|||
- The next blocker layer is narrower too. Those modal wrappers are not abstract helpers; inside `World_HandleKeyboardInput_13e8_14b4` they already wrap concrete user-facing lanes including exit-to-DOS confirmation (`0x22d`), quick save (`0x13f`), quick load (`0x13e`), restart/main-menu handling (`Game_RestartMaybe`), and the neighboring load/menu gump lanes. Separately, event `0x7e` remains the only other recovered writer of `0x6045` besides `Key_CheckCheatToggle`, so a successful `jassica16` match can still be undone later by that independent runtime path. `Key_CheckCheatToggle` itself is now comment-backed as keydown-only and still requires top-row `1` / `6` scan codes at the tail, leaving keypad digits and other non-matching input routes as a still-live explanation for failed tests.
|
||||
- Cross-game verification against the currently opened `REGRET.EXE` now has a runtime correction too. The F10 branch at `1148:0d0e` still reaches the same modifier helper at `11e0:01a8`, and live testing shows the practical gesture is hold `F10` first and then press `Ctrl`, not `Alt`. The same BIOS-backed helper swap should be verified directly in that target before promoting renames there. The same runtime test also explains the repeated immortality popups: the F10 branch is not debounced, so holding the keys lets repeated F10 keydown events flip immortality on and off multiple times. The real gameplay difference remains the latch code: `1148:34d2` (`Key_CheckSecretCodeSequences`) still contains a `jassica16` table at `1480:2ff0`, but the latch-enabling sequence in No Regret is the second table at `1480:2ffc`, decoded as `loosecannon`, which toggles `1480:0ac0` and mirrors the result into the F10 latch byte `1480:009b`.
|
||||
- Retail hidden-menu patching remains open, but the failed branches are now better separated from the current writable candidate. Verified file/fixup anchors are `0007:0d75` / `0007:0d79` (file `0x70d75` / relocation entry `0x71d68`) and `000c:99dd` / `000c:99e0` (file `0xc99dd`, seg126 chain `0x25e0`). The deferred `0x42f -> 000c:99dd -> 000b:9c0d` design remains explicitly rejected: it visibly entered the hidden UI path, but it halted with the retail `FILE\FLEX.C, line 83` failure and dropped into the quit line, so `0x42f` is the wrong deferred context even though the modal wrapper address itself was valid. The newer direct `0007:0d79 -> 000b:9a86` current-slot retarget with the narrowed `000b:9a8d` arg patch was also runtime-tested and produced no hidden menu, so the writable `/Writable/CRUSADER-PATCHED.EXE` test build is now moved to the next defensible variant instead: restore the direct hook to `000a:5276`, keep the current-slot wrapper unpatched, and retarget the later controller-side `000c:99e0` call to `000b:9c0d` while zeroing only the inherited modal-wrapper words at `000b:9c4a`.
|
||||
- The next retail test build is narrower still. User runtime feedback on the first `Ctrl+Q` patch is: the mouse pointer appears, then gameplay hangs with only a single right-edge pixel still updating. That makes the remaining failure look more like post-entry control-flow fallout than a bad entry address. The PowerShell patch therefore now also rewrites raw `000c:99e8` / live `13e8:25e8` from `PUSH 0x3e8` to a near jump into the shared epilogue at `13e8:29a7`, so the reused `13e8:25dd` deferred lane exits immediately after the retargeted `13e8:25e0 -> 13a0:020d` call instead of falling through into the original `0x42f` branch tail logic.
|
||||
- DOSBox-X debugger capture now shows the same hang surviving that tail-skip patch, and the stop point is materially informative: the live runtime state matches seg131 `Interpreter_NextUsecodeOp` at `1418:04c3..051d`, specifically just before the `1408:02f5` call at `1418:051d`. That means the blunt `Ctrl+Q -> 13a0:020d` patch is not merely stuck in the seg109 modal wrapper; it has activated the interpreter-side debugger-state path guarded by non-null `0x659c/0x659e`, and the freeze now looks like a bad or incomplete seg1408 debugger-state lifecycle rather than a simple wrong branch tail. Current best implication: stop iterating on the blunt modal force-open patch and pivot the next patch design toward constructing or safely emulating a real `usecode_debugger_break_state_create` object at `1408:0000` before relying on the seg109 UI lane.
|
||||
- The next executable patch still follows that pivot, but the boot-time callback rewrite is now explicitly retired. The current PowerShell build repurposes the gated `0x410` body at `13e8:230d` to lazily construct a seg1408 state object through the existing far-call slot at `13e8:2352 -> 1408:0000`, stores the returned far pointer into `0x659c/0x659e`, and then reuses the **second** existing far-call slot in that same body (`13e8:235c`) to jump directly to `usecode_debugger_open_for_current_unit` at `13a0:0086` with zeroed wrapper arguments. This keeps the patch hotkey-local instead of rewriting the shared seg1408 callback table at `1478:65ab`, while the older direct and deferred modal-force-open sites remain restored.
|
||||
- The callback-table design is now negative evidence rather than the live candidate. Even after fixing an NE-segment indexing mistake (`1478:65ab` had first been retargeted to segment `109` instead of `117` for `13a0:0086`), the global callback rewrite still caused startup failure. The surviving script fixes from that pass remain important: the large `13e8:230d` body must use on-disk `FF FF 00 00` placeholders rather than disassembly-resolved far operands, and its patched byte array must include the final trailing `0xC7` so patch/restore verification matches the retail executable length. With the global callback rewrite removed and the second local call slot retargeted instead, the script now round-trips cleanly on a fresh copied retail EXE (`apply -> patched`, `restore -> original`) and also cleans up the stale old `1478:65ab` callback retarget if that earlier crashing build had already been applied.
|
||||
- The direct hotkey-to-wrapper retarget is now negative evidence too. The local-call redesign fixed startup and let the game reach gameplay, but pressing `Ctrl+Q` immediately quit through the normal `"No pity. No mercy. No remorse."` shutdown line, which is more consistent with entering the modal UI while the original keypress is still live than with a boot-time relocation problem. The next patch therefore keeps the hotkey-local object creation but stops calling `13a0:0086` on the keypress itself.
|
||||
- The live candidate is now a per-object callback redirect. The `0x410` body at `13e8:230d` still creates/stores the seg1408 debugger-state object at `0x659c/0x659e`, but the second existing far-call slot in that body (`13e8:235c`) is now retargeted to `1408:0419` (`usecode_debugger_enable_single_step`) instead of directly opening the UI. The created object's first word is rewritten from the shared callback-table offset `0x65ab` to the private relocated slot `0x65af`, and the private dword at `1478:65af` is retargeted from `1408:0474` to `13a0:0086`. That should let the *next* interpreter-side debugger callback open the current-unit UI without inheriting the live `Ctrl+Q` key event, while the original shared `1478:65ab` slot stays restored to the retail no-op.
|
||||
- The PowerShell patcher now round-trips cleanly for this per-object callback design on a fresh copied retail EXE too: `13e8:230d` body patched/restored, `13e8:235c` step-arm call patched/restored, private callback slot `1478:65af` patched/restored, and legacy shared callback slot `1478:65ab` held at original in both states.
|
||||
- User runtime on that per-object single-step variant is now also informative negative evidence: the game boots and reaches gameplay, but pressing `Ctrl+Q` produces no visible effect at all, not even the original CD-transfer toast, which implies the hotkey body is being intercepted but the deferred break still is not surfacing. Current best read is that the single-step path at `+0x75` remains gated by the seg1418 nesting counters `+0x76/+0x78` often enough that the callback never fires in the observed test path.
|
||||
- The live patch candidate therefore now sets **break-next** mode directly instead of single-step mode. The repurposed `13e8:230d` body still constructs/stores the seg1408 debugger-state object and repoints that object to the private callback slot `1478:65af -> 13a0:0086`, but it now writes `+0x75 = 0` and `+0x74 = 1` in the object itself rather than retargeting the second `13e8:235c` call slot to `1408:0419`. That matches the surviving UI-side control path at `13a0:1e5d`, where `+0x74` is the unconditional break-on-next-callback mode while `+0x75` is the nesting-sensitive single-step mode.
|
||||
- The PowerShell patcher also round-trips cleanly for this break-next design on a fresh copied retail EXE: `13e8:230d` body patched/restored, private callback slot `1478:65af` patched/restored, shared callback slot `1478:65ab` held at original, and the stale second-call-slot cleanup removed from the write path because those bytes now belong to the patched body itself.
|
||||
- User runtime on that `1478:65af` break-next variant is now negative evidence as well: the game crashes on launch again, so even the supposedly private `65af` slot now looks too globally visible to repurpose. Current best implication is that the object-local `0x65af` first-word rewrite can stay as the arm point, but the actual callback entry must move off the live callback-table dword itself.
|
||||
- The live candidate is therefore now a **guarded trampoline** at the original seg1408 no-op callback code. The PowerShell patcher keeps the `13e8:230d` break-next object creation/store path, but restores the shared `1478:65ab` slot, stops repointing `1478:65af` to `13a0:0086`, patches `1408:0474` into a tiny guard that returns immediately unless `0x659c/0x659e` is armed, and uses the apparently unused relocated dword at `1478:6597` as the far target slot for `13a0:0086`. This newer `6597`/`1408:0474` build now also round-trips cleanly on a fresh copied retail EXE: `13e8:230d` body patched/restored, guarded callback code at file `0xCEE6F` patched/restored, wrapper target slot at file `0xEA197` patched/restored, and all older direct/deferred experiment sites held at original bytes.
|
||||
- The root-cause read on the `65af` startup failures is now sharper: `0x65af` is not an alternate vtable base at all. The constructor at `1408:0000` writes `0x65ab` to object offset `+0`, and the dispatch sites prove that object `CALLF [BX]` uses the dword at `65ab -> 1408:046f` while object `CALLF [BX+4]` uses the next dword at `65af -> 1408:0474`. Rewriting the object first word to `0x65af` therefore corrupts the second method lookup (`[BX+4]`) instead of selecting a “private callback table”, which explains the launch-time instability and the other inconsistent runtime fallout from the earlier single-step and break-next builds.
|
||||
- The live candidate is now the narrower **method-0 deferred callback** design. The PowerShell patcher still keeps the `13e8:230d` lazy object creation/store path and still arms break-next mode by writing `+0x75 = 0` / `+0x74 = 1`, but it explicitly preserves the object's vtable base as `0x65ab`, restores the method-1 helper at `1408:0474`, patches only the method-0 break callback at `1408:046f` to indirect through the spare relocated dword `1478:6597`, and uses that slot as the far target for `13a0:0086`. This corrected `046f`/`6597` build also round-trips cleanly on a fresh copied retail EXE: `13e8:230d` body patched/restored, break callback code at file `0xCEE6F` patched/restored, deferred target slot at file `0xEA197` patched/restored, and all older direct/deferred experiment sites held at original bytes.
|
||||
- User runtime on that shared-`046f` method-0 build is now negative evidence too: startup still crashes, which makes the shared method body just as globally sensitive as the shared `65ab/65af` vtable slots. Current implication: the deferred path still looks right, but the callback implementation must move to a truly private per-object table instead of any shared seg1408 body or shared vtable dword.
|
||||
- The live candidate is now a **private two-entry vtable** built from unused relocated dwords with no current data uses. The PowerShell patcher still keeps the `13e8:230d` lazy object creation/store path and still arms break-next mode with `+0x75 = 0` / `+0x74 = 1`, but it now rewrites the created object's vtable base to `0x658f`, retargets private method 0 `1478:658f -> 13a0:0086`, retargets private method 1 `1478:6593 -> 1408:0474`, and leaves the shared callback bodies and shared `65ab/65af` table entries untouched. This private-vtable build also round-trips cleanly on a fresh copied retail EXE: `13e8:230d` body patched/restored, private method 0 slot at file `0xEA18F` patched/restored, private method 1 slot at file `0xEA193` patched/restored, and all older direct/deferred experiment sites held at original bytes.
|
||||
- User runtime on that first private-vtable placement is now negative evidence too: startup still crashes, which proves the `658f/6593` pair is also startup-visible despite the lack of direct data-use hits. Current best implication is that the private-vtable strategy itself still looks structurally right, but the specific dword pair must move farther away from the debugger-global cluster and any hidden boot-time consumers.
|
||||
- The full six-candidate private-vtable harness is now retired. User runtime results:
|
||||
- Candidate A (`1478:6724/6728`) = DOSBox closes on start
|
||||
- Candidate B (`1478:672c/6730`) = fatal `Load program failed -- error code 201 -- C:\CRUSADER.EXE`
|
||||
- Candidate C (`1478:6734/6738`) = DOSBox closes on start
|
||||
- Candidate D (`1478:6718/671c`) = startup crash
|
||||
- Candidate E (`1478:6720/6724`) = startup crash
|
||||
- Candidate F (`1478:6738/673c`) = startup crash
|
||||
Ghidra follow-up now explains why: `1478:6718..673c` is a live function-pointer table containing `UsecodeProcess_*`, `Process_Terminate`, `Process_Fail`, and nearby null handlers, not spare relocated dwords. The script no longer offers those candidates.
|
||||
- The first guarded shared-callback pair is now negative evidence too. Candidates G/H still crashed on startup, and the best new explanation is structural: that design overwrote both `1408:046f` and the adjacent `1408:0474`, but `0474` is a real helper that returns `DX:AX = 0`, not dead padding. Destroying that zero-return behavior may itself be enough to destabilize startup.
|
||||
- The method-0-only shared-callback pair is now negative evidence too. User runtime on Patch 1 / Patch 2 showed both startup-crashing, which means preserving `1408:0474` was necessary but not sufficient: the shared `1408:046f` body is still too broad if it jumps straight into debugger UI code.
|
||||
- The live patch family is now an **interpreter callsite retarget** design. Candidate M/N are retired negative evidence: both startup-crashed, the embedded private stub inside the patched `13e8:230d` body was malformed, and the supposed deferred target slot at `1478:6597` is no longer treated as spare storage. The current PowerShell build still keeps the retail debugger object's real `1478:65ab` vtable base and still arms break-next through the patched `13e8:230d` body, but it now avoids both the shared seg1408 callback bodies and the `1478:6597` data slot entirely. Instead, it patches the existing interpreter `CALLF usecode_debugger_maybe_break_on_current_line` at `1418:04b5` to a corrected private stub at `13e8:232d`, and it also reuses the second retail far-call slot inside `13e8:230d` (`13e8:235c`) as the actual private UI-call target. The `13e8:230d` body itself now correctly handles both cases: reuse and arm an existing debugger-state object at `0x659c/659e`, or lazily create/store one before arming break-next. One implementation bug from the first O/P refactor is now fixed too: the second `13e8:235c` relocation write is part of candidate application and verification, so the live build now really routes to the selected wrapper instead of accidentally leaving that slot on retail `Dispatch_ModalGump`. Current candidates:
|
||||
- Candidate O = interpreter callsite retarget -> `13a0:020d`, with `13a0:024a` zeroed inherited modal-wrapper words
|
||||
- Candidate P = interpreter callsite retarget -> `13a0:0086`, with `13a0:008f` zeroed inherited current-unit-wrapper words
|
||||
Both apply/restore cleanly on a disposable retail copy and are the next runtime tests.
|
||||
- Full chronology for this patch line now lives in `docs/retail-debugger-patch-attempts.md`, including the failed global callback rewrite, direct wrapper call, single-step `65af` build, break-next `65af` build, guarded `0474` trampoline, shared `046f` method patch, and the current private-vtable candidate.
|
||||
- The hidden-menu orphan model is now materially stronger too. New live renames in seg1408 (`usecode_debugger_break_state_create`, `usecode_debugger_maybe_break_on_current_line`, `usecode_debugger_breakpoint_insert_sorted`, `usecode_debugger_has_breakpoint`, `usecode_debugger_callstack_push_entry`, `usecode_debugger_callstack_pop_entry`, `usecode_debugger_enable_single_step`, `usecode_debugger_clear_step_state`, `usecode_debugger_current_entry_get_unit_name`) line up with the seg109 UI in a way the cheat-only hook never did. The concrete interpreter-side handoff at `1418:04aa..04b5` now calls `usecode_debugger_maybe_break_on_current_line` whenever the far pointer at `0x659c/0x659e` is non-null, and that helper checks `(file,line)` breakpoints before callbacking through the debugger-state object's vtable. Current best read is therefore that the retail orphan happened one layer earlier than the cheat/event experiments: the seg109 current-unit debugger UI likely used to be entered from this seg1408 breakpoint object, but retail no longer appears to instantiate/store that object at `0x659c/0x659e`. That makes the breakpoint callback lane a stronger original-entry candidate than direct event `0x103` retargeting.
|
||||
- The live NE `CRUSADER.EXE` mapping for that hidden-menu lane is now explicit and comment-backed in Ghidra too: direct hook `1130:2b75/2b78`, current-slot wrapper `13a0:0086` with constructor arg site `13a0:008d`, modal wrapper `13a0:020d` with inherited-arg patch subsite `13a0:024a`, listener create/dispatch `13a0:19b1` / `13a0:1df3`, compiled `0x410` CD-transfer-display body `13e8:2303`, deferred controller-side hook `13e8:25dd/25e0`, and the supporting cheat-state data cells at `1020:2833`, `1020:283d`, `1020:0844`, `1020:6045`, `1020:604f`, and `1020:6050`. The `0x410` body is still documented in place rather than renamed because it remains embedded inside the oversized `World_HandleKeyboardInput_13e8_14b4` function object. This improved live handoff and patch reproducibility still does not justify a headline estimate change by itself.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue