简体   繁体   中英

C# Windows Service Creates Process but doesn't executes it

So I have checked many many sites, researched for days now. And I have not found or come up with one of my own solution for this problem.

I know, apparently since Windows Vista, a Windows Service since its created in Session 0 its not able to interact with whats considered GUI executables like console apps and other softwares that are part of other Sessions that are not Session 0.

According to Microsoft, a Service that does this would be a potential 'Virus'. Which I understand the reasoning for their thinking. But this is the only solution for our problems.

//This is how I am calling the process.
public void startVM(string vmname) {

    string cmdline = startvm --type headless VM2000";
    ProcessStartInfo startInfo = new ProcessStartInfo("cmd.exe");
    startInfo.WindowStyle = ProcessWindowStyle.Minimized;
    startInfo.Arguments = string.Format(@"c:\vms\vboxmanage startvm {0}",vmname);
    Process.Start(startInfo);

}

So this is what happens:

I create a Windows Service, this service on startup will start a Process. In this case "cmd.exe". I have checked many times and I am certain that the process is actually created. But the arguments, the actual commands, that I want that cmd.exe to execute...they are being ignored. They just never happen. I tested the code elsewhere, as a library, as a windows form application it is working like clockwork. But yet, as a Service it won't work.

I have tried solutions like enabling to interact with Desktop. Even from Registry Key. I have tried even calling different executables, and it happens the same thing: it creates the process, but it doesn't execute the commands or arguments.

I have read many have had this problem... however no solution have been found by all these sites that I have seen this problem for. Even users from StackOverflow.

    //Located in the service class inheriting from ServiceBase
    protected override void OnStart(string[] args)
    {
        //System.Diagnostics.Debugger.Launch();
        IVBoxCom vBox = new VBoxCom();
        //This method calls the method you see above.
        vBox.StartVM("WIN2K");

    }

This is the Service Installer Class:

        ServiceInstaller installer = new ServiceInstaller();
        installer.ServiceName = "Steven-VBoxService"; //This has to be the exact Name of the Service that has ServiceBase Class
        installer.DisplayName = "Steven-VBoxService";
        installer.StartType = ServiceStartMode.Manual;
        base.Installers.Add(installer);

        //Creates an Executable that convokes the Service previously installed.
        //Note: In theory, I can create 10 Services, and run them in a single Service Process
        ServiceProcessInstaller installer2 = new ServiceProcessInstaller();
        installer2.Account = ServiceAccount.LocalSystem;    //Windows service.
        //installer2.Password = "sh9852"; //Why would I used these options?
        //installer2.Username = @"FITZMALL\hernandezs";
        installer2.Password = null;
        installer2.Username = null;
        base.Installers.Add(installer2);

I have noticed that when I want to start the service, it gets stuck at "Starting", then it just stops. But the Process the cmd.exe or the VBoxManage.exe get created but never actually do anything at all.

So the only alternative to this is to trick the OS. And make an instance of the Process from the Kernel but changing who was the creator. Let me elaborate.

Since Windows Vista and greater...Microsoft thought that having the Windows Service as a Service that can interactive with User GUI was a bad idea(and I agree at some point) because it may be potentially a virus that will run everytime at startup. So they created something called a Session 0. All your services are in this Session so that they are not able to interact with your user(or Session 1 +) GUI. Meaning the Windows Service has no access to cmd.exe, VBoxManage.exe, any other app that has GUI interaction.

So... the solution to the problem is tricking the OS, creating the Process from the Kernel with Platform Invokes(Win 32 API) which is not that common for a day to day developer in C#. When creating the Process from the KernelDLL you have access to change who the User or the Creator is. In this case instead of having the Session 0 creating the Process, I changed it to the current Session ID, or current User. This made it possible for my Windows Service Work like I wanted.

For this idea to work you have to read a lot about KernelDll, advapi32.dll, mostly their methods and enum declarations since its not something you can just reference into your project. Those two need to be P/Invoke in order to use them.

The Following Class that I created makes it possible for you to create a process as the current user and not as Session 0. Hence solving my original problem.

//Just use the Class Method no need to instantiate it:
ApplicationLoader.CreateProcessAsUser(string filename, string args)




[SuppressUnmanagedCodeSecurity]
class ApplicationLoader
{
    /// <summary>
    /// No Need to create the class.
    /// </summary>
    private ApplicationLoader() { }




    enum TOKEN_INFORMATION_CLASS
    {

        TokenUser = 1,
        TokenGroups,
        TokenPrivileges,
        TokenOwner,
        TokenPrimaryGroup,
        TokenDefaultDacl,
        TokenSource,
        TokenType,
        TokenImpersonationLevel,
        TokenStatistics,
        TokenRestrictedSids,
        TokenSessionId,
        TokenGroupsAndPrivileges,
        TokenSessionReference,
        TokenSandBoxInert,
        TokenAuditPolicy,
        TokenOrigin,
        TokenElevationType,
        TokenLinkedToken,
        TokenElevation,
        TokenHasRestrictions,
        TokenAccessInformation,
        TokenVirtualizationAllowed,
        TokenVirtualizationEnabled,
        TokenIntegrityLevel,
        TokenUIAccess,
        TokenMandatoryPolicy,
        TokenLogonSid,
        MaxTokenInfoClass
    }



    [StructLayout(LayoutKind.Sequential)]
    public struct STARTUPINFO
    {
        public Int32 cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        public Int32 dwX;
        public Int32 dwY;
        public Int32 dwXSize;
        public Int32 dwXCountChars;
        public Int32 dwYCountChars;
        public Int32 dwFillAttribute;
        public Int32 dwFlags;
        public Int16 wShowWindow;
        public Int16 cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public Int32 dwProcessID;
        public Int32 dwThreadID;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_ATTRIBUTES
    {
        public Int32 Length;
        public IntPtr lpSecurityDescriptor;
        public bool bInheritHandle;
    }

    public enum SECURITY_IMPERSONATION_LEVEL
    {
        SecurityAnonymous,
        SecurityIdentification,
        SecurityImpersonation,
        SecurityDelegation
    }

    public enum TOKEN_TYPE
    {
        TokenPrimary = 1,
        TokenImpersonation
    }

    public const int GENERIC_ALL_ACCESS = 0x10000000;
    public const int CREATE_NO_WINDOW = 0x08000000;


    [DllImport("advapi32.dll", EntryPoint = "ImpersonateLoggedOnUser", SetLastError = true,
          CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    public static extern IntPtr ImpersonateLoggedOnUser(IntPtr hToken);

    [
       DllImport("kernel32.dll",
          EntryPoint = "CloseHandle", SetLastError = true,
          CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)
    ]
    public static extern bool CloseHandle(IntPtr handle);

    [
       DllImport("advapi32.dll",
          EntryPoint = "CreateProcessAsUser", SetLastError = true,
          CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)
    ]
    public static extern bool
       CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine,
                           ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes,
                           bool bInheritHandle, Int32 dwCreationFlags, IntPtr lpEnvrionment,
                           string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo,
                           ref PROCESS_INFORMATION lpProcessInformation);

    [
       DllImport("advapi32.dll",
          EntryPoint = "DuplicateTokenEx")
    ]
    public static extern bool
       DuplicateTokenEx(IntPtr hExistingToken, Int32 dwDesiredAccess,
                        ref SECURITY_ATTRIBUTES lpThreadAttributes,
                        Int32 ImpersonationLevel, Int32 dwTokenType,
                        ref IntPtr phNewToken);


    [DllImport("Kernel32.dll", SetLastError = true)]
    //[return: MarshalAs(UnmanagedType.U4)]
    public static extern IntPtr WTSGetActiveConsoleSessionId();

    [DllImport("advapi32.dll")]
    public static extern IntPtr SetTokenInformation(IntPtr TokenHandle, IntPtr TokenInformationClass, IntPtr TokenInformation, IntPtr TokenInformationLength);


    [DllImport("wtsapi32.dll", SetLastError = true)]
    public static extern bool WTSQueryUserToken(uint sessionId, out IntPtr Token);

    private static int getCurrentUserSessionID()
    {
        uint dwSessionId = (uint)WTSGetActiveConsoleSessionId();

        //Gets the ID of the User logged in with WinLogOn
        Process[] processes = Process.GetProcessesByName("winlogon");
        foreach (Process p in processes)
        {
            if ((uint)p.SessionId == dwSessionId)
            {

                //this is the process controlled by the same sessionID
                return p.SessionId; 
            }
        }

        return -1;
    }



    /// <summary>
    /// Actually calls and creates the application.
    /// </summary>
    /// <param name="filename"></param>
    /// <param name="args"></param>
    /// <returns></returns>
    public static Process CreateProcessAsUser(string filename, string args)
    {
        //var replaces IntPtr
        var hToken = WindowsIdentity.GetCurrent().Token; //gets Security Token of Current User.


        var hDupedToken = IntPtr.Zero;

        var pi = new PROCESS_INFORMATION();
        var sa = new SECURITY_ATTRIBUTES();
        sa.Length = Marshal.SizeOf(sa);

        try
        {
            if (!DuplicateTokenEx(
                    hToken,
                    GENERIC_ALL_ACCESS,
                    ref sa,
                    (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
                    (int)TOKEN_TYPE.TokenPrimary,
                    ref hDupedToken
                ))
                throw new Win32Exception(Marshal.GetLastWin32Error());




            var si = new STARTUPINFO();
            si.cb = Marshal.SizeOf(si);
            si.lpDesktop = "";

            var path = Path.GetFullPath(filename);
            var dir = Path.GetDirectoryName(path);

            //Testing
            uint curSessionid = (uint)ApplicationLoader.getCurrentUserSessionID();

            if (!WTSQueryUserToken(curSessionid,out hDupedToken))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            // Revert to self to create the entire process; not doing this might
            // require that the currently impersonated user has "Replace a process
            // level token" rights - we only want our service account to need
            // that right.
            using (var ctx = WindowsIdentity.Impersonate(IntPtr.Zero))
            {
                if (!CreateProcessAsUser(
                                        hDupedToken,
                                        path,
                                        string.Format("\"{0}\" {1}", filename.Replace("\"", "\"\""), args),
                                        ref sa, ref sa,
                                        false, CREATE_NO_WINDOW, IntPtr.Zero,
                                        dir, ref si, ref pi
                                ))
                    throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            return Process.GetProcessById(pi.dwProcessID);
        }
        finally
        {
            if (pi.hProcess != IntPtr.Zero)
                CloseHandle(pi.hProcess);
            if (pi.hThread != IntPtr.Zero)
                CloseHandle(pi.hThread);
            if (hDupedToken != IntPtr.Zero)
                CloseHandle(hDupedToken);
        }
    }
}

Modify the class at your will. Just be careful not to touch a lot of the initial enum declarations or the external methods if you have no clue how those work yet.

The problem with your original code (as shown in the question) is very simple: you left out the /c argument to cmd.exe to tell it to run your command.

In other words, you were trying to do this:

cmd c:\vms\vboxmanage startvm {0}

whereas what you needed to do was this:

cmd /c c:\vms\vboxmanage startvm {0}

or this:

c:\vms\vboxmanage startvm {0}

Now, that said, there are some applications that don't like running in a service context. Note that this isn't because they display a GUI but for any one of several other reasons. (For example, some applications only work if Explorer is running on the same desktop.)

It's possible that vboxmanage is such an application, but it's more likely that your original code would have worked perfectly if you hadn't forgotten the /c .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM