0

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 ??

mrid
  • 5,782
  • 5
  • 28
  • 71
  • 1
    First of all: _"I don't have a problem with the user performing the task from cmd...I just don't want him/her to...know what parameters to pass"_. **Huh?** Isn't not wanting them to know the parameters the same as not wanting them to be able to do it at all? Anyway, your question is too broad...phrases like "any way" and "better approach" are almost always invitations for a wide range of answers, the definition of a question that's too broad. That said, I think you should explore simply elevating the _current_ process instead of starting your program again. – Peter Duniho Jan 06 '18 at 06:23
  • @PeterDuniho sorry I've updated my question to remove that line. I just need an approach, why is it too broad ??? My program is supposed to run in offices where users most probably won't have admin access, that's why I don't want to elevate the program itself – mrid Jan 06 '18 at 06:25
  • [_"if your question could be answered by an entire book, or has many valid answers (but no way to determine which - if any - are correct), then it is probably too broad for our format"_](https://stackoverflow.com/help/closed-questions) – Peter Duniho Jan 06 '18 at 06:29
  • _"My program is supposed to run in offices where users most probably won't have admin access, that's why I don't want to elevate the program itself"_ -- seems like a non-sequitur to me. First of all, according to your description, when you use `runas` the user still gets the UAC prompt...if they don't have admin access, how is that useful? If your answer is that you've embedded the user and password in the program, **a)** that's a huge security hole, and **b)** I'm not talking about having the program run elevated initially, but rather to impersonate admin just for the operation. – Peter Duniho Jan 06 '18 at 06:31
  • I don't see the point of hiding the parameters. Seems like a waste of effort. –  Jan 06 '18 at 06:32
  • @PeterDuniho As an example, all users should be able to run the program, but only the admin should be able to change the password. Even a link that could help would suffice – mrid Jan 06 '18 at 06:33
  • What you need to do is assign roles to users. So when user logs into the application based on the role user will have buttons/menus etc visible or not visible in the UI. User with admin role will be able to see Change password button and Normal user will see that button. – Chetan Jan 06 '18 at 07:55

2 Answers2

5

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 ... :(

rene
  • 41,474
  • 78
  • 114
  • 152
1

Why you need to start another instance of the program to start with Admin privileges. Make changes to your program so that you can run with Admin privileges the first instance itself.

Modify the manifest that gets embedded in the program. On Visual Studio 2008 (or higher), go to: Project > Add New Item > select "Application Manifest File".

Change the <requestedExecutionLevel> element to:

<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />

This will prompt for UAC (or Admin privileges).

Sunil
  • 3,404
  • 10
  • 23
  • 31