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:
Marco 2026-03-25 17:36:16 +01:00
commit 7310c4fe96
13 changed files with 1008 additions and 1959 deletions

View file

@ -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
}
$stateChanged = $true
}
$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
}
if ($wrapperCurrentState -eq 'Original') {
$stateChanged = (Save-OriginalBytesIfMissing -StateData $stateData -SiteKey 'Wrapper' -SiteOffset $sites.Wrapper.Offset -Bytes $wrapperCurrent) -or $stateChanged
$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 ($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 {
$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.'
}
}
}
$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
$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)) {