Search code examples
c#winapiwiximpersonationwix3

CreateProcessAsUser returns C0000142 in one scenario, but works in another


We have a problem which only occurs in a new scenario we have to support.

Here's the scenario in which it works since many years:

  • An interactive user without administrative permissions starts our wix bootstrapper, which starts our update-service which then starts a second instance of the wix bootstrapper as local system. This second instance with local system permissions is used to install our software. Because we have to do some database stuff too, we execute a tool called dbinit within a custom action and because this tool has to access a sql server, it has to run under the origin user privileges. This works fine.

enter image description here

  • Now there's an additional scenario we have to support: It's not an interactive user anymore, who starts our wix bootstrapper, now it's a windows service. I thought this would be pretty straightforward and should work out of the box, but ohhh boy, was I wrong. Here's the sequence diagram of the new scenario:

enter image description here

As you can see, in the new scenario, CreateProcessAsUser also succeeds, but the dbinit-process then immediately exits with error code C0000142.

We tried creating custom WindowStation/Desktop. The permissions should be correct (checked with ProcessHacker) and we also marked the handles as inheritable. But regardless they are inheritable or we set lpDesktop accordingly, it just doesn't change anything.

What we found out is, if the user under which the windows service is running is in the local administrator group, it works, but we can't do that in production.

In many code samples I found people are using LogonUser to get the token, but because our user is a MSA (Managed Service Account) most of the time and we don't have passwords, I don't think that's possible.

UPDATE 1:

If I set lpDesktop to an empty string, it works in about 50 % of the cases so it seems, that it has to do something with WindowStation and Desktop. What I don't get is, why it does not work consistently and why it doesn't help to create custom WindowStation and Desktop with proper rights.

UPDATE 2:

Some code:

internal static SafeTokenHandle GetProcessAccessToken(int processId)
{
    var process = Process.GetProcessById(processId);
    if (OpenProcessToken(process.Handle, TOKEN_DUPLICATE, out IntPtr tokenHandle))
        return new SafeTokenHandle(tokenHandle);
    else
        throw new Win32Exception();
}

internal static SafeTokenHandle DuplicateAccessToken(SafeTokenHandle token)
{
    var success = DuplicateTokenEx(token,
                                    TOKEN_ALL_ACCESS,
                                    null,
                                    IMPERSONATION_LEVEL_SecurityIdentification,
                                    TOKEN_TYPE_TokenPrimary,
                                    out IntPtr newToken);


    return success ? new SafeTokenHandle(newToken) : throw new Win32Exception();
}

private bool Start()
{
    using (var processToken = GetProcessAccessToken(StartInfo.ProcessIdToImpersonateUserContext))
    {
        using (var newToken = DuplicateAccessToken(processToken))
        {
            var si = new STARTUPINFO();
            var pi = new PROCESS_INFORMATION();

            var safeProcessHandle = new SafeProcessHandle();
            var safeThreadHandle = new SafeThreadHandle();

            SafeFileHandle redirectedStandardOutputParentHandle = null;

            try
            {
                var profileInfo = new PROFILEINFO();
                profileInfo.dwSize = Marshal.SizeOf(profileInfo);
                profileInfo.lpUserName = "LimitedUser";

                var succeeded = LoadUserProfile(newToken, ref profileInfo);
                if (!succeeded)
                    throw new Win32Exception();

                var cmdLine = $"\"{StartInfo.FileName}\" {StartInfo.Arguments}".Trim();

                if (StartInfo.RedirectStandardOutput)
                {
                    CreatePipe(out redirectedStandardOutputParentHandle, out si.hStdOutput);
                    si.dwFlags = STARTF_USESTDHANDLES;
                }

                int creationFlags = 0;
                if (StartInfo.CreateNoWindow)
                    creationFlags |= CREATE_NO_WINDOW;

                creationFlags |= CREATE_UNICODE_ENVIRONMENT;

                int logonFlags = 0;
                if (StartInfo.LoadUserProfile)
                    logonFlags |= (int)LogonFlags.LOGON_WITH_PROFILE;

                string workingDirectory = StartInfo.WorkingDirectory;
                if (string.IsNullOrEmpty(workingDirectory))
                    workingDirectory = Environment.CurrentDirectory;

                var envBlock = GetEnvironmentBlock(newToken);

                succeeded = CreateProcessAsUserW(newToken,
                                                    null,
                                                    cmdLine,
                                                    null,
                                                    null,
                                                    true,
                                                    creationFlags,
                                                    new HandleRef(null, envBlock.DangerousGetHandle()),
                                                    workingDirectory,
                                                    si,
                                                    pi);

                if (!succeeded)
                    throw new Win32Exception();
                if (pi.hProcess != (IntPtr)0 && pi.hProcess != INVALID_HANDLE_VALUE)
                    safeProcessHandle.InitialSetHandle(pi.hProcess);
                if (pi.hThread != (IntPtr)0 && pi.hThread != INVALID_HANDLE_VALUE)
                    safeThreadHandle.InitialSetHandle(pi.hThread);

                DestroyEnvironmentBlock(envBlock.DangerousGetHandle());
            }
            finally
            {
                si.Dispose();
            }

            if (StartInfo.RedirectStandardOutput)
            {
                var enc = StartInfo.StandardOutputEncoding ?? Console.OutputEncoding;
                StandardOutput = new StreamReader(new FileStream(redirectedStandardOutputParentHandle, FileAccess.Read, 4096, false), enc, true, 4096);
            }

            _processHandle = safeProcessHandle;

            safeThreadHandle.Dispose();

            return true;
        }
    }
}

Solution

  • We still don't know exactly, why Windows in our scenario is always using winsta0\default, but we think it has something to do with rundll32 being involved.

    The reason why setting lpDesktop to a custom WindowStation and Desktop never worked is, that we mixed up Ansi and Unicode. As soon as we set CharSet explicitly to Unicode, it worked. Here's the code:

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    internal class STARTUPINFO
    {
        public int cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        ...
    }
    
    [DllImport(ADVAPI32, CharSet = CharSet.Unicode, SetLastError = true, BestFitMapping = false)]
    [SuppressUnmanagedCodeSecurity()]
    internal extern static bool CreateProcessAsUser(SafeHandle hToken, string lpApplicationName, string lpCommandLine,
                                                    SECURITY_ATTRIBUTES lpProcessAttributes, SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles,
                                                    int dwCreationFlags, HandleRef lpEnvironment, string lpCurrentDirectory, STARTUPINFO lpStartupInfo,
                                                    PROCESS_INFORMATION lpProcessInformation);
    

    Our solution now works as follow:

    • If the process, where we take and duplicate the token from, runs in session 0, we create a custom WindowStation and Desktop, so we don't run into permission issues.
    • If the process does not run in session 0, we leave everything as it was and ran for years, because then it uses winsta0\default which is perfectly fine, because the user of the interactive process seems to have proper permissions.