Friday 19 April 2013

Windows Impersonation, Access Tokens and so on

I think anyone that has done some kind of Asp.Net development should have a basic idea of what Windows Impersonation is (running a thread impersonated under a user different from the one running the process), but well, let's delve into this a bit more.

When a process is run by a user (runs under the security context of that user), it means that the Access Token of that user gets assigned to the Process, and will be used for any security checking done when the process tries to access a securable resource. This token is a Primary Token. As for the threads of a process, they can or can not have an associated Access Token. If this token is present, it means that such thread is using impersonation, otherwise it's not impersonating. If impersonating, this Token (Impersonation Token) is used to validate the interaction of the thread with a securable object, if not impersonating, the Process Token (Primary Token) is used for these checks. You can read a good explanation here

Access Token -An access token contains the security information for a logon session. The system creates an access token when a user logs on, and every process executed on behalf of the user has a copy of the token. The token identifies the user, the user's groups, and the user's privileges. The system uses the token to control access to securable objects and to control the ability of the user to perform various system-related operations on the local computer. There are two kinds of access token, primary and impersonation.

When a Thread is created it does not get associated any Access Token (Impersonation Token), so it will run in the Security Context of its Process. This is so irrespective of whether the parent thread is or is not being impersonated. Likewise, when a new Process is created, the "impersonation status" of the calling thread does not have any effect, the new Process will get a Primary Token like the one of the calling Process (unless that we're running it under a different user by means of CreateProcessAsUser, CreateProcessWithLogonW or CreateProcessWithTokenW. This is pretty well explained here

So note that threads are always created without any Impersonation Token (CreateThread and its .Net wrappers don't have any token or user-password parameters), it's later on, while running, when the Thread can impersonate itself (and undo such impersonation later on). The thread would first obtain a Primary Token, that would use to create an Impersonation Token.
.Net Base Library does not provide any managed equivalent, so we'll need to resort to PInvoke:

 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);
}

and we would use it like this:

        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);
                
            //I think LogonUser returns a Primary Token, we have to duplicate it to obtain 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;
        }

On the other hand, the case of creating a Process under a different account (for which we have its user and password) is contemplated by the Base Library straightaway, we can do it like this:

var process = new Process();
// Configure the process using the StartInfo properties.
process.StartInfo.FileName = processPath;
process.StartInfo.WindowStyle = ProcessWindowStyle.Normal;

//this is specific to the 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();

Yes, having to create a SecureString from a normal string by appending chars instead of directly accepting a string in a constructor seems rather odd to me too.

Notice that I've defined 2 impersonation methods, one that expects a username and a password, and another one that expects an Access Token. This is the norm in Win32 API, some functions expecting user and pass, and others expecting a Token. In .Net we can obtain the identity under which a thread is running (impersonated or not) with just this line:

System.Security.Principal.WindowsIdentity.GetCurrent()

and from that WindowsIdentity object we can get its associated token via the Token property. Unfortunately, it simply returns a handler (an IntPtr) to the token, so if you were to want to get or set fields in the token, you would have to use the native GetTokenInformation and SetTokenInformation functions.

1 comment:

  1. I know this is article is around a year old now, but how can you tell from within program code whether Impersonation is active, and which user is being impersonated?

    Unfortunately as my code is in C++ if the answer is framework methods then I won't be able to do it.

    ReplyDelete