Saturday 2 June 2018

Adding a Command Line to PowerShell's Process Listing

My NtObjectManager PowerShell module has plenty of useful functions and cmdlets, but it's not really designed for general use-cases such as system administration. I was reminded of this when I got a reply to a tweet I made announcing a new version of the module.

While the Get-NtProcess cmdlet does have a CommandLine property it's not really a good idea to use it just for that. Each process object returned from the cmdlet is an instance of the NtProcess class which maintains an open handle. While the garbage collector should eventually kick in and clean up for you it's still bad practice to leave open handles lying around.

Wouldn't it be useful if you could get the command line of a process without requiring a large third party module? As pointed out in the tweet you can use WMI but it's uglier than calling Get-Process. It also has a small flaw, it doesn't show the command lines for elevated processes on the same desktop which Get-NtProcess can do (at least on Windows 8 and above).

So I decided to investigate how I might add the command line to the existing Get-Process cmdlet in the simplest way possible. To do so I expose some functionality in PowerShell which I'm guessing few realise exists, or for that matter need to use. First let's see what type of object Get-Process is returning. We can call GetType on the returned objects and find that out.

PS C:\> $ps = Get-Process
PS C:\> $ps[0].GetType() | select Fullname

FullName
--------
System.Diagnostics.Process

We can see that it's just returning the list of normal framework Process objects. Nothing too surprising there, however one thing is interesting, the object has more properties available than the Process class in the framework. A good example is the Path property which returns the full path to the main executable:

PS C:\> $ps[0] | select Path

Path
----
C:\Program Files (x86)\Google\Chrome\Application\chrome.exe

Where does that come from? Using the Get-Member cmdlet gives us a clue.

PS C:\> $ps[0] | Get-Member Path

   TypeName: System.Diagnostics.Process

Name MemberType     Definition
---- ----------     ----------
Path ScriptProperty System.Object Path {get=$this.Mainmodule.FileName;}

Very interesting, it seems to a script property, after some digging it turns out this script is added in something called a Type Extension file which has been around since the early days of PowerShell. It allows you to add arbitrary properties and methods to existing types. The extensions for the Process class are in $PSHome\types.ps1xml, a snippet is shown below.

<Type>
 <Name>System.Diagnostics.Process</Name>
 <Members>
  <ScriptProperty>
   <Name>Path</Name>
    <GetScriptBlock>$this.Mainmodule.FileName</GetScriptBlock>
   </ScriptProperty>
...

Of course what I should have done first is just checked Lee Holmes blog where he wrote a description of these Type Extension files a mere 12 years ago! Anyway you can also get the help for this feature using running Get-Help about_Types.

This sounds ideal, we can add our create out own Type Extension file and add a scripted property to pull out the command line. We just need to write it, the easiest solution would be to use my NtApiDotNet .NET library, but if you're using that you might as well just use the NtObjectManager module to begin with as the library is what the module is built on. Therefore, we'll need to re-implement everything in C# using Add-Type then just invoke that to get the command line when necessary. This is not too hard, I just based it on the code in my library.

If you copy the gist into a file with a ps1xml extension you can then add the extension to the current session using the Update-TypeData cmdlet and passing the path to the file. If you want this to persist you can add the call to your profile, or save the session and reload it.

PS C:\> Update-TypeData .\command_line_type.ps1xml
PS C:\> Get-Process explorer | select CommandLine

CommandLine
-----------
C:\WINDOWS\Explorer.EXE

I've tried to make it as compact as possible, for example I don't enable SeDebugPrivilege which would be useful for administrators as it'd allow you to read the command line from almost any process on the system. You could add that feature if you like. One thing I had to do is also call OpenProcess on the PID, which is odd as the Process class actually has a SafeHandle property which returns a native handle for the process. Unfortunately the framework opens this handle with PROCESS_ALL_ACCESS rights, not the limited PROCESS_QUERY_LIMITED_INFORMATION access we require. This means that elevated processes can not be opened, removing the advantage this approach gives us.

This is also the reason WMI doesn't return all command lines. The WMI host process impersonates the caller when querying the command line for a process. WMI then uses an old method of extracting the command line by reading the command line directly from memory (using a technique similar to this StackOverflow post). As this requires PROCESS_VM_READ access this fails with elevated processes. Perhaps they should move to the NtQueryInformationProcess approach on modern versions of Windows ;-)

PS C:\> start -verb runas notepad test.txt
PS C:\> Get-WmiObject Win32_process | ? Name -eq "notepad.exe" | select CommandLine

CommandLine
-----------

PS C:\> Get-Process notepad | select CommandLine

CommandLine
-----------
"C:\WINDOWS\system32\notepad.exe" test.txt

Hope you find this information useful, there's loads of useful functionality in PowerShell which can make your life much easier. You just have to find it first :-)