Potřebovali jste někdy volat z vaší aplikace jiný program, ale s nutností spuštění pod právy jiného uživatele (tj. jako RunAs). Já ano. Pro řešení tohoto problému jsem vytvořil třídu ProcessUtil.
Pro spuštění jiného programu slouží v .NET metoda System.Diagnostics.Process.Start, které se pomoci objektu ProcessStartInfo určí potřebné parametry.
Věc ale není tak jednoduchá, protože pokud potřebujeme spustit program pod právy jiného uživatele, můžou nastat tři různé případy:
1) UAC elevace
Jedná se o spuštění programu se zvýšením práv na administrátora. Ve Windows tomu odpovídá volba Spustit jako správce (Run as administrátor). Touto volbou se většinou po odsouhlasení uživatelem buď aplikace rovnou spustí (případ uživatele lokálního administrátora) nebo, pokud uživatel není na počítači lokální administrátor, tak je nabídnut dialog pro přihlášení jiného uživatele.
V .NET takovéto spuštění provedeme tak, že objektu ProcessStartInfo nastavíme způsob spuštění pomoci Shell - UseShellExecute a vlastnost Verb nastavíme na hodnotu runas. Dále ještě můžeme nastavit handler dialogu, který bude zobrazen (aby byl zobrazen jako modalní k naší aplikaci). Také musíme ošetřit případ, když uživatel dialog stornuje.
Kód bude vypadat například takto:
var startInfo = new ProcessStartInfo(command)
{
UseShellExecute = true,
WorkingDirectory = Environment.CurrentDirectory,
Verb = "runas" //Start Elevated
};
if (this.Windowhandle != IntPtr.Zero)
{
//Make the UAC dialog modal to Windowhandle
startInfo.ErrorDialog = true;
startInfo.ErrorDialogParentHandle = this.Windowhandle;
}
try
{
var p = Process.Start(startInfo);
//Wait for the process to end.
p.WaitForExit();
return true;
}
catch (System.ComponentModel.Win32Exception)
{
//User cancelled UAC dialog
return false;
}
2) RUNAS
Úplně jiný případ spuštění jiného programu je situace, kdy žádný dialog uživateli nechceme zobrazit. Uživatelské jméno a heslo uživatele, pod kterým má spouštěný program běžet, máme v naší aplikaci k dispozici (například nějakým bezpečným způsobem uložené v configu nebo databázi). V takovém případě objektu ProcessStartInfo musíme uživatele nastavit vlastnostmi UserName, Domain a Password (typu SecureString) a dále nesmíme použít shell (nastavení UseShellExecute = false).
string userName = ExtractLogin(user);
string domainName = ExtractDomain(user);
var startInfo = new ProcessStartInfo(command)
{
UseShellExecute = false,
WorkingDirectory = Environment.CurrentDirectory,
Domain = domainName,
UserName = userName,
Password = MakeSecureString(password)
};
...
private static SecureString MakeSecureString(string text)
{
SecureString secure = new SecureString();
foreach (char c in text)
{
secure.AppendChar(c);
}
return secure;
}
3) RunAs /NetOnly
Bohužel ještě existuje nutnost třetího způsobu jak spustit proces pod jiným uživatelem. Jedná se o funkcionalitu, kterou dělá command line program runas s parametrem /netonly.
O co se jedná? Nejlépe si to vysvětlíme na příkladu. Máme počítač PC1, který není členem domény mydomain (nebo není členem žádné domény). Dále máme počítač PC2, který je členem domény mydomain. Na počítači PC2 máme program XY, který potřebujeme zavolat vzdáleně z počítače PC1, tedy \\PC2\shared\XY.exe. A abychom měli práva program spustit, tak volání budeme provádět pod nějakým uživatelem z domény mydomain. V tomto případě, kdy program nelze spustit lokálně, musíme použít tento parametr /netonly.
.NET pro spouštění při UseShellExecute = false používá Windows funkci CreateProcessWithLogonW. Abychom dosáhli požadovaného chování parametru netonly utility runas, musíme funkci nastavit příznak logonFlag na hodnotu LOGON_NETCREDENTIALS_ONLY. Problém je ale v tom, že na nastavení tohoto příznaku není v .NET objektu ProcessStartInfo podpora, a tak si všechno musíme udělat sami přes Win32 API volání.
(Pozn.: Volat přímo runas nemůžeme, protože tam se heslo zadává z uživatelského vstupu.)
Třída ProcessUtil
Pro spuštění programu pod jiným uživatelem jsem napsal třídu ProcessUtil, která obsahuje podporu pro spuštění všemi třemi výše popsanými způsoby. Vše se provádí jednotnou metodou RunCommandAs, která v té nejobecnější variantě obsahuje parametry:
string command, string workingDirectory, string user, string password, bool netOnly, bool showOutput, IntPtr windowhandle.
Pro spuštění s UAC dialogem se použije user a password null, jinak se určí uživatel ve tvaru domena\Username a jeho heslo. Pro třetí způsob spuštění se nastaví netOnly: true (v takovém případě se pak spuštění provede pomocnou metodou StartProcessWithLogonNetOnly).
Dále je ještě možné parametrem showOutput=true určit, zda se má spuštění provést se zobrazením výstupu, kdy se pak volání provede pomoci příkazu cmd.exe s ‘/C command & pause’.
Celý kód třídy ProcessUtil je zde:
internal static class ProcessUtil
{
#region NativeMethods class
private static class NativeMethods
{
[StructLayout(LayoutKind.Sequential)]
public class STARTUPINFO
{
public int cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public class PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[Flags]
internal enum LogonFlags
{
//Log on, but use the specified credentials on the network only. The new process uses the same token as the caller, but the system creates
//a new logon session within LSA, and the process uses the specified credentials as the default credentials.
//This value can be used to create a process that uses a different set of credentials locally than it does remotely.
//This is useful in inter-domain scenarios where there is no trust relationship.
//The system does not validate the specified credentials. Therefore, the process can start, but it may not have access to network resources.
LOGON_NETCREDENTIALS_ONLY = 2,
//Log on, then load the user profile in the HKEY_USERS registry key. The function returns after the profile is loaded.
//Loading the profile can be time-consuming, so it is best to use this value only if you must access the information in the HKEY_CURRENT_USER registry key.
LOGON_WITH_PROFILE = 1
}
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
public static extern bool CreateProcessWithLogonW(string userName, string domain, IntPtr password, LogonFlags logonFlags, [MarshalAs(UnmanagedType.LPTStr)] string appName, System.Text.StringBuilder cmdLine, int creationFlags, IntPtr environmentBlock, [MarshalAs(UnmanagedType.LPTStr)] string lpCurrentDirectory, STARTUPINFO lpStartupInfo, PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
}
#endregion
#region action methods
/// <summary>
/// Run an command under another user credentials.
/// </summary>
/// <param name="command">Executable file with arguments</param>
/// <param name="workingDirectory">Working directory</param>
/// <param name="user">Username with domain of desired credentials or null for Elevated Start</param>
/// <param name="password">Password of desired credentials</param>
/// <param name="showOutput">Show Output window</param>
/// <param name="netOnly">Indicates that the user information specified is for remote access only. (RunAs /NetOnly functionality)</param>
/// <param name="windowhandle">windowhandle for UAC dilog or IntPtr.Zero</param>
public static bool RunCommandAs(string command, string workingDirectory, string user, string password, bool netOnly, bool showOutput, IntPtr windowhandle)
{
string cmd = string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("COMSPEC")) ? "cmd.exe" : System.Environment.GetEnvironmentVariable("COMSPEC");
var startInfo = new ProcessStartInfo(cmd, "/c " + EncodeParameterArgument(command + (showOutput ? " & pause" : "")))
{
WorkingDirectory = workingDirectory,
LoadUserProfile = false,
CreateNoWindow = !showOutput,
WindowStyle = !showOutput ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal
};
if (string.IsNullOrEmpty(user))
{
startInfo.UseShellExecute = true;
startInfo.Verb = "runas"; //Start Elevated
}
else
{
string userName = ExtractLogin(user);
string domainName = ExtractDomain(user);
startInfo.UseShellExecute = false;
startInfo.Domain = domainName;
startInfo.UserName = userName;
startInfo.Password = MakeSecureString(password);
}
if (windowhandle != IntPtr.Zero)
{
//Make the UAC dialog modal to Windowhandle
startInfo.ErrorDialog = true;
startInfo.ErrorDialogParentHandle = windowhandle;
}
try
{
if (string.IsNullOrEmpty(user) || !netOnly)
{
//Run command normaly using Process class
var process = System.Diagnostics.Process.Start(startInfo);
//Wait for the process to end.
process.WaitForExit();
}
else
{
//Run command with RunAs /NetOnly functionality (CreateProcessWithLogonW with LOGON_NETCREDENTIALS_ONLY LogonFlag) and wait for the process to end.
StartProcessWithLogonNetOnly(startInfo, true);
}
return true;
}
catch (System.ComponentModel.Win32Exception)
{
if (string.IsNullOrEmpty(user))
{
//User cancelled UAC dialog
return false;
}
throw;
}
}
/// <summary>
/// Run an command under another user credentials.
/// </summary>
/// <param name="command">Executable file with arguments</param>
/// <param name="workingDirectory">Working directory</param>
/// <param name="user">Username with domain of desired credentials or null for Elevated Start</param>
/// <param name="password">Password of desired credentials</param>
/// <param name="netOnly">Indicates that the user information specified is for remote access only. (RunAs /NetOnly functionality)</param>
/// <param name="showOutput">Show Output window</param>
public static bool RunCommandAs(string command, string workingDirectory, string user, string password, bool netOnly, bool showOutput)
{
return RunCommandAs(command, workingDirectory, user, password, netOnly, showOutput, IntPtr.Zero);
}
/// <summary>
/// Run an command under another user credentials.
/// </summary>
/// <param name="command">Executable file with arguments</param>
/// <param name="workingDirectory">Working directory</param>
/// <param name="user">Username with domain of desired credentials or null for Elevated Start</param>
/// <param name="password">Password of desired credentials</param>
/// <param name="netOnly">Indicates that the user information specified is for remote access only. (RunAs /NetOnly functionality)</param>
public static bool RunCommandAs(string command, string workingDirectory, string user, string password, bool netOnly)
{
return RunCommandAs(command, workingDirectory, user, password, netOnly, false, IntPtr.Zero);
}
/// <summary>
/// Run an command under another user credentials.
/// </summary>
/// <param name="command">Executable file with arguments</param>
/// <param name="workingDirectory">Working directory</param>
/// <param name="user">Username with domain of desired credentials or null for Elevated Start</param>
/// <param name="password">Password of desired credentials</param>
public static bool RunCommandAs(string command, string workingDirectory, string user, string password)
{
return RunCommandAs(command, workingDirectory, user, password, false, false, IntPtr.Zero);
}
/// <summary>
/// Run an command under another user credentials.
/// </summary>
/// <param name="command">Executable file with arguments</param>
/// <param name="workingDirectory">Working directory</param>
public static bool RunCommandAs(string command, string workingDirectory)
{
return RunCommandAs(command, workingDirectory, null, null, false, false, IntPtr.Zero);
}
#endregion
#region private member functions
private static void StartProcessWithLogonNetOnly(ProcessStartInfo startInfo, bool waitForExit)
{
if (startInfo.UseShellExecute)
{
throw new InvalidOperationException("UseShellExecute must be false.");
}
if (startInfo.LoadUserProfile)
{
throw new InvalidOperationException("LoadUserProfile cannot be used.");
}
if (string.IsNullOrEmpty(startInfo.UserName))
{
throw new InvalidOperationException("UserName is empty.");
}
var cmdLine = BuildCommandLine(startInfo.FileName, startInfo.Arguments);
var lpStartupInfo = new NativeMethods.STARTUPINFO();
var lpProcessInformation = new NativeMethods.PROCESS_INFORMATION();
int creationFlags = 0;
if (startInfo.CreateNoWindow)
{
creationFlags |= 0x8000000;
}
IntPtr zero = IntPtr.Zero;
string workingDirectory = startInfo.WorkingDirectory;
if (string.IsNullOrEmpty(workingDirectory))
{
workingDirectory = Environment.CurrentDirectory;
}
NativeMethods.LogonFlags logonFlags = NativeMethods.LogonFlags.LOGON_NETCREDENTIALS_ONLY; //NetOnly;
IntPtr passwordPrt = IntPtr.Zero;
try
{
if (startInfo.Password == null)
{
passwordPrt = Marshal.StringToCoTaskMemUni(string.Empty);
}
else
{
passwordPrt = Marshal.SecureStringToCoTaskMemUnicode(startInfo.Password);
}
int error = 0;
bool flag = NativeMethods.CreateProcessWithLogonW(startInfo.UserName, startInfo.Domain, passwordPrt, logonFlags, null, cmdLine, creationFlags, zero, workingDirectory, lpStartupInfo, lpProcessInformation);
if (!flag)
{
error = Marshal.GetLastWin32Error();
}
if (!flag)
{
if (error != 0xc1 && error != 0xd8)
{
throw new Win32Exception(error);
}
throw new Win32Exception(error, "Invalid Application");
}
}
finally
{
if (passwordPrt != IntPtr.Zero)
{
Marshal.ZeroFreeCoTaskMemUnicode(passwordPrt);
}
}
if (waitForExit)
{
NativeMethods.WaitForSingleObject(lpProcessInformation.hProcess, 0xFFFFFFFF);
}
}
private static StringBuilder BuildCommandLine(string executableFileName, string arguments)
{
StringBuilder builder = new StringBuilder();
string str = executableFileName.Trim();
bool flag = str.StartsWith("\"", StringComparison.Ordinal) && str.EndsWith("\"", StringComparison.Ordinal);
if (!flag)
{
builder.Append("\"");
}
builder.Append(str);
if (!flag)
{
builder.Append("\"");
}
if (!string.IsNullOrEmpty(arguments))
{
builder.Append(" ");
builder.Append(arguments);
}
return builder;
}
private static SecureString MakeSecureString(string text)
{
SecureString secure = new SecureString();
foreach (char c in text)
{
secure.AppendChar(c);
}
return secure;
}
/// <summary>
/// Encodes an argument for passing into a program
/// </summary>
/// <param name="original">The value that should be received by the program</param>
/// <returns>The value which needs to be passed to the program for the original value
/// to come through</returns>
public static string EncodeParameterArgument(string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
value = "\"" + System.Text.RegularExpressions.Regex.Replace(value, @"(\\)+$", @"$1$1") + "\"";
return value;
}
/// <summary>
/// Vrací samotné přihlašovací jméno uživatele
/// </summary>
/// <param name="loginName">Přihlašovací jméno ve tvaru "domain\login" nebo "[email protected]", případně pouze login</param>
/// <returns>Přihlašovací jméno uživatele</returns>
private static string ExtractLogin(string loginName)
{
string strExp = loginName.Replace("/", "\\");
int index = strExp.IndexOf('\\');
if (index != -1)
{
strExp = strExp.Substring(index + 1);
}
else
{
index = strExp.IndexOf('@');
if (index != -1)
{
strExp = strExp.Substring(0, index);
}
}
return strExp;
}
/// <summary>
/// Vrací název domeny z přihlašovacího jména uživatele
/// </summary>
/// <param name="loginName">Přihlašovací jméno ve tvaru "domain\login" nebo "[email protected]", případně pouze login</param>
/// <returns>Název domeny uživatele</returns>
private static string ExtractDomain(string loginName)
{
string strExp = loginName.Replace("/", "\\");
int index = strExp.IndexOf('\\');
if (index != -1)
{
strExp = strExp.Substring(0, index);
}
else
{
index = strExp.IndexOf('@');
if (index != -1)
{
strExp = strExp.Substring(index + 1);
}
else
{
strExp = "";
}
}
index = strExp.IndexOf('.');
if (index != -1)
{
strExp = strExp.Substring(0, index);
}
return strExp.ToUpper(System.Globalization.CultureInfo.CurrentCulture);
}
#endregion
}