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:
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.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.
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.
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;
}
}
}
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:
WindowStation
and Desktop
, so we don't run into permission issues.winsta0\default
which is perfectly fine, because the user of the interactive process seems to have proper permissions.