I love this module for one simple feature - Get-NtProcess has a CommandLine property that shows you the entire command line (including args) used to start the process. Yeah, you can get that with WMI/CIM but this is easier. :-)— r_keith_hill (@r_keith_hill) May 25, 2018
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 :-)