Charteris Community Server

Welcome to the Charteris plc Community
Welcome to Charteris Community Server Sign in | Join | Help
in Search

Chris Dickson's Blog

June 2008 - Posts

  • Exploring the WCF Named Pipe Binding - Part 3

    In this post I will show one way to restrict access to the named pipe created by the WCF named pipe listener, to provide a partial workaround for the security flaw mentioned in my last post.

    The strategy is to target directly the internal property AllowedUsers on the type System.ServiceModel.Channels.NamedPipeChannelListener. We cannot call this property normally because it is internal to WCF, but reflection allows an alternative way to invoke it. Since this only needs to be done once, when Open is called on the ServiceHost to build the service run-time, the performance cost of using reflection is not an issue here. We will populate this AllowedUsers collection with the SID for a Group representing the authorised users of the service we are protecting, supplied as a parameter of the binding before the service is opened. It turns out we also need to add the SID for the service account itself, for reasons I will explain in more detail below. WCF will then use this collection of SIDs, rather than its default list (EVERYONE), when calling CreateNamedPipe in the PipeConnectionListener.

    After Open has been called on the ServiceHost, WCF builds the server run-time stack. The key part of this process which interests us is the point where the channel listener is created by the transport binding element. When using the standard netNamedPipe binding, the relevant transport binding element is of type System.ServiceModel.NamedPipeTransportBindingElement. We can conveniently perform our amendment to the configuration of the listener, by subclassing this NamedPipeTransportBindingElement and overriding the virtual method BuildChannelListener<>(). This allows us to get a reference to the listener after it has been created by the standard WCF transport binding element code, but before BeginAccept() is called on it (whch is when the first pipe instance is created).

    Here is some code for a custom named pipe binding which implements this strategy:

    using System;
    using System.Collections.Generic;
    using System.ServiceModel.Channels;
    using System.ServiceModel;
    using System.Reflection;
    using System.Security.Principal;
    using System.Threading;

    namespace Charteris.ChrisDicksonBlog.Samples
    {
      public class AclSecuredNamedPipeBinding : CustomBinding
     
    {

        public AclSecuredNamedPipeBinding(): base()
           {
               NetNamedPipeBinding standardBinding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.Transport);
               foreach (BindingElement element in standardBinding.CreateBindingElements())
              {
                  NamedPipeTransportBindingElement transportElement = element as NamedPipeTransportBindingElement;
                  base.Elements.Add(
                 null != transportElement ? new AclSecuredNamedPipeTransportBindingElement(transportElement) : element);

              }
              AddUserOrGroup(WindowsIdentity.GetCurrent().User);
           }

        public void AddUserOrGroup(SecurityIdentifier sid)
           {
              List<SecurityIdentifier> allowedUsers
                  = Elements.Find<AclSecuredNamedPipeTransportBindingElement>().AllowedUsers;

              if (!allowedUsers.Contains(sid))
              {
                  allowedUsers.Add(sid);
              }
           }
       }

      public class AclSecuredNamedPipeTransportBindingElement : NamedPipeTransportBindingElement
     
    {
           
    private static Type namedPipeChannelListenerType 
                  = Type.GetType("System.ServiceModel.Channels.NamedPipeChannelListener, System.ServiceModel", false);

        public AclSecuredNamedPipeTransportBindingElement(NamedPipeTransportBindingElement inner): base(inner)
          
    {
            
    if (inner is AclSecuredNamedPipeTransportBindingElement)
             {
                
    _allowedUsers = new List<SecurityIdentifier>(
                  ((AclSecuredNamedPipeTransportBindingElement)inner)._allowedUsers);
          }
        }

        public override BindingElement Clone()
           {
             return new AclSecuredNamedPipeTransportBindingElement(this);
           }

        public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
          
    {
          IChannelListener<TChannel> listener = base.BuildChannelListener<TChannel>(context);
          PropertyInfo p = namedPipeChannelListenerType.GetProperty(
                  "AllowedUsers", BindingFlags.Instance|BindingFlags.NonPublic);
          p.SetValue(listener, _allowedUsers, null);
          return listener;
        }

        internal List<SecurityIdentifier> AllowedUsers { get { return _allowedUsers; } }
        private List<SecurityIdentifier> _allowedUsers = new List<SecurityIdentifier>();
      }
    }
     

    As it stands, this code allows the SIDs of service users to be added in code but not by means of service configuration. The latter is left as an exercise for the reader, as they say.

    Using this custom binding, we can restrict use of a service endpoint to members of a specific Windows group, by means of code like this in the service host:

    AclSecuredNamedPipeBinding binding = new AclSecuredNamedPipeBinding();

    SecurityIdentifier allowedGroup
         = (SecurityIdentifier)(new NTAccount("NPServiceUsers").Translate(typeof(SecurityIdentifier)));

    binding.AddUserOrGroup(allowedGroup);

    ...

    _serviceHost.AddServiceEndpoint(... , binding, ...);

    ...

    _serviceHost.Open()

    I described this as a partial workaround for the flaw in the default security provided by the standard binding. It is not a full workaround because SIDs which are allowed access to the pipe still have the powerful permission FILE_CREATE_PIPE_INSTANCE, which ideally we would not want anyone other then the service account itself to have.

    I said I would say something about why we need to add the service account itself to the AllowedUsers collection. This relates back to the CREATOR OWNER anomaly in the pipe DACL, which I raised in my last post. You might think (and I suspect one of the WCF developers thought) that this ACE in the DACL would grant the service account the rights it needs to set up the listener and handle client requests arriving on the pipe. This isn't the case, though... it is actually the EVERYONE ACE which enables a service using the standard binding to work correctly.

    Let's look what happens if we remove the line

              AddUserOrGroup(WindowsIdentity.GetCurrent().User);

    from the constructor the custom binding, so that the DACL on the pipe just contains the NETWORK deny ACE, an ACE allowing access to our service users' group, and the CREATOR OWNER ACE. In other words, just like the one created by the standard binding, except with our service users' group instead of EVERYONE. 

    With this configuration, the service appears to start correctly, but as soon as the first client message hits the pipe, the service host starts to consume CPU cycles uncontrollably (and ultimately has to be killed) and the client never gets any response. Turning on tracing shows that the service is repeatedly trying to create a new pipe instance, and failing with an Access Denied error:

    <E2ETraceEvent xmlns="http://schemas.microsoft.com/2004/06/E2ETraceEvent"><System xmlns="http://schemas.microsoft.com/2004/06/windows/eventlog/system"><EventID>131075</EventID><Type>3</Type><SubType Name="Error">0</SubType><Level>2</Level><TimeCreated SystemTime="2008-05-14T09:47:27.8109616Z" /><Source Name="System.ServiceModel" /><Correlation ActivityID="{905d5b25-0f13-4f25-b3fb-a31d9a69738f}" /><Execution ProcessName="WCFDemoNPServer" ProcessID="5916" ThreadID="3" /><Channel /><Computer>#####</Computer></System><ApplicationData><TraceData><DataItem><TraceRecord xmlns="http://schemas.microsoft.com/2004/10/E2ETraceEvent/TraceRecord" Severity="Error"><TraceIdentifier>http://msdn.microsoft.com/en-GB/library/System.ServiceModel.Diagnostics.ThrowingException.aspx</TraceIdentifier><Description>Throwing an exception.</Description><AppDomain>WCFDemoNPServer.exe</AppDomain>
    <Exception>
    <ExceptionType>System.ServiceModel.AddressAccessDeniedException, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>
    <Message>
    Cannot listen on pipe 'net.pipe://localhost/WCFDemoNPServer/NPService': Unrecognized error 5 (0x5)
    </Message>
    <StackTrace>  
       at System.ServiceModel.Channels.PipeConnectionListener.CreatePipe()
       at System.ServiceModel.Channels.PipeConnectionListener.BeginAccept(AsyncCallback callback, Object state)
       at System.ServiceModel.Channels.BufferedConnectionListener.BeginAccept(AsyncCallback callback, Object state)
       at System.ServiceModel.Channels.TracingConnectionListener.BeginAccept(AsyncCallback callback, Object state)
       at System.ServiceModel.Channels.ConnectionAcceptor.AcceptIfNecessary(Boolean startAccepting)
       at System.ServiceModel.Channels.ConnectionAcceptor.HandleCompletedAccept(IAsyncResult result)
       at System.ServiceModel.Channels.ConnectionAcceptor.AcceptCompletedCallback(IAsyncResult result)
       at System.ServiceModel.Diagnostics.Utility.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
       at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
       at System.ServiceModel.Channels.PipeConnectionListener.PendingAccept.OnAcceptComplete(Boolean haveResult, Int32 error, Int32 numBytes)
       at System.ServiceModel.Channels.OverlappedContext.CompleteCallback(UInt32 error, UInt32 numBytes, NativeOverlapped* nativeOverlapped)
       at System.ServiceModel.Diagnostics.Utility.IOCompletionThunk.UnhandledExceptionFrame(UInt32 error, UInt32 bytesRead, NativeOverlapped* nativeOverlapped)
       at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)
    </StackTrace>

    <ExceptionString>System.ServiceModel.AddressAccessDeniedException: Cannot listen on pipe 'net.pipe://localhost/WCFDemoNPServer/NPService': Unrecognized error 5 (0x5) ---&amp;gt; System.IO.PipeException: Cannot listen on pipe 'net.pipe://localhost/WCFDemoNPServer/NPService': Unrecognized error 5 (0x5)
       --- End of inner exception stack trace ---</ExceptionString><InnerException><ExceptionType>System.IO.PipeException, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType><Message>Cannot listen on pipe 'net.pipe://localhost/WCFDemoNPServer/NPService': Unrecognized error 5 (0x5)</Message><StackTrace>   at System.ServiceModel.Channels.PipeConnectionListener.CreatePipe()
       at System.ServiceModel.Channels.PipeConnectionListener.BeginAccept(AsyncCallback callback, Object state)
       at System.ServiceModel.Channels.BufferedConnectionListener.BeginAccept(AsyncCallback callback, Object state)
       at System.ServiceModel.Channels.TracingConnectionListener.BeginAccept(AsyncCallback callback, Object state)
       at System.ServiceModel.Channels.ConnectionAcceptor.AcceptIfNecessary(Boolean startAccepting)
       at System.ServiceModel.Channels.ConnectionAcceptor.HandleCompletedAccept(IAsyncResult result)
       at System.ServiceModel.Channels.ConnectionAcceptor.AcceptCompletedCallback(IAsyncResult result)
       at System.ServiceModel.Diagnostics.Utility.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
       at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
       at System.ServiceModel.Channels.PipeConnectionListener.PendingAccept.OnAcceptComplete(Boolean haveResult, Int32 error, Int32 numBytes)
       at System.ServiceModel.Channels.OverlappedContext.CompleteCallback(UInt32 error, UInt32 numBytes, NativeOverlapped* nativeOverlapped)
       at System.ServiceModel.Diagnostics.Utility.IOCompletionThunk.UnhandledExceptionFrame(UInt32 error, UInt32 bytesRead, NativeOverlapped* nativeOverlapped)
       at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)
    </StackTrace><ExceptionString>System.IO.PipeException: Cannot listen on pipe 'net.pipe://localhost/WCFDemoNPServer/NPService': Unrecognized error 5 (0x5)</ExceptionString></InnerException>

    </Exception></TraceRecord></DataItem></TraceData></ApplicationData></E2ETraceEvent> 

    In essence, it seems to me that what is happening is that the service succeeds in getting a handle to the first pipe instance, at the time the pipe is created, but because it hasn't granted itself an ACE in the DACL on the pipe, it is locking itself out of obtaining handles to new instances of the pipe, which it needs to do as soon as a client request is received on the first instance. And there is clearly another bug in the IO completion code for the PipeConnectionListener, which causes this exception to recurse rather than faulting the service host.

    So, we have to make the service account itself an AllowedUser, to stop this happening.

    Posted Jun 23 2008, 02:08 PM by chrisdi with 11 comment(s)
    Filed under: , ,
  • Exploring the WCF Named Pipe Binding - Part 2

    In my previous post I explained how the named pipe for a WCF NetNamedPipe endpoint is named, and how a client discovers this name in order to connect to the service. This time, I'm looking at the Windows-level security.

    Both the named pipe itself, and the shared memory object used by the server to publish the name of the pipe to clients, are objects which Windows secures with Access Control Lists (ACLs). Let's look at the named pipe itself first of all...

    The ACL set up when WCF creates the named pipe looks like this in SDDL (Security Description Definition Language):

    D:(D;;FA;;;NU)(A;;0x12019f;;;WD)(A;;0x12019f;;;CO)

    The elements of this SDDL translate as follows:

    (D;;FA;;;NU) - Deny Full Access to NETWORK USERS - that is: deny the access rights specified by the access mask GENERIC_ALL, to any security context having membership of the group with well-known SID S-1-5-2

    (A;;0x12019f;;;WD) - Allow the access rights specified by the access mask 0x0012019f, to EVERYONE (the well-known SID S-1-1-0)

    (A;;0x12019f;;;CO) - Allow the access rights specified by the access mask 0x0012019f, to the well-known SID S-1-3-0 (CREATOR OWNER)

    The first entry enforces the rule that a WCF service endpoint with NetNamedPipe binding can only be accessed by a client process running on the same machine as the service. This is because any logon token created when a user is authenticated over a network protocol has the NETWORK USERS SID S-1-5-2 added to it by the system.

    The second ACE allows any authenticated user which is not a network logon to have the specified access to the named pipe. The access mask 0x0012019f corresponds to the following access rights:

    0x00100000 - SYNCHRONIZE

    0x00020000 - READ_CONTROL

    0x00000100 - FILE_WRITE_ATTRIBUTES

    0x00000080 - FILE_READ_ATTRIBUTES

    0x00000010 - FILE_WRITE_EA

    0x00000008 - FILE_READ_EA

    0x00000004 - FILE_CREATE_PIPE_INSTANCE

    0x00000002 - FILE_WRITE_DATA

    0x00000001 - FILE_READ_DATA

    More on this in a moment.

    The third ACE looks a bit odd to me. My understanding is that CREATOR OWNER is a placeholder SID which is really only relevant when a new security descriptor is being created for a new object using an existing descriptor as the pattern: if the template descriptor contains ACEs for the CREATOR OWNER SID, the corresponding ACEs in the security descriptor created for the new object have the SID for the principal which created the object. No logon token actually contains the CREATOR OWNER SID, as far as I know. Now, when an access check is being done against an ACL-protected object, only the ACEs which match a SID in the logon token are relevant to granting or denying permission. If I'm right that no logon token is ever going to contain the CREATOR OWNER SID, then this third ACE on the pipe's DACL will never have any function in an access check performed when a handle to the pipe is acquired. I suspect that the intention of the WCF developers was that this ACE would provide the access permissions for the service process whose channel listener created the pipe: but it doesn't do this, as I will demonstrate in a subsequent post.

    For the remainder of this post, let's focus on that second ACE, which grants permissions to the EVERYONE group. Did you raise an eyebrow at that FILE_CREATE_PIPE_INSTANCE permission? Do we really want EVERYONE to have permission to create an instance of the service's named pipe? No, we certainly do not! This is a bug in WCF which opens a serious security vulnerability.

    The problem is that any code at all, which is able to execute on the machine where the service lives, can call the Win32 API CreateNamedPipe with appropriate arguments and get a valid server-side handle to an instance of the WCF service's named pipe. It can then call ConnectNamePipe, whereupon it will be in direct competition with the actual service for incoming client connections to the service. Sooner or later some unsuspecting client trying to send a request to the service will be allocated to the instance of the pipe "owned" by the rogue process rather than one owned by the service.

    At best, the client's request to the service will just fail. But the rogue process might also read the data in the client's request; use the client's credentials by calling ImpersonateNamedPipeClient; or possibly return spoof response data to the client.

    We really need to do something about this, but what? Can we control the DACL which gets put on the pipe, when the service runtime is created? Let's deconstruct exactly where this happens...

    The  DACL applied to a named pipe is determined by the lpSecurityAttributes argument passed to Windows when CreateNamedPipe is first called:

    HANDLE WINAPI CreateNamedPipe(
      __in      LPCTSTR lpName,
      __in      DWORD dwOpenMode,
      __in      DWORD dwPipeMode,
      __in      DWORD nMaxInstances,
      __in      DWORD nOutBufferSize,
      __in      DWORD nInBufferSize,
      __in      DWORD nDefaultTimeOut,
      __in_opt  LPSECURITY_ATTRIBUTES lpSecurityAttributes
    );
    

    In WCF, this function is declared in System.ServiceModel.Channels.UnsafeNativeMethods, and is called by the private method CreatePipe() of System.ServiceModel.Channels.PipeConnectionListener, which is the implementation of IConnectionListener used by the service channel stack of the netNamedPipe binding. CreatePipe() is invoked when IConnectionListener.BeginAccept() is called by the service runtime. Our old friend Reflector shows us that the lpSecurityAttributes argument for CreateNamedPipe() is constructed in the PipeConnectionListener.CreatePipe method, using a hard-coded constant -1073741824, and a private member field, allowedSids, of type List<SecurityIdentifier>.

    That constant, -1073741824, is just 0xC0000000 in decimal, which is the value of GENERIC_READ|GENERIC_WRITE (defined in  WinNT.h). This specifies the access mask which is granted to each of the allowed SIDs. Generic access masks are translated by Windows into the corresponding standard and specific access mask bits applicable to the type of object being secured: in this case, the translated mask is the 0x0012019f we saw in the pipe DACL actually created.

    The list of allowed SIDs for the PipeConnectionListener is supplied in its constructor. If we look at the NamedPipeTransportBindingElement which defines how the transport channel is built for the netNamedPipe binding, we see that it too has a private List<SecurityIdentifier> field, called allowedUsers, and a corresponding internal property, AllowedUsers. So it looks as though the original intention of the WCF design was that the binding should define a set of SIDs which were to be allowed to access the pipe, and each one would get GENERIC_READ|GENERIC_WRITE access to the pipe. If this worked, it would not solve the problem that the DACL gives away FILE_CREATE_PIPE_INSTANCE rights to the pipe, but at least it would restrict access (including for that particular right) to a group of SIDs which the service configuration could control. This would be a big improvement on giving the right away to EVERYONE , even if it does not completely solve the problem.

    Unfortunately, the plumbing does not appear to be all there in the WCF bits to make this work: the allowedUsers in the binding element is not hooked up to the allowedSids of the PipeConnectionListener when the service runtime is built. In my next post, we'll look at ways to get round this.

    Posted Jun 16 2008, 07:04 PM by chrisdi with 8 comment(s)
    Filed under: , ,
Powered by Community Server (Commercial Edition), by Telligent Systems