We have an IIS WCF service that launches another process (app.exe) as a different user. I have complete control over both applications (and this is a dev environment for now). The IIS app pool runs as me, a domain user (DOMAIN\nirvin), who is also a local administrator on the box. The second process is supposed to run as a local user (svc-low). I am using System.Diagnostics.Process.Start(ProcessStartInfo)
to launch the process. The process launches successfully - I know because there are no exceptions thrown, and I get a process ID. But the process dies immediately, and I get an error in the Event Log that looks like:
Faulting application name: app.exe, version: 1.0.3.0, time stamp: 0x514cd763
Faulting module name: KERNELBASE.dll, version: 6.2.9200.16451, time stamp: 0x50988aa6
Exception code: 0xc06d007e
Fault offset: 0x000000000003811c
Faulting process id: 0x10a4
Faulting application start time: 0x01ce274b3c83d62d
Faulting application path: C:\Program Files\company\app\app.exe
Faulting module path: C:\Windows\system32\KERNELBASE.dll
Report Id: 7a45cd1c-933e-11e2-93f8-005056b316dd
Faulting package full name:
Faulting package-relative application ID:
I've got pretty thorough logging in app.exe (now), so I don't think it's throwing errors in the .NET code (anymore).
Here's the real obnoxious part: I figured I was just launching the process wrong, so I copied my Process.Start()
call in a dumb WinForms app and ran it on the machine as myself, hoping to tinker around till I got the parameters right. So of course that worked the very first time and every time since: I'm able to consistently launch the second process and have it run as intended. It's only launching from IIS that doesn't work.
I've tried giving svc-low permission to "Log on as a batch job" and I've tried giving myself permission to "Replace a process level token" (in Local Security Policy), but neither seem to have made any difference.
Help!
Environment Details
- Windows Server 2012
- .NET 4.5 (all applications mentioned)
Additional Details
At first app.exe was a Console Application. Trying to launch was making conhost.exe generate errors in the Event Log, so I switched app.exe to be a Windows Application. That took conhost out of the equation but left me the situation described here. (Guided down that path by this question.)
The ProcessStartInfo
object I use looks like this:
new ProcessStartInfo
{
FileName = fileName,
Arguments = allArguments,
Domain = domainName,
UserName = userName,
Password = securePassword,
WindowStyle = ProcessWindowStyle.Hidden,
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = false
//LoadUserProfile = true //I've done it with and without this set
};
An existing question says I should go down to the native API, but a) that question addresses a different situation and b) the success of the dumb WinForms app suggests that Process.Start
is a viable choice for the job.
I ended up opening a case with Microsoft, and this is the information I was given:
Process.Start internally calls CreateProcessWithLogonW(CPLW) when credentials are specified. CreateProcessWithLogonW cannot be called from a Windows Service Environment (such as an IIS WCF service). It can only be called from an Interactive Process (an application launched by a user who logged on via CTRL-ALT-DELETE).
(that's verbatim from the support engineer; emphasis mine)
They recommended I use CreateProcessAsUser
instead. They gave me some useful sample code, which I then adapted to my needs, and now everything works great!
The end result was this:
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security;
public class ProcessHelper
{
static ProcessHelper()
{
UserToken = IntPtr.Zero;
}
private static IntPtr UserToken { get; set; }
public int StartProcess(ProcessStartInfo processStartInfo)
{
LogInOtherUser(processStartInfo);
Native.STARTUPINFO startUpInfo = new Native.STARTUPINFO();
startUpInfo.cb = Marshal.SizeOf(startUpInfo);
startUpInfo.lpDesktop = string.Empty;
Native.PROCESS_INFORMATION processInfo = new Native.PROCESS_INFORMATION();
bool processStarted = Native.CreateProcessAsUser(UserToken, processStartInfo.FileName, processStartInfo.Arguments,
IntPtr.Zero, IntPtr.Zero, true, 0, IntPtr.Zero, null,
ref startUpInfo, out processInfo);
if (!processStarted)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
uint processId = processInfo.dwProcessId;
Native.CloseHandle(processInfo.hProcess);
Native.CloseHandle(processInfo.hThread);
return (int) processId;
}
private static void LogInOtherUser(ProcessStartInfo processStartInfo)
{
if (UserToken == IntPtr.Zero)
{
IntPtr tempUserToken = IntPtr.Zero;
string password = SecureStringToString(processStartInfo.Password);
bool loginResult = Native.LogonUser(processStartInfo.UserName, processStartInfo.Domain, password,
Native.LOGON32_LOGON_BATCH, Native.LOGON32_PROVIDER_DEFAULT,
ref tempUserToken);
if (loginResult)
{
UserToken = tempUserToken;
}
else
{
Native.CloseHandle(tempUserToken);
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
}
private static String SecureStringToString(SecureString value)
{
IntPtr stringPointer = Marshal.SecureStringToBSTR(value);
try
{
return Marshal.PtrToStringBSTR(stringPointer);
}
finally
{
Marshal.FreeBSTR(stringPointer);
}
}
public static void ReleaseUserToken()
{
Native.CloseHandle(UserToken);
}
}
internal class Native
{
internal const int LOGON32_LOGON_INTERACTIVE = 2;
internal const int LOGON32_LOGON_BATCH = 4;
internal const int LOGON32_PROVIDER_DEFAULT = 0;
[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
internal struct STARTUPINFO
{
public int cb;
[MarshalAs(UnmanagedType.LPStr)]
public string lpReserved;
[MarshalAs(UnmanagedType.LPStr)]
public string lpDesktop;
[MarshalAs(UnmanagedType.LPStr)]
public string lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public System.UInt32 nLength;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle;
}
[DllImport("advapi32.dll", EntryPoint = "LogonUserW", SetLastError = true, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
internal extern static bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
[DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUserA", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
internal extern static bool CreateProcessAsUser(IntPtr hToken, [MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
[MarshalAs(UnmanagedType.LPStr)] string lpCommandLine, IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes, bool bInheritHandle, uint dwCreationFlags, IntPtr lpEnvironment,
[MarshalAs(UnmanagedType.LPStr)] string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
internal extern static bool CloseHandle(IntPtr handle);
}
There are some pre-requisites to making this code work. The user running it must have the user right to 'Replace a process level token' and 'Adjust memory quotas for a process', while the 'other user' must have the user right to 'Log on as a batch job'. These settings can be found under the Local Security Policy (or possibly through Group Policy). If you change them, a restart will be required.
UserToken
is a property that can be closed via ReleaseUserToken
because we will call StartProcess
repeatedly and we were told not to log the other user on again and again.
That SecureStringToString()
method was taken from this question. Using SecureString
was not part of Microsoft's recommendation; I did it this way so as not to break compatibility with some other code.
Exception code: 0xc06d007e
This is an exception that's specific to Microsoft Visual C++, facility code 0x6d. The error code is 0x007e (126), ERROR_MOD_NOT_FOUND, "The specified module could not be found". This exception is raised when a delay-loaded DLL cannot be found. Most programmers have the code that generates this exception on their machine, vc/include/delayhlp.cpp in the Visual Studio install directory.
Well, it is the typical "file not found" mishap, specific to a DLL. If you have no idea what DLL is missing then you can use SysInternals' ProcMon utility. You'll see the program search for the DLL and not finding just before it bombs.
A classic way to get poorly designed programs to crash with Process.Start() is by not setting the ProcessStartInfo.WorkingDirectory property to the directory in which the EXE is stored. It usually is by accident but won't be when you use the Process class. Doesn't look like you do so tackle that first.
来源:https://stackoverflow.com/questions/15581142/why-is-this-process-crashing-as-soon-as-it-is-launched