Windows Process Listing Using WTS API – Part 1

In this detailed walkthrough of process listing using WTS API, you will learn the importance of the process listing and enumeration of anti-malware agents and will get your hands dirty with the source code

Windows Process Listing Using WTS API – Part 1
Photo by Filip Bunkens / Unsplash

Enumeration plays one of the most important roles in red-teaming. The one who is good enough in enumeration, will eventually find a loophole and penetrate through the security layers of the organization. In this post, I will guide you through the code of getting a list of processes running on the target system.

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

Overview

Generally, an executable file is called an Image file because it contains the information of execution. As soon as you click it, the windows process loader will fork the process and load it into memory. The running instance of the image file is now known as the process.

Now you must be thinking that, why do you need this? Why not simply start the tasks of post-exploitation and installing meterpreter persistence, pivoting through networks and stuff? – Well this is OK when you are learning stuff on dedicated attack labs provided by the platforms such as HTB or AttackDefense which gives you a simulated environment. But in the real world, there are a lot of agents and services running in the enterprise systems that would easily detect even the rarest technique you can think of.

Since listing processes is normal as even the employees use it often to list and kill resource-hungry tasks and it will always be off the radar of EDRs and other agents installed on the system, we can leverage this to

  • Enumerate the processes to target on the victim's machine
  • Identify running antiviruses or agents so that you should always be stealthy
  • Get memory dumps of the processes or inject them into the memory to disrupt the logic of the applications
  • Other post exploitations like privilege escalation, process migrations, token stealing, impersonation.

Although there are different ways that I will be discussing in the upcoming posts, in this post I will stick to WTSEnumerateProcessEx API from Wtsapi32 library in the defined Win32 API. This is because different techniques require different privileges and might return different data.

About WTS Enumerate API

Wtsapi32 is usually used for remote desktop services but can run in a local context by providing the current server handle. Handles are basically the pointers I will discuss in a separate post with you, for now, you can consider them as a reference to a specific object, like the handle to file can be used either to read or write data and server handle is used to query information or execute certain commands on the remote desktop.

We will be using the WTSEnumerateProcessesExA function defined in the WtsApi32 library.

BOOL WTSEnumerateProcessesExA(
  [in]      HANDLE hServer,
  [in, out] DWORD  *pLevel,
  [in]      DWORD  SessionId,
  [out]     LPSTR  *ppProcessInfo,
  [out]     DWORD  *pCount
);
Signature of WTSEnumerateProcessesExA from wtsapi32.h header

Here [in] means the function will accept this argument and [out] means you pass the variable and it will assign the value to it. Mostly pointer variables will be used in place of [out] because they store reference which is preserved for the entire scope of the variable or program.

Following is the description of the parameters in the same order as in the function signature

  1. Since in this case we are concerned with the current desktop, so we can use WTS_CURRENT_SERVER_HANDLE which will tell the function to enumerate the processes from the current server instead
  2. There are two levels of information you can fetch, and from the infosec perspective, it is recommended to gather as much as information you can. Therefore to get the data of type WTS_PROCESS_INFO_EX, I have set it to 0x1 here
  3. When a user logs in to the windows, they create a session. Windows is a multi-user operation and different services run with different users to prevent permission issues and with controlled access extra security. You can get the list of all the processes from all the sessions by sending WTS_ANY_SESSION to the function
  4. This pointer holds the reference to all the information related to processing which is in the WTS_PROCESS_INFO_EXA struct. Since the parameter is marked as [out] this means we are supposed to send the nullptr and the function will initialize it on its own.
  5. A reference to the DWORD variable that will contain a total number of processes the pointer contains. This will be later used to iterate over the pointer container.

Now since we have understood all about parameters, the function returns BOOL which we can use to determine whether the execution resulted in success or failure.

Formatting and Printing the Process Details

Since we have a pointer to the struct of WTS_PROCESS_INFO_EXA which stored dwProcessCount process entries are in continuous memory, we can use (pProcesses + c) iterate over each process entry. Skipping the bytes to fit the size of the structure will be taken care of by the compiler. At the low level, it will be translated to (BASE + sizeof(_STRUCT) * Index)

After printing the basic information the there comes complex information such as getting SID and user details who have started the process (aka effective user in the Linux terms). The SID is the security identification number provided to each user during login and are unique for every user account across the domain. This information is generated for each user by the Local Security Auditor (or lsass.service) and it is equivalent to a Unix UID system.

It is a PSID structure and you can use the ConvertSidToStringSidA function from the sddl.h header which is defined in the Advapi32.lib library. The function accepts a pointer to the SID object, which you can supply by providing pUserSid and the second parameter accepts a nullptr initialized PCHAR which will contain the serialized value of the SID. If the function is executed successfully, it will return TRUE otherwise FALSE and the possible reason for failures is listed here. You can find this here in the source code

BOOL ConvertSidToStringSidA(
  [in]  PSID  Sid,
  [out] LPSTR *StringSid
);
Signature of ConvertSidToStringSidA function from sddl.h header

Since this is unique for users across the domain, this means it can be used to get the Domain and account name of the user. Well this can be done by using LookupAccountSidA function from the Winbase.h header and defined in Advapi32.lib.

BOOL LookupAccountSidA(
  [in, optional]  LPCSTR        lpSystemName,
  [in]            PSID          Sid,
  [out, optional] LPSTR         Name,
  [in, out]       LPDWORD       cchName,
  [out, optional] LPSTR         ReferencedDomainName,
  [in, out]       LPDWORD       cchReferencedDomainName,
  [out]           PSID_NAME_USE peUse
);
Signature of LookupAccountSidA function from Winbase.h header

The function parameter description with respect to the source code implemented here are as follows

  1. Since we want to get the account and domain for the local system only, this parameter must be set to nullptr
  2. Provide the pointer to the SID from pUserSid
  3. Provide an array of the MAX_PATH length which will hold the account name
  4. Create a DWORD and assign the value of MAX_PATH to it. Then later pass the reference of it to the parameter. On success, this will contain the length of the Name buffer including terminating NULL character
  5. Similar to point 3, create a CHAR array for the domain name
  6. Create a DWORD and assign the value of MAX_PATH to it. Then later pass the reference of it to the parameter. On success, this will contain the length of the ReferencedDomainName buffer including terminating NULL character
  7. This parameter requires a reference to SID_NAME_USE enumeration that contains the type of the SID. For example, whether it is a user or a group. The function which returns the serialized type of the token is coded here

Note: The function might fail if the buffer size of Name or ReferencedDomainName is too small or cchName or cchReferenceDomainName is Zero value

Listing the processes with unprivileged user

From the unprivileged session, you will see most of the account and domain name lookups will fail because of insufficient permissions and if you try an escalated system, it will still fail for very few of them.

The account and domain is shown for most of the processes in the privileged session

But when you look same processes in the Process Explorer, it will be shown. Well, I have good news for you. I scratched my head for you to find both reason and solution for you.

The account and domain name of the process is shown in privileged Process Explorer

When you look carefully into the process explorer's own process details, you will see it has SeDebugPrivilege is enabled. You can think of Privileges are like Capabilities in Linux and SeDebugPrivilege is similar to CAP_SYS_PTRACE in Linux. It allows the process to debug its memory, so it will surely be able to get the basic details like the username of the owner.

SeDebugPrivilege is enabled in the Process Explorer

To know how dangerous SeDebugPrivilege is, I would recommend you to read this post by Microsoft – https://devblogs.microsoft.com/oldnewthing/20080314-00/?p=23113. In the next post, I share a walkthrough of the AddSeDebugPrivileges() and the SpawnElevatedProcess() function defined in the pch.h file

References