HOWTO: Get the command line of a process

How would you get the command line of a process? Some people have suggested that you use remote thread injection, call GetCommandLine(), then IPC the result back. This might work most of the time on Windows XP, but on Windows Vista it doesn’t work on system and service processes. This is because CreateRemoteThread only works on processes in the same session ID as the caller – in Windows Vista, services and other system processes run in session 0 while user programs run in higher sessions. The best and safest way is to read a structure present in every Windows process.

The Process Environment Block (PEB) is usually stored in the high regions of process memory, above 0x7ff00000. These regions also contain Thread Environment Blocks (TEBs). The PEB address is different for almost every process, so you can’t simply use a hardcoded constant. There’s only one way (in user mode) to get the PEB address: NtQueryInformationProcess. Its (simplified) function definition is:

NtQueryInformationProcess(
    IN HANDLE ProcessHandle,
    IN PROCESS_INFORMATION_CLASS ProcessInformationClass,
    OUT PVOID ProcessInformation,
    IN ULONG ProcessInformationLength,
    OUT PULONG ReturnLength
    );

The ProcessInformationClass we want to use is the first one, ProcessBasicInformation (with a value of 0). The structure for this is named PROCESS_BASIC_INFORMATION:

typedef struct _PROCESS_BASIC_INFORMATION
{
    NTSTATUS ExitStatus;
    PVOID PebBaseAddress; /* contains the PEB address! */
    ULONG_PTR AffinityMask;
    DWORD BasePriority;
    HANDLE UniqueProcessId;
    HANDLE InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;

The problem with calling NtQueryInformationProcess is that you’ll have to find the address of it yourself. Here’s some code that finds the PEB address of any process:

typedef NTSTATUS (NTAPI *_NtQueryInformationProcess)(
    HANDLE ProcessHandle,
    DWORD ProcessInformationClass, /* can't be bothered defining the whole enum */
    PVOID ProcessInformation,
    DWORD ProcessInformationLength,
    PDWORD ReturnLength
    );

typedef struct _PROCESS_BASIC_INFORMATION
{
    ...
} PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;

PVOID GetPebAddress(int pid)
{
    _NtQueryInformationProcess NtQueryInformationProcess = (_NtQueryInformationProcess)
        GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");
    PROCESS_BASIC_INFORMATION pbi;
    HANDLE processHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);

    NtQueryInformationProcess(processHandle, 0, &pbi, sizeof(pbi), NULL);
    CloseHandle(processHandle);

    return pbi.PebBaseAddress;
}

Once you get the address of the PEB, you’ll have to read its contents. This can easily be done using ReadProcessMemory. Inside the PEB, there’s a pointer to a second structure, RTL_USER_PROCESS_PARAMETERS. Here’s some stuff from the the PEB struct definition:

typedef struct _PEB
{
    /* +0x0 */ BOOLEAN InheritedAddressSpace; /* BOOLEANs are one byte each */
    /* +0x1 */ BOOLEAN ReadImageFileExecOptions;
    /* +0x2 */ BOOLEAN BeingDebugged;
    /* +0x3 */ BOOLEAN Spare;
    /* +0x4 */ HANDLE Mutant;
    /* +0x8 */ PVOID ImageBaseAddress;
    /* +0xc */ PPEB_LDR_DATA LoaderData;
    /* +0x10 */ PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
    ...

Those comments on the left hand side are offsets from the beginning of the PEB; if we want to get the address of ProcessParameters, we simply read 4 bytes from PEB address + 0x10. For example:

PVOID pebAddress = ...; /* get the PEB address */
PVOID rtlUserProcParamsAddress;

ReadProcessMemory(processHandle, /* open the process first... */
    (PCHAR)pebAddress + 0x10,
    &rtlUserProcParamsAddress, /* we'll just read directly into our variable */
    sizeof(PVOID),
    NULL
    );

So, now we have the address of ProcessParameters. Let’s look inside it:

typedef struct _RTL_USER_PROCESS_PARAMETERS
{
    ULONG MaximumLength;
    ULONG Length;
    ULONG Flags;
    ULONG DebugFlags;
    PVOID ConsoleHandle;
    ULONG ConsoleFlags;
    HANDLE StdInputHandle;
    HANDLE StdOutputHandle;
    HANDLE StdErrorHandle;
    /* +0x24 */ UNICODE_STRING CurrentDirectoryPath;
    HANDLE CurrentDirectoryHandle;
    /* +0x30 */ UNICODE_STRING DllPath;
    /* +0x38 */ UNICODE_STRING ImagePathName;
    /* +0x40 */ UNICODE_STRING CommandLine;
    ... /* more stuff you probably won't care about */

A UNICODE_STRING is simply a counted Unicode string:

typedef struct _UNICODE_STRING
{
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

It’s pretty obvious what you have to do from here on. You have to read the desired UNICODE_STRING structure and then read the contents of Buffer (Length is in bytes, not characters). (Now that you’ve seen the definition of RTL_USER_PROCESS_PARAMETERS, you’ll probably want other strings as well!) A complete sample program is below. Note that the code does not work on x64 due to the hard-coded offsets; you may want to include the structure definitions for the PEB and process parameters and use FIELD_OFFSET to get the correct offsets.

#include <windows.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI *_NtQueryInformationProcess)(
    HANDLE ProcessHandle,
    DWORD ProcessInformationClass,
    PVOID ProcessInformation,
    DWORD ProcessInformationLength,
    PDWORD ReturnLength
    );

typedef struct _UNICODE_STRING
{
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

typedef struct _PROCESS_BASIC_INFORMATION
{
    LONG ExitStatus;
    PVOID PebBaseAddress;
    ULONG_PTR AffinityMask;
    LONG BasePriority;
    ULONG_PTR UniqueProcessId;
    ULONG_PTR ParentProcessId;
} PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;

PVOID GetPebAddress(HANDLE ProcessHandle)
{
    _NtQueryInformationProcess NtQueryInformationProcess =
        (_NtQueryInformationProcess)GetProcAddress(
        GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");
    PROCESS_BASIC_INFORMATION pbi;

    NtQueryInformationProcess(ProcessHandle, 0, &pbi, sizeof(pbi), NULL);

    return pbi.PebBaseAddress;
}

int wmain(int argc, WCHAR *argv[])
{
    int pid;
    HANDLE processHandle;
    PVOID pebAddress;
    PVOID rtlUserProcParamsAddress;
    UNICODE_STRING commandLine;
    WCHAR *commandLineContents;

    if (argc < 2)
    {
        printf("Usage: getprocesscommandline [pid]\n");
        return 1;
    }

    pid = _wtoi(argv[1]);

    if ((processHandle = OpenProcess(
        PROCESS_QUERY_INFORMATION | /* required for NtQueryInformationProcess */
        PROCESS_VM_READ, /* required for ReadProcessMemory */
        FALSE, pid)) == 0)
    {
        printf("Could not open process!\n");
        return GetLastError();
    }

    pebAddress = GetPebAddress(processHandle);

    /* get the address of ProcessParameters */
    if (!ReadProcessMemory(processHandle, (PCHAR)pebAddress + 0x10,
        &rtlUserProcParamsAddress, sizeof(PVOID), NULL))
    {
        printf("Could not read the address of ProcessParameters!\n");
        return GetLastError();
    }

    /* read the CommandLine UNICODE_STRING structure */
    if (!ReadProcessMemory(processHandle, (PCHAR)rtlUserProcParamsAddress + 0x40,
        &commandLine, sizeof(commandLine), NULL))
    {
        printf("Could not read CommandLine!\n");
        return GetLastError();
    }

    /* allocate memory to hold the command line */
    commandLineContents = (WCHAR *)malloc(commandLine.Length);

    /* read the command line */
    if (!ReadProcessMemory(processHandle, commandLine.Buffer,
        commandLineContents, commandLine.Length, NULL))
    {
        printf("Could not read the command line string!\n");
        return GetLastError();
    }

    /* print it */
    /* the length specifier is in characters, but commandLine.Length is in bytes */
    /* a WCHAR is 2 bytes */
    printf("%.*S\n", commandLine.Length / 2, commandLineContents);
    CloseHandle(processHandle);
    free(commandLineContents);

    return 0;
}

23 Comments

  1. Hi!

    This code is great, but how can I get command line in ASCII (this code accesses to command line in UNICODE that it’s gotten by GetCommandLineW) that it’s stored in a different address and it’s gotten by GetCommandLineA?

  2. This code gets the command line directly from the PEB, and this is not how GetCommandLineW and GetCommandLineA work. If you disassemble them you will see that they both return a value stored at a specific address (on my machine GetCommandLineA returns an address which is stored at VA 0xcd594). This address is set by BaseDllInitialize (in kernel32.dll). BaseDllInitialize simply gets the command line from the PEB, copies it to a location that GetCommandLineW uses, and converts it to ANSI using RtlUnicodeStringToAnsiString and copies it so GetCommandLineA can use it.

    A possible solution would be to read 4 bytes from GetCommandLineA+0x1 which would give you the address that GetCommandLineA uses. You can then ReadProcessMemory using this address to get the command line address.

  3. Only one more thing, if you mofify UNICODE_STRING gotten with your method (for example: wcscpy (commandLine.Buffer, L”hello”)) and, after that, you call GetCommandLineW you got “hello” instead the original command line, that it’s not the case of GetCommandLineA, because it’s stored in a different address like you said.

    Thanks!

  4. gr8 work!
    But I was looking at PEB in MSDN(2005) and I saw this

    typedef struct _PEB {
    BYTE Reserved1[2];
    BYTE BeingDebugged;
    BYTE Reserved2[229];
    PVOID Reserved3[59];
    ULONG SessionId;
    } PEB, *PPEB;

    so I was wondering how you were able to get that detailed PEB struct!

    1. MS doesn’t like to give details on undocumented Windows functions and structures (replacing many fields with “reserved”). Luckily we have many resources available such as NTinternals (where I got the PEB definition from). I also have a copy of some internal Windows header files… Just keep looking and you’ll find a lot of info on Windows internals.

  5. hey wj32!

    I know you posted this nearly 1 year ago, but, I was wondering if you could help me with a problem.

    Your code above works, flawlessly. However, I’m interested in doing something else. I’m attempting to patch a PEB field.

    GetModuleFileName (to my knowledge) retrieves the path from:

    /* +0x38 */ UNICODE_STRING ImagePathName;

    Now, I’m attempting to alter the ImagePathName. I can read it without any issues, however, writing to it seems to be failing (using WriteProcessMemory, naturally).

    WriteProcessMemory succeeds (no errors), but, yet GetModuleFileName is still retrieving the unmodified path.

    Any ideas?

  6. We’re using this useful recipe in http://code.google.com/p/psutil/

    The code doesn’t work for processes not properly initilized yet (we’re keeping track of this issue here: http://code.google.com/p/psutil/issues/detail?id=56) in which case ReadProcessMemory() returns ERROR_PARTIAL_COPY.

    We solved this problem on Windows XP 32 bit by trapping the first two ReadProcessMemory() calls in a while loop which gets repeated in case of ERROR_PARTIAL_COPY until the cmdline is properly retrieved, so you might want to modify your code.

    The same solution didn’t work on Windows 7 64 bit which keeps returning ERROR_PARTIAL_COPY.

    1. I found that the command line becomes available after the process loads kernel32.dll. This can be detected by handling LOAD_DLL_DEBUG_EVENT while debugging the process (if it’s debugged via CreateProcess).

  7. Hi. Great post. You mentioned that this code won’t work with x64 processes. It’s true. Can you write more about how get PEB from x64 processes? Maybe any code sample?

  8. Can’t get a successful compile:

    Compiler: Default compiler
    Building Makefile: “U:ToolsDev-CppProjectsTestMakefile.win”
    Executing make…
    make.exe -f “U:ToolsDev-CppProjectsTestMakefile.win” all
    gcc.exe -c main.c -o main.o -I”u:/Tools/Dev-Cpp/include”

    main.c:4: error: syntax error before ‘*’ token

    main.c:10: error: `NTSTATUS’ declared as function returning a function
    main.c: In function `GetPebAddress’:
    main.c:31: error: `_NtQueryInformationProcess’ undeclared (first use in this function)
    main.c:31: error: (Each undeclared identifier is reported only once
    main.c:31: error: for each function it appears in.)

    main.c:31: error: syntax error before “NtQueryInformationProcess”

    make.exe: *** [main.o] Error 1

    Execution terminated

  9. This is my code… MCLs is really a std:string and the conversion could ofcourse be done by your own choise.. also the HANDLE has to be valid using following:

    HANDLE Ph = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid);
    if(Ph == NULL) return GetLastError();

    I know i write ugly code 🙂

    int GetProcessCommand(HANDLE Ph, MCLs& into)
    {
    typedef NTSTATUS (NTAPI *_QueryProc)(HANDLE, DWORD, PVOID, DWORD, PDWORD);
    HMODULE Hm = GetModuleHandleA(“ntdll.dll”); // Get address of QueryProc
    auto Query = (_QueryProc)GetProcAddress(Hm, “NtQueryInformationProcess”);
    if((Hm == NULL) || (Query == NULL)) return GetLastError(); // Failed

    PROCESS_BASIC_INFORMATION pbi; _PEB peb;
    _RTL_USER_PROCESS_PARAMETERS upp;
    if(Query(Ph, 0, &pbi, sizeof(pbi), NULL)) return GetLastError();

    if(ReadProcessMemory(Ph, pbi.PebBaseAddress, &peb, sizeof(_PEB), NULL))
    { // Now we have to read the USER_PROCESS_PARAMETERS from the _PEB
    size_t sz = sizeof(_RTL_USER_PROCESS_PARAMETERS);
    if(ReadProcessMemory(Ph, peb.ProcessParameters, &upp, sz, NULL))
    { // Now we have to read the CommandLine from the ProcessParameters
    WCHAR* buf = (WCHAR*)malloc(sz = upp.CommandLine.MaximumLength);
    if(ReadProcessMemory(Ph, upp.CommandLine.Buffer, buf, sz, NULL))
    { // Now we have to convert the WCHAR* to string
    size_t max = upp.CommandLine.Length / 2; into.resize(max);
    wcstombs_s(&sz, into._Myptr(), max + 1, buf, _TRUNCATE);
    }
    free(buf);
    }
    }
    return 0;
    }

  10. hi there!
    i have 2 problems:
    1) i included but ReadProcessMemory is still unidentified.
    2) how do i run your sample program on X64?

Leave a Reply