12

The code below works for me well to get command line string of 32-bit process from a 32-bit app, 64-bit process from a 64-bit app and 32-bit process from 64-bit app. This will break if I try to use for 64-bit process from 32-bit app. The reason being the structure size difference in PROCESS_BASIC_INFORMATION and address size. So here are my questions -

1) The suggestion given in process hacker ( http://processhacker.sourceforge.net/forums/viewtopic.php?f=15&t=181 ) to use wow64 function doesn't seem to work and fail with following error -

NtWow64ReadVirtualMemory64 error: 8000000D while reading ProcessParameters address from A68291A0004028E0

Has anyone tried this and could successfully get information? I posted the same in their forum asking their opinion.

2) Is there any other approach to query peb information that can work for x86 and x64 reliably?

int get_cmdline_from_pid( DWORD dwPid, char** cmdLine )
{
    DWORD dw, read;
    HANDLE hProcess;
    NtQueryInformationProcess* pNtQip;
    PROCESS_BASIC_INFORMATION pbInfo;
    UNICODE_STRING cmdline;
    WCHAR* wcmdLine;

    *cmdLine = NULL;

    hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwPid );
    if( !hProcess )
        return FALSE;

    pNtQip = (NtQueryInformationProcess*) GetProcAddress(GetModuleHandle("ntdll.dll"), 
                                                    "NtQueryInformationProcess");
    if(!pNtQip)
        return FALSE;

    pNtQip(hProcess, PROCESSBASICINFOMATION, &pbInfo, sizeof(pbInfo), NULL);

    #ifdef _WIN64
        ReadProcessMemory(hProcess, pbInfo.PebBaseAddress + 0x20, &dw, sizeof(dw), 
                         &read); 
    #else
        ReadProcessMemory(hProcess, pbInfo.PebBaseAddress + 0x10, &dw, sizeof(dw), 
                          &read); 
    #endif

    #ifdef _WIN64
        ReadProcessMemory(hProcess, (PCHAR)dw+112, &cmdline, sizeof(cmdline), &read);
    #else
        ReadProcessMemory(hProcess, (PCHAR)dw+64, &cmdline, sizeof(cmdline), &read);
    #endif

     wcmdLine = (WCHAR *)malloc(sizeof(char)*(cmdline.Length + 2));
     if( !wcmdLine )
         return FALSE;

     ReadProcessMemory(hProcess, (PVOID)cmdline.Buffer, wcmdLine, 
                  cmdline.Length+2, &read);

     *cmdLine = mmwin32_util_widetoansi(wcmdLine);
     free(wcmdLine);

     CloseHandle(hProcess);

     return TRUE;
}
Kartlee
  • 1,129
  • 2
  • 19
  • 33

2 Answers2

20

A bit late answer maybe, but here is a code that does it. It supports 32 or 64 bit process, and 32 bit process on WOW64 (meaning you can compile for Win32 and X64). It uses undocumented functions, so use at your own risk :-)

GetCmdLine.cpp:

#include "stdafx.h"
#include "GetCmdLine.h"

int _tmain(int argc, _TCHAR* argv[])
{
    if (argc < 2)
    {
        printf("Format is GetCmdLine <process id>\n");
        return 0;
    }

    // get process identifier
    DWORD dwId = _wtoi(argv[1]);

    // open the process
    HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwId);
    DWORD err = 0;
    if (hProcess == NULL)
    {
        printf("OpenProcess %u failed\n", dwId);
        err = GetLastError();
        return -1;
    }

    // determine if 64 or 32-bit processor
    SYSTEM_INFO si;
    GetNativeSystemInfo(&si);

    // determine if this process is running on WOW64
    BOOL wow;
    IsWow64Process(GetCurrentProcess(), &wow);

    // use WinDbg "dt ntdll!_PEB" command and search for ProcessParameters offset to find the truth out
    DWORD ProcessParametersOffset = si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64 ? 0x20 : 0x10;
    DWORD CommandLineOffset = si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64 ? 0x70 : 0x40;

    // read basic info to get ProcessParameters address, we only need the beginning of PEB
    DWORD pebSize = ProcessParametersOffset + 8;
    PBYTE peb = (PBYTE)malloc(pebSize);
    ZeroMemory(peb, pebSize);

    // read basic info to get CommandLine address, we only need the beginning of ProcessParameters
    DWORD ppSize = CommandLineOffset + 16;
    PBYTE pp = (PBYTE)malloc(ppSize);
    ZeroMemory(pp, ppSize);

    PWSTR cmdLine;

    if (wow)
    {
        // we're running as a 32-bit process in a 64-bit OS
        PROCESS_BASIC_INFORMATION_WOW64 pbi;
        ZeroMemory(&pbi, sizeof(pbi));

        // get process information from 64-bit world
        _NtQueryInformationProcess query = (_NtQueryInformationProcess)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtWow64QueryInformationProcess64");
        err = query(hProcess, 0, &pbi, sizeof(pbi), NULL);
        if (err != 0)
        {
            printf("NtWow64QueryInformationProcess64 failed\n");
            CloseHandle(hProcess);
            return -1;
        }

        // read PEB from 64-bit address space
        _NtWow64ReadVirtualMemory64 read = (_NtWow64ReadVirtualMemory64)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtWow64ReadVirtualMemory64");
        err = read(hProcess, pbi.PebBaseAddress, peb, pebSize, NULL);
        if (err != 0)
        {
            printf("NtWow64ReadVirtualMemory64 PEB failed\n");
            CloseHandle(hProcess);
            return -1;
        }

        // read ProcessParameters from 64-bit address space
        // PBYTE* parameters = (PBYTE*)*(LPVOID*)(peb + ProcessParametersOffset); // address in remote process address space
        PVOID64 parameters = (PVOID64) * ((PVOID64*)(peb + ProcessParametersOffset)); // corrected 64-bit address, see comments
        err = read(hProcess, parameters, pp, ppSize, NULL);
        if (err != 0)
        {
            printf("NtWow64ReadVirtualMemory64 Parameters failed\n");
            CloseHandle(hProcess);
            return -1;
        }

        // read CommandLine
        UNICODE_STRING_WOW64* pCommandLine = (UNICODE_STRING_WOW64*)(pp + CommandLineOffset);
        cmdLine = (PWSTR)malloc(pCommandLine->MaximumLength);
        err = read(hProcess, pCommandLine->Buffer, cmdLine, pCommandLine->MaximumLength, NULL);
        if (err != 0)
        {
            printf("NtWow64ReadVirtualMemory64 Parameters failed\n");
            CloseHandle(hProcess);
            return -1;
        }
    }
    else
    {
        // we're running as a 32-bit process in a 32-bit OS, or as a 64-bit process in a 64-bit OS
        PROCESS_BASIC_INFORMATION pbi;
        ZeroMemory(&pbi, sizeof(pbi));

        // get process information
        _NtQueryInformationProcess query = (_NtQueryInformationProcess)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");
        err = query(hProcess, 0, &pbi, sizeof(pbi), NULL);
        if (err != 0)
        {
            printf("NtQueryInformationProcess failed\n");
            CloseHandle(hProcess);
            return -1;
        }

        // read PEB
        if (!ReadProcessMemory(hProcess, pbi.PebBaseAddress, peb, pebSize, NULL))
        {
            printf("ReadProcessMemory PEB failed\n");
            CloseHandle(hProcess);
            return -1;
        }

        // read ProcessParameters
        PBYTE* parameters = (PBYTE*)*(LPVOID*)(peb + ProcessParametersOffset); // address in remote process adress space
        if (!ReadProcessMemory(hProcess, parameters, pp, ppSize, NULL))
        {
            printf("ReadProcessMemory Parameters failed\n");
            CloseHandle(hProcess);
            return -1;
        }

        // read CommandLine
        UNICODE_STRING* pCommandLine = (UNICODE_STRING*)(pp + CommandLineOffset);
        cmdLine = (PWSTR)malloc(pCommandLine->MaximumLength);
        if (!ReadProcessMemory(hProcess, pCommandLine->Buffer, cmdLine, pCommandLine->MaximumLength, NULL))
        {
            printf("ReadProcessMemory Parameters failed\n");
            CloseHandle(hProcess);
            return -1;
        }
    }
    printf("%S\n", cmdLine);
    return 0;
}

GetCmdLine.h:

#pragma once
#include "stdafx.h"

// NtQueryInformationProcess for pure 32 and 64-bit processes
typedef NTSTATUS (NTAPI *_NtQueryInformationProcess)(
    IN HANDLE ProcessHandle,
    ULONG ProcessInformationClass,
    OUT PVOID ProcessInformation,
    IN ULONG ProcessInformationLength,
    OUT PULONG ReturnLength OPTIONAL
    );

typedef NTSTATUS (NTAPI *_NtReadVirtualMemory)(
    IN HANDLE ProcessHandle,
    IN PVOID BaseAddress,
    OUT PVOID Buffer,
    IN SIZE_T Size,
    OUT PSIZE_T NumberOfBytesRead);

// NtQueryInformationProcess for 32-bit process on WOW64
typedef NTSTATUS (NTAPI *_NtWow64ReadVirtualMemory64)(
    IN HANDLE ProcessHandle,
    IN PVOID64 BaseAddress,
    OUT PVOID Buffer,
    IN ULONG64 Size,
    OUT PULONG64 NumberOfBytesRead);

// PROCESS_BASIC_INFORMATION for pure 32 and 64-bit processes
typedef struct _PROCESS_BASIC_INFORMATION {
    PVOID Reserved1;
    PVOID PebBaseAddress;
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;
    PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;

// PROCESS_BASIC_INFORMATION for 32-bit process on WOW64
// The definition is quite funky, as we just lazily doubled sizes to match offsets...
typedef struct _PROCESS_BASIC_INFORMATION_WOW64 {
    PVOID Reserved1[2];
    PVOID64 PebBaseAddress;
    PVOID Reserved2[4];
    ULONG_PTR UniqueProcessId[2];
    PVOID Reserved3[2];
} PROCESS_BASIC_INFORMATION_WOW64;

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

typedef struct _UNICODE_STRING_WOW64 {
  USHORT Length;
  USHORT MaximumLength;
  PVOID64 Buffer;
} UNICODE_STRING_WOW64;
Eli Burke
  • 2,729
  • 27
  • 25
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • I guess the downvote my answer just got was because somebody thinks my answer is wrong. But I can't see how that is so. Just how would you fit a 64 bit address into a 32 bit variable? Your code may work if the yop 32 its of the pointer happen to be zero. – David Heffernan Apr 20 '13 at 08:17
  • I don't know about your downvote, I don't even have an upvote on mine, but where do you see a 64-bit address stored in a 32-bit variable in the code? – Simon Mourier Apr 20 '13 at 13:29
  • You've got one now. OK, so `_NtWow64ReadVirtualMemory64` uses `PVOID64` to side step that issue. I cannot find any documentation for that. – David Heffernan Apr 20 '13 at 13:35
  • Thanks for the upvote :-) Like I said, this is all undocumented indeed. – Simon Mourier Apr 20 '13 at 13:38
  • Would you agree with my answer's analysis then? If you are sticking to documented functions that is. – David Heffernan Apr 20 '13 at 13:40
  • Oh yes sure, WMI is the only official way to go. – Simon Mourier Apr 20 '13 at 13:46
  • Thanks for that. The downvote made me wonder whether I was talking rubbish or not. I don't like to have errors in my answers. – David Heffernan Apr 20 '13 at 13:48
  • Didn't downvote but was just wondering, shouldn't 64 on wow64 be running the some stuct as 32 bit on wow64 (e.g. double size to match offset. – weeyoung Apr 29 '13 at 07:51
  • @weeyoung - sure. read the code and comments. It has PVOID64 for what matters and hacked doubled structs for matching offsets. – Simon Mourier Apr 29 '13 at 08:28
  • What i mean is that your code seems to be using the same function and struct for 32 bit and 64 bit, while a 32 on a 64 is using some other function and struct. Anyway, your code do work. though im using window's MEMORY_BASIC_INFORMATION64. Thanks. ;) – weeyoung May 02 '13 at 07:21
  • @weeyoung - Yes, that's correct, because IntPtr varies automatically in size (4 or 8) when compiled for 32-bit vs 64-bit. – Simon Mourier May 02 '13 at 07:49
  • 3
    There is one more bug. When process is 32-bit and you read PEB address from 64-bit process you store it in PVOID64 - maybe you expect there will be 64 bit stored in this variable, but that's not true. MSDN says this pointer will be truncated, and then you use this truncated pointer as address in 64-bit process adress space. Interesting thing is truncated pointer is SOMETIMES valid (if high order bytes of original pointer were 0) and SOMETIMES not. For example you code fails always on Win8 while getting command line for IE 11 x64. Solution is easy - use ULONG64 instead PVOID64 in all places. – Ezh Jul 25 '14 at 17:00
  • @ezh - That's possible. I'll check that when I have some time. Note that would be "one bug", not "one more bug"... – Simon Mourier Jul 25 '14 at 21:17
  • Is it intentional that you do `ZeroMemory(peb, pebSize);` twice? Maybe you wanted `ZeroMemory(pp, ppSize);`second time? – sasha.sochka Nov 01 '14 at 21:38
  • Does this cover the case of a 32-bit process getting the command line of another 32 bit process, both running under WoW64? I don't see you detect the bitness of the target process, just your own. – GetFree Apr 19 '17 at 15:21
  • 2
    Thanks to @Ezh for pointing out the truncated pointer issue. Based on this I just changed the line: PBYTE* parameters = (PBYTE*)*(LPVOID*)(peb + ProcessParametersOffset); TO THIS LINE: PVOID64 parameters = (PVOID64) * ((PVOID64*)(peb + ProcessParametersOffset)); This change is only in the wow block. This fixes the issue. – Gautam Jain Oct 16 '18 at 09:58
  • @SimonMourier: Sorry, I was writing something but I misread your code: so basically `NtWow64QueryInformationProcess64` is able to get the Peb address of a 64 bit process even from a 32 bit process, and then you read 64 bit memory from a 32 bit process with `NtWow64ReadVirtualMemory64`? – ceztko Aug 19 '20 at 16:37
  • 1
    @ceztko - yes, the code supposedly works in every 32/64 process/OS combination... well... except 64bit process on 32bit OS :-) – Simon Mourier Aug 19 '20 at 16:56
  • If you can't make the code above work under WOW64, check the size of PVOID64 in your build environment. For example, MinGW typedefs it to a 32-bit pointer and the posted code won't work. PVOID64 should work fine on Windows, but I had to change back to ULONG64 as suggested by @Ezh – Eli Burke Jul 22 '21 at 21:42
2

Your 32 bit pointers are not wide enough to store addresses in the 64 bit address space of the target process and will be truncated. Thus, what you are attempting is impossible. This is one of the situations where Raymond Chen would advise you to stop using the emulator.

Having invoked Raymond Chen's name, I did a quick search to see if he had any useful nuggets. That search turned up this article: Why is there no supported way to get the command line of another process?. The useful nugget is the observation that Win32_Process.CommandLine gives you what you need (somehow). So, my advice is to give WMI a go.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Thanks for your reply. I already mentioned it in my description. I am now why wow functions doesn't work as suggested in process hacker. If there is any other way to tackle this issue, please add your comment. – Kartlee Sep 16 '11 at 15:42
  • 2
    Every answer referring to Raymond Chen should be upvoted. What he wrote about getting the command line was also the first thing that came to my mind when I read the question title :-) – Joey Sep 16 '11 at 16:31
  • @Joey I even put his name in twice to make sure any willing Raymond auto-upvoters didn't miss it if they were scanning!! ;-) – David Heffernan Sep 16 '11 at 16:32