Starting a process with credentials from a Windows Service
Asked Answered
A

8

15

I have a Windows service that runs as mydomain\userA. I want to be able to run arbitrary .exes from the service. Normally, I use Process.Start() and it works fine, but in some cases I want to run the executable as a different user (mydomain\userB).

If I change the ProcessStartInfo I use to start the process to include credentials, I start getting errors - either an error dialog box that says "The application failed to initialize properly (0xc0000142). Click on OK to terminate the application.", or an "Access is denied" Win32Exception. If I run the process-starting code from the command-line instead of running it in the service, the process starts using the correct credentials (I've verified this by setting the ProcessStartInfo to run whoami.exe and capturing the command-line output).

I've also tried impersonation using WindowsIdentity.Impersonate(), but this hasn't worked - as I understand it, impersonation only affects the current thread, and starting a new process inherits the process' security descriptor, not the current thread.

I'm running this in an isolated test domain, so both userA and userB are domain admins, and both have the Log On as a Service right domain-wide.

Accrue answered 24/3, 2009 at 15:20 Comment(0)
F
20

When you launch a new process using ProcessStartInfo the process is started in the same window station and desktop as the launching process. If you are using different credentials then the user will, in general, not have sufficient rights to run in that desktop. The failure to initialize errors are caused when user32.dll attempts to initialize in the new process and can't.

To get around this you must first retrieve the security descriptors associated with the window station and desktop and add the appropriate permissions to the DACL for your user, then launch your process under the new credentials.

EDIT: A detailed description on how to do this and sample code was a little long for here so I put together an article with code.

        //The following security adjustments are necessary to give the new 
        //process sufficient permission to run in the service's window station
        //and desktop. This uses classes from the AsproLock library also from 
        //Asprosys.
        IntPtr hWinSta = GetProcessWindowStation();
        WindowStationSecurity ws = new WindowStationSecurity(hWinSta,
          System.Security.AccessControl.AccessControlSections.Access);
        ws.AddAccessRule(new WindowStationAccessRule("LaunchProcessUser",
            WindowStationRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow));
        ws.AcceptChanges();

        IntPtr hDesk = GetThreadDesktop(GetCurrentThreadId());
        DesktopSecurity ds = new DesktopSecurity(hDesk,
            System.Security.AccessControl.AccessControlSections.Access);
        ds.AddAccessRule(new DesktopAccessRule("LaunchProcessUser",
            DesktopRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow));
        ds.AcceptChanges();

        EventLog.WriteEntry("Launching application.", EventLogEntryType.Information);

        using (Process process = Process.Start(psi))
        {
        }
Francoise answered 24/3, 2009 at 17:37 Comment(6)
Thanks; it looks like this is the cause of the problem. Do you have any suggestions on the best way to retrieve the DACL and add permissions for the 2nd user?Accrue
You don't need to set the Window Station and Desktop DACLs if you create the service interactive.Sitar
Is it the same for starting a process with credentials from a "WCF Service" ?Brainpan
For the missing interop methods you need after you have added AsproLock. Here they are: [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr GetProcessWindowStation(); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr GetThreadDesktop(int dwThreadId); [DllImport("kernel32.dll", SetLastError = true)] public static extern int GetCurrentThreadId();Oberstone
Thanks for this answer. I've posted an answer with a "compact" standalone code that does not require the AsproLock library.Psaltery
@StephenMartin Is there a way to give the child process its own window station and desktop? Or is this approach of letting it share the parent station and desktop considered to be OK?Tommietommy
P
22

Based on the answer by @StephenMartin.

A new process launched using the Process class runs in the same window station and desktop as the launching process. If you are running the new process using different credentials, then the new process won't have permissions to access the window station and desktop. What results in errors like 0xC0000142.

The following is a "compact" standalone code to grant a user an access to the current window station and desktop. It does not require the AsproLock library.

Call the GrantAccessToWindowStationAndDesktop method with the username you use to run the Process (Process.StartInfo.UserName), before calling Process.Start.

public static void GrantAccessToWindowStationAndDesktop(string username)
{
    IntPtr handle;
    const int WindowStationAllAccess = 0x000f037f;
    handle = GetProcessWindowStation();
    GrantAccess(username, handle, WindowStationAllAccess);
    const int DesktopRightsAllAccess = 0x000f01ff;
    handle = GetThreadDesktop(GetCurrentThreadId());
    GrantAccess(username, handle, DesktopRightsAllAccess);
}

private static void GrantAccess(string username, IntPtr handle, int accessMask)
{
    SafeHandle safeHandle = new NoopSafeHandle(handle);
    GenericSecurity security =
        new GenericSecurity(
            false, ResourceType.WindowObject, safeHandle,
            AccessControlSections.Access);

    security.AddAccessRule(
        new GenericAccessRule(
            new NTAccount(username), accessMask, AccessControlType.Allow));
    security.Persist(safeHandle, AccessControlSections.Access);
}

[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr GetProcessWindowStation();

[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr GetThreadDesktop(int dwThreadId);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern int GetCurrentThreadId();

// All the code to manipulate a security object is available in .NET framework,
// but its API tries to be type-safe and handle-safe, enforcing a special
// implementation (to an otherwise generic WinAPI) for each handle type.
// This is to make sure only a correct set of permissions can be set
// for corresponding object types and mainly that handles do not leak.
// Hence the AccessRule and the NativeObjectSecurity classes are abstract.
// This is the simplest possible implementation that yet allows us to make use
// of the existing .NET implementation, sparing necessity to
// P/Invoke the underlying WinAPI.

private class GenericAccessRule : AccessRule
{
    public GenericAccessRule(
        IdentityReference identity, int accessMask, AccessControlType type) :
        base(identity, accessMask, false, InheritanceFlags.None,
             PropagationFlags.None, type)
    {
    }
}

private class GenericSecurity : NativeObjectSecurity
{
    public GenericSecurity(
        bool isContainer, ResourceType resType, SafeHandle objectHandle,
        AccessControlSections sectionsRequested)
        : base(isContainer, resType, objectHandle, sectionsRequested)
    {
    }

    new public void Persist(
        SafeHandle handle, AccessControlSections includeSections)
    {
        base.Persist(handle, includeSections);
    }

    new public void AddAccessRule(AccessRule rule)
    {
        base.AddAccessRule(rule);
    }

    #region NativeObjectSecurity Abstract Method Overrides

    public override Type AccessRightType
    {
        get { throw new NotImplementedException(); }
    }

    public override AccessRule AccessRuleFactory(
        System.Security.Principal.IdentityReference identityReference, 
        int accessMask, bool isInherited, InheritanceFlags inheritanceFlags,
        PropagationFlags propagationFlags, AccessControlType type)
    {
        throw new NotImplementedException();
    }

    public override Type AccessRuleType
    {
        get { return typeof(AccessRule); }
    }

    public override AuditRule AuditRuleFactory(
        System.Security.Principal.IdentityReference identityReference,
        int accessMask, bool isInherited, InheritanceFlags inheritanceFlags,
        PropagationFlags propagationFlags, AuditFlags flags)
    {
        throw new NotImplementedException();
    }

    public override Type AuditRuleType
    {
        get { return typeof(AuditRule); }
    }

    #endregion
}

// Handles returned by GetProcessWindowStation and GetThreadDesktop
// should not be closed
private class NoopSafeHandle : SafeHandle
{
    public NoopSafeHandle(IntPtr handle) :
        base(handle, false)
    {
    }

    public override bool IsInvalid
    {
        get { return false; }
    }

    protected override bool ReleaseHandle()
    {
        return true;
    }
}
Psaltery answered 6/6, 2015 at 20:22 Comment(4)
This helped me solve a big issue. Thanks a lot. Keep up the good workAvoirdupois
This code is a gem hidden online. I hope this never disappears. It's impossible to create a windows service that spawns child processes, on different accounts, without using this code.Tommietommy
I agree, this is utterly indispensable code which is not limited to services - it also solves the same problem when starting processes as a different user from Web apps/APIs running in application pools under IIS. Many thanks.Sika
Six years on and still saving people's baconEruption
F
20

When you launch a new process using ProcessStartInfo the process is started in the same window station and desktop as the launching process. If you are using different credentials then the user will, in general, not have sufficient rights to run in that desktop. The failure to initialize errors are caused when user32.dll attempts to initialize in the new process and can't.

To get around this you must first retrieve the security descriptors associated with the window station and desktop and add the appropriate permissions to the DACL for your user, then launch your process under the new credentials.

EDIT: A detailed description on how to do this and sample code was a little long for here so I put together an article with code.

        //The following security adjustments are necessary to give the new 
        //process sufficient permission to run in the service's window station
        //and desktop. This uses classes from the AsproLock library also from 
        //Asprosys.
        IntPtr hWinSta = GetProcessWindowStation();
        WindowStationSecurity ws = new WindowStationSecurity(hWinSta,
          System.Security.AccessControl.AccessControlSections.Access);
        ws.AddAccessRule(new WindowStationAccessRule("LaunchProcessUser",
            WindowStationRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow));
        ws.AcceptChanges();

        IntPtr hDesk = GetThreadDesktop(GetCurrentThreadId());
        DesktopSecurity ds = new DesktopSecurity(hDesk,
            System.Security.AccessControl.AccessControlSections.Access);
        ds.AddAccessRule(new DesktopAccessRule("LaunchProcessUser",
            DesktopRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow));
        ds.AcceptChanges();

        EventLog.WriteEntry("Launching application.", EventLogEntryType.Information);

        using (Process process = Process.Start(psi))
        {
        }
Francoise answered 24/3, 2009 at 17:37 Comment(6)
Thanks; it looks like this is the cause of the problem. Do you have any suggestions on the best way to retrieve the DACL and add permissions for the 2nd user?Accrue
You don't need to set the Window Station and Desktop DACLs if you create the service interactive.Sitar
Is it the same for starting a process with credentials from a "WCF Service" ?Brainpan
For the missing interop methods you need after you have added AsproLock. Here they are: [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr GetProcessWindowStation(); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr GetThreadDesktop(int dwThreadId); [DllImport("kernel32.dll", SetLastError = true)] public static extern int GetCurrentThreadId();Oberstone
Thanks for this answer. I've posted an answer with a "compact" standalone code that does not require the AsproLock library.Psaltery
@StephenMartin Is there a way to give the child process its own window station and desktop? Or is this approach of letting it share the parent station and desktop considered to be OK?Tommietommy
D
4

Based on the answer by @Stephen Martin and Martin Prikryl.

This code helps you to run a process with different user credentials from a service.
I have now optimized the source code.
The removal and setting of rights is now also possible.

namespace QlikConnectorPSExecute
{
    #region Usings
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.Security.AccessControl;
    using System.Security.Principal;
    #endregion

    //inspired by: https://mcmap.net/q/303744/-starting-a-process-with-credentials-from-a-windows-service
    public class WindowsGrandAccess : IDisposable
    {
        #region DLL-Import
        // All the code to manipulate a security object is available in .NET framework,
        // but its API tries to be type-safe and handle-safe, enforcing a special implementation
        // (to an otherwise generic WinAPI) for each handle type. This is to make sure
        // only a correct set of permissions can be set for corresponding object types and
        // mainly that handles do not leak.
        // Hence the AccessRule and the NativeObjectSecurity classes are abstract.
        // This is the simplest possible implementation that yet allows us to make use
        // of the existing .NET implementation, sparing necessity to
        // P/Invoke the underlying WinAPI.

        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr GetProcessWindowStation();

        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr GetThreadDesktop(int dwThreadId);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern int GetCurrentThreadId();
        #endregion

        #region Variables && Properties
        public static int WindowStationAllAccess { get; private set; } = 0x000f037f;
        public static int DesktopRightsAllAccess { get; private set; } = 0x000f01ff;

        private GenericSecurity WindowStationSecurity {get; set;}
        private GenericSecurity DesktopSecurity { get; set; }
        private int? OldWindowStationMask { get; set; }
        private int? OldDesktopMask { get; set; }
        private NTAccount AccountInfo { get; set; }
        private SafeHandle WsSafeHandle { get; set; }
        private SafeHandle DSafeHandle { get; set; }
        #endregion

        #region Constructor & Dispose
        public WindowsGrandAccess(NTAccount accountInfo, int windowStationMask, int desktopMask)
        {
            if (accountInfo != null)
                Init(accountInfo, windowStationMask, desktopMask);
        }

        public void Dispose()
        {
            try
            {
                if (AccountInfo == null)
                    return;

                RestAccessMask(OldWindowStationMask, WindowStationAllAccess, WindowStationSecurity, WsSafeHandle);
                RestAccessMask(OldDesktopMask, DesktopRightsAllAccess, DesktopSecurity, DSafeHandle);
            }
            catch (Exception ex)
            {
                throw new Exception($"The object \"{nameof(WindowsGrandAccess)}\" could not be dispose.", ex);
            }
        }
        #endregion

        #region Methods
        private void Init(NTAccount accountInfo, int windowStationMask, int desktopMask)
        {
            AccountInfo = accountInfo;

            WsSafeHandle = new NoopSafeHandle(GetProcessWindowStation());
            WindowStationSecurity = new GenericSecurity(false, ResourceType.WindowObject, WsSafeHandle, AccessControlSections.Access);

            DSafeHandle = new NoopSafeHandle(GetThreadDesktop(GetCurrentThreadId()));
            DesktopSecurity = new GenericSecurity(false, ResourceType.WindowObject, DSafeHandle, AccessControlSections.Access);

            OldWindowStationMask = ReadAccessMask(WindowStationSecurity, WsSafeHandle, windowStationMask);
            OldDesktopMask = ReadAccessMask(DesktopSecurity, DSafeHandle, desktopMask);
        }

        private AuthorizationRuleCollection GetAccessRules(GenericSecurity security)
        {
            return security.GetAccessRules(true, false, typeof(NTAccount));
        }

        private int? ReadAccessMask(GenericSecurity security, SafeHandle safeHandle, int accessMask)
        {
            var ruels = GetAccessRules(security);

            var username = AccountInfo.Value;
            if (!username.Contains("\\"))
                username = $"{Environment.MachineName}\\{username}";

            var userResult = ruels.Cast<GrantAccessRule>().SingleOrDefault(r => r.IdentityReference.Value.ToLower() == username.ToLower() && accessMask == r.PublicAccessMask);
            if (userResult == null)
            {
                AddGrandAccess(security, accessMask, safeHandle);
                userResult = ruels.Cast<GrantAccessRule>().SingleOrDefault(r => r.IdentityReference.Value.ToLower() == username.ToLower());
                if (userResult != null)
                    return userResult.PublicAccessMask;
            }
            else
              return userResult.PublicAccessMask;

            return null;
        }

        private void AddGrandAccess(GenericSecurity security, int accessMask, SafeHandle safeHandle)
        {
            var rule = new GrantAccessRule(AccountInfo, accessMask, AccessControlType.Allow);
            security.AddAccessRule(rule);
            security.Persist(safeHandle, AccessControlSections.Access);
        }

        private void RemoveGrantAccess(GenericSecurity security, int accessMask, SafeHandle safeHandle)
        {
            var rule = new GrantAccessRule(AccountInfo, accessMask, AccessControlType.Allow);
            security.RemoveAccessRule(rule);
            security.Persist(safeHandle, AccessControlSections.Access);
        }

        private void SetGrandAccess(GenericSecurity security, int accessMask, SafeHandle safeHandle)
        {
            var rule = new GrantAccessRule(AccountInfo, accessMask, AccessControlType.Allow);
            security.SetAccessRule(rule);
            security.Persist(safeHandle, AccessControlSections.Access);
        }

        private void RestAccessMask(int? oldAccessMask, int fullAccessMask, GenericSecurity security, SafeHandle safeHandle)
        {
            if (oldAccessMask == null)
                RemoveGrantAccess(security, fullAccessMask, safeHandle);
            else if (oldAccessMask != fullAccessMask)
            {
                SetGrandAccess(security, oldAccessMask.Value, safeHandle);
            }
        }
        #endregion

        #region private classes
        private class GenericSecurity : NativeObjectSecurity
        {
            public GenericSecurity(
                bool isContainer, ResourceType resType, SafeHandle objectHandle,
                AccessControlSections sectionsRequested)
                : base(isContainer, resType, objectHandle, sectionsRequested) { }

            new public void Persist(SafeHandle handle, AccessControlSections includeSections)
            {
                base.Persist(handle, includeSections);
            }

            new public void AddAccessRule(AccessRule rule)
            {
                base.AddAccessRule(rule);
            }

            new public bool RemoveAccessRule(AccessRule rule)
            {
                return base.RemoveAccessRule(rule);
            }

            new public void SetAccessRule(AccessRule rule)
            {
                base.SetAccessRule(rule);
            }

            new public AuthorizationRuleCollection GetAccessRules(bool includeExplicit, bool includeInherited, Type targetType)
            {
                return base.GetAccessRules(includeExplicit, includeInherited, targetType);
            }

            public override Type AccessRightType
            {
                get { throw new NotImplementedException(); }
            }

            public override AccessRule AccessRuleFactory(
                System.Security.Principal.IdentityReference identityReference,
                int accessMask, bool isInherited, InheritanceFlags inheritanceFlags,
                PropagationFlags propagationFlags, AccessControlType type)
            {
                return new GrantAccessRule(identityReference, accessMask, isInherited, inheritanceFlags, propagationFlags, type);
            }

            public override Type AccessRuleType
            {
                get { return typeof(AccessRule); }
            }

            public override AuditRule AuditRuleFactory(
                System.Security.Principal.IdentityReference identityReference, int accessMask,
                bool isInherited, InheritanceFlags inheritanceFlags,
                PropagationFlags propagationFlags, AuditFlags flags)
            {
                throw new NotImplementedException();
            }

            public override Type AuditRuleType
            {
                get { return typeof(AuditRule); }
            }
        }

        private class GrantAccessRule : AccessRule
        {
            public GrantAccessRule(IdentityReference identity, int accessMask, bool isInherited,
                                     InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags,
                                     AccessControlType type) :
                                     base(identity, accessMask, isInherited,
                                          inheritanceFlags, propagationFlags, type) { }

            public GrantAccessRule(IdentityReference identity, int accessMask, AccessControlType type) :
                base(identity, accessMask, false, InheritanceFlags.None,
                     PropagationFlags.None, type) { }

            public int PublicAccessMask
            {
                get { return base.AccessMask; }
            }
        }

        // Handles returned by GetProcessWindowStation and GetThreadDesktop should not be closed
        private class NoopSafeHandle : SafeHandle
        {
            public NoopSafeHandle(IntPtr handle) :
                base(handle, false) {}

            public override bool IsInvalid
            {
                get { return false; }
            }

            protected override bool ReleaseHandle()
            {
                return true;
            }
        }
        #endregion
    }
}

Using Sample

using (var windowsAccess = new WindowsGrandAccess(accountInfo, WindowsGrandAccess.WindowStationAllAccess, WindowsGrandAccess.DesktopRightsAllAccess))
{
   ...
}

Thank you.

Dinorahdinosaur answered 25/4, 2017 at 12:42 Comment(0)
S
2

This is symptomatic of :
- insufficient rights;
- failure load of a library;

Use Filemon to detect some access denied or
WinDbg to run the application in a debugger and view any issue.

Somatist answered 24/3, 2009 at 15:50 Comment(1)
now i read your answer... +1 for the Filemon reference, I think that will point out the problem.Exalted
I
2

I have reimplemented Martin Prikryl's answer in Python, which I hope someone finds useful.

I ran into this problem running a subprocess in a Python script. I was using the pythonnet package to run System.Diagnostics.Process as different user. My issue was that the subprocess was not running and I received no stdout or stderr.

# Import .NET objects using pythonnet
from System.Diagnostics import Process

# Use .NET API to run a subprocess using the given executable
# as the target user, in the provided working directory.
process = Process()
process.StartInfo.UseShellExecute = False
process.StartInfo.CreateNoWindow = True
process.StartInfo.LoadUserProfile = True
process.StartInfo.RedirectStandardOutput = True
process.StartInfo.RedirectStandardError = True
process.StartInfo.WorkingDirectory = working_dir
process.StartInfo.Domain = "mydomain"
process.StartInfo.UserName = username.lower().replace("mydomain\\", "")
process.StartInfo.PasswordInClearText = password
process.StartInfo.FileName = executable
process.StartInfo.Arguments = " ".join(args)

# Run the subprocess.
process.Start()

# Read subprocess console output
stdout = process.StandardOutput.ReadToEnd()
stderr = process.StandardError.ReadToEnd()
log.info(f"\n{executable} subprocess stdout:\n\n{stdout}")
log.info(f"{executable} subprocess stderr:\n\n{stderr}")
log.info(f"Done running {executable} as {username}.")

I used Martin Prikryl's answer, but I reimplemented it in Python using the pyWin32 library, which solved my issue.:

import win32api, win32process, win32service, win32security

WINDOW_STATION_ALL_ACCESS = 983935
DESKTOP_RIGHTS_ALL_ACCESS = 983551
SE_WINDOW_OBJECT = 7
DACL_SECURITY_INFORMATION = 4


def set_access(user, handle, access):
    info = win32security.GetSecurityInfo(
        handle, SE_WINDOW_OBJECT, DACL_SECURITY_INFORMATION
    )
    dacl = info.GetSecurityDescriptorDacl()
    dacl.AddAccessAllowedAce(win32security.ACL_REVISION, access, user)
    win32security.SetSecurityInfo(
        handle, SE_WINDOW_OBJECT, DACL_SECURITY_INFORMATION, None, None, dacl, None
    )


username = "mattsegal"
user, domain, user_type = win32security.LookupAccountName("", username)
thread_id = win32api.GetCurrentThreadId()
station_handle = win32process.GetProcessWindowStation()
desktop_handle = win32service.GetThreadDesktop(thread_id)
set_access(user, station_handle, WINDOW_STATION_ALL_ACCESS)
set_access(user, desktop_handle, DESKTOP_RIGHTS_ALL_ACCESS)
Indican answered 7/11, 2019 at 1:32 Comment(0)
C
0

How are you setting the domain, user, and password? Are you setting the domain properly as well as the password (it must use a SecureString).

Also, are you setting the WorkingDirectory property? When using a UserName and Password, the documentation states that you must set the WorkingDirectory property.

Canthus answered 24/3, 2009 at 15:27 Comment(1)
I'm setting domain, username and password (as a SecureString) correctly - if I run this code outside of the Windows service, it works as expected. Setting the WorkingDirectory doesn't seem to have any affect, either.Accrue
E
0

It may be that any process kicked off by a service must also have the "Log on as a Service" privelege.

If the user id that you are using to start the second process does not have administrative rights to the box, this could be the case.

An easy test would be to change the local security policy to give the userid "Log on as a service" and try it again.

Edit: After the additional info..

Grazing over Google on this one, it appears that 0xc0000142 relates to not being able to initialize a needed DLL. Is there something that the service has open that the spawned process needs? In any case, it looks like it has to do with the process that's kicked off, and not how you are doing it.

Exalted answered 24/3, 2009 at 15:59 Comment(2)
Both users have Log on as a Service enabled.Accrue
Added some more to my answer. It looks like you may have some conflicts between the service and the spawned process based on the 0xc0000142 error.Exalted
S
0

I had this problem today, and I spent quite some time trying to figure it out. What I ended up doing was to create the service as interactive (using the Allow service to interact with desktop checkbox in services.msc). As soon as I did that the 0xc0000142 errors went away.

Sitar answered 26/5, 2009 at 7:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.