- Introduced a new command 'annotate-usecode' to import USECODE IR JSON annotation hints as Ghidra comments on compiled anchors. - Added argument parsing for multiple IR JSON files, comment type selection, and a dry-run option. - Implemented logic to read annotation records from the provided IR files and set comments on the corresponding addresses in Ghidra. - Enhanced JSON schema to include response structure for the new command.
715 lines
No EOL
26 KiB
PowerShell
715 lines
No EOL
26 KiB
PowerShell
param(
|
|
[ValidateSet('1', '2', '3', '4')]
|
|
[string]$Choice
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$exePath = Join-Path $PSScriptRoot 'CRUSADER.EXE'
|
|
$statePath = Join-Path $PSScriptRoot 'patch_crusader_cheat_menu.state.json'
|
|
|
|
$sites = @{
|
|
Hook = @{
|
|
Label = 'Hidden menu direct 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 = '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')
|
|
)
|
|
}
|
|
}
|
|
|
|
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'
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
$hookState = Get-SiteState -FileBytes $FileBytes -Site $sites.Hook
|
|
$wrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper
|
|
|
|
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 ''
|
|
Write-Host '1. Apply supported hidden-menu patch (aliases to Experiment B)'
|
|
Write-Host '2. Apply Experiment B (retarget + modal arg fix)'
|
|
Write-Host '3. Restore original bytes'
|
|
Write-Host '4. Exit'
|
|
Write-Host ''
|
|
}
|
|
|
|
function Set-DesiredState {
|
|
param(
|
|
[bool]$HookPatched,
|
|
[bool]$WrapperPatched,
|
|
[string]$Label
|
|
)
|
|
|
|
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
|
|
$stateData = Get-StateData
|
|
|
|
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
|
|
$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
|
|
}
|
|
}
|
|
if ($wrapperCurrentState -eq 'Original') {
|
|
$stateChanged = (Save-OriginalBytesIfMissing -StateData $stateData -SiteKey 'Wrapper' -SiteOffset $sites.Wrapper.Offset -Bytes $wrapperCurrent) -or $stateChanged
|
|
}
|
|
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.'
|
|
}
|
|
}
|
|
}
|
|
|
|
$legacyDeferredHookBytes = $sites.LegacyDeferredHook.Original
|
|
$legacyDeferredHookTargetSeg = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetSeg
|
|
$legacyDeferredHookTargetOffset = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetOffset
|
|
$legacyDeferredHookReserved = 0
|
|
$legacyDeferredWrapperBytes = $sites.LegacyDeferredWrapper.Original
|
|
|
|
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)
|
|
$hookState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Hook
|
|
$wrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Wrapper
|
|
$verifiedHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.Hook
|
|
$verifiedLegacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.LegacyDeferredHook
|
|
$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 $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 ("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 ''
|
|
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 ''
|
|
}
|
|
|
|
function Invoke-MenuChoice {
|
|
param([string]$SelectedChoice)
|
|
|
|
switch ($SelectedChoice.Trim()) {
|
|
'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)'
|
|
}
|
|
'2' {
|
|
Set-DesiredState -HookPatched $true -WrapperPatched $true -Label 'Experiment B (A + B)'
|
|
}
|
|
'3' {
|
|
Set-DesiredState -HookPatched $false -WrapperPatched $false -Label 'Restore original bytes'
|
|
}
|
|
'4' {
|
|
return $false
|
|
}
|
|
default {
|
|
Write-Warning 'Invalid selection.'
|
|
}
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
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."
|
|
}
|
|
|
|
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, 2, 3, or 4'
|
|
if ([string]::IsNullOrEmpty($choice)) { break mainloop }
|
|
|
|
if (-not (Invoke-MenuChoice -SelectedChoice $choice)) {
|
|
break mainloop
|
|
}
|
|
} |