Usar Process.Start () para iniciar un proceso como un usuario diferente desde dentro de un Servicio de Windows

Me gustaría ejecutar periódicamente un .NET arbitrario bajo una cuenta de usuario especificada desde un Servicio de Windows.

Hasta ahora tengo mi servicio de Windows ejecutándose con lógica para decidir cuál es el proceso objective y cuándo ejecutarlo. El proceso objective se inicia de la siguiente manera:

  1. El servicio de Windows se inicia utilizando credenciales de “administrador”.
  2. Cuando llega el momento, se ejecuta un proceso .NET intermedio con argumentos que detallan qué proceso se debe iniciar (nombre de archivo, nombre de usuario, dominio, contraseña).
  3. Este proceso crea un nuevo System.Diagnostics.Process, asocia un objeto ProcessStartInfo lleno con los argumentos que se le pasan y luego llama a Start () en el objeto de proceso.

La primera vez que esto sucede, el proceso objective se ejecuta correctamente y luego se cierra normalmente . Sin embargo, cada vez posterior, tan pronto como se inicia el proceso de destino, arroja el error “La aplicación no se inicializó correctamente (0xc0000142)”. Reiniciar el servicio de Windows permitirá que el proceso vuelva a ejecutarse correctamente (para la primera ejecución).

Naturalmente, el objective es lograr que el proceso objective se ejecute con éxito todo el tiempo.

Con respecto al paso 2 anterior: Para ejecutar un proceso como un usuario diferente, .NET llama a la función win32 CreateProcessWithLogonW. Esta función requiere un identificador de ventana para iniciar sesión en el usuario especificado. Dado que el servicio de Windows no se está ejecutando en modo interactivo, no tiene ningún identificador de ventana. Este proceso intermedio resuelve el problema, ya que tiene un identificador de ventana que se puede pasar al proceso de destino.

Por favor, no hay sugerencias de usar psexec o el planificador de tareas de Windows. He aceptado mi suerte en la vida, y eso incluye resolver el problema de la manera indicada anteriormente.

Parece que tengo una implementación en funcionamiento (Works On My Machine (TM)) para los siguientes escenarios:

Archivo por lotes, ensamblaje de consola .NET, aplicación .NET Windows Forms.

Así es cómo:

Tengo un servicio de Windows ejecutándose como el usuario Administrador. Agregué las siguientes políticas al usuario administrador:

  • Inicie sesión como un servicio
  • Actuar como parte del sistema operativo
  • Ajustar las cuotas de memoria para un proceso
  • Reemplazar un token de nivel de proceso

Estas políticas se pueden agregar abriendo el Panel de control / Herramientas administrativas / Política de seguridad local / Asignación de derechos de usuario. Una vez que se establecen, las políticas no entran en vigencia hasta el próximo inicio de sesión. Puede usar otro usuario en lugar del Administrador, lo que puede hacer que las cosas sean un poco más seguras 🙂

Ahora, mi servicio de Windows tiene los permisos necesarios para iniciar trabajos como otros usuarios. Cuando se necesita iniciar un trabajo, el servicio ejecuta un ensamblaje separado (ensamblado de consola .NET “Starter”) que inicia el proceso por mí.

El siguiente código, ubicado en el servicio de Windows, ejecuta mi ensamblado de consola “Starter”:

Process proc = null; System.Diagnostics.ProcessStartInfo info; string domain = string.IsNullOrEmpty(row.Domain) ? "." : row.Domain; info = new ProcessStartInfo("Starter.exe"); info.Arguments = cmd + " " + domain + " " + username + " " + password + " " + args; info.WorkingDirectory = Path.GetDirectoryName(cmd); info.UseShellExecute = false; info.RedirectStandardError = true; info.RedirectStandardOutput = true; proc = System.Diagnostics.Process.Start(info); 

El ensamblaje de la consola luego inicia el proceso objective a través de llamadas interoperativas:

 class Program { #region Interop [StructLayout(LayoutKind.Sequential)] public struct LUID { public UInt32 LowPart; public Int32 HighPart; } [StructLayout(LayoutKind.Sequential)] public struct LUID_AND_ATTRIBUTES { public LUID Luid; public UInt32 Attributes; } public struct TOKEN_PRIVILEGES { public UInt32 PrivilegeCount; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] public LUID_AND_ATTRIBUTES[] Privileges; } enum TOKEN_INFORMATION_CLASS { TokenUser = 1, TokenGroups, TokenPrivileges, TokenOwner, TokenPrimaryGroup, TokenDefaultDacl, TokenSource, TokenType, TokenImpersonationLevel, TokenStatistics, TokenRestrictedSids, TokenSessionId, TokenGroupsAndPrivileges, TokenSessionReference, TokenSandBoxInert, TokenAuditPolicy, TokenOrigin, TokenElevationType, TokenLinkedToken, TokenElevation, TokenHasRestrictions, TokenAccessInformation, TokenVirtualizationAllowed, TokenVirtualizationEnabled, TokenIntegrityLevel, TokenUIAccess, TokenMandatoryPolicy, TokenLogonSid, MaxTokenInfoClass } [Flags] enum CreationFlags : uint { CREATE_BREAKAWAY_FROM_JOB = 0x01000000, CREATE_DEFAULT_ERROR_MODE = 0x04000000, CREATE_NEW_CONSOLE = 0x00000010, CREATE_NEW_PROCESS_GROUP = 0x00000200, CREATE_NO_WINDOW = 0x08000000, CREATE_PROTECTED_PROCESS = 0x00040000, CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000, CREATE_SEPARATE_WOW_VDM = 0x00001000, CREATE_SUSPENDED = 0x00000004, CREATE_UNICODE_ENVIRONMENT = 0x00000400, DEBUG_ONLY_THIS_PROCESS = 0x00000002, DEBUG_PROCESS = 0x00000001, DETACHED_PROCESS = 0x00000008, EXTENDED_STARTUPINFO_PRESENT = 0x00080000 } public enum TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation } public enum SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation } [Flags] enum LogonFlags { LOGON_NETCREDENTIALS_ONLY = 2, LOGON_WITH_PROFILE = 1 } enum LOGON_TYPE { LOGON32_LOGON_INTERACTIVE = 2, LOGON32_LOGON_NETWORK, LOGON32_LOGON_BATCH, LOGON32_LOGON_SERVICE, LOGON32_LOGON_UNLOCK = 7, LOGON32_LOGON_NETWORK_CLEARTEXT, LOGON32_LOGON_NEW_CREDENTIALS } enum LOGON_PROVIDER { LOGON32_PROVIDER_DEFAULT, LOGON32_PROVIDER_WINNT35, LOGON32_PROVIDER_WINNT40, LOGON32_PROVIDER_WINNT50 } #region _SECURITY_ATTRIBUTES //typedef struct _SECURITY_ATTRIBUTES { // DWORD nLength; // LPVOID lpSecurityDescriptor; // BOOL bInheritHandle; //} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES; #endregion struct SECURITY_ATTRIBUTES { public uint Length; public IntPtr SecurityDescriptor; public bool InheritHandle; } [Flags] enum SECURITY_INFORMATION : uint { OWNER_SECURITY_INFORMATION = 0x00000001, GROUP_SECURITY_INFORMATION = 0x00000002, DACL_SECURITY_INFORMATION = 0x00000004, SACL_SECURITY_INFORMATION = 0x00000008, UNPROTECTED_SACL_SECURITY_INFORMATION = 0x10000000, UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000, PROTECTED_SACL_SECURITY_INFORMATION = 0x40000000, PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000 } #region _SECURITY_DESCRIPTOR //typedef struct _SECURITY_DESCRIPTOR { // UCHAR Revision; // UCHAR Sbz1; // SECURITY_DESCRIPTOR_CONTROL Control; // PSID Owner; // PSID Group; // PACL Sacl; // PACL Dacl; //} SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR; #endregion [StructLayoutAttribute(LayoutKind.Sequential)] struct SECURITY_DESCRIPTOR { public byte revision; public byte size; public short control; // public SECURITY_DESCRIPTOR_CONTROL control; public IntPtr owner; public IntPtr group; public IntPtr sacl; public IntPtr dacl; } #region _STARTUPINFO //typedef struct _STARTUPINFO { // DWORD cb; // LPTSTR lpReserved; // LPTSTR lpDesktop; // LPTSTR lpTitle; // DWORD dwX; // DWORD dwY; // DWORD dwXSize; // DWORD dwYSize; // DWORD dwXCountChars; // DWORD dwYCountChars; // DWORD dwFillAttribute; // DWORD dwFlags; // WORD wShowWindow; // WORD cbReserved2; // LPBYTE lpReserved2; // HANDLE hStdInput; // HANDLE hStdOutput; // HANDLE hStdError; //} STARTUPINFO, *LPSTARTUPINFO; #endregion struct STARTUPINFO { public uint cb; [MarshalAs(UnmanagedType.LPTStr)] public string Reserved; [MarshalAs(UnmanagedType.LPTStr)] public string Desktop; [MarshalAs(UnmanagedType.LPTStr)] public string Title; public uint X; public uint Y; public uint XSize; public uint YSize; public uint XCountChars; public uint YCountChars; public uint FillAttribute; public uint Flags; public ushort ShowWindow; public ushort Reserverd2; public byte bReserverd2; public IntPtr StdInput; public IntPtr StdOutput; public IntPtr StdError; } #region _PROCESS_INFORMATION //typedef struct _PROCESS_INFORMATION { // HANDLE hProcess; // HANDLE hThread; // DWORD dwProcessId; // DWORD dwThreadId; } // PROCESS_INFORMATION, *LPPROCESS_INFORMATION; #endregion [StructLayout(LayoutKind.Sequential)] struct PROCESS_INFORMATION { public IntPtr Process; public IntPtr Thread; public uint ProcessId; public uint ThreadId; } [DllImport("advapi32.dll", SetLastError = true)] static extern bool InitializeSecurityDescriptor(IntPtr pSecurityDescriptor, uint dwRevision); const uint SECURITY_DESCRIPTOR_REVISION = 1; [DllImport("advapi32.dll", SetLastError = true)] static extern bool SetSecurityDescriptorDacl(ref SECURITY_DESCRIPTOR sd, bool daclPresent, IntPtr dacl, bool daclDefaulted); [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] extern static bool DuplicateTokenEx( IntPtr hExistingToken, uint dwDesiredAccess, ref SECURITY_ATTRIBUTES lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, TOKEN_TYPE TokenType, out IntPtr phNewToken); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool LogonUser( string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken ); #region GetTokenInformation //BOOL WINAPI GetTokenInformation( // __in HANDLE TokenHandle, // __in TOKEN_INFORMATION_CLASS TokenInformationClass, // __out_opt LPVOID TokenInformation, // __in DWORD TokenInformationLength, // __out PDWORD ReturnLength //); #endregion [DllImport("advapi32.dll", SetLastError = true)] static extern bool GetTokenInformation( IntPtr TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, IntPtr TokenInformation, int TokenInformationLength, out int ReturnLength ); #region CreateProcessAsUser // BOOL WINAPI CreateProcessAsUser( // __in_opt HANDLE hToken, // __in_opt LPCTSTR lpApplicationName, // __inout_opt LPTSTR lpCommandLine, // __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes, // __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, // __in BOOL bInheritHandles, // __in DWORD dwCreationFlags, // __in_opt LPVOID lpEnvironment, // __in_opt LPCTSTR lpCurrentDirectory, // __in LPSTARTUPINFO lpStartupInfo, // __out LPPROCESS_INFORMATION lpProcessInformation); #endregion [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool CreateProcessAsUser( IntPtr Token, [MarshalAs(UnmanagedType.LPTStr)] string ApplicationName, [MarshalAs(UnmanagedType.LPTStr)] string CommandLine, ref SECURITY_ATTRIBUTES ProcessAttributes, ref SECURITY_ATTRIBUTES ThreadAttributes, bool InheritHandles, uint CreationFlags, IntPtr Environment, [MarshalAs(UnmanagedType.LPTStr)] string CurrentDirectory, ref STARTUPINFO StartupInfo, out PROCESS_INFORMATION ProcessInformation); #region CloseHandle //BOOL WINAPI CloseHandle( // __in HANDLE hObject // ); #endregion [DllImport("Kernel32.dll")] extern static int CloseHandle(IntPtr handle); [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen); [DllImport("advapi32.dll", SetLastError = true)] internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid); [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct TokPriv1Luid { public int Count; public long Luid; public int Attr; } //static internal const int TOKEN_QUERY = 0x00000008; internal const int SE_PRIVILEGE_ENABLED = 0x00000002; //static internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; internal const int TOKEN_QUERY = 0x00000008; internal const int TOKEN_DUPLICATE = 0x0002; internal const int TOKEN_ASSIGN_PRIMARY = 0x0001; #endregion [STAThread] static void Main(string[] args) { string username, domain, password, applicationName; username = args[2]; domain = args[1]; password = args[3]; applicationName = @args[0]; IntPtr token = IntPtr.Zero; IntPtr primaryToken = IntPtr.Zero; try { bool result = false; result = LogonUser(username, domain, password, (int)LOGON_TYPE.LOGON32_LOGON_NETWORK, (int)LOGON_PROVIDER.LOGON32_PROVIDER_DEFAULT, out token); if (!result) { int winError = Marshal.GetLastWin32Error(); } string commandLine = null; #region security attributes SECURITY_ATTRIBUTES processAttributes = new SECURITY_ATTRIBUTES(); SECURITY_DESCRIPTOR sd = new SECURITY_DESCRIPTOR(); IntPtr ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(sd)); Marshal.StructureToPtr(sd, ptr, false); InitializeSecurityDescriptor(ptr, SECURITY_DESCRIPTOR_REVISION); sd = (SECURITY_DESCRIPTOR)Marshal.PtrToStructure(ptr, typeof(SECURITY_DESCRIPTOR)); result = SetSecurityDescriptorDacl(ref sd, true, IntPtr.Zero, false); if (!result) { int winError = Marshal.GetLastWin32Error(); } primaryToken = new IntPtr(); result = DuplicateTokenEx(token, 0, ref processAttributes, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out primaryToken); if (!result) { int winError = Marshal.GetLastWin32Error(); } processAttributes.SecurityDescriptor = ptr; processAttributes.Length = (uint)Marshal.SizeOf(sd); processAttributes.InheritHandle = true; #endregion SECURITY_ATTRIBUTES threadAttributes = new SECURITY_ATTRIBUTES(); threadAttributes.SecurityDescriptor = IntPtr.Zero; threadAttributes.Length = 0; threadAttributes.InheritHandle = false; bool inheritHandles = true; //CreationFlags creationFlags = CreationFlags.CREATE_DEFAULT_ERROR_MODE; IntPtr environment = IntPtr.Zero; string currentDirectory = currdir; STARTUPINFO startupInfo = new STARTUPINFO(); startupInfo.Desktop = ""; PROCESS_INFORMATION processInformation; result = CreateProcessAsUser(primaryToken, applicationName, commandLine, ref processAttributes, ref threadAttributes, inheritHandles, 16, environment, currentDirectory, ref startupInfo, out processInformation); if (!result) { int winError = Marshal.GetLastWin32Error(); File.AppendAllText(logfile, DateTime.Now.ToLongTimeString() + " " + winError + Environment.NewLine); } } catch { int winError = Marshal.GetLastWin32Error(); File.AppendAllText(logfile, DateTime.Now.ToLongTimeString() + " " + winError + Environment.NewLine); } finally { if (token != IntPtr.Zero) { int x = CloseHandle(token); if (x == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); x = CloseHandle(primaryToken); if (x == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); } } } 

El procedimiento básico es:

  1. Inicie sesión en el usuario
  2. convertir el token dado en un token primario
  3. Usando este token, ejecuta el proceso
  4. Cierre el mango cuando haya terminado.

Este es el código de desarrollo recién llegado de mi máquina y no está cerca de estar listo para su uso en entornos de producción. El código aquí todavía tiene errores. Para empezar: no estoy seguro de si los controladores están cerrados en el punto correcto, y hay algunas funciones de interoperabilidad definidas anteriormente que no son necesarias. El proceso de inicio separado también realmente me molesta. Idealmente, me gustaría que todas estas cosas de Job se envuelvan en un ensamblaje para usar desde nuestra API y este servicio. Si alguien tiene alguna sugerencia aquí, sería apreciada.

No sugeriré psexec ni el planificador de tareas. Pero, ¿has mirado a Sudowin ?

Hace casi exactamente lo que desea, con la excepción de que solicita una contraseña antes de ejecutar el proceso.

Además, al ser de código abierto y todo, puede ver cómo ejecuta los procesos desde el servicio asociado una y otra vez.

Solo una suposición: ¿estás usando LoadUserProfile = true con la información de inicio? CreateProcessWithLogonW no carga subárbol de registro de usuario de forma predeterminada, a menos que se lo indique.

La razón por la que falla la llamada después de la primera vez es muy probablemente porque usa un descriptor de seguridad “predeterminado” (lo que sea que sea).

de msdn :

lpProcessAttributes [en, opcional]

Un puntero a una estructura SECURITY_ATTRIBUTES que especifica un descriptor de seguridad para el nuevo objeto de proceso y determina si los procesos secundarios pueden heredar el identificador devuelto al proceso. Si lpProcessAttributes es NULL o lpSecurityDescriptor es NULL, el proceso obtiene un descriptor de seguridad predeterminado y el identificador no se puede heredar. El descriptor de seguridad predeterminado es el del usuario al que se hace referencia en el parámetro hToken. Este descriptor de seguridad puede no permitir el acceso de la persona que llama, en cuyo caso el proceso puede no abrirse nuevamente después de que se ejecuta. El identificador de proceso es válido y continuará teniendo derechos de acceso completos.

Supongo que CreateProcessWithLogonW está creando este descriptor de seguridad predeterminado (en cualquier caso, no estoy especificando uno).

Es hora de comenzar a intervenir …

No necesita un identificador de ventana para usar CreateProcessWithLogonW, no estoy seguro de dónde proviene su información.

La aplicación no pudo inicializar el error tiene muchas causas, pero casi siempre está relacionada con la seguridad o los recursos de usuario agotados. Es extremadamente difícil diagnosticar esto sin mucha más información sobre lo que está ejecutando y el contexto en el que se está ejecutando. Pero las cosas a considerar son: ¿el usuario proporcionado tiene los permisos correctos para acceder al directorio del archivo ejecutable? el usuario tiene permiso para acceder a la estación de la ventana y al escritorio en el que se está iniciando, ¿tiene los permisos correctos en los archivos dll que necesita cargar en la inicialización, etc.

Acabo de leer este comentario en msdn ( http://msdn.microsoft.com/en-us/library/ms682431(VS.85).aspx ):

¡No llame a las aplicaciones de usuario con esta función! ChristianWimmer |
Editar | Mostrar historial, espere Si va a llamar a las aplicaciones del modo de usuario que ofrecen edición de documentos y cosas similares (como Word), se perderán todos los datos no guardados. Esto se debe a que la secuencia de apagado habitual no se aplica a los procesos iniciados con CreateProcessWithLogonW. De esta forma, las aplicaciones iniciadas no reciben WM_QUERYENDSESSION, WM_ENDSESSION y el mensaje WM_QUIT más importante. Entonces no piden guardar la información o limpiar sus cosas. Simplemente se irán sin previo aviso. Esta función no es fácil de usar y debe usarse con precaución.

Es solo “mala experiencia de usuario”. Nadie lo espera

Esto podría explicar lo que he observado: funciona la primera vez. Falla cada vez posterior. Eso refuerza mi creencia de que algo no se está limpiando adecuadamente internamente

Usted dice que “El servicio de Windows se inicia utilizando las credenciales de” administrador ”

¿Te refieres a la cuenta real de ‘Administrador’ o a un usuario en el grupo ‘Administradores’? Comenzar el servicio como administrador resolvió esto para mí.

Tuve problemas similares cuando traté de iniciar el PhantomJS-binary con el “runas” -verb dentro de un servicio de Windows. Ahora he resuelto el problema usando el siguiente procedimiento:

  • Hacerse pasar por usuario
  • Comience el proceso (sin UI)
  • Hacerse pasar de nuevo

Puede usar el Impersonator-Class para suplantación. También es importante establecer las siguientes Propiedades en ProcessStartInfo, para que la aplicación no intente acceder a la IU de Windows:

 var processStartInfo = new ProcessStartInfo() { FileName = $@"{assemblyFolder}\PhantomJS\phantomjs.exe", Arguments = $"--webdriver={port}", RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, UseShellExecute = false, CreateNoWindow = true, ErrorDialog = false, WindowStyle = ProcessWindowStyle.Hidden };