ALEX
ALEX

Reputation: 201

Starting GUI programs via OpenSSH on Windows?

I'm trying to execute a labview VI, launching that from a .bat file, called via ssh, from another Windows machine. So I do

 ssh myuser@IP
 cd  Desktop
 launchVis.bat

I connect with user and password. myuser have all the rights to launch the batch file. Meanwhile I check the execution via RDP connection on the same machine.

If I run the bat file from a cmd line on the remote machine, the VI starts normally If I run the bat file from the ssh connection, i can see the output of echoes in the bat file but LabVIEW will be launched in a different session from RDP-TCP#1. The result is that I can see a "LabVIEW" process started in a Session named Services, but I cannot see the VI executing and in general, I don't know IF is executing or not.

Googling about the problem, It seems that I cannot avoid to start processes in "Services" Session and, for this reason, I cannot launch any GUI Program via SSH. Suggested solutions are using PsExec or, maybe a third part of ssh Server (with a third part ssh Server I reached my goal over Windows 7)

Upvotes: 11

Views: 22008

Answers (5)

Aviad P.
Aviad P.

Reputation: 32659

The best way I've found is creating a scheduled task and invoking it, see https://serverfault.com/a/1160137/30441

Including the actual answer from that link here (credit to Kristian Dimitrov)

You can do that. Let's say you are connected to a remote machine via ssh with user 'office' and you want to run Mozzila Firefox. You have to first create a scheduled task:

$User = "office"
$Firefox = New-ScheduledTaskAction -Execute "C:\Program Files\Mozilla Firefox\firefox.exe"
Register-ScheduledTask -TaskName "Firefox" -User $User -Action $Firefox

Then you can execute it by running the following command:

Start-ScheduledTask -TaskName "Firefox"

Upvotes: 0

I solved this problem with these two scripts and the PsExeс tool. Just download the tool and place it with the powershell and batch scripts in the user folder. And after that you will be able to run GUI applications directly through SSH by calling RunGUIAppFromSSH.bat your_program parameters.

Note that for this solution, you don't need to run the SSH server as a user (it will work with the standard OpenSSH server installed in the usual way).

StartProcessAsCurrentUser.ps1

param(
    [Parameter(Mandatory=$true)][string]$Program,
    [string]$cmdline = ''
)

$Source = @"
using System;
using System.Runtime.InteropServices;

namespace murrayju.ProcessExtensions
{
public static class ProcessExtensions
{
    #region Win32 Constants

    private const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
    private const int CREATE_NO_WINDOW = 0x08000000;

    private const int CREATE_NEW_CONSOLE = 0x00000010;

    private const uint INVALID_SESSION_ID = 0xFFFFFFFF;
    private static readonly IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;

    #endregion

    #region DllImports
    
    const string SE_TCB_NAME = "SeTcbPrivilege";
    const int SE_PRIVILEGE_ENABLED = 0x00000002;
    const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
    
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    struct TokPriv1Luid
    {
        public int Count;
        public long Luid;
        public int Attr;
    }
    
    [DllImport("Kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern Int32 GetLastError();

    [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
    private static extern bool CreateProcessAsUser(
        IntPtr hToken,
        String lpApplicationName,
        String lpCommandLine,
        IntPtr lpProcessAttributes,
        IntPtr lpThreadAttributes,
        bool bInheritHandle,
        uint dwCreationFlags,
        IntPtr lpEnvironment,
        String lpCurrentDirectory,
        ref STARTUPINFO lpStartupInfo,
        out PROCESS_INFORMATION lpProcessInformation);

    [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
    private static extern bool DuplicateTokenEx(
        IntPtr ExistingTokenHandle,
        uint dwDesiredAccess,
        IntPtr lpThreadAttributes,
        int TokenType,
        int ImpersonationLevel,
        ref IntPtr DuplicateTokenHandle);

    [DllImport("userenv.dll", SetLastError = true)]
    private static extern bool CreateEnvironmentBlock(ref IntPtr lpEnvironment, IntPtr hToken, bool bInherit);

    [DllImport("userenv.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr hSnapshot);
    
    [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
    internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);

    [DllImport("kernel32.dll")]
    private static extern uint WTSGetActiveConsoleSessionId();

    [DllImport("Wtsapi32.dll")]
    private static extern uint WTSQueryUserToken(uint SessionId, ref IntPtr phToken);

    [DllImport("wtsapi32.dll", SetLastError = true)]
    private static extern int WTSEnumerateSessions(
        IntPtr hServer,
        int Reserved,
        int Version,
        ref IntPtr ppSessionInfo,
        ref int pCount);
        
    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);
        
    [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
    private static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);

    #endregion

    #region Win32 Structs

    private enum SW
    {
        SW_HIDE = 0,
        SW_SHOWNORMAL = 1,
        SW_NORMAL = 1,
        SW_SHOWMINIMIZED = 2,
        SW_SHOWMAXIMIZED = 3,
        SW_MAXIMIZE = 3,
        SW_SHOWNOACTIVATE = 4,
        SW_SHOW = 5,
        SW_MINIMIZE = 6,
        SW_SHOWMINNOACTIVE = 7,
        SW_SHOWNA = 8,
        SW_RESTORE = 9,
        SW_SHOWDEFAULT = 10,
        SW_MAX = 10
    }

    private enum WTS_CONNECTSTATE_CLASS
    {
        WTSActive,
        WTSConnected,
        WTSConnectQuery,
        WTSShadow,
        WTSDisconnected,
        WTSIdle,
        WTSListen,
        WTSReset,
        WTSDown,
        WTSInit
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public uint dwProcessId;
        public uint dwThreadId;
    }

    private enum SECURITY_IMPERSONATION_LEVEL
    {
        SecurityAnonymous = 0,
        SecurityIdentification = 1,
        SecurityImpersonation = 2,
        SecurityDelegation = 3,
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct STARTUPINFO
    {
        public int cb;
        public String lpReserved;
        public String lpDesktop;
        public String lpTitle;
        public uint dwX;
        public uint dwY;
        public uint dwXSize;
        public uint dwYSize;
        public uint dwXCountChars;
        public uint dwYCountChars;
        public uint dwFillAttribute;
        public uint dwFlags;
        public short wShowWindow;
        public short cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }

    private enum TOKEN_TYPE
    {
        TokenPrimary = 1,
        TokenImpersonation = 2
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct WTS_SESSION_INFO
    {
        public readonly UInt32 SessionID;

        [MarshalAs(UnmanagedType.LPStr)]
        public readonly String pWinStationName;

        public readonly WTS_CONNECTSTATE_CLASS State;
    }

    #endregion

    // Gets the user token from the currently active session
    private static bool GetSessionUserToken(ref IntPtr phUserToken)
    {
        var bResult = false;
        var hImpersonationToken = IntPtr.Zero;
        var activeSessionId = INVALID_SESSION_ID;
        var pSessionInfo = IntPtr.Zero;
        var sessionCount = 0;

        // Get a handle to the user access token for the current active session.
        if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, ref pSessionInfo, ref sessionCount) != 0)
        {
            var arrayElementSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
            var current = pSessionInfo;

            for (var i = 0; i < sessionCount; i++)
            {
                var si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO));
                current += arrayElementSize;

                if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive)
                {
                    activeSessionId = si.SessionID;
                }
            }
        }

        // If enumerating did not work, fall back to the old method
        if (activeSessionId == INVALID_SESSION_ID)
        {
            activeSessionId = WTSGetActiveConsoleSessionId();
        }

        if (WTSQueryUserToken(activeSessionId, ref hImpersonationToken) != 0)
        {
            // Convert the impersonation token to a primary token
            bResult = DuplicateTokenEx(hImpersonationToken, 0, IntPtr.Zero,
                (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, (int)TOKEN_TYPE.TokenPrimary,
                ref phUserToken);

            CloseHandle(hImpersonationToken);
        }

        return bResult;
    }

    public static bool StartProcessAsCurrentUser(string appPath, string cmdLine = null, string workDir = null, bool visible = true)
    {
        var hUserToken = IntPtr.Zero;
        var startInfo = new STARTUPINFO();
        var procInfo = new PROCESS_INFORMATION();
        var pEnv = IntPtr.Zero;
        int iResultOfCreateProcessAsUser;
        IntPtr LoggedInUserToken = IntPtr.Zero;

        startInfo.cb = Marshal.SizeOf(typeof(STARTUPINFO));

        try
        {
            if (!GetSessionUserToken(ref hUserToken))
            {
                throw new Exception("StartProcessAsCurrentUser: GetSessionUserToken failed. ErrCode: " + GetLastError().ToString());
            }

            uint dwCreationFlags = CREATE_UNICODE_ENVIRONMENT | (uint)(visible ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW);
            startInfo.wShowWindow = (short)(visible ? SW.SW_SHOW : SW.SW_HIDE);
            startInfo.lpDesktop = "winsta0\\default";

            if (!CreateEnvironmentBlock(ref pEnv, hUserToken, false))
            {
                throw new Exception("StartProcessAsCurrentUser: CreateEnvironmentBlock failed.");
            }

            if (!CreateProcessAsUser(hUserToken,
                appPath, // Application Name
                cmdLine, // Command Line
                IntPtr.Zero,
                IntPtr.Zero,
                false,
                dwCreationFlags,
                pEnv,
                workDir, // Working directory
                ref startInfo,
                out procInfo))
            {
                throw new Exception("StartProcessAsCurrentUser: CreateProcessAsUser failed.\n");
            }

            iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
        }
        finally
        {
            CloseHandle(hUserToken);
            if (pEnv != IntPtr.Zero)
            {
                DestroyEnvironmentBlock(pEnv);
            }
            CloseHandle(procInfo.hThread);
            CloseHandle(procInfo.hProcess);
        }
        return true;
    }
}
}


"@


Add-Type -ReferencedAssemblies 'System', 'System.Runtime.InteropServices' -TypeDefinition $Source -Language CSharp

[murrayju.ProcessExtensions.ProcessExtensions]::StartProcessAsCurrentUser($Program, $cmdline)

RunGUIAppFromSSH.bat

@echo off
%~dp0\PsExec.exe -sd powershell %~dp0\StartProcessAsCurrentUser.ps1 '%1' '%2'

How it works?

The StartProcessAsCurrentUser.ps1 script calls the special WinAPI function CreateProcessAsUser, which allows us to run any GUI application from the user's perspective. We need to run this script via the PsExec tool because some of the WinAPI functions used require being called from the system context, which is why the -s flag is necessary. Additionally, some functions require the SE_TCB_NAME privilege. I didn’t encounter issues with this, but if you get a 1314 error, I recommend reading more about it.

Upvotes: 0

Daniel Krajnik
Daniel Krajnik

Reputation: 137

Preamble

What helped me solve this issue is learning that running sshd in "interactive mode" is equivalent to running sshd as a regular user (not root). It's not something I've had to ever do on Linux, but once defined this way it's easier to find help online.

There are three issues with running sshd as an unprivileged user:

  1. Can't access ports below 1024.
  2. Can't read ssh host keys (located in /etc/ssh/ssh_host_{ecdsa,ed25519,rsa,dsa}_key{,.pub}
  3. Can't write PID file to ssh_host_ecdsa_key

source

Solution

  1. Host keys

Open cmd prompt in %userprofile%\.ssh

ssh-keygen -q -N "" -t dsa -f ./ssh_host_dsa_key
ssh-keygen -q -N "" -t rsa -b 4096 -f ./ssh_host_rsa_key
ssh-keygen -q -N "" -t ecdsa -f ./ssh_host_ecdsa_key
ssh-keygen -q -N "" -t ed25519 -f ./ssh_host_ed25519_key
  1. Sshd_config

Copy %programdata%\ssh\sshd_config to %userprofile%\ssh

Port <BETWEEN-1024-AND-65535>
HostKey C:\Users\<USER>\.ssh\ssh_host_rsa_key
HostKey C:\Users\<USER>\.ssh\ssh_host_dsa_key
HostKey C:\Users\<USER>\.ssh\ssh_host_ecdsa_key
HostKey C:\Users\<USER>\.ssh\ssh_host_ed25519_key
PidFile C:\Users\<USER>\.ssh\sshd.pid
  1. Open new port in firewall

netsh advfirewall firewall add rule name="Open Port <BETWEEN-1024-AND-65535>" dir=in action=allow protocol=TCP localport=<BETWEEN-1024-AND-65535>

  1. Startup batch script

Create sshd-interactive-mode.bat in the Startup* folder:

start "" C:\Users\user\bin\SilentCMD\SilentCMD.exe "C:\Program Files\OpenSSH\sshd.exe" -f C:\Users\user\.ssh\sshd_config

SilentCMD spawns it as a background task. Download it and save to =%userprofile%\bin\SilentCMD= start "" prevents cmd prompt from lingering on the desktop after sshd exits

Notes

  • I chose to keep everything in user's .ssh directory, but you may choose a separate directory.
  • Startup folder: C:\Users\<USER>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup

Upvotes: 0

ALEX
ALEX

Reputation: 201

The "solution", using a third party ssh server is this:

  1. Uninstall the Optional Feature "OpenSSH Server" of Windows10
  2. Download and install the third party ssh server. I used freeSSHd
  3. DO NOT install this software AS AS SERVICE.
  4. Set users allowed in the Settings of the software.
  5. Put a link to the exe of this software in the shell::startup folder, so it will be run at startup.

In that way, the ssh server will be launched as user and CAN execute GUI programs.

Unfortunately, I didn't manage to achieve the same goal with the Windows official OpenSSH server, because it fail to be launched as User, but seems working only as a Service

Upvotes: 8

domih
domih

Reputation: 1578

Running SSHd as a service makes launching desktop applications difficult, because a service has no access to the user desktop (Windows Station, WinSta0) link

Option 1: Start SSHd as a user

  • This fails with the latest OpenSSH implementation with fork of unprivileged child failed, as running SSHd as a user is no more possible since OpenSSH 7.5 released in 2017 link
  • Use an old implementation or a fork of OpenSSH, e.g. dropbear, FreeSSH or similar

Option 2: Use a launcher that has access to the Windows Station

On Windows, there are 2 options left to launch desktop applications over SSH: link to Github OpenSSH issue

  • schtasks
  • psexec on localhost

Usage:

ssh user@host "psexec -i 1 mydesktoplauncher.bat"

See psexec documentation for optional arguments like -i 1

Upvotes: 12

Related Questions