Powering up Powershell for Remote Desktop Services
Powershell is a powerful tool that lets us perform many tasks and gather incredibly diverse sets of information. When
Powershell remoting became available, this made it easy to do the same things on a remote system as on the local system.
For many server and workstations configurations, there is little more which one could ask of a shell with out of the box
functionality. Fortunately, Powershell is also easily extensible.
Due to the way some Windows APIs work, there are limitations to what information can be accessed from the normal
execution context of a Powershell host. This is particularly true in the case of the server role formerly known as Terminal
Services, Remote Desktop Services. If an administrator wants to get information about what is going on in a user session
of a graphical nature or other such things that are only visible in that session, they can’t simply just execute a cmdlet to get
it. With Windows Server 2008, Session 0 Isolation was added to Windows which prevents most UI interactions with user
sessions from services. Each session also gets its own window station and one or more desktops, further segregating UI
information.
It is possible to get around this issue with a bit of programming and elbow grease and create a Powershell context in another
session which can be used to obtain the information that the administrator wants. For the solution that I’m going to outline,
we’ll need the following things:
- Something to host a Runspace in the desired session
- Something to redirect input/output from that Runspace to the Runspace we’re using in the Powershell host that we can see;
this is a two-part requirement as that something needs to exist in both our Runspace and the Runspace in the other session - Something to actually put our Runspace host in the desired session
There is a program that has been used by administrators for years prior to the existence of Powershell called PSExec. It
performs the creation of new processes and redirects input/output. It uses a Windows service to execute functions that
need SYSTEM privileges. We’ll want to do similar things, and newer Microsoft Technologies make that much easier on us to program.
Windows Communication Foundation does all the heavy lifting for passing information between processes saving us the
trouble of needing to roll on our own IPC protocol with named pipes, sockets, etc. Using a NetNamedPipeBinding lets us
avoid many of the common firewall issues that could be encountered with using socket or web based communications.
Turning PSObjects returned from invoking commands in a Runspace is done with a single function call with one
parameter each way with the PSSerializer class.
Luckily, hosting a Runspace is pretty easy; there’s even an example in the documentation for the class. Code to do that
can fairly easily be made within a .NET executable. If you look at the example mentioned above, you’ll see that there are
some things that just wouldn’t work for our needs. We don’t need to actually display any output, we need to create a
Runspace that stays constant so that session state can be maintained, execute commands as we send them from our
Powershell host, and return the results. Since we’re using Powershell, it would also be preferable to return the results as
objects rather than just text; we can do that with the use of the PSSerializer class as mentioned above.
In order to launch a process in the right context with CreateProcessWithTokenW, we’ll need some code to execute as
SYSTEM. Since we’re presumably an administrator on the server, otherwise none of this should work, we can install
and communicate a service that runs as SYSTEM in order to meet this need. Running processes as SYSTEM gives them
the ability to do just about anything while erasing the evidence of what they’re doing if they so choose, so this is
something that generally should be avoided and when it has to be done, we’ll run as little code as needed in that context.
We’ll also want to be sure to create our process with only the security that it needs which we can do by actively restricting
the token or by finding a token that already exists in the session and using it.
In order to interact with Powershell, we’ll want to make two cmdlets. One cmdlet will be used to creation a new Runspace
in a specified session. The other will be used to invoke commands on that Runspace and return the results.
Taking all of the above into consideration, a sensible course of action would be to create the following:
- An executable that self-hosts a WCF service over a named pipe binding
that receives commands to invoke on a Runspace and returns back the
results as serialized PSObjects. - A service that hosts the executable in the SYSTEM context so it can be
used to create instances of the executable in the desired session(s). - A cmdlet that checks for the installation and state of the service,
remediates it if necessary, connects to it via WCF, asks it to return
information about the executable running in the specified session,
and then connects to the executable in the specified session via WCF. - A cmdlet that invokes arbitrary code in the specified session when
given the text to run and the session with which to communicate. It
then communicates with the executable, receives the results of the
code as serialized PSObjects, deserializes them, and writes them as
PSObjects to the current pipeline.
Assuming we call #3 New-SessionRunspace and #4 Invoke-SessionRunspace,
we could import our module and run the following at an elevated Powershell
prompt to execute some code in session X:
$sr = New-SessionRunspace X
Invoke-SessionRunspace X “[System.Diagnostics.Process]::GetCurrentProcess().SessionId”
Here’s an example implementation of this: https://1drv.ms/1vN5BMi (Source and Binaries
included)
Follow us on Twitter, https://www.twitter.com/WindowsSDK.