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 } }