Bassie
Bassie

Reputation: 10390

Hiding the Console Window When Running Process as a User with DllImports

Update

Error message with below attempt:

I am seeing this error output from TEST.EXE (retrieved with 2> in CMD line):

ERROR: Logon failure: unknown user name or bad password. 

But not seeing any error from the process which is calling the EXE

Tried, as per the below suggestions (I think) to create a process which calls the process which I need:

    public static void getProc()
    {
        SecureString ss = CreatePW();

        ProcessStartInfo startInfo = new ProcessStartInfo("cmd", "\"/C cd C:\\users\\user.name\\desktop & TEST\"")
        {
            WindowStyle = ProcessWindowStyle.Hidden,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            CreateNoWindow = true,

            WorkingDirectory = @"C:\windows\system32",
            Verb = "runas",
            Domain = "DOMAIN",
            UserName = "zzkillcitrix",
            Password = ss
        };

        string asd = "";
        Process proc = Process.Start(startInfo);

        proc.OutputDataReceived += (x, y) => asd += y.Data;
        proc.BeginOutputReadLine();
        proc.WaitForExit();
    }

Where TEST.EXE is a build of the following(sends the argument to CMD):

    public static void getProc()
    {
        SecureString ss = CreatePW();

        ProcessStartInfo startInfo = new ProcessStartInfo("cmd", "Command_which_requires_user_authentication_goes_here")
        {
            WindowStyle = ProcessWindowStyle.Hidden,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            CreateNoWindow = true,
        };

        Process proc = Process.Start(startInfo);

        proc.OutputDataReceived += (x, y) => asd += y.Data;
        proc.BeginOutputReadLine();
        proc.WaitForExit();
    }

When running TEST.exe by double clicking, I am still seeing the CMD window. When running the above (top method), TEST.exe doesn't appear to be running, as I am not seeing the messagebox windows.

This is probably because the user credentials are not being passed to the EXE file.


I have a process which I call using the following ProcessStartInfo details:

ProcessStartInfo startInfo = new ProcessStartInfo("cmd", "/C tasklist /S " + server + " /FI \"SESSION eq " + sessID + "\" /FO CSV /NH")
{
    WindowStyle = ProcessWindowStyle.Minimized,
    UseShellExecute = false,
    RedirectStandardOutput = true,
    RedirectStandardError = true,
    CreateNoWindow = true,

    WorkingDirectory = @"C:\windows\system32",
    Verb = "runas",
    Domain = "DOMAIN",
    UserName = "zzkillcitrix",
    Password = ss,
}

From MSDN I know that when running a Process such as this as a user, the CreateNoWindow and WindowStyle properties do not work. This includes setting WindowStyle = ProcessWindowStyle.Hidden.

I am therefore trying to achieve a Console-less Process.Start by declaring the following:

[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();

[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

const int SW_HIDE = 0;
const int SW_SHOW = 5;

And then calling proc like so:

Process proc = Process.Start(startInfo);

var handle = proc.MainWindowHandle;
ShowWindow(handle, SW_HIDE);

proc.OutputDataReceived += (x, y) => procList.Add(y.Data);
proc.BeginOutputReadLine();
proc.WaitForExit();

When the process runs, a console window still flashes up briefly. Am I trying to hide a Window Handle where this isn't being recognised as a Console Window?

I have also tried calling the ShowWindow method, generally, at the beginning of the Program, but the console window still appears.

Would appreciate any guidance on this at all.

Upvotes: 1

Views: 1562

Answers (2)

Bassie
Bassie

Reputation: 10390

The only way of doing this I could find that actually worked is by running the commands via a remote PowerShell session as a user who has full domain access:

static class Ps
{
    private const string RemoteHost = "ADMINSERVER.DOMAIN1.co.uk";
    private static string _errors;

    /// <summary>
    /// Executes the given command over a remote powershell session
    /// </summary>
    /// <param name="args"></param>
    /// <returns></returns>
    public static Collection<PSObject> RemoteCommand(string args)
    {
        SupportMi.Trace = $"Remote Command({args}) {{";

        var script = BuildScript(args);
        var results = Execute(script);

        SupportMi.Trace = $"RES: {results[0]} ERR: {_errors} }}";
        return results;
    }

    /// <summary>
    /// Takes a complete script and executes it over a powershell Runspace. In this case it is being
    /// sent to the server, and the results of the execution are checked for any errors. 
    /// </summary>
    /// <param name="script"></param>
    /// <returns></returns>
    private static Collection<PSObject> Execute(string script)
    {
        var results = new Collection<PSObject>();

        // Using a runspace
        using (var runspace = RunspaceFactory.CreateRunspace())
        {
            runspace.Open();

            using (var pipeline = runspace.CreatePipeline())
            {
                pipeline.Commands.AddScript(script);

                try
                {
                    results = pipeline.Invoke();
                    var errors = pipeline.Error.Read(pipeline.Error.Count);
                    foreach (var error in errors)
                    {
                        _errors += error;
                    }
                }
                catch (Exception ex)
                {
                    results.Add(new PSObject(ex.Message));
                    SupportMi.Trace = ex.Message;
                }
            }
        }

        return results;
    }

    /// <summary>
    /// Takes a string argument to be sent to the remote powershell session and arranges it in the correct format,
    /// ready to be sent to the server.
    /// </summary>
    /// <param name="args"></param>
    /// <returns></returns>
    private static string BuildScript(string args)
    {
        // Build the script
        var script = Creds() + Environment.NewLine +
                    $"Invoke-Command -session $sessions -ScriptBlock {{ {args} /U DOMAIN1\\adminusername /P adminpassword }}" + Environment.NewLine +
                    "Remove-PSSession -Session $sessions" + Environment.NewLine +
                    "exit";

        return script;
    }

    /// <summary>
    /// Returns the credentials for a remote PowerShell session in the form 
    /// of a few lines of PowerShell script. 
    /// </summary>
    /// <returns></returns>
    private static string Creds()
    {
        return "$pw = convertto-securestring -AsPlainText -Force -String \"adminpassword\"" + Environment.NewLine +
               "$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist \"DOMAIN1\\adminusername\",$pw" + Environment.NewLine +
               $"$sessions = New-PSSession -ComputerName {RemoteHost} -credential $cred";
    }
}

So to maintain a console-less action we can call the RemoteCommand method and pass in the command we want to send. RemoteCommand will first build our query/command using BuildScript and Creds (for the admin credentials).

This is then sent to the Execute method, which creates a new PowerShell RunSpace, which can run the command.

Some caveats of using this method :

  • The app must be run on the domain on which the "admin" server exists
  • There must exist an admin account which has access to any machine which wishes to run this code as well as the server (in this case, ADMINSERVER)
  • Any errors will exist on the remote server, so we must take care to handle these properly so that they are passed back into our application

Upvotes: 0

EylM
EylM

Reputation: 6103

Change

WindowStyle = ProcessWindowStyle.Minimized

To

WindowStyle = ProcessWindowStyle.Hidden;

Upvotes: 1

Related Questions