Impersonating Windows, Access Tokens, and more

Let’s explore Windows Impersonation, a technique in Asp.Net development that allows a thread to execute under the identity of a different user than the one running the process.

Every process runs under a specific user’s security context. This means the user’s Access Token, known as a Primary Token, is attached to the process and used for security checks whenever the process attempts to access secure resources. Threads within a process may or may not have their own Access Token. When a thread has an associated Access Token, called an Impersonation Token, it utilizes this token for validating access to secure objects. If no impersonation is in place, the process’s Primary Token is used. For a deeper dive into this, refer to here.

An Access Token holds security details for a logon session. It’s created upon user login, and each process running on behalf of the user receives a copy. This token identifies the user, their groups, and privileges. The system leverages this token to manage access to secure objects and govern the user’s ability to perform system-related tasks on the local machine. There are two types of access tokens: primary and impersonation.

Threads are created without an Impersonation Token, inheriting the security context of their parent process. This holds true whether or not the parent thread itself is impersonating. Similarly, when a new process is spawned, the “impersonation status” of the calling thread has no bearing on the new process; it receives its own Primary Token mirroring the calling process, unless explicitly run as a different user via methods like CreateProcessAsUser, CreateProcessWithLogonW, or CreateProcessWithTokenW. This process is well-detailed in here.

Threads, upon creation via CreateThread (and its .Net counterparts), do not receive an Impersonation Token by default. They can, however, impersonate at a later stage during execution and revert back if needed. This involves obtaining a Primary Token and subsequently using it to generate an Impersonation Token.

Since the .Net Base Library lacks direct managed equivalents for this, PInvoke comes into play:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class NativeSecurityHelper
{
    public const int LOGON32_LOGON_INTERACTIVE = 2;
    public const int LOGON32_LOGON_NETWORK = 3;
    public const int LOGON32_LOGON_BATCH = 4;
    public const int LOGON32_LOGON_SERVICE = 5;
    public const int LOGON32_LOGON_UNLOCK = 7;
    public const int LOGON32_LOGON_NETWORK_CLEARTEXT = 8;
    public const int LOGON32_LOGON_NEW_CREDENTIALS = 9;
    public const int LOGON32_PROVIDER_DEFAULT = 0;

[DllImport("advapi32.dll", SetLastError = true)]
    public static extern bool LogonUser(
        string lpszUsername,
        string lpszDomain,
        string lpszPassword,
        int dwLogonType,
        int dwLogonProvider,
        out IntPtr phToken
        );

//I'll use it mainly to create an Impersonation Token from a Primary Token
    [DllImport("advapi32.dll", SetLastError = true)]
    public static extern bool DuplicateToken(IntPtr ExistingTokenHandle,
        int SECURITY_IMPERSONATION_LEVEL,
        out IntPtr DuplicateTokenHandle);
}

Here’s how it’s used:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static WindowsImpersonationContext ImpersonateCurrentThread(string domain, string userName, string password)
{
    IntPtr primaryToken;
    IntPtr impersonationToken;
    //if it's a local logon, LogonUser expects "." for the domain
    domain = domain ?? ".";

NativeSecurityHelper.LogonUser(userName, domain, password,
                        NativeSecurityHelper.LOGON32_LOGON_INTERACTIVE, NativeSecurityHelper.LOGON32_PROVIDER_DEFAULT, out primaryToken);

// LogonUser returns a Primary Token; duplicate it to get an Impersonation Token
    NativeSecurityHelper.DuplicateToken(primaryToken, 2, out impersonationToken);

return ImpersonateCurrentThread(impersonationToken);
}

public static WindowsImpersonationContext ImpersonateCurrentThread(IntPtr impersonationToken)
{
    WindowsIdentity windowsIdentity = new WindowsIdentity(impersonationToken);
    WindowsImpersonationContext impersonationContext = windowsIdentity.Impersonate();
    return impersonationContext;
}

Creating a process under a different account with known credentials is directly supported by the Base Library:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var process = new Process();
// Configure the process using the StartInfo properties.
process.StartInfo.FileName = processPath;
process.StartInfo.WindowStyle = ProcessWindowStyle.Normal;

// Specific to RunAs functionality
process.StartInfo.UseShellExecute = false;

process.StartInfo.UserName = "testUser";
string password = "axSt23uv";

SecureString secPass = new SecureString();
foreach (char c in password)
{
 secPass.AppendChar(c);
}
process.StartInfo.Password = secPass;
process.Start();

The necessity to construct a SecureString by individually appending characters, rather than directly using a string constructor, might seem unusual.

Note the two impersonation methods: one accepting username and password, the other an Access Token. This mirrors the Win32 API, where some functions work with user credentials, and others with tokens. In .Net, obtaining the identity of the running thread (impersonated or not) is straightforward:

1
System.Security.Principal.WindowsIdentity.GetCurrent()

This WindowsIdentity object provides access to its associated token through Token property. However, it only returns a handle (IntPtr) to the token. To manipulate token fields, you would need to utilize native GetTokenInformation and SetTokenInformation functions.

Licensed under CC BY-NC-SA 4.0
Last updated on Jul 27, 2023 05:45 +0100