Charteris Community Server

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

Chris Dickson's Blog

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.

Published Jun 23 2008, 02:08 PM by chrisdi
Filed under: , ,

Comments

 

2 Static » Blog Archive » Exploring the WCF Named Pipe Binding - Part 3 said:

Pingback from  2 Static  &raquo; Blog Archive   &raquo; Exploring the WCF Named Pipe Binding - Part 3

June 23, 2008 5:35 PM
 

Mathew said:

I am using Selfhost app and using custom binding code you wrote in this blog, I am not ablle to get type info,it is always giving me NULL,any idea what causing the problem.

private static Type namedPipeChannelListenerType

             = Type.GetType("System.ServiceModel.Channels.NamedPipeChannelListener, System.ServiceModel", false);

MY APP CODE IS:

 ServiceHost serviceHost = new ServiceHost(typeof(CalculatorService), new Uri("localhost/.../service"));

          AclSecuredNamedPipeBinding binding = new AclSecuredNamedPipeBinding();

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

          binding.AddUserOrGroup(allowedGroup);

          serviceHost.AddServiceEndpoint("Microsoft.ServiceModel.Samples.ICalculator", binding, "net.pipe://localhost/ServiceModelSamples/service");

               // Open the ServiceHost to create listeners and start listening for messages.

          serviceHost.Open();

July 10, 2008 6:58 PM
 

chrisdi said:

@Mathew: I was lazy when getting my demo code to work, and copied System.ServiceModel.dll locally, so the unqualified assembly name sufficed for me. If you change the string argument to reference the fully qualified name of the assembly then the loader will find it in the GAC:

... Type.GetType("System.ServiceModel.Channels.NamedPipeChannelListener, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", false);

July 10, 2008 7:43 PM
 

MADHU@MICROSOFT BLOG said:

(1)Chris wrote excellent blog about Named pipe Binding blogs.charteris.com/.../exploring-the-wcf-named-pipe-binding-part-1.aspx

July 11, 2008 9:42 PM
 

Pregnant Man » All About Named Pipe Binding said:

Pingback from  Pregnant Man &raquo; All About Named Pipe Binding

July 12, 2008 8:01 AM
 

Anmol said:

How did you call this from the client??

May 14, 2009 12:07 PM
 

chrisdi said:

@anmol: Any standard WCF style for the client-side stack should work. Just use the standard named pipe binding. For example, one way to do it all in code is this:

ChannelFactory<IYourServiceContract> cf = new ChannelFactory<IYourServiceContract>(

   new NetNamedPipeBinding(NetNamedPipeSecurityMode.Transport),

   new EndpointAddress("net.pipe://localhost/... etc")); // URL for your service

IYourServiceContract svcProxy = cf.CreateChannel();

svcProxy.DoServiceStuff(...);

(exception handling & disposal code omitted; and real code will probably use config for the endpoint/binding parameters)

May 14, 2009 4:10 PM
 

Anmol said:

it's gving me the error by saying that no end point found. I have implemented this into C# exe then i am calling this WCF custome channle application from C# DLL and then i am calling this DLL from VBScript which is giving me this problem.

May 19, 2009 9:44 AM
 

Luka said:

i added the administrators group(running the service as an administrator)

added my account as well but i keep getting the error:

"The server has rejected the client credentials."

Did anyone else fix this...and where exactly should i start looking :)

November 17, 2009 10:36 PM
 

Luka said:

Never mind...the problem was the address. net.tcp://blabla is bad...net.tcp://localhost/blabla is good :)

November 17, 2009 11:01 PM
 

Chris Dickson's Blog said:

In my previous post in this series, far longer ago than I intended , I showed how the WCF named pipe

December 4, 2009 5:53 PM
 

Chris Dickson's Blog said:

To finish where I left off last time , a quick look at the P/Invoke code we need in order to find the

November 1, 2010 1:50 PM
 

Chris Dickson's Blog said:

This is the third in a series of posts looking at the detail of how message exchanges occur over a WCF

November 24, 2010 1:44 PM
 

plumbing fitting said:

This program is interesting to study with, since I was a Computer Science student.

March 27, 2011 7:54 AM
 

What is the best choice for .net inter-process communication? - Programmers Goodies said:

Pingback from  What is the best choice for .net inter-process communication? - Programmers Goodies

July 6, 2011 5:47 AM

Leave a Comment

(required) 
(optional)
(required) 
Submit
Powered by Community Server (Commercial Edition), by Telligent Systems