Windows Process Listing using NTQuerySystemInformation

Get acquainted with the undocumented low-level yet powerful APIs from winternls and how to use the NtQuerySystemInformation function to get a list of all the processes running in the system

Windows Process Listing using NTQuerySystemInformation

Hello friends! Today you will learn one of the most underrated yet powerful APIs from the undocumented set of APIs. In this post, I will guide you on how to get the crucial details like the memory usage of the process along with basic details like PID, Process Name and Session ID using NTQuerySystemInformation function from ntdll.dll library

WinAPI-RedBlue/Process Listing/NT Query System Api at main · tbhaxor/WinAPI-RedBlue
Source codes of Windows API Exploitation for Red and Blue teams from Pentester Academy - WinAPI-RedBlue/Process Listing/NT Query System Api at main · tbhaxor/WinAPI-RedBlue

Overview

NTQuerySystemInformation is defined in the low-level library ntdll.dll, which contains the NT kernel functions. These functions are then later used by other high-level APIs like WTS32Api, ToolHelp32 or PSApi with another set of functions to provide required details.

For a long time, this API wasn't documented on MSDN because it's being used internally by the Windows OS and its developers. But later on, some reverse engineers dug deep into the applications and found that it exists and tried creating malware that circumvented the anti-malware software. So, Microsoft decided to document it in order to have the signatures for such malware. Well, this is purely what I think, not from any official resource.

This is widely used in the Sysinternal tools like Process Explorer and the Windows Task Manager. In this post, I will only guide you through its usage, not in conjunction with other functions to enumerate furthermore details of the process. You can use your knowledge from the previous posts and create your own stealthy process explorer tool. I would love to see that, once you do, let me know at [email protected]

Code Walkthrough

C++ is a strictly typed compiled language that validates the parameters and the return type of a program during compile time.  The winternl.h header file does not contain the prototype of this function. So let's have it in our pch.h header file

EXTERN_C NTSTATUS NTAPI NtQuerySystemInformation(
	IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
	OUT PVOID               SystemInformation,
	IN ULONG                SystemInformationLength,
	OUT PULONG              ReturnLength OPTIONAL
);
Signature of NtQuerySystemInformation function

This signature is shamelessly copied from the MSDN documentation here. This also contains other rarely used APIs from the ntdll.dll library.

You will see IN and OUT placeholders before the types of the parameters. They are defined as nothing, so during compile time, nothing will be replaced at that place. This is only used to help developers by giving additional information about how to handle the argument.

#ifndef IN
#define IN
#endif

#ifndef OUT
#define OUT
#endif
IN and OUT definition in the minwindef.h header file

Getting an appropriate buffer with a list of all the processes

The NtQuerySystemInformation function requires a pre-allocated buffer to copy the information and its size. Since we don't have the required size, we can use the same function to return the amount of size required and loop until all the process gets copied into the buffer.

Function parameters description for NtQuerySystemInformation are as follows:–

  1. Pass the value SystemProcessInformation from the SYSTEM_INFORMATION_CLASS enumeration class to tell the function that we want to process specific information only.
  2. Convert the allocated buffer of SYSTEM_PROCESS_INFORMATION struct to LPVOID and pass its reference here. Once the function will succeed, this will contain all the required information about processes
  3. Pass the size used while allocating the above buffer.
  4. Create a DWORD to contain a number of bytes returned and pass its reference to this parameter. If the function fails with the error code STATUS_INFO_LENGTH_MISMATCH, this parameter will contain the actual size required to allocate the container buffer.

Traversing the entries from the buffer

The very first thing to print is the unique process identifier. In this, we will get UniqueProcessId in the form of a HANDLE object. Luckily there is a macro used to convert the handles to ULONGHandleToUlong.

Now it's time for the image name (aka process's name). It is a UNICODE_STRING structure containing an optional Buffer field. To keep things simple and easy, you can use the following ternary syntax directly in the std::wcout stream.

(p->ImageName.Buffer ? p->ImageName.Buffer : L"")
Handle optional image name buffer using ternary operation

After printing the number of handles open and threads running, their other set fields left contain the data related to occupied size in bytes. Now printing such a number is pretty boring. We can use the StrFormatByteSizeW from the shlwapi.h header file. You can see the usage of this function here, in the code. If the function fails, it will return NULL

PWSTR StrFormatByteSizeW(
        LONGLONG qdw,
  [out] PWSTR    pszBuf,
        UINT     cchBuf
);
Signature of StrFormatByteSizeW function from the shlwapi.h header file
  1. Size in bytes to be converted to human-readable format.
  2. Create a buffer with size MAX_PATH and pass its reference here which will contain the contents of the formatted size
  3. Pass the size of the buffer pointed to by pszBuf, in characters.
💡
In Windows 10, size is reported in base 10 rather than base 2. For example, 1 KB is 1000 bytes rather than 1024.

Check the video below to know how this function works and what information has been returned by it.

Resources