Post

NtQuerySystemInformation

Exploring Process Enumeration with NtQuerySystemInformation

Understanding how to enumerate processes in a system is crucial. One method to achieve this is through the use of NtQuerySystemInformation, a syscall that provides extensive information about the system. This function, exported from the ntdll.dll module, requires the use of GetModuleHandle and GetProcAddress to retrieve its address.

Understanding NtQuerySystemInformation

According to Microsoft’s documentation, NtQuerySystemInformation can return a wealth of system information. For process enumeration, we focus on using the SystemProcessInformation flag, which returns an array of SYSTEM_PROCESS_INFORMATION structures, each representing a running process.

Retrieving NtQuerySystemInformation

To use NtQuerySystemInformation, you first need to retrieve its address from ntdll.dll. This involves defining a function pointer and using GetProcAddress and GetModuleHandle to locate the function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Function pointer
typedef NTSTATUS (NTAPI* fnNtQuerySystemInformation)(
	SYSTEM_INFORMATION_CLASS SystemInformationClass,
	PVOID                    SystemInformation,
	ULONG                    SystemInformationLength,
	PULONG                   ReturnLength
);

fnNtQuerySystemInformation pNtQuerySystemInformation = NULL;

// Getting NtQuerySystemInformation's address
pNtQuerySystemInformation = (fnNtQuerySystemInformation)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtQuerySystemInformation");
if (pNtQuerySystemInformation == NULL) {
	printf("[!] GetProcAddress Failed With Error : %d\n", GetLastError());
	return FALSE;
}

Parameters of NtQuerySystemInformation

The function takes several parameters:

  • SystemInformationClass: Specifies the type of system information to return.
  • SystemInformation: A pointer to a buffer that receives the requested information.
  • SystemInformationLength: The size of the buffer.
  • ReturnLength: A pointer to a variable that receives the actual size of the information.

For process enumeration, the SystemProcessInformation flag is used, which returns an array of SYSTEM_PROCESS_INFORMATION structures.

The SYSTEM_PROCESS_INFORMATION Structure

The SYSTEM_PROCESS_INFORMATION structure contains several fields, but for process enumeration, the focus is on UNICODE_STRING ImageName and UniqueProcessId. These fields provide the process name and ID, respectively. The NextEntryOffset field is used to navigate through the array of processes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    BYTE Reserved1[48];
    UNICODE_STRING ImageName;
    KPRIORITY BasePriority;
    HANDLE UniqueProcessId;
    PVOID Reserved2;
    ULONG HandleCount;
    ULONG SessionId;
    PVOID Reserved3;
    SIZE_T PeakVirtualSize;
    SIZE_T VirtualSize;
    ULONG Reserved4;
    SIZE_T PeakWorkingSetSize;
    SIZE_T WorkingSetSize;
    PVOID Reserved5;
    SIZE_T QuotaPagedPoolUsage;
    PVOID Reserved6;
    SIZE_T QuotaNonPagedPoolUsage;
    SIZE_T PagefileUsage;
    SIZE_T PeakPagefileUsage;
    SIZE_T PrivatePageCount;
    LARGE_INTEGER Reserved7[6];
} SYSTEM_PROCESS_INFORMATION;

Calling NtQuerySystemInformation

To enumerate processes, NtQuerySystemInformation must be called twice. The first call determines the size of the buffer needed, and the second call retrieves the process information. The first call is expected to fail with a STATUS_INFO_LENGTH_MISMATCH error, which indicates the buffer size required.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ULONG                        uReturnLen1    = NULL,
                             uReturnLen2    = NULL;
PSYSTEM_PROCESS_INFORMATION  SystemProcInfo = NULL;
NTSTATUS                     STATUS         = NULL;

// First NtQuerySystemInformation call
// This will fail with STATUS_INFO_LENGTH_MISMATCH
// But it will provide information about how much memory to allocate (uReturnLen1)
pNtQuerySystemInformation(SystemProcessInformation, NULL, NULL, &uReturnLen1);

// Allocating enough buffer for the returned array of `SYSTEM_PROCESS_INFORMATION` struct
SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (SIZE_T)uReturnLen1);
if (SystemProcInfo == NULL) {
	printf("[!] HeapAlloc Failed With Error : %d\n", GetLastError());
	return FALSE;
}
 
// Second NtQuerySystemInformation call
// Calling NtQuerySystemInformation with the correct arguments, the output will be saved to 'SystemProcInfo'
STATUS = pNtQuerySystemInformation(SystemProcessInformation, SystemProcInfo, uReturnLen1, &uReturnLen2);
if (STATUS != 0x0) {
	printf("[!] NtQuerySystemInformation Failed With Error : 0x%0.8X \n", STATUS);
	return FALSE;
}

Iterating Through Processes

Once the process information is retrieved, iterate through the array to access each process’s name and ID. Use NextEntryOffset to move to the next process in the array.

1
2
// 'SystemProcInfo' will now represent a new element in the array
SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)((ULONG_PTR)SystemProcInfo + SystemProcInfo->NextEntryOffset);

Freeing Allocated Memory

After processing the information, ensure that the allocated memory is freed to prevent memory leaks. Save the initial address of the allocated buffer to free it after use.

1
2
// Since we will modify 'SystemProcInfo', we will save its initial value before the while loop to free it later
pValueToFree = SystemProcInfo;

Complete Code for Process Enumeration

The complete code for enumerating processes using NtQuerySystemInformation is provided below. This code demonstrates how to retrieve and iterate through process information efficiently.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
BOOL GetRemoteProcessHandle(LPCWSTR szProcName, DWORD* pdwPid, HANDLE* phProcess) {

	fnNtQuerySystemInformation   pNtQuerySystemInformation = NULL;
	ULONG                        uReturnLen1               = NULL,
                                 uReturnLen2               = NULL;
    PSYSTEM_PROCESS_INFORMATION  SystemProcInfo            = NULL;
    NTSTATUS                     STATUS                    = NULL;
	PVOID                        pValueToFree              = NULL;
	
	pNtQuerySystemInformation = (fnNtQuerySystemInformation)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtQuerySystemInformation");
	if (pNtQuerySystemInformation == NULL) {
		printf("[!] GetProcAddress Failed With Error : %d\n", GetLastError());
		return FALSE;
	}

	pNtQuerySystemInformation(SystemProcessInformation, NULL, NULL, &uReturnLen1);

	SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (SIZE_T)uReturnLen1);
	if (SystemProcInfo == NULL) {
		printf("[!] HeapAlloc Failed With Error : %d\n", GetLastError());
		return FALSE;
	}

	// Since we will modify 'SystemProcInfo', we will save its initial value before the while loop to free it later
	pValueToFree = SystemProcInfo;

	STATUS = pNtQuerySystemInformation(SystemProcessInformation, SystemProcInfo, uReturnLen1, &uReturnLen2);
	if (STATUS != 0x0) {
		printf("[!] NtQuerySystemInformation Failed With Error : 0x%0.8X \n", STATUS);
		return FALSE;
	}

	while (TRUE) {

		// Check the process's name size
		// Comparing the enumerated process name to the intended target process
		if (SystemProcInfo->ImageName.Length && wcscmp(SystemProcInfo->ImageName.Buffer, szProcName) == 0) {
			
			// Opening a handle to the target process, saving it, and then breaking 
			*pdwPid		= (DWORD)SystemProcInfo->UniqueProcessId;
			*phProcess	= OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)SystemProcInfo->UniqueProcessId);
			break;
		}

		// If NextEntryOffset is 0, we reached the end of the array
		if (!SystemProcInfo->NextEntryOffset)
			break;

		// Move to the next element in the array
		SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)((ULONG_PTR)SystemProcInfo + SystemProcInfo->NextEntryOffset);
	}

	// Free using the initial address
	HeapFree(GetProcessHeap(), 0, pValueToFree);

	// Check if we successfully got the target process handle
	if (*pdwPid == NULL || *phProcess == NULL)
		return FALSE;
	else
		return TRUE;
}

The Undocumented Aspects of NtQuerySystemInformation

Despite its utility, NtQuerySystemInformation remains largely undocumented. The SYSTEM_PROCESS_INFORMATION structure, for instance, contains several Reserved fields whose purposes are not fully known. Alternative documentation, such as ReactOS and System Informer, provides more detailed insights into these structures.

Conclusion

This method provides a comprehensive view of running processes, aiding in malware analysis and system monitoring. By mastering these techniques, cybersecurity professionals can enhance their ability to detect and analyze malicious activities on a system.

Final project

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define TARGET_PROCESS L"Notepad.exe"

int EnumProcess_using_NtQuerySystemInformation_example() {
	DWORD		Pid = NULL;
	HANDLE		hProcess = NULL;

	if (!GetRemoteProcessHandleUsingNtQuerySystem(TARGET_PROCESS, &Pid, &hProcess)) {
		wprintf(L"[!] Cound Not Get %s's Process Id \n", TARGET_PROCESS);
		return -1;
	}

	wprintf(L"[+] FOUND \"%s\" - Of Pid : %d \n", TARGET_PROCESS, Pid);

	CloseHandle(hProcess);

	printf("[#] Press <Enter> To Quit ... ");
	getchar();

	return 0;
}

Demo

NtQuerySystemInformation

This post is licensed under CC BY 4.0 by the author.