Skip to content

Commit

Permalink
Simplified process by passing script as encoded string, including arg…
Browse files Browse the repository at this point in the history
…uments
  • Loading branch information
tmontney committed Dec 25, 2023
1 parent 4edc4a7 commit 7c1d0b1
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 93 deletions.
117 changes: 35 additions & 82 deletions PS in Batch.ps1
Original file line number Diff line number Diff line change
@@ -1,92 +1,44 @@
function ConvertTo-BatchWrapped([String]$PSScriptPath, [Hashtable]$Arguments) {
$PSScriptContents = Get-Content -Path $PSScriptPath -Raw -ErrorAction Stop
$BatchBase64 = ConvertTo-MultilineBatchBase64 -VarName "TargetScript" -VarText $PSScriptContents

$TempBase64OutputLine = @()
for ($i = 1; $i -le $BatchBase64.VarCount; $i++) {
$TempBase64OutputLine += "echo !TargetScript$i! > %TempBase64Output%"
function ConvertTo-PSBatchScript {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[String]
$PSScriptContents,
[Parameter(Mandatory = $false)]
[String[]]
$Arguments = @(),
# Consider using delayed expansion on variables
# If you explicitly specify $env:TEMP and run this as another user, it will map to the wrong user's temp folder
[Parameter(Mandatory = $false)]
[String]
$CurrentWorkingDirectory = "%TEMP%"
)

try
{ [void]([ScriptBlock]::Create($PSScriptContents)) }
catch
{ Write-Warning -Message "There was a problem parsing 'PSScriptContents' as a PowerShell script."; Write-Error $_; return }

if ($Arguments.Count -gt 0) {
$ArgumentsAL = [System.Collections.ArrayList]::new()
$ArgumentsAL.AddRange($Arguments)

$cliXml = [System.Management.Automation.PSSerializer]::Serialize($ArgumentsAL)
$ArgsBase64 = "-EncodedArguments " + ([Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($cliXml)))
}

$InvokePSLine = "%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File ""%TempTargetScript%"""
if ($Arguments) {
$Arguments.Keys | ForEach-Object {
if ($Arguments[$_] -is [String]) {
$InvokePSLine += " -$_ ""$($Arguments[$_])"""
}
else {
$InvokePSLine += " -$_ $($Arguments[$_])"
}
}
}
$BatchBase64 = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($PSScriptContents))
$InvokePSLine = "%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -EncodedCommand $BatchBase64 $ArgsBase64"

$BatchContents = @"
return @"
@echo off
setlocal EnableDelayedExpansion
set LF=^
REM !!! Do not remove the two lines above this; required to make a newline variable !!!
REM To generate the following, use certutil -decode or ConvertTo-MultilineBatchBase64 in PowerShell.Common.psm1
REM At this time, it isn't possible to decode with certutil using Base64 encoded by [System.Convert]::ToBase64String
REM certutil appears to keep lines to ~64 characters; recommended maximium line length is 127
REM Set the Base64 of the target script
$($BatchBase64.Var)
REM Set current working directory to a temporary folder
cd $CurrentWorkingDirectory
REM Set the temporary file paths
set "TempBase64Output=%temp%\TargetScript.%random%.txt"
set "TempTargetScript=%temp%\TargetScript.%random%.ps1"
REM Decode the TargetScript variable
$($TempBase64OutputLine -join "`n")
certutil -decode %TempBase64Output% %TempTargetScript% > NUL
REM Execute TargetScript
REM Execute PowerShell script
$InvokePSLine
REM Clean up temporary files
del %TempBase64Output%
del %TempTargetScript%
"@

Set-Content -Path "$Script:ScriptCWD\$(([System.IO.Path]::GetFileNameWithoutExtension($PSScriptPath))).bat" -Value $BatchContents -Force
}

function ConvertTo-MultilineBatchBase64([String]$VarName, [String]$VarText) {
#$VarBase64 = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($Text))

$VarTextTempFile = New-TemporaryFile
$VarBase64TempFile = New-TemporaryFile
Set-Content -Path $VarTextTempFile -Value $VarText
[void](certutil.exe -f -encode $VarTextTempFile $VarBase64TempFile)

$Var = @()
$VarCount = 1
$VarBase64 = Get-Content -Path $VarBase64TempFile | Select-Object -Skip 1 | Select-Object -SkipLast 1

Remove-Item -Path $VarTextTempFile -Force -ErrorAction SilentlyContinue
Remove-Item -Path $VarBase64TempFile -Force -ErrorAction SilentlyContinue

if ($VarBase64) {
$Var = @("set $VarName$VarCount=$($VarBase64[0])!LF!")
if ($VarBase64.Length -gt 1) {
for ($i = 1; $i -lt $VarBase64.Length; $i++) {
if ($i % 100 -eq 0) {
$VarCount += 1
$Var += "set $VarName$VarCount=$($VarBase64[$i])!LF!"
}
else {
$Var += "set $VarName$VarCount=!$VarName$VarCount!$($VarBase64[$i])!LF!"
}
}
}
}

return [PSCustomObject]@{
VarCount = $VarCount
Var = ($Var -join "`n")
}
}

####################
Expand All @@ -101,4 +53,5 @@ else {
throw "Cannot determine script's working directory"
}

ConvertTo-BatchWrapped -PSScriptPath "$Script:ScriptCWD\Sample PS Script.ps1" -Arguments @{"Message" = "Hello world!" }
$PSScriptContents = Get-Content -Path "$Script:ScriptCWD\Sample PS Script.ps1" -Raw
ConvertTo-PSBatchScript -PSScriptContents $PSScriptContents -Arguments @("Hello world!") | Set-Content -LiteralPath "$Script:ScriptCWD\Sample PS Script.bat" -Force
19 changes: 8 additions & 11 deletions Sample PS Script.ps1
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[Parameter(Mandatory = $false)]
[String]
$Message
$Message = "Hello"
)

if ($MyInvocation.MyCommand.Path) {
$ScriptCWD = (Get-Item -Path $MyInvocation.MyCommand.Path).Directory.FullName
}
elseif ($PSScriptRoot) {
$ScriptCWD = $PSScriptRoot
}
else {
throw "Cannot determine script's working directory"
}
# For whatever reason it wasn't changed in the batch script
# We don't want to dirty up System32
if ((Get-Location).Path -eq "$env:WINDIR\System32")
{ Set-Location -Path $env:TEMP }

$ScriptCWD = (Get-Location).Path

Write-Output -InputObject "Script current working directory: $ScriptCWD"

Expand Down

1 comment on commit 7c1d0b1

@tmontney
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MicrosoftDocs/PowerShell-Docs#10751 pertains to how I figured out the EncodedArguments parameter.

Please sign in to comment.