OdeToCode IC Logo

CreateProcessAsUser

Friday, October 29, 2004

Here is a solution you can use in rare circumstances and with caution. Comments are more then welcomed.

Anytime someone approaches me with a design requiring the creation of a server side process – I flinch. Spinning up a process on the server is something to avoid. Having said that, I found it useful in a small Intranet applications (a build engine) to put a web service / ASP.NET interface in front of some command line CM tools. The interface is available to a small number of trusted individuals.

I wanted to run these processes with the identity of the client, but this poses a problem. The Process class in System.Diagnostics can start a new process, but the process always inherits the security context of the parent process. Even if the ASP.NET thread invoking the Start method is impersonating a client, the Process still starts with the ASP.NET worker process credentials.

Enter .NET 2.0, which includes the User, Domain, and Password properties on the ProcessStartInfo type. In .NET 2.0 you can start a process under a different set of credentials. The catch is having a password to give. I don't want the burden of managing passwords when there is a domain controller handy.

Having found no solution in the framework, the next step was to look into the Win32 API. There are four basic functions to start a process: CreateProcess, CreateProcessWithLogonW, CreateProcessAsUser, and CreateProcessWithTokenW. Since CreateProcess doesn’t allow an alternate identity, and one of 11 parameters to CreateProcessWithLogonW is a password, those are both out of the running.

The remaining two (CreateProcessWithTokenW and CreateProcessAsUser) both accept a token instead of a username and password, so these look promising. CreateProcessWithTokenW allows greater fine tuning of the logon type and creation flags, which I didn't need, so I focused in on CreateProcessAsUser.

CreateProcessAsUser accepts a user token as the first parameter. We can easily get a token representing the client we are impersonating using the WindowsIdentity class in .NET, but there still exists a problem. The token will be an impersonation token - which isn’t good enough for CreateProcessAsUser. However, the docs mention that you can use DuplicateTokenEx to convert an impersonation token into a primary token. The code at the end of this post demonstrates the incantations.

Note: The calling process needs SeAssignPrimaryTokenPrivilege and SeIncreaseQuotasPrivilege privileges, which the NETWORK SERVICE account has by default on Windows 2003.

Note: I’m not sure how far the ‘primary token’ yielded by DuplicateTokenEx can go. Could I effectively delegate without delegation enabled? I wouldn't think so. Must experiment. Anyone know?

Note: If you are thinking of using this to launch an interactive GUI application on the server – don’t. The process will start in a non-interactive window station and remain invisible but consuming memory.

 

private void CreateProcessAsUser()
{
   IntPtr hToken = WindowsIdentity.GetCurrent().Token;         
   IntPtr hDupedToken = IntPtr.Zero;        
   
   ProcessUtility.PROCESS_INFORMATION pi = new ProcessUtility.PROCESS_INFORMATION();
   
   try
   {
      ProcessUtility.SECURITY_ATTRIBUTES sa = new ProcessUtility.SECURITY_ATTRIBUTE();
      sa.Length = Marshal.SizeOf(sa); 
 
      bool result = ProcessUtility.DuplicateTokenEx(
            hToken, 
            ProcessUtility.GENERIC_ALL_ACCESS,
            ref sa, 
            (int)ProcessUtility.SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
            (int)ProcessUtility.TOKEN_TYPE.TokenPrimary, 
            ref hDupedToken
         );
   
      if(!result)
      {               
         throw new ApplicationException("DuplicateTokenEx failed");
      }
 
 
      ProcessUtility.STARTUPINFO si = new ProcessUtility.STARTUPINFO();
      si.cb = Marshal.SizeOf(si);
      si.lpDesktop = String.Empty;           
 
      result = ProcessUtility.CreateProcessAsUser(
                           hDupedToken, 
                           @"",
                           String.Empty,
                           ref sa, ref sa, 
                           false, 0, IntPtr.Zero, 
                           @"C:\", ref si, ref pi
                     );
 
      if(!result)
      {  
         int error = Marshal.GetLastWin32Error();
         string message = String.Format("CreateProcessAsUser Error: {0}", error);
         throw new ApplicationException(message);
      }
   }
   finally
   {
      if(pi.hProcess != IntPtr.Zero)
         ProcessUtility.CloseHandle(pi.hProcess);
      if(pi.hThread != IntPtr.Zero)
         ProcessUtility.CloseHandle(pi.hThread);
      if(hDupedToken != IntPtr.Zero)
         ProcessUtility.CloseHandle(hDupedToken);      
   }
}

 

ProcessUtility…

 

public class ProcessUtility
{
   [StructLayout(LayoutKind.Sequential)]
      public struct STARTUPINFO
   {
      public Int32 cb;
      public string lpReserved;
      public string lpDesktop;
      public string lpTitle;
      public Int32 dwX;
      public Int32 dwY;
      public Int32 dwXSize;
      public Int32 dwXCountChars;
      public Int32 dwYCountChars;
      public Int32 dwFillAttribute;
      public Int32 dwFlags;
      public Int16 wShowWindow;
      public Int16 cbReserved2;
      public IntPtr lpReserved2;
      public IntPtr hStdInput;
      public IntPtr hStdOutput;
      public IntPtr hStdError;
   }
 
   [StructLayout(LayoutKind.Sequential)]
      public struct PROCESS_INFORMATION
   {
      public IntPtr hProcess;
      public IntPtr hThread;
      public Int32 dwProcessID;
      public Int32 dwThreadID;
   }
 
   [StructLayout(LayoutKind.Sequential)]
      public struct SECURITY_ATTRIBUTES
   {
      public Int32 Length;
      public IntPtr lpSecurityDescriptor;
      public bool bInheritHandle;
   }
 
   public enum SECURITY_IMPERSONATION_LEVEL
   {
      SecurityAnonymous,
      SecurityIdentification,
      SecurityImpersonation,
      SecurityDelegation
   }
 
   public enum TOKEN_TYPE
   {
      TokenPrimary = 1, 
      TokenImpersonation
   } 
 
   public const int GENERIC_ALL_ACCESS = 0x10000000;
 
   [
      DllImport("kernel32.dll", 
         EntryPoint = "CloseHandle", SetLastError = true, 
         CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)
   ]
   public static extern bool CloseHandle(IntPtr handle);
 
   [
      DllImport("advapi32.dll", 
         EntryPoint = "CreateProcessAsUser", SetLastError = true, 
         CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)
   ]
   public static extern bool 
      CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, 
                          ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, 
                          bool bInheritHandle, Int32 dwCreationFlags, IntPtr lpEnvrionment,
                          string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, 
                          ref PROCESS_INFORMATION lpProcessInformation);
 
   [
      DllImport("advapi32.dll", 
         EntryPoint = "DuplicateTokenEx")
   ]
   public static extern bool 
      DuplicateTokenEx(IntPtr hExistingToken, Int32 dwDesiredAccess, 
                       ref SECURITY_ATTRIBUTES lpThreadAttributes,
                       Int32 ImpersonationLevel, Int32 dwTokenType, 
                       ref IntPtr phNewToken);
}