Crusader_Decomp/patch_crusader_cheat_menu.ps1
Marco 7310c4fe96 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.
2026-03-25 17:36:16 +01:00

1241 lines
No EOL
56 KiB
PowerShell

param(
[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 = $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 = '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)
LegacyBadPatched = [byte[]](0x9A, 0x86, 0x9A, 0x0B, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90)
OriginalPatterns = @(
@('68', '03', '01', '9A', 'FF', 'FF', '00', '00', '83', 'C4', '02')
)
Fixup = @{
OperandOffset = 0x70D79
OriginalTargetSeg = 92
OriginalTargetOffset = 0x0476
PatchedTargetSeg = 117
PatchedTargetOffset = 0x0086
RetailSourceSegmentIndex = 39
RetailChainOffset = 0x2B79
RetailEntryOffset = 0x71D68
}
}
Wrapper = @{
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)
OriginalPatterns = @(
@('6A', '01', 'FF', '76', '08', 'FF', '76', '06')
)
}
LegacyDeferredHook = @{
Label = 'Rejected deferred-event hook site'
Offset = 0xC99DD
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)
OriginalPatterns = @(
@('68', '03', '01', '9A', 'FF', 'FF', '00', '00', '83', 'C4', '02')
)
Fixup = @{
OperandOffset = 0xC99E1
OriginalTargetSeg = 92
OriginalTargetOffset = 0x0476
PatchedTargetSeg = 117
PatchedTargetOffset = 0x020D
}
}
LegacyDeferredWrapper = @{
Label = 'Rejected modal wrapper arg site'
Offset = 0xB9C48
Original = [byte[]](0x6A, 0x00, 0xFF, 0x76, 0x08, 0xFF, 0x76, 0x06)
Patched = [byte[]](0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0x90, 0x90)
OriginalPatterns = @(
@('6A', '00', 'FF', '76', '08', 'FF', '76', '06')
)
}
}
$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)
return (($Bytes | ForEach-Object { $_.ToString('X2') }) -join ' ')
}
function Get-ByteSlice {
param(
[byte[]]$Bytes,
[int]$Offset,
[int]$Count
)
if ($Offset -lt 0 -or ($Offset + $Count) -gt $Bytes.Length) {
throw "Requested byte range 0x{0:X}-0x{1:X} is outside the file." -f $Offset, ($Offset + $Count - 1)
}
$slice = New-Object byte[] $Count
[Array]::Copy($Bytes, $Offset, $slice, 0, $Count)
return $slice
}
function Get-U16Le {
param(
[byte[]]$Bytes,
[int]$Offset
)
return [BitConverter]::ToUInt16($Bytes, $Offset)
}
function Get-U32Le {
param(
[byte[]]$Bytes,
[int]$Offset
)
return [BitConverter]::ToUInt32($Bytes, $Offset)
}
function Set-U16Le {
param(
[byte[]]$Bytes,
[int]$Offset,
[int]$Value
)
$raw = [BitConverter]::GetBytes([UInt16]$Value)
[Array]::Copy($raw, 0, $Bytes, $Offset, 2)
}
function Test-ByteArrayEqual {
param(
[byte[]]$Left,
[byte[]]$Right
)
if ($Left.Length -ne $Right.Length) {
return $false
}
for ($i = 0; $i -lt $Left.Length; $i++) {
if ($Left[$i] -ne $Right[$i]) {
return $false
}
}
return $true
}
function Test-BytePatternMatch {
param(
[byte[]]$Bytes,
[object[]]$Pattern
)
if ($Bytes.Length -ne $Pattern.Length) {
return $false
}
for ($i = 0; $i -lt $Bytes.Length; $i++) {
$expected = [string]$Pattern[$i]
if ($expected -eq '??') {
continue
}
if ($Bytes[$i] -ne [Convert]::ToByte($expected, 16)) {
return $false
}
}
return $true
}
function Convert-BytesToStoredHex {
param([byte[]]$Bytes)
return (($Bytes | ForEach-Object { $_.ToString('X2') }) -join '')
}
function Convert-StoredHexToBytes {
param([string]$Hex)
if ([string]::IsNullOrWhiteSpace($Hex)) {
return $null
}
$normalized = $Hex.Replace(' ', '')
if (($normalized.Length % 2) -ne 0) {
throw "Invalid stored hex string length: $Hex"
}
$result = New-Object byte[] ($normalized.Length / 2)
for ($i = 0; $i -lt $result.Length; $i++) {
$result[$i] = [Convert]::ToByte($normalized.Substring($i * 2, 2), 16)
}
return $result
}
function Get-HookFixupInfo {
param(
[byte[]]$FileBytes,
[hashtable]$Site
)
$neHeaderOffset = [int](Get-U32Le -Bytes $FileBytes -Offset 0x3C)
$neSignature = Get-ByteSlice -Bytes $FileBytes -Offset $neHeaderOffset -Count 2
if ($neSignature[0] -ne 0x4E -or $neSignature[1] -ne 0x45) {
throw 'Executable does not contain a valid NE header at e_lfanew.'
}
$numSegments = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x1C))
$segmentTableOffset = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x22)) + $neHeaderOffset
$alignmentShift = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x32))
$operandFileOffset = [int]$Site.Fixup.OperandOffset
$segmentInfoByIndex = @{}
$sourceSegment = $null
for ($index = 1; $index -le $numSegments; $index++) {
$segmentEntryOffset = $segmentTableOffset + (($index - 1) * 8)
$sectorOffset = [int](Get-U16Le -Bytes $FileBytes -Offset $segmentEntryOffset)
if ($sectorOffset -eq 0) {
continue
}
$segmentLength = [int](Get-U16Le -Bytes $FileBytes -Offset ($segmentEntryOffset + 2))
if ($segmentLength -eq 0) {
$segmentLength = 0x10000
}
$segmentFlags = [int](Get-U16Le -Bytes $FileBytes -Offset ($segmentEntryOffset + 4))
$segmentFileOffset = [int]($sectorOffset -shl $alignmentShift)
$segmentInfo = @{
SegmentIndex = $index
SegmentEntryOffset = $segmentEntryOffset
SegmentFileOffset = $segmentFileOffset
SegmentLength = $segmentLength
SegmentFlags = $segmentFlags
HasReloc = (($segmentFlags -band 0x0100) -ne 0)
}
$segmentInfoByIndex[$index] = $segmentInfo
if ($segmentFileOffset -le $operandFileOffset -and $operandFileOffset -lt ($segmentFileOffset + $segmentLength)) {
$sourceSegment = $segmentInfo
break
}
}
if ($null -eq $sourceSegment) {
$fallbackSegmentIndex = [int]$Site.Fixup.RetailSourceSegmentIndex
$fallbackChainOffset = [int]$Site.Fixup.RetailChainOffset
if ($segmentInfoByIndex.ContainsKey($fallbackSegmentIndex)) {
$fallbackSegment = $segmentInfoByIndex[$fallbackSegmentIndex]
if (($fallbackSegment.SegmentFileOffset + $fallbackChainOffset) -eq $operandFileOffset) {
$sourceSegment = $fallbackSegment
}
}
}
if ($null -eq $sourceSegment) {
throw ("Could not locate the hook operand file offset 0x{0:X} in any NE code segment." -f $operandFileOffset)
}
if (-not $sourceSegment.HasReloc) {
throw 'Hook source segment does not advertise a relocation table.'
}
$segmentIndex = [int]$sourceSegment.SegmentIndex
$segmentFileOffset = [int]$sourceSegment.SegmentFileOffset
$segmentLength = [int]$sourceSegment.SegmentLength
$relocTableOffset = $segmentFileOffset + $segmentLength
$relocCount = [int](Get-U16Le -Bytes $FileBytes -Offset $relocTableOffset)
$entryOffset = $relocTableOffset + 2
$chainOffset = $operandFileOffset - $segmentFileOffset
for ($index = 0; $index -lt $relocCount; $index++) {
$addrType = $FileBytes[$entryOffset]
$relType = $FileBytes[$entryOffset + 1]
$entryChainOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 2)
if ($entryChainOffset -eq $chainOffset) {
return @{
SegmentIndex = $segmentIndex
SegmentFileOffset = $segmentFileOffset
RelocTableOffset = $relocTableOffset
EntryOffset = $entryOffset
EntryIndex = $index
AddrType = $addrType
RelType = $relType
ChainOffset = $entryChainOffset
TargetSeg = $FileBytes[$entryOffset + 4]
Reserved = $FileBytes[$entryOffset + 5]
TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 6)
}
}
if ($addrType -eq 3 -and (($relType -band 0x03) -eq 0)) {
$visited = New-Object 'System.Collections.Generic.HashSet[int]'
$currentChainOffset = $entryChainOffset
while ($currentChainOffset -ne 0xFFFF -and $currentChainOffset -lt $segmentLength) {
if (-not $visited.Add($currentChainOffset)) {
break
}
if ($currentChainOffset -eq $chainOffset) {
return @{
SegmentIndex = $segmentIndex
SegmentFileOffset = $segmentFileOffset
RelocTableOffset = $relocTableOffset
EntryOffset = $entryOffset
EntryIndex = $index
AddrType = $addrType
RelType = $relType
ChainOffset = $entryChainOffset
TargetSeg = $FileBytes[$entryOffset + 4]
Reserved = $FileBytes[$entryOffset + 5]
TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 6)
}
}
$currentChainOffset = Get-U16Le -Bytes $FileBytes -Offset ($segmentFileOffset + $currentChainOffset)
}
}
$entryOffset += 8
}
$fallbackEntryOffset = [int]$Site.Fixup.RetailEntryOffset
$fallbackChainOffset = [int]$Site.Fixup.RetailChainOffset
if ($fallbackEntryOffset -ge 0 -and ($fallbackEntryOffset + 7) -lt $FileBytes.Length) {
$addrType = $FileBytes[$fallbackEntryOffset]
$relType = $FileBytes[$fallbackEntryOffset + 1]
$entryChainOffset = [int](Get-U16Le -Bytes $FileBytes -Offset ($fallbackEntryOffset + 2))
if (
$sourceSegment.SegmentIndex -eq [int]$Site.Fixup.RetailSourceSegmentIndex -and
$chainOffset -eq $fallbackChainOffset -and
$addrType -eq 3 -and
(($relType -band 0x03) -eq 0) -and
$entryChainOffset -eq $fallbackChainOffset
) {
return @{
SegmentIndex = $segmentIndex
SegmentFileOffset = $segmentFileOffset
RelocTableOffset = $relocTableOffset
EntryOffset = $fallbackEntryOffset
EntryIndex = -1
AddrType = $addrType
RelType = $relType
ChainOffset = $entryChainOffset
TargetSeg = $FileBytes[$fallbackEntryOffset + 4]
Reserved = $FileBytes[$fallbackEntryOffset + 5]
TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($fallbackEntryOffset + 6)
}
}
}
throw ("Could not find hook relocation entry for segment {0} chain offset 0x{1:X}." -f $segmentIndex, $chainOffset)
}
function Get-StateData {
if (-not (Test-Path -LiteralPath $statePath)) {
return @{}
}
$raw = Get-Content -LiteralPath $statePath -Raw
if ([string]::IsNullOrWhiteSpace($raw)) {
return @{}
}
$parsed = ConvertFrom-Json -InputObject $raw -AsHashtable
if ($null -eq $parsed) {
return @{}
}
return $parsed
}
function Save-StateData {
param([hashtable]$StateData)
$json = ConvertTo-Json -InputObject $StateData -Depth 4
Set-Content -LiteralPath $statePath -Value $json -Encoding ASCII
}
function Get-SavedOriginalBytes {
param(
[hashtable]$StateData,
[string]$SiteKey,
[int]$SiteOffset
)
if (-not $StateData.ContainsKey($SiteKey)) {
return $null
}
$siteState = $StateData[$SiteKey]
if ($siteState -isnot [hashtable]) {
return $null
}
if (-not $siteState.ContainsKey('OriginalBytes')) {
return $null
}
if ($siteState.ContainsKey('Offset') -and ([int]$siteState.Offset -ne $SiteOffset)) {
return $null
}
return Convert-StoredHexToBytes -Hex ([string]$siteState.OriginalBytes)
}
function Save-OriginalBytesIfMissing {
param(
[hashtable]$StateData,
[string]$SiteKey,
[int]$SiteOffset,
[byte[]]$Bytes
)
if ($StateData.ContainsKey($SiteKey) -and ($StateData[$SiteKey] -is [hashtable])) {
$existing = $StateData[$SiteKey]
if ($existing.ContainsKey('OriginalBytes') -and $existing.ContainsKey('Offset') -and ([int]$existing.Offset -eq $SiteOffset)) {
return $false
}
}
$StateData[$SiteKey] = @{
Offset = $SiteOffset
OriginalBytes = Convert-BytesToStoredHex -Bytes $Bytes
}
return $true
}
function Get-SiteState {
param(
[byte[]]$FileBytes,
[hashtable]$Site
)
$current = Get-ByteSlice -Bytes $FileBytes -Offset $Site.Offset -Count $Site.Original.Length
if ($Site.ContainsKey('Fixup')) {
$fixupInfo = Get-HookFixupInfo -FileBytes $FileBytes -Site $Site
$isFixupOriginal = ($fixupInfo.TargetSeg -eq $Site.Fixup.OriginalTargetSeg) -and ($fixupInfo.TargetOffset -eq $Site.Fixup.OriginalTargetOffset)
$isFixupPatched = ($fixupInfo.TargetSeg -eq $Site.Fixup.PatchedTargetSeg) -and ($fixupInfo.TargetOffset -eq $Site.Fixup.PatchedTargetOffset)
if ($Site.ContainsKey('LegacyBadPatched') -and (Test-ByteArrayEqual -Left $current -Right $Site.LegacyBadPatched) -and $isFixupOriginal) {
return 'LegacyBadPatch'
}
if ((Test-ByteArrayEqual -Left $current -Right $Site.Patched) -and $isFixupPatched) {
return 'Patched'
}
if ((Test-ByteArrayEqual -Left $current -Right $Site.Original) -and $isFixupOriginal) {
return 'Original'
}
foreach ($pattern in $Site.OriginalPatterns) {
if ((Test-BytePatternMatch -Bytes $current -Pattern $pattern) -and $isFixupOriginal) {
return 'Original'
}
}
$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'
}
if (Test-ByteArrayEqual -Left $current -Right $Site.Patched) {
return 'Patched'
}
if (Test-ByteArrayEqual -Left $current -Right $Site.Original) {
return 'Original'
}
foreach ($pattern in $Site.OriginalPatterns) {
if (Test-BytePatternMatch -Bytes $current -Pattern $pattern) {
return 'Original'
}
}
return 'Unknown'
}
function Assert-SiteStateKnown {
param(
[byte[]]$FileBytes,
[hashtable]$Site
)
$state = Get-SiteState -FileBytes $FileBytes -Site $Site
if ($state -eq 'Unknown') {
$current = Get-ByteSlice -Bytes $FileBytes -Offset $Site.Offset -Count $Site.Original.Length
throw (
"{0} at file offset 0x{1:X} does not match either expected byte sequence.`nCurrent : {2}`nOriginal: {3}`nPatched : {4}`n`nRefusing to modify an unknown executable state." -f
$Site.Label,
$Site.Offset,
(Format-HexBytes -Bytes $current),
(Format-HexBytes -Bytes $Site.Original),
(Format-HexBytes -Bytes $Site.Patched)
)
}
}
function Set-ByteSlice {
param(
[byte[]]$Bytes,
[int]$Offset,
[byte[]]$Value
)
[Array]::Copy($Value, 0, $Bytes, $Offset, $Value.Length)
}
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 ("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 ''
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 ''
}
function Set-DesiredState {
param(
[bool]$CtrlQPatched,
[string]$Label,
[string]$ProfileKey
)
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
$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
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
$legacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.LegacyDeferredHook
$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
}
$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
}
$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 = 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
Set-U16Le -Bytes $fileBytes -Offset ($hookFixupInfo.EntryOffset + 6) -Value $hookTargetOffset
Set-ByteSlice -Bytes $fileBytes -Offset $sites.Wrapper.Offset -Value $wrapperBytes
Set-ByteSlice -Bytes $fileBytes -Offset $sites.LegacyDeferredHook.Offset -Value $legacyDeferredHookBytes
$fileBytes[$legacyDeferredHookFixupInfo.EntryOffset + 4] = [byte]$legacyDeferredHookTargetSeg
$fileBytes[$legacyDeferredHookFixupInfo.EntryOffset + 5] = [byte]$legacyDeferredHookReserved
Set-U16Le -Bytes $fileBytes -Offset ($legacyDeferredHookFixupInfo.EntryOffset + 6) -Value $legacyDeferredHookTargetOffset
Set-ByteSlice -Bytes $fileBytes -Offset $sites.LegacyDeferredWrapper.Offset -Value $legacyDeferredWrapperBytes
[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.'
}
if ($verifiedHookFixupInfo.TargetSeg -ne $hookTargetSeg -or $verifiedHookFixupInfo.TargetOffset -ne $hookTargetOffset) {
throw 'Hook relocation verification failed after write.'
}
if (-not (Test-ByteArrayEqual -Left $verifiedWrapperBytes -Right $wrapperBytes)) {
throw 'Wrapper-site verification failed after write.'
}
if (-not (Test-ByteArrayEqual -Left $verifiedLegacyDeferredHookBytes -Right $legacyDeferredHookBytes)) {
throw 'Rejected deferred-event hook cleanup verification failed after write.'
}
if ($verifiedLegacyDeferredHookFixupInfo.TargetSeg -ne $legacyDeferredHookTargetSeg -or $verifiedLegacyDeferredHookFixupInfo.TargetOffset -ne $legacyDeferredHookTargetOffset) {
throw 'Rejected deferred-event relocation cleanup verification failed after write.'
}
if (-not (Test-ByteArrayEqual -Left $verifiedLegacyDeferredWrapperBytes -Right $legacyDeferredWrapperBytes)) {
throw 'Rejected modal-wrapper cleanup verification failed after write.'
}
Write-Host ''
Write-Host ("Applied: {0}" -f $Label)
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 '- 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().ToLowerInvariant()) {
'1' {
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
}
'2' {
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 -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.'
}
}
return $true
}
if (-not (Test-Path -LiteralPath $exePath)) {
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')) {
[void](Invoke-MenuChoice -SelectedChoice $Choice)
return
}
:mainloop while ($true) {
$currentBytes = [System.IO.File]::ReadAllBytes($exePath)
Show-Status -FileBytes $currentBytes
$choice = Read-Host 'Select 1-4'
if ([string]::IsNullOrEmpty($choice)) { break mainloop }
if (-not (Invoke-MenuChoice -SelectedChoice $choice)) {
break mainloop
}
}