Add 'annotate-usecode' command to import USECODE IR JSON annotations
- 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.
This commit is contained in:
parent
4d3c8cd81b
commit
daa363c3d2
39 changed files with 41450 additions and 871 deletions
715
patch_crusader_cheat_menu.ps1
Normal file
715
patch_crusader_cheat_menu.ps1
Normal file
|
|
@ -0,0 +1,715 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue