Understanding and Defending Against Access Token Theft: Finding Alternatives to winlogon.exe

Justin Bui
Posts By SpecterOps Team Members
12 min readOct 1, 2019

--

Introduction

This blogpost will describe the concept of access token manipulation and how this technique can be utilized against winlogon.exe to impersonate a SYSTEM access token from an administrator context. This technique is mapped to MITRE ATT&CK under Access Token Manipulation.

Impersonating a SYSTEM access token is useful in cases that certain privileges have been stripped away from the local administrator account through Group Policy. For example, SeDebugPrivilege can be removed from the local administrator group to make it harder for an attacker to dump credentials or interact with memory of other processes. However, privileges cannot be revoked from the SYSTEM account as they are necessary for the operating system to run. This makes SYSTEM access tokens extremely valuable for attackers in hardened environments.

After introducing the concept of access token manipulation, I show how to detect malicious access token manipulation using system access control lists (SACLs) to audit process objects. One shortcoming of this detection is that defenders must proactively understand which processes are targets for access token manipulation.

Lastly, I explore alternate SYSTEM processes that can have their access token impersonated like winlogon.exe. I explain my methodology behind finding these processes and what I learned along the way.

Stealing Access Tokens

Note: Skip this section if you’re familiar with access token manipulation and want to dive into the methodology behind searching for alternate SYSTEM processes that can have their access token impersonated.

The following Windows API calls can be used to steal and abuse access tokens: OpenProcess(), OpenProcessToken(), ImpersonateLoggedOnUser() , DuplicateTokenEx(), CreateProcessWithTokenW().

Stealing Access Tokens with Windows API

OpenProcess() takes a process identifier (PID) and returns a process handle. The process handle must be opened with the PROCESS_QUERY_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION or the PROCESS_ALL_ACCESS access right to be useable with OpenProcessToken().

MSDN Documentation for OpenProcess

OpenProcessToken() takes a process handle and an access rights flag as input. It will open a handle to the access token associated with a process. The token handle must be opened with the TOKEN_QUERY and TOKEN_DUPLICATE access rights to be useable with ImpersonateLoggedOnUser(). Alternatively, the token handle can be opened with only the TOKEN_DUPLICATE access right to be useable with DuplicateTokenEx().

MSDN Documentation for OpenProcessToken

With the token handle from OpenProcessToken(), we are able to use ImpersonatedLoggedOnUser() to allow our current thread to impersonate another logged-on user. The thread will continue to impersonate the logged-on user until RevertToSelf() is called or the thread exits.

MSDN Documentation for ImpersonateLoggedOnUser

If we want to spawn a process as another user, we must use DuplicateTokenEx() on the resulting token handle from OpenProcessToken() to create a new access token. DuplicateTokenEx() must be called with the TOKEN_ADJUST_DEFAULT, TOKEN_ADJUST_SESSIONID, TOKEN_QUERY, TOKEN_DUPLICATE, and TOKEN_ASSIGN_PRIMARY access rights to be useable with CreateProcessWithTokenW(). The access token created by DuplicateTokenEx() can be passed to CreateProcessWithTokenW() to spawn a process using the duplicated token.

MSDN Documentation for DuplicateTokenEx
MSDN Documentation for CreateProcessWithTokenW

I put together a small bit of code to demonstrate this, heavily adopted from this blogpost on primary token manipulation by @kondencuotas.

The code used in my testing can be found here: https://github.com/justinbui/PrimaryTokenTheft

Elevating to SYSTEM with winlogon.exe

Earlier this year, Nick Landers mentioned an easy way to elevate from local admin to NT AUTHORITY\SYSTEM.

As a local administrator (in a high-integrity context), you can steal the access token for winlogon.exe to impersonate SYSTEM in your current thread or spawn a new process as SYSTEM.

Stealing SYSTEM token from winlogon.exe

Detection

An access control list (ACL) is a list of access control entries (ACE). Each ACE in an ACL identifies a trustee and specifies the access rights allowed, denied, or audited for that trustee. The security descriptor for a securable object can contain two types of ACLs: a DACL and a SACL. (Source)

Our detection will be based on system access control lists (SACLs). We can apply a SACL to process objects to log successful/failed access attempts to the Windows Security Log.

We can do this easily using NtObjectManager from James Forshaw. Much of the work below is heavily adapted from James Forshaw’s blog on bypassing SACL auditing on LSASS here. His blogpost gave me a good understanding of SACLs and how they could be manipulated with NtObjectManager.

auditpol /set /category:"Object Access" /success:enable /failure:enable$p = Get-NtProcess -name winlogon.exe -Access GenericAll,AccessSystemSecuritySet-NtSecurityDescriptor $p “S:(AU;SAFA;0x1400;;;WD)” Sacl

Let’s break this down, the first line will enable system auditing for successful and failed object access.

The second line will obtain a handle to the winlogon.exe process with the GenericAll and AccessSystemSecurity access rights. The AccessSystemSecurity right is necessary to access the SACL.

The third line will apply an audit ACE type (AU) that will generate a security event for successful/failed access (SAFA) from the Everyone(WD) group. The 0x1400 is significant as it is the bitwise OR of 0x400 (PROCESS_QUERY_INFORMATION) and 0x1000 (PROCESS_QUERY_LIMITED_INFORMATION). Either of these access rights can be used to obtain the access token from a process object (as well as PROCESS_ALL_ACCESS).

Great! With this SACL in place we should be able to get alerts when winlogon.exe is accessed with specific access rights.

Case 1: PROCESS_QUERY_INFORMATION

Running the test program, we see EID (Event ID) 4656 is generated showing the process object that was requested, the process that requested access and the access right(s) requested. The access mask of 0x1400 is because a handle that has the PROCESS_QUERY_INFORMATION access right is automatically granted the PROCESS_QUERY_LIMITED_INFORMATION access right as well.

Case 2: PROCESS_QUERY_LIMITED_INFORMATION

I recompiled the test program to only require the PROCESS_QUERY_LIMITED_INFORMATION access right and re-ran it. We see EID 4656 is generated again showing the process that requested a handle to winlogon.exe with an access right of 0x1000 representing the PROCESS_QUERY_LIMITED_INFORMATION access right.

Additionally, we see EID 4663 is generated showing our test program attempt to access the process object after requesting a handle. This allows us to detect access token manipulation with higher fidelity by searching for EID 4656 followed by EID 4663.

Case 3: PROCESS_ALL_ACCESS

Recompiling the test program to use the PROCESS_ALL_ACCESS access right resulted in identical EIDs as Case 2 with the EID 4656 showing the additional access rights requested.

Interesting to note that the access mask for EID 4663 is 0x1000 which represents thePROCESS_QUERY_LIMITED_INFORMATION access right. Additionally, when the test program is run with the PROCESS_QUERY_INFORMATION access right, EID 4656 is generated and EID 4663 is not.

Methodology: Finding winlogon.exe Alternatives

Aside from winlogon.exe, I was curious if there were other SYSTEM processes that could be targets for token theft. If there were, what differentiates them from other SYSTEM processes that you cannot steal tokens from?

Testing a Hypotheses

First, I wanted to see if there were other processes that I could also steal a SYSTEM token from. I brute-forced the SYSTEM processes (excluding svchost.exe), as a local administrator running in a high-integrity context, and found a few other processes which we could steal SYSTEM tokens from. These processes were lsass.exe, OfficeClickToRun.exe, dllhost.exe and unsecapp.exe. I will refer to these processes as “known good” processes.

Stealing SYSTEM token from unsecapp.exe

As I went through the list of SYSTEM processes, I noticed some processes would fail with an access denied (System Error - Code 5) during the OpenProcess() which would cause the remainder of the program to fail.

For some SYSTEM processes, OpenProcess() would succeed, but OpenProcessToken() would fail with an access denied error. I would learn the reason for this later in my research.

Working Backwards (Why Does This Work?)

My goal was to find the difference in security settings between SYSTEM processes that allowed for token manipulation to be successful.

I decided to do a comparison between winlogon.exe and spoolsv.exe. Both of these are SYSTEM processes, however, I am able to steal the SYSTEM access token from only winlogon.exe.

Session ID

I opened both processes in Process Explorer and tried to manually discover differences between them. I remembered from Nick’s tweet that he mentioned winlogon.exe being an exception in Session ID 1.

I did this comparison against the other “known good” processes and found they were all running in Session ID 0. Unfortunately, this wasn’t the difference I was looking for.

Comparing Session ID between two “known good” processes

Advanced Security Settings in Process Explorer

I decided to look further into the Advanced Security Settings for winlogon.exe and spoolsv.exe. I noticed that the Permission Entry for the Administrators group had slightly different Advanced Permissions. For winlogon.exe, the Administrator group had the privilege to Terminate, Read Memory, and Read Permissions. These privileges were not available to the Administrator group within spoolsv.exe.

I attempted to allow all permissions on spoolsv.exe and attempt to steal the access token. Unfortunately, this did not result in a SYSTEM cmd prompt.

I also attempted to start/stop the process again to see if the permissions would be applied on process start, this also failed.

Get-ACL

I decided to use Get-ACL in PowerShell to look at the security descriptor for winlogon.exe and spoolsv.exe.

Get-ACL against winlogon.exe and spoolsv.exe

The Owner, Group and Access seem to be identical between the two processes. Next, I decided to convert the Security Descriptor Definition Language (SDDL) string, using ConvertFrom-SddlString, to look for differences.

SDDL for winlogon.exe and spoolsv.exe

The DiscretionaryAcl for the BUILTIN\Administrators group seem to be identical. I was running out of ideas, but decided to look at Process Explorer one last time.

The Breakthrough! (TokenUser vs TokenOwner)

Looking at the Advanced Security Settings again in Process Explorer, I saw that the Owner field specified the local Administrators group in all of the “known good” processes!

TokenOwner for winlogon.exe and unsecapp.exe

I compared this to some SYSTEM processes which I could not steal the access token from. The Owner seemed to be a differentiating factor.

TokenOwner for spoolsv.exe and svchost.exe

My coworker, @jaredcatkinson, mentioned that the Owner specified within Process Explorer was the TokenOwner which could be retrieved with GetTokenInformation().

Conveniently, I found a PowerShell script on GitHub called Get-Token.ps1 which enumerates all process and thread tokens.

Token object for winlogon.exe from Get-Token.ps1

Taking a look at winlogon.exe, we see there is a mismatch in the UserName and OwnerName field. Looking into the script’s implementation, I saw that these fields correlated to the TOKEN_USER and TOKEN_OWNER structure.

The TOKEN_USER structure identifies the user associated with the access token. The TOKEN_OWNER structure identifies the user who is owner of any process created with the access token. This seems to be the main distinction that allows us to steal access tokens from some SYSTEM processes, but not others!

For some SYSTEM processes, OpenProcess() would succeed, but OpenProcessToken() would fail with an access denied error. I would learn the reason for this later in my research.

I am now able to answer a question I previously asked myself, why does OpenProcessToken() fail with an access denied on some SYSTEM processes? This failed because I was not the TOKEN_OWNER.

The following one-liner will parse the output of Get-Token for objects where the UserName is SYSTEM and the OwnerName is not SYSTEM. It will then grab the ProcessName and ProcessID attribute from each object.

Get-Token | Where-Object {$_.UserName -eq ‘NT AUTHORITY\SYSTEM’ -and $_.OwnerName -ne ‘NT AUTHORITY\SYSTEM’} | Select-Object ProcessName,ProcessID | Format-Table

Great! We should be able to steal the access token for these SYSTEM processes and impersonate a SYSTEM access token. Let’s verify our hypothesis.

I manually went through the list of PIDs and noticed most of them were valid candidates for access token manipulation! There were some processes that failed however.

OpenProcess() returns access denied for wininit.exe and csrss.exe

Protected Process Light

I saw that each of the SYSTEM processes that I was not able to steal a token from failed with an access denied during the OpenProcess() call. I opened up Process Explorer and found a common property that seemed likely to be causing this behavior: PsProtectedSignerWinTcb-Light.

As I went through the list of SYSTEM processes, I noticed some processes would fail with an access denied (System Error — Code 5) during the OpenProcess() which would cause the remainder of the program to fail.

Reading through Alex Ionescu’s blog and this Stack Overflow post, I learned that this Protected property was related to Protected Process Light (PPL).

PPL will only allow OpenProcess() to be called on a process if the access right specified is PROCESS_QUERY_LIMITED_INFORMATION. Our test program requires OpenProcess() to be called with the PROCESS_QUERY_INFORMATION access right for the handle to be compatible with OpenProcessToken(), resulting in System Error — Code 5 (access denied). Or so I thought …

While testing for detections, I learned that the minimum permissions needed for OpenProcessToken() is the PROCESS_QUERY_LIMITED_INFORMATION access right, contrary to Microsoft documentation. I modified the access rights requested during the OpenProcess() API call and was rewarded with a SYSTEM cmd prompt.

Results

The following SYSTEM processes can have their access token stolen when calling OpenProcess() with the PROCESS_QUERY_INFORMATION access right:

  • dllhost.exe
  • lsass.exe
  • OfficeClickToRun.exe
  • svchost.exe (only some PIDs)
  • Sysmon64.exe
  • unsecapp.exe
  • VGAuthService.exe
  • vmacthlp.exe
  • vmtoolsd.exe
  • winlogon.exe

The following SYSTEM processes, with Protected Process Light, can have their access token stolen when calling OpenProcess() with the PROCESS_QUERY_LIMITED_INFORMATION access right:

  • csrss.exe
  • Memory Compression.exe
  • services.exe
  • smss.exe
  • wininit.exe

Some of these processes may be specific to my Windows dev environment, but I encourage you to perform this test in your own environments!

Conclusion

To recap, winlogon.exe has an access token that can be stolen to impersonate a SYSTEM context. I dug into detecting access token manipulation using SACLs and the Windows Security Logs.

I attempted to search for other SYSTEM processes that had similar attributes to winlogon.exe. I highlighted my methodology for digging into this subject and was able to find other SYSTEM processes that are susceptible to access token manipulation. Additionally, I dug into the root cause behind why certain processes were susceptible to access token manipulation and why others were not.

To steal an access token from a SYSTEM process:

  • The BUILTIN\Administrator must be the TokenOwner to perform OpenProcessToken() for a process.
  • Use OpenProcess() with the PROCESS_QUERY_LIMITED_INFORMATION access right against SYSTEM processes protected by Protected Process Light (PPL).

Thanks for taking the time to read this post, I hope you learned a little about the Windows API, SACLs, Windows processes, Windows tokens, and access token manipulation!

Credits

Get-Token.ps1 by vector-sec

--

--