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 ??
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.
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.
}
}
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);
}
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
}
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;
}
}
}
this is what you end up with:
you have to trust me that the UAC prompt didn't contain command arguments ... :(