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.
1241 lines
No EOL
56 KiB
PowerShell
1241 lines
No EOL
56 KiB
PowerShell
param(
|
|
[ValidateSet('1', '2', '3', '4', 'candidate-i', 'candidate-j', 'candidate-m', 'candidate-n', 'candidate-o', 'candidate-p', 'restore', 'exit')]
|
|
[string]$Choice,
|
|
|
|
[string]$ExePath = $(
|
|
if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'CRUSADER.EXE')) {
|
|
Join-Path $PSScriptRoot 'CRUSADER.EXE'
|
|
}
|
|
elseif (Test-Path -LiteralPath 'F:\Apps\Crusader No Remorse\CRUSADER.EXE') {
|
|
'F:\Apps\Crusader No Remorse\CRUSADER.EXE'
|
|
}
|
|
else {
|
|
Join-Path $PSScriptRoot 'CRUSADER.EXE'
|
|
}
|
|
)
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$exePath = $ExePath
|
|
$statePath = Join-Path $PSScriptRoot 'patch_crusader_cheat_menu.state.json'
|
|
|
|
$sites = @{
|
|
CtrlQDebuggerInit = @{
|
|
Label = 'Ctrl+Q debugger-state init body'
|
|
Offset = 0xC970D
|
|
Original = [byte[]](
|
|
0xA0, 0x4F, 0x60, 0xB4, 0x00, 0xF7, 0xD8, 0x1B, 0xC0, 0x40, 0xA2, 0x4F, 0x60, 0x80, 0x3E, 0x4F,
|
|
0x60, 0x00, 0x74, 0x47, 0x6A, 0xFF, 0x6A, 0xFF, 0xC4, 0x1E, 0xD0, 0x4C, 0x26, 0x8A, 0x47, 0x05,
|
|
0x50, 0x1E, 0x68, 0xD2, 0x60, 0x6A, 0x00, 0x6A, 0x00, 0x83, 0xEC, 0x06, 0xC7, 0x86, 0x76, 0xFF,
|
|
0x00, 0x00, 0x8B, 0x86, 0x76, 0xFF, 0xF7, 0xD0, 0x89, 0x86, 0x78, 0xFF, 0xC6, 0x86, 0x7A, 0xFF,
|
|
0x00, 0x6A, 0x00, 0x6A, 0x00, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x14, 0x52, 0x50, 0x9A,
|
|
0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x08, 0x5F, 0x5E, 0xC9, 0xCB, 0x6A, 0xFF, 0x6A, 0xFF, 0xC4,
|
|
0x1E, 0xD0, 0x4C, 0x26, 0x8A, 0x47, 0x05, 0x50, 0x1E, 0x68, 0xEE, 0x60, 0x6A, 0x00, 0x6A, 0x00,
|
|
0x83, 0xEC, 0x06, 0xC7
|
|
)
|
|
Patched = [byte[]](
|
|
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x74, 0x10, 0xC4, 0x1E, 0x9C, 0x65, 0xC6, 0x47, 0x75,
|
|
0x00, 0xC6, 0x47, 0x74, 0x01, 0x5F, 0x5E, 0xC9, 0xCB, 0x6A, 0x00, 0x6A, 0x00, 0xE9, 0x25, 0x00,
|
|
0x55, 0x8B, 0xEC, 0xA1, 0x9C, 0x65, 0x39, 0x46, 0x06, 0x75, 0x16, 0xA1, 0x9E, 0x65, 0x39, 0x46,
|
|
0x08, 0x75, 0x0E, 0xC4, 0x5E, 0x06, 0xC7, 0x47, 0x74, 0x00, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0xEB,
|
|
0x0E, 0x5D, 0xCB, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0xEB, 0x0A, 0x9A,
|
|
0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x5D, 0xCB, 0x0B, 0xC2, 0x74, 0x13, 0xA3, 0x9C, 0x65,
|
|
0x89, 0x16, 0x9E, 0x65, 0x89, 0xC3, 0x8E, 0xC2, 0xC6, 0x47, 0x75, 0x00, 0xC6, 0x47, 0x74, 0x01,
|
|
0x5F, 0x5E, 0xC9, 0xCB
|
|
)
|
|
LegacyPatched = [byte[]](
|
|
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x52, 0x31, 0xC0, 0x50, 0x50, 0xEB, 0x36,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x0B, 0xC2, 0x75,
|
|
0x03, 0xE9, 0x46, 0x06, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E, 0x65, 0xC4, 0x1E, 0x9C, 0x65, 0xC6,
|
|
0x47, 0x75, 0x01, 0xC6, 0x47, 0x74, 0x00, 0xC7, 0x47, 0x76, 0x00, 0x00, 0xC7, 0x47, 0x78, 0x00,
|
|
0x00, 0xE9, 0x26, 0x06, 0xC7
|
|
)
|
|
LegacyPatchedVariants = @(
|
|
[byte[]](
|
|
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x5E, 0x31, 0xC0, 0x50, 0x50, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04,
|
|
0xEB, 0x0D, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x5F, 0x5E, 0xC9, 0xCB, 0x0B, 0xC2,
|
|
0x74, 0xF8, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E, 0x65, 0x6A, 0x00, 0x6A, 0x00, 0xEB, 0xE2, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xC7
|
|
),
|
|
[byte[]](
|
|
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x5E, 0x31, 0xC0, 0x50, 0x50, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0xEB, 0x0D, 0x9A,
|
|
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x04, 0x5F, 0x5E, 0xC9, 0xCB, 0x0B, 0xC2, 0x74, 0xF7, 0xA3,
|
|
0x9C, 0x65, 0x89, 0x16, 0x9E, 0x65, 0x6A, 0x00, 0x6A, 0x00, 0xEB, 0xE2, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0xEC, 0x06, 0xC7
|
|
),
|
|
[byte[]](
|
|
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x75, 0x55, 0x31, 0xC0, 0x50, 0x50, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04,
|
|
0x52, 0x50, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E,
|
|
0x65, 0xC4, 0x1E, 0x9C, 0x65, 0xC7, 0x07, 0xAF, 0x65, 0xC6, 0x47, 0x75, 0x01, 0xC6, 0x47, 0x74,
|
|
0x00, 0x5F, 0x5E, 0xC9, 0xCB, 0x90, 0x90
|
|
)
|
|
)
|
|
LegacyPatchedFixupState = 'Any'
|
|
OriginalPatterns = @(
|
|
@('A0', '4F', '60', 'B4', '00', 'F7', 'D8', '1B', 'C0', '40', 'A2', '4F', '60', '80', '3E', '4F', '60', '00', '74', '47')
|
|
)
|
|
Fixup = @{
|
|
OperandOffset = 0xC9753
|
|
OriginalTargetSeg = 107
|
|
OriginalTargetOffset = 0x0046
|
|
PatchedTargetSeg = 130
|
|
PatchedTargetOffset = 0x0000
|
|
}
|
|
}
|
|
DebuggerCallback = @{
|
|
Label = 'legacy secondary call cleanup'
|
|
Offset = 0xC975D
|
|
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
|
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
|
OriginalPatterns = @(
|
|
@('FF', 'FF', '00', '00')
|
|
)
|
|
Fixup = @{
|
|
OperandOffset = 0xC975D
|
|
OriginalTargetSeg = 101
|
|
OriginalTargetOffset = 0x1588
|
|
PatchedTargetSeg = 101
|
|
PatchedTargetOffset = 0x1588
|
|
}
|
|
}
|
|
PrivateBreakpointMethod0 = @{
|
|
Label = 'private break callback slot'
|
|
Offset = 0xEA328
|
|
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
|
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
|
OriginalPatterns = @(
|
|
@('FF', 'FF', '00', '00')
|
|
)
|
|
Fixup = @{
|
|
OperandOffset = 0xEA328
|
|
OriginalTargetSeg = 133
|
|
OriginalTargetOffset = 0x1162
|
|
PatchedTargetSeg = 117
|
|
PatchedTargetOffset = 0x0086
|
|
}
|
|
}
|
|
PrivateBreakpointMethod1 = @{
|
|
Label = 'private helper callback slot'
|
|
Offset = 0xEA32C
|
|
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
|
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
|
OriginalPatterns = @(
|
|
@('FF', 'FF', '00', '00')
|
|
)
|
|
Fixup = @{
|
|
OperandOffset = 0xEA32C
|
|
OriginalTargetSeg = 133
|
|
OriginalTargetOffset = 0x1278
|
|
PatchedTargetSeg = 130
|
|
PatchedTargetOffset = 0x0474
|
|
}
|
|
}
|
|
LegacyBreakpointCallback = @{
|
|
Label = 'legacy seg1408 break callback target'
|
|
Offset = 0xEA1AB
|
|
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
|
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
|
OriginalPatterns = @(
|
|
@('FF', 'FF', '00', '00')
|
|
)
|
|
Fixup = @{
|
|
OperandOffset = 0xEA1AB
|
|
OriginalTargetSeg = 130
|
|
OriginalTargetOffset = 0x046F
|
|
PatchedTargetSeg = 117
|
|
PatchedTargetOffset = 0x0086
|
|
}
|
|
}
|
|
CallbackGuardCode = @{
|
|
Label = 'seg1408 break-next dispatch patch'
|
|
Offset = 0xCEAD3
|
|
Original = [byte[]](0x26, 0x8B, 0x1F, 0xFF, 0x1F)
|
|
Patched = [byte[]](0xFF, 0x1E, 0x97, 0x65, 0x90)
|
|
OriginalPatterns = @(
|
|
@('26', '8B', '1F', 'FF', '1F')
|
|
)
|
|
}
|
|
CallbackTargetSlot = @{
|
|
Label = 'break-next private stub target slot'
|
|
Offset = 0xEA197
|
|
Original = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
|
Patched = [byte[]](0xFF, 0xFF, 0x00, 0x00)
|
|
OriginalPatterns = @(
|
|
@('FF', 'FF', '00', '00')
|
|
)
|
|
Fixup = @{
|
|
OperandOffset = 0xEA197
|
|
OriginalTargetSeg = 5
|
|
OriginalTargetOffset = 0x08D2
|
|
PatchedTargetSeg = 131
|
|
PatchedTargetOffset = 0x2318
|
|
}
|
|
}
|
|
InterpreterBreakCall = @{
|
|
Label = 'interpreter debugger break callsite'
|
|
Offset = 0xCFAB5
|
|
Original = [byte[]](0x9A, 0xFF, 0xFF, 0x00, 0x00)
|
|
Patched = [byte[]](0x9A, 0xFF, 0xFF, 0x00, 0x00)
|
|
OriginalPatterns = @(
|
|
@('9A', 'FF', 'FF', '00', '00')
|
|
)
|
|
Fixup = @{
|
|
OperandOffset = 0xCFAB6
|
|
OriginalTargetSeg = 130
|
|
OriginalTargetOffset = 0x0053
|
|
PatchedTargetSeg = 131
|
|
PatchedTargetOffset = 0x232D
|
|
}
|
|
}
|
|
Hook = @{
|
|
Label = 'Rejected direct cheat hook site'
|
|
Offset = 0x70D75
|
|
Original = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
|
Patched = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
|
LegacyBadPatched = [byte[]](0x9A, 0x86, 0x9A, 0x0B, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90)
|
|
OriginalPatterns = @(
|
|
@('68', '03', '01', '9A', 'FF', 'FF', '00', '00', '83', 'C4', '02')
|
|
)
|
|
Fixup = @{
|
|
OperandOffset = 0x70D79
|
|
OriginalTargetSeg = 92
|
|
OriginalTargetOffset = 0x0476
|
|
PatchedTargetSeg = 117
|
|
PatchedTargetOffset = 0x0086
|
|
RetailSourceSegmentIndex = 39
|
|
RetailChainOffset = 0x2B79
|
|
RetailEntryOffset = 0x71D68
|
|
}
|
|
}
|
|
Wrapper = @{
|
|
Label = 'Rejected current-slot wrapper arg site'
|
|
Offset = 0xB9A8D
|
|
Original = [byte[]](0x6A, 0x01, 0xFF, 0x76, 0x08, 0xFF, 0x76, 0x06)
|
|
Patched = [byte[]](0x6A, 0x01, 0x6A, 0x00, 0x6A, 0x00, 0x90, 0x90)
|
|
OriginalPatterns = @(
|
|
@('6A', '01', 'FF', '76', '08', 'FF', '76', '06')
|
|
)
|
|
}
|
|
LegacyDeferredHook = @{
|
|
Label = 'Rejected deferred-event hook site'
|
|
Offset = 0xC99DD
|
|
Original = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
|
Patched = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
|
OriginalPatterns = @(
|
|
@('68', '03', '01', '9A', 'FF', 'FF', '00', '00', '83', 'C4', '02')
|
|
)
|
|
Fixup = @{
|
|
OperandOffset = 0xC99E1
|
|
OriginalTargetSeg = 92
|
|
OriginalTargetOffset = 0x0476
|
|
PatchedTargetSeg = 117
|
|
PatchedTargetOffset = 0x020D
|
|
}
|
|
}
|
|
LegacyDeferredWrapper = @{
|
|
Label = 'Rejected modal wrapper arg site'
|
|
Offset = 0xB9C48
|
|
Original = [byte[]](0x6A, 0x00, 0xFF, 0x76, 0x08, 0xFF, 0x76, 0x06)
|
|
Patched = [byte[]](0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0x90, 0x90)
|
|
OriginalPatterns = @(
|
|
@('6A', '00', 'FF', '76', '08', 'FF', '76', '06')
|
|
)
|
|
}
|
|
}
|
|
|
|
$candidateProfiles = [ordered]@{
|
|
'candidate-o' = @{
|
|
MenuKey = '1'
|
|
Label = 'Candidate O'
|
|
ArmMode = 'interpreter-callsite'
|
|
UiTargetSeg = 117
|
|
UiTargetOffset = 0x020D
|
|
PatchCurrentUnitWrapper = $false
|
|
PatchModalWrapper = $true
|
|
Summary = 'interpreter callsite retarget -> corrected private modal stub with zeroed modal-wrapper args'
|
|
}
|
|
'candidate-p' = @{
|
|
MenuKey = '2'
|
|
Label = 'Candidate P'
|
|
ArmMode = 'interpreter-callsite'
|
|
UiTargetSeg = 117
|
|
UiTargetOffset = 0x0086
|
|
PatchCurrentUnitWrapper = $true
|
|
PatchModalWrapper = $false
|
|
Summary = 'interpreter callsite retarget -> corrected private current-unit stub with zeroed current-slot args'
|
|
}
|
|
}
|
|
|
|
$script:ctrlQPatchedTemplate = [byte[]]$sites.CtrlQDebuggerInit.Patched.Clone()
|
|
$script:configuredProfileKey = 'candidate-o'
|
|
|
|
function Find-ByteSequenceOffset {
|
|
param(
|
|
[byte[]]$Bytes,
|
|
[byte[]]$Pattern
|
|
)
|
|
|
|
$lastStart = $Bytes.Length - $Pattern.Length
|
|
for ($start = 0; $start -le $lastStart; $start++) {
|
|
$matched = $true
|
|
for ($index = 0; $index -lt $Pattern.Length; $index++) {
|
|
if ($Bytes[$start + $index] -ne $Pattern[$index]) {
|
|
$matched = $false
|
|
break
|
|
}
|
|
}
|
|
|
|
if ($matched) {
|
|
return $start
|
|
}
|
|
}
|
|
|
|
throw 'Could not find the private-vtable base immediate inside the Ctrl+Q patch template.'
|
|
}
|
|
|
|
function New-CtrlQPatchedBytes {
|
|
param([string]$ArmMode)
|
|
|
|
switch ($ArmMode) {
|
|
'interpreter-callsite' {
|
|
return [byte[]](
|
|
0xA1, 0x9C, 0x65, 0x0B, 0x06, 0x9E, 0x65, 0x74, 0x10, 0xC4, 0x1E,
|
|
0x9C, 0x65, 0xC6, 0x47, 0x75, 0x00, 0xC6, 0x47, 0x74, 0x01, 0x5F,
|
|
0x5E, 0xC9, 0xCB, 0x6A, 0x00, 0x6A, 0x00, 0xE9, 0x25, 0x00, 0x55,
|
|
0x8B, 0xEC, 0xA1, 0x9C, 0x65, 0x39, 0x46, 0x06, 0x75, 0x16, 0xA1,
|
|
0x9E, 0x65, 0x39, 0x46, 0x08, 0x75, 0x0E, 0xC4, 0x5E, 0x06, 0xC7,
|
|
0x47, 0x74, 0x00, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0xEB, 0x0E, 0x5D,
|
|
0xCB, 0x90, 0x90, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04,
|
|
0xEB, 0x0A, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x04, 0x5D,
|
|
0xCB, 0x0B, 0xC2, 0x74, 0x13, 0xA3, 0x9C, 0x65, 0x89, 0x16, 0x9E,
|
|
0x65, 0x89, 0xC3, 0x8E, 0xC2, 0xC6, 0x47, 0x75, 0x00, 0xC6, 0x47,
|
|
0x74, 0x01, 0x5F, 0x5E, 0xC9, 0xCB
|
|
)
|
|
}
|
|
default {
|
|
throw "Unsupported arm mode '$ArmMode'."
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-ConfiguredCandidateProfile {
|
|
return $candidateProfiles[$script:configuredProfileKey]
|
|
}
|
|
|
|
function Set-ConfiguredCandidateProfile {
|
|
param([string]$ProfileKey)
|
|
|
|
if (-not $candidateProfiles.Contains($ProfileKey)) {
|
|
throw "Unknown candidate profile '$ProfileKey'."
|
|
}
|
|
|
|
$profile = $candidateProfiles[$ProfileKey]
|
|
$sites.CtrlQDebuggerInit.Patched = New-CtrlQPatchedBytes -ArmMode ([string]$profile.ArmMode)
|
|
$sites.DebuggerCallback.Fixup.PatchedTargetSeg = [int]$profile.UiTargetSeg
|
|
$sites.DebuggerCallback.Fixup.PatchedTargetOffset = [int]$profile.UiTargetOffset
|
|
$script:configuredProfileKey = $ProfileKey
|
|
}
|
|
|
|
function Test-CandidateApplied {
|
|
param(
|
|
[byte[]]$FileBytes,
|
|
[string]$ProfileKey
|
|
)
|
|
|
|
$previousProfileKey = $script:configuredProfileKey
|
|
try {
|
|
Set-ConfiguredCandidateProfile -ProfileKey $ProfileKey
|
|
return (
|
|
(Get-SiteState -FileBytes $FileBytes -Site $sites.CtrlQDebuggerInit) -eq 'Patched' -and
|
|
(Get-SiteState -FileBytes $FileBytes -Site $sites.InterpreterBreakCall) -eq 'Patched' -and
|
|
(Get-SiteState -FileBytes $FileBytes -Site $sites.DebuggerCallback) -eq 'Patched' -and
|
|
(Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackGuardCode) -eq 'Original' -and
|
|
(Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackTargetSlot) -eq 'Original' -and
|
|
(Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper) -eq $(if ($candidateProfiles[$ProfileKey].PatchCurrentUnitWrapper) { 'Patched' } else { 'Original' }) -and
|
|
(Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredWrapper) -eq $(if ($candidateProfiles[$ProfileKey].PatchModalWrapper) { 'Patched' } else { 'Original' })
|
|
)
|
|
}
|
|
finally {
|
|
Set-ConfiguredCandidateProfile -ProfileKey $previousProfileKey
|
|
}
|
|
}
|
|
|
|
function Get-AppliedCandidateProfileKey {
|
|
param([byte[]]$FileBytes)
|
|
|
|
foreach ($profileKey in $candidateProfiles.Keys) {
|
|
if (Test-CandidateApplied -FileBytes $FileBytes -ProfileKey $profileKey) {
|
|
return $profileKey
|
|
}
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
Set-ConfiguredCandidateProfile -ProfileKey $script:configuredProfileKey
|
|
|
|
function Format-HexBytes {
|
|
param([byte[]]$Bytes)
|
|
|
|
return (($Bytes | ForEach-Object { $_.ToString('X2') }) -join ' ')
|
|
}
|
|
|
|
function Get-ByteSlice {
|
|
param(
|
|
[byte[]]$Bytes,
|
|
[int]$Offset,
|
|
[int]$Count
|
|
)
|
|
|
|
if ($Offset -lt 0 -or ($Offset + $Count) -gt $Bytes.Length) {
|
|
throw "Requested byte range 0x{0:X}-0x{1:X} is outside the file." -f $Offset, ($Offset + $Count - 1)
|
|
}
|
|
|
|
$slice = New-Object byte[] $Count
|
|
[Array]::Copy($Bytes, $Offset, $slice, 0, $Count)
|
|
return $slice
|
|
}
|
|
|
|
function Get-U16Le {
|
|
param(
|
|
[byte[]]$Bytes,
|
|
[int]$Offset
|
|
)
|
|
|
|
return [BitConverter]::ToUInt16($Bytes, $Offset)
|
|
}
|
|
|
|
function Get-U32Le {
|
|
param(
|
|
[byte[]]$Bytes,
|
|
[int]$Offset
|
|
)
|
|
|
|
return [BitConverter]::ToUInt32($Bytes, $Offset)
|
|
}
|
|
|
|
function Set-U16Le {
|
|
param(
|
|
[byte[]]$Bytes,
|
|
[int]$Offset,
|
|
[int]$Value
|
|
)
|
|
|
|
$raw = [BitConverter]::GetBytes([UInt16]$Value)
|
|
[Array]::Copy($raw, 0, $Bytes, $Offset, 2)
|
|
}
|
|
|
|
function Test-ByteArrayEqual {
|
|
param(
|
|
[byte[]]$Left,
|
|
[byte[]]$Right
|
|
)
|
|
|
|
if ($Left.Length -ne $Right.Length) {
|
|
return $false
|
|
}
|
|
|
|
for ($i = 0; $i -lt $Left.Length; $i++) {
|
|
if ($Left[$i] -ne $Right[$i]) {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
function Test-BytePatternMatch {
|
|
param(
|
|
[byte[]]$Bytes,
|
|
[object[]]$Pattern
|
|
)
|
|
|
|
if ($Bytes.Length -ne $Pattern.Length) {
|
|
return $false
|
|
}
|
|
|
|
for ($i = 0; $i -lt $Bytes.Length; $i++) {
|
|
$expected = [string]$Pattern[$i]
|
|
if ($expected -eq '??') {
|
|
continue
|
|
}
|
|
|
|
if ($Bytes[$i] -ne [Convert]::ToByte($expected, 16)) {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
function Convert-BytesToStoredHex {
|
|
param([byte[]]$Bytes)
|
|
|
|
return (($Bytes | ForEach-Object { $_.ToString('X2') }) -join '')
|
|
}
|
|
|
|
function Convert-StoredHexToBytes {
|
|
param([string]$Hex)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Hex)) {
|
|
return $null
|
|
}
|
|
|
|
$normalized = $Hex.Replace(' ', '')
|
|
if (($normalized.Length % 2) -ne 0) {
|
|
throw "Invalid stored hex string length: $Hex"
|
|
}
|
|
|
|
$result = New-Object byte[] ($normalized.Length / 2)
|
|
for ($i = 0; $i -lt $result.Length; $i++) {
|
|
$result[$i] = [Convert]::ToByte($normalized.Substring($i * 2, 2), 16)
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function Get-HookFixupInfo {
|
|
param(
|
|
[byte[]]$FileBytes,
|
|
[hashtable]$Site
|
|
)
|
|
|
|
$neHeaderOffset = [int](Get-U32Le -Bytes $FileBytes -Offset 0x3C)
|
|
$neSignature = Get-ByteSlice -Bytes $FileBytes -Offset $neHeaderOffset -Count 2
|
|
if ($neSignature[0] -ne 0x4E -or $neSignature[1] -ne 0x45) {
|
|
throw 'Executable does not contain a valid NE header at e_lfanew.'
|
|
}
|
|
|
|
$numSegments = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x1C))
|
|
$segmentTableOffset = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x22)) + $neHeaderOffset
|
|
$alignmentShift = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x32))
|
|
|
|
$operandFileOffset = [int]$Site.Fixup.OperandOffset
|
|
$segmentInfoByIndex = @{}
|
|
$sourceSegment = $null
|
|
for ($index = 1; $index -le $numSegments; $index++) {
|
|
$segmentEntryOffset = $segmentTableOffset + (($index - 1) * 8)
|
|
$sectorOffset = [int](Get-U16Le -Bytes $FileBytes -Offset $segmentEntryOffset)
|
|
if ($sectorOffset -eq 0) {
|
|
continue
|
|
}
|
|
|
|
$segmentLength = [int](Get-U16Le -Bytes $FileBytes -Offset ($segmentEntryOffset + 2))
|
|
if ($segmentLength -eq 0) {
|
|
$segmentLength = 0x10000
|
|
}
|
|
|
|
$segmentFlags = [int](Get-U16Le -Bytes $FileBytes -Offset ($segmentEntryOffset + 4))
|
|
$segmentFileOffset = [int]($sectorOffset -shl $alignmentShift)
|
|
$segmentInfo = @{
|
|
SegmentIndex = $index
|
|
SegmentEntryOffset = $segmentEntryOffset
|
|
SegmentFileOffset = $segmentFileOffset
|
|
SegmentLength = $segmentLength
|
|
SegmentFlags = $segmentFlags
|
|
HasReloc = (($segmentFlags -band 0x0100) -ne 0)
|
|
}
|
|
$segmentInfoByIndex[$index] = $segmentInfo
|
|
|
|
if ($segmentFileOffset -le $operandFileOffset -and $operandFileOffset -lt ($segmentFileOffset + $segmentLength)) {
|
|
$sourceSegment = $segmentInfo
|
|
break
|
|
}
|
|
}
|
|
|
|
if ($null -eq $sourceSegment) {
|
|
$fallbackSegmentIndex = [int]$Site.Fixup.RetailSourceSegmentIndex
|
|
$fallbackChainOffset = [int]$Site.Fixup.RetailChainOffset
|
|
if ($segmentInfoByIndex.ContainsKey($fallbackSegmentIndex)) {
|
|
$fallbackSegment = $segmentInfoByIndex[$fallbackSegmentIndex]
|
|
if (($fallbackSegment.SegmentFileOffset + $fallbackChainOffset) -eq $operandFileOffset) {
|
|
$sourceSegment = $fallbackSegment
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($null -eq $sourceSegment) {
|
|
throw ("Could not locate the hook operand file offset 0x{0:X} in any NE code segment." -f $operandFileOffset)
|
|
}
|
|
|
|
if (-not $sourceSegment.HasReloc) {
|
|
throw 'Hook source segment does not advertise a relocation table.'
|
|
}
|
|
|
|
$segmentIndex = [int]$sourceSegment.SegmentIndex
|
|
$segmentFileOffset = [int]$sourceSegment.SegmentFileOffset
|
|
$segmentLength = [int]$sourceSegment.SegmentLength
|
|
$relocTableOffset = $segmentFileOffset + $segmentLength
|
|
$relocCount = [int](Get-U16Le -Bytes $FileBytes -Offset $relocTableOffset)
|
|
$entryOffset = $relocTableOffset + 2
|
|
$chainOffset = $operandFileOffset - $segmentFileOffset
|
|
|
|
for ($index = 0; $index -lt $relocCount; $index++) {
|
|
$addrType = $FileBytes[$entryOffset]
|
|
$relType = $FileBytes[$entryOffset + 1]
|
|
$entryChainOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 2)
|
|
if ($entryChainOffset -eq $chainOffset) {
|
|
return @{
|
|
SegmentIndex = $segmentIndex
|
|
SegmentFileOffset = $segmentFileOffset
|
|
RelocTableOffset = $relocTableOffset
|
|
EntryOffset = $entryOffset
|
|
EntryIndex = $index
|
|
AddrType = $addrType
|
|
RelType = $relType
|
|
ChainOffset = $entryChainOffset
|
|
TargetSeg = $FileBytes[$entryOffset + 4]
|
|
Reserved = $FileBytes[$entryOffset + 5]
|
|
TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 6)
|
|
}
|
|
}
|
|
|
|
if ($addrType -eq 3 -and (($relType -band 0x03) -eq 0)) {
|
|
$visited = New-Object 'System.Collections.Generic.HashSet[int]'
|
|
$currentChainOffset = $entryChainOffset
|
|
while ($currentChainOffset -ne 0xFFFF -and $currentChainOffset -lt $segmentLength) {
|
|
if (-not $visited.Add($currentChainOffset)) {
|
|
break
|
|
}
|
|
|
|
if ($currentChainOffset -eq $chainOffset) {
|
|
return @{
|
|
SegmentIndex = $segmentIndex
|
|
SegmentFileOffset = $segmentFileOffset
|
|
RelocTableOffset = $relocTableOffset
|
|
EntryOffset = $entryOffset
|
|
EntryIndex = $index
|
|
AddrType = $addrType
|
|
RelType = $relType
|
|
ChainOffset = $entryChainOffset
|
|
TargetSeg = $FileBytes[$entryOffset + 4]
|
|
Reserved = $FileBytes[$entryOffset + 5]
|
|
TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 6)
|
|
}
|
|
}
|
|
|
|
$currentChainOffset = Get-U16Le -Bytes $FileBytes -Offset ($segmentFileOffset + $currentChainOffset)
|
|
}
|
|
}
|
|
|
|
$entryOffset += 8
|
|
}
|
|
|
|
$fallbackEntryOffset = [int]$Site.Fixup.RetailEntryOffset
|
|
$fallbackChainOffset = [int]$Site.Fixup.RetailChainOffset
|
|
if ($fallbackEntryOffset -ge 0 -and ($fallbackEntryOffset + 7) -lt $FileBytes.Length) {
|
|
$addrType = $FileBytes[$fallbackEntryOffset]
|
|
$relType = $FileBytes[$fallbackEntryOffset + 1]
|
|
$entryChainOffset = [int](Get-U16Le -Bytes $FileBytes -Offset ($fallbackEntryOffset + 2))
|
|
if (
|
|
$sourceSegment.SegmentIndex -eq [int]$Site.Fixup.RetailSourceSegmentIndex -and
|
|
$chainOffset -eq $fallbackChainOffset -and
|
|
$addrType -eq 3 -and
|
|
(($relType -band 0x03) -eq 0) -and
|
|
$entryChainOffset -eq $fallbackChainOffset
|
|
) {
|
|
return @{
|
|
SegmentIndex = $segmentIndex
|
|
SegmentFileOffset = $segmentFileOffset
|
|
RelocTableOffset = $relocTableOffset
|
|
EntryOffset = $fallbackEntryOffset
|
|
EntryIndex = -1
|
|
AddrType = $addrType
|
|
RelType = $relType
|
|
ChainOffset = $entryChainOffset
|
|
TargetSeg = $FileBytes[$fallbackEntryOffset + 4]
|
|
Reserved = $FileBytes[$fallbackEntryOffset + 5]
|
|
TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($fallbackEntryOffset + 6)
|
|
}
|
|
}
|
|
}
|
|
|
|
throw ("Could not find hook relocation entry for segment {0} chain offset 0x{1:X}." -f $segmentIndex, $chainOffset)
|
|
}
|
|
|
|
function Get-StateData {
|
|
if (-not (Test-Path -LiteralPath $statePath)) {
|
|
return @{}
|
|
}
|
|
|
|
$raw = Get-Content -LiteralPath $statePath -Raw
|
|
if ([string]::IsNullOrWhiteSpace($raw)) {
|
|
return @{}
|
|
}
|
|
|
|
$parsed = ConvertFrom-Json -InputObject $raw -AsHashtable
|
|
if ($null -eq $parsed) {
|
|
return @{}
|
|
}
|
|
|
|
return $parsed
|
|
}
|
|
|
|
function Save-StateData {
|
|
param([hashtable]$StateData)
|
|
|
|
$json = ConvertTo-Json -InputObject $StateData -Depth 4
|
|
Set-Content -LiteralPath $statePath -Value $json -Encoding ASCII
|
|
}
|
|
|
|
function Get-SavedOriginalBytes {
|
|
param(
|
|
[hashtable]$StateData,
|
|
[string]$SiteKey,
|
|
[int]$SiteOffset
|
|
)
|
|
|
|
if (-not $StateData.ContainsKey($SiteKey)) {
|
|
return $null
|
|
}
|
|
|
|
$siteState = $StateData[$SiteKey]
|
|
if ($siteState -isnot [hashtable]) {
|
|
return $null
|
|
}
|
|
|
|
if (-not $siteState.ContainsKey('OriginalBytes')) {
|
|
return $null
|
|
}
|
|
|
|
if ($siteState.ContainsKey('Offset') -and ([int]$siteState.Offset -ne $SiteOffset)) {
|
|
return $null
|
|
}
|
|
|
|
return Convert-StoredHexToBytes -Hex ([string]$siteState.OriginalBytes)
|
|
}
|
|
|
|
function Save-OriginalBytesIfMissing {
|
|
param(
|
|
[hashtable]$StateData,
|
|
[string]$SiteKey,
|
|
[int]$SiteOffset,
|
|
[byte[]]$Bytes
|
|
)
|
|
|
|
if ($StateData.ContainsKey($SiteKey) -and ($StateData[$SiteKey] -is [hashtable])) {
|
|
$existing = $StateData[$SiteKey]
|
|
if ($existing.ContainsKey('OriginalBytes') -and $existing.ContainsKey('Offset') -and ([int]$existing.Offset -eq $SiteOffset)) {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
$StateData[$SiteKey] = @{
|
|
Offset = $SiteOffset
|
|
OriginalBytes = Convert-BytesToStoredHex -Bytes $Bytes
|
|
}
|
|
return $true
|
|
}
|
|
|
|
function Get-SiteState {
|
|
param(
|
|
[byte[]]$FileBytes,
|
|
[hashtable]$Site
|
|
)
|
|
|
|
$current = Get-ByteSlice -Bytes $FileBytes -Offset $Site.Offset -Count $Site.Original.Length
|
|
if ($Site.ContainsKey('Fixup')) {
|
|
$fixupInfo = Get-HookFixupInfo -FileBytes $FileBytes -Site $Site
|
|
$isFixupOriginal = ($fixupInfo.TargetSeg -eq $Site.Fixup.OriginalTargetSeg) -and ($fixupInfo.TargetOffset -eq $Site.Fixup.OriginalTargetOffset)
|
|
$isFixupPatched = ($fixupInfo.TargetSeg -eq $Site.Fixup.PatchedTargetSeg) -and ($fixupInfo.TargetOffset -eq $Site.Fixup.PatchedTargetOffset)
|
|
|
|
if ($Site.ContainsKey('LegacyBadPatched') -and (Test-ByteArrayEqual -Left $current -Right $Site.LegacyBadPatched) -and $isFixupOriginal) {
|
|
return 'LegacyBadPatch'
|
|
}
|
|
|
|
if ((Test-ByteArrayEqual -Left $current -Right $Site.Patched) -and $isFixupPatched) {
|
|
return 'Patched'
|
|
}
|
|
|
|
if ((Test-ByteArrayEqual -Left $current -Right $Site.Original) -and $isFixupOriginal) {
|
|
return 'Original'
|
|
}
|
|
|
|
foreach ($pattern in $Site.OriginalPatterns) {
|
|
if ((Test-BytePatternMatch -Bytes $current -Pattern $pattern) -and $isFixupOriginal) {
|
|
return 'Original'
|
|
}
|
|
}
|
|
|
|
$legacyCandidates = @()
|
|
if ($Site.ContainsKey('LegacyPatched')) {
|
|
$legacyCandidates += ,([byte[]]$Site.LegacyPatched)
|
|
}
|
|
if ($Site.ContainsKey('LegacyPatchedVariants')) {
|
|
$legacyCandidates += $Site.LegacyPatchedVariants
|
|
}
|
|
|
|
foreach ($legacyCandidate in $legacyCandidates) {
|
|
if (Test-ByteArrayEqual -Left $current -Right $legacyCandidate) {
|
|
$legacyFixupState = if ($Site.ContainsKey('LegacyPatchedFixupState')) { [string]$Site.LegacyPatchedFixupState } else { 'Patched' }
|
|
if (
|
|
($legacyFixupState -eq 'Patched' -and $isFixupPatched) -or
|
|
($legacyFixupState -eq 'Original' -and $isFixupOriginal) -or
|
|
($legacyFixupState -eq 'Any' -and ($isFixupPatched -or $isFixupOriginal))
|
|
) {
|
|
return 'LegacyPatched'
|
|
}
|
|
}
|
|
}
|
|
|
|
return 'Unknown'
|
|
}
|
|
|
|
if (Test-ByteArrayEqual -Left $current -Right $Site.Patched) {
|
|
return 'Patched'
|
|
}
|
|
|
|
if (Test-ByteArrayEqual -Left $current -Right $Site.Original) {
|
|
return 'Original'
|
|
}
|
|
|
|
foreach ($pattern in $Site.OriginalPatterns) {
|
|
if (Test-BytePatternMatch -Bytes $current -Pattern $pattern) {
|
|
return 'Original'
|
|
}
|
|
}
|
|
|
|
return 'Unknown'
|
|
}
|
|
|
|
function Assert-SiteStateKnown {
|
|
param(
|
|
[byte[]]$FileBytes,
|
|
[hashtable]$Site
|
|
)
|
|
|
|
$state = Get-SiteState -FileBytes $FileBytes -Site $Site
|
|
if ($state -eq 'Unknown') {
|
|
$current = Get-ByteSlice -Bytes $FileBytes -Offset $Site.Offset -Count $Site.Original.Length
|
|
throw (
|
|
"{0} at file offset 0x{1:X} does not match either expected byte sequence.`nCurrent : {2}`nOriginal: {3}`nPatched : {4}`n`nRefusing to modify an unknown executable state." -f
|
|
$Site.Label,
|
|
$Site.Offset,
|
|
(Format-HexBytes -Bytes $current),
|
|
(Format-HexBytes -Bytes $Site.Original),
|
|
(Format-HexBytes -Bytes $Site.Patched)
|
|
)
|
|
}
|
|
}
|
|
|
|
function Set-ByteSlice {
|
|
param(
|
|
[byte[]]$Bytes,
|
|
[int]$Offset,
|
|
[byte[]]$Value
|
|
)
|
|
|
|
[Array]::Copy($Value, 0, $Bytes, $Offset, $Value.Length)
|
|
}
|
|
|
|
function Show-Status {
|
|
param([byte[]]$FileBytes)
|
|
|
|
$selectedProfile = Get-ConfiguredCandidateProfile
|
|
$appliedProfileKey = Get-AppliedCandidateProfileKey -FileBytes $FileBytes
|
|
$appliedProfile = if ($null -ne $appliedProfileKey) { $candidateProfiles[$appliedProfileKey] } else { $null }
|
|
|
|
$ctrlQState = Get-SiteState -FileBytes $FileBytes -Site $sites.CtrlQDebuggerInit
|
|
$interpreterCallState = Get-SiteState -FileBytes $FileBytes -Site $sites.InterpreterBreakCall
|
|
$privateDispatchState = Get-SiteState -FileBytes $FileBytes -Site $sites.DebuggerCallback
|
|
$callbackGuardState = Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackGuardCode
|
|
$callbackTargetState = Get-SiteState -FileBytes $FileBytes -Site $sites.CallbackTargetSlot
|
|
$hookState = Get-SiteState -FileBytes $FileBytes -Site $sites.Hook
|
|
$wrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper
|
|
$deferredHookState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredHook
|
|
$deferredWrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredWrapper
|
|
|
|
Write-Host ''
|
|
Write-Host 'CRUSADER.EXE patch status'
|
|
Write-Host '------------------------'
|
|
Write-Host ("EXE: {0}" -f $exePath)
|
|
Write-Host ("Selected candidate : {0} ({1})" -f $selectedProfile.Label, $selectedProfile.Summary)
|
|
if ($null -ne $appliedProfile) {
|
|
Write-Host ("Applied candidate : {0} ({1})" -f $appliedProfile.Label, $appliedProfile.Summary)
|
|
}
|
|
elseif ($ctrlQState -eq 'Original') {
|
|
Write-Host 'Applied candidate : Retail/original'
|
|
}
|
|
else {
|
|
Write-Host 'Applied candidate : Unknown'
|
|
}
|
|
Write-Host ("0x410 init body @ 0x{0:X}: {1}" -f $sites.CtrlQDebuggerInit.Offset, $ctrlQState)
|
|
Write-Host ("Interpreter call @ 0x{0:X}: {1}" -f $sites.InterpreterBreakCall.Offset, $interpreterCallState)
|
|
Write-Host ("Private UI call @ 0x{0:X}: {1}" -f $sites.DebuggerCallback.Offset, $privateDispatchState)
|
|
Write-Host ("Legacy break hook @ 0x{0:X}: {1}" -f $sites.CallbackGuardCode.Offset, $callbackGuardState)
|
|
Write-Host ("Legacy target slot @ 0x{0:X}: {1}" -f $sites.CallbackTargetSlot.Offset, $callbackTargetState)
|
|
Write-Host ("Current-unit args @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
|
Write-Host ("Modal wrapper args @ 0x{0:X}: {1}" -f $sites.LegacyDeferredWrapper.Offset, $deferredWrapperState)
|
|
Write-Host ("Deferred hook cleanup @ 0x{0:X}: {1}" -f $sites.LegacyDeferredHook.Offset, $deferredHookState)
|
|
Write-Host ("Direct hook cleanup @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
|
Write-Host ''
|
|
foreach ($profileKey in $candidateProfiles.Keys) {
|
|
$profile = $candidateProfiles[$profileKey]
|
|
$marker = if ($appliedProfileKey -eq $profileKey) { 'applied' } else { 'ready' }
|
|
Write-Host ("{0}. Apply {1} [{2}] {3}" -f $profile.MenuKey, $profile.Label, $marker, $profile.Summary)
|
|
}
|
|
Write-Host '3. Restore original bytes'
|
|
Write-Host '4. Exit'
|
|
Write-Host ''
|
|
}
|
|
|
|
function Set-DesiredState {
|
|
param(
|
|
[bool]$CtrlQPatched,
|
|
[string]$Label,
|
|
[string]$ProfileKey
|
|
)
|
|
|
|
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
|
|
|
|
$appliedProfileKey = Get-AppliedCandidateProfileKey -FileBytes $fileBytes
|
|
$profileKeyForStateCheck = if ($null -ne $appliedProfileKey) { $appliedProfileKey } elseif ($CtrlQPatched) { $ProfileKey } else { $script:configuredProfileKey }
|
|
$profileKeyForWrite = if ($CtrlQPatched) { $ProfileKey } else { $profileKeyForStateCheck }
|
|
|
|
if ([string]::IsNullOrWhiteSpace($profileKeyForStateCheck)) {
|
|
throw 'No candidate profile is available for state validation.'
|
|
}
|
|
|
|
Set-ConfiguredCandidateProfile -ProfileKey $profileKeyForStateCheck
|
|
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.CtrlQDebuggerInit
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.InterpreterBreakCall
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.DebuggerCallback
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.CallbackGuardCode
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.CallbackTargetSlot
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod0
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod1
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyBreakpointCallback
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.Hook
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.Wrapper
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyDeferredHook
|
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyDeferredWrapper
|
|
|
|
Set-ConfiguredCandidateProfile -ProfileKey $profileKeyForWrite
|
|
$activeProfile = Get-ConfiguredCandidateProfile
|
|
|
|
$ctrlQFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.CtrlQDebuggerInit
|
|
$interpreterBreakCallFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.InterpreterBreakCall
|
|
$debuggerCallbackFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.DebuggerCallback
|
|
$callbackTargetFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.CallbackTargetSlot
|
|
$privateMethod0FixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod0
|
|
$privateMethod1FixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.PrivateBreakpointMethod1
|
|
$legacyCallbackFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.LegacyBreakpointCallback
|
|
$hookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.Hook
|
|
$legacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.LegacyDeferredHook
|
|
$ctrlQBytes = if ($CtrlQPatched) { $sites.CtrlQDebuggerInit.Patched } else { $sites.CtrlQDebuggerInit.Original }
|
|
$ctrlQTargetSeg = if ($CtrlQPatched) { [int]$sites.CtrlQDebuggerInit.Fixup.PatchedTargetSeg } else { [int]$sites.CtrlQDebuggerInit.Fixup.OriginalTargetSeg }
|
|
$ctrlQTargetOffset = if ($CtrlQPatched) { [int]$sites.CtrlQDebuggerInit.Fixup.PatchedTargetOffset } else { [int]$sites.CtrlQDebuggerInit.Fixup.OriginalTargetOffset }
|
|
$interpreterBreakCallBytes = $sites.InterpreterBreakCall.Original
|
|
$interpreterBreakCallSeg = [int]$sites.InterpreterBreakCall.Fixup.OriginalTargetSeg
|
|
$interpreterBreakCallOffset = [int]$sites.InterpreterBreakCall.Fixup.OriginalTargetOffset
|
|
if ($CtrlQPatched) {
|
|
$interpreterBreakCallBytes = $sites.InterpreterBreakCall.Patched
|
|
$interpreterBreakCallSeg = [int]$sites.InterpreterBreakCall.Fixup.PatchedTargetSeg
|
|
$interpreterBreakCallOffset = [int]$sites.InterpreterBreakCall.Fixup.PatchedTargetOffset
|
|
}
|
|
$debuggerCallbackBytes = $sites.DebuggerCallback.Original
|
|
$debuggerCallbackSeg = [int]$sites.DebuggerCallback.Fixup.OriginalTargetSeg
|
|
$debuggerCallbackOffset = [int]$sites.DebuggerCallback.Fixup.OriginalTargetOffset
|
|
if ($CtrlQPatched) {
|
|
$debuggerCallbackBytes = $sites.DebuggerCallback.Patched
|
|
$debuggerCallbackSeg = [int]$sites.DebuggerCallback.Fixup.PatchedTargetSeg
|
|
$debuggerCallbackOffset = [int]$sites.DebuggerCallback.Fixup.PatchedTargetOffset
|
|
}
|
|
$callbackGuardBytes = $sites.CallbackGuardCode.Original
|
|
$callbackTargetBytes = $sites.CallbackTargetSlot.Original
|
|
$callbackTargetSeg = [int]$sites.CallbackTargetSlot.Fixup.OriginalTargetSeg
|
|
$callbackTargetOffset = [int]$sites.CallbackTargetSlot.Fixup.OriginalTargetOffset
|
|
$privateMethod0Bytes = $sites.PrivateBreakpointMethod0.Original
|
|
$privateMethod0TargetSeg = [int]$sites.PrivateBreakpointMethod0.Fixup.OriginalTargetSeg
|
|
$privateMethod0TargetOffset = [int]$sites.PrivateBreakpointMethod0.Fixup.OriginalTargetOffset
|
|
$privateMethod1Bytes = $sites.PrivateBreakpointMethod1.Original
|
|
$privateMethod1TargetSeg = [int]$sites.PrivateBreakpointMethod1.Fixup.OriginalTargetSeg
|
|
$privateMethod1TargetOffset = [int]$sites.PrivateBreakpointMethod1.Fixup.OriginalTargetOffset
|
|
$legacyCallbackBytes = $sites.LegacyBreakpointCallback.Original
|
|
$legacyCallbackTargetSeg = [int]$sites.LegacyBreakpointCallback.Fixup.OriginalTargetSeg
|
|
$legacyCallbackTargetOffset = [int]$sites.LegacyBreakpointCallback.Fixup.OriginalTargetOffset
|
|
$hookBytes = $sites.Hook.Original
|
|
$hookTargetSeg = [int]$sites.Hook.Fixup.OriginalTargetSeg
|
|
$hookTargetOffset = [int]$sites.Hook.Fixup.OriginalTargetOffset
|
|
$hookReserved = 0
|
|
$wrapperBytes = if ($CtrlQPatched -and $activeProfile.PatchCurrentUnitWrapper) { $sites.Wrapper.Patched } else { $sites.Wrapper.Original }
|
|
$legacyDeferredHookBytes = $sites.LegacyDeferredHook.Original
|
|
$legacyDeferredHookTargetSeg = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetSeg
|
|
$legacyDeferredHookTargetOffset = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetOffset
|
|
$legacyDeferredHookReserved = 0
|
|
$legacyDeferredWrapperBytes = if ($CtrlQPatched -and $activeProfile.PatchModalWrapper) { $sites.LegacyDeferredWrapper.Patched } else { $sites.LegacyDeferredWrapper.Original }
|
|
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.CtrlQDebuggerInit.Offset -Value $ctrlQBytes
|
|
$fileBytes[$ctrlQFixupInfo.EntryOffset + 4] = [byte]$ctrlQTargetSeg
|
|
$fileBytes[$ctrlQFixupInfo.EntryOffset + 5] = 0
|
|
Set-U16Le -Bytes $fileBytes -Offset ($ctrlQFixupInfo.EntryOffset + 6) -Value $ctrlQTargetOffset
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.InterpreterBreakCall.Offset -Value $interpreterBreakCallBytes
|
|
$fileBytes[$interpreterBreakCallFixupInfo.EntryOffset + 4] = [byte]$interpreterBreakCallSeg
|
|
$fileBytes[$interpreterBreakCallFixupInfo.EntryOffset + 5] = 0
|
|
Set-U16Le -Bytes $fileBytes -Offset ($interpreterBreakCallFixupInfo.EntryOffset + 6) -Value $interpreterBreakCallOffset
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.DebuggerCallback.Offset -Value $debuggerCallbackBytes
|
|
$fileBytes[$debuggerCallbackFixupInfo.EntryOffset + 4] = [byte]$debuggerCallbackSeg
|
|
$fileBytes[$debuggerCallbackFixupInfo.EntryOffset + 5] = 0
|
|
Set-U16Le -Bytes $fileBytes -Offset ($debuggerCallbackFixupInfo.EntryOffset + 6) -Value $debuggerCallbackOffset
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.CallbackGuardCode.Offset -Value $callbackGuardBytes
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.CallbackTargetSlot.Offset -Value $callbackTargetBytes
|
|
$fileBytes[$callbackTargetFixupInfo.EntryOffset + 4] = [byte]$callbackTargetSeg
|
|
$fileBytes[$callbackTargetFixupInfo.EntryOffset + 5] = 0
|
|
Set-U16Le -Bytes $fileBytes -Offset ($callbackTargetFixupInfo.EntryOffset + 6) -Value $callbackTargetOffset
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.PrivateBreakpointMethod0.Offset -Value $privateMethod0Bytes
|
|
$fileBytes[$privateMethod0FixupInfo.EntryOffset + 4] = [byte]$privateMethod0TargetSeg
|
|
$fileBytes[$privateMethod0FixupInfo.EntryOffset + 5] = 0
|
|
Set-U16Le -Bytes $fileBytes -Offset ($privateMethod0FixupInfo.EntryOffset + 6) -Value $privateMethod0TargetOffset
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.PrivateBreakpointMethod1.Offset -Value $privateMethod1Bytes
|
|
$fileBytes[$privateMethod1FixupInfo.EntryOffset + 4] = [byte]$privateMethod1TargetSeg
|
|
$fileBytes[$privateMethod1FixupInfo.EntryOffset + 5] = 0
|
|
Set-U16Le -Bytes $fileBytes -Offset ($privateMethod1FixupInfo.EntryOffset + 6) -Value $privateMethod1TargetOffset
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.LegacyBreakpointCallback.Offset -Value $legacyCallbackBytes
|
|
$fileBytes[$legacyCallbackFixupInfo.EntryOffset + 4] = [byte]$legacyCallbackTargetSeg
|
|
$fileBytes[$legacyCallbackFixupInfo.EntryOffset + 5] = 0
|
|
Set-U16Le -Bytes $fileBytes -Offset ($legacyCallbackFixupInfo.EntryOffset + 6) -Value $legacyCallbackTargetOffset
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.Hook.Offset -Value $hookBytes
|
|
$fileBytes[$hookFixupInfo.EntryOffset + 4] = [byte]$hookTargetSeg
|
|
$fileBytes[$hookFixupInfo.EntryOffset + 5] = [byte]$hookReserved
|
|
Set-U16Le -Bytes $fileBytes -Offset ($hookFixupInfo.EntryOffset + 6) -Value $hookTargetOffset
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.Wrapper.Offset -Value $wrapperBytes
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.LegacyDeferredHook.Offset -Value $legacyDeferredHookBytes
|
|
$fileBytes[$legacyDeferredHookFixupInfo.EntryOffset + 4] = [byte]$legacyDeferredHookTargetSeg
|
|
$fileBytes[$legacyDeferredHookFixupInfo.EntryOffset + 5] = [byte]$legacyDeferredHookReserved
|
|
Set-U16Le -Bytes $fileBytes -Offset ($legacyDeferredHookFixupInfo.EntryOffset + 6) -Value $legacyDeferredHookTargetOffset
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.LegacyDeferredWrapper.Offset -Value $legacyDeferredWrapperBytes
|
|
|
|
[System.IO.File]::WriteAllBytes($exePath, $fileBytes)
|
|
|
|
$verifyBytes = [System.IO.File]::ReadAllBytes($exePath)
|
|
$ctrlQState = Get-SiteState -FileBytes $verifyBytes -Site $sites.CtrlQDebuggerInit
|
|
$interpreterCallState = Get-SiteState -FileBytes $verifyBytes -Site $sites.InterpreterBreakCall
|
|
$privateDispatchState = Get-SiteState -FileBytes $verifyBytes -Site $sites.DebuggerCallback
|
|
$callbackGuardState = Get-SiteState -FileBytes $verifyBytes -Site $sites.CallbackGuardCode
|
|
$callbackTargetState = Get-SiteState -FileBytes $verifyBytes -Site $sites.CallbackTargetSlot
|
|
$hookState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Hook
|
|
$wrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Wrapper
|
|
$deferredHookState = Get-SiteState -FileBytes $verifyBytes -Site $sites.LegacyDeferredHook
|
|
$deferredWrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites.LegacyDeferredWrapper
|
|
$verifiedCtrlQFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.CtrlQDebuggerInit
|
|
$verifiedInterpreterBreakCallFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.InterpreterBreakCall
|
|
$verifiedDebuggerCallbackFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.DebuggerCallback
|
|
$verifiedCallbackTargetFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.CallbackTargetSlot
|
|
$verifiedPrivateMethod0FixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.PrivateBreakpointMethod0
|
|
$verifiedPrivateMethod1FixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.PrivateBreakpointMethod1
|
|
$verifiedLegacyCallbackFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.LegacyBreakpointCallback
|
|
$verifiedHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.Hook
|
|
$verifiedLegacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.LegacyDeferredHook
|
|
$verifiedCtrlQBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.CtrlQDebuggerInit.Offset -Count $ctrlQBytes.Length
|
|
$verifiedInterpreterBreakCallBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.InterpreterBreakCall.Offset -Count $interpreterBreakCallBytes.Length
|
|
$verifiedDebuggerCallbackBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.DebuggerCallback.Offset -Count $debuggerCallbackBytes.Length
|
|
$verifiedCallbackGuardBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.CallbackGuardCode.Offset -Count $callbackGuardBytes.Length
|
|
$verifiedCallbackTargetBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.CallbackTargetSlot.Offset -Count $callbackTargetBytes.Length
|
|
$verifiedPrivateMethod0Bytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.PrivateBreakpointMethod0.Offset -Count $privateMethod0Bytes.Length
|
|
$verifiedPrivateMethod1Bytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.PrivateBreakpointMethod1.Offset -Count $privateMethod1Bytes.Length
|
|
$verifiedLegacyCallbackBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyBreakpointCallback.Offset -Count $legacyCallbackBytes.Length
|
|
$verifiedHookBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.Hook.Offset -Count $hookBytes.Length
|
|
$verifiedWrapperBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.Wrapper.Offset -Count $wrapperBytes.Length
|
|
$verifiedLegacyDeferredHookBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyDeferredHook.Offset -Count $legacyDeferredHookBytes.Length
|
|
$verifiedLegacyDeferredWrapperBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyDeferredWrapper.Offset -Count $legacyDeferredWrapperBytes.Length
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedCtrlQBytes -Right $ctrlQBytes)) {
|
|
throw 'Ctrl+Q debugger-init body verification failed after write.'
|
|
}
|
|
|
|
if ($verifiedCtrlQFixupInfo.TargetSeg -ne $ctrlQTargetSeg -or $verifiedCtrlQFixupInfo.TargetOffset -ne $ctrlQTargetOffset) {
|
|
throw 'Ctrl+Q debugger-init relocation verification failed after write.'
|
|
}
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedInterpreterBreakCallBytes -Right $interpreterBreakCallBytes)) {
|
|
throw 'Interpreter debugger callsite verification failed after write.'
|
|
}
|
|
|
|
if ($verifiedInterpreterBreakCallFixupInfo.TargetSeg -ne $interpreterBreakCallSeg -or $verifiedInterpreterBreakCallFixupInfo.TargetOffset -ne $interpreterBreakCallOffset) {
|
|
throw 'Interpreter debugger callsite relocation verification failed after write.'
|
|
}
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedDebuggerCallbackBytes -Right $debuggerCallbackBytes)) {
|
|
throw 'Private debugger UI call verification failed after write.'
|
|
}
|
|
|
|
if ($verifiedDebuggerCallbackFixupInfo.TargetSeg -ne $debuggerCallbackSeg -or $verifiedDebuggerCallbackFixupInfo.TargetOffset -ne $debuggerCallbackOffset) {
|
|
throw 'Private debugger UI call relocation verification failed after write.'
|
|
}
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedCallbackGuardBytes -Right $callbackGuardBytes)) {
|
|
throw 'Break-next dispatch patch verification failed after write.'
|
|
}
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedCallbackTargetBytes -Right $callbackTargetBytes)) {
|
|
throw 'Guarded callback target slot bytes verification failed after write.'
|
|
}
|
|
|
|
if ($verifiedCallbackTargetFixupInfo.TargetSeg -ne $callbackTargetSeg -or $verifiedCallbackTargetFixupInfo.TargetOffset -ne $callbackTargetOffset) {
|
|
throw 'Guarded callback target relocation verification failed after write.'
|
|
}
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedPrivateMethod0Bytes -Right $privateMethod0Bytes)) {
|
|
throw 'Private vtable method-0 bytes verification failed after write.'
|
|
}
|
|
|
|
if ($verifiedPrivateMethod0FixupInfo.TargetSeg -ne $privateMethod0TargetSeg -or $verifiedPrivateMethod0FixupInfo.TargetOffset -ne $privateMethod0TargetOffset) {
|
|
throw 'Private vtable method-0 relocation verification failed after write.'
|
|
}
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedPrivateMethod1Bytes -Right $privateMethod1Bytes)) {
|
|
throw 'Private vtable method-1 bytes verification failed after write.'
|
|
}
|
|
|
|
if ($verifiedPrivateMethod1FixupInfo.TargetSeg -ne $privateMethod1TargetSeg -or $verifiedPrivateMethod1FixupInfo.TargetOffset -ne $privateMethod1TargetOffset) {
|
|
throw 'Private vtable method-1 relocation verification failed after write.'
|
|
}
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedLegacyCallbackBytes -Right $legacyCallbackBytes)) {
|
|
throw 'Legacy callback cleanup bytes verification failed after write.'
|
|
}
|
|
|
|
if ($verifiedLegacyCallbackFixupInfo.TargetSeg -ne $legacyCallbackTargetSeg -or $verifiedLegacyCallbackFixupInfo.TargetOffset -ne $legacyCallbackTargetOffset) {
|
|
throw 'Legacy callback cleanup relocation verification failed after write.'
|
|
}
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedHookBytes -Right $hookBytes)) {
|
|
throw 'Hook-site verification failed after write.'
|
|
}
|
|
|
|
if ($verifiedHookFixupInfo.TargetSeg -ne $hookTargetSeg -or $verifiedHookFixupInfo.TargetOffset -ne $hookTargetOffset) {
|
|
throw 'Hook relocation verification failed after write.'
|
|
}
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedWrapperBytes -Right $wrapperBytes)) {
|
|
throw 'Wrapper-site verification failed after write.'
|
|
}
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedLegacyDeferredHookBytes -Right $legacyDeferredHookBytes)) {
|
|
throw 'Rejected deferred-event hook cleanup verification failed after write.'
|
|
}
|
|
|
|
if ($verifiedLegacyDeferredHookFixupInfo.TargetSeg -ne $legacyDeferredHookTargetSeg -or $verifiedLegacyDeferredHookFixupInfo.TargetOffset -ne $legacyDeferredHookTargetOffset) {
|
|
throw 'Rejected deferred-event relocation cleanup verification failed after write.'
|
|
}
|
|
|
|
if (-not (Test-ByteArrayEqual -Left $verifiedLegacyDeferredWrapperBytes -Right $legacyDeferredWrapperBytes)) {
|
|
throw 'Rejected modal-wrapper cleanup verification failed after write.'
|
|
}
|
|
|
|
Write-Host ''
|
|
Write-Host ("Applied: {0}" -f $Label)
|
|
Write-Host ("0x410 init body @ 0x{0:X}: {1}" -f $sites.CtrlQDebuggerInit.Offset, $ctrlQState)
|
|
Write-Host ("Interpreter call @ 0x{0:X}: {1}" -f $sites.InterpreterBreakCall.Offset, $interpreterCallState)
|
|
Write-Host ("Private UI call @ 0x{0:X}: {1}" -f $sites.DebuggerCallback.Offset, $privateDispatchState)
|
|
Write-Host ("Legacy break hook @ 0x{0:X}: {1}" -f $sites.CallbackGuardCode.Offset, $callbackGuardState)
|
|
Write-Host ("Legacy target slot @ 0x{0:X}: {1}" -f $sites.CallbackTargetSlot.Offset, $callbackTargetState)
|
|
Write-Host ("Current-unit args @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
|
Write-Host ("Modal wrapper args @ 0x{0:X}: {1}" -f $sites.LegacyDeferredWrapper.Offset, $deferredWrapperState)
|
|
Write-Host ("Deferred hook cleanup @ 0x{0:X}: {1}" -f $sites.LegacyDeferredHook.Offset, $deferredHookState)
|
|
Write-Host ("Direct hook cleanup @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
|
Write-Host ''
|
|
Write-Host 'What this means:'
|
|
Write-Host '- Ctrl+Q still goes through the real 0x410 keyboard lane and keeps the original 0x844 cheat gate.'
|
|
if ($CtrlQPatched) {
|
|
Write-Host ('- The 0x410 body now correctly handles both cases: it arms an existing seg1408 debugger-state object in place, or lazily creates one and stores it at 0x659c/0x659e before arming break-next mode (+0x74=1, +0x75=0).')
|
|
Write-Host ("- {0} stops using the unsafe 1478:6597 data slot entirely and retargets the existing interpreter call at 1418:04b5 straight into the corrected private 13e8:232d stub." -f $activeProfile.Label)
|
|
if ($activeProfile.PatchCurrentUnitWrapper) {
|
|
Write-Host '- The current-unit debugger wrapper at 13a0:0086 has its inherited caller-word pushes zeroed so the callback does not forward the debugger-object pointer as UI arguments.'
|
|
}
|
|
if ($activeProfile.PatchModalWrapper) {
|
|
Write-Host '- The modal debugger wrapper at 13a0:020d has its inherited caller-word pushes zeroed for the same reason.'
|
|
}
|
|
}
|
|
else {
|
|
Write-Host '- All debugger patch changes are restored to the retail byte pattern, including the 0x410 body and the interpreter break call at 1418:04b5.'
|
|
Write-Host '- Older private-vtable and direct/deferred experiment sites are also written back to retail bytes during restore.'
|
|
}
|
|
Write-Host ''
|
|
}
|
|
|
|
function Invoke-MenuChoice {
|
|
param([string]$SelectedChoice)
|
|
|
|
switch ($SelectedChoice.Trim().ToLowerInvariant()) {
|
|
'1' {
|
|
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
|
}
|
|
'2' {
|
|
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
|
}
|
|
'candidate-i' {
|
|
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
|
}
|
|
'candidate-j' {
|
|
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
|
}
|
|
'candidate-m' {
|
|
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
|
}
|
|
'candidate-n' {
|
|
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
|
}
|
|
'candidate-o' {
|
|
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
|
}
|
|
'candidate-p' {
|
|
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
|
|
}
|
|
'3' {
|
|
Set-DesiredState -CtrlQPatched $false -ProfileKey $null -Label 'Restore original bytes'
|
|
}
|
|
'restore' {
|
|
Set-DesiredState -CtrlQPatched $false -ProfileKey $null -Label 'Restore original bytes'
|
|
}
|
|
'4' {
|
|
return $false
|
|
}
|
|
'exit' {
|
|
return $false
|
|
}
|
|
default {
|
|
Write-Warning 'Invalid selection.'
|
|
}
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $exePath)) {
|
|
throw "CRUSADER.EXE was not found at '$exePath'. Pass -ExePath to point at the retail install or place the EXE next to this script."
|
|
}
|
|
|
|
if ($PSBoundParameters.ContainsKey('Choice')) {
|
|
[void](Invoke-MenuChoice -SelectedChoice $Choice)
|
|
return
|
|
}
|
|
|
|
:mainloop while ($true) {
|
|
$currentBytes = [System.IO.File]::ReadAllBytes($exePath)
|
|
Show-Status -FileBytes $currentBytes
|
|
$choice = Read-Host 'Select 1-4'
|
|
if ([string]::IsNullOrEmpty($choice)) { break mainloop }
|
|
|
|
if (-not (Invoke-MenuChoice -SelectedChoice $choice)) {
|
|
break mainloop
|
|
}
|
|
} |