In previous posts, I covered how to observe process information in Windbg by starting a debugging session and dumping the Proccess Environment Block.
And how we can step through the EPROCESS structure, including a linked-list of active processes via ActiveProcessLinks.
But in this post, we'll discuss yet another way of gleaning information about processes in Windows, this time from another structure within the Windows ecosystem: the SYSTEM_PROCESS_INFORMATION structure.
SYSTEM_PROCESS_INFORMATION Structure
Microsoft tells us in their documentation that this entry is related to various entries which hold important information about components within the Windows operating system.
When the SystemInformationClass parameter is SystemProcessInformation, the buffer pointed to by the SystemInformation parameter contains a SYSTEM_PROCESS_INFORMATION structure for each process. Each of these structures is immediately followed in memory by one or more SYSTEM_THREAD_INFORMATION structures that provide info for each thread in the preceding process. For more information about SYSTEM_THREAD_INFORMATION, see the section about this structure in this article.
The buffer pointed to by the SystemInformation parameter should be large enough to hold an array that contains as many SYSTEM_PROCESS_INFORMATION and SYSTEM_THREAD_INFORMATION structures as there are processes and threads running in the system. This size is specified by the ReturnLength parameter.
Microsoft goes on to give us the following type definition for the SYSTEM_PROCESS_INFORMATION entry and its inner structures, which gives us access to process variables like ImageNames, UniqueProcessId, and more:
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;
NTDLL, Home of .. Many Functions
In previous posts, I talked about how NTDLL.dll is where Windows user space frequently calls into in order to talk to relevant low-level parts of Windows and to do stuff in general. And this case is no exception.
In retrieving information from the SYSTEM_PROCESS_INFORMATION, we'll need to communicate with NTDLL through a couple of system calls. To reach the SYSTEM_PROCESS_INFORMATION structure, we'll need to do so through the Windows API via the QuerySystemInformation function, which Microsoft provides us with the following type definition for:
__kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);
Using Csharp to Talk to NTDLL
We'll translate this to Csharp and create the following declaration to call the NtQuerySystemInformation function and access the SYSTEM_PROCESS_INFORMATION structure. Since this function resides in NTDLL, we'll use the extern keyword to tell the compiler this. Furthermore, we know that the NTSTATUS returned from this call is an unsigned integer. So we'll define it like so, uint NtQuerySystemInformation
. And for SystemInformationClass
, too.
PVOID
, a pointer to void, is an IntPtr
in Csharp, an integer whose size is that of a pointer. This is for referencing unmanaged memory. Per Microsoft's documentation:
The IntPtr type can be used by languages that support pointers and as a common means of referring to data between languages that do and do not support pointers. IntPtr objects can also be used to hold handles. For example, instances of IntPtr are used extensively in the System. IO.
And SystemInformationLength
is a ulong
, or unsigned integer, which is a uint
in Csharp. And ReturnLength
is also a uint
. So, our initial declaration looks like this:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class Program
{
[DllImport("ntdll.dll")]
public static extern uint NtQuerySystemInformation(uint SystemInformationClass, IntPtr SystemInformation, uint SystemInformationLength, out uint ReturnLength);
To correctly read the outputs from the Windows API, as well as this structure, we'll need to utilize Unicode. This signature is straight forward. We have a Length
, Max Length
, and Buffer
. Microsoft clarifies this in their documentation:
When the ProcessInformationClass parameter is ProcessImageFileName, the buffer pointed to by the ProcessInformation parameter should be large enough to hold a UNICODE_STRING structure as well as the string itself. The string stored in the Buffer member is the name of the image file.
If the buffer is too small, the function fails with the STATUS_INFO_LENGTH_MISMATCH error code and the ReturnLength parameter is set to the required buffer size.
The UNICODE_STRING signature in Csharp is as follows:
[StructLayout(LayoutKind.Sequential)]
public struct UNICODE_STRING
{
public ushort Length;
public ushort MaximumLength;
public IntPtr Buffer;
}
Next, we'll need to Marshal the unmanaged SYSTEM_PROCESS_INFORMATION
structure. Microsoft provides us this type definition:
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;
But if we dig a bit deeper, we find this type definition provided by Microsoft is seemingly incomplete. Software analyst Geoff Chappell has provided a much more thorough overview of this structure.
If we reference Geoff Chappel's documentation, we see the SYSTEM_PROCESS_INFORMATION structure actually includes many attributes that Microsoft doesn't officially list.
So, here we'll use Geoff Chappell's analysis for reference since it provides a much more comprehensive layout of the structure.
We'll once again use a Csharp StructLayout
to Marshal this information so our program can handle it. After converting the types, our layout for the SYSTEM_PROCESS_INFORMATION
structure looks like this:
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEM_PROCESS_INFORMATION
{
public IntPtr NextEntryOffset;
public uint NumberOfThreads;
public LARGE_INTEGER WorkingSetPrivateSize;
public uint HardFaultCount;
public uint NumberOfThreadsHighWatermark;
public ulong CycleTime;
public LARGE_INTEGER CreateTime;
public LARGE_INTEGER UserTime;
public LARGE_INTEGER KernelTime;
public UNICODE_STRING ImageName;
public int BasePriority;
public IntPtr UniqueProcessId;
public IntPtr InheritedFromUniqueProcessId;
public uint HandleCount;
public uint SessionId;
public IntPtr UniqueProcessKey;
public IntPtr PeakVirtualSize;
public IntPtr VirtualSize;
public uint PageFaultCount;
public IntPtr PeakWorkingSetSize;
public IntPtr WorkingSetSize;
public IntPtr QuotaPeakPagedPoolUsage;
public IntPtr QuotaPagedPoolUsage;
public IntPtr QuotaPeakNonPagedPoolUsage;
public IntPtr QuotaNonPagedPoolUsage;
public IntPtr PagefileUsage;
public IntPtr PeakPagefileUsage;
public IntPtr PrivatePageCount;
public LARGE_INTEGER ReadOperationCount;
public LARGE_INTEGER WriteOperationCount;
public LARGE_INTEGER OtherOperationCount;
public LARGE_INTEGER ReadTransferCount;
public LARGE_INTEGER WriteTransferCount;
public LARGE_INTEGER OtherTransferCount;
}
The large_integer
will need to be correctly defined too. This was used in the aforementioned documentation. And it represents a 64-bit signed integer, e.g. long QuadPart
:
[StructLayout(LayoutKind.Sequential)]
public struct LARGE_INTEGER
{
public long QuadPart;
}
Next we'll declare and initialize the variables for our function. These will all be unsigned integers with the exception of the IntPtr. dwRet
will hold our return value from NtQuerySystemInformation
. dwSize
represents the size of the memory buffers we'll be operating on. We'll initialize this to zero. And dwStatus
represents a default error code indicating that a length mismatch has occurred. We'll set this as the default error status for now. And last, we'll initialize our pointer to zero.
public static void Main()
{
uint dwRet;
uint dwSize = 0x0;
uint dwStatus = 0xC0000004;
IntPtr p = IntPtr.Zero;
Now we'll initialize a loop. We'll check that our pointer isn't zero, if so we free it, then we Marshal an int
of memory on dwSize
, and use the elements we've declared to call into to NtQuerySystemInformation.
If the status returns 0, we break and continue. But if we encounter any other status than the 0xC0000004
indicating a mismatched length, we print an error and bail out entirely.
Else, we do a bitwise operation dwSize = dwRet + (2 << 12);
and increase the size of dwRet
, which is needed to hold the result.
while (true)
{
if (p != IntPtr.Zero) Marshal.FreeHGlobal(p);
p = Marshal.AllocHGlobal((int)dwSize);
dwStatus = NtQuerySystemInformation(5, p, dwSize, out dwRet);
if (dwStatus == 0) { break; }
else if (dwStatus != 0xC0000004)
{
Marshal.FreeHGlobal(p);
p = IntPtr.Zero;
Console.WriteLine("Data retrieval failed");
return;
}
dwSize = dwRet + (2 << 12);
}
Finally, we can loop through the values and print attributes from the SYSTEM_PROCESS_INFORMATION
structure as desired. We first assign an IntPtr
currentPtr
, to p.
We then use this to reference the unmanaged memory we've Marshaled in var processInfo
using Marshal.PtrToStructure
. Once more, we see the niceness of 'statically typed' Csharp here. We reference our pointer using the specific data types we've tailored for it, like this:
var processInfo = (SYSTEM_PROCESS_INFORMATION)Marshal.PtrToStructure(currentPtr, typeof(SYSTEM_PROCESS_INFORMATION));
And then we write out output with Console.WriteLine
, checking the values of the attributes we're referencing. If the ImageName.Buffer
is non-zero, we likely have a valid ImageName
. So we call Marshal.PtrToStringUni(processInfo.ImageName.Buffer)
on it to get the Unicode ImageName. And to extract the UniqueProcessId, we convert the value to a Int64
, a signed integer.
After each record, we move to the next entry using the NextEntryOffset
value. We convert this to a 32 bit integer, though. Per Microsoft's documentation:
NextEntryOffset (4 bytes): A 32-bit unsigned integer that MUST specify the offset, in bytes, from the current FILE_LINK_ENTRY_INFORMATION structure to the next FILE_LINK_ENTRY_INFORMATION structure. A value of 0 indicates this is the last entry structure.
Altogether, our last bit of code will look like this:
IntPtr currentPtr = p;
do
{
var processInfo = (SYSTEM_PROCESS_INFORMATION)Marshal.PtrToStructure(currentPtr, typeof(SYSTEM_PROCESS_INFORMATION));
Console.WriteLine($"[*] Image name: {(processInfo.ImageName.Buffer != IntPtr.Zero ? Marshal.PtrToStringUni(processInfo.ImageName.Buffer) : "")}");
Console.WriteLine($" > PID: {processInfo.UniqueProcessId.ToInt64()}");
Console.WriteLine();
// Calculate the offset to the next process entry
int offset = processInfo.NextEntryOffset.ToInt32();
if (offset == 0)
break;
// Move to the next process entry
currentPtr = IntPtr.Add(currentPtr, offset);
} while (true);
Marshal.FreeHGlobal(p);
}
}
On Github I've uploaded the Csharp code for this demonstration to a small repository dubbed "Cardinal."
No comments:
Post a Comment