exe21sp.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Restore equivalent ps1 from exe built by ps12exe (exe -> ps1). .DESCRIPTION Uses AsmResolver to read script payload from exe: - For exe built with the standard program frame: reads the embedded .NET manifest resource "main.ps1" as UTF-8 to get the original script. Packed exes first unwrap the compressed "main" launcher payload and look inside it (gzip for Windows PowerShell builds, Brotli for Core builds). Brotli is not available on .NET Framework, so under Windows PowerShell a Core exe is handed off to pwsh (PowerShell 7); if pwsh is missing, an error is reported. - For minimal exe compiled with TinySharp: parses its CIL and PE image, restores the output string and exit code captured by TinySharp, and generates a minimal ps1 containing only that string (and optional exit statement) to equivalently reproduce the behavior. .PARAMETER inputFile Path or URL to the .exe file to decompile. .PARAMETER outputFile Optional path for the output ps1 file. If not specified, writes to stdout when redirected, otherwise writes to <exe>.ps1 in the same directory as the input. .EXAMPLE exe21sp -inputFile .\myapp.exe exe21sp -inputFile .\myapp.exe -outputFile .\myapp.ps1 .EXAMPLE Get-ChildItem *.exe | exe21sp ".\app.exe" | exe21sp #> [CmdletBinding()] param( [Parameter(ValueFromPipeline=$true)] [string[]]$inputFile, [string]$outputFile, #_if PSScript [ArgumentCompleter({ Param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) . "$PSScriptRoot\src\LocaleArgCompleter.ps1" @PSBoundParameters })] #_endif [string]$Localize, [switch]$help ) #_if PSScript $global:LastExitCode = 0 $LocalizeData = . $PSScriptRoot\src\LocaleLoader.ps1 -Localize $Localize . $PSScriptRoot\src\WriteI18n.ps1 Set-I18nData -I18nData $LocalizeData.exe21spI18nData function Show-exe21spHelp { . $PSScriptRoot\src\HelpShower.ps1 -HelpData $LocalizeData.exe21spHelpData | Write-Host } if ($help) { Show-exe21spHelp return } $inputItemsToProcess = @($inputFile) + @($input) | Where-Object { $_ } if ($outputFile -and ($outputFile = $outputFile.Trim()) -and $outputFile -notmatch '\.ps1$') { $outputFile = $outputFile + '.ps1' } if ($inputItemsToProcess.Count -eq 0) { Show-exe21spHelp Write-Host Write-I18n Error NoneInput -Category InvalidArgument if ([System.Console]::IsOutputRedirected -or [System.Console]::IsInputRedirected -or [System.Console]::IsErrorRedirected) { $global:LastExitCode = 2 return } & $PSScriptRoot\src\Interact\exe21sp.ps1 -Localize $Localize return } function Resolve-ExeInputPath { param([string]$PathOrUrl) if ($PathOrUrl -match "^(https?|ftp)://") { $tempFile = [System.IO.Path]::GetTempFileName() try { Invoke-WebRequest -Uri $PathOrUrl -OutFile $tempFile -ErrorAction Stop return [PSCustomObject]@{ Path = $tempFile; IsTemp = $true } } catch { Remove-Item -LiteralPath $tempFile -Force -ErrorAction SilentlyContinue throw } } $resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($PathOrUrl) return [PSCustomObject]@{ Path = $resolved; IsTemp = $false } } # Windows PowerShell(.NET Framework)没有 BrotliStream:Core 产物的 Brotli 负载解压转交 pwsh 完成。 # 主处理流程仅在脚本/模块模式运行(exe 版只是 #_require ps12exe 后转发),故直接用同目录脚本,无需模块导入。 function Invoke-ExtractionInPwsh([string]$ExePath) { $pwsh = Get-Command pwsh -ErrorAction Ignore if (-not $pwsh) { throw 'pwsh not installed' } $tempOut = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + '.ps1') try { & $pwsh.Source -NoProfile -File "$PSScriptRoot\exe21sp.ps1" -inputFile $ExePath -outputFile $tempOut | Out-Null if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $tempOut)) { throw "pwsh extraction failed (exit $LASTEXITCODE)" } return [System.IO.File]::ReadAllText($tempOut, [System.Text.Encoding]::UTF8) } finally { Remove-Item -LiteralPath $tempOut -Force -ErrorAction Ignore } } $Refs = @( 'System', 'System.Core', 'System.IO.Compression', 'netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' ) Get-ChildItem -LiteralPath $PSScriptRoot\src\bin\AsmResolver -Recurse -Filter *.dll | ForEach-Object { $Refs += $_.FullName try { Add-Type -LiteralPath $_.FullName -ErrorVariable $null } catch { $_.Exception.LoaderExceptions | Out-String | Write-Verbose $Error.Remove($_) } } $ExtractorCode = Get-Content -LiteralPath $PSScriptRoot\src\programFrames\exe21sp.cs -Raw -Encoding UTF8 Add-Type -TypeDefinition $ExtractorCode -ReferencedAssemblies $Refs -IgnoreWarnings . $PSScriptRoot\src\TaskbarProgress.ps1 $total = $inputItemsToProcess.Count $currentIndex = 0 foreach ($currentInput in $inputItemsToProcess) { Write-TaskbarProgress -Percent ([Math]::Min(100, [int](($currentIndex / $total) * 100))) $resolved = $null try { $resolved = Resolve-ExeInputPath -PathOrUrl $currentInput } catch { if ($currentInput -match "^(https?|ftp)://") { Write-I18n Error InputUrlFailed $currentInput } else { Write-I18n Error FileNotFound $currentInput } $global:LastExitCode = 3 Write-TaskbarProgressError $currentIndex++ continue } $currentExe = $resolved.Path if (-not (Test-Path -LiteralPath $currentExe -PathType Leaf)) { Write-I18n Error FileNotFound $currentInput $global:LastExitCode = 3 Write-TaskbarProgressError if ($resolved.IsTemp) { Remove-Item -LiteralPath $currentExe -Force -ErrorAction SilentlyContinue } $currentIndex++ continue } try { $script = [exe21sp.Extractor]::ExtractScriptFromExe($currentExe) } catch [exe21sp.BrotliUnavailableException] { # 当前是 Windows PowerShell,Core 的 Brotli 负载交给 pwsh 解压。 try { $script = Invoke-ExtractionInPwsh -ExePath $currentExe } catch { Write-I18n Error CoreExtractNeedsPwsh $global:LastExitCode = 1 Write-TaskbarProgressError if ($resolved.IsTemp) { Remove-Item -LiteralPath $currentExe -Force -ErrorAction SilentlyContinue } $currentIndex++ continue } } catch { $msg = $_.Exception.Message if ($LocalizeData.exe21spI18nData.ContainsKey($msg)) { Write-I18n Error $msg } else { Write-Error $msg } $global:LastExitCode = 1 Write-TaskbarProgressError if ($resolved.IsTemp) { Remove-Item -LiteralPath $currentExe -Force -ErrorAction SilentlyContinue } $currentIndex++ continue } if ($resolved.IsTemp) { Remove-Item -LiteralPath $currentExe -Force -ErrorAction SilentlyContinue } if ($null -eq $script) { Write-I18n Error NoEmbeddedScript $currentInput $global:LastExitCode = 1 Write-TaskbarProgressError $currentIndex++ continue } $currentOutFile = $outputFile if (-not $currentOutFile -and -not ([System.Console]::IsOutputRedirected -or [System.Console]::IsInputRedirected -or [System.Console]::IsErrorRedirected)) { $baseName = if ($currentInput -match "^(https?|ftp)://") { [System.IO.Path]::GetFileNameWithoutExtension([System.Uri]::new($currentInput).Segments[-1]) } else { [System.IO.Path]::GetFileNameWithoutExtension($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($currentInput)) } $dir = if ($currentInput -match "^(https?|ftp)://") { $PWD.Path } else { [System.IO.Path]::GetDirectoryName($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($currentInput)) } $currentOutFile = [System.IO.Path]::Combine($dir, "$baseName.ps1") } if ($currentOutFile) { $currentOutFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($currentOutFile) [System.IO.File]::WriteAllText($currentOutFile, $script, [System.Text.UTF8Encoding]::new($false)) Write-Verbose "Written to $currentOutFile" if ([System.Console]::IsOutputRedirected -or [System.Console]::IsInputRedirected -or [System.Console]::IsErrorRedirected) { Write-Output $currentOutFile } } else { Write-Output $script } $currentIndex++ } Write-TaskbarProgress -Percent 100 Write-TaskbarProgressClear #_else #_require ps12exe #_!!exe21sp @PSBoundParameters #_endif |