Skip to content

Latest commit

 

History

History
467 lines (359 loc) · 19.6 KB

File metadata and controls

467 lines (359 loc) · 19.6 KB

Enumerating Processes with WTS Functions: A Technical Guide

Programmatically listing running processes is a fundamental task in Windows system administration and development. While standard APIs like EnumProcesses or WMI's Win32_Process class are common, they might not provide the necessary session context, especially in multi-user environments like Remote Desktop Services (RDS), formerly known as Windows Terminal Services (WTS).

The Windows Terminal Services API offers a specialized set of functions designed for managing RDS sessions, users, and processes within those sessions. This article delves into two key WTS functions for process enumeration: WTSEnumerateProcesses and its more detailed counterpart, WTSEnumerateProcessesEx. We will explore their syntax, associated data structures, usage with C++ code examples, memory management considerations, and inherent limitations.

Method 1: Basic Process Enumeration with WTSEnumerateProcesses

The WTSEnumerateProcesses function provides a foundational method to retrieve information about active processes running on a specified RD Session Host server.

API Function: WTSEnumerateProcesses

This function retrieves a list of processes along with basic information like Process ID (PID), Session ID, process name, and the user's Security Identifier (SID).

Syntax (C/C++):

BOOL WTSEnumerateProcessesA(
  [in]  HANDLE             hServer,
  [in]  DWORD              Reserved,
  [in]  DWORD              Version,
  [out] PWTS_PROCESS_INFOA *ppProcessInfo,
  [out] DWORD              *pCount
);

BOOL WTSEnumerateProcessesW(
  [in]  HANDLE             hServer,
  [in]  DWORD              Reserved,
  [in]  DWORD              Version,
  [out] PWTS_PROCESS_INFOW *ppProcessInfo,
  [out] DWORD              *pCount
);

(Note: The WTSEnumerateProcesses macro automatically selects the ANSI (A) or Unicode (W) version based on project settings.)

Parameters:

  • hServer: A handle to the target RD Session Host server. Use WTSOpenServer to get a handle to a remote server, or WTS_CURRENT_SERVER_HANDLE for the local machine.
  • Reserved: This parameter is reserved and must be set to 0.
  • Version: Specifies the version of the request. This must be set to 1.
  • ppProcessInfo: A pointer to a variable that receives a pointer to an array of WTS_PROCESS_INFO structures. The API allocates this buffer.
  • pCount: A pointer to a DWORD variable that receives the number of WTS_PROCESS_INFO structures returned in the ppProcessInfo buffer.

Associated Structure: WTS_PROCESS_INFO

This structure contains the basic information returned for each process.

Definition (C/C++):

typedef struct _WTS_PROCESS_INFOA {
  DWORD SessionId;
  DWORD ProcessId;
  LPSTR pProcessName;
  PSID  pUserSid;
} WTS_PROCESS_INFOA, *PWTS_PROCESS_INFOA;

typedef struct _WTS_PROCESS_INFOW {
  DWORD SessionId;
  DWORD ProcessId;
  LPWSTR pProcessName;
  PSID   pUserSid;
} WTS_PROCESS_INFOW, *PWTS_PROCESS_INFOW;

Members:

  • SessionId: The RDS session identifier associated with the process.
  • ProcessId: The unique process identifier (PID).
  • pProcessName: Pointer to a null-terminated string containing the executable file name.
  • pUserSid: Pointer to the Security Identifier (SID) of the user associated with the process's primary access token.

Memory Management: WTSFreeMemory

A crucial aspect of using WTSEnumerateProcesses is memory management. The function allocates the memory buffer pointed to by ppProcessInfo. It is the caller's responsibility to free this memory once it's no longer needed using the WTSFreeMemory function.

void WTSFreeMemory(
  [in] PVOID pMemory
);

Failure to call WTSFreeMemory will result in memory leaks.

Code Example (C++)

Here's a C++ example demonstrating how to use WTSEnumerateProcesses to list processes on the current machine. This example requires linking against wtsapi32.lib.

#include <windows.h>
#include <wtsapi32.h>
#include <sddl.h>
#include <stdio.h>
#include <string>
#include <locale>
#include <codecvt>

#pragma comment(lib, "wtsapi32.lib")

// Helper function to convert SID to Username (Domain\User)
std::wstring GetUserNameFromSid(PSID sid) {
    if (sid == nullptr) {
        return L"";
    }

    WCHAR name[128];
    WCHAR domain[64];
    DWORD nameLen = _countof(name);
    DWORD domainLen = _countof(domain);
    SID_NAME_USE use;

    if (!LookupAccountSidW(nullptr, sid, name, &nameLen, domain, &domainLen, &use)) {
        // Optionally check GetLastError() for specific errors
        return L"<Error LookupAccountSid>";
    }

    // Handle well-known SIDs or cases where domain might be empty
    if (domainLen > 0) {
        return std::wstring(domain) + L"\\" + name;
    } else {
        return std::wstring(name); // e.g., "NT AUTHORITY\SYSTEM"
    }
}

bool EnumerateProcessesBasic() {
    PWTS_PROCESS_INFOW pProcessInfo = nullptr;
    DWORD processCount = 0;

    if (!WTSEnumerateProcessesW(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pProcessInfo, &processCount)) {
        fprintf(stderr, "WTSEnumerateProcesses failed. Error %lu\n", GetLastError());
        return false;
    }

    printf("--- Processes (Basic Info) ---\n");
    printf("%5s %3s %-30s %s\n", "PID", "SID", "User", "Process Name");
    printf("--------------------------------------------------------------------\n");

    for (DWORD i = 0; i < processCount; ++i) {
        PWTS_PROCESS_INFOW pi = &pProcessInfo[i];
        std::wstring userName = GetUserNameFromSid(pi->pUserSid);
        
        // Convert wstring to narrow string for printf if needed (basic example)
        std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
        std::string narrowUserName = converter.to_bytes(userName);
        std::string narrowProcessName = converter.to_bytes(pi->pProcessName ? pi->pProcessName : L"<N/A>");

        printf("%5lu %3lu %-30s %s\n", 
               pi->ProcessId, 
               pi->SessionId, 
               narrowUserName.c_str(),
               narrowProcessName.c_str());
    }

    // CRITICAL: Free the memory allocated by WTSEnumerateProcesses
    if (pProcessInfo) {
        WTSFreeMemory(pProcessInfo);
        pProcessInfo = nullptr; // Good practice
    }

    return true;
}

/*
int main() {
    if (!EnumerateProcessesBasic()) {
        return 1;
    }
    return 0;
}
*/

Technical Details & Limitations

  • Basic Information: This function provides only a limited set of process details.
  • Administrator Privileges: To see processes running in sessions other than the caller's own, or system processes, the calling application typically needs to run with administrative privileges.
  • NULL SID/Name: For certain system processes (like the Idle process, PID 0) or when running without sufficient rights, the pUserSid might be NULL, and pProcessName might be missing or generic (e.g., "System").
  • Memory Management: Correctly calling WTSFreeMemory is essential to prevent leaks.

While useful for basic process listing, especially when session context is important, WTSEnumerateProcesses lacks the detailed metrics often required for in-depth analysis. For that, we turn to its extended counterpart.

Method 2: Extended Process Enumeration with WTSEnumerateProcessesEx

While WTSEnumerateProcesses provides basic process information, WTSEnumerateProcessesEx offers a more comprehensive view with additional metrics like memory usage, thread count, handle count, and CPU time. This function was introduced in Windows 7 and Windows Server 2008 R2.

API Function: WTSEnumerateProcessesEx

This enhanced function allows for retrieving either basic or extended process information based on the specified level parameter.

Syntax (C/C++):

BOOL WTSEnumerateProcessesExA(
  [in]      HANDLE hServer,
  [in, out] DWORD  *pLevel,
  [in]      DWORD  SessionId,
  [out]     LPSTR  *ppProcessInfo,
  [out]     DWORD  *pCount
);

BOOL WTSEnumerateProcessesExW(
  [in]      HANDLE hServer,
  [in, out] DWORD  *pLevel,
  [in]      DWORD  SessionId,
  [out]     LPWSTR *ppProcessInfo,
  [out]     DWORD  *pCount
);

Parameters:

  • hServer: A handle to the target RD Session Host server. Use WTSOpenServer to get a handle to a remote server, or WTS_CURRENT_SERVER_HANDLE for the local machine.
  • pLevel: A pointer to a DWORD that, on input, specifies the type of information to return:
    • 0: Return an array of WTS_PROCESS_INFO structures (basic information).
    • 1: Return an array of WTS_PROCESS_INFO_EX structures (extended information).
  • SessionId: The session ID for which to enumerate processes. Use WTS_ANY_SESSION to enumerate processes across all sessions.
  • ppProcessInfo: A pointer to a variable that receives a pointer to an array of structures. The type of structure depends on the pLevel parameter.
  • pCount: A pointer to a DWORD variable that receives the number of structures returned.

Associated Structure: WTS_PROCESS_INFO_EX

When pLevel is set to 1, the function returns an array of WTS_PROCESS_INFO_EX structures, which contain extended process information.

Definition (C/C++):

typedef struct _WTS_PROCESS_INFO_EXA {
  DWORD         SessionId;
  DWORD         ProcessId;
  LPSTR         pProcessName;
  PSID          pUserSid;
  DWORD         NumberOfThreads;
  DWORD         HandleCount;
  DWORD         PagefileUsage;
  DWORD         PeakPagefileUsage;
  DWORD         WorkingSetSize;
  DWORD         PeakWorkingSetSize;
  LARGE_INTEGER UserTime;
  LARGE_INTEGER KernelTime;
} WTS_PROCESS_INFO_EXA, *PWTS_PROCESS_INFO_EXA;

typedef struct _WTS_PROCESS_INFO_EXW {
  DWORD         SessionId;
  DWORD         ProcessId;
  LPWSTR        pProcessName;
  PSID          pUserSid;
  DWORD         NumberOfThreads;
  DWORD         HandleCount;
  DWORD         PagefileUsage;
  DWORD         PeakPagefileUsage;
  DWORD         WorkingSetSize;
  DWORD         PeakWorkingSetSize;
  LARGE_INTEGER UserTime;
  LARGE_INTEGER KernelTime;
} WTS_PROCESS_INFO_EXW, *PWTS_PROCESS_INFO_EXW;

Members:

  • SessionId, ProcessId, pProcessName, pUserSid: Same as in WTS_PROCESS_INFO.
  • NumberOfThreads: The number of threads in the process.
  • HandleCount: The number of handles the process has open.
  • PagefileUsage: The amount of page file space being used by the process, in bytes.
  • PeakPagefileUsage: The peak amount of page file space used by the process, in bytes.
  • WorkingSetSize: The current working set size of the process, in bytes.
  • PeakWorkingSetSize: The peak working set size of the process, in bytes.
  • UserTime: The amount of time the process has executed in user mode, in 100-nanosecond intervals.
  • KernelTime: The amount of time the process has executed in kernel mode, in 100-nanosecond intervals.

Memory Management: WTSFreeMemoryEx

Unlike WTSEnumerateProcesses which uses WTSFreeMemory, the extended version requires a different function for memory cleanup: WTSFreeMemoryEx. This function takes additional parameters to specify the type of memory being freed.

BOOL WTSFreeMemoryExA(
  [in] WTS_TYPE_CLASS WTSTypeClass,
  [in] PVOID          pMemory,
  [in] ULONG          NumberOfEntries
);

BOOL WTSFreeMemoryExW(
  [in] WTS_TYPE_CLASS WTSTypeClass,
  [in] PVOID          pMemory,
  [in] ULONG          NumberOfEntries
);

Parameters:

  • WTSTypeClass: Specifies the type of structure being freed. For WTSEnumerateProcessesEx with pLevel=1, use WTSTypeProcessInfoLevel1.
  • pMemory: Pointer to the memory to free.
  • NumberOfEntries: The number of structures in the array.

Code Example (C++)

Here's a C++ example demonstrating how to use WTSEnumerateProcessesEx to retrieve extended process information:

#include <windows.h>
#include <wtsapi32.h>
#include <sddl.h>
#include <stdio.h>
#include <string>
#include <locale>
#include <codecvt>

#pragma comment(lib, "wtsapi32.lib")

// Helper function to convert SID to Username (reused from previous example)
std::wstring GetUserNameFromSid(PSID sid) {
    // Implementation same as in previous example
    // ...
}

// Helper function to format CPU time
std::wstring GetCpuTime(const LARGE_INTEGER& kernelTime, const LARGE_INTEGER& userTime) {
    // Convert 100-nanosecond intervals to seconds
    ULONGLONG totalTime = (kernelTime.QuadPart + userTime.QuadPart) / 10000000ULL;
    
    ULONGLONG hours = totalTime / 3600;
    ULONGLONG minutes = (totalTime % 3600) / 60;
    ULONGLONG seconds = totalTime % 60;
    
    wchar_t buffer[32];
    swprintf_s(buffer, L"%02llu:%02llu:%02llu", hours, minutes, seconds);
    return std::wstring(buffer);
}

bool EnumerateProcessesExtended() {
    PWTS_PROCESS_INFO_EXW pProcessInfo = nullptr;
    DWORD processCount = 0;
    DWORD level = 1;  // Request extended information
    
    if (!WTSEnumerateProcessesExW(WTS_CURRENT_SERVER_HANDLE, &level, 
                                 WTS_ANY_SESSION, (LPWSTR*)&pProcessInfo, &processCount)) {
        fprintf(stderr, "WTSEnumerateProcessesEx failed. Error %lu\n", GetLastError());
        return false;
    }
    
    printf("--- Processes (Extended Info) ---\n");
    printf("%5s %3s %4s %5s %10s %10s %10s %s\n", 
           "PID", "SID", "Thds", "Hdls", "WS(KB)", "Peak(KB)", "CPU Time", "Process Name");
    printf("-------------------------------------------------------------------------\n");
    
    std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
    
    for (DWORD i = 0; i < processCount; ++i) {
        PWTS_PROCESS_INFO_EXW pi = &pProcessInfo[i];
        std::wstring userName = GetUserNameFromSid(pi->pUserSid);
        std::wstring cpuTime = GetCpuTime(pi->KernelTime, pi->UserTime);
        
        // Convert to narrow strings for printf
        std::string narrowProcessName = converter.to_bytes(pi->pProcessName ? pi->pProcessName : L"<N/A>");
        
        printf("%5lu %3lu %4lu %5lu %10lu %10lu %10s %s\n", 
               pi->ProcessId, 
               pi->SessionId,
               pi->NumberOfThreads,
               pi->HandleCount,
               pi->WorkingSetSize / 1024,  // Convert to KB
               pi->PeakWorkingSetSize / 1024,  // Convert to KB
               converter.to_bytes(cpuTime).c_str(),
               narrowProcessName.c_str());
    }
    
    // CRITICAL: Free the memory allocated by WTSEnumerateProcessesEx
    if (pProcessInfo) {
        WTSFreeMemoryEx(WTSTypeProcessInfoLevel1, pProcessInfo, processCount);
        pProcessInfo = nullptr;
    }
    
    return true;
}

/*
int main() {
    if (!EnumerateProcessesExtended()) {
        return 1;
    }
    return 0;
}
*/

Technical Details & Limitations

  • Memory Usage Limitations: A significant limitation of WTS_PROCESS_INFO_EX is that memory-related fields (PagefileUsage, WorkingSetSize, etc.) are 32-bit DWORD values even on 64-bit systems. This means they can only represent values up to 4GB. For processes using more memory, these fields will overflow and show incorrect values.

  • CPU Time Representation: The UserTime and KernelTime fields are stored as LARGE_INTEGER values representing 100-nanosecond intervals. Proper conversion is needed to display them in a human-readable format.

  • Administrator Privileges: Like WTSEnumerateProcesses, this function requires administrative privileges to see processes in sessions other than the caller's own.

  • Memory Management: The memory allocated by WTSEnumerateProcessesEx must be freed using WTSFreeMemoryEx with the correct type class (WTSTypeProcessInfoLevel1 for extended information) and count.

  • Availability: This function is available starting from Windows 7 and Windows Server 2008 R2, so it won't work on older Windows versions.

Comparison and Practical Use Cases

Direct Comparison

Feature WTSEnumerateProcesses WTSEnumerateProcessesEx
Available Since Windows Vista Windows 7/Server 2008 R2
Information Detail Basic (PID, Session, Name, SID) Extended (adds threads, handles, memory, CPU time)
Structure WTS_PROCESS_INFO WTS_PROCESS_INFO_EX (when pLevel=1)
Memory Freeing WTSFreeMemory WTSFreeMemoryEx
Session Filtering No (all sessions) Yes (can specify SessionId parameter)
Memory Metrics None Yes, but limited to 32-bit values
CPU Time None Yes (UserTime and KernelTime)

When to Use Which Function

  • Use WTSEnumerateProcesses when:

    • You only need basic process identification (PID, name, session, user).
    • You're working with older Windows versions (pre-Windows 7).
    • Simplicity is preferred over detailed metrics.
  • Use WTSEnumerateProcessesEx when:

    • You need detailed process metrics (threads, handles, memory, CPU time).
    • You want to filter processes by session ID.
    • You're working with Windows 7/Server 2008 R2 or newer.
    • You're aware of and can handle the 32-bit limitation for memory metrics.

Practical Use Cases

  1. RDS Session Management:

    • Monitor per-session resource usage to identify problematic users or applications.
    • Track process distribution across sessions for load balancing.
  2. Security Monitoring:

    • Identify unexpected processes running in user sessions.
    • Detect processes running with elevated privileges.
    • Monitor for processes running in disconnected sessions.
  3. Performance Analysis:

    • Track resource-intensive processes across multiple user sessions.
    • Identify memory leaks or handle leaks in long-running processes.
    • Compare CPU usage patterns between different user sessions.
  4. Custom Administration Tools:

    • Build session-aware task managers for RDS environments.
    • Create automated session cleanup tools that target specific process types.
    • Develop monitoring dashboards for multi-user environments.

Conclusion

The Windows Terminal Services API provides specialized functions for process enumeration in multi-user environments, particularly useful for Remote Desktop Services scenarios. WTSEnumerateProcesses offers basic process information with a simple interface, while WTSEnumerateProcessesEx provides more detailed metrics at the cost of slightly more complex usage.

When working with these functions, remember these key points:

  1. Choose the right function based on your information needs and target Windows version.
  2. Handle memory correctly by using the appropriate free function (WTSFreeMemory or WTSFreeMemoryEx).
  3. Be aware of limitations, particularly the 32-bit size of memory metrics in WTS_PROCESS_INFO_EX.
  4. Consider privilege requirements for accessing processes across different sessions.

By leveraging these WTS functions, developers and administrators can build more effective tools for managing and monitoring multi-user Windows environments, particularly in Remote Desktop Services deployments where session context is crucial for proper process management.

References