views:

3723

answers:

7

I'd like to periodically run an arbitrary .NET exe under a specified user account from a Windows Service.

So far I've got my windows service running with logic to decide what the target process is, and when to run it. The target process is started in the following manner:

  1. The Windows Service is started using "administrator" credentials.
  2. When the time comes, an intermediate .NET process is executed with arguments detailing which process should be started (filename, username, domain, password).
  3. This process creates a new System.Diagnostics.Process, associates a ProcessStartInfo object filled with the arguments passed to it, and then calls Start() on the process object.

The first time this happens, the target process executes fine and then closes normally. Every subsequent time however, as soon as the target process is started it throws the error "Application failed to initalize properly (0xc0000142)". Restarting the Windows Service will allow the process to run successfully once again (for the first execution).

Naturally, the goal is to have target process execute successfully every time.

Regarding step 2 above: To run a process as a different user .NET calls the win32 function CreateProcessWithLogonW. This function requires a window handle to log the specified user in. Since the Windows Service isn't running in Interactive Mode it has no window handle. This intermediate process solves the issue, as it has a window handle which can be passed to the target process.

Please, no suggestions of using psexec or the windows task planner. I've accepted my lot in life, and that includes solving the problem in the manner stated above.

+1  A: 

I won't suggest neither psexec nor the task planner. But, have you looked at Sudowin?

It does almost exactly what you wish, with the exception it asks for a password before executing the process.

Also, being open source and all, you can see how it executes processes from the associated service time and again.

Vinko Vrsalovic
I'm browsing the svn repository of sudowin now, after your suggestion. It seems they ignore Systme.Diagnostics.Process completely, and interop CreateProcessAsUser instead of CreateProcessWithLogonW. Maybe that's the trick...
Matt Jacobsen
Worth a try, at least.
Vinko Vrsalovic
A: 

Just a guess - are you using LoadUserProfile=true with the start info? CreateProcessWithLogonW does not load user registry hive by default, unless you tell it to.

liggett78
I didn't think I needed the registry or indeed the user profile, but I've just tried your suggestion to see if it had any positive effect. Unfortuanately not.. :(
Matt Jacobsen
This is of course only relevant if started apps need to have access to the user hive.
liggett78
As stated above in the original post: The target process is executed successfully ONCE. if it can execute once then that means it had access to all resources required.
Matt Jacobsen
A: 

You do not need a window handle to use CreateProcessWithLogonW, I'm not sure where your information came from.

The application failed to initialize error has many causes, but it is almost always related to security or exhausted user resources. It is extremely difficult to diagnose this without a lot more information about what you are running and the context that you're running in. But things to look into are: does the user provided have the correct permissions to access the executable file's directory, does the user have permission to access the window station and desktop in which it is being launched, does it have correct permissions on any dll files it needs to load at initialization, etc.

Stephen Martin
It isn't a security issue. If it was, I wouldn't be able to successfully start the target process at all.
Matt Jacobsen
Starting the target process directly from the service and providing user credentials always results in an "invalid handle" exception. Starting an intermediate process without attempting to change credentials, and then starting the target process from this intermediate process, alleveates the issue.
Matt Jacobsen
What account does your service use to run? "you cannot call CreateProcessWithLogonW from a process that is running under the LocalSystem account" (See CreateProcessWithLogonW function on MSDN)
liggett78
If there is a security issue with the Window Station or Desktop the process will start and then abort when it tries to do its user32 initialization. I'll have to double check when other security errors occur.
Stephen Martin
@liggett78 As mentioned in the original post: I'm running the service as "administrator" at the moment - not LocalSystem
Matt Jacobsen
@Stephen Martin Can you think of any general security practices where a process is granted execution rights only for the first execution? I don't think it's a security issue.
Matt Jacobsen
If you are launching the same process under the same credentials then it is unlikely to be security related. It seems, from you posts so far, most likely to be an initialization error while loading a dependency. Since we know nothing about the process you are launching it is very hard to tell.
Stephen Martin
This would probably be caused by the first run not closing down properly and not releasing resources correctly. Again, we would really need more info on the process and how it runs.
Stephen Martin
I observe the same behaviour with both a simple batch file, and a .net console application as a target process. That leads me to believe that the target process isn't at fault.
Matt Jacobsen
just to make my previous comment clearer: niether test process grabs any resources from the system. the batch file is just "ECHO |TIME /T > c:\temp\test.txt"
Matt Jacobsen
A: 

I've just read this comment over at msdn (http://msdn.microsoft.com/en-us/library/ms682431(VS.85).aspx):

Don't call user applications with this function! ChristianWimmer |
Edit | Show History Please Wait If you going to call user mode applications that offer document editing and similar stuff (like Word), all unsaved data will be lost. This is because the usual shutdown sequence does not apply to processes launched with CreateProcessWithLogonW. In this way the launched applications do not get WM_QUERYENDSESSION, WM_ENDSESSION and the most important WM_QUIT message. So they don't ask for saving the data or clean up their stuff. They will just quit without warning. This function is not user friendly and should be used with caution.

It is just "bad user experience". Nobody expects it.

This could explain what I've observed: Works the first time. Fails every subsequent time. That reinforces my belief that something isn't being cleaned up properly internally

Matt Jacobsen
A: 

The reason that the call fails after the first time is very probably because it uses a "default" security descriptor (whatever that is).

from msdn:

lpProcessAttributes [in, optional]

A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new process object and determines whether child processes can inherit the returned handle to the process. If lpProcessAttributes is NULL or lpSecurityDescriptor is NULL, the process gets a default security descriptor and the handle cannot be inherited. The default security descriptor is that of the user referenced in the hToken parameter. This security descriptor may not allow access for the caller, in which case the process may not be opened again after it is run. The process handle is valid and will continue to have full access rights.

I guess CreateProcessWithLogonW is creating this default security descriptor (in any case, I'm not specifying one).

Time to start Interopping...

Matt Jacobsen
A: 

You say that "The Windows Service is started using "administrator" credentials"

Do you mean the actual 'Administrator' account, or a user in the 'Administrators' group? Starting the service as Administrator solved this for me.

Fiona
You probably have a problem there in that your Administrator user must always be logged in, yes?
Matt Jacobsen
+1  A: 

I seem to have a working implementation (Works On My Machine(TM)) for the following scenarios:

Batch File, .NET Console Assembly, .NET Windows Forms application.

Here's how:

I have a windows service running as the Administrator user. I add the following policies to the Administrator user:

  • Log on as a service
  • Act as part of the operating system
  • Adjust memory quotas for a process
  • Replace a process level token

These policies can be added by opening Control Panel/ Administrative Tools / Local Security Policy / User Rights Assignment. Once they are set, the policies don't take effect until next login. You can use another user instead of the Administrator, which might make things a bit safer :)

Now, my windows service has the required permissions to start jobs as other users. When a job needs to be started the service executes a seperate assembly ("Starter" .NET console assembly) which initiates the process for me.

The following code, located in the windows service, executes my "Starter" console assembly:

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

The console assembly then starts the target process via interop calls:

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

The basic procedure is:

  1. Log the user on
  2. convert the given token into a primary token
  3. Using this token, execute the process
  4. Close the handle when finished.

This is development code fresh from my machine and no way near ready for use in production environments. The code here is still buggy - For starters: I'm not sure whether the handles are closed at the right point, and there's a few interop functions defined above that aren't required. The separate starter process also really annoys me. Ideally I'd like all this Job stuff wrapped up in an assembly for use from our API as well as this service. If someone has any suggestions here, they'd be appreciated.

Matt Jacobsen
Worked for me, too. Thanks!
0xA3