Get Process Info with NtQueryInformationProcess

转自:https://www.codeproject.com/Articles/19685/Get-Process-Info-with-NtQueryInformationProcess (有源码参考)

Screenshot - GetNtProcessInfo.png

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):

  1. sm_EnableTokenPrivilege or your custom token privilege function to enable SE_DEBUG_NAME.
  2. sm_LoadNTDLLFunctions. Keep the HMODULE variable returned to free the library later.
  3. Get the process ID. Either specify one manually, or use EnumProcessesGetCurrentProcessIdCreateToolhelp32Snapshot, etc.
  4. sm_GetNtProcessInfo with process ID and the smPROCESSINFO variable.
  5. Output the contents of your smPROCESSINFO variable/array to your desired medium.
  6. 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}; 

    // Get a token for this process.

    if (!OpenProcessToken(GetCurrentProcess(),
                          TOKEN_ADJUST_PRIVILEGES |
                          TOKEN_QUERY, &hToken))
    {
        return FALSE;
    }

    // Get the LUID for the privilege. 

    if(LookupPrivilegeValue(NULL, pszPrivilege,
                            &tkp.Privileges[0].Luid)) 
    {
        tkp.PrivilegeCount = 1;  // one privilege to set    

        tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

        // Set the privilege for this process. 

        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 APIEnumProcesses(). 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;

    //== only to have better chance to read processes =====

    if(!sm_EnableTokenPrivilege(SE_DEBUG_NAME))
        return 0;

    // Get a list of Process IDs of current running processes

    if(EnumProcesses((DWORD*)&dwPIDs, dwArraySize, &dwSizeNeeded))
    {
        HMODULE hNtDll = sm_LoadNTDLLFunctions();

        if(hNtDll)
        {
            // Get detail info on each process

            dwPIDCount = dwSizeNeeded / sizeof(DWORD);
            for(DWORD p = 0; p < MAX_PI && p < dwPIDCount; p++)
            {
                if(sm_GetNtProcessInfo(dwPIDs[p], &lpi[p]))
                {
                      // Do something else upon success

                }
            }
            sm_FreeNTDLLFunctions(hNtDll);
        }
    }

    // Return either PID count or MAX_PI whichever is smaller

    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()
{
    // Load NTDLL Library and get entry address

    // for NtQueryInformationProcess

    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.

// Attempt to access process

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.

// Try to allocate buffer 

hHeap = GetProcessHeap();

dwSize = sizeof(smPROCESS_BASIC_INFORMATION);

pbi = (smPPROCESS_BASIC_INFORMATION)HeapAlloc(hHeap,
                                              HEAP_ZERO_MEMORY,
                                              dwSize);
// Did we successfully allocate memory

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.

// Attempt to get basic info on process

NTSTATUS dwStatus = gNtQueryInformationProcess(hProcess,
                                               ProcessBasicInformation,
                                               pbi,
                                               dwSize,
                                               &dwSizeNeeded);
// Did we successfully get basic info on process

if(dwStatus >= 0)
{
    // Basic Info

    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.

// Read Process Environment Block (PEB)

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...

// We got Process Parameters, is CommandLine filled in

if(peb_upp.CommandLine.Length > 0) {
    // Yes, try to read CommandLine

    pwszBuffer = (WCHAR *)HeapAlloc(hHeap,
                                    HEAP_ZERO_MEMORY,
                                    peb_upp.CommandLine.Length);
    // If memory was allocated, continue

    if(pwszBuffer)
    {
       if(ReadProcessMemory(hProcess,
                            peb_upp.CommandLine.Buffer,
                            pwszBuffer,
                            peb_upp.CommandLine.Length,
                            &dwBytesRead))
       {
            // if commandline is larger than our variable, truncate

            if(peb_upp.CommandLine.Length >= sizeof(spi.szCmdLine)) 
                dwBufferSize = sizeof(spi.szCmdLine) - sizeof(TCHAR);
            else
                dwBufferSize = peb_upp.CommandLine.Length;
                            
            // Copy CommandLine to our structure variable

#if defined(UNICODE) || (_UNICODE)
            // Since core NT functions operate in Unicode

            // there is no conversion if application is

            // compiled for Unicode

            StringCbCopyN(spi.szCmdLine, sizeof(spi.szCmdLine),
                          pwszBuffer, dwBufferSize);
#else
            // Since core NT functions operate in Unicode

            // we must convert to Ansi since our application

            // is not compiled for Unicode

            WideCharToMultiByte(CP_ACP, 0, pwszBuffer,
                                (int)(dwBufferSize / sizeof(WCHAR)),
                                spi.szCmdLine, sizeof(spi.szCmdLine),
                                NULL, NULL);
#endif
        }
        if(!HeapFree(hHeap, 0, pwszBuffer)) {
            // failed to free memory

            bReturnStatus = FALSE;
            goto gnpiFreeMemFailed;
        }
    }
}    // Read CommandLine in Process Parameters

...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.

// We got Process Parameters, is ImagePathName filled in

if(peb_upp.ImagePathName.Length > 0) {
    // Yes, try to read Image Path

    pwszBuffer = (WCHAR *)HeapAlloc(hHeap,
                                    HEAP_ZERO_MEMORY,
                                    peb_upp.ImagePathName.Length);
    // If memory was allocated, continue

    if(pwszBuffer)
    {
       if(ReadProcessMemory(hProcess,
                            peb_upp.ImagePathName.Buffer,
                            pwszBuffer,
                            peb_upp.ImagePathName.Length,
                            &dwBytesRead))
       {
            // if image path is larger than our variable, truncate

            if(peb_upp.ImagePathName.Length >= sizeof(spi.szImgPath)) 
                dwBufferSize = sizeof(spi.szImgPath) - sizeof(TCHAR);
            else
                dwBufferSize = peb_upp.ImagePathName.Length;
                            
            // Copy ImagePathName to our structure variable

#if defined(UNICODE) || (_UNICODE)
            // Since core NT functions operate in Unicode

            // there is no conversion if application is

            // compiled for Unicode

            StringCbCopyN(spi.szImgPath, sizeof(spi.szImgPath),
                          pwszBuffer, dwBufferSize);
#else
            // Since core NT functions operate in Unicode

            // we must convert to Ansi since our application

            // is not compiled for Unicode

            WideCharToMultiByte(CP_ACP, 0, pwszBuffer,
                                (int)(dwBufferSize / sizeof(WCHAR)),
                                spi.szImgPath, sizeof(spi.szImgPath),
                                NULL, NULL);
#endif
        }
        if(!HeapFree(hHeap, 0, pwszBuffer)) {
            // failed to free memory

            bReturnStatus = FALSE;
            goto gnpiFreeMemFailed;
        }
    }
}    // Read ImagePathName in Process Parameters

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.

// System process for WinXP and later is PID 4 and we cannot access
// PEB, but we know it is aka ntoskrnl.exe so we will manually define it

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.

posted @ 2020-03-29 16:25  糖果的二师兄  阅读(327)  评论(0编辑  收藏  举报