0

I've spent a few days on this problem and even though there are tons of different examples online it's a tricky problem and I can't get them to work in my scenario.

I have a Windows service that runs under the Local System account. It has a WCF endpoint listening to API requests. When told via the API, the service is supposed to start a new process in the System session (0) and with the "Worker" account credentials. The process is a worker that checks for work in a queue and does it. If it does not find work, it will sleep for a bit and check again. If it does find work, it starts a new process in the same session and with the same credentials and does the work. After it's done the work it closes.

The "Worker" is a domain account and a member of the local administrators group on the machine, which has execute permissions on the executable. The machine is on the same domain as the account.

The problem is that when the service tries to start the process it gets a ERROR_ACCESS_DENIED (5) error code from the CreateProcessAsUser method.

I tried running the same code on a Windows 7 machine with the same credentials and it works fine, but it gets that error code when running on Windows Server 2008.

The code's too big to show here, so I've put it elsewhere...

ProcessHelper: http://pastie.org/private/y7idu3nw4xv1fxzeizbn9g

The service calls the StartAsUserFromService method to start the process, which internally calls CreateProcessAsUser after establishing a session. The process calls the StartAsUserFromApplication method to start its successor, which internally calls CreateProcessWithLogonW.

ImpersonationContext: http://pastie.org/private/xppc7wnoidajmpq8h8sg

The service needs to get the user token to start a process as them. The process doesn't need that to start its successor. As far as I can tell the impersonation is successful on Server 2008, but it doesn't have some permissions, and I can't figure out which.

EDIT:

I tried both a local administrator account and a domain account on the Windows 7 machine, and they work fine. But neither of them work on the Server 2008 machine. There must be a permission missing somewhere, but I don't know where; the error message isn't helpful.

I also tried ticking the "run as administrator" box in the compatibility tab of the executable, but it made no difference.

EDIT:

I used process monitor to see what's going on in the service and this is where it's getting the error...

Date & Time:    12/02/2014 11:44:03
Event Class:    File System
Operation:  CreateFile
Result: ACCESS DENIED
Path:   D:\..\executable.exe
TID:    6244
Duration:   0.0000450
Desired Access: Read Data/List Directory, Execute/Traverse, Read Attributes, Synchronize
Disposition:    Open
Options:    Synchronous IO Non-Alert, Non-Directory File
Attributes: n/a
ShareMode:  Read, Delete
AllocationSize: n/a
Impersonating:  Domain\Worker

and

Date & Time:    12/02/2014 11:44:03
Event Class:    File System
Operation:  CreateFile
Result: ACCESS DENIED
Path:   D:\..\executable.exe
TID:    6244
Duration:   0.0000480
Desired Access: Execute/Traverse, Synchronize
Disposition:    Open
Options:    Synchronous IO Non-Alert, Non-Directory File
Attributes: n/a
ShareMode:  Read, Delete
AllocationSize: n/a
Impersonating:  Domain\Worker
Edgar
  • 4,348
  • 4
  • 40
  • 59
  • 1
    Does the domain user have log on as a service rights? http://technet.microsoft.com/en-us/library/cc739424(v=ws.10).aspx – David Heffernan Feb 12 '14 at 11:30
  • @David Yes it does. Even though it shouldn't need them because I'm not starting the process as a service with `LogonType.Service`. I'm starting it with `LogonType.Interactive`. – Edgar Feb 12 '14 at 11:32
  • Oh silly me. That was a daft question that I asked. Sorry. – David Heffernan Feb 12 '14 at 11:36
  • @David That's ok, I'm willing to test any idea. I also tried to tick the "Allow service to interact with desktop" box in service manager, but it didn't help. Is there a similar permission for processes/user accounts? – Edgar Feb 12 '14 at 11:39
  • You don't want your service to interact with the desktop. And you probably don't want it to run under LocalSystem. Does the security log report why your logon fails? – David Heffernan Feb 12 '14 at 11:48
  • That's correct, the process it starts doesn't need any user input. I run the service under Local System because it needs some `SE_*` permissions to start processes. The service also writes to the file system. There's nothing in the event log, it logs in successfully, but is not allowed to start the process. – Edgar Feb 12 '14 at 11:53
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/47317/discussion-between-edgar-and-david-heffernan) – Edgar Feb 12 '14 at 11:54

2 Answers2

1

Some tips :
How to Impersonate
Impersonation code in C#
Impersonation Libraries (Class & Com Class)
WindowsIdentity.Impersonate Method

Try using this sample (found this somewhere) :

using System;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Security.Permissions;

[assembly:SecurityPermissionAttribute(SecurityAction.RequestMinimum,UnmanagedCode=true)]
[assembly:PermissionSetAttribute(SecurityAction.RequestMinimum, Name ="FullTrust")] 
public class ImpersonationDemo 
{

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

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

    public enum TOKEN_TYPE
    {
        TokenPrimary = 1,
        TokenImpersonation
    } 

    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
    private unsafe static extern int FormatMessage(int dwFlags, ref IntPtr lpSource, int dwMessageId, int dwLanguageId, ref String lpBuffer, int nSize, IntPtr *Arguments);

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

    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError=true)]
    public extern static bool DuplicateToken(IntPtr ExistingTokenHandle,  int SECURITY_IMPERSONATION_LEVEL, ref IntPtr DuplicateTokenHandle);

    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public extern static bool DuplicateTokenEx( IntPtr hExistingToken, uint dwDesiredAccess, ref SECURITY_ATTRIBUTES lpTokenAttributes,
        SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
        TOKEN_TYPE TokenType,
        out IntPtr phNewToken);

    // GetErrorMessage formats and returns an error message
    // corresponding to the input errorCode.
    public unsafe static string GetErrorMessage(int errorCode)
    {
        int FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100;
        int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
        int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;

        int messageSize = 255;
        String lpMsgBuf = "";
        int dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;

        IntPtr ptrlpSource = IntPtr.Zero;
        IntPtr prtArguments = IntPtr.Zero;

        int retVal = FormatMessage(dwFlags, ref ptrlpSource, errorCode, 0, ref lpMsgBuf, messageSize, &prtArguments);
        if (0 == retVal)
        {
            throw new Exception("Failed to format message for error code " + errorCode + ". ");
        }

    return lpMsgBuf;
    }

    // Test harness.
    // If you incorporate this code into a DLL, be sure to demand FullTrust.
    [PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")]
    public static void Main(string[] args)
    {
        IntPtr tokenHandle = new IntPtr(0);
        IntPtr dupeTokenHandle = new IntPtr(0);
        try
        {
            string UserName, MachineName;

            // Get the user token for the specified user, machine, and password using the
            // unmanaged LogonUser method.

            Console.Write("Enter the name of a machine on which to log on: ");
            MachineName = Console.ReadLine();

            Console.Write("Enter the login of a user on {0} that you wish to impersonate: ", MachineName);
            UserName = Console.ReadLine();

            Console.Write("Enter the password for {0}: ", UserName);

            const int LOGON32_PROVIDER_DEFAULT = 3;
            //This parameter causes LogonUser to create a primary token.
            const int LOGON32_LOGON_INTERACTIVE = 8;

            tokenHandle = IntPtr.Zero;
            dupeTokenHandle = IntPtr.Zero;

            // Call LogonUser to obtain a handle to an access token.
            bool returnValue = LogonUser(UserName, MachineName, "mm4geata", 
                LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, ref tokenHandle);

            Console.WriteLine("LogonUser called.");

            if (false == returnValue)
            {
                int ret = Marshal.GetLastWin32Error();
                Console.WriteLine("LogonUser failed with error code : {0}", ret);
                Console.WriteLine("\nError: [{0}] {1}\n", ret, GetErrorMessage(ret));

                return;
            }

            Console.WriteLine("Did LogonUser Succeed? " + (returnValue? "Yes" : "No"));
            Console.WriteLine("Value of Windows NT token: " + tokenHandle);

            // Check the identity.
            Console.WriteLine("Before impersonation: " + WindowsIdentity.GetCurrent().Name);

            //bool retVal = DuplicateToken(tokenHandle, SecurityImpersonation, ref dupeTokenHandle);

            SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
            sa.bInheritHandle = true;
            sa.Length = Marshal.SizeOf(sa);
            sa.lpSecurityDescriptor = (IntPtr)0;

            bool retVal = DuplicateTokenEx(tokenHandle, 0x10000000, ref sa, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenImpersonation, out dupeTokenHandle); 


            if (false == retVal)
            {
                CloseHandle(tokenHandle);
                Console.WriteLine("Exception thrown in trying to duplicate token.");
                return;
            }


            // The token that is passed to the following constructor must
            // be a primary token in order to use it for impersonation.
            WindowsIdentity newId = new WindowsIdentity(dupeTokenHandle);
            WindowsImpersonationContext impersonatedUser = newId.Impersonate();

            // Check the identity.
            Console.WriteLine("After impersonation: " + WindowsIdentity.GetCurrent().Name);

            // Stop impersonating the user.
            impersonatedUser.Undo();

            // Check the identity.
            Console.WriteLine("After Undo: " + WindowsIdentity.GetCurrent().Name);

            // Free the tokens.
            if (tokenHandle != IntPtr.Zero)
            CloseHandle(tokenHandle);
            if (dupeTokenHandle != IntPtr.Zero)
            CloseHandle(dupeTokenHandle);
        }
        catch(Exception ex)
        {
            Console.WriteLine("Exception occurred. " + ex.Message);
        }

    }
}
lsalamon
  • 7,998
  • 6
  • 50
  • 63
  • The only difference from my code that I can see is that `SECURITY_ATTRIBUTES.bInheritHandle` is set to `true` and it's using `TOKEN_TYPE.TokenImpersonation` instead of `TokenPrimary`. I'll try this later, but I don't think it will help. I think I need a primary token to start a process. – Edgar Feb 12 '14 at 08:20
  • No, neither of those made a difference. – Edgar Feb 12 '14 at 11:23
  • See the links I added, especially in the example WindowsIdentity.Impersonate Method – lsalamon Feb 12 '14 at 12:03
  • I can't find anything different from what I'm doing in the examples. Do you think the impersonation is not getting the right permissions, and that's why it's not allowed to start the executable? – Edgar Feb 12 '14 at 12:09
  • Tried the examples, different ways of impersonating a user, still getting error code 5. – Edgar Feb 12 '14 at 14:01
0

I managed to make the processes start with this code:

ProcessHelper: http://pastie.org/private/dlkytj8rbigs8ixwtg

TokenImpersonationContext: http://pastie.org/private/nu3pvpghoea6pwwlvjuq

The service calls the StartAsUserFromService method, and the process calls the StartAsUserFromApplication method to start its successor.

I'm using LogonType.Batch in the LogonUser call because the process needs to talk to another WCF service and needs to authenticate. LogonType.Network and LogonType.NetworkClearText could be used, but caused permission issues in the Net.Tcp Port Sharing Service with the Worker user account.

This answer was helpful: Using Process.Start() to start a process as a different user from within a Windows Service

Community
  • 1
  • 1
Edgar
  • 4,348
  • 4
  • 40
  • 59