Skip to content

Instantly share code, notes, and snippets.

@JustinGrote
Last active September 21, 2022 09:34
Show Gist options
  • Save JustinGrote/866536458bfa097cb18a4b82181e5f16 to your computer and use it in GitHub Desktop.
Save JustinGrote/866536458bfa097cb18a4b82181e5f16 to your computer and use it in GitHub Desktop.
Powershell Debug Anywhere Design
#Exposes the Powershell Remoting Protocol via a TCP Port
using namespace System.Management.Automation.Runspaces
using namespace Renci.SshNet
using namespace System.Net.Sockets
using namespace System.Threading.Tasks
using namespace System.IO.Pipes
using namespace System.IO
[CmdletBinding(DefaultParameterSetName = 'Server')]
param (
[Parameter(Position = 0, ParameterSetName = 'Client')][String]$ComputerName = 'localhost',
[Parameter(ParameterSetName = 'Client')][String]$PipeName = $("PSHost.$ComputerName.$(New-Guid)"),
[Int]$Port = 7073,
[Parameter(ParameterSetName = 'Server')][ipaddress]$ListenAddress = '127.0.0.1',
[Parameter(ParameterSetName = 'Server')][Switch]$AllowRemoteConnections,
[Parameter(ParameterSetName = 'Server')][Switch]$NoSSHRelay,
[Parameter(ParameterSetName = 'Server')][String]$SSHRelayHost = 'pwsh.link',
[Parameter(ParameterSetName = 'Server')][Int]$SSHRelayPort = 2222
)
if ($AllowRemoteConnections) { $ListenAddress = '0.0.0.0' }
function Get-PSRemotingNamedPipe ($ID = $PID) {
<#
.SYNOPSIS
Gets the PSRemoting autogenerated named pipe of the specified process
.NOTES
Reimplementation of internal powershell method NamedPipeUtils.CreateProcessPipeName
#>
$process = Get-Process -Id $ID -ErrorAction Stop
$processStartTime = $process.starttime.tofiletime()
$processStartId = if ($PSEdition -eq 'Desktop' -or $isWindows) {
$processStartTime
} else {
$processStartTime.ToString('X8').Substring(1, 8)
}
# $pipeNamePrefix = 'PSHost'
# if (-not ($PSEdition -eq 'Desktop' -or $isWindows)) {
# $pipeNamePrefix = "CoreFxPipe_$pipeNamePrefix"
# }
return (@(
'PSHost'
$processStartID
$process.Id
'DefaultAppDomain'
$process.ProcessName
) -join '.').ToString([cultureinfo]::InvariantCulture)
}
function Connect-PSRemotingNamedPipe ([String]$Name = (Get-PSRemotingNamedPipe), [String]$ComputerName = '.') {
#Connect the debug pipe
$pipeclient = [NamedPipeClientStream]::new(
$ComputerName, #string serverName
$Name, #string pipeName
[PipeDirection]::InOut, #PipeDirection direction
[PipeOptions]::Asynchronous #PipeOptions options
)
Write-Host -NoNewline -Fore Cyan "Connecting to Powershell Named Pipe $Name..."
$pipeclient.Connect()
Write-Host -Fore Green 'OK!'
return $pipeClient
}
function Start-PSRemotingTCPListener ([Int]$Port = 7073, [ipaddress]$ListenAddress = '127.0.0.1') {
#Connect the tcp listener. 7073 = ps in hex :)
$tcpListener = [TcpListener]::new($ListenAddress, $port)
$tcpListener.Start()
Write-Host -NoNewline -Fore Cyan "Waiting for connection to ${listenAddress}:$port..."
$tcpClient = $tcpListener.AcceptTcpClient()
Write-Host -Fore Green 'OK!'
return $tcpClient
}
function Connect-PSRemotingTCPClient ([string]$ComputerName = 'localhost', [Int]$Port = 7073) {
#Connect the tcp listener. 7073 = ps in hex :)
return [TcpClient]::new($ComputerName, $port)
}
function Start-PSRemotingPipe ($PipeName) {
$namedPipeClient = [NamedPipeServerStream]::new(
$PipeName,
[PipeDirection]::InOut,
1, #maxNumberofServerInstances
[PipeTransmissionMode]::Message,
[PipeOptions]::Asynchronous
)
Write-Host -NoNewline -Fore Cyan "Waiting for Enter-PSHostProcess -CustomPipeName $PipeName ..."
$namedPipeClient.WaitForConnection()
#Enter-PSHostProcess disconnects the first connection for some reason, so we wait for the reconnect callback
$namedPipeClient.Disconnect()
$namedPipeClient.WaitForConnection()
Write-Host -Fore Green 'OK!'
return $namedPipeClient
}
function Join-Stream {
param (
#Provide an array of two streams to join together
[ValidateCount(2, 2)][Stream[]]$Stream,
#Wait for one of the streams to complete or disconnect. The script will return the first stream to disconnect
[Switch]$Wait
)
$copyStreamTasks = @(
$Stream[0].CopyToAsync($Stream[1])
$Stream[1].CopyToAsync($Stream[0])
)
if ($Wait) {
$completedTaskIndex = [Task]::WaitAny($copyStreamTasks)
return $copyStreamTasks[$completedTaskIndex]
} else {
$copyStreamTasks
}
}
function Connect-SSHRelayServer {
param (
[ValidateNotNullOrEmpty]$NoSSHForward = $NoSSHForward,
[ValidateNotNullOrEmpty]$SSHHost = $SSHHost,
[ValidateNotNullOrEmpty]$SSHPort = $SSHPort
)
$tempPath = [io.path]::GetTempPath()
[String[]]$SSHAssembly = 'Renci.SshNet.dll', 'SshNet.Security.Cryptography.dll' | ForEach-Object {Join-Path $tempPath $PSItem}
$SSHZipPath = join-path $tempPath 'Renci.SSHNet.Zip'
if ($false -in (Test-Path $SSHAssembly)) {
[Net.Webclient]::new().DownloadFile(
"https://gist.githubusercontent.com/JustinGrote/866536458bfa097cb18a4b82181e5f16/raw/Renci.SSHNet.zip",
$SSHZipPath
)
Expand-Archive $SSHZipPath -DestinationPath $tempPath -Verbose
}
Add-Type -Path $SSHAssembly -ErrorAction Stop
try {
$sshClient = [SSHClient]::new(
'pwsh.link', #string host
2222, #int port
'whoever', #string username
'whatever' #string password
)
$sshClient.KeepAliveInterval = [timespan]::FromSeconds(1)
$errorWatcher = Register-ObjectEvent -InputObject $SSHClient -EventName ErrorOccurred -Action { Write-Warning ($EventArgs.exception) }
$sshClient.Connect()
$sshClient.AddForwardedPort(
[Renci.SshNet.ForwardedPortRemote]::new(
'127.0.0.1',
40022,
'127.0.0.1',
2222
)
)
$sshclient.forwardedports[0].start()
Read-Host 'enter to stop'
} finally {
$sshClient.Disconnect()
Unregister-Event -SourceIdentifier $errorWatcher.name
}
}
#region main
do {
try {
if ($PSCmdlet.ParameterSetName -eq 'Client') {
$tcpClient = Connect-PSRemotingTCPClient -ComputerName $ComputerName -Port $Port
$pipeClient = Start-PSRemotingPipe -PipeName $PipeName
} elseif ($PSCmdlet.ParameterSetName -eq 'Server') {
$pipeClient = Connect-PSRemotingNamedPipe
$tcpClient = Start-PSRemotingTCPListener
} else { throw [System.NotSupportedException]"Unknown Parameter Set $($PSCmdlet.ParameterSetName)" }
$firstClosedStream = Join-Stream -Wait $pipeClient, $tcpClient.GetStream()
if (-not $firstClosedStream.IsCompletedSuccessfully) {
throw $firstClosedStream.Exception
}
} catch { Write-Error $PSItem } finally {
$pipeClient.Close()
$pipeClient.Dispose()
$tcpClient.Close()
$tcpClient.Dispose()
if ($tcpListener) { $tcpListener.Stop() }
}
} while ($PSCmdlet.ParameterSetName -eq 'Server')
#endregion main
function Get-PSRemotingNamedPipe () {
<#
.SYNOPSIS
Gets the PSRemoting autogenerated named pipe of the specified process
.NOTES
Reimplementation of internal powershell method NamedPipeUtils.CreateProcessPipeName
#>
param(
[int]$ID = $PID,
[switch]$PipeNameOnly
)
$process = Get-Process -Id $ID -ErrorAction Stop
$processStartTime = $process.starttime.tofiletime()
$processStartId = if ($PSEdition -eq 'Desktop' -or $isWindows) {
$processStartTime
} else {
$processStartTime.ToString('X8').Substring(1, 8)
}
$pipeName = (@(
'PSHost'
$processStartID
$process.Id
'DefaultAppDomain'
$process.ProcessName
) -join '.').ToString([cultureinfo]::InvariantCulture)
if ($PipeNameOnly) {return $pipeName}
if (-not ($PSEdition -eq 'Desktop' -or $isWindows)) {
return $pipeName = "/tmp/CoreFxPipe_$pipeName"
} else {
return $pipeName = "\\.\\pipe\\$pipeName"
}
}
#Main
#Get sshdog
$tempPath = [io.path]::GetTempPath()
[String[]]$SSHAssembly = 'sshdog.exe' | ForEach-Object {Join-Path $tempPath $PSItem}
$SSHZipPath = join-path $tempPath 'sshdog.zip'
if ($false -in (Test-Path $SSHAssembly)) {
[Net.Webclient]::new().DownloadFile(
"https://gist.githubusercontent.com/JustinGrote/866536458bfa097cb18a4b82181e5f16/raw/sshdog.zip",
$SSHZipPath
)
Expand-Archive $SSHZipPath -DestinationPath $tempPath -Verbose
}
$SSHDogPath = join-path $tempPath 'sshdog.exe'
$authorizedKey = "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAyrTLP1RWuJSnAkgo3pqXfmEqEFNWzZrPp280CXYcMF7X7Le+7INvBRMxWwdS6o4xDXq7wqx6s+VtF6taWxLEqQZDhxTeEvcMXsYRv6jFdw8Hhv536D36KPsMB8dsTBG73kldWhloxMWf5P4R03lCJcmh4NQaCTyrB74cO0/mn+ars4KI+FRTq3T8SL70j4zbE9D+I8VZGA4e2ixJ0ua42u1LxIEldAlDGVGKLsAsE4rAQdNtCq+Zo7KFCcR0/D+g8Rq1xnuB4pHwWLjtlLi1omxxjs5huTWq0wnx7Z0xXCz6PDxunuy5Klqd1kcmZ86Asa9nV8mr7IG2d0R74g998Q== 9c:ba:e4:12:85:3d:39:c8:fa:92:fc:b0:38:e4:30:7e jgrote-key-20190930"
$namedPipePath = (Get-PSRemotingNamedPipe -FullPath)
Start-Job {
$SSHDogArgs = @(
"-authorizedkeys",
$USING:authorizedKey,
"-pipeName",
$USING:namedPipePath
)
& $USING:SSHDogPath @SSHDogArgs
}
Write-Host -ForegroundColor green "Waiting on Debugger for $PID"
while (
-not (Get-Runspace -id 1).Debugger.IsActive
) {
sleep 0.5
}
"step1"
"step2"
"step3"
"step4"
"step5"

CURRENT (5.1+ Compatibility)

  1. Main process starts the bootstrap script (e.g. iwr git.io/PSAnywhere | iex)
  2. Monitor for the process going into debugging state with a "sidecar" runspace that will watch the availability event for debug status
  3. When a process goes into debug, write to the main host that debug was detected and sidecar is starting
  4. Spawn a new namedpipe client that attaches to the relevant process' autogenerated named pipe
  5. Start a new instance of the golang ssh server and redirect stdio to the named pipe. The golang ssh server will have a powershell subsystem that simply interacts with stdin/out EDIT: Investigating using https://github.com/intothevoid/sshserver instead of sshdog
  6. [Optional] Expose the ssh port via rendevous service (Use gonnel/azure relay/websocat/inlets/whatever)
  7. You should now be able to enter-pssession -hostname -port <custom server port, will default to 22222> from the client and then debug-runspace (id) to debug the process.

This will work in vscode too and vscode debugger will pick up the debug and you can step there.

IDEAL (PS7.1+ only)

  1. Make a new generic transport handler for TCP sockets or websockets and build in the rendevous directly into the transport handler (client would have discovery mechanism for enter-pssession, server would auto-create rendevous). Unfortunately these classes are internal and sealed so making a "plugin" powershell module doesn't seem feasible, it would have to be added to powershell itself.
  2. This will eliminate dependence on golang ssh server (because a far as I can tell there are no good free C# ssh server implementations)
This file has been truncated, but you can view the full file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment