CreateProcessAsUser Creating Window in Active Session
Asked Answered
F

3

14

I am using CreateProcessAsUser from a windows service (please can we stay on-topic and assume I have a very good reason for doing this). Contrary to what everyone else is asking here I am getting a window in my active terminal session (session 1) instead of the same session as the service (session 0) - which is undesirable.

I appropriated Scott Allen's code; and came up with the following. Notable changes are the "revert to self", the "CREATE_NO_WINDOW" and command-line args support.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Security.Principal;
using System.ComponentModel;
using System.IO;

namespace SourceCode.Runtime.ChildProcessService
{
    [SuppressUnmanagedCodeSecurity]
    class NativeMethods
    {
        [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("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);

        public static Process CreateProcessAsUser(string filename, string args)
        {
            var hToken = WindowsIdentity.GetCurrent().Token;
            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);

                // 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, 0, 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);
            }
        }
    }
}

Now assume that the service is running under 'Domain\MyService' and I am currently logged in as 'Domain\Administrator' - and I am booting a console application as a worker process. When I use a client application to access the service (the service isn't started in console mode i.e. it is in session 0) and execute the method that invokes the CreateProcessAsUser the worker process appears on my desktop.

Now I could make it a windows application with no window to side-step the creation of the console window; however, at the end of the day it's still being created in session 1.

Any ideas why the console application isn't being created in the same session as the service?

Fulminous answered 1/2, 2012 at 12:35 Comment(6)
It looks like it might have something to do with this dark magic but I can't figure out how to skip it.Fulminous
Tried using "Service-0×0-3e7$\Default" as the desktop - which causes the application to crash.Fulminous
What version of Windows? Have you tried leaving lpDeskTop at null?Then
@HansPassant Server 2008 R2. I have indeed tried setting lpDeskTop to null (which caused a crash in the worker process) - I also tried setting it to various hard-coded values such as the built-in session 0 desktop name.Fulminous
If so many circumstances are causing crashes, perhaps there is a marshaling issue. Can you post the code for your structs and externs?Recompense
@Recompense I added the entire code in.Fulminous
D
6

As you are probably aware already, the isolation of Session 0 is for security reason and you can read more about it here http://msdn.microsoft.com/en-us/windows/hardware/gg463353.aspx

With regard to why your console app is created in active session (e.g. session 1), this actually linked back directly to your user token. When you ask for current user token, this token automatically carried with it the session id information - in this case it is the login terminal services session (session 1). This session id is reference by the token which then replicated in the DuplicateTokenEx and then used in the CreateProcessAsUser call. In order to force the creation of the your console application in session 0, you will need to make an explicit call to the SetTokenInformation API (advapi32.dll), passed in your hDupedToken before calling CreateProcessAsUser like below

..................
UInt32 dwSessionId = 0;  // set it to session 0
SetTokenInformation(hDupedToken, TokenInformationClass.TokenSessionId, ref dwSessionId, (UInt32) IntPtr.Size);
.................
CreateProcessAsUser(hDupedToken, ....)

Here is more info on SetTokenInformation http://msdn.microsoft.com/en-us/library/windows/desktop/aa379591(v=vs.85).aspx

Decelerate answered 8/2, 2012 at 12:40 Comment(6)
I thought it might be something stupid like this. I am going to give this a shot.Fulminous
Many thanks for the help - I am just having problems with acquiring the SE_TCB_NAME right (needed for SetTokenInformation). I have granted my service account the "Act as part of the operating system" right (as well as SERVICE for shotgun insurance). I have tried using OpenThreadToken with TOKEN_ALL_ACCESS - to no avail. You have earned the +250 - I am just hoping you have a little more expertise in this regard to help me out.Fulminous
You get the bounty. Time to start another bounty ;).Fulminous
Thanks for the bounty. I don't think there is much way around it if your service is launched using user account which by itself has limited OS privilege. Can't you launch your service using LocalSystem account? LocalSystem can launch your process and automatically have that security right and access to other WTS API which you may need in TS environment.Decelerate
I had considered that but it has a really high installer impact (our primary service needs to run under a domain account) - still I'll keep it in the bag for later and see if management will bite given the risk. Thanks for the help.Fulminous
Just as a suggestion, you can also try launching the process using psexec and specify the session parameter and see if that works. I haven't tried it myself.Decelerate
A
1

I was able to implement the initial post as a working solution on my end, however, I can't seem to find a way to keep my console Window hidden. I tried STARTF_USESHOWWINDOW and SW_HIDE but my command window still pops up. Any idea why?

    public const int STARTF_USESHOWWINDOW = 0x0000000;
    public const int SW_HIDE = 0;


   public static Process CreateProcessAsUser(string filename, string args)
    {
        var hToken = WindowsIdentity.GetCurrent().Token;
        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 = String.Empty; 

            si.dwFlags = STARTF_USESHOWWINDOW;
            si.wShowWindow = SW_HIDE;

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

            // 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))
            {
                UInt32 dwSessionId = 1;  // set it to session 0
                SetTokenInformation(hDupedToken, TOKEN_INFORMATION_CLASS.TokenSessionId,
                    ref dwSessionId, (UInt32)IntPtr.Size);
                if (!CreateProcessAsUser(
                                        hDupedToken,
                                        path,
                                        string.Format("\"{0}\" {1}", filename.Replace("\"", "\"\""), args),
                                        ref sa, ref sa,
                                        false, 0, 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);
        }
    }
Aleen answered 13/6, 2016 at 22:53 Comment(0)
R
0

Try messing around with the CharSet named parameter of MarshalAs, StructLayout, and DllImport. You might need to add MarshalAs to various strings in order to do this. Don't bother with Unicode: you aren't using this. I recommend setting them all to CharSet.Ansi first. Run all the tests that you've already tried--that is, setting the desktop and all that fun stuff. If it crashes, switch them all to auto. If it still doesn't work, remove them all.

Assuming none of this works, switch to CreateUserProcessW and CharSet.Unicode so you know what you're getting. On second thought, just skip to this step.

If you need to set the UnmanagedType with MarshalAs for strings, you want UnmanagedType.LPStr for Ansi, UnmanagedType.LPTStr for Auto, and UnmanagedType.LPWStr for Unicode. Actually, do this for all your strings anyway.

Recompense answered 8/2, 2012 at 8:31 Comment(2)
I've never had to set the CharSet for these functions unless they were A or W variants, so I'm putting my bets on Auto.Recompense
Good luck! If you're feeling ambitious, skip straight to Unicode.Recompense

© 2022 - 2024 — McMap. All rights reserved.