4

I use CreateProcessAsUser from a Windows Service in order to launch an application for the current active user. So far it works great with applications on a local drive.

But if the executable exists on a network share, the service generates 5: ERROR_ACCESS_DENIED when I use the full server name (\myserver\path\app.exe). I can also generate 2: ERROR_FILE_NOT_FOUND if I use the mapped drive instead (P:\path\app.exe).

I can launch the application fine from explorer. It really sounds like I cannot get a proper token duplicate as the service fails to properly impersonate me on the server.

I tried several different implementations of CreateProcessAsUser from various posts to no avail. This is brand new (psychedelic) stuff for me, and frankly, I can't wait to get back into .NET :) I guess the offending line is around here:

DuplicateTokenEx(
    hUserToken,
    (Int32)MAXIMUM_ALLOWED,
    ref sa,
    (Int32)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
    (Int32)TOKEN_TYPE.TokenPrimary,
    ref hUserTokenDup);

CreateEnvironmentBlock(ref pEnv, hUserTokenDup, true);

Int32 dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT;

PROCESS_INFORMATION pi;
STARTUPINFO si = new STARTUPINFO();
si.cb = Marshal.SizeOf(si);
si.lpDesktop = "winsta0\\default";

CreateProcessAsUser(hUserTokenDup,    // client's access token
    null,                             // file to execute
    commandLine,                      // command line
    ref sa,                           // pointer to process SECURITY_ATTRIBUTES
    ref sa,                           // pointer to thread SECURITY_ATTRIBUTES
    false,                            // handles are not inheritable
    dwCreationFlags,                  // creation flags
    pEnv,                             // pointer to new environment block 
    workingDirectory,                 // name of current directory 
    ref si,                           // pointer to STARTUPINFO structure
    out pi);                          // receives information about new process

Here's the full sample code, I guess it can be useful:

using System;
using System.Text;
using System.Security;
using System.Management;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Win32
{
    public class Win32API
    {
        [StructLayout(LayoutKind.Sequential)]
        struct SECURITY_ATTRIBUTES
        {
            public Int32 Length;
            public IntPtr lpSecurityDescriptor;
            public Boolean bInheritHandle;
        }

        enum TOKEN_TYPE
        {
            TokenPrimary = 1,
            TokenImpersonation = 2
        }

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

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

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

        const UInt32 MAXIMUM_ALLOWED = 0x2000000;
        const Int32 CREATE_UNICODE_ENVIRONMENT = 0x00000400;
        const Int32 NORMAL_PRIORITY_CLASS = 0x20;
        const Int32 CREATE_NEW_CONSOLE = 0x00000010;

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern Boolean CloseHandle(IntPtr hSnapshot);

        [DllImport("kernel32.dll")]
        public static extern UInt32 WTSGetActiveConsoleSessionId();

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

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

        [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
        extern static Boolean DuplicateTokenEx(
            IntPtr ExistingTokenHandle,
            UInt32 dwDesiredAccess,
            ref SECURITY_ATTRIBUTES lpThreadAttributes,
            Int32 TokenType,
            Int32 ImpersonationLevel,
            ref IntPtr DuplicateTokenHandle);

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

        [DllImport("userenv.dll", SetLastError = true)]
        static extern Boolean DestroyEnvironmentBlock(IntPtr lpEnvironment);

        /// <summary>
        /// Creates the process in the interactive desktop with credentials of the logged in user.
        /// </summary>
        public static Boolean CreateProcessAsUser(String commandLine, String workingDirectory, out StringBuilder output)
        {
            Boolean processStarted = false;
            output = new StringBuilder();

            try
            {
                UInt32 dwSessionId = WTSGetActiveConsoleSessionId();
                output.AppendLine(String.Format("Active console session Id: {0}", dwSessionId));

                IntPtr hUserToken = IntPtr.Zero;
                WTSQueryUserToken(dwSessionId, ref hUserToken);

                SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
                sa.Length = Marshal.SizeOf(sa);

                IntPtr hUserTokenDup = IntPtr.Zero;
                DuplicateTokenEx(
                    hUserToken,
                    (Int32)MAXIMUM_ALLOWED,
                    ref sa,
                    (Int32)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
                    (Int32)TOKEN_TYPE.TokenPrimary,
                    ref hUserTokenDup);


                if (hUserTokenDup != IntPtr.Zero)
                {
                    output.AppendLine(String.Format("DuplicateTokenEx() OK (hToken: {0})", hUserTokenDup));

                    Int32 dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;

                    IntPtr pEnv = IntPtr.Zero;
                    if (CreateEnvironmentBlock(ref pEnv, hUserTokenDup, true))
                    {
                        dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT;
                        output.AppendLine(String.Format("CreateEnvironmentBlock() success."));
                    }
                    else
                    {
                        output.AppendLine(String.Format("CreateEnvironmentBlock() FAILED (Last Error: {0})", Marshal.GetLastWin32Error()));
                        pEnv = IntPtr.Zero;
                    }

                    // Launch the process in the client's logon session.
                    PROCESS_INFORMATION pi;

                    STARTUPINFO si = new STARTUPINFO();
                    si.cb = Marshal.SizeOf(si);
                    si.lpDesktop = "winsta0\\default";

                    output.AppendLine(String.Format("CreateProcess (Path:{0}, CurrDir:{1})", commandLine, workingDirectory));

                    if (CreateProcessAsUser(hUserTokenDup,    // client's access token
                            null,                // file to execute
                            commandLine,        // command line
                            ref sa,                // pointer to process SECURITY_ATTRIBUTES
                            ref sa,                // pointer to thread SECURITY_ATTRIBUTES
                            false,                // handles are not inheritable
                            dwCreationFlags,    // creation flags
                            pEnv,                // pointer to new environment block 
                            workingDirectory,    // name of current directory 
                            ref si,                // pointer to STARTUPINFO structure
                            out pi                // receives information about new process
                        ))
                    {
                        processStarted = true;
                        output.AppendLine(String.Format("CreateProcessAsUser() OK (PID: {0})", pi.dwProcessId));
                    }
                    else
                    {
                        output.AppendLine(String.Format("CreateProcessAsUser() failed (Last Error: {0})", Marshal.GetLastWin32Error()));
                    }

                    if (DestroyEnvironmentBlock(pEnv))
                    {
                        output.AppendLine("DestroyEnvironmentBlock: Success");
                    }
                    else
                    {
                        output.AppendLine(String.Format("DestroyEnvironmentBlock() failed (Last Error: {0})", Marshal.GetLastWin32Error()));
                    }
                }
                else
                {
                    output.AppendLine(String.Format("DuplicateTokenEx() failed (Last Error: {0})", Marshal.GetLastWin32Error()));
                }
                CloseHandle(hUserTokenDup);
                CloseHandle(hUserToken);
            }
            catch (Exception ex)
            {
                output.AppendLine("Exception occurred: " + ex.Message);
            }
            return processStarted;
        }
    }
}

It works great with local executables like this:

StringBuilder result = new StringBuilder();
Win32API.CreateProcessAsUser(@"C:\Windows\notepad.exe", @"C:\Windows\", out result);

My question: What needs to be tuned in order to properly access a network share using a duplicate token?

Joe
  • 2,496
  • 1
  • 22
  • 30
  • 1
    Check the return values of DuplicateTokenEx and CreateProcessAsUser and obtain the last error if they fail to get an idea as to why they fail. – 500 - Internal Server Error Jan 22 '13 at 17:32
  • @500-InternalServerError - DuplicateTokenEx works fine but CreateProcessAsUser gives me 5: ERROR_ACCESS_DENIED. Do you have any suggestion in what options I could change to mimic a real user more closely? – Joe Jan 22 '13 at 17:45
  • 1
    Getting this to work does not appear to be trivial. [This question](http://stackoverflow.com/questions/8081429/createprocessasuser-doesnt-work-when-change-user) might provide some clues. – 500 - Internal Server Error Jan 22 '13 at 17:53
  • @500-InternalServerError Thanks! It did provide clues, now I create/destroy an environment block. But unfortunately I still can't make it work on a network share (but it works locally). I think I have isolated the cause though so I completely updated my question with new explanation and code sample. – Joe Jan 23 '13 at 22:49

1 Answers1

2

When you use this against a share that allows guest access (i.e. no username/password), the command works correctly, but when you use it against a share that requires authentication to use it doesn't work.

UI invocations get the redirector involved, which automatically establishes the connection to the remote server which is needed for the execution.

A workaround, mind you, but not a real solution is to use a cmd based relay to get to the executable, so for the command line you make it something like:

CreateProcessAsUser(@"cmd /c ""start \\server\share\binary.exe""", @"C:\Windows", out result);

Then change the startupinfo to SW_HIDE the cmd window using:

si.cb = Marshal.SizeOf(si);
si.lpDesktop = "winsta0\\default";
si.dwFlags = 0x1; // STARTF_USESHOWWINDOW
si.wShowWindow = 0; // SW_HIDE

The cmd invocation is the bit of shim to get completely into the user's environment before starting the command - this will take advantage of all credentials for accessing the server.

Mind you, you will probably have to have a little bit of logic to prevent the SW_HIDE for directly invoked applications (e.g. check for cmd at the start of the commandLine string?)

Anya Shenanigans
  • 91,618
  • 3
  • 107
  • 122
  • Thanks Petesh, that "cmd" workaround did the trick! But the syntax with the quotes didn't work on my end during CreateProcessAsUser(). It worked once I set lpApplicationName and lpCurrentDirectory to null, and formatting the lpCommandName to "cmd /c \\\\myserver\\path\\myapp.exe", getting rid of both the "start" and the quotes. – Joe Jan 24 '13 at 16:30
  • 1
    Yeah, the quoting is tricky to get right. The reason for the `start` is to terminate the shell once the remote application has launched – Anya Shenanigans Jan 24 '13 at 16:31
  • Understood. I'll try to add it then, thanks for the help. I've been stuck with this issue since before the holidays :P – Joe Jan 24 '13 at 16:31
  • Indeed, start works too. Final commandline: "cmd /c start \\\\myserver\\mypath\\myapp.exe". – Joe Jan 24 '13 at 16:40