6

I'm calling a self-elevating powershell script from C# code. The Script resets DNS Settings. The script works fine when called from unelevated powershell, but takes no effect when called from C# code with no exceptions thrown. My Execution policy is temporarily set on unrestricted and I'm running Visual Studio as Admin.

Does anyone know what's wrong?

The C#:

    class Program
{
    static void Main(string[] args)
    {

        var pathToScript = @"C:\Temp\test.ps1";
        Execute(pathToScript);

        Console.ReadKey();


    }
    public static void Execute(string command)
    {
        using (var ps = PowerShell.Create())
        {
            var results = ps.AddScript(command).Invoke();
            foreach (var result in results)
            {
                Console.WriteLine(result.ToString());
            }
        }
    }


}

The script:

# Get the ID and security principal of the current user account
$myWindowsID = [System.Security.Principal.WindowsIdentity]::GetCurrent();
$myWindowsPrincipal = New-Object System.Security.Principal.WindowsPrincipal($myWindowsID);

# Get the security principal for the administrator role
$adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator;

# Check to see if we are currently running as an administrator
if ($myWindowsPrincipal.IsInRole($adminRole))
{
    # We are running as an administrator, so change the title and background colour to indicate this
    $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)";
    $Host.UI.RawUI.BackgroundColor = "DarkBlue";
    Clear-Host;
}
else {
    # We are not running as an administrator, so relaunch as administrator

    # Create a new process object that starts PowerShell
    $newProcess = New-Object System.Diagnostics.ProcessStartInfo "PowerShell";

    # Specify the current script path and name as a parameter with added scope and support for scripts with spaces in it's path
    $newProcess.Arguments = "& '" + $script:MyInvocation.MyCommand.Path + "'"

    # Indicate that the process should be elevated
    $newProcess.Verb = "runas";

    # Start the new process
    [System.Diagnostics.Process]::Start($newProcess);

    # Exit from the current, unelevated, process
    Exit;
}

# Run your code that needs to be elevated here...
Set-DnsClientServerAddress -InterfaceIndex 9 -ResetServerAddresses
Olaf Dlugosz
  • 81
  • 1
  • 4
  • VS doesn't automatically run AS ADMIN. The code will run from .exe file correctly. To run from VS, create a shortcut to VS. Then right click shortcut and select RUN AS ADMIN. – jdweng Apr 10 '20 at 10:44
  • I'm running as admin already. Sorry forgot to mention – Olaf Dlugosz Apr 10 '20 at 11:02
  • If you are connecting to a remote machine you need admin on both local and remote. Then any paths you need the dollar sign like //myserver/c$ – jdweng Apr 10 '20 at 11:40
  • No remote machine, all of this is done locally. – Olaf Dlugosz Apr 10 '20 at 12:13
  • 1
    The communication between the c# and Power shell are through streams (see : https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershellstreamtype?view=pscore-6.2.0). How do you plan to do the communications? How are you planning to wait in c# for PowerShell to complete? See : https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell?view=pscore-6.2.0 – jdweng Apr 10 '20 at 12:33
  • 1
    I just got this to work by adding "powershell -ExecutionPolicy Bypass -File" before the script. Thank you anyways! – Olaf Dlugosz Apr 10 '20 at 13:27
  • Just an aside: `;` is only ever necessary in PowerShell code if you place _multiple_ commands on the same line. – mklement0 Apr 10 '20 at 14:22

1 Answers1

5

As you've just determined yourself, the primary problem was that script execution was disabled on your system, necessitating (at least) a process-level change of PowerShell's execution policy, as the following C# code demonstrates, which calls
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass before invoking the script file (*.ps1):

  • For an alternative approach that uses the initial session state to set the per-process execution policy, see this answer.

  • The approach below can in principle be used to persistently change the execution policy for the current user, namely by replacing .AddParameter("Scope", "Process") with .AddParameter("Scope", "CurrentUser")

    • Caveat: When using a PowerShell (Core) 7+ SDK, persistent changes to the local machine's policy (.AddParameter("Scope", "LocalMachine")) - which require running with elevation (as admin) - are seen by that SDK project only; see this answer for details.

Caveat: If the current user's / machine's execution policy is controlled by a GPO (Group Policy Object), it can NOT be overridden programmatically - neither per process, nor persistently (except via GPO changes).

  class Program
  {
    static void Main(string[] args)
    {

      var pathToScript = @"C:\Temp\test.ps1";
      Execute(pathToScript);

      Console.ReadKey();

    }

    public static void Execute(string command)
    {
      using (var ps = PowerShell.Create())
      {

        // Make sure that script execution is enabled at least for 
        // the current process.
        // For extra safety, you could try to save and restore
        // the policy previously in effect after executing your script.
        ps.AddCommand("Set-ExecutionPolicy")
          .AddParameter("Scope", "Process")
          .AddParameter("ExecutionPolicy", "Bypass")
          .Invoke();

        // Now invoke the script and print its success output.
        // Note: Use .AddCommand() (rather than .AddScript()) even
        //       for script *files*.
        //       .AddScript() is meant for *strings 
        //       containing PowerShell statements*.
        var results = ps.AddCommand(command).Invoke();
        foreach (var result in results)
        {
          Console.WriteLine(result.ToString());
        }

        // Also report non-terminating errors, if any.
        foreach (var error in ps.Streams.Error)
        {
          Console.Error.WriteLine("ERROR: " + error.ToString());
        }

      }
    }

  }

Note that the code also reports any non-terminating errors that the script may have reported, via stderr (the standard error output stream).

Without the Set-ExecutionPolicy call, if the execution policy didn't permit (unsigned) script execution, PowerShell would report a non-terminating error via its error stream (.Streams.Error) rather than throw an exception.

If you had checked .Streams.Error to begin with, you would have discovered the specific cause of your problem sooner.

Therefore:

  • When using the PowerShell SDK, in addition to relying on / catching exceptions, you must examine .Streams.Error to determine if (at least formally less severe) errors occurred.

Potential issues with your PowerShell script:

  • You're not waiting for the elevated process to terminate before returning from your PowerShell script.

  • You're not capturing the elevated process' output, which you'd have to via the .RedirectStandardInput and .RedirectStandardError properties of the System.Diagnostics.ProcessStartInfo instance, and then make your script output the results.

  • See this answer for how to do that.

The following, streamlined version of your code addresses the first point, and invokes the powershell.exe CLI via -ExecutionPolicy Bypass too.

  • If you're using the Windows PowerShell SDK, this shouldn't be necessary (because the execution policy was already changed in the C# code), but it could be if you're using the PowerShell [Core] SDK, given that the two PowerShell editions have separate execution-policy settings.
# Check to see if we are currently running as an administrator
$isElevated = & { net session *>$null; $LASTEXITCODE -eq 0 }
if ($isElevated)
{
    # We are running as an administrator, so change the title and background color to indicate this
    $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)"
    $Host.UI.RawUI.BackgroundColor = "DarkBlue"
    Clear-Host
}
else {
    # We are not running as an administrator, so relaunch as administrator

    # Create a new process object that starts PowerShell
    $psi = New-Object System.Diagnostics.ProcessStartInfo 'powershell.exe'

    # Specify the current script path and name as a parameter with and support for scripts with spaces in its path
    $psi.Arguments = '-ExecutionPolicy Bypass -File "{0}"' -f 
                     $script:MyInvocation.MyCommand.Path

    # Indicate that the process should be elevated.
    $psi.Verb = 'RunAs'
    # !! For .Verb to be honored, .UseShellExecute must be $true
    # !! In .NET Framework, .UseShellExecute *defaults* to $true,
    # !! but no longer in .NET Core.
    $psi.UseShellExecute = $true 

    # Start the new process, wait for it to terminate, then
    # exit from the current, unelevated process, passing the exit code through.
    exit $(
     try { ([System.Diagnostics.Process]::Start($psi).WaitForExit()) } catch { Throw }
    )

}

# Run your code that needs to be elevated here...
Set-DnsClientServerAddress -InterfaceIndex 9 -ResetServerAddresses
mklement0
  • 382,024
  • 64
  • 607
  • 775