Introduction
This article will show a method to read the following items of a process, primarily using the NtQueryInformationProcess()
and ReadProcessMemory()
functions:
- Process ID
- Parent ID
- Affinity Mask
- Exit Code Status
- Command Line of Process
- Path of Process Image File
- Terminal Services Session ID
- Flag, if process is currently under debugging
- Address of Process Environment Block (PEB)
This information is returned in a variable declared as a structure, smPROCESSINFO
. This structure is defined in NtProcessInfo.h:
typedef struct _smPROCESSINFO
{
DWORD dwPID;
DWORD dwParentPID;
DWORD dwSessionID;
DWORD dwPEBBaseAddress;
DWORD dwAffinityMask;
LONG dwBasePriority;
LONG dwExitStatus;
BYTE cBeingDebugged;
TCHAR szImgPath[MAX_UNICODE_PATH];
TCHAR szCmdLine[MAX_UNICODE_PATH];
} smPROCESSINFO;
Although there are Windows APIs to retrieve most of the values above, this article will show how to obtain those values while getting those not available through Windows APIs. Note: this method uses structures and functions in the core NTDLL.DLL, which could change in future versions. Microsoft recommends using Windows APIs to "safely" obtain information from the system.
The core functions to retrieve the above information are provided in NtProcessInfo.h and NtProcessInfo.cpp. Just include these into your project h/cpp files, reference the functions, and compile. If you include these files in your project/solution list, don't forget to exclude them from the build. The main function, sm_GetNtProcessInfo()
, requires a process ID and a variable declared as smPROCESSINFO
. I recommend calling the functions in the order below (step 5 and 6 could be swapped):
sm_EnableTokenPrivilege
or your custom token privilege function to enable SE_DEBUG_NAME
.sm_LoadNTDLLFunctions
. Keep the HMODULE
variable returned to free the library later.- Get the process ID. Either specify one manually, or use
EnumProcesses
, GetCurrentProcessId
, CreateToolhelp32Snapshot
, etc. sm_GetNtProcessInfo
with process ID and the smPROCESSINFO
variable.- Output the contents of your
smPROCESSINFO
variable/array to your desired medium. sm_FreeNTDLLFunctions
with the HMODULE
variable returned from sm_LoadNTDLLFunctions
.
The demo application with this article is a basic Win32, with a ListView common control child window to list the process contents. The code was also used with no problem within an MFC application. This code was written in Visual Studio .NET 2003 SP1, and is intended for Win2K or later.
Enable Debug Privilege
In order for the current user to read information for most processes, we must enable the debug privilege. The user token or the group token the user belongs to must already have the debug privilege assigned. To determine which privileges a token has, use the GetTokenInformation()
function. For our function, we pass SE_DEBUG_NAME
as the parameter, and if the function successfully enables the privilege, it will return TRUE
.
BOOL sm_EnableTokenPrivilege(LPCTSTR pszPrivilege)
{
HANDLE hToken = 0;
TOKEN_PRIVILEGES tkp = {0};
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES |
TOKEN_QUERY, &hToken))
{
return FALSE;
}
if(LookupPrivilegeValue(NULL, pszPrivilege,
&tkp.Privileges[0].Luid))
{
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tkp, 0,
(PTOKEN_PRIVILEGES)NULL, 0);
if (GetLastError() != ERROR_SUCCESS)
return FALSE;
return TRUE;
}
return FALSE;
}
Enumerate the Process IDs
To get a list of running processes, we will use the Process Status API, EnumProcesses()
. There are several ways to get process IDs. A few are mentioned above in the introduction. With a process ID, we call the sm_GetNtProcessInfo()
function to fill our smPROCESSINFO
variable. For the sake of the demo, I limited the array to 50 processes (defined as MAX_PI
).
DWORD EnumProcesses2Array(smPROCESSINFO lpi[MAX_PI])
{
DWORD dwPIDs[MAX_PI] = {0};
DWORD dwArraySize = MAX_PI * sizeof(DWORD);
DWORD dwSizeNeeded = 0;
DWORD dwPIDCount = 0;
if(!sm_EnableTokenPrivilege(SE_DEBUG_NAME))
return 0;
if(EnumProcesses((DWORD*)&dwPIDs, dwArraySize, &dwSizeNeeded))
{
HMODULE hNtDll = sm_LoadNTDLLFunctions();
if(hNtDll)
{
dwPIDCount = dwSizeNeeded / sizeof(DWORD);
for(DWORD p = 0; p < MAX_PI && p < dwPIDCount; p++)
{
if(sm_GetNtProcessInfo(dwPIDs[p], &lpi[p]))
{
}
}
sm_FreeNTDLLFunctions(hNtDll);
}
}
return (DWORD)(dwPIDCount > MAX_PI) ? MAX_PI : dwPIDCount;
}
Access NTDLL Functions
The NtQueryInformationProcess()
function does not have an import library, so we must use run-time dynamic linking to access this function in ntdll.dll. Define the function in the header, then obtain the entry point address with GetProcAddress()
.
typedef NTSTATUS (NTAPI *pfnNtQueryInformationProcess)(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
pfnNtQueryInformationProcess gNtQueryInformationProcess;
HMODULE sm_LoadNTDLLFunctions()
{
HMODULE hNtDll = LoadLibrary(_T("ntdll.dll"));
if(hNtDll == NULL) return NULL;
gNtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(hNtDll,
"NtQueryInformationProcess");
if(gNtQueryInformationProcess == NULL) {
FreeLibrary(hNtDll);
return NULL;
}
return hNtDll;
}
Get Process Basic Information
We open the process with the PROCESS_QUERY_INFORMATION
access right to get basic information, and since we will use the ReadProcessMemory()
function to read the PEB, the process must also be opened with the PROCESS_VM_READ
access right.
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
PROCESS_VM_READ, FALSE, dwPID);
if(hProcess == INVALID_HANDLE_VALUE)
{
return FALSE;
}
Now, we allocate memory for our PROCESS_BASIC_INFORMATION
structure variable.
hHeap = GetProcessHeap();
dwSize = sizeof(smPROCESS_BASIC_INFORMATION);
pbi = (smPPROCESS_BASIC_INFORMATION)HeapAlloc(hHeap,
HEAP_ZERO_MEMORY,
dwSize);
if(!pbi) {
CloseHandle(hProcess);
return FALSE;
}
This structure is defined in both winternl.h and ntddk.h. The definition below comes from the Win2003 DDK ntddk.h, since both my winternl.h in Visual Studio 2003 and the one at Microsoft MSDN doesn't contain as much detail. I also found that winternl.h and ntddk.h conflict each other during compilation, so I decided to copy the newest definition with a new name (added sm as prefix) in my header file NtProcessInfo.h (included in the downloads).
typedef struct _smPROCESS_BASIC_INFORMATION {
LONG ExitStatus;
smPPEB PebBaseAddress;
ULONG_PTR AffinityMask;
LONG BasePriority;
ULONG_PTR UniqueProcessId;
ULONG_PTR InheritedFromUniqueProcessId;
} smPROCESS_BASIC_INFORMATION, *smPPROCESS_BASIC_INFORMATION;
Then, we get the basic information for a process via the NtQueryInformationProcess()
function.
NTSTATUS dwStatus = gNtQueryInformationProcess(hProcess,
ProcessBasicInformation,
pbi,
dwSize,
&dwSizeNeeded);
if(dwStatus >= 0)
{
spi.dwPID = (DWORD)pbi->UniqueProcessId;
spi.dwParentPID = (DWORD)pbi->InheritedFromUniqueProcessId;
spi.dwBasePriority = (LONG)pbi->BasePriority;
spi.dwExitStatus = (NTSTATUS)pbi->ExitStatus;
spi.dwPEBBaseAddress = (DWORD)pbi->PebBaseAddress;
spi.dwAffinityMask = (DWORD)pbi->AffinityMask;
Reading the PEB
From the basic information, we already get the base address, if any, of the PEB in the PebBaseAddress
pointer variable. If the address is not equal to zero, we just pass this address to the ReadProcessMemory()
function. If successful, it should return the process information in our PEB structure variable, which also contains the BeingDebugged
and SessionId
variables.
if(pbi->PebBaseAddress)
{
if(ReadProcessMemory(hProcess, pbi->PebBaseAddress, &peb, sizeof(peb), &dwBytesRead))
{
spi.dwSessionID = (DWORD)peb.SessionId;
spi.cBeingDebugged = (BYTE)peb.BeingDebugged;
The PEB
structure is defined as:
typedef struct _smPEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
smPPEB_LDR_DATA Ldr;
smPRTL_USER_PROCESS_PARAMETERS ProcessParameters;
BYTE Reserved4[104];
PVOID Reserved5[52];
smPPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved6[128];
PVOID Reserved7[1];
ULONG SessionId;
} smPEB, *smPPEB;
From this point, we also have the memory addresses for Ldr
, which we are not using in this article. Basically, the PEB_LDR_DATA
structure contains a doubly-linked list of the loaded modules in the process. We also have the memory address for ProcessParameters
, which will give us the CommandLine
...
if(peb_upp.CommandLine.Length > 0) {
pwszBuffer = (WCHAR *)HeapAlloc(hHeap,
HEAP_ZERO_MEMORY,
peb_upp.CommandLine.Length);
if(pwszBuffer)
{
if(ReadProcessMemory(hProcess,
peb_upp.CommandLine.Buffer,
pwszBuffer,
peb_upp.CommandLine.Length,
&dwBytesRead))
{
if(peb_upp.CommandLine.Length >= sizeof(spi.szCmdLine))
dwBufferSize = sizeof(spi.szCmdLine) - sizeof(TCHAR);
else
dwBufferSize = peb_upp.CommandLine.Length;
#if defined(UNICODE) || (_UNICODE)
StringCbCopyN(spi.szCmdLine, sizeof(spi.szCmdLine),
pwszBuffer, dwBufferSize);
#else
WideCharToMultiByte(CP_ACP, 0, pwszBuffer,
(int)(dwBufferSize / sizeof(WCHAR)),
spi.szCmdLine, sizeof(spi.szCmdLine),
NULL, NULL);
#endif
}
if(!HeapFree(hHeap, 0, pwszBuffer)) {
bReturnStatus = FALSE;
goto gnpiFreeMemFailed;
}
}
}
...and the ImagePathName
variables as UNICODE_STRING
structures. Unicode paths could be as long as 32K characters, and are usually prefixed with '\\?\' or '\??\'. Since native NT APIs operate in Unicode, we have to convert the buffers to ANSI when the calling application is not compiled for Unicode and is using TCHAR
instead of WCHAR
.
if(peb_upp.ImagePathName.Length > 0) {
pwszBuffer = (WCHAR *)HeapAlloc(hHeap,
HEAP_ZERO_MEMORY,
peb_upp.ImagePathName.Length);
if(pwszBuffer)
{
if(ReadProcessMemory(hProcess,
peb_upp.ImagePathName.Buffer,
pwszBuffer,
peb_upp.ImagePathName.Length,
&dwBytesRead))
{
if(peb_upp.ImagePathName.Length >= sizeof(spi.szImgPath))
dwBufferSize = sizeof(spi.szImgPath) - sizeof(TCHAR);
else
dwBufferSize = peb_upp.ImagePathName.Length;
#if defined(UNICODE) || (_UNICODE)
StringCbCopyN(spi.szImgPath, sizeof(spi.szImgPath),
pwszBuffer, dwBufferSize);
#else
WideCharToMultiByte(CP_ACP, 0, pwszBuffer,
(int)(dwBufferSize / sizeof(WCHAR)),
spi.szImgPath, sizeof(spi.szImgPath),
NULL, NULL);
#endif
}
if(!HeapFree(hHeap, 0, pwszBuffer)) {
bReturnStatus = FALSE;
goto gnpiFreeMemFailed;
}
}
}
For the system process (PID = 4 on XP and later, 8 on Win2K, and 2 on NT 4), we manually specify the path for the process since we know it is %SystemRoot%\System32\ntoskrnl.exe, but is not returned by the API. Ntoskrnl.exe could also be ntkrnlmp.exe if Symmetric Multi-Processing (SMP) is present, or ntkrnlpa.exe if Physical Address Extension (PAE) is present. In any case, the actual filename will be ntoskrnl.exe, but the OriginalFilename
field in the file version block will contain the real name. We use the ExpandEnvironmentStrings()
API to replace the %SystemRoot% system environment variable with the actual root path of Windows.
if(spi.dwPID == 4)
{
ExpandEnvironmentStrings(_T("%SystemRoot%\\System32\\ntoskrnl.exe"),
spi.szImgPath, sizeof(spi.szImgPath));
}
As mentioned above with the PROCESS_BASIC_INFORMATION
structure, the RTL_USER_PROCESS_PARAMETERS
structure is defined in winternl.h and ntddk.h.
typedef struct _smRTL_USER_PROCESS_PARAMETERS {
BYTE Reserved1[16];
PVOID Reserved2[10];
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
} smRTL_USER_PROCESS_PARAMETERS, *smPRTL_USER_PROCESS_PARAMETERS;
Cleanup
Here, we free the NTDLL.DLL we loaded earlier. That's it!
void sm_FreeNTDLLFunctions(HMODULE hNtDll)
{
if(hNtDll)
FreeLibrary(hNtDll);
gNtQueryInformationProcess = NULL;
}
Points of Interest
I wrote this article to share a method I learned while trying to get process information for a basic process explorer without using the simple Tool Help functions. Supposedly, the Tool Help functions started in Win9x, and was reluctantly incorporated into the NT based operating systems starting with XP. I wanted to dig in a little deeper and try using an NT Native API for my own experiences.
Some of the "safe" Windows API functions to obtain another process' information are:
ProcessIdToSessionId
CheckRemoteDebuggerPresent
GetExitCodeProcess
GetProcessAffinityMask
GetProcessImageFileName
CreateRemoteThread
- Other Process and Thread Functions
Note: I did receive the following message during debug in the demo application, but appears to occur prior to WinMain, and appears to be handled since a second-chance exception is not thrown:
"First-chance exception at 0x7c918fea in GetNtProcessInfo.exe:
0xC0000005: Access violation writing location 0x00000010."
I did not get this exception with the real world implementation of NtProcessInfo.h and NtProcessInfo.cpp.
History