22

I'm trying to start an elevated process from with a non-elevated process, but I also need to supply the username and password for a user with administrative credentials. I've tried both the "runas" method for elevation as well as using a manifest, but both yield different errors.

For example, if I do this (without using a manifest that requires elevation):

ProcessStartInfo info = new ProcessStartInfo(path);

info.UseShellExecute = false;
info.UserName = username;
info.Password = securePwd;
info.Domain = "MyDomain";
info.Verb = "runas";

var proc = Process.Start(info);

The process launches without displaying the UAC confirmation dialog and fails upon trying to execute the command that requires administrator permissions (I'm just trying to write a test file to the Program Files directory).

If I add a manifest to the target application that indicates that it requires elevation, then I get a Win32Exception stating that the operation requires elevation.

The issue seems to be setting UseShellExecute to false(as both approaches work fine when this is not the case), but I have to set it to false in order to launch the process under a different user account.

How can I launch an elevated process from a non-elevated process and supply the username and password manually?

BOUNTY EDIT: While the user cannot be required to enter administrator credentials, a UAC nag dialog is perfectly acceptable. I'm not looking to bypass UAC here.

Adam Robinson
  • 182,639
  • 35
  • 285
  • 343
  • 1
    For those that might be curious, this application is part of an auto-update package that will run an MSI that will install for all users. Given that it's highly likely that the user actually running the application will not have administrative credentials (nor will they know an administrator's username and password), I need to be able to allow an actual administrator to supply these credentials once and run this application on several machines simultaneously. – Adam Robinson Mar 26 '12 at 13:17
  • 1
    I also need to account for the scenario where UAC is turned off. – Adam Robinson Mar 26 '12 at 13:18
  • Your manifest is using _requestedExecutionLevel_ as advised [here](http://blogs.msdn.com/b/oldnewthing/archive/2010/07/26/10042389.aspx), correct? I'm just making sure everything you've done to hint escalated privileges is squared away before I try to diagnose the issue. A manifest will not work in XP (see the linked article), but it will work in Vista forward. – MrGomez Mar 28 '12 at 17:35
  • @MrGomez: Yes, the manifest is created correctly. If I launch it without specifying a user, it works fine **if UAC is on** (though I have to enter the credentials manually, which I must avoid), but if I specify the credentials it will not allow the process to elevate. – Adam Robinson Mar 28 '12 at 17:45
  • 1
    Thanks for the response and the edit. I've added an answer that I believe will work for you. Worst case, it's something to check for! – MrGomez Mar 28 '12 at 19:16
  • Just now came to my mind.... Have you tried doing it manually? Assuming you are logged in as a non-privileged user, right clicking the application you want to run with elevated privileges, selecting RunAs, and providing the exact same values you are using in your program: username, securePwd, "MyDomain"? – Only You Apr 04 '12 at 06:05
  • @OnlyYou: Yes, that works if UAC is enabled, but I need to be able to supply the credentials programmatically and be able to support scenarios where UAC is disabled. – Adam Robinson Apr 04 '12 at 13:26

6 Answers6

15

i was surprised there's no way to do this, until i found an on blog entry by Chris Jackson:

Why Can’t I Elevate My Application to Run As Administrator While Using CreateProcessWithLogonW?

You need a bootstrapper. Some process which will let you do the transition to the alternate user, which could be responsible for running the requireAdministrator application. So, you could design something like this:

enter image description here

Why don’t we just create the ShellExecuteWithLogonW API? I’ll never say never, and we might at some point. But today, the use cases for this APIs have been use cases where there has been an alternate design which is superior.

The most common request is for people writing home-grown software deployment software, where they’d like to encode credentials right into the app and elevate their own process. The real problem here is not the lack of the API, it’s that you have admin credentials encoded in your app for the world to read.

So the solution requires ShellExecute, it's the only one that knows how to trigger a Consent dialog.

It brings up a good point: What are you doing with a person's password already?

Bonus Chatter

There's no UAC on Server Core because there's no windows to show a consent prompt.

Visual Vincent
  • 18,045
  • 5
  • 28
  • 75
Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • A single administrative user will supply network administrator credentials to be used for installing updates when they elect to have them installed. These credentials will be passed securely down to the client, which will use them to initiate the update process. I'll admit I had not considered having a *middle* process (launched with the administrator credentials) then use ShellExecute to launch the elevated process. I will give that a shot. – Adam Robinson Apr 04 '12 at 17:38
  • Perfect! Using an intermediate process running with the specified credentials which then launches another process with `runas` was perfect. – Adam Robinson Apr 04 '12 at 18:24
  • i really wish `runas` wasn't the only way to elevate a process; but it's good that you got it working. And now i know how to do it. As a practical matter you can re-launch your own process as another user, with a command line argument telling yourself to ShellExecute another guy. Or launch the other program as another user, with a command line argument telling it to relaunch itself as administrator. Or use a *third* application. If i needed a "run my own program as another use administrative", i would relaunch myself as another user, then `runas` to elevate. – Ian Boyd Apr 04 '12 at 18:29
  • 2
    That's what I'm doing. A single executable launched three times. – Adam Robinson Apr 04 '12 at 22:25
  • 1
    While I wholeheartedly agree with the design considerations and security issue raised by this answer, for those who insist on doing this there is a simple way: start your process by calling "cmd.exe /C" with admin credentials specified as in the question, where your desired executable and any command-line parameters follow the /C option. This will open an extra cmd window (admittedly non-ideal), and prompt the user to accept elevation via a UAC dialog. – Michael Repucci Dec 19 '14 at 17:35
3

From MSDN:

You cannot elevate an already running process. Thus, you should refactor your app to be separated into admin & non-admin operations - running the default application with normal privileges and starting another elevated process for each administrative operation.

Let's work with that, assuming you request administrator rights from the outset on the processes that require them. Based upon the context you've provided:

The issue seems to be setting UseShellExecute to false (as both approaches work fine when this is not the case), but I have to set it to false in order to launch the process under a different user account.

As you mentioned, exactly as noted in the documentation for UseShellExecute:

UseShellExecute must be false if the UserName property is not Nothing or an empty string, or an InvalidOperationException will be thrown when the Process.Start(ProcessStartInfo) method is called.

We now know you're executing your program directly instead of through the use of a shell. This is valuable information.

Backpathing through the documentation, the docs for ProcessStartInfo carry the following security note:

This class contains a link demand at the class level that applies to all members. A SecurityException is thrown when the immediate caller does not have full-trust permission. For details about security demands, see Link Demands.

So, you don't have the right Link Demand. While trying to solve your permissions issue, you inadvertently created another permissions issue.

The upshot is you need to decorate your calling method with the right Security Demand, which should be FullTrust. You can do this declaratively or imperatively within your code.

(Additional reading)

MrGomez
  • 23,788
  • 45
  • 72
  • Interesting, but *which* demand? And the docs seem to imply that the security is the same regardless of usage; why would it fail with `UseShellExecute = false` but not with `= true`? – Adam Robinson Mar 28 '12 at 19:24
  • 1
    Excuse my terseness. According to the permission scoping (`[PermissionSetAttribute(SecurityAction.LinkDemand, Name = "FullTrust")]`) and documentation, you'll require `FullTrust`. As for why it fails when `UseShellExecute = false`, note that this differentiates whether your code is run by `ShellExecute` or `CreateProcess`; `ShellExecute` will give you a completely different security chain and set of fixups. For more information, I recommend seeing [here](http://stackoverflow.com/questions/5255086/when-do-we-need-to-set-useshellexecute-to-true) and walking your current code in a debugger. – MrGomez Mar 28 '12 at 19:37
  • I see; I don't think I read it correctly the first time. Unfortunately, I've tried decorating both the method and the class with that attribute, but the behavior is unchanged. – Adam Robinson Mar 28 '12 at 19:42
  • @AdamRobinson No worries. What happens when you try this imperatively, a la `CustomPermission MyPermission = new CustomPermission(PermissionState.Unrestricted); MyPermission.Demand();`? – MrGomez Mar 28 '12 at 19:46
  • @AdamRobinson Oh well. Open mouth, insert foot. One last thing to try, _because you will require administrator privileges to use the desired user and password credentials due to the FullTrust demand_, is to include `requestedExecutionLevel` in your manifest file [as here](http://www.aneef.net/2009/06/29/request-uac-elevation-for-net-application-managed-code/), with the security demand in place as I described above. I cannot find any other applicable advice. – MrGomez Mar 28 '12 at 20:06
  • From MSDN: _You cannot elevate an already running process. Thus, you should refactor your app to be separated into admin & non-admin operations - running the default application with normal privileges and starting another elevated process for each administrative operation._ From [here](http://blogs.msdn.com/b/knom/archive/2007/06/11/vista-user-account-control-with-net.aspx). – MrGomez Mar 28 '12 at 20:12
  • @AdamRobinson Therefore, I would presume that the correct way to refactor your application is to define your separate process with a separate manifest file that requires escalation (for example, by placing it in a separate executable file). When invoked, this will trigger the prompt exactly as you requested, allowing you to proceed with your custom user's credentials normally. – MrGomez Mar 28 '12 at 20:32
  • That's exactly what I'm trying to do. I have a *non-elevated process* that is attempting to launch an *elevated process* (the normal and recommended way of doing these things). The only difference is that I want to launch the elevated process *under a different account*. – Adam Robinson Mar 29 '12 at 01:27
  • @AdamRobinson I understand, though I apparently failed to read your initial errata and misunderstood your intent. [By design](http://blogs.msdn.com/b/oldnewthing/archive/2010/04/05/9990371.aspx), there is no stock-standard method of letting an unprivileged user escalate to a privileged one without going through administrator. You can daemonize your code running as your target user and call into that, or you can abuse _ShellExecute_ to meet your needs, but the facilities aren't there precisely because there's very little stopping you from going to town once you have those credentials. – MrGomez Mar 29 '12 at 02:17
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/9441/discussion-between-mrgomez-and-adam-robinson) – MrGomez Mar 29 '12 at 02:57
1

If you're authoring a Windows Installer (MSI) application, and updating it using MSPs, then Windows Installer has built-in support for exactly your scenario: - check out User Account Control (UAC) Patching.

It works basically like this:

  • When you author the original MSI, You generate a certificate, and you put its public key (or something like that) in the MSI.
  • The target machine's admin installs the MSI on the machine.
  • You author an update (MSP), and sign it with the certificate.
  • Any user on the target machine can now install the update - Windows Installer will validate the certificate against the public key in the original MSI, and agree to install if so. I don't think you'll get a UAC prompt at all, though I'm not sure.
Jonathan
  • 6,939
  • 4
  • 44
  • 61
  • Interesting; I may have to investigate this. It doesn't quite mesh with my current approach (the current stub application fully uninstalls the existing version and installs a fresh copy of the new version for various unrelated reasons), but it might be something I'll look at if I can't find another way around this. My current plan is to use an updater service that can kick off the installation unattended and report back to the user via some sort of IPC. – Adam Robinson Apr 04 '12 at 13:30
0

According to the MSDN documentation:

When UseShellExecute is false, you can start only executables by using the Process object.

I noticed your declaration var proc = Process.Start(info); is not using Process as the class type.

Also make sure parameter path is the fully qualified path to the executable. For instance, "c:\\directory\\contains\\process_to_be_started\\executable.exe"

According to the MSDN documentation this is important:

The WorkingDirectory property must be set if UserName and Password are provided. If the property is not set, the default working directory is %SYSTEMROOT%\system32.

I would try below code to run target process with elevated privileges (with admin rights).

ProcessStartInfo info = new ProcessStartInfo(path);

info.UseShellExecute = false;
info.UserName = username;
info.Password = securePwd;
info.Domain = "MyDomain";
info.Verb = "runas";
info.WorkingDirectory = "c:\\directory\\contains\\process_to_be_started"

'var proc = Process.Start(info);

Process proc = Process.Start(info);
Only You
  • 2,051
  • 1
  • 21
  • 34
  • 4
    It doesn't matter if you declare the variable with "var" or with "Process", it's no difference – Onkelborg Apr 04 '12 at 06:20
  • @Onkelborg Is correct; `var` is simply implicitly typing the variable. It's compiled as if I'd declared it as `Process`. That aside, I am already fully qualifying the path. – Adam Robinson Apr 04 '12 at 13:28
0

The ProcessStartInfo.Verb="runas" is for only windows Vista and higher, so you should ask for the system level, and not do the elevation for XP.

I think if you choose ProcessStartInfo.Verb="runas", you should not specify user name and password.

If UAC is of, then it suppose to succeed anyway, it shouldn't be a problem.

sara
  • 3,824
  • 9
  • 43
  • 71
  • Sorry, no. You're correct that it's only for Vista and above and I'll have to test for that, but our current test machine is Windows 7. With UAC off, using `"runas"` simply causes the program to fail when run as a non-administrator. With UAC on, it prompts for administrator credentials. – Adam Robinson Apr 04 '12 at 15:04
0

This is still a challenge in Windows 10 in 2022, but using the ideas put forth in Ian Boyd's answer I was able to get it working. Since no other answers here provide a comprehensive accounting of what it takes... I'll include my code below.

Based off Ian's answer, I understood that it is not possible to BOTH open a process under another user AND elevate that process's privileges... two processes are necessary.

One - to run as another user.

Two - to run with elevated permissions.

The first process is responsible for running the 2nd process as another user. The 2nd process tries to elevate itself - the key being, it does so under the context it's already running... i.e. the user it's being run as from the first process.

First process:

static async Task Main(string[] args)
{
    try
    {
        StartupInfo startupInfo = new StartupInfo();
        startupInfo.reserved = null;
        startupInfo.flags &= Startf_UseStdHandles;
        startupInfo.stdOutput = (IntPtr)StdOutputHandle;
        startupInfo.stdError = (IntPtr)StdErrorHandle;

        UInt32 exitCode = 123456;
        ProcessInformation processInfo = new ProcessInformation();
        String command = @"RunStep2.exe";
        String user = "usernameGoesHere";
        String domain = "domainGoesHere";
        String password = "passwordGoesHere";
        String currentDirectory = System.IO.Directory.GetCurrentDirectory();

        try
        {
            CreateProcessWithLogonW(
                user,
                domain,
                password,
                (UInt32)1,
                null,
                command,
                (UInt32)0,
                (UInt32)0,
                currentDirectory,
                ref startupInfo,
                out processInfo);
            }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }

        Console.WriteLine("Running ...");
        WaitForSingleObject(processInfo.process, Infinite);
        GetExitCodeProcess(processInfo.process, ref exitCode);

        Console.WriteLine("Exit code: {0}", exitCode);

        CloseHandle(processInfo.process);
        CloseHandle(processInfo.thread);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        Console.WriteLine(ex.ToString());
        Console.ReadLine();
    }
}


public const UInt32 Infinite = 0xffffffff;
public const Int32 Startf_UseStdHandles = 0x00000100;
public const Int32 StdOutputHandle = -11;
public const Int32 StdErrorHandle = -12;

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct StartupInfo
{
    public int cb;
    public String reserved;
    public String desktop;
    public String title;
    public int x;
    public int y;
    public int xSize;
    public int ySize;
    public int xCountChars;
    public int yCountChars;
    public int fillAttribute;
    public int flags;
    public UInt16 showWindow;
    public UInt16 reserved2;
    public byte reserved3;
    public IntPtr stdInput;
    public IntPtr stdOutput;
    public IntPtr stdError;
}

public struct ProcessInformation
{
    public IntPtr process;
    public IntPtr thread;
    public int processId;
    public int threadId;
}

[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateProcessWithLogonW(
    String userName,
    String domain,
    String password,
    UInt32 logonFlags,
    String applicationName,
    String commandLine,
    UInt32 creationFlags,
    UInt32 environment,
    String currentDirectory,
    ref StartupInfo startupInfo,
    out ProcessInformation processInformation);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetExitCodeProcess(IntPtr process, ref UInt32 exitCode);

[DllImport("Kernel32.dll", SetLastError = true)]
public static extern UInt32 WaitForSingleObject(IntPtr handle, UInt32 milliseconds);

[DllImport("Kernel32.dll", SetLastError = true)]
public static extern IntPtr GetStdHandle(IntPtr handle);

[DllImport("Kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr handle);

And then the 2nd process is:

private static bool IsAdministrator()
{
    WindowsIdentity identity = WindowsIdentity.GetCurrent();
    WindowsPrincipal principal = new WindowsPrincipal(identity);
    return principal.IsInRole(WindowsBuiltInRole.Administrator);
}

static void Main(string[] args)
{
    if (IsAdministrator() == false)
    {
        // Restart program and run as admin
        // Under the context of the same user this is being run under right now
        // But this time, with elevated administrator privileges 
        var exeName = Process.GetCurrentProcess().MainModule.FileName;
        ProcessStartInfo startInfo222 = new ProcessStartInfo(exeName);
        startInfo222.Verb = "runas";
        Process.Start(startInfo222);
        Environment.Exit(123);
        return;
    }

    ProcessStartInfo startInfo = new ProcessStartInfo();
    startInfo.UseShellExecute = false;
    startInfo.FileName = "cmd.exe";
    startInfo.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFilesX86);
    startInfo.Verb = "runas";
    startInfo.Arguments = "/C COMMAND-GOES-HERE";
    startInfo.ErrorDialog = true;

    Process process = new Process();
    process.StartInfo = startInfo;
    process.Start();

    process.WaitForExit();

    Console.WriteLine("process.ExitCode: " + process.ExitCode);
    Console.ReadLine();
}
JeremyW
  • 5,157
  • 6
  • 29
  • 30