Add detailed log for retail debugger patch attempts in CRUSADER.EXE
This commit introduces a comprehensive document outlining the various executable-patching attempts aimed at revealing the hidden retail usecode debugger within the CRUSADER.EXE file. The document serves multiple purposes, including preserving negative evidence, recording patch shapes and their rationales, and ensuring that runtime outcomes are linked to specific patch generations. Key sections include: - Ground rules for patching and validation processes. - A table of stable facts regarding the debugger's structure and behavior. - A detailed attempt log documenting each patch's shape, mechanical and runtime results, and verdicts. - Root-cause findings from failed paths, providing insights into the challenges faced during the patching process. - Current live candidates for further testing and exploration. This documentation is intended to streamline future patching efforts and improve the understanding of the underlying mechanics of the debugger.
This commit is contained in:
parent
ded6db3adc
commit
7310c4fe96
13 changed files with 1008 additions and 1959 deletions
|
|
@ -1,17 +1,211 @@
|
|||
param(
|
||||
[ValidateSet('1', '2', '3', '4')]
|
||||
[string]$Choice
|
||||
[ValidateSet('1', '2', '3', '4', 'candidate-i', 'candidate-j', 'candidate-m', 'candidate-n', 'candidate-o', 'candidate-p', 'restore', 'exit')]
|
||||
[string]$Choice,
|
||||
|
||||
[string]$ExePath = $(
|
||||
if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'CRUSADER.EXE')) {
|
||||
Join-Path $PSScriptRoot 'CRUSADER.EXE'
|
||||
}
|
||||
elseif (Test-Path -LiteralPath 'F:\Apps\Crusader No Remorse\CRUSADER.EXE') {
|
||||
'F:\Apps\Crusader No Remorse\CRUSADER.EXE'
|
||||
}
|
||||
else {
|
||||
Join-Path $PSScriptRoot 'CRUSADER.EXE'
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$exePath = Join-Path $PSScriptRoot 'CRUSADER.EXE'
|
||||
$exePath = $ExePath
|
||||
$statePath = Join-Path $PSScriptRoot 'patch_crusader_cheat_menu.state.json'
|
||||
|
||||
$sites = @{
|
||||
CtrlQDebuggerInit = @{
|
||||
Label = 'Ctrl+Q debugger-state init body'
|
||||
Offset = 0xC970D
|
||||
Original = [byte[]](
|
||||
0xA0, 0x4F, 0x60, 0xB4, 0x00, 0xF7, 0xD8, 0x1B, 0xC0, 0x40, 0xA2, 0x4F, 0x60, 0x80, 0x3E, 0x4F,
|
||||
0x60, 0x00, 0x74, 0x47, 0x6A, 0xFF, 0x6A, 0xFF, 0xC4, 0x1E, 0xD0, 0x4C, 0x26, 0x8A, 0x47, 0x05,
|
||||
0x50, 0x1E, 0x68, 0xD2, 0x60, 0x6A, 0x00, 0x6A, 0x00, 0x83, 0xEC, 0x06, 0xC7, 0x86, 0x76, 0xFF,
|
||||
0x00, 0x00, 0x8B, 0x86, 0x76, 0xFF, 0xF7, 0xD0, 0x89, 0x86, 0x78, 0xFF, 0xC6, 0x86, 0x7A, 0xFF,
|
||||
0x00, 0x6A, 0x00, 0x6A, 0x00, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x14, 0x52, 0x50, 0x9A,
|
||||
0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x08, 0x5F, 0x5E, 0xC9, 0xCB, 0x6A, 0xFF, 0x6A, 0xFF, 0xC4,
|
||||
0x1E, 0xD0, 0x4C, 0x26, 0x8A, 0x47, 0x05, 0x50, 0x1E, 0x68, 0xEE, 0x60, 0x6A, 0x00, 0x6A, 0x00,
|
||||
0x83, 0xEC, 0x06, 0xC7
|
||||
)
|
||||
Patched = [byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x74, 0x10, 0xC4, 0x1E, 0x9C, 0x65, 0xC6, 0x47, 0x75,
|
||||
0x00, 0xC6, 0x47, 0x74, 0x01, 0x5F, 0x5E, 0xC9, 0xCB, 0x6A, 0x00, 0x6A, 0x00, 0xE9, 0x25, 0x00,
|
||||
0x55, 0x8B, 0xEC, 0xA1, 0x9C, 0x65, 0x39, 0x46, 0x06, 0x75, 0x16, 0xA1, 0x9E, 0x65, 0x39, 0x46,
|
||||
0x08, 0x75, 0x0E, 0xC4, 0x5E, 0x06, 0xC7, 0x47, 0x74, 0x00, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0xEB,
|
||||
0x0E, 0x5D, 0xCB, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0xEB, 0x0A, 0x9A,
|
||||
0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x5D, 0xCB, 0x0B, 0xC2, 0x74, 0x13, 0xA3, 0x9C, 0x65,
|
||||
0x89, 0x16, 0x9E, 0x65, 0x89, 0xC3, 0x8E, 0xC2, 0xC6, 0x47, 0x75, 0x00, 0xC6, 0x47, 0x74, 0x01,
|
||||
0x5F, 0x5E, 0xC9, 0xCB
|
||||
)
|
||||
LegacyPatched = [byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x52, 0x31, 0xC0, 0x50, 0x50, 0xEB, 0x36,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x0B, 0xC2, 0x75,
|
||||
0x03, 0xE9, 0x46, 0x06, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E, 0x65, 0xC4, 0x1E, 0x9C, 0x65, 0xC6,
|
||||
0x47, 0x75, 0x01, 0xC6, 0x47, 0x74, 0x00, 0xC7, 0x47, 0x76, 0x00, 0x00, 0xC7, 0x47, 0x78, 0x00,
|
||||
0x00, 0xE9, 0x26, 0x06, 0xC7
|
||||
)
|
||||
LegacyPatchedVariants = @(
|
||||
[byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x5E, 0x31, 0xC0, 0x50, 0x50, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04,
|
||||
0xEB, 0x0D, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x5F, 0x5E, 0xC9, 0xCB, 0x0B, 0xC2,
|
||||
0x74, 0xF8, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E, 0x65, 0x6A, 0x00, 0x6A, 0x00, 0xEB, 0xE2, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xC7
|
||||
),
|
||||
[byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x5E, 0x31, 0xC0, 0x50, 0x50, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0xEB, 0x0D, 0x9A,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x04, 0x5F, 0x5E, 0xC9, 0xCB, 0x0B, 0xC2, 0x74, 0xF7, 0xA3,
|
||||
0x9C, 0x65, 0x89, 0x16, 0x9E, 0x65, 0x6A, 0x00, 0x6A, 0x00, 0xEB, 0xE2, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0xEC, 0x06, 0xC7
|
||||
),
|
||||
[byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x55, 0x31, 0xC0, 0x50, 0x50, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04,
|
||||
0x52, 0x50, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E,
|
||||
0x65, 0xC4, 0x1E, 0x9C, 0x65, 0xC7, 0x07, 0xAF, 0x65, 0xC6, 0x47, 0x75, 0x01, 0xC6, 0x47, 0x74,
|
||||
0x00, 0x5F, 0x5E, 0xC9, 0xCB, 0x90, 0x90
|
||||
)
|
||||
)
|
||||
LegacyPatchedFixupState = 'Any'
|
||||
OriginalPatterns = @(
|
||||
@('A0', '4F', '60', 'B4', '00', 'F7', 'D8', '1B', 'C0', '40', 'A2', '4F', '60', '80', '3E', '4F', '60', '00', '74', '47')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xC9753
|
||||
OriginalTargetSeg = 107
|
||||
OriginalTargetOffset = 0x0046
|
||||
PatchedTargetSeg = 130
|
||||
PatchedTargetOffset = 0x0000
|
||||
}
|
||||
}
|
||||
DebuggerCallback = @{
|
||||
Label = 'legacy secondary call cleanup'
|
||||
Offset = 0xC975D
|
||||
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xC975D
|
||||
OriginalTargetSeg = 101
|
||||
OriginalTargetOffset = 0x1588
|
||||
PatchedTargetSeg = 101
|
||||
PatchedTargetOffset = 0x1588
|
||||
}
|
||||
}
|
||||
PrivateBreakpointMethod0 = @{
|
||||
Label = 'private break callback slot'
|
||||
Offset = 0xEA328
|
||||
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xEA328
|
||||
OriginalTargetSeg = 133
|
||||
OriginalTargetOffset = 0x1162
|
||||
PatchedTargetSeg = 117
|
||||
PatchedTargetOffset = 0x0086
|
||||
}
|
||||
}
|
||||
PrivateBreakpointMethod1 = @{
|
||||
Label = 'private helper callback slot'
|
||||
Offset = 0xEA32C
|
||||
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xEA32C
|
||||
OriginalTargetSeg = 133
|
||||
OriginalTargetOffset = 0x1278
|
||||
PatchedTargetSeg = 130
|
||||
PatchedTargetOffset = 0x0474
|
||||
}
|
||||
}
|
||||
LegacyBreakpointCallback = @{
|
||||
Label = 'legacy seg1408 break callback target'
|
||||
Offset = 0xEA1AB
|
||||
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xEA1AB
|
||||
OriginalTargetSeg = 130
|
||||
OriginalTargetOffset = 0x046F
|
||||
PatchedTargetSeg = 117
|
||||
PatchedTargetOffset = 0x0086
|
||||
}
|
||||
}
|
||||
CallbackGuardCode = @{
|
||||
Label = 'seg1408 break-next dispatch patch'
|
||||
Offset = 0xCEAD3
|
||||
Original = [byte[]](0x26, 0x8B, 0x1F, 0xFF, 0x1F)
|
||||
Patched = [byte[]](0xFF, 0x1E, 0x97, 0x65, 0x90)
|
||||
OriginalPatterns = @(
|
||||
@('26', '8B', '1F', 'FF', '1F')
|
||||
)
|
||||
}
|
||||
CallbackTargetSlot = @{
|
||||
Label = 'break-next private stub target slot'
|
||||
Offset = 0xEA197
|
||||
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xEA197
|
||||
OriginalTargetSeg = 5
|
||||
OriginalTargetOffset = 0x08D2
|
||||
PatchedTargetSeg = 131
|
||||
PatchedTargetOffset = 0x2318
|
||||
}
|
||||
}
|
||||
InterpreterBreakCall = @{
|
||||
Label = 'interpreter debugger break callsite'
|
||||
Offset = 0xCFAB5
|
||||
Original = [byte[]](0x9A, 0xFF, 0xFF, 0x00, 0x00)
|
||||
Patched = [byte[]](0x9A, 0xFF, 0xFF, 0x00, 0x00)
|
||||
OriginalPatterns = @(
|
||||
@('9A', 'FF', 'FF', '00', '00')
|
||||
)
|
||||
Fixup = @{
|
||||
OperandOffset = 0xCFAB6
|
||||
OriginalTargetSeg = 130
|
||||
OriginalTargetOffset = 0x0053
|
||||
PatchedTargetSeg = 131
|
||||
PatchedTargetOffset = 0x232D
|
||||
}
|
||||
}
|
||||
Hook = @{
|
||||
Label = 'Hidden menu direct hook site'
|
||||
Label = 'Rejected direct cheat hook site'
|
||||
Offset = 0x70D75
|
||||
Original = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
||||
Patched = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
||||
|
|
@ -31,7 +225,7 @@ $sites = @{
|
|||
}
|
||||
}
|
||||
Wrapper = @{
|
||||
Label = 'Current-slot wrapper arg site'
|
||||
Label = 'Rejected current-slot wrapper arg site'
|
||||
Offset = 0xB9A8D
|
||||
Original = [byte[]](0x6A, 0x01, 0xFF, 0x76, 0x08, 0xFF, 0x76, 0x06)
|
||||
Patched = [byte[]](0x6A, 0x01, 0x6A, 0x00, 0x6A, 0x00, 0x90, 0x90)
|
||||
|
|
@ -66,6 +260,137 @@ $sites = @{
|
|||
}
|
||||
}
|
||||
|
||||
$candidateProfiles = [ordered]@{
|
||||
'candidate-o' = @{
|
||||
MenuKey = '1'
|
||||
Label = 'Candidate O'
|
||||
ArmMode = 'interpreter-callsite'
|
||||
UiTargetSeg = 117
|
||||
UiTargetOffset = 0x020D
|
||||
PatchCurrentUnitWrapper = $false
|
||||
PatchModalWrapper = $true
|
||||
Summary = 'interpreter callsite retarget -> corrected private modal stub with zeroed modal-wrapper args'
|
||||
}
|
||||
'candidate-p' = @{
|
||||
MenuKey = '2'
|
||||
Label = 'Candidate P'
|
||||
ArmMode = 'interpreter-callsite'
|
||||
UiTargetSeg = 117
|
||||
UiTargetOffset = 0x0086
|
||||
PatchCurrentUnitWrapper = $true
|
||||
PatchModalWrapper = $false
|
||||
Summary = 'interpreter callsite retarget -> corrected private current-unit stub with zeroed current-slot args'
|
||||
}
|
||||
}
|
||||
|
||||
$script:ctrlQPatchedTemplate = [byte[]]$sites.CtrlQDebuggerInit.Patched.Clone()
|
||||
$script:configuredProfileKey = 'candidate-o'
|
||||
|
||||
function Find-ByteSequenceOffset {
|
||||
param(
|
||||
[byte[]]$Bytes,
|
||||
[byte[]]$Pattern
|
||||
)
|
||||
|
||||
$lastStart = $Bytes.Length - $Pattern.Length
|
||||
for ($start = 0; $start -le $lastStart; $start++) {
|
||||
$matched = $true
|
||||
for ($index = 0; $index -lt $Pattern.Length; $index++) {
|
||||
if ($Bytes[$start + $index] -ne $Pattern[$index]) {
|
||||
$matched = $false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($matched) {
|
||||
return $start
|
||||
}
|
||||
}
|
||||
|
||||
throw 'Could not find the private-vtable base immediate inside the Ctrl+Q patch template.'
|
||||
}
|
||||
|
||||
function New-CtrlQPatchedBytes {
|
||||
param([string]$ArmMode)
|
||||
|
||||
switch ($ArmMode) {
|
||||
'interpreter-callsite' {
|
||||
return [byte[]](
|
||||
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x74, 0x10, 0xC4, 0x1E,
|
||||
0x9C, 0x65, 0xC6, 0x47, 0x75, 0x00, 0xC6, 0x47, 0x74, 0x01, 0x5F,
|
||||
0x5E, 0xC9, 0xCB, 0x6A, 0x00, 0x6A, 0x00, 0xE9, 0x25, 0x00, 0x55,
|
||||
0x8B, 0xEC, 0xA1, 0x9C, 0x65, 0x39, 0x46, 0x06, 0x75, 0x16, 0xA1,
|
||||
0x9E, 0x65, 0x39, 0x46, 0x08, 0x75, 0x0E, 0xC4, 0x5E, 0x06, 0xC7,
|
||||
0x47, 0x74, 0x00, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0xEB, 0x0E, 0x5D,
|
||||
0xCB, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04,
|
||||
0xEB, 0x0A, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x5D,
|
||||
0xCB, 0x0B, 0xC2, 0x74, 0x13, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E,
|
||||
0x65, 0x89, 0xC3, 0x8E, 0xC2, 0xC6, 0x47, 0x75, 0x00, 0xC6, 0x47,
|
||||
0x74, 0x01, 0x5F, 0x5E, 0xC9, 0xCB
|
||||
)
|
||||
}
|
||||
default {
|
||||
throw "Unsupported arm mode '$ArmMode'."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ConfiguredCandidateProfile {
|
||||
return $candidateProfiles[$script:configuredProfileKey]
|
||||
}
|
||||
|
||||
function Set-ConfiguredCandidateProfile {
|
||||
param([string]$ProfileKey)
|
||||
|
||||
if (-not $candidateProfiles.Contains($ProfileKey)) {
|
||||
throw "Unknown candidate profile '$ProfileKey'."
|
||||
}
|
||||
|
||||
$profile = $candidateProfiles[$ProfileKey]
|
||||
$sites.CtrlQDebuggerInit.Patched = New-CtrlQPatchedBytes -ArmMode ([string]$profile.ArmMode)
|
||||
$sites.DebuggerCallback.Fixup.PatchedTargetSeg = [int]$profile.UiTargetSeg
|
||||
$sites.DebuggerCallback.Fixup.PatchedTargetOffset = [int]$profile.UiTargetOffset
|
||||
$script:configuredProfileKey = $ProfileKey
|
||||
}
|
||||
|
||||
function Test-CandidateApplied {
|
||||
param(
|
||||
[byte[]]$FileBytes,
|
||||
[string]$ProfileKey
|
||||
)
|
||||
|
||||
$previousProfileKey = $script:configuredProfileKey
|
||||
try {
|
||||
Set-ConfiguredCandidateProfile -ProfileKey $ProfileKey
|
||||
return (
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.CtrlQDebuggerInit) -eq 'Patched' -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.InterpreterBreakCall) -eq 'Patched' -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.DebuggerCallback) -eq 'Patched' -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackGuardCode) -eq 'Original' -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackTargetSlot) -eq 'Original' -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper) -eq $(if ($candidateProfiles[$ProfileKey].PatchCurrentUnitWrapper) { 'Patched' } else { 'Original' }) -and
|
||||
(Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredWrapper) -eq $(if ($candidateProfiles[$ProfileKey].PatchModalWrapper) { 'Patched' } else { 'Original' })
|
||||
)
|
||||
}
|
||||
finally {
|
||||
Set-ConfiguredCandidateProfile -ProfileKey $previousProfileKey
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AppliedCandidateProfileKey {
|
||||
param([byte[]]$FileBytes)
|
||||
|
||||
foreach ($profileKey in $candidateProfiles.Keys) {
|
||||
if (Test-CandidateApplied -FileBytes $FileBytes -ProfileKey $profileKey) {
|
||||
return $profileKey
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
Set-ConfiguredCandidateProfile -ProfileKey $script:configuredProfileKey
|
||||
|
||||
function Format-HexBytes {
|
||||
param([byte[]]$Bytes)
|
||||
|
||||
|
|
@ -449,6 +774,27 @@ function Get-SiteState {
|
|||
}
|
||||
}
|
||||
|
||||
$legacyCandidates = @()
|
||||
if ($Site.ContainsKey('LegacyPatched')) {
|
||||
$legacyCandidates += ,([byte[]]$Site.LegacyPatched)
|
||||
}
|
||||
if ($Site.ContainsKey('LegacyPatchedVariants')) {
|
||||
$legacyCandidates += $Site.LegacyPatchedVariants
|
||||
}
|
||||
|
||||
foreach ($legacyCandidate in $legacyCandidates) {
|
||||
if (Test-ByteArrayEqual -Left $current -Right $legacyCandidate) {
|
||||
$legacyFixupState = if ($Site.ContainsKey('LegacyPatchedFixupState')) { [string]$Site.LegacyPatchedFixupState } else { 'Patched' }
|
||||
if (
|
||||
($legacyFixupState -eq 'Patched' -and $isFixupPatched) -or
|
||||
($legacyFixupState -eq 'Original' -and $isFixupOriginal) -or
|
||||
($legacyFixupState -eq 'Any' -and ($isFixupPatched -or $isFixupOriginal))
|
||||
) {
|
||||
return 'LegacyPatched'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown'
|
||||
}
|
||||
|
||||
|
|
@ -502,18 +848,49 @@ function Set-ByteSlice {
|
|||
function Show-Status {
|
||||
param([byte[]]$FileBytes)
|
||||
|
||||
$selectedProfile = Get-ConfiguredCandidateProfile
|
||||
$appliedProfileKey = Get-AppliedCandidateProfileKey -FileBytes $FileBytes
|
||||
$appliedProfile = if ($null -ne $appliedProfileKey) { $candidateProfiles[$appliedProfileKey] } else { $null }
|
||||
|
||||
$ctrlQState = Get-SiteState -FileBytes $FileBytes -Site $sites.CtrlQDebuggerInit
|
||||
$interpreterCallState = Get-SiteState -FileBytes $FileBytes -Site $sites.InterpreterBreakCall
|
||||
$privateDispatchState = Get-SiteState -FileBytes $FileBytes -Site $sites.DebuggerCallback
|
||||
$callbackGuardState = Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackGuardCode
|
||||
$callbackTargetState = Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackTargetSlot
|
||||
$hookState = Get-SiteState -FileBytes $FileBytes -Site $sites.Hook
|
||||
$wrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper
|
||||
$deferredHookState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredHook
|
||||
$deferredWrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredWrapper
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'CRUSADER.EXE patch status'
|
||||
Write-Host '------------------------'
|
||||
Write-Host ("EXE: {0}" -f $exePath)
|
||||
Write-Host ("A hook @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
||||
Write-Host ("B wrapper @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
||||
Write-Host ("Selected candidate : {0} ({1})" -f $selectedProfile.Label, $selectedProfile.Summary)
|
||||
if ($null -ne $appliedProfile) {
|
||||
Write-Host ("Applied candidate : {0} ({1})" -f $appliedProfile.Label, $appliedProfile.Summary)
|
||||
}
|
||||
elseif ($ctrlQState -eq 'Original') {
|
||||
Write-Host 'Applied candidate : Retail/original'
|
||||
}
|
||||
else {
|
||||
Write-Host 'Applied candidate : Unknown'
|
||||
}
|
||||
Write-Host ("0x410 init body @ 0x{0:X}: {1}" -f $sites.CtrlQDebuggerInit.Offset, $ctrlQState)
|
||||
Write-Host ("Interpreter call @ 0x{0:X}: {1}" -f $sites.InterpreterBreakCall.Offset, $interpreterCallState)
|
||||
Write-Host ("Private UI call @ 0x{0:X}: {1}" -f $sites.DebuggerCallback.Offset, $privateDispatchState)
|
||||
Write-Host ("Legacy break hook @ 0x{0:X}: {1}" -f $sites.CallbackGuardCode.Offset, $callbackGuardState)
|
||||
Write-Host ("Legacy target slot @ 0x{0:X}: {1}" -f $sites.CallbackTargetSlot.Offset, $callbackTargetState)
|
||||
Write-Host ("Current-unit args @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
||||
Write-Host ("Modal wrapper args @ 0x{0:X}: {1}" -f $sites.LegacyDeferredWrapper.Offset, $deferredWrapperState)
|
||||
Write-Host ("Deferred hook cleanup @ 0x{0:X}: {1}" -f $sites.LegacyDeferredHook.Offset, $deferredHookState)
|
||||
Write-Host ("Direct hook cleanup @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
||||
Write-Host ''
|
||||
Write-Host '1. Apply supported hidden-menu patch (aliases to Experiment B)'
|
||||
Write-Host '2. Apply Experiment B (retarget + modal arg fix)'
|
||||
foreach ($profileKey in $candidateProfiles.Keys) {
|
||||
$profile = $candidateProfiles[$profileKey]
|
||||
$marker = if ($appliedProfileKey -eq $profileKey) { 'applied' } else { 'ready' }
|
||||
Write-Host ("{0}. Apply {1} [{2}] {3}" -f $profile.MenuKey, $profile.Label, $marker, $profile.Summary)
|
||||
}
|
||||
Write-Host '3. Restore original bytes'
|
||||
Write-Host '4. Exit'
|
||||
Write-Host ''
|
||||
|
|
@ -521,95 +898,120 @@ function Show-Status {
|
|||
|
||||
function Set-DesiredState {
|
||||
param(
|
||||
[bool]$HookPatched,
|
||||
[bool]$WrapperPatched,
|
||||
[string]$Label
|
||||
[bool]$CtrlQPatched,
|
||||
[string]$Label,
|
||||
[string]$ProfileKey
|
||||
)
|
||||
|
||||
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
|
||||
$stateData = Get-StateData
|
||||
|
||||
$appliedProfileKey = Get-AppliedCandidateProfileKey -FileBytes $fileBytes
|
||||
$profileKeyForStateCheck = if ($null -ne $appliedProfileKey) { $appliedProfileKey } elseif ($CtrlQPatched) { $ProfileKey } else { $script:configuredProfileKey }
|
||||
$profileKeyForWrite = if ($CtrlQPatched) { $ProfileKey } else { $profileKeyForStateCheck }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($profileKeyForStateCheck)) {
|
||||
throw 'No candidate profile is available for state validation.'
|
||||
}
|
||||
|
||||
Set-ConfiguredCandidateProfile -ProfileKey $profileKeyForStateCheck
|
||||
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.CtrlQDebuggerInit
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.InterpreterBreakCall
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.DebuggerCallback
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.CallbackGuardCode
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.CallbackTargetSlot
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod0
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod1
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyBreakpointCallback
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.Hook
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.Wrapper
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyDeferredHook
|
||||
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyDeferredWrapper
|
||||
|
||||
$hookCurrent = Get-ByteSlice -Bytes $fileBytes -Offset $sites.Hook.Offset -Count $sites.Hook.Original.Length
|
||||
Set-ConfiguredCandidateProfile -ProfileKey $profileKeyForWrite
|
||||
$activeProfile = Get-ConfiguredCandidateProfile
|
||||
|
||||
$ctrlQFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.CtrlQDebuggerInit
|
||||
$interpreterBreakCallFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.InterpreterBreakCall
|
||||
$debuggerCallbackFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.DebuggerCallback
|
||||
$callbackTargetFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.CallbackTargetSlot
|
||||
$privateMethod0FixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod0
|
||||
$privateMethod1FixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod1
|
||||
$legacyCallbackFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.LegacyBreakpointCallback
|
||||
$hookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.Hook
|
||||
$wrapperCurrent = Get-ByteSlice -Bytes $fileBytes -Offset $sites.Wrapper.Offset -Count $sites.Wrapper.Original.Length
|
||||
$legacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.LegacyDeferredHook
|
||||
$hookCurrentState = Get-SiteState -FileBytes $fileBytes -Site $sites.Hook
|
||||
$wrapperCurrentState = Get-SiteState -FileBytes $fileBytes -Site $sites.Wrapper
|
||||
|
||||
$stateChanged = $false
|
||||
if ($hookCurrentState -eq 'Original') {
|
||||
$stateChanged = (Save-OriginalBytesIfMissing -StateData $stateData -SiteKey 'Hook' -SiteOffset $sites.Hook.Offset -Bytes $hookCurrent) -or $stateChanged
|
||||
if (-not $stateData.ContainsKey('HookFixup')) {
|
||||
$stateData['HookFixup'] = @{
|
||||
TargetSeg = $hookFixupInfo.TargetSeg
|
||||
TargetOffset = $hookFixupInfo.TargetOffset
|
||||
Reserved = $hookFixupInfo.Reserved
|
||||
}
|
||||
$stateChanged = $true
|
||||
}
|
||||
$ctrlQBytes = if ($CtrlQPatched) { $sites.CtrlQDebuggerInit.Patched } else { $sites.CtrlQDebuggerInit.Original }
|
||||
$ctrlQTargetSeg = if ($CtrlQPatched) { [int]$sites.CtrlQDebuggerInit.Fixup.PatchedTargetSeg } else { [int]$sites.CtrlQDebuggerInit.Fixup.OriginalTargetSeg }
|
||||
$ctrlQTargetOffset = if ($CtrlQPatched) { [int]$sites.CtrlQDebuggerInit.Fixup.PatchedTargetOffset } else { [int]$sites.CtrlQDebuggerInit.Fixup.OriginalTargetOffset }
|
||||
$interpreterBreakCallBytes = $sites.InterpreterBreakCall.Original
|
||||
$interpreterBreakCallSeg = [int]$sites.InterpreterBreakCall.Fixup.OriginalTargetSeg
|
||||
$interpreterBreakCallOffset = [int]$sites.InterpreterBreakCall.Fixup.OriginalTargetOffset
|
||||
if ($CtrlQPatched) {
|
||||
$interpreterBreakCallBytes = $sites.InterpreterBreakCall.Patched
|
||||
$interpreterBreakCallSeg = [int]$sites.InterpreterBreakCall.Fixup.PatchedTargetSeg
|
||||
$interpreterBreakCallOffset = [int]$sites.InterpreterBreakCall.Fixup.PatchedTargetOffset
|
||||
}
|
||||
if ($wrapperCurrentState -eq 'Original') {
|
||||
$stateChanged = (Save-OriginalBytesIfMissing -StateData $stateData -SiteKey 'Wrapper' -SiteOffset $sites.Wrapper.Offset -Bytes $wrapperCurrent) -or $stateChanged
|
||||
$debuggerCallbackBytes = $sites.DebuggerCallback.Original
|
||||
$debuggerCallbackSeg = [int]$sites.DebuggerCallback.Fixup.OriginalTargetSeg
|
||||
$debuggerCallbackOffset = [int]$sites.DebuggerCallback.Fixup.OriginalTargetOffset
|
||||
if ($CtrlQPatched) {
|
||||
$debuggerCallbackBytes = $sites.DebuggerCallback.Patched
|
||||
$debuggerCallbackSeg = [int]$sites.DebuggerCallback.Fixup.PatchedTargetSeg
|
||||
$debuggerCallbackOffset = [int]$sites.DebuggerCallback.Fixup.PatchedTargetOffset
|
||||
}
|
||||
if ($stateChanged) {
|
||||
Save-StateData -StateData $stateData
|
||||
}
|
||||
|
||||
if ($HookPatched) {
|
||||
$hookBytes = $sites.Hook.Patched
|
||||
$hookTargetSeg = [int]$sites.Hook.Fixup.PatchedTargetSeg
|
||||
$hookTargetOffset = [int]$sites.Hook.Fixup.PatchedTargetOffset
|
||||
$hookReserved = 0
|
||||
}
|
||||
else {
|
||||
$hookBytes = Get-SavedOriginalBytes -StateData $stateData -SiteKey 'Hook' -SiteOffset $sites.Hook.Offset
|
||||
if ($null -eq $hookBytes) {
|
||||
if ($hookCurrentState -eq 'Original' -or $hookCurrentState -eq 'LegacyBadPatch') {
|
||||
$hookBytes = $hookCurrent
|
||||
}
|
||||
else {
|
||||
throw 'No saved original bytes are available for the Experiment A hook site. Restore requires either a prior patch run with this script or your full executable backup.'
|
||||
}
|
||||
}
|
||||
|
||||
if ($stateData.ContainsKey('HookFixup')) {
|
||||
$hookTargetSeg = [int]$stateData['HookFixup'].TargetSeg
|
||||
$hookTargetOffset = [int]$stateData['HookFixup'].TargetOffset
|
||||
$hookReserved = [int]$stateData['HookFixup'].Reserved
|
||||
}
|
||||
else {
|
||||
$hookTargetSeg = [int]$sites.Hook.Fixup.OriginalTargetSeg
|
||||
$hookTargetOffset = [int]$sites.Hook.Fixup.OriginalTargetOffset
|
||||
$hookReserved = 0
|
||||
}
|
||||
}
|
||||
|
||||
if ($WrapperPatched) {
|
||||
$wrapperBytes = $sites.Wrapper.Patched
|
||||
}
|
||||
else {
|
||||
$wrapperBytes = Get-SavedOriginalBytes -StateData $stateData -SiteKey 'Wrapper' -SiteOffset $sites.Wrapper.Offset
|
||||
if ($null -eq $wrapperBytes) {
|
||||
if ($wrapperCurrentState -eq 'Original') {
|
||||
$wrapperBytes = $wrapperCurrent
|
||||
}
|
||||
else {
|
||||
throw 'No saved original bytes are available for the Experiment B wrapper site. Restore requires either a prior patch run with this script or your full executable backup.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$callbackGuardBytes = $sites.CallbackGuardCode.Original
|
||||
$callbackTargetBytes = $sites.CallbackTargetSlot.Original
|
||||
$callbackTargetSeg = [int]$sites.CallbackTargetSlot.Fixup.OriginalTargetSeg
|
||||
$callbackTargetOffset = [int]$sites.CallbackTargetSlot.Fixup.OriginalTargetOffset
|
||||
$privateMethod0Bytes = $sites.PrivateBreakpointMethod0.Original
|
||||
$privateMethod0TargetSeg = [int]$sites.PrivateBreakpointMethod0.Fixup.OriginalTargetSeg
|
||||
$privateMethod0TargetOffset = [int]$sites.PrivateBreakpointMethod0.Fixup.OriginalTargetOffset
|
||||
$privateMethod1Bytes = $sites.PrivateBreakpointMethod1.Original
|
||||
$privateMethod1TargetSeg = [int]$sites.PrivateBreakpointMethod1.Fixup.OriginalTargetSeg
|
||||
$privateMethod1TargetOffset = [int]$sites.PrivateBreakpointMethod1.Fixup.OriginalTargetOffset
|
||||
$legacyCallbackBytes = $sites.LegacyBreakpointCallback.Original
|
||||
$legacyCallbackTargetSeg = [int]$sites.LegacyBreakpointCallback.Fixup.OriginalTargetSeg
|
||||
$legacyCallbackTargetOffset = [int]$sites.LegacyBreakpointCallback.Fixup.OriginalTargetOffset
|
||||
$hookBytes = $sites.Hook.Original
|
||||
$hookTargetSeg = [int]$sites.Hook.Fixup.OriginalTargetSeg
|
||||
$hookTargetOffset = [int]$sites.Hook.Fixup.OriginalTargetOffset
|
||||
$hookReserved = 0
|
||||
$wrapperBytes = if ($CtrlQPatched -and $activeProfile.PatchCurrentUnitWrapper) { $sites.Wrapper.Patched } else { $sites.Wrapper.Original }
|
||||
$legacyDeferredHookBytes = $sites.LegacyDeferredHook.Original
|
||||
$legacyDeferredHookTargetSeg = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetSeg
|
||||
$legacyDeferredHookTargetOffset = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetOffset
|
||||
$legacyDeferredHookReserved = 0
|
||||
$legacyDeferredWrapperBytes = $sites.LegacyDeferredWrapper.Original
|
||||
$legacyDeferredWrapperBytes = if ($CtrlQPatched -and $activeProfile.PatchModalWrapper) { $sites.LegacyDeferredWrapper.Patched } else { $sites.LegacyDeferredWrapper.Original }
|
||||
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.CtrlQDebuggerInit.Offset -Value $ctrlQBytes
|
||||
$fileBytes[$ctrlQFixupInfo.EntryOffset + 4] = [byte]$ctrlQTargetSeg
|
||||
$fileBytes[$ctrlQFixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($ctrlQFixupInfo.EntryOffset + 6) -Value $ctrlQTargetOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.InterpreterBreakCall.Offset -Value $interpreterBreakCallBytes
|
||||
$fileBytes[$interpreterBreakCallFixupInfo.EntryOffset + 4] = [byte]$interpreterBreakCallSeg
|
||||
$fileBytes[$interpreterBreakCallFixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($interpreterBreakCallFixupInfo.EntryOffset + 6) -Value $interpreterBreakCallOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.DebuggerCallback.Offset -Value $debuggerCallbackBytes
|
||||
$fileBytes[$debuggerCallbackFixupInfo.EntryOffset + 4] = [byte]$debuggerCallbackSeg
|
||||
$fileBytes[$debuggerCallbackFixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($debuggerCallbackFixupInfo.EntryOffset + 6) -Value $debuggerCallbackOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.CallbackGuardCode.Offset -Value $callbackGuardBytes
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.CallbackTargetSlot.Offset -Value $callbackTargetBytes
|
||||
$fileBytes[$callbackTargetFixupInfo.EntryOffset + 4] = [byte]$callbackTargetSeg
|
||||
$fileBytes[$callbackTargetFixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($callbackTargetFixupInfo.EntryOffset + 6) -Value $callbackTargetOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.PrivateBreakpointMethod0.Offset -Value $privateMethod0Bytes
|
||||
$fileBytes[$privateMethod0FixupInfo.EntryOffset + 4] = [byte]$privateMethod0TargetSeg
|
||||
$fileBytes[$privateMethod0FixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($privateMethod0FixupInfo.EntryOffset + 6) -Value $privateMethod0TargetOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.PrivateBreakpointMethod1.Offset -Value $privateMethod1Bytes
|
||||
$fileBytes[$privateMethod1FixupInfo.EntryOffset + 4] = [byte]$privateMethod1TargetSeg
|
||||
$fileBytes[$privateMethod1FixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($privateMethod1FixupInfo.EntryOffset + 6) -Value $privateMethod1TargetOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.LegacyBreakpointCallback.Offset -Value $legacyCallbackBytes
|
||||
$fileBytes[$legacyCallbackFixupInfo.EntryOffset + 4] = [byte]$legacyCallbackTargetSeg
|
||||
$fileBytes[$legacyCallbackFixupInfo.EntryOffset + 5] = 0
|
||||
Set-U16Le -Bytes $fileBytes -Offset ($legacyCallbackFixupInfo.EntryOffset + 6) -Value $legacyCallbackTargetOffset
|
||||
Set-ByteSlice -Bytes $fileBytes -Offset $sites.Hook.Offset -Value $hookBytes
|
||||
$fileBytes[$hookFixupInfo.EntryOffset + 4] = [byte]$hookTargetSeg
|
||||
$fileBytes[$hookFixupInfo.EntryOffset + 5] = [byte]$hookReserved
|
||||
|
|
@ -624,15 +1026,97 @@ function Set-DesiredState {
|
|||
[System.IO.File]::WriteAllBytes($exePath, $fileBytes)
|
||||
|
||||
$verifyBytes = [System.IO.File]::ReadAllBytes($exePath)
|
||||
$ctrlQState = Get-SiteState -FileBytes $verifyBytes -Site $sites.CtrlQDebuggerInit
|
||||
$interpreterCallState = Get-SiteState -FileBytes $verifyBytes -Site $sites.InterpreterBreakCall
|
||||
$privateDispatchState = Get-SiteState -FileBytes $verifyBytes -Site $sites.DebuggerCallback
|
||||
$callbackGuardState = Get-SiteState -FileBytes $verifyBytes -Site $sites.CallbackGuardCode
|
||||
$callbackTargetState = Get-SiteState -FileBytes $verifyBytes -Site $sites.CallbackTargetSlot
|
||||
$hookState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Hook
|
||||
$wrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Wrapper
|
||||
$deferredHookState = Get-SiteState -FileBytes $verifyBytes -Site $sites.LegacyDeferredHook
|
||||
$deferredWrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites.LegacyDeferredWrapper
|
||||
$verifiedCtrlQFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.CtrlQDebuggerInit
|
||||
$verifiedInterpreterBreakCallFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.InterpreterBreakCall
|
||||
$verifiedDebuggerCallbackFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.DebuggerCallback
|
||||
$verifiedCallbackTargetFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.CallbackTargetSlot
|
||||
$verifiedPrivateMethod0FixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.PrivateBreakpointMethod0
|
||||
$verifiedPrivateMethod1FixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.PrivateBreakpointMethod1
|
||||
$verifiedLegacyCallbackFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.LegacyBreakpointCallback
|
||||
$verifiedHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.Hook
|
||||
$verifiedLegacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.LegacyDeferredHook
|
||||
$verifiedCtrlQBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.CtrlQDebuggerInit.Offset -Count $ctrlQBytes.Length
|
||||
$verifiedInterpreterBreakCallBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.InterpreterBreakCall.Offset -Count $interpreterBreakCallBytes.Length
|
||||
$verifiedDebuggerCallbackBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.DebuggerCallback.Offset -Count $debuggerCallbackBytes.Length
|
||||
$verifiedCallbackGuardBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.CallbackGuardCode.Offset -Count $callbackGuardBytes.Length
|
||||
$verifiedCallbackTargetBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.CallbackTargetSlot.Offset -Count $callbackTargetBytes.Length
|
||||
$verifiedPrivateMethod0Bytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.PrivateBreakpointMethod0.Offset -Count $privateMethod0Bytes.Length
|
||||
$verifiedPrivateMethod1Bytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.PrivateBreakpointMethod1.Offset -Count $privateMethod1Bytes.Length
|
||||
$verifiedLegacyCallbackBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyBreakpointCallback.Offset -Count $legacyCallbackBytes.Length
|
||||
$verifiedHookBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.Hook.Offset -Count $hookBytes.Length
|
||||
$verifiedWrapperBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.Wrapper.Offset -Count $wrapperBytes.Length
|
||||
$verifiedLegacyDeferredHookBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyDeferredHook.Offset -Count $legacyDeferredHookBytes.Length
|
||||
$verifiedLegacyDeferredWrapperBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyDeferredWrapper.Offset -Count $legacyDeferredWrapperBytes.Length
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedCtrlQBytes -Right $ctrlQBytes)) {
|
||||
throw 'Ctrl+Q debugger-init body verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedCtrlQFixupInfo.TargetSeg -ne $ctrlQTargetSeg -or $verifiedCtrlQFixupInfo.TargetOffset -ne $ctrlQTargetOffset) {
|
||||
throw 'Ctrl+Q debugger-init relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedInterpreterBreakCallBytes -Right $interpreterBreakCallBytes)) {
|
||||
throw 'Interpreter debugger callsite verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedInterpreterBreakCallFixupInfo.TargetSeg -ne $interpreterBreakCallSeg -or $verifiedInterpreterBreakCallFixupInfo.TargetOffset -ne $interpreterBreakCallOffset) {
|
||||
throw 'Interpreter debugger callsite relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedDebuggerCallbackBytes -Right $debuggerCallbackBytes)) {
|
||||
throw 'Private debugger UI call verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedDebuggerCallbackFixupInfo.TargetSeg -ne $debuggerCallbackSeg -or $verifiedDebuggerCallbackFixupInfo.TargetOffset -ne $debuggerCallbackOffset) {
|
||||
throw 'Private debugger UI call relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedCallbackGuardBytes -Right $callbackGuardBytes)) {
|
||||
throw 'Break-next dispatch patch verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedCallbackTargetBytes -Right $callbackTargetBytes)) {
|
||||
throw 'Guarded callback target slot bytes verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedCallbackTargetFixupInfo.TargetSeg -ne $callbackTargetSeg -or $verifiedCallbackTargetFixupInfo.TargetOffset -ne $callbackTargetOffset) {
|
||||
throw 'Guarded callback target relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedPrivateMethod0Bytes -Right $privateMethod0Bytes)) {
|
||||
throw 'Private vtable method-0 bytes verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedPrivateMethod0FixupInfo.TargetSeg -ne $privateMethod0TargetSeg -or $verifiedPrivateMethod0FixupInfo.TargetOffset -ne $privateMethod0TargetOffset) {
|
||||
throw 'Private vtable method-0 relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedPrivateMethod1Bytes -Right $privateMethod1Bytes)) {
|
||||
throw 'Private vtable method-1 bytes verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedPrivateMethod1FixupInfo.TargetSeg -ne $privateMethod1TargetSeg -or $verifiedPrivateMethod1FixupInfo.TargetOffset -ne $privateMethod1TargetOffset) {
|
||||
throw 'Private vtable method-1 relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedLegacyCallbackBytes -Right $legacyCallbackBytes)) {
|
||||
throw 'Legacy callback cleanup bytes verification failed after write.'
|
||||
}
|
||||
|
||||
if ($verifiedLegacyCallbackFixupInfo.TargetSeg -ne $legacyCallbackTargetSeg -or $verifiedLegacyCallbackFixupInfo.TargetOffset -ne $legacyCallbackTargetOffset) {
|
||||
throw 'Legacy callback cleanup relocation verification failed after write.'
|
||||
}
|
||||
|
||||
if (-not (Test-ByteArrayEqual -Left $verifiedHookBytes -Right $hookBytes)) {
|
||||
throw 'Hook-site verification failed after write.'
|
||||
}
|
||||
|
|
@ -659,33 +1143,75 @@ function Set-DesiredState {
|
|||
|
||||
Write-Host ''
|
||||
Write-Host ("Applied: {0}" -f $Label)
|
||||
Write-Host ("A hook @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
||||
Write-Host ("B wrapper @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
||||
Write-Host ("0x410 init body @ 0x{0:X}: {1}" -f $sites.CtrlQDebuggerInit.Offset, $ctrlQState)
|
||||
Write-Host ("Interpreter call @ 0x{0:X}: {1}" -f $sites.InterpreterBreakCall.Offset, $interpreterCallState)
|
||||
Write-Host ("Private UI call @ 0x{0:X}: {1}" -f $sites.DebuggerCallback.Offset, $privateDispatchState)
|
||||
Write-Host ("Legacy break hook @ 0x{0:X}: {1}" -f $sites.CallbackGuardCode.Offset, $callbackGuardState)
|
||||
Write-Host ("Legacy target slot @ 0x{0:X}: {1}" -f $sites.CallbackTargetSlot.Offset, $callbackTargetState)
|
||||
Write-Host ("Current-unit args @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
||||
Write-Host ("Modal wrapper args @ 0x{0:X}: {1}" -f $sites.LegacyDeferredWrapper.Offset, $deferredWrapperState)
|
||||
Write-Host ("Deferred hook cleanup @ 0x{0:X}: {1}" -f $sites.LegacyDeferredHook.Offset, $deferredHookState)
|
||||
Write-Host ("Direct hook cleanup @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
||||
Write-Host ''
|
||||
Write-Host 'What this means:'
|
||||
Write-Host '- Experiment A retargets the existing cheat-success far call into cheat_menu_open_from_current_slot while keeping the original event-dispatch framing.'
|
||||
Write-Host '- Experiment B preserves the wrapper mode byte `1` but forces the two ambiguous 16-bit constructor parameters to zero instead of inheriting arbitrary caller-frame values.'
|
||||
Write-Host '- Restore also cleans up the rejected deferred-event patch sites if they were left behind by earlier attempts.'
|
||||
Write-Host '- Ctrl+Q still goes through the real 0x410 keyboard lane and keeps the original 0x844 cheat gate.'
|
||||
if ($CtrlQPatched) {
|
||||
Write-Host ('- The 0x410 body now correctly handles both cases: it arms an existing seg1408 debugger-state object in place, or lazily creates one and stores it at 0x659c/0x659e before arming break-next mode (+0x74=1, +0x75=0).')
|
||||
Write-Host ("- {0} stops using the unsafe 1478:6597 data slot entirely and retargets the existing interpreter call at 1418:04b5 straight into the corrected private 13e8:232d stub." -f $activeProfile.Label)
|
||||
if ($activeProfile.PatchCurrentUnitWrapper) {
|
||||
Write-Host '- The current-unit debugger wrapper at 13a0:0086 has its inherited caller-word pushes zeroed so the callback does not forward the debugger-object pointer as UI arguments.'
|
||||
}
|
||||
if ($activeProfile.PatchModalWrapper) {
|
||||
Write-Host '- The modal debugger wrapper at 13a0:020d has its inherited caller-word pushes zeroed for the same reason.'
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host '- All debugger patch changes are restored to the retail byte pattern, including the 0x410 body and the interpreter break call at 1418:04b5.'
|
||||
Write-Host '- Older private-vtable and direct/deferred experiment sites are also written back to retail bytes during restore.'
|
||||
}
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
function Invoke-MenuChoice {
|
||||
param([string]$SelectedChoice)
|
||||
|
||||
switch ($SelectedChoice.Trim()) {
|
||||
switch ($SelectedChoice.Trim().ToLowerInvariant()) {
|
||||
'1' {
|
||||
Write-Warning 'Experiment A alone is not supported on the cheat-code path. Applying the safer Experiment B patch instead.'
|
||||
Set-DesiredState -HookPatched $true -WrapperPatched $true -Label 'Experiment B (via menu option 1 alias)'
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
||||
}
|
||||
'2' {
|
||||
Set-DesiredState -HookPatched $true -WrapperPatched $true -Label 'Experiment B (A + B)'
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
||||
}
|
||||
'candidate-i' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
||||
}
|
||||
'candidate-j' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
||||
}
|
||||
'candidate-m' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
||||
}
|
||||
'candidate-n' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
||||
}
|
||||
'candidate-o' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
||||
}
|
||||
'candidate-p' {
|
||||
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
||||
}
|
||||
'3' {
|
||||
Set-DesiredState -HookPatched $false -WrapperPatched $false -Label 'Restore original bytes'
|
||||
Set-DesiredState -CtrlQPatched $false -ProfileKey $null -Label 'Restore original bytes'
|
||||
}
|
||||
'restore' {
|
||||
Set-DesiredState -CtrlQPatched $false -ProfileKey $null -Label 'Restore original bytes'
|
||||
}
|
||||
'4' {
|
||||
return $false
|
||||
}
|
||||
'exit' {
|
||||
return $false
|
||||
}
|
||||
default {
|
||||
Write-Warning 'Invalid selection.'
|
||||
}
|
||||
|
|
@ -695,7 +1221,7 @@ function Invoke-MenuChoice {
|
|||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $exePath)) {
|
||||
throw "CRUSADER.EXE was not found next to the script. Put this .ps1 file in the same folder as CRUSADER.EXE."
|
||||
throw "CRUSADER.EXE was not found at '$exePath'. Pass -ExePath to point at the retail install or place the EXE next to this script."
|
||||
}
|
||||
|
||||
if ($PSBoundParameters.ContainsKey('Choice')) {
|
||||
|
|
@ -706,7 +1232,7 @@ if ($PSBoundParameters.ContainsKey('Choice')) {
|
|||
:mainloop while ($true) {
|
||||
$currentBytes = [System.IO.File]::ReadAllBytes($exePath)
|
||||
Show-Status -FileBytes $currentBytes
|
||||
$choice = Read-Host 'Select 1, 2, 3, or 4'
|
||||
$choice = Read-Host 'Select 1-4'
|
||||
if ([string]::IsNullOrEmpty($choice)) { break mainloop }
|
||||
|
||||
if (-not (Invoke-MenuChoice -SelectedChoice $choice)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue