(All code, presented here, is available at the end of the article as a ZIP file for download. As such, some definitions and declarations might be omitted, for brevity. This is why references to P/Invoke are NOT made, as all the definitions are already inserted in the code)

An interesting problem came up one day, namely spawning a process in another user’s process space, and more specifically – his/her Terminal Session. Since we have one default active station and a desktop in a single session, the process would have been rather straightforward, if it weren’t for the .NET and P/Invoke usage.

Anyway, on to the topic at hand. For a fine introduction to stations, sessions, desktops and the like, see Sessions, Desktops and Windows Stations. It is assumed from now on that you are familiar with these notions. So, let’s delve into some actual programming.

The first subtopic is how to list the available (i.e. active) Terminal Sessions. Simple – WTSEnumerateSessions() is the answer. Since we are interested in listing the sessions at the current host only, we will be using the WTS_CURRENT_SERVER_HANDLE constant. If you wish to manage another RD host (i.e. another computer), then you will need to open it first, using the WTSOpenServer() function:

        [DllImport(“wtsapi32.dll”, SetLastError = true)]
        static extern IntPtr WTSOpenServer([MarshalAs(UnmanagedType.LPStr)] String pServerName);

and then you use it like this:

         public static IntPtr OpenServer(String Name)
            IntPtr server = WTSOpenServer(Name);
            return server;

Assuming we are only interested in the current host, then we enumerate in the following way:

        public struct WTS_SESSION_INFO
            public Int32 SessionID;

            public String pWinStationName;

            public WTS_CONNECTSTATE_CLASS State;

        public enum WTS_CONNECTSTATE_CLASS

        public static List<WTS_SESSION_INFO> ListSessionsInfo()
            IntPtr WTS_CURRENT_SERVER_HANDLE = (IntPtr)null;
            List<WTS_SESSION_INFO> ret = new List<WTS_SESSION_INFO>();

                IntPtr ppSessionInfo = IntPtr.Zero;

                Int32 count = 0;
                Int32 retval = WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, ref ppSessionInfo, ref count);
                Int32 dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));

                Int64 current = (int)ppSessionInfo;

                if (retval != 0)
                    for (int i = 0; i < count; i++)
                        WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure((System.IntPtr)current, typeof(WTS_SESSION_INFO));
                        current += dataSize;



            return ret;

WTSEnumerateSessions will return the session list in ppSessionInfo, and the number of WTS_SESSION_INFO structures in the count varuable. For those of you acquainted with the beautiful pointer arithmetics in C/C++ – you get a pointer to the array of WTS_SESSION_INFO structures. Then we actually do some pointer arithmetics to get each WTS_SESSION_INFO structure in the for(…) loop, increasing current with the size of the structure to get a pointer to the next one. The structure is then added to the List. Finally, the unmanaged data structures are released via a call to WTSFreeMemory().

Then we go through the List of WTS_SESSION_INFO structures, and if a session is active, then we proceed with creating a process in it:

            List<WTS_SESSION_INFO> lSessions = ListSessionsInfo();

            foreach (WTS_SESSION_INFO si in lSessions)
                if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive)

The code is rather straightforward – InjectSession(…) just creates the process in that session (see below).

On to the core function now – the single most important function here is CreateProcessAsUser():

        public struct PROCESS_INFORMATION
            public IntPtr hProcess;
            public IntPtr hThread;
            public int dwProcessId;
            public int dwThreadId;

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        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 dwYSize;
            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;

        [DllImport(“advapi32.dll”, SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool CreateProcessAsUser(
            IntPtr hToken,
            string lpApplicationName,
            string lpCommandLine,
            ref SECURITY_ATTRIBUTES lpProcessAttributes,
            ref SECURITY_ATTRIBUTES lpThreadAttributes,
            bool bInheritHandles,
            uint dwCreationFlags,
            IntPtr lpEnvironment,
            string lpCurrentDirectory,
            ref STARTUPINFO lpStartupInfo,
            out PROCESS_INFORMATION lpProcessInformation);

        public static PROCESS_INFORMATION InjectProcessAsUser(UInt32 sessionID, string sCommandLine)
            int err = 0;

            STARTUPINFO si = new STARTUPINFO();

            pi.dwProcessId = -1;
            pi.dwThreadId = -1;
            pi.hProcess = IntPtr.Zero;
            pi.hThread = IntPtr.Zero;

            si.cb = Marshal.SizeOf(si);
            si.lpDesktop = “WinSta0\Default”;

            IntPtr uToken = new IntPtr();

            if (!WTSQueryUserToken(sessionID, out uToken))
                err = Marshal.GetLastWin32Error();
                throw new Win32Exception(err);

            // Create structs
            SECURITY_ATTRIBUTES saProcessAttributes = new SECURITY_ATTRIBUTES();
            SECURITY_ATTRIBUTES saThreadAttributes = new SECURITY_ATTRIBUTES();

            saProcessAttributes.nLength = Marshal.SizeOf(saProcessAttributes);
            saThreadAttributes.nLength = Marshal.SizeOf(saThreadAttributes);

            if (!CreateProcessAsUser(uToken, null, sCommandLine,
            ref saProcessAttributes, ref saThreadAttributes, false, 0, IntPtr.Zero, null, ref si, out pi))
                // Throw exception
                err = Marshal.GetLastWin32Error();

                throw new Win32Exception(err);

            return pi;

First, we create a PROCESS_INFORMATION structure, which will be the return value of the function, and initialize it to invalid values, just to be on the safe side. Then we create a STARTUPINFO structure, to be passed later as a parameter to CreateProcessAsUser(). That structure is used only for specifying the desktop for the process to be connected to (here, that is). Since we specify the desktop to be “WinSta0Default”, then we will connect to the desktop of the interactive user, logged in that station. See the exact algorithm at Process Connection to a Window Station – we fall in point 3, the first subitem.

This allows our process to interact with the user, logged in that station.

Then we get the token of the user, logged in that session, via WTSQueryUserToken(). IMPORTANT: the caller must be running in the context of the LocalSystem account and have the SE_TCB_NAME privilege. This is why this topic is mostly applicable to a Windows service.

Finally, the very call to CreateProcessAsUser() is made – the program to be started is passed in the sCommandLine variable – and the PROCESS_INFORMATION structure is returned.

Another note here – the process is NOT hidden from the end user, and will be visible to him/her, if s/he has the privilege to list his/her processes.

If you need to monitor the state of the users’ sessions – log on/off, or disconnects – you can subscribe to the SENS interface, see step 2 of Mr. Jerry Wang’s fine article on the subject. The code there is ready to use.

Further topics of interest are communication between these “spawned” processes and your own, or running the process as an Administrator (thus it will not be visible to a “Standard user”), etc., but other articles will cover these areas (coming soon).

Comments/suggestions are welcome.

And the download link is here: CreateProcessAsUser.