Windows Process Listing Using WTS API – Part 2

In this post, you will learn how to gracefully enable SeDebugPrivilege and automatically launch the process using ShellExecuteExA with administrator privileges. This is in continuation to part 1 of windows process listing using wts api.

Windows Process Listing Using WTS API – Part 2
Photo by Daniil Komov / Unsplash

This is the second part in continuation to Part 1 of Windows Process Listing Using WTS API. So if you haven't read that, please do it before going forward with this post. In this part, I will be discussing how to enable SeDebugPrivilege in the administrator process and automatically elevate the process if it's running with the least privileged user. So let's first begin with the walkthrough of the AddSeDebugPrivilege() function in the pch.h file

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

Adding Provision for Allowing SeDebugPrivilege

This privilege is already there in the administrator owned processes but not enabled by default because of security reasons. This is being invoked from the main function in the Source.cpp file in the starting. So let's start its walkthrough as well

First of all, open the process to get its handle. You can get this by calling the OpenProcess() function from the processthreadapi.h header file.

HANDLE OpenProcess(
  [in] DWORD dwDesiredAccess,
  [in] BOOL  bInheritHandle,
  [in] DWORD dwProcessId
);
Signature of OpenProcess function from processthreadapi.h header

The function accepts three parameters, described as follows

  1. We need to query the information in the process to get and adjust token privileges, so we need to use PROCESS_QUERY_INFORMATION from process access rights
  2. Whether or not the process handle is inherited, it doesn't concern us. So let's keep it FALSE
  3. Provide the process id for which you want to open the handle. Since we want to open the current process handle, I have used the GetCurrentProcessId() function from processthreadapi.h. Alternatively, you can use the GetCurrentProcess() function, this will give you the handle with PROCESS_ALL_ACCESS access right.

NOTE:– Since you will surely get the handle on the current process, but it's a good practice to add safeguard just in case it's blocked somehow. You can do this by checking whether it is NULL or INVALID_HANDLE_VALUE. Well in the above function, on failure, it will return NULL. I won't go deeper in explaining the difference, as it is already explained here

To get the process's token handle, I have used the OpenProcessToken() function from the processthreadapi.h header file. If the function succeeds, it will return TRUE.

BOOL OpenProcessToken(
  [in]  HANDLE  ProcessHandle,
  [in]  DWORD   DesiredAccess,
  [out] PHANDLE TokenHandle
);
Signature of OpenProcessToken function from processthreadapi.h header

Provide the handle of the current process handle before this step. The handle must have PROCESS_QUERY_INFORMATION permission. That is why to have only particular permission in the handle, I have used OpenProcess instead of GetCurrentProcess.

  1. Not of our concern, simply provide FALSE
  2. Create a variable of type HANDLE and pass its reference here to get the token handle

Since we now have all the prerequisites of adding the privilege to the token, let's move to query the privilege value using the LookupPrivilegeValueA() function from the Winbase.h header file. If the function executes successfully, it will return TRUE

BOOL LookupPrivilegeValueA(
  [in, optional] LPCSTR lpSystemName,
  [in]           LPCSTR lpName,
  [out]          PLUID  lpLuid
);
Signature of LookupPrivilegeValueA function from Winbase.h header
  1. Provide nullptr here, because we have to perform a lookup on the local system
  2. Since the privilege, we are looking for is "SeDebugPrivilege", pass this string here. You can find all the privilege names listed here
  3. Create an LUID instance and assign it to nullptr and pass its reference here. The function will automatically allocate memory and assign the value to it.

Till now you have completed the first step. The next step is to adjust the token privileges using the AdjustTokenPrivileges() function from the securitybaseapi.h header. If the function is executed successfully, it will return TRUE otherwise FALSE.

BOOL AdjustTokenPrivileges(
  [in]            HANDLE            TokenHandle,
  [in]            BOOL              DisableAllPrivileges,
  [in, optional]  PTOKEN_PRIVILEGES NewState,
  [in]            DWORD             BufferLength,
  [out, optional] PTOKEN_PRIVILEGES PreviousState,
  [out, optional] PDWORD            ReturnLength
);
Signature of LookupPrivilegeValueA function from Winbase.h header
  1. Provide the token handle with TOKEN_ADJUST_PRIVILEGES access right to the handle
  2. We are supposed to enable the privilege, set this parameter to FALSE
  3. Create an object of TOKEN_PRIVILEGE struct. Set the count of privileges to 1, because we have only one privilege to update. In the second property's first index, assigned the LUID of the SeDebugPrivilege value and attribute set to SE_PRIVILEGE_ENABLED
  4. The buffer length is required only if PreviousState is passed. Since we are going to query for the privileges again, so this parameter can be NULL.
  5. This parameter is not required, so we can set it to nullptr
  6. Since the PreviousState is nullptr, this parameter can also be nullptr

Note:– I also got confused between NULL and nullptr. They aren't the same if you are coming from the old school Borland C++ or C background. It's basically introduced to avoid the ambiguity in function overloading with foo(int) and foo(int*). So, if it is a pointer, use nullptr and it will implicitly be converted to any pointer type.

So at first, the method returned true without enabling the privileges. After looking at the documentation of the function again, I found that AdjustTokenPrivileges() function cannot add new privileges, but only tries to enable the privileges available in the token. In addition to this, the NewState parameter can specify privileges that the token does not have, without causing the function to fail. And when the application was executed as a standard user, "SeDebugPrivilege" is not provided to it in the privileges set.

It's better to recheck whether the privileges are enabled or not. If the privilege exists and is enabled then we can move forward otherwise try to start the program with the Administrator user. So to check this, you can use the PrivilegeCheck function from the securitybaseapi.h header file. If all the privileges passed in RequiredPrivileges buffer are enabled, this function will return a nonzero response, otherwise FALSE

BOOL PrivilegeCheck(
  [in]      HANDLE         ClientToken,
  [in, out] PPRIVILEGE_SET RequiredPrivileges,
  [out]     LPBOOL         pfResult
);
Signature of the PrivilegeCheck function from the securitybaseapi.h header file.
  1. Pass in the HANDLE to the token, we have opened it earlier in the previous part of this topic. Make sure that the token must be open for TOKEN_QUERY access
  2. First of all, create an instance of the PRIVILEGE_SET struct. Since we have to look for all the privileges enabled in the PRIVILEGE_SET, set the Control member to PRIVILEGE_SET_ALL_NECESSARY. Now set the PrivilegeCount to 1, because we have only one privilege for lookup and now in the Privilege array, set the LUID to the value of SeDebugPrivilege we have found earlier.
    PRIVILEGE_SET tokPrivSet;
    tokPrivSet.Control = PRIVILEGE_SET_ALL_NECESSARY;
    tokPrivSet.PrivilegeCount = 1;
    tokPrivSet.Privilege[0].Luid = pDebugPriv;
    
    Now pass the reference of tokPrivSet to the current parameter of the function. On success, this will contain the Attributes field set for the privilege. The attributes values can be found here
  3. Create a BOOL variable and pass its reference here. This value will be TRUE only if all the privileges in the privilege set are enabled. So we can ignore the attributes from the Privilege array and use this value to determine whether "SeDebugPrivilege" is enabled or not

Automatically Elevating Privileges using ShellExecuteExA

Suppose you are running the program in the victim's system. How will you automatically escalate to the Administrator if the unprivileged user is running the system? Well, the answer is asking for the user to "Run as an Administrator".

There are two ways to do that: The first one is using manifest file and set requireAdministator in the requestedPrivileges section of the file as shown here and the other is using the ShellExecuteExA function from theshellapi.h header file. If the function executes successfully, it will return TRUE

BOOL ShellExecuteExA(
  [in, out] SHELLEXECUTEINFOA *pExecInfo
);
Signature of the ShellExecuteExA function from the shellapi.h header file

Well in this what we gonna do is "If the program is unable to enable SeDebugPrivilege, it will automatically ask for the elevation via UAC and start an elevated process add SeDebugPrivilege and continue further dumping all the process details". Isn't this sound interesting 😄. There is only one parameter, so lets breakdown it's parameters.

  1. After you have instantiated the object of the SHELLEXECUTEINFOA struct, compute the size of the struct and assign it to cbSize
  2. Use the default masks for the application as this doesn't concern us for now. You can set fMask to SEE_MASK_DEFAULT
  3. We are supposed to create an independent process with no message boxes, so set hwnd property to nullptr to prevent inheriting from the parent window.
  4. This is now the main thing! To request for administrator elevation, we should set lpVerb to runas
  5. To provide which file to run, we have to first find the path of the currently running process and assign it to lpFile. Well, it can be done by calling the GetModuleFileNameA() function from the libloaderapi.h header file, implemented at the starting of the SpawnElevatedProcess function in the pch.h file
  6. We don't have any additional parameters, so you can set lpParameters to nullptr
  7. After the elevation, it will open a command prompt and print all the details on the standard output, it is required to show the normal window. We need to pass SW_NORMAL (for more values) value to nShow.  

Pass the function reference of the SHELLEXECUTEINFOA object to ShellExecuteExA function and close the current running program using exit(1) function.

You can see the working of the code in the following GIF! It looks pretty awesome 😊

Resources