From c2cc808ef68654f37e7b2c2a261d9c39082570ea Mon Sep 17 00:00:00 2001 From: amd64fox <62529699+amd64fox@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:09:42 +0300 Subject: [PATCH] fix(block_slots): prepare semantic patching for Spotify 1.2.96 - resolve slot enums from mapper control flow instead of field offsets - allow short or exact versions in the validation workflow --- .github/workflows/check_spotx.yml | 43 +++-- run.ps1 | 276 ++++++++++++++++++++++++++++-- 2 files changed, 287 insertions(+), 32 deletions(-) diff --git a/.github/workflows/check_spotx.yml b/.github/workflows/check_spotx.yml index 4613488..26725cc 100644 --- a/.github/workflows/check_spotx.yml +++ b/.github/workflows/check_spotx.yml @@ -1,38 +1,53 @@ name: Check Spotx for the latest Spotify +run-name: Check SpotX for Spotify ${{ inputs.version || 'latest' }} on: workflow_dispatch: + inputs: + version: + description: Spotify version like 1.2.95 or 1.2.95.453, empty means latest + required: false + default: '' + type: string jobs: build: runs-on: windows-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Run Spotx shell: powershell + env: + SPOTIFY_VERSION: ${{ inputs.version }} run: | - $jsonUrl = "https://raw.githubusercontent.com/LoaderSpot/table/refs/heads/main/table/versions.json" - $resp = Invoke-RestMethod -Uri $jsonUrl + $spotifyVersion = ([string]$env:SPOTIFY_VERSION).Trim() - # Use fullversion because installer names include architecture suffixes - $latestEntry = $resp.PSObject.Properties | Select-Object -First 1 - if (-not $latestEntry) { - throw "No Spotify versions found in $jsonUrl" + if ([string]::IsNullOrWhiteSpace($spotifyVersion)) { + $jsonUrl = "https://raw.githubusercontent.com/LoaderSpot/table/refs/heads/main/table/versions.json" + $resp = Invoke-RestMethod -Uri $jsonUrl + + # Use fullversion for the installer name + $latestEntry = $resp.PSObject.Properties | Select-Object -First 1 + if (-not $latestEntry) { + throw "No Spotify versions found in $jsonUrl" + } + + $spotifyVersion = [string]$latestEntry.Value.fullversion + if ([string]::IsNullOrWhiteSpace($spotifyVersion) -or $spotifyVersion -notmatch '^\d+\.\d+\.\d+\.\d+\.g[0-9a-f]{8}$') { + throw "Invalid Spotify fullversion for $($latestEntry.Name)" + } + } + elseif ($spotifyVersion -notmatch '^\d+\.\d+\.\d+(?:\.\d+)?$') { + throw "Invalid Spotify version: $spotifyVersion" } - $spotifyFullVersion = $latestEntry.Value.fullversion - if ([string]::IsNullOrWhiteSpace($spotifyFullVersion) -or $spotifyFullVersion -notmatch '^\d+\.\d+\.\d+\.\d+\.g[0-9a-f]{8}$') { - throw "Invalid Spotify fullversion for $($latestEntry.Name)" - } + Write-Host "Selected Spotify version: $spotifyVersion" # Incoming parameters $parameters = @{ Verbose = $true new_theme = $true - version = $spotifyFullVersion + version = $spotifyVersion confirm_spoti_recomended_over = $true cache_limit = 1000 block_update_on = $true diff --git a/run.ps1 b/run.ps1 index a760c3a..a55f284 100644 --- a/run.ps1 +++ b/run.ps1 @@ -3409,6 +3409,250 @@ function Read-X64BlockSlotsField { throw "Unexpected block_slots field instruction" } +function Get-X64BlockSlotsMapperSequenceEnum { + param( + [byte[]]$Bytes, + [int]$SequenceOffset, + [int]$BranchOffset, + [bool]$MatchTaken = $true + ) + + $movEcxR8d = Convert-HexStringToBytes '41 8B C8' + $subEcx8 = Convert-HexStringToBytes '83 E9' + $subEcx32 = Convert-HexStringToBytes '81 E9' + $cmpEcx8 = Convert-HexStringToBytes '83 F9' + $cmpEcx32 = Convert-HexStringToBytes '81 F9' + if (-not [BinaryScanner]::MatchBytes($Bytes, $SequenceOffset, $movEcxR8d)) { + return @() + } + + $matches = @() + for ($enumValue = 1; $enumValue -le 0x3FF; $enumValue++) { + $ecx = [int64]$enumValue + $zeroFlag = $false + $hasFlags = $false + $cursor = $SequenceOffset + $movEcxR8d.Length + $valid = $true + $matched = $false + + while ($cursor -le $BranchOffset) { + if ([BinaryScanner]::MatchBytes($Bytes, $cursor, $subEcx8)) { + $ecx -= [int64](Read-X64SignedByte -Bytes $Bytes -Offset ($cursor + 2)) + $zeroFlag = ($ecx -eq 0) + $hasFlags = $true + $cursor += 3 + continue + } + if ([BinaryScanner]::MatchBytes($Bytes, $cursor, $subEcx32)) { + $ecx -= [int64][BitConverter]::ToInt32($Bytes, $cursor + 2) + $zeroFlag = ($ecx -eq 0) + $hasFlags = $true + $cursor += 6 + continue + } + if ([BinaryScanner]::MatchBytes($Bytes, $cursor, $cmpEcx8)) { + $compareValue = [int64](Read-X64SignedByte -Bytes $Bytes -Offset ($cursor + 2)) + $zeroFlag = ($ecx -eq $compareValue) + $hasFlags = $true + $cursor += 3 + continue + } + if ([BinaryScanner]::MatchBytes($Bytes, $cursor, $cmpEcx32)) { + $compareValue = [int64][BitConverter]::ToInt32($Bytes, $cursor + 2) + $zeroFlag = ($ecx -eq $compareValue) + $hasFlags = $true + $cursor += 6 + continue + } + + $branchLength = 0 + $branchOnEqual = $false + if (($cursor + 2) -le $Bytes.Length -and ($Bytes[$cursor] -eq 0x74 -or $Bytes[$cursor] -eq 0x75)) { + $branchLength = 2 + $branchOnEqual = ($Bytes[$cursor] -eq 0x74) + } + elseif (($cursor + 6) -le $Bytes.Length -and $Bytes[$cursor] -eq 0x0F -and + ($Bytes[$cursor + 1] -eq 0x84 -or $Bytes[$cursor + 1] -eq 0x85)) { + $branchLength = 6 + $branchOnEqual = ($Bytes[$cursor + 1] -eq 0x84) + } + else { + $valid = $false + break + } + + if (-not $hasFlags) { + $valid = $false + break + } + $branchTaken = if ($branchOnEqual) { $zeroFlag } else { -not $zeroFlag } + if ($cursor -eq $BranchOffset) { + $matched = if ($MatchTaken) { $branchTaken } else { -not $branchTaken } + break + } + if ($branchTaken) { + $valid = $false + break + } + $cursor += $branchLength + } + + if ($valid -and $matched) { + $matches += [uint32]$enumValue + } + } + + return $matches +} + +function Get-BlockSlotsMapperEnumValue { + param( + [byte[]]$Bytes, + [object]$PeInfo, + [int64]$MapperStartRva, + [int64]$MapperEndRva, + [int[]]$AnchorRefs + ) + + $mapperOffset = Get-PEOffsetFromRva -Sections $PeInfo.Sections -Rva $MapperStartRva + if ($null -eq $mapperOffset) { + throw 'slot_is_disabled mapper offset was not found' + } + + $mapperOffset = [int]$mapperOffset + $mapperEndOffset = [int64]$mapperOffset + ($MapperEndRva - $MapperStartRva) + if ($mapperEndOffset -le $mapperOffset -or $mapperEndOffset -gt $Bytes.Length) { + throw 'slot_is_disabled mapper range is invalid' + } + + $stringCases = @( + [PSCustomObject]@{ + PrefixLength = 20 + Pattern = Convert-HexStringToBytes '0F 57 C0 0F 11 02 48 89 7A 10 48 89 7A 18 41 B8 10 00 00 00 48 8D 15 00 00 00 00' + Mask = Convert-HexStringToBytes 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 00 00 00 00' + DispatchMode = 'Target' + }, + [PSCustomObject]@{ + PrefixLength = 18 + Pattern = Convert-HexStringToBytes '0F 57 C0 0F 11 02 48 89 7A 10 48 89 7A 18 44 8D 41 0F 48 8D 15 00 00 00 00' + Mask = Convert-HexStringToBytes 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 00 00 00 00' + DispatchMode = 'Fallthrough' + } + ) + $movEcxR8d = Convert-HexStringToBytes '41 8B C8' + $cmpR8d8 = Convert-HexStringToBytes '41 83 F8' + $cmpR8d32 = Convert-HexStringToBytes '41 81 F8' + $enumValues = @{} + + foreach ($anchorRef in $AnchorRefs) { + if (([int64]$anchorRef + 7) -gt $mapperEndOffset) { + continue + } + + foreach ($stringCase in $stringCases) { + $caseOffset = [int]$anchorRef - $stringCase.PrefixLength + if ($caseOffset -lt $mapperOffset -or + -not [BinaryScanner]::MatchMaskedBytes($Bytes, $caseOffset, $stringCase.Pattern, $stringCase.Mask)) { + continue + } + + $dispatches = @() + if ($stringCase.DispatchMode -eq 'Target') { + for ($branchOffset = $mapperOffset; $branchOffset -lt $caseOffset; $branchOffset++) { + if (($Bytes[$branchOffset] -eq 0x74 -or $Bytes[$branchOffset] -eq 0x75) -and + ($branchOffset + 2) -le $mapperEndOffset) { + $branchOnEqual = ($Bytes[$branchOffset] -eq 0x74) + $branchTarget = [int64]$branchOffset + 2 + + (Read-X64SignedByte -Bytes $Bytes -Offset ($branchOffset + 1)) + } + elseif ($Bytes[$branchOffset] -eq 0x0F -and ($branchOffset + 6) -le $mapperEndOffset -and + ($Bytes[$branchOffset + 1] -eq 0x84 -or $Bytes[$branchOffset + 1] -eq 0x85)) { + $branchOnEqual = ($Bytes[$branchOffset + 1] -eq 0x84) + $branchTarget = [int64]$branchOffset + 6 + [BitConverter]::ToInt32($Bytes, $branchOffset + 2) + } + else { + continue + } + + if ($branchTarget -eq $caseOffset) { + $dispatches += [PSCustomObject]@{ + Offset = $branchOffset + BranchOnEqual = $branchOnEqual + MatchTaken = $true + } + } + } + } + else { + if ($caseOffset -ge ($mapperOffset + 6) -and $Bytes[$caseOffset - 6] -eq 0x0F -and + ($Bytes[$caseOffset - 5] -eq 0x84 -or $Bytes[$caseOffset - 5] -eq 0x85)) { + $branchOffset = $caseOffset - 6 + $branchOnEqual = ($Bytes[$caseOffset - 5] -eq 0x84) + $branchTarget = [int64]$caseOffset + [BitConverter]::ToInt32($Bytes, $caseOffset - 4) + } + elseif ($caseOffset -ge ($mapperOffset + 2) -and + ($Bytes[$caseOffset - 2] -eq 0x74 -or $Bytes[$caseOffset - 2] -eq 0x75)) { + $branchOffset = $caseOffset - 2 + $branchOnEqual = ($Bytes[$caseOffset - 2] -eq 0x74) + $branchTarget = [int64]$caseOffset + + (Read-X64SignedByte -Bytes $Bytes -Offset ($caseOffset - 1)) + } + else { + continue + } + + if ($branchTarget -lt $mapperOffset -or $branchTarget -ge $mapperEndOffset -or $branchTarget -eq $caseOffset) { + continue + } + $dispatches += [PSCustomObject]@{ + Offset = $branchOffset + BranchOnEqual = $branchOnEqual + MatchTaken = $false + } + } + + foreach ($dispatch in $dispatches) { + $branchOffset = [int]$dispatch.Offset + $selectsEquality = ($dispatch.BranchOnEqual -eq $dispatch.MatchTaken) + if ($selectsEquality -and $branchOffset -ge ($mapperOffset + 4) -and + [BinaryScanner]::MatchBytes($Bytes, $branchOffset - 4, $cmpR8d8)) { + $enumValue = [int64](Read-X64SignedByte -Bytes $Bytes -Offset ($branchOffset - 1)) + if ($enumValue -gt 0 -and $enumValue -le 0x3FF) { + $enumValues['{0:X}' -f $enumValue] = [uint32]$enumValue + } + } + if ($selectsEquality -and $branchOffset -ge ($mapperOffset + 7) -and + [BinaryScanner]::MatchBytes($Bytes, $branchOffset - 7, $cmpR8d32)) { + $enumValue = [BitConverter]::ToUInt32($Bytes, $branchOffset - 4) + if ($enumValue -gt 0 -and $enumValue -le 0x3FF) { + $enumValues['{0:X}' -f $enumValue] = $enumValue + } + } + + $sequenceStart = [Math]::Max($mapperOffset, $branchOffset - 0x100) + for ($candidateOffset = $sequenceStart; $candidateOffset -lt $branchOffset; $candidateOffset++) { + if (-not [BinaryScanner]::MatchBytes($Bytes, $candidateOffset, $movEcxR8d)) { + continue + } + $sequenceEnums = @(Get-X64BlockSlotsMapperSequenceEnum ` + -Bytes $Bytes ` + -SequenceOffset $candidateOffset ` + -BranchOffset $branchOffset ` + -MatchTaken $dispatch.MatchTaken) + foreach ($enumValue in $sequenceEnums) { + $enumValues['{0:X}' -f [uint32]$enumValue] = [uint32]$enumValue + } + } + } + } + } + + if ($enumValues.Count -ne 1) { + throw "Expected one slot_is_disabled enum value, found $($enumValues.Count)" + } + return [uint32](@($enumValues.Values)[0]) +} + function Get-BlockSlotsPredicateInfo { param( [byte[]]$Bytes, @@ -3565,12 +3809,6 @@ function Get-BlockSlotsPredicateInfo { } } - if ($flagField.Value -ne ($valueField.Value + 1) -or - $valueField.Value -ne ($gateField.Value + 0x18) -or - $fallbackField.Value -ne ($gateField.Value - 8)) { - return $null - } - $jumpDisplacement = [int64]$returnFalseOffset - ([int64]$patchOffset + 2) if ($jumpDisplacement -lt 0 -or $jumpDisplacement -gt 0x7F) { return $null @@ -3655,12 +3893,23 @@ function Find-BlockSlotsBinaryPatchLocation { if ($anchorFunctionSize -lt 0x100 -or $anchorFunctionSize -gt 0x4000) { throw 'Unexpected slot_is_disabled mapper function size' } - $anchorFunctions['{0:X}' -f [int64]$anchorFunction[0]] = $true + $anchorFunctions['{0:X}' -f [int64]$anchorFunction[0]] = [PSCustomObject]@{ + StartRva = [int64]$anchorFunction[0] + EndRva = [int64]$anchorFunction[1] + } } if ($anchorFunctions.Count -ne 1) { throw "Expected one slot_is_disabled mapper function, found $($anchorFunctions.Count)" } + $mapperFunction = @($anchorFunctions.Values)[0] + $slotDisabledEnum = Get-BlockSlotsMapperEnumValue ` + -Bytes $Bytes ` + -PeInfo $PeInfo ` + -MapperStartRva $mapperFunction.StartRva ` + -MapperEndRva $mapperFunction.EndRva ` + -AnchorRefs $anchorRefs + $callPatterns = @( [PSCustomObject]@{ Pattern = Convert-HexStringToBytes 'E8 00 00 00 00 84 C0 75 00 BA 00 00 00 00' @@ -3679,7 +3928,6 @@ function Find-BlockSlotsBinaryPatchLocation { $textStart = [int]$text.RawPtr $textEnd = [int64]$text.RawPtr + [int64]$text.RawSize $validatedTargets = @{} - $ambiguousEnums = @{} foreach ($callPattern in $callPatterns) { $searchOffset = $textStart @@ -3698,7 +3946,7 @@ function Find-BlockSlotsBinaryPatchLocation { try { $enumValue = [BitConverter]::ToUInt32($Bytes, $callOffset + $callPattern.EnumOffset) - if ($enumValue -eq 0 -or $enumValue -gt 0x3FF) { + if ($enumValue -ne $slotDisabledEnum) { continue } @@ -3750,12 +3998,7 @@ function Find-BlockSlotsBinaryPatchLocation { } } else { - if ([uint32]$validatedTargets[$key].EnumValue -ne [uint32]$enumValue) { - $ambiguousEnums[$key] = $true - } - else { - $validatedTargets[$key].CallerCount++ - } + $validatedTargets[$key].CallerCount++ } } catch { @@ -3767,9 +4010,6 @@ function Find-BlockSlotsBinaryPatchLocation { if ($validatedTargets.Count -eq 0) { throw 'block_slots semantic function was not found' } - if ($ambiguousEnums.Count -gt 0) { - throw 'block_slots semantic callers use different enum values' - } if ($validatedTargets.Count -ne 1) { throw "Expected one block_slots semantic function, found $($validatedTargets.Count)" }