Introduction
This article is a brief, and somewhat rehashed, introduction to the steps involved in obtaining the command-line arguments of a process other than the current process. The two primary functions involved are OpenProcess()
and ReadProcessMemory()
. Another function that is used, although not required, is NtQueryInformationProcess()
.
When I first looked at this problem, I thought that I could just run the GetCommandLine()
function in the target process using CreateRemoteThread()
. That turned out to be a bit too involved.
Getting the list of processes
There are several ways of getting the list of running processes. One is via the Process32First()
/Process32Next()
pair. The other is with EnumProcesses()
followed by GetModuleFileNameEx()
to get the path of the first module in the process which is usually the executable. For my example, I'll use the former.
HANDLE hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (INVALID_HANDLE_VALUE != hProcessSnapshot)
{
PROCESSENTRY32 ProcessEntry = {0};
ProcessEntry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hProcessSnapshot, &ProcessEntry) != FALSE)
{
do
{
} while (Process32Next(hProcessSnapshot, &ProcessEntry) != FALSE);
}
CloseHandle(hProcessSnapshot);
}
Getting the PEB's starting address
Most of the literature that I read indicated that the starting address of the PEB was always located at memory address 0x7ffdf000. I did find one reference that indicated it to be a random address for Windows XP SP2. I did a very brief experiment on such a machine and found that the address was still located at 0x7ffdf000. That said, I went ahead and accounted for both by defaulting to 0x7ffdf000 but then possibly overriding that by calling NtQueryInformationProcess()
like:
typedef LONG (WINAPI NTQIP)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);
NTQIP *lpfnNtQueryInformationProcess;
PROCESS_BASIC_INFORMATION pbi;
pbi.PebBaseAddress = (_PEB *) 0x7ffdf000;
DWORD dwSize;
HMODULE hLibrary = GetModuleHandle(_T("ntdll.dll"));
if (NULL != hLibrary)
{
lpfnNtQueryInformationProcess = (NTQIP *) GetProcAddress(hLibrary,
"NtQueryInformationProcess");
if (NULL != lpfnNtQueryInformationProcess)
(*lpfnNtQueryInformationProcess)(hProcess,
ProcessBasicInformation, &pbi, sizeof(pbi), &dwSize);
}
I found that you could also use ZwQueryInformationProcess()
as it has the same signature. With the starting address known, we can now read the PEB. One question that should have popped into your head is how we can read each process' PEB by specifying the same address. I'm going to hazard a guess and say that the magic of this lies in the depths of ReadProcessMemory()
. Given that it takes a handle to the process and the address of the PEB, it must internally map that virtual address into a physical address before doing the actual reading.
Enabling the Debug access privilege
I learned some interesting and useful information from this project regarding privileges. While a particular privilege might be added to a user or group account, that does not necessarily mean that the privilege has been enabled. On my development machine, I am a member of the Administrators group, thus I have lots and lots of privileges. One of these is the Debug privilege (i.e., SeDebugPrivilege
). Consequently I did not run into any access-related issues when trying to open a process or read a process' memory. It was not until Toby Opferman brought to my attention that the Debug privilege should be enabled.
To explore this further, I went to my other development machine and tried the sample project while logged in using the local Guest account. Sure enough, when trying to open several of the processes, I was presented with an error 5 (access denied). To remedy this, I simply added the local Group account to the Debug policy. At this point, the Debug privilege has been added to and enabled for the Guest account. I'm not sure what would disable this privilege under normal circumstances, but to account for that situation, I used the following:
HANDLE hToken;
TOKEN_PRIVILEGES tokenPriv;
LUID luidDebug;
if (OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES, &hToken) != FALSE)
{
if (LookupPrivilegeValue(_T(""), SE_DEBUG_NAME,
&luidDebug) != FALSE)
{
tokenPriv.PrivilegeCount = 1;
tokenPriv.Privileges[0].Luid = luidDebug;
tokenPriv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE,
&tokenPriv, sizeof(tokenPriv), NULL, NULL);
}
}
Navigating the PEB
This figure indicates that we must read the first 20 bytes of the PEB to get the address of the process' parameter information block. This is done with:
struct __PEB
{
DWORD dwFiller[4];
DWORD dwInfoBlockAddress;
} PEB;
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
PROCESS_VM_READ, FALSE, dwProcessID);
ReadProcessMemory(hProcess, pbi.PebBaseAddress, &PEB, sizeof(PEB), &dwSize);
Although probably insignificant, I found that the address of the parameter information block was at address 0x20000 for all but some of the "special" system processes. This figure indicates that we must read the first 72 bytes of this block to get the address of the buffer that contains the command-line arguments. This is done with:
struct __INFOBLOCK
{
DWORD dwFiller[16];
WORD wLength;
WORD wMaxLength;
DWORD dwCmdLineAddress;
} Block;
ReadProcessMemory(hProcess, (LPVOID) PEB.dwInfoBlockAddress,
&Block, sizeof(Block), &dwSize);
At this point, we know the address of the buffer containing the command-line arguments as well as the length of that buffer. Since the buffer is Unicode, we'll need to use a wide character type to hold the contents. We can allocate memory on the stack and set its size big enough (1K would probably suffice), or we can allocate memory on the heap by using the value contained in the length field preceding the buffer. I found that the difference between the two length fields was consistently 2 bytes. That would account for the '\0' character.
TCHAR *pszCmdLine = new TCHAR;
ReadProcessMemory(hProcess, (LPVOID) Block.dwCmdLineAddress,
pszCmdLine, Block.wMaxLength, &dwSize);
Guess what? We now have the process' command-line arguments tucked nicely away in the pszCmdLine
variable.
Don't forget to free up the memory when you are done with it.
Alternative approach
Almost halfway through this article, Christophe indicates that there are three ways of obtaining a process' command-line arguments. I dismissed the third option of capturing the output from Tlist because it just feels so cheesy. The second option seemed to be fairly straightforward but I am not familiar enough with all of the possible ramifications (e.g., authority) that could arise from injecting code into another's address space. Thus I opted to read through the PEB to get at the desired information.
I did find, however, that I could not access some of the processes running on my machine. Two errors that I frequently saw were ERROR_PARTIAL_COPY
and ERROR_INVALID_PARAMETER
. Getting access to and debugging a process is out of scope and not the intent of this article. I leave the task of accounting for those to the interested reader.
Enjoy!