Search code examples
c#winformsuacelevated-privileges

C# Elevate program at runtime


I have a C# program in which I want certain features to require admin password. To solve this, I started another instance of the application as an elevated process and passed command-line arguments to it, so the process knows what task it has to perform.

Process proc = new Process();
proc.StartInfo.Arguments = "PARAMETERS HERE");
proc.StartInfo.FileName = Application.ExecutablePath;
proc.StartInfo.UseShellExecute = true;
proc.StartInfo.Verb = "runas";
proc.Start();

This is working fine, however I have one small problem. I just noticed that the UAC prompt that pops up to start the new process displays not just the application name and path, but also the command line parameters being passed to it. This way the user can see the parameters being passed and directly pass there arguments from run command or command prompt.

Is there any way to prevent this ? Or a better approach for elevating a running program ??


Solution

  • Instead of providing the arguments on the commandline you can pass them on once the second instance started, for example by having a named pipe between the two instances. To determine if the process that started is the first one, I use a named mutex, largely inspired on What is a good pattern for using a Global Mutex in C#? except that I use a Local mutex here, to have it restricted to a (Terminal) session.

    Main

    Here you see the creation of the Mutex and based on if the Mutex got created or not we know if we're the first or the second instance.

    static string MyPipeName = $"MyApp_{Environment.UserDomainName}_{Environment.UserName}";
    
    static void Main(string[] args)
    {
        bool created; // true if Mutex is created by this process 
        using(var mutex = new Mutex(false, @"Local\" + MyPipeName, out created)) // this needs proper securing
        {
            var gotit = mutex.WaitOne(2000); // take ownership
            if (!created)
            {
                if (gotit) 
                {
                    args = SecondInstance(mutex);
                    Console.WriteLine("I'm the second instance");
                }
                else 
                {
                    // no idea what to do here, log? crash? explode?
                }
            } 
            else 
            {
                FirstInstance(mutex);
                Console.WriteLine("I'm the first instance");
            }
    
            ProgramLoop(args); // our main program, this can be Application.Run for Winforms apps.
        }
    }
    

    FirstInstance

    In the FirstInstance method we setup an delegate that will, when called, start a NamedPipeServerStream, Release the mutex (for extra safeguarding in the second process), Launches itself again and waits for a client to connect on the named pipe. Once done, it sends the arguments and waits for a confirmation. It continues once the Mutex is released.

    static void FirstInstance(Mutex mutex)
    {
        StartSecondInstanceHandler += (args) => 
        {
            using(var srv = new NamedPipeServerStream(MyPipeName)) // this needs proper securing
            {
                mutex.ReleaseMutex();
                // kick off a second instance of this app
                Relaunch();
    
                srv.WaitForConnection();
                using(var sr = new StreamReader(srv))
                {
                    using(var sw = new StreamWriter(srv))    
                    {
                        Trace.WriteLine("Server Started and writing");
                        // send the arguments to the second instance
                        sw.WriteLine(args);
                        sw.Flush();
                        Trace.WriteLine("Server done writing");
                        // the client send back an ack, is not strictly needed
                        Trace.WriteLine("ack: {0}", sr.ReadLine());
                    }
                }
                mutex.WaitOne();
            }
        };
    }
    
    // our public static delegate, accessible by calling
    // Program.StartSecondInstanceHandler("/fubar");
    public static Action<string> StartSecondInstanceHandler = null;
    
    // just launch the app
    static void Relaunch()
    {
        var p = new ProcessStartInfo();
        p.FileName = Environment.CommandLine;
        p.UseShellExecute = true;
        p.Verb = "runas";
        var started = Process.Start(p);
    } 
    

    SecondInstance

    When the second instance is started we setup a NamedPipeClientStream, connect to the server and Read the response and send back a confirmation. The Mutex is released and the arguments are returned (I used a quick hack there by splitting on spaces).

    static string[] SecondInstance(Mutex mutex) 
    {
        string arguments = String.Empty;
        Console.WriteLine("Client NamedPipe starting");
        using(var nps = new NamedPipeClientStream(MyPipeName))
        {
            nps.Connect();  // we expect the server to be running
    
            using(var sr = new StreamReader(nps))
            {
                arguments = sr.ReadLine();
                Console.WriteLine($"received args: {arguments}");
                using(var sw = new StreamWriter(nps))
                {
                    sw.WriteLine("Arguments received!");
                }
            }
            mutex.ReleaseMutex(); // we're done
        }
        return arguments.Split(' '); // quick hack, this breaks when you send /b:"with spaces" /c:foobar
    }
    

    program loop

    To be complete here is a dull program loop

    static void ProgramLoop(string[] args)
    {
        // main loop
        string line;
        while((line = Console.ReadLine()) != String.Empty)
        {
            switch(line)
            {
                case "admin":
                    if (StartSecondInstanceHandler != null)
                    {
                        Console.WriteLine("elevating ...");
                        StartSecondInstanceHandler("/foo:bar /baz:fu");
                        Console.WriteLine("... elevation started");
                    } 
                    else
                    {
                        Console.WriteLine("you are elevated with these arguments: {0}", String.Join(' ',args));
                    }
                break;
                default:
                    Console.WriteLine("you typed '{0}', type 'admin' or leave empty to leave", line);
                break;
            }
        }
    }
    

    Putting it all together ...

    this is what you end up with:

    first and second

    you have to trust me that the UAC prompt didn't contain command arguments ... :(