-
-
Save jasper22/b7dac41ec82786163fd05f6a46bfb24a to your computer and use it in GitHub Desktop.
Log PSRP packets to a file and subsequently parse them into rich PSObjects
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright: (c) 2020, Jordan Borean (@jborean93) <[email protected]> | |
| # MIT License (see LICENSE or https://opensource.org/licenses/MIT) | |
| Add-Type -TypeDefinition @' | |
| using System; | |
| using System.IO; | |
| using System.IO.Pipes; | |
| using System.Management.Automation; | |
| using System.Management.Automation.Runspaces; | |
| using System.Threading; | |
| namespace NaughtyPipe | |
| { | |
| public class Delegator : IDisposable | |
| { | |
| private RunspacePool _runspacePool; | |
| private PipeStream _originRead; | |
| private PipeStream _originWrite; | |
| private PipeStream _targetRead; | |
| private PipeStream _targetWrite; | |
| private ScriptBlock _originDelegate; | |
| private ScriptBlock _targetDelegate; | |
| private Thread _readThread; | |
| private Thread _writeThread; | |
| public Delegator(PipeStream origin, ScriptBlock originDelegate, | |
| PipeStream target, ScriptBlock targetDelegate) | |
| : this(origin, origin, originDelegate, target, target, targetDelegate) | |
| { } | |
| public Delegator(PipeStream originRead, PipeStream originWrite, ScriptBlock originDelegate, | |
| PipeStream targetRead, PipeStream targetWrite, ScriptBlock targetDelegate) | |
| { | |
| if (!originRead.CanRead) | |
| throw new ArgumentException("Must be able to read from originRead"); | |
| if (!originWrite.CanWrite) | |
| throw new ArgumentException("Must be able to write to originWrite"); | |
| if (!targetRead.CanRead) | |
| throw new ArgumentException("Must be able to read from targetRead"); | |
| if (!targetWrite.CanWrite) | |
| throw new ArgumentException("Must be able to write to targetWrite"); | |
| _originRead = originRead; | |
| _originWrite = originWrite; | |
| _originDelegate = originDelegate; | |
| _targetRead = targetRead; | |
| _targetWrite = targetWrite; | |
| _targetDelegate = targetDelegate; | |
| _runspacePool = RunspaceFactory.CreateRunspacePool(2, 2); | |
| } | |
| public void Start() | |
| { | |
| _runspacePool.Open(); | |
| _readThread = new Thread(() => Runner(_originRead, _targetWrite, _originDelegate)); | |
| _readThread.Start(); | |
| _writeThread = new Thread(() => Runner(_targetRead, _originWrite, _targetDelegate)); | |
| _writeThread.Start(); | |
| } | |
| public void Wait() | |
| { | |
| _readThread.Join(); | |
| _writeThread.Join(); | |
| } | |
| private void Runner(PipeStream readPipe, PipeStream writePipe, ScriptBlock delegateFunc) | |
| { | |
| try | |
| { | |
| using (StreamReader sr = new StreamReader(readPipe)) | |
| using (StreamWriter sw = new StreamWriter(writePipe)) | |
| { | |
| while (true) | |
| { | |
| string line = line = sr.ReadLine(); | |
| if (String.IsNullOrEmpty(line)) | |
| break; | |
| using (PowerShell pipeline = PowerShell.Create()) | |
| { | |
| pipeline.RunspacePool = _runspacePool; | |
| pipeline.AddScript(delegateFunc.ToString(), true); | |
| pipeline.AddArgument(line); | |
| pipeline.Invoke(); | |
| } | |
| sw.WriteLine(line); | |
| sw.Flush(); | |
| } | |
| } | |
| } | |
| catch (Exception e) | |
| { | |
| if (e is IOException || e is ObjectDisposedException) | |
| return; | |
| throw; | |
| } | |
| } | |
| public void Dispose() | |
| { | |
| if (_readThread != null) | |
| _readThread.Join(); | |
| if (_writeThread != null) | |
| _writeThread.Join(); | |
| _runspacePool.Dispose(); | |
| GC.SuppressFinalize(this); | |
| } | |
| ~Delegator() { this.Dispose(); } | |
| } | |
| } | |
| '@ | |
| enum Destination { | |
| Client = 0x00000001 | |
| Server = 0x00000002 | |
| } | |
| enum MessageType { | |
| SESSION_CAPABILITY = 0x00010002 | |
| INIT_RUNSPACEPOOL = 0x00010004 | |
| PUBLIC_KEY = 0x00010005 | |
| ENCRYPTED_SESSION_KEY = 0x00010006 | |
| PUBLIC_KEY_REQUEST = 0x00010007 | |
| CONNECT_RUNSPACEPOOL = 0x00010008 | |
| RUNSPACEPOOL_INIT_DATA = 0x0002100B | |
| RESET_RUNSPACE_STATE = 0x0002100C | |
| SET_MAX_RUNSPACES = 0x00021002 | |
| SET_MIN_RUNSPACES = 0x00021003 | |
| RUNSPACE_AVAILABILITY = 0x00021004 | |
| RUNSPACEPOOL_STATE = 0x00021005 | |
| CREATE_PIPELINE = 0x00021006 | |
| GET_AVAILABLE_RUNSPACES = 0x00021007 | |
| USER_EVENT = 0x00021008 | |
| APPLICATION_PRIVATE_DATA = 0x00021009 | |
| GET_COMMAND_METADATA = 0x0002100A | |
| RUNSPACEPOOL_HOST_CALL = 0x00021100 | |
| RUNSPACEPOOL_HOST_RESPONSE = 0x00021101 | |
| PIPELINE_INPUT = 0x00041002 | |
| END_OF_PIPELINE_INPUT = 0x00041003 | |
| PIPELINE_OUTPUT = 0x00041004 | |
| ERROR_RECORD = 0x00041005 | |
| PIPELINE_STATE = 0x00041006 | |
| DEBUG_RECORD = 0x00041007 | |
| VERBOSE_RECORD = 0x00041008 | |
| WARNING_RECORD = 0x00041009 | |
| PROGRESS_RECORD = 0x00041010 | |
| INFORMATION_RECORD = 0x00041011 | |
| PIPELINE_HOST_CALL = 0x00041100 | |
| PIPELINE_HOST_RESPONSE = 0x00041101 | |
| } | |
| Function ConvertTo-PSSessionFragment { | |
| <# | |
| .SYNOPSIS | |
| Convert a raw PSRP fragment to an object. | |
| .PARAMETER InputObject | |
| The fragment(s) bytes. | |
| .EXAMPLE | |
| $rawFragment = [Convert]::FromBase64String($fragmentSource) | |
| ConvertTo-PSSessionFragment -InputObject $rawFragment | |
| .OUTPUTS | |
| PSSession.Fragment | |
| ObjectID = The unique identifier for a fragmented PSRP message. | |
| FragmentID = The unique identifier of the fragments in a fragmented PSRP message. | |
| Start = Whether this is the start PSRP message fragment for the ObjectID (PSRP Message). | |
| End = Whether this is the last PSRP message fragment for the ObjectID (PSRP Message). | |
| Blob = The PSRP message fragment bytes. | |
| .NOTES | |
| A raw fragment from a PSSession can contain 1, or multiple fragments which this cmdlet will output all of them. | |
| The structure of this fragment is documented in [MS-PSRP] 2.2.4 Packet Fragment | |
| https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-psrp/3610dae4-67f7-4175-82da-a3fab83af288. | |
| #> | |
| [OutputType('PSSession.Fragment')] | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory=$true)] | |
| [byte[]] | |
| $InputObject | |
| ) | |
| while ($InputObject) { | |
| # The integer values are in network binary order so we need to reverse the entries. | |
| [Array]::Reverse($InputObject, 0, 8) | |
| $objectId = [BitConverter]::ToUInt64($InputObject, 0) | |
| [Array]::Reverse($InputObject, 8, 8) | |
| $fragmentId = [BitConverter]::ToUInt64($InputObject, 8) | |
| $startEndByte = $InputObject[16] | |
| $start = [bool]($startEndByte -band 0x1) | |
| $end = [bool]($startEndByte -band 0x2) | |
| [Array]::Reverse($InputObject, 17, 4) | |
| $length = [BitConverter]::ToUInt32($InputObject, 17) | |
| [byte[]]$blob = $InputObject[21..(20 + $length)] | |
| $InputObject = $InputObject[(21 + $length)..($InputObject.Length)] | |
| if ($start -and $fragmentId -ne 0) { | |
| Write-Error -Message "Fragment $objectId start is expecting a fragment ID of 0 but got $fragmentId" | |
| continue | |
| } | |
| [PSCustomObject]@{ | |
| PSTypeName = 'PSSession.Fragment' | |
| ObjectID = $objectId | |
| FragmentID = $fragmentId | |
| Start = $start | |
| End = $end | |
| Blob = $blob | |
| } | |
| } | |
| } | |
| Function ConvertTo-PSSessionMessage { | |
| <# | |
| .SYNOPSIS | |
| Convert a completed PSRP fragment to a PSRP message object. | |
| .PARAMETER InputObject | |
| The completed fragment bytes. | |
| .PARAMETER ObjectID | |
| The ObjectID of the fragment(s) the PSRP message belonged to. | |
| .EXAMPLE | |
| $rawFragment = [Convert]::FromBase64String($fragmentSource) | |
| ConvertTo-PSSessionFragment -InputObject $rawFragment | ForEach-Object { | |
| if ($_.Start -and $_.End) { | |
| ConvertTo-PSSessionMessage -InputObject $_.Blob -ObjectID $_.ObjectID | |
| } | |
| } | |
| .OUTPUTS | |
| PSSession.Message | |
| ObjectID = The unique identifier for the fragment the PSRP message belongs to. | |
| Destination = The destination of the message | |
| MessageType = The type of the message. | |
| RPID = The RunspacePool ID as a GUID the message targets. | |
| PID = The Pipeline ID as a GUID the message targets. | |
| Message = The parsed message as a PSObject. | |
| Raw = The raw CLIXML of the message as a string. | |
| .NOTES | |
| The structure of this message is documented in [MS-PSRP] 2.2.1 PowerShell Remoting Protocol Message. | |
| https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-psrp/497ac440-89fb-4cb3-9cc1-3434c1aa74c3 | |
| #> | |
| [OutputType('PSSession.Message')] | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory=$true)] | |
| [byte[]] | |
| $InputObject, | |
| [Parameter(Mandatory=$true)] | |
| [UInt64] | |
| $ObjectID | |
| ) | |
| $destination = [Destination][BitConverter]::ToInt32($InputObject, 0) | |
| $messageType = [MessageType][BitConverter]::ToInt32($InputObject, 4) | |
| $rpIdBytes = $InputObject[8..23] | |
| $rpId = [Guid]::new([byte[]]$rpIdBytes) | |
| $psIdBytes = $InputObject[24..39] | |
| $psId = [Guid]::new([byte[]]$psIdBytes) | |
| # Handle if the blob contains the UTF-8 BOM or not. | |
| $startIdx = 40 | |
| if ($InputObject[40] -eq 239 -and $InputObject[41] -eq 187 -and $InputObject[42] -eq 191) { | |
| $startIdx = 43 | |
| } | |
| [byte[]]$dataBytes = $InputObject[$startIdx..$InputObject.Length] | |
| $message = [Text.Encoding]::UTF8.GetString($dataBytes) | |
| $tmpPath = [IO.Path]::GetTempFileName() | |
| try { | |
| Set-Content -LiteralPath $tmpPath -Value @" | |
| <Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"> | |
| $message | |
| </Objs> | |
| "@ | |
| $psObject = Import-Clixml -LiteralPath $tmpPath | |
| } | |
| finally { | |
| Remove-Item -Path $tmpPath | |
| } | |
| # Make our CLIXML pretty with indents so it can be easily parsed by a human | |
| $stringWriter = [IO.StringWriter]::new() | |
| $xmlWriter = $null | |
| try { | |
| $xmlWriter = [Xml.XmlTextWriter]::new($stringWriter) | |
| $xmlWriter.Formatting = [Xml.Formatting]::Indented | |
| $xmlWriter.Indentation = 2 | |
| ([xml]$message).WriteContentTo($xmlWriter) | |
| $xmlWriter.Flush() | |
| $stringWriter.Flush() | |
| $prettyXml = $stringWriter.ToString() | |
| } | |
| finally { | |
| if ($xmlWriter) { | |
| $xmlWriter.Dispose() | |
| } | |
| $stringWriter.Dispose() | |
| } | |
| [PSCustomObject]@{ | |
| PSTypeName = 'PSSession.Message' | |
| ObjectID = $ObjectID | |
| Destination = $destination | |
| MessageType = $messageType | |
| RPID = $rpId | |
| PID = $psId | |
| Message = $psObject | |
| Raw = $prettyXml | |
| } | |
| } | |
| Function ConvertTo-PSSessionPacket { | |
| <# | |
| .SYNOPSIS | |
| Parse the PSRP packets generated by New-PSSessionLogger into a rich PSObject. | |
| .PARAMETER InputObject | |
| The OutOfProc PSRP XML packet to convert. | |
| .EXAMPLE | |
| $log = 'C:\temp\pssession.log' | |
| Remove-Item -Path $log -ErrorAction SilentlyContinue | |
| $session = New-PSSessionLogger -LogPath $log | |
| try { | |
| Invoke-Command -Session $session -ScriptBlock { echo "hi" } | |
| } | |
| finally { | |
| $session | Remove-PSSession | |
| } | |
| Get-Content -Path $log | ConvertTo-PSSessionPacket | |
| .OUTPUTS | |
| PSSession.Packet | |
| Type = The OutOfProc XML element type. | |
| PSGuid = The PSGuid assigned to the packet | |
| Stream = The stream of the packet (only when Type -eq 'Data') | |
| Fragments = The fragments contains in the packet (only when Type -eq 'Data') | |
| Messages = The completed PSRP messages in the fragments (only when Type -eq 'Data') | |
| Raw = The raw OutOfProc XML value. | |
| #> | |
| [OutputType('PSSession.Packet')] | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory=$true, ValueFromPipeline=$true)] | |
| [String[]] | |
| $InputObject | |
| ) | |
| begin { | |
| $fragmentBuffer = @{} | |
| } | |
| process { | |
| foreach ($packet in $InputObject) { | |
| $xmlData = ([xml]$packet).DocumentElement | |
| $fragments = $null | |
| $messages = $null | |
| if ($xmlData.Name -eq 'Data') { | |
| $rawFragment = [Convert]::FromBase64String($xmlData.'#text') | |
| $fragments = ConvertTo-PSSessionFragment -InputObject $rawFragment | |
| $messages = $fragments | ForEach-Object -Process { | |
| if ($_.Start) { | |
| $fragmentBuffer.($_.ObjectID) = [Collections.Generic.List[Byte]]@() | |
| } | |
| $buffer = $fragmentBuffer.($_.ObjectID) | |
| $buffer.AddRange($_.Blob) | |
| if ($_.End) { | |
| $fragmentBuffer.Remove($_.ObjectID) | |
| ConvertTo-PSSessionMessage -InputObject $buffer -ObjectID $_.ObjectID | |
| } | |
| } | |
| } | |
| [PSCustomObject]@{ | |
| PSTypeName = 'PSSession.Packet' | |
| Type = $xmlData.Name | |
| PSGuid = $xmlData.PSGuid | |
| Stream = $xmlData.Stream | |
| Fragments = $fragments | |
| Messages = $messages | |
| Raw = $packet | |
| } | |
| } | |
| } | |
| end { | |
| foreach ($kvp in $fragmentBuffer.GetEnumerator()) { | |
| Write-Warning -Message "Incomplete buffer for fragment $($kvp.Key)" | |
| } | |
| } | |
| } | |
| Function Watch-PSSessionLog { | |
| <# | |
| .SYNOPSIS | |
| Watches a PSSession logging file and outputs parsed PSSession packets as they come in. | |
| .PARAMETER Path | |
| The log file to watch. | |
| .PARAMETER ScanHistory | |
| Process any existing entries in the log file before waiting for new events. | |
| .PARAMETER Wait | |
| Keep on reading the log file even once a session has closed. | |
| .EXAMPLE | |
| Watch-PSSessionLog -Path C:\temp\pssession.log | |
| .NOTES | |
| This cmdlet blocks until the pipeline is cancelled with ctrl+c or the session was closed. | |
| #> | |
| [OutputType('PSSession.Packet')] | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory=$true)] | |
| [String] | |
| $Path, | |
| [Switch] | |
| $ScanHistory, | |
| [Switch] | |
| $Wait | |
| ) | |
| process { | |
| $fs = [IO.File]::Open($Path, [IO.FileMode]::OpenOrCreate, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite) | |
| $sr = $null | |
| try { | |
| if (-not $ScanHistory) { | |
| $null = $fs.Seek(0, [IO.SeekOrigin]::End) | |
| } | |
| $waitUntil = $fs.Length | |
| $sr = [IO.StreamReader]::new($fs) | |
| while ($true) { | |
| $line = $sr.ReadLine() | |
| if (-not $line) { | |
| if ($Wait -or ($fs.Position -le $waitUntil)) { | |
| continue | |
| } | |
| else { | |
| break | |
| } | |
| } | |
| $line | ConvertTo-PSSessionPacket | |
| } | |
| } | |
| finally { | |
| if ($sr) { | |
| $sr.Dispose() | |
| } | |
| $fs.Dispose() | |
| } | |
| } | |
| } | |
| Function Format-PSSessionPacket { | |
| <# | |
| .SYNOPSIS | |
| Formats a PSSession.Packet to a more human friendly output. | |
| .PARAMETER InputObject | |
| The PSSession.Packet object to format. | |
| .EXAMPLE | |
| Watch-PSSessionLog -Path C:\temp\pssession.log | Format-PSSessionPacket | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory=$true, ValueFromPipeline=$true)] | |
| [PSTypeName('PSSession.Packet')] | |
| $InputObject | |
| ) | |
| process { | |
| # The properties are padded to the length of the longest property | |
| $padding = "Fragments".Length + 1 | |
| $valuePadding = " " * ($padding + 2) | |
| $formatComplexValue = { | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory=$true, ValueFromPipeline=$true)] | |
| $InputObject, | |
| [int] | |
| $PaddingLength = 0 | |
| ) | |
| $padding = " " * $PaddingLength | |
| # Get the length of the longest property | |
| $propertyPadding = 0 | |
| foreach ($prop in $InputObject.PSObject.Properties.Name) { | |
| if ($prop.Length -gt $propertyPadding) { | |
| $propertyPadding = $prop.Length | |
| } | |
| } | |
| $sb = [Text.StringBuilder]::new() | |
| foreach ($prop in $InputObject.PSObject.Properties) { | |
| $formattedValue = $prop.Value | |
| if ('System.Management.Automation.PSCustomObject' -in $formattedValue.PSTypeNames) { | |
| $formattedValue = @($formattedValue) | |
| } | |
| if ($formattedValue -is [Array]) { | |
| $formattedValue = foreach ($entry in $formattedValue) { | |
| if ($entry -is [PSCustomObject]) { | |
| $valuePadding = $propertyPadding + 3 | |
| $entry = foreach ($subEntry in $entry) { | |
| ($subEntry | &$formatComplexValue -PaddingLength $valuePadding).Trim() | |
| } | |
| $entry = $entry -join "`n" | |
| } | |
| $entry.Trim() | |
| } | |
| $formattedValue = $formattedValue -join ("`n`n" + " " * $valuePadding) | |
| } | |
| $null = $sb. | |
| Append($padding). | |
| Append($prop.Name). | |
| Append(" " * ($propertyPadding - $prop.Name.Length)). | |
| Append(" : $formattedValue`n") | |
| } | |
| $sb.ToString() | |
| } | |
| $obj = $InputObject | Select-Object -Property @( | |
| 'Type', | |
| @{ N = 'PSGuid'; E = { $_.PSGuid.ToString() } }, | |
| 'Stream', | |
| @{ | |
| N = 'Fragments' | |
| E = { | |
| @($_.Fragments | Select-Object -Property @( | |
| 'ObjectID', | |
| 'FragmentID', | |
| 'Start', | |
| 'End', | |
| @{ N = 'Length'; E = { $_.Blob.Length } } | |
| )) | |
| } | |
| }, | |
| @{ | |
| N = 'Messages' | |
| E = { | |
| @($_.Messages | Select-Object -Property @( | |
| 'ObjectID', | |
| 'Destination', | |
| 'MessageType', | |
| @{ N = 'RPID'; E = { $_.RPID.ToString() } }, | |
| @{ N = 'PID'; E = { $_.PID.ToString() } }, | |
| @{ N = 'Object'; E = { "`n" + $_.Raw } } | |
| )) | |
| } | |
| } | |
| ) | |
| $msg = $obj | &$formatComplexValue | |
| Write-Host $msg | |
| } | |
| } | |
| Function New-PSSessionLogger { | |
| <# | |
| .SYNOPSIS | |
| Create a new PSSession that logs the PSRP data packets. | |
| .PARAMETER FilePath | |
| Create a new PowerShell process to attach to as the PSSession target. Other use -Name to attach to an existing | |
| bidirectional named pipe. | |
| .PARAMETER ArgumentList | |
| Optional arguments when starting a new process. | |
| .PARAMETER Name | |
| Instead of starting a new process, attach the PSSession to the named pipe specified. This must be a bidirectional | |
| pipe that can send and receive PSRP packets. | |
| .PARAMETER LogPath | |
| The path to log the PSRP packets exchanged between the client and server. | |
| .EXAMPLE Open a logged session to Windows PowerShell | |
| $session = New-PSSessionLogger -LogPath pssession.log | |
| Invoke-Command -Session $session -ScriptBlock { $PSVersionTable } | |
| $session.Dispose() | |
| .EXAMPLE Open a logged session to PowerShell | |
| $session = New-PSSessionLogger -LogPath pssession.log -FilePath pwsh | |
| Invoke-Command -Session $session -ScriptBlock { $PSVersionTable } | |
| $session.Dispose() | |
| .OUTPUTS PSSession | |
| This is a PSSession object that can be used with Enter-PSSession/Invoke-Command. Make sure you call the | |
| '.Dispose()' method on this object to clean up any resources running in the background. | |
| .NOTES | |
| This is a proof of concept and not really safe at all. Requires PowerShell 6+. Make sure you call .Dispose | |
| #> | |
| [OutputType([Management.Automation.Runspaces.PSSession])] | |
| [CmdletBinding(DefaultParameterSetName='Process')] | |
| param ( | |
| [Parameter(Mandatory=$true)] | |
| [String] | |
| $LogPath, | |
| [Parameter(ParameterSetName='Process')] | |
| [String] | |
| $FilePath = 'powershell', | |
| [Parameter(ParameterSetName='Process')] | |
| [String[]] | |
| $ArgumentList = @('-NoProfile', '-NoLogo'), | |
| [Parameter(ParameterSetName='Pipe')] | |
| [String] | |
| $Name | |
| ) | |
| # The delegate is run in a separate runspace so it doesn't have access to our vars. | |
| $onLine = [ScriptBlock]::Create(@' | |
| [CmdletBinding()] | |
| param ([String]$Data) | |
| Add-Content -LiteralPath '{0}' -Value $Data | |
| '@ -f $LogPath) | |
| $process = $null | |
| $disposables = [Collections.Generic.List[PSObject]]@() | |
| try { | |
| if ($PSCmdlet.ParameterSetName -eq 'Process') { | |
| $process = Start-Process -FilePath $FilePath -ArgumentList $ArgumentList -PassThru -WindowStyle Hidden | |
| $Name = 'PSHost.{0}.{1}.DefaultAppDomain.{2}' -f ( | |
| # UNIX: $process.StartTime.ToFileTime().ToString("X8").Substring(1,8) | |
| $process.StartTime.ToFileTime().ToString([CultureInfo]::InvariantCulture), | |
| $process.Id, | |
| $process.ProcessName | |
| ) | |
| } | |
| # This is the InOut pipe of the peer we want to connect to. | |
| $targetPipe = [IO.Pipes.NamedPipeClientStream]::new( | |
| '.', | |
| $Name, | |
| [IO.Pipes.PipeDirection]::InOut, | |
| [IO.Pipes.PipeOptions ]::Asynchronous | |
| ) | |
| $targetPipe.Connect() | |
| $disposables.Add($targetPipe) | |
| # This is our intermediate pipe that our PSSession connects to and then forwards onto targetPipe. | |
| $pipeName = 'Naughty-{0}' -f [Guid]::NewGuid() | |
| $naughtyPipe = [IO.Pipes.NamedPipeServerStream]::new( | |
| $pipeName, | |
| [IO.Pipes.PipeDirection]::InOut, | |
| 1, | |
| [IO.Pipes.PipeTransmissionMode]::Byte, | |
| [IO.Pipes.PipeOptions ]::Asynchronous | |
| ) | |
| $disposables.Add($naughtyPipe) | |
| $naughtyPipeWait = $naughtyPipe.BeginWaitForConnection($null, $null) | |
| # Runspace has OpenAsync but it does not have an waitable result so we run the blocking version in a separate | |
| # pipeline. This Runspace targets our intermediate pipe which then forwards to the target pipe. | |
| $ps = [PowerShell]::Create() | |
| $disposables.Add($ps) | |
| $null = $ps.AddScript({ | |
| $connInfo = [Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($args[0]) | |
| $rs = [RunspaceFactory]::CreateRunspace($connInfo, $args[1], $null) | |
| $rs.Open() | |
| $rs | |
| }).AddArgument($pipeName).AddArgument($Host) | |
| $rsConnectWait = $ps.BeginInvoke() | |
| # Now the Runspace open is running in the background we can wait until it's connected. | |
| $naughtyPipe.EndWaitForConnection($naughtyPipeWait) | |
| $delegator = [NaughtyPipe.Delegator]::new($naughtyPipe, $onLine, $targetPipe, $onLine) | |
| $disposables.Add($delegator) | |
| $delegator.Start() | |
| # Get the Runspace and use reflection to build a PSSession object from it. | |
| $rs = $ps.EndInvoke($rsConnectWait)[0] | |
| $rs = $rs -as $rs.GetType() # EndInvoke() returns as a PSObject and the constructor chokes on that. | |
| $cstr = [Management.Automation.Runspaces.PSSession].GetConstructor( | |
| 'NonPublic, Instance', $null, [type[]]$rs.GetType(), $null) | |
| $session = $cstr.Invoke(@($rs)) | |
| # Add a .Dispose() method that disposes of our resources. | |
| $disposeParams = @{ | |
| Name = 'Dispose' | |
| MemberType = 'ScriptMethod' | |
| Value = { | |
| $session | Remove-PSSession | |
| Add-Content -Path $LogPath -Value '' | |
| if ($process) { | |
| $process | Stop-Process -Force | |
| } | |
| $delegator.Dispose() | |
| $naughtyPipe.Dispose() | |
| $targetPipe.Dispose() | |
| }.GetNewClosure() | |
| } | |
| $session | Add-Member @disposeParams -PassThru | |
| } | |
| catch { | |
| if ($process) { | |
| $process | Stop-Process -Force | |
| } | |
| foreach ($waste in $disposables) { | |
| $waste.Dispose() | |
| } | |
| throw | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment