VB.NET Get command line for external process

June 10, 2010 — 12 Comments

I’ve been working on this for a few days and have finally got it doing what it is supposed to do, so thought I would post it here in case it helps someone else out. Basically this VB.NET code will get the command line for an external process, even if your application did not start that process (which is the limitation you have if you try using Process.StartInfo). This gets the full command line that was used to start the process so it includes the path to the executable and any command line arguments / parameters specified.

The first thing I should point out is that this only works on either a 32 bit system or on a 64 bit system where your process and the process that you want to get the command line for are both 32 bit processes (running in WOW64 mode). I’m still working on parsing the PEB for the 64 bit versions of Windows but hopefully will have a 64 bit version up and running soon. My code has been tested on Windows XP 32 bit and Windows 7 64 bit. If you want to get the command line for a process not running under your own user account then you must be an administrator and on Windows 7 (and presumably Vista) you must do Run As Administrator.

Second thing I should point out is that this method makes use of my NativeMemoryReader class, which you can get the source code for here: http://www.vbforums.com/showthread.php?p=3819578

Third thing to point out is that this method along with loads of others that wrap native Windows API functionality in easy to use .NET methods will be included in a class library (imaginatively named Cjwdev.WindowsAPI) that I will be releasing soon. Keep an eye on this blog for more details over the next couple of weeks.

So here are the definitions for the APIs that this method makes use of and some constants I’ve declared.

''' <summary>
''' Retrieves information about a specific process, returns 0 if operation was successful
''' </summary>
''' <param name="handle">A handle to the process to get information for</param>
''' <param name="processinformationclass">The level of information to retrieve, 0 = basic</param>
''' <param name="ProcessInformation">OUTPUT An instance of the Basic_Process_Information class that will be populated with information</param>
''' <param name="ProcessInformationLength">The size of the ProcessInformation parameter</param>
''' <param name="ReturnLength">OUTPUT The amount of data that was written to the object passed in to the ProcessInformation parameter</param>
<DllImport("ntdll.dll", EntryPoint:="NtQueryInformationProcess")> _
Public Shared Function NtQueryInformationProcess(ByVal handle As IntPtr, ByVal processinformationclass As UInteger, ByRef ProcessInformation As Process_Basic_Information, ByVal ProcessInformationLength As Integer, ByRef ReturnLength As UInteger) As Integer
End Function

''' <summary>
''' Holds basic information about a process, used by NtQueryInformationProcess function
''' </summary>
<StructLayoutAttribute(LayoutKind.Sequential)> _
    Public Structure PROCESS_BASIC_INFORMATION
    Public Reserved1 As System.IntPtr
    Public PebBaseAddress As IntPtr
    <MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst:=2, ArraySubType:=UnmanagedType.SysUInt)> _
    Public Reserved2() As System.IntPtr
    Public UniqueProcessId As UInteger
    Public Reserved3 As System.IntPtr
End Structure

Const PROC_PARAMS_OFFSET As Integer = 16
Const CMDLINE_LENGTH_OFFSET As Integer = 64

The NtQueryInformationProcess API declared above is an internal API that the Microsoft documentation states “could change with the next release of Windows” but as mentioned earlier I’ve tested it with XP and Windows 7 and it was fine on both.

and here is my function that returns the command line (as a string):

''' <summary>
''' Gets the command line that was used to start a process, including any command line arguments
''' </summary>
''' <param name="TargetProcess">The process to get the command line for</param>
Public Shared Function GetCommandLineX86(ByVal TargetProcess As Process) As String
    'Open the process ready for memory reading
    Using MemoryReader As New NativeMemoryReader(TargetProcess)
        Dim ProcessInfo As New PROCESS_BASIC_INFORMATION
        'Get basic information about the process, including the PEB address
        Dim Result As Integer = NtQueryInformationProcess(MemoryReader.TargetProcessHandle, 0, ProcessInfo, Marshal.SizeOf(ProcessInfo), 0)
        If Not Result = 0 Then
            Throw New System.ComponentModel.Win32Exception(Result)
        End If
        Dim ParamPtrBytes(3) As Byte
        'Get a byte array that represents the pointer held in the ProcessParameters member of the PEB structure
        ParamPtrBytes = MemoryReader.ReadMemory(New IntPtr(ProcessInfo.PebBaseAddress.ToInt32 + PROC_PARAMS_OFFSET), ParamPtrBytes.Length – 1)
        If ParamPtrBytes Is Nothing Then
            Throw New ApplicationException("Memory could not be read from PebBaseAddress + " & PROC_PARAMS_OFFSET & ". Ensure that you have access to the full range of memory specified")
        End If
        'Convert the byte array to a pointer so that we now have the address for the RTL_USER_PROCESS_PARAMETERS structure
        Dim ParamPtr As IntPtr = New IntPtr(BitConverter.ToInt32(ParamPtrBytes, 0))
        Dim ParamBytes(7) As Byte
        'Read 8 bytes from the start of the CommandLine member of the RTL_USER_PROCESS_PARAMETERS structure
        ParamBytes = MemoryReader.ReadMemory(New IntPtr(ParamPtr.ToInt32 + CMDLINE_LENGTH_OFFSET), ParamBytes.Length – 1)
        If ParamBytes Is Nothing Then
            Throw New ApplicationException("Memory could not be read from ProcessParameters + " & CMDLINE_LENGTH_OFFSET & ". Ensure that you have access to the full range of memory specified")
        End If
        'The first 2 bytes in the CommandLine member tell us the length of the command line string in bytes
        Dim CmdLineLength As Integer = BitConverter.ToInt16(ParamBytes, 0)
        'The last 4 bytes of the CommandLine member are a pointer to the actual command line string
        Dim CmdLinePtr As New IntPtr(BitConverter.ToInt32(ParamBytes, 4))
        'Now that we have the address and length of the string, we can read it into a byte array
        Dim CmdLineBytes() As Byte = MemoryReader.ReadMemory(CmdLinePtr, CmdLineLength – 1)
        'No more memory needs reading from the process so close the handle
        MemoryReader.Close()
        'Return a string representation of the bytes we got for the command line string
        Return System.Text.Encoding.Unicode.GetString(CmdLineBytes).Trim
    End Using
End Function

So once you have copied that method and the API definitions above into your project then if you wanted to get the command line for each svchost.exe process then you could do this for example (on a 32 bit OS anyway, as svchost is a 64 bit process on a 64 bit OS) :

'Loop through each svchost process
For Each Proc As Process In Process.GetProcessesByName("svchost")
    'Get the command line and show it in a messagebox
    MessageBox.Show(GetCommandLineX86(Proc))
Next

'
'

As always let me know if you find this code useful or have any questions.

Chris

12 responses to VB.NET Get command line for external process

  1. 

    Thanks for the writeup, Chris! I was just looking fo the same thing
    myself; seems it can’t be done without “directly” accessing process’
    address space? (C here)

    • 

      Yeah I certainly could not find any other way and unfortunately I still haven’t been able to even get this method of directly reading the process’s virtual memory working for 64 bit processes… though to be honest I haven’t spent much time on it recently.

  2. 

    In VB.net you can use WMI to do this…In this example we are searching for *.exe with -D in the arguments. I’m not sure about the permission requirements but this works for me.

    Dim QS As New ManagementObjectSearcher(“Select * from Win32_Process WHERE name like ‘%.EXE’ and CommandLine like ‘%-D%'”)
    Dim objCol As ManagementObjectCollection = QS.Get

    For Each objMgmt In objCol
    MsgBox(objMgmt(“commandline”).ToString())
    Next

    • 

      Yeah I avoid using WMI where possible (because of the performance and reliability issues I’ve seen with it) 🙂 but thanks I’m sure that code will be useful for other people

  3. 

    Hi

    Would you be able to provide this to me in VBA? This would be extremely useful for me as I am looking to determine the filenames of msaccess.exe (the db not the application) processes. I will then use this to set the priority and processor affinity.
    Many Thanks and Best Regards
    Ian

    • 

      I dont use VBA very often so I may be wrong but I dont think this would be possible in VBA. If it is I’m afraid I have no idea how you would do it. Sorry!

  4. 

    I get error:

    Return System.Text.Encoding.Unicode.GetString(CmdLineBytes).Trim

    ArgumentsNull

    Can you help me?

    • 

      Which version of Windows are you using and is it 32 bit or 64 bit? If its 64 bit, have you compiled your program to target x86, x64 or AnyCPU?

  5. 

    If i run my app in X86 its work but explain me, if i compile it in X86 i can use under 64bits?

    Best Regards And thank you for you time 🙂

    • 

      Yes if you compile to X86 your application will still run fine under a 64 bit OS. It will just be run in what is known as WOW64 mode – a 32 bit emulation mode. The majority of the software you use on a 64 bit OS is probably still 32 bit software running in WOW64 mode (you can see which processes are 32 bit processes by going to the Processes tab in Task Manager and looking for any processes that have *32 after the name).
      However, if you read the second paragraph of this blog post you will notice that this API code will not work if you try to run it against a 64 bit process when your application is running in WOW64 mode… and so far I have found no way to overcome this issue. Even the built in Process class in the .NET framework cant get around the fact that you have issues accessing properties of 64 bit processes from a 32 bit process, so I doubt I am ever going to find a solution.

  6. 

    Thank you cjwdev for your reply, i will add a some “try’s” in code for don’t return erros if anyone run this in 64bits 🙂

Leave a comment