|
-
My previous post looked at the Mandatory Integrity Control mechanism and what it means for named pipe communication in general. Now I turn to dealing with this issue in WCF. The scenario I particularly want to support is a WCF service hosted in a Windows Service process, providing service over a named pipe transport to client code hosted in an Internet Explorer low integrity process on the same machine. We’ll do this with a custom binding element that we can add to the standard NetNamedPipeBinding elements on the service side. This will hold an instance of the IntegrityLabel class described in my last post, representing the Mandatory Integrity label we want to specify for the pipe. Its job as a binding element will be to grab the channel listener just after it has been opened, find the pipe handle on which the service is listening, and apply the integrity label to its SACL. Here’s the basic framework: 1: public class NetNamedPipeSecurityExtensionsBindingElement : BindingElement
2: {
3: IntegrityLabel _pipeMandatoryIntegrityLabel;
4:
5: public NetNamedPipeSecurityExtensionsBindingElement(
6: IntegrityLabel pipeIntegrityLabelRequired)
7: {
8: _pipeMandatoryIntegrityLabel = new IntegrityLabel(pipeIntegrityLabelRequired.Level,
9: pipeIntegrityLabelRequired.Policy);
10: }
11:
12: public override BindingElement Clone()
13: {
14: return new NetNamedPipeSecurityExtensionsBindingElement(_pipeMandatoryIntegrityLabel);
15: }
16:
17: public override T GetProperty<T>(BindingContext context)
18: {
19: if (typeof(T) == typeof(IntegrityLabel))
20: {
21: return new IntegrityLabel(_pipeMandatoryIntegrityLabel.Level,
22: _pipeMandatoryIntegrityLabel.Policy) as T;
23: }
24: return context.GetInnerProperty<T>();
25: }
26:
27: public override IChannelListener<TChannel> BuildChannelListener<TChannel>(
28: BindingContext context)
29: {
30: IChannelListener<TChannel> listener = context.BuildInnerChannelListener<TChannel>();
31: listener.Opened += new EventHandler(OnChannelListenerOpened);
32: return listener;
33: }
34:
35: void OnChannelListenerOpened(object sender, EventArgs e)
36: {
37: ChannelListenerBase listener = sender as ChannelListenerBase;
38: if (null != listener)
39: {
40: // Find a suitable pipe handle and apply the integrity label
41: }
42: }
43: }
The difficult part is finding the pipe handle, as it is not held by the channel listener. The channel listener can be though of as a listener for logical channels arriving at an endpoint: it is not directly concerned with the physical details of the transport layer. One reason for this is that on a connection-oriented transport like Named Pipes or TCP/IP, there is not a direct one-to-one correspondence between physical connections and channels – connections may be pooled and reused. So, in addition to the channel listener, there is also a transport connection listener. The two are connected indirectly via an internal WCF abstraction called a TransportManager. The relationship is illustrated in the following diagram.

As I have noted in the diagram, there are two different TransportManager types used for Named Pipes in WCF. Up to now I have focussed solely on the one used for self-hosted services. The hosted transport manager is rather more obscure to understand, and I have not yet tackled that.
For the self-hosted scenario, in order to find the pipe handle, we need to:
- locate the transport manager table: for the Named Pipe transport, this is a singleton held in a static field of one of the base classes for the channel listener.
- look up in the transport manager table the appropriate transport manager for the URI on which our endpoint is listening: in our self-hosted scenario this will be an instance of ExclusiveNamedPipeTransportManager.
- find the IConnectionListener object stored in a field of the TransportManager: this may not be the actual transport connection listener, but could be some other type wrapping the inner connection listener (for instance, a wrapper to implement tracing when WCF tracing is enabled).
- to get the actual PipeConnectionListener instance, we need to walk down the chain of IConnectionListener instances, in “Russian Doll” fashion, until we find the one we are after.
- Enumerate the PendingAccept list associated with the PipeConnectionListener. If we are not too unlucky, we should find that one of these contains the first pipe handle created when the channel listener was opened, and we can use this to apply the integrity label.
All this involves dealing extensively with WCF internal types, and in several cases with private member fields thereof. So it all has to be done using reflection and so comes with a big health warning that WCF service packs may come along and break everything. For this reason I wrap the reflection code in a series of Wrapper types which mirror the internal WCF types, and at every step in my actual code I validate carefully that everything looks as I expect it to – though I have omitted much of the error checking in the extracts I show here, just to make it more readable.
First I have a wrapper for the TransportManagerTable, which encapsulates the logic of finding the table and looking up the appropriate TransportManager for the endpoint. The lookup is complicated by the fact that the table uses an internal WCF type, BaseUriWithWildcard, to key its entries. This type is based on the endpoint URI, but also takes into account the HostNameComparisonMode configured for the binding. In order to perform the lookup we need to construct an instance of this key type using reflection to invoke the appropriate internal constructor.
1: public class TransportManagerTableWrapper
2: {
3: private object _transportManagerTable;
4: private IList _tableContents;
5: private PropertyInfo _contentKeyProperty;
6: private PropertyInfo _contentValueProperty;
7: private Type _contentKeyType;
8: private HostNameComparisonMode _hostNameComparisonMode;
9:
10: public TransportManagerTableWrapper(ChannelListenerBase channelListener)
11: {
12: PropertyInfo pi = channelListener.GetType().GetProperty("TransportManagerTable",
13: BindingFlags.Instance | BindingFlags.NonPublic);
14: _transportManagerTable = pi.GetValue(channelListener, null);
15: _tableContents = _transportManagerTable.GetType().GetMethod("GetAll").Invoke(
16: _transportManagerTable, null) as IList;
17: Type tableContentsItemType = _tableContents[0].GetType();
18: var contentItemPairTypes = tableContentsItemType.GetGenericArguments();
19: _contentKeyProperty = tableContentsItemType.GetProperty("Key");
20: _contentValueProperty = tableContentsItemType.GetProperty("Value");
21: _contentKeyType = contentItemPairTypes[0];
22: PropertyInfo hncmProperty = channelListener.GetType().GetProperty(
23: "HostNameComparisonMode", BindingFlags.Instance|BindingFlags.Public);
24: _hostNameComparisonMode = (HostNameComparisonMode)hncmProperty.GetValue(
25: channelListener, null);
26: }
27:
28: public TransportManagerWrapper GetTransportManager(Uri listenUri)
29: {
30: object keyBaseUriWithWildcard = CreateBaseUriWithWildcard(listenUri);
31: object transportManager = null;
32: foreach (object kvPair in _tableContents)
33: {
34: if (_contentKeyProperty.GetValue(kvPair, null).Equals(keyBaseUriWithWildcard))
35: {
36: transportManager = _contentValueProperty.GetValue(kvPair, null);
37: break;
38: }
39: }
40: if (listenUri.Scheme == Uri.UriSchemeNetPipe)
41: {
42: return new NamedPipeTransportManagerWrapper(transportManager);
43: }
44: return new TransportManagerWrapper(transportManager);
45: }
46:
47: private object CreateBaseUriWithWildcard(Uri listenUri)
48: {
49: BindingFlags ctorBindingFlags = BindingFlags.Instance;
50: ctorBindingFlags |= (Environment.Version.Major >= 4) ?
51: BindingFlags.Public : BindingFlags.NonPublic;
52: ConstructorInfo ctor = _contentKeyType.GetConstructor(
53: ctorBindingFlags, null,
54: new Type[] { typeof(Uri), typeof(HostNameComparisonMode) },
55: null);
56: return ctor.Invoke(new object[]{listenUri, _hostNameComparisonMode});
57: }
58: }
The NamedPipeTransportManagerWrapper type returned by the GetTransportManager method shown above encapsulates the logic for the next stage – walking the chain of IConnectionListener types which it encloses. Here is the key method (again note that required error checking is missing from this sample code):
1: public PipeConnectionListenerWrapper PipeConnectionListener
2: {
3: get
4: {
5: FieldInfo connectionListenerFieldInfo = WrappedType.GetField("connectionListener",
6: BindingFlags.Instance | BindingFlags.NonPublic);
7: if (null != connectionListenerFieldInfo)
8: {
9: object connectionListener = connectionListenerFieldInfo.GetValue(
10: _transportManager);
11:
12: // connectionListener may be wrapping another IConnectionListener type
13: // we need the innermost IConnectionListener
14: int recursionCountLeft = 10;
15: while (recursionCountLeft > 0
16: && null == connectionListener.GetType().GetField(
17: "pendingAccepts",
18: BindingFlags.Instance | BindingFlags.NonPublic))
19: {
20: recursionCountLeft--;
21: FieldInfo[] fields = connectionListener.GetType().GetFields(
22: BindingFlags.Instance | BindingFlags.NonPublic);
23: foreach (FieldInfo f in fields)
24: {
25: if ("IConnectionListener".Equals(f.FieldType.Name))
26: {
27: connectionListener = f.GetValue(connectionListener);
28: break;
29: }
30: }
31: }
32: return new PipeConnectionListenerWrapper(connectionListener);
33: }
34: else
35: {
36: // unsupported transport manager type
37: // e.g. HostedNamedPipeTransportManager
38: throw ...
39: }
40: }
41: }
Finally, we have a PipeConnectionListenerWrapper, which encapsulates an iterator for the pipe handles held by PendingAccept instances, within the connection listener:
1: public IEnumerable<SafeHandle> PendingPipes
2: {
3: get
4: {
5: IList pendingAccepts = null;
6: FieldInfo pendingAcceptField = _wrappedType.GetField("pendingAccepts",
7: BindingFlags.Instance | BindingFlags.NonPublic);
8: if (null != pendingAcceptField)
9: {
10: pendingAccepts = pendingAcceptField.GetValue(_pipeConnectionListener) as IList;
11: }
12: foreach (object pendingAccept in pendingAccepts)
13: {
14: var pipeHandle = pendingAccept.GetType().GetField("pipeHandle",
15: BindingFlags.Instance|BindingFlags.NonPublic).GetValue(pendingAccept);
16: yield return pipeHandle as SafeHandle;
17: }
18: }
19: }
Using these helper classes we can now complete the implementation of our binding element class, by doing the following in the OnChannelListenerOpened event handler:
1: TransportManagerTableWrapper transportManagerTable =
2: new TransportManagerTableWrapper(listener);
3: NamedPipeTransportManagerWrapper transportManager =
4: transportManagerTable.GetTransportManager(listener.Uri)
5: as NamedPipeTransportManagerWrapper;
6: PipeConnectionListenerWrapper pipeConnectionListener =
7: transportManager.PipeConnectionListener;
8: bool successfullyAppliedLabel = false;
9: List<Win32Exception> exceptions = new List<Win32Exception>();
10: foreach (SafePipeHandle pipeHandle in pipeConnectionListener.PendingPipes)
11: {
12: try
13: {
14: _pipeMandatoryIntegrityLabel.ApplyToResource(pipeHandle);
15: successfullyAppliedLabel = true;
16: break;
17: }
18: catch (Win32Exception exception)
19: {
20: exceptions.Add(exception);
21: }
22: }
23:
24: // ... do something with errors if we didn't succeed in setting the MI label
It all seems to work, based on the testing I have done to date, at least to support the scenario I outlined in the introduction above. I hope someone else may find this useful. Any feedback would be most welcome.
EDITED 8-3-11: Minor change to TransportManagerTableWrapper to support .NET 4.0 as well as .NET 3.5
|
-
The release of Windows Vista introduced a completely new dimension to the Windows operating system’s security model for determining what code executing in user mode can do. In addition to the concepts of access rights, and system privileges, user mode processes are now also constrained by the “integrity level” with which they are associated. This integrity level is determined by a special Access Control Entry (ACE) called the Mandatory Integrity Label, added to the System Access Control List (SACL) of the process token, or implied as the default integrity level if no explicit mandatory label ACE is present. Most user mode process tokens do not have an integrity label and are therefore given the default integrity level of “Medium”; processes started using “Run as Administrator…” (or in an administrator’s interactive login session) have “High” integrity level. Services running as Local System have an even higher integrity level: “System”. Internet Explorer always runs in a Low integrity process. What difference does this make? Well, the key point of this mechanism is that all securable objects also have an associated integrity level, and write access to such objects is constrained by the operating system so that regardless of the discretionary access rights in the object’s access control list, no process is allowed to perform a write operation on an object with a higher integrity level than its own access token. My interest in this was sparked by enquiries from people who needed inter-process communication between a low integrity process (invariably IE hosting their code in some way) and another custom process such as a windows service or a tray applet, and were hoping to use WCF with the named pipe binding to implement this. Such a scheme worked fine on Windows XP but broke when moved to Vista or Windows 7. This is because named pipes are securable objects provided by the operating system kernel, and the pipes created by the WCF binding have no integrity label. They are therefore implicitly treated by default as having medium integrity level and are consequently inaccessible to low integrity processes regardless of what access permissions the DACL grants. Not one to be deterred by apparent constraints in WCF’s implementation, I set about investigating whether and how this scenario could be supported. The good news is that I found a way, which I will explain over the course of a few posts. The bad news is that it involves some fairly hairy use of reflection over the WCF types, so Microsoft support won’t be too enthusiastic about helping if it’s used in production, and it will be at the mercy of changes to implementation details in future .NET versions and service packs. The first step is to fill in the gaps in the .NET library types as regards controlling the mandatory integrity label associated with a securable object. The .NET security types for handling security descriptors, access control lists and access control entries are entirely ignorant of the Mandatory Integrity Control mechanism introduced by Windows Vista, so we need to P/Invoke Win32 APIs directly in order to do anything with integrity labels. My solution is a class, IntegrityLabel, which can be used like this: 1: // Read the current integrity label of a NamedPipeServerStream's pipe, if it has one
2: IntegrityLabel currentLabel = IntegrityLabel.GetCurrentIntegrityLabelForResource(
3: pipe.SafePipeHandle,
4: ResourceType.KernelObject);
5: Console.WriteLine(
6: null != currentLabel ?
7: String.Format("Pipe MIC label: {0}", currentLabel.Sddl) :
8: "Pipe has no MIC label");
9:
10: // Output is "S:(ML;;NW;;;LW)" if pipe is marked low integrity
or this:
1: // Add a Low Integrity mandatory label to a named pipe's security descriptor
2: // (must be a handle to the first server pipe instance)
3: IntegrityLabel lbl = new IntegrityLabel(IntegrityLabel.IntegrityLevel.Low);
4: lbl.ApplyToResource(pipe.SafePipeHandle, ResourceType.KernelObject);
5:
6: // This enables low integrity processes to write to the pipe (if the pipe DACL allows).
7: // Be sure to validate all data arriving via the pipe very carefully.
I am not going to post the entire code for my IntegrityLabel, as it is still work-in-progress for the moment, but I will sketch out how I have implemented it.
Reading the label and instantiating IntegrityLabel from it, as in the first example, involves a call to the Win32 API GetSecurityInfo, specifying the new value LABEL_SECURITY_INFORMATION (0x00000010) for the SECURITY_INFORMATION flag. The SACL returned by the function is then copied and parsed. While writing this I found that there are some public classes in the namespace System.Security.AccessControl which can be leveraged to avoid too much hand-parsing of the security structures. For example, I have a constructor like this which consumes the binary SACL obtained from the Win32 call:
1: public IntegrityLabel(byte[] saclBinaryForm, int offset)
2: {
3: GenericAce ace = null;
4: if (null != saclBinaryForm)
5: {
6: RawAcl aclRaw = new RawAcl(saclBinaryForm, 0);
7: if (0 >= aclRaw.Count) throw new ArgumentException("No ACEs in ACL", "saclBinaryForm");
8: ace = aclRaw[0];
9: if (Win32.SYSTEM_MANDATORY_LABEL_ACE_TYPE != (int)ace.AceType)
10: throw new ArgumentException("No Mandatory Integrity Label in ACL", "saclBinaryForm");
11: byte[] aceBytes = new byte[ace.BinaryLength];
12: ace.GetBinaryForm(aceBytes, 0);
13: _policy = new IntegrityPolicy(aceBytes, 4);
14: _level = new IntegrityLevel(aceBytes, 8);
15: return;
16: }
17: throw new ArgumentNullException("saclBinaryForm");
18: }
The type System.Security.AccessControl.RawAcl saves me the trouble of boilerplate parsing of the ACL structure’s header, and gives me an instance of System.Security.AccessControl.GenericAce which ensures that I don’t mess up the extraction of the access control entry (ACE) structure which holds the integrity label. This GenericAce object is actually of type CustomAce, a type is specifically designed to support extending the existing System.Security.AccessControl types to new ACE types not defined when this was written, in this case the SYSTEM_MANDATORY_LABEL_ACE structure. CustomAce exposes the basic details of the ACE header but can’t help with the specifics of the SYSTEM_MANDATORY_LABEL_ACE structure, so I am on my own again to pick out the integrity policy mask (parsed in the constructor of my nested IntegrityPolicy class) and to find the start of the SID which defines the integrity level to which the ACE relates.
Here is the IntegrityPolicy constructor used when reading the binary form obtained from the Win32 API: it just interprets the integrity policy bitmask. Internally I store the policy in its SDDL string form:
1: internal IntegrityPolicy(byte[] binaryForm, int offset)
2: {
3: int accessMask = BitConverter.ToInt32(binaryForm, offset);
4: StringBuilder sb = new StringBuilder(4);
5: if (0 != (accessMask & Win32.SYSTEM_MANDATORY_LABEL_NO_WRITE_UP)) sb.Append("NW");
6: if (0 != (accessMask & Win32.SYSTEM_MANDATORY_LABEL_NO_READ_UP)) sb.Append("NR");
7: if (0 != (accessMask & Win32.SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP)) sb.Append("NX");
8: _sddl = sb.ToString();
9: }
Similarly, the IntegrityLevel constructor which interprets the binary form data. This time it is one of the SIDs which define the integrity levels recognised by the MIC mechanism, and the System.Security.Principal.SecurityIdentifer framework type provides the necessary parsing and interpretation. Again, I store the level in the form it appears in SDDL strings:
1: internal IntegrityLevel(byte[] binaryForm, int offset)
2: {
3: SecurityIdentifier sid = new SecurityIdentifier(binaryForm, offset);
4: if ("S-1-16-4096".Equals(sid.Value))
5: {
6: _sddl = "LW";
7: }
8: else if ("S-1-16-8192".Equals(sid.Value))
9: {
10: _sddl = "ME";
11: }
12: else if ("S-1-16-12288".Equals(sid.Value))
13: {
14: _sddl = "HI";
15: }
16: else if ("S-1-16-16384".Equals(sid.Value))
17: {
18: _sddl = "SI";
19: }
20: else throw new ArgumentException(String.Format("Invalid SID for integrity level: {0}", sid.Value));
21: }
We now turn to the other use case, writing an integrity label to the SACL of an object. The key Win32 API here is SetSecurityInfo:
1: public unsafe void ApplyToResource(SafeHandle handle, ResourceType objectType)
2: {
3: fixed (byte* pSacl = GetSaclBinaryForm())
4: {
5: int result = Win32.SetSecurityInfo(
6: handle,
7: ResourceType.KernelObject,
8: Win32.LABEL_SECURITY_INFORMATION,
9: null, null, null, pSacl);
10: if (0 != result)
11: {
12: throw new Win32Exception(result);
13: }
14: }
15: }
Currently I am implementing GetSaclBinaryForm by round-tripping the SDDL representation of the label via the Win32 APIs ConvertStringSecurityDescriptorToSecurityDescriptor and GetSecurityDescriptorSacl, but I may change everything to use the binary representation internally. Anyway, the key here is that the SACL structure provided to SetSecurityInfo should contain just the required MIC label.
Armed with my IntegrityLabel type, it takes just the two lines of code shown above to lower the integrity level of a resource such as a named pipe. In order for the call to succeed, the handle supplied to IntegrityLabel.ApplyToResource() must have been opened with WRITE_OWNER permission enabled. In the case of named pipe objects, this requirement introduces a small gotcha: in the CreateNamedPipe API, the required access permissions for the pipe handle are specified in the dwOpenMode argument, and this API overloads the meaning of the WRITE_OWNER bit flag 0x80000, to mean FILE_FLAG_FIRST_PIPE_INSTANCE as well as WRITE_OWNER. It has a certain logic to it, certainly, but the upshot is that calling SetSecurityInfo to specify a MIC integrity label can only ever succeed using a handle to the first server instance of a named pipe, and not a handle to a subsequent instance.
Next time we’ll look at using this knowledge, together with the IntegrityLabel class, to locate and lower the integrity of the pipe created by the WCF NetNamedPipeBinding.
|
-
This is the third in a series of posts looking at the detail of how message exchanges occur over a WCF channel using the named pipe binding. In the first post of the series, we introduced the Framing Protocol. In the second we looked into the protocol’s upgrade mechanism whereby a security context for message exchange is negotiated, if Transport security is configured on the binding. In this post I want to consider what best practice might be as regards the choice of configuration options for security when using NetNamedPipeBinding. First, let’s review the options: - We can include Transport security in the binding, or exclude it completely (SecurityMode=None)
- If we include Transport security:
- We have three possible settings for the ProtectionLevel, which affects whether the messages passed are signed/encrypted. These are: None; Sign; and EncryptAndSign
- We have two effective options for the AllowedImpersonationLevel, Identification (server can identify the client) and Impersonation (server can access local resources as the client). We can also set this level to Delegation on the client, but it has the same effect as setting Impersonation.
Choosing protection level As we saw in my last post, these settings determine what happens to messages as they pass from the stream upgrade provider in the client-side channel stack to the stream upgrade provider in the service-side channel stack. Signing gives message integrity assurance – i.e. that the message bytes are not changed en route between these two points. Encryption gives message privacy assurance – i.e. that an eavesdropper cannot read the bytes as they travel between these two points and learn the contents of the messages. Those are the “benefits”. What about the costs? We saw that each of these functions involves an SSPI call into the security provider from each of the upgrade providers, in respect of every message, thus involving at least two context switches between user mode and kernel mode for every message. The nature of the functions is that they perform cryptographic operations on the message data, typically not a trivial workload in terms of CPU clock cycles. So how do we balance the cost and benefit to decide whether we need the signing and encryption services? It seems to me that the supposed benefits are minimal. When remote connections are precluded, as they are by the ACLs created by the binding, named pipes are mechanisms implemented by the operating system entirely in memory. When messages are sent via a pipe there is no network or file IO involved. There is no way to eavesdrop on a particular open pipe instance being used to transfer data, or write data to it, unless one can obtain the handle to the specific instance concerned, and WCF does not provide any obvious means to discover the handle of a currently open pipe being used by the binding. In short, the only possible attacker you are going to be foiling by signing or encrypting the messages is someone who has already succeeded in attaching a debugger to your client or service process, or who has attached a kernel debugger, and who has also expended a lot of time discovering and attacking the pipe, when it would probably be far simpler to look at or change the data in the messages before they are signed/encrypted or after the binding has decrypted them. So I have come to the conclusion that I cannot conceive any scenario where the ProtectionLevel settings Sign and EncryptAndSign are useful when using the standard NetNamedPipeBinding. If anyone can suggest any, I would welcome your comments. So in my view our options are narrowed to: - Use Transport security with ProtectionLevel=None, if you need the service to identify or impersonate its clients
- Dispense with Transport security completely if your service does not need to identify or impersonate its clients
Choosing impersonation level The key questions here are (1) does the service need to know the identity of each individual client using it; and (2) does the service need to access any resource on the local machine using the client identity. This is an application-specific question and there can be no generic answers. If the answer is yes to either question, the simplest way to implement the required functionality is to use Transport security with ProtectionLevel=None. In client config, set the ClientCredentials.Windows.AllowedImpersonationLevel to Indentification if only the identity of the client is required by the service, or to Impersonation if the service needs to access a resource acting as the client. Dispensing with Transport security If the answer is “no“ to both the above questions, then we don’t need WCF’s Transport security at all. Although it sounds slightly scary to be “dispensing with Transport security”, as I’ve explained above this doesn’t mean one is removing all security: the named pipe transport is inherently just as secure without the WCF-induced SSPI security layered on top of it. What is more, you can still control access to the service effectively by leveraging the ACL associated with the pipe, as I have shown here: I regard the ACL as an important part of the true “Transport security” provided by this message transport. Getting rid of WCF’s Transport security corresponds to setting the security mode of the binding (NetNamedPipeSecurityMode) to “None” instead of “Transport”, either in the config file or in code (in the constructor of the instance of the binding, for example). When you do this, the upgrade phase of the Framing Protocol is not invoked at all by the client-side channel: there is no longer an Upgrade Provider in the channel stack. One caveat: although the upgrade mechanism is an optional part of the Framing Protocol preamble, this is not a detail negotiated as part of the protocol. So both client and service must have agreed beforehand whether Transport security is being used, and the binding configuration must match on each side. WCF is not very forgiving if there is a mismatch of expectations, and it may not be immediately apparent what is wrong. For example, take a client configured with security mode None and a service configured with security mode Transport. The client channel stack will fail to send the Framing Protocol’s UpgradeRequest record at the point where the WCF service implementation is expecting it. When this occurs, what actually happens is that the service aborts the channel and the pipe instance is abruptly closed. No framing protocol Fault Record is sent back to the client to point out the error of its ways: from the client’s point of view this is a pity, since all that the client will see is an IO error when it attempts to read the pipe. The opposite configuration mismatch is not so much of a problem for the client, as the exception seen is fairly clear and explanatory: System.ServiceModel.ProtocolException: The requested upgrade is not supported by 'net.pipe://localhost/wcfdemonpserver/NPService'. This could be due to mismatched bindings (for example security enabled on the client and not on the server).
|
-
This is the second in a series of posts looking at the detail of how message exchanges occur over a WCF channel using the named pipe binding. In the first post of the series, we introduced the Framing Protocol, saw how it included an upgrade phase whereby the client and server negotiate a security context within which message exchange over the named pipe will occur, and demonstrated how the .NET Framework class System.Net.Security.NegotiateStream could be used to implement the protocol exchanges involved in such an upgrade. This time we will look into exactly what is going on in this upgrade process, and how it can be controlled. Continuing to use my spoof service to listen on the pipe and spy on the data stream from the client, as described previously, this is what we see if we read the contents of the Upgrade Record instead of using the .NET NegotiateStream to process it:  The first five bytes we read from the pipe initiate Microsoft’s .NET Negotiate Stream protocol. The .NET Negotiate Stream protocol specifies how the client and server exchange security tokens to negotiate a security context. Underlying this stream protocol is the Security Support Provider Interface (SSPI), which is Microsoft’s implementation of the public-domain Generic Security Services API, designed to abstract security providers away from application code. The essential pattern is that security tokens are generated by each side’s security provider and passed to the other party’s security provider, the exchange of tokens continuing until both security providers agree that a valid security context has been established, or one or other decides that a suitable context cannot be negotiated. These bytes can be interpreted as follows: | 0x16 | Indicates that this is a .NET Negotiate Stream Handshake Message of type HandshakeInProgress | | 0x01, 0x00 | Specify that version 1.0 of the .NET Negotiate Stream protocol is being used | | 0x00, 0x37 | Specify that the AuthPayload field of the message, which follows, is 55 (0x37) bytes long | The second read from the pipe retrieved the 55 bytes of the AuthPayload field, which I have partly redacted to remove the domain and workstation names in my environment. These bytes constitute the first security token passed from the client to the server as part of the SSPI handshake.To generate this, the client-side WCF channel stack has called the SSPI functions AcquireCredentialsHandle, specifying that the Negotiate security provider should be used, and then InitializeSecurityContext, to obtain the first security token to be passed to the server. The Negotiate security provider has decided that the actual security package to be used for authentication is the NTLM provider (it will always do this when the target server is the same machine), and it is an NTLM token which we see being passed. The NTLM authentication protocol is documented on MSDN, and from this we can see how the token works. The first eight bytes identify it as an NTLM token. The following 4 bytes specify that the token is an NTLM Negotiate message. The next 4 bytes (0xb7, 0xb2, 0x18, 0xe2)) comprise a NTLM NEGOTIATE structure, which is a bit field specifying the options which the client is proposing as characteristics of the security context. Translating these in terms of the Win32 symbolic constants for the option flags, shows the following: | 0xb7 | 10110111 | NTLMSSP_NEGOTIATE_LM_KEY, NTLMSSP_NEGOTIATE_SEAL, NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_REQUEST_TARGET, NTLMSSP_NEGOTIATE_OEM, NTLMSSP_NEGOTIATE_UNICODE | | 0xb2 | 10110010 | NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED, NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED, NTLMSSP_NEGOTIATE_NTLM | | 0x18 | 00011000 | NTLMSSP_NEGOTIATE_IDENTIFY, NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY | | 0xe2 | 11100010 | NTLMSSP_NEGOTIATE_56, NTLMSSP_NEGOTIATE_KEY_EXCH, NTLMSSP_NEGOTIATE_128, NTLMSSP_NEGOTIATE_VERSION | The flags which directly correspond to configuration of the WCF client’s NetNamedPipe binding are highlighted: - NTLMSSP_NEGOTIATE_IDENTIFY: Specifies that the client is willing to allow the server to identify him, but is not prepared to allow the server to impersonate him. This corresponds to the enumerated value System.Security.Principal.TokenImpersonationLevel.Identify. If this flag were not present, the client would be allowing the equivalent of System.Security.Principal.TokenImpersonationLevel.Impersonation
- NTLMSSP_NEGOTIATE_SEAL: Requires that the messages to be exchanged in the proposed security context will all be encrypted, and…
- NTLMSSP_NEGOTIATE_SIGN: Requires that the messages to be exchanged in the proposed security context will contain a cryptographically calculated signature to assure message integrity. These two flags correspond to the enumeration values in System.Net.Security.ProtectionLevel: if they are both absent, ProtectionLevel.None; if both present, ProtectionLevel.EncryptAndSign; if just NTLMSSP_NEGOTIATE_SIGN, then ProtectionLevel.Sign
As I said, these flags represent the proposals the client side is making as to what characteristics the negotiated security context should have. When the token is received at the server end (i.e. in the stream upgrade provider of the Service’s channel stack), it is passed into the SSPI function AcceptSecurityContext, which generates the token which the server will pass back to the client. In my test harness, the token returned through the pipe by the Service looks like this, again redacted: We can see this has the same signature at the start, identifying it as an NTLM token. In fact it is the server Challenge token, containing a nonce and a series of attribute properties identifying the server. The four bytes highlighted are the server’s version of the NEGOTIATE structure, which differ somewhat from the value sent by the client. Translating them gives: | 0x35 | 00110101 | NTLMSSP_NEGOTIATE_SEAL, NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_REQUEST_TARGET, NTLMSSP_NEGOTIATE_UNICODE | | 0xc2 | 11000010 | NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMSSP_NEGOTIATE_NTLM | | 0x99 | 10011001 | NTLMSSP_NEGOTIATE_TARGET_INFO, NTLMSSP_NEGOTIATE_IDENTIFY, NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY, NTLMSSP_TARGET_TYPE_DOMAIN | | 0xe2 | 11100010 | NTLMSSP_NEGOTIATE_56, NTLMSSP_NEGOTIATE_KEY_EXCH, NTLMSSP_NEGOTIATE_128, NTLMSSP_NEGOTIATE_VERSION | The key flags we highlighted before show that the server has agreed the client’s proposal that messages should be both encrypted and signed, and that the security context should only allow the client to be identified, not impersonated. The flags highlighted in green have been added by the server, to confirm that it is including information about itself (the “target” of the client’s request) within the properties of this token, and that this information includes a Domain name, not just a server name. The client-side stream upgrade provider reads this token from the pipe and passes it into another call to the SSPI function InitializeSecurityContext. This returns a third token, the NTLM Authenticate token, which the client passes back through the pipe to the server. This contains the hash data which enables the server to validate the client’s identity and discover the keys to be used for signing and encryption. When the token is passed by the server-side stream upgrade provider back into the SSPI AcceptSecurityContext function, provided everything is in order the handshake will be complete and the security context for further message exchange established. Controlling TokenImpersonationLevel There is some misinformation on MSDN which suggests that TokenImpersonationLevel.Identification is the only impersonation level supported by the NetNamedPipe binding. This is not the case: on the client side, the NTLMSSP_NEGOTIATE_IDENTIFY flag in the NTLM handshake can be controlled via the ClientCredentials instance associated with the client channel stack. Using the default instance, TokenImpersonationLevel.Identification is specified, so the flag is set. But if the ClientCredentials are configured explicitly for TokenImpersonationLevel.Impersonation, this flag is removed from the NTLM handshake message. 1: ChannelFactory<IMyService> myChannelFactory = new ChannelFactory<IMyService>(myBinding, myEndpoint);
2: myChannelFactory.Credentials.Windows.AllowedImpersonationLevel
3: = System.Security.Principal.TokenImpersonationLevel.Impersonation;
On the service side, the service bindings will not complain about the absence of the NTLMSSP_NEGOTIATE_IDENTIFY flag, with the result that the logon token generated when the security context is established is a valid impersonation token which the service may be able to use to impersonate the client identity. This surfaces in the service implementation as the property ServiceSecurityContext.WindowsIdentity.ImpersonationLevel being set to “Impersonation” instead of “Identification”. Note that the OperationBehavior ImpersonationOption property has no effect on the WindowsIdentity impersonation level: it is only relevant to Message security.
Whether this truly allows the service to impersonate the client depends on whether the service’s process possesses the SeImpersonatePrivilege security privilege. If it does, then resources to which the client’s account has access can be accessed by the service even though the service process’s own account does not have access:
1: using (WindowsImpersonationContext impCtx
2: = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Impersonate())
3: {
4: // Access resources using the client's identity
5: // E.g. read from client User's Documents folder
6: }
Note that there is no way to achieve TokenImpersonationLevel.Delegation in the ServiceSecurityContext: if you set TokenImpersonationLevel.Delegation on the client channel’s ClientCredentials, nothing breaks, but the effect on the server side is identical: the ServiceSecurityContext will have TokenImpersonationLevel.Impersonation.
Edit 19-Nov-2010: A final note on Impersonation. Because the client and service are on the same machine, when using the NetNamedPipeBinding Impersonation does not mean exactly what the WCF documentation suggests that it means i.e. local resource access only. In fact the service can also access remote resources such as network file shares using the client’s identity, but cannot delegate it (so a remote service invoked from the local service’s implementation while it was impersonating the client could not in turn use the client credentials to impersonate the client). Impersonation really means that the client identity is valid for resource access up to one network hop away from where the client logon was initially obtained. The same concept exists in the unmanaged world, using DCOM: the concept hasn’t changed, it’s just that the WCF documentation isn’t quite as clear as the Win32 and COM documentation.
Controlling ProtectionLevel
The ProtectionLevel associated with Transport security of the NetNamedPipeBinding can be set in the WCF sections of the configuration file, or directly via the binding in code. For example, the following code would result in the NTLMSSP_NEGOTIATE_SIGN flag being set, but the NTLMSSP_NEGOTIATE_SEAL flag being clear, when a client-side channel using this binding initiated the NTLM handshake:
1:
2: NetNamedPipeBinding stdBinding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.Transport);
3: stdBinding.Security.Transport.ProtectionLevel = System.Net.Security.ProtectionLevel.Sign;
If the ProtectionLevel configured on the server side binding requires greater security than the settings required by the client, the handshake fails and an exception is thrown when the channel is opened:
System.ServiceModel.Security.SecurityNegotiationException: A remote side security requirement was not fulfilled during authentication. Try increasing the ProtectionLevel and/or ImpersonationLevel.
If however, the client settings are “stronger” than the server settings, the server automatically adjusts to accept the security proposed by the client. For example, if the client bindings specify ProtectionLevel.Sign and the service bindings specify ProtectionLevel.None, the NTLM handshake will succeed in establishing a security context in which all messages exchanged carry a signature, but are not encrypted.
That concludes my deep dive into the Transport security aspects of the NetNamedPipeBinding, but before I leave the topic of security in this binding, I want to take a step back and review how we ought to be using it in practice. That’s for my next post.
|
-
This is the first in a short series of posts looking into the detail of how message exchanges occur over a WCF channel using the named pipe binding. Although I first introduced this question in the context of a security vulnerability of this binding in .NET 3.5, it is of more general interest, judging by the number of questions I have seen in newsgroups and the blogosphere wondering about interoperability with WCF over a named pipe transport. In particular, anyone who aspires to: - implementing a non-.NET client to communicate with a WCF service over named pipes; or
- implementing a non-.NET named pipe server which WCF clients can interoperate with using the named pipe binding;
will have to grapple with each of the issues I will discuss in this series. By way of introduction, let’s develop the pipe listener code I showed in my previous post, to look at the data which arrives from the client immediately after the connection is established. This time we’ll read and display on the console the data sent by the client: using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, -1)) // Calls CreateNamedPipe { pipeServer.WaitForConnection(); // Calls ConnectNamedPipe byte[] buffer = new byte[0x1000]; int readThisTime = pipeServer.Read(buffer, 0, 0x1000); Console.WriteLine("{0} bytes received:\n", readThisTime); OutputBufferFormatted(buffer, readThisTime); } OutputBufferFormatted is a formatting function I wrote to dump both the hex and character representation of the bytes received. I won’t bore you with the gory details. This is the output I saw when my WCF client first connected to this listener: So, what’s going on here?… eight bytes in I recognise the URL as being the service URL which my client was expecting to communicate with, but why has the client sent that in this way, and what are those other bytes all about? The answer is to be found in the .NET Message Framing Protocol. This is a protocol designed by Microsoft for framing a messaging interaction so as to provide support for a variety of message encoding formats and transport stream semantics, delimit message boundaries, and enable malformed messages to be skipped. It is used by three of the standard WCF bindings NetTcpBinding, NetMsmqBinding and the binding we are currently interested in, the NetNamedPipeBinding (or rather, more accurately, their respective transport binding elements NetTcpBindingElement, etc). The protocol is published in MSDN. From it we can readily interpret the data sent by our client as follows: | 0x0, 0x1, 0x0 | Version record: Version 1.0 of the Message Framing Protocol is being used by the client | | 0x1, 0x2 | Mode record: The client is using the protocol in its Duplex mode. This initiates the protocol for multiple bidirectional messages. | | 0x2, 0x2e, net.pipe://lo… | Via record: The byte value 0x2e specifies the length of the URL. The URL specifies the destination of the subsequent messages which will be framed in the protocol. | | 0x3, 0x8 | Known Encoding record: The byte value 0x8 indicates that the client will be using “Binary encoding with in-band dictionary”. We’ll touch on what that means later. | Taken together, these comprise the first four, mandatory, records of the client’s protocol Preamble Message. The Preamble Message is not complete, however, as we do not yet see a Preamble End record (a single byte with value 0xc). We need to read from the pipe again to get the rest of the Preamble Message: this is because the WCF client is writing to the pipe in Message mode, in which each write is treated by the system as a separate message. In this mode, each call we make to NamedPipeServerStream.Read() will not read beyond the end of a client message unit. If we change the listener code to call Read twice, we see more data: Interpreting this according to the framing protocol: | 0x9 | Indicates the start of an Upgrade Request record | | 0x15 | Length prefix for the upgrade protocol which follows (21 bytes) | | application/negotiate | The name of the upgrade protocol requested by the client | By sending this record the client is requesting that a protocol identified by the string “application/negotiate” should now be executed in order to upgrade the communication stream. This protocol is the Negotiate mechanism (often known as SPNEGO) defined by RFC4178, by which a mutually agreed security context can be negotiated between the client and server, to provide Privacy and Integrity for the data being sent over the pipe. The client is requesting this because the WCF NetNamedPipeBinding by default sets the SecurityMode to “Transport”. Note that we still haven’t received the Preamble End record - at this stage, the ball is in our court to continue the protocol: our listener must send an Upgrade Response record back to the client before the process can proceed. Once we do that, the client will initiate the SPNEGO protocol, and we will have to fulfill the service side of that protocol, before the framing protocol exchange continues. I don’t want to go into any details on this now, so we will step over this part by using the .NET implementation of the protocol, System.Net.Security.NegotiateStream, to wrap our pipe data stream, as shown below. … Console.WriteLine("End of initial data stream.\nClient is waiting for our Upgrade Response\n"); // Send Upgrade Response record pipeServer.WriteByte(0xa); pipeServer.WaitForPipeDrain(); Console.WriteLine("Upgrade Response sent\n"); Console.WriteLine("Wrapping pipe in a NegotiateStream\n"); NegotiateStream negoStream = new NegotiateStream(pipeServer, true); negoStream.AuthenticateAsServer(); // now we use the negoStream in future IO rather than the pipeServer … The AuthenticateAsServer method call encompasses the entire series of token exchanges between the listener and the client, to establish the security context. Subsequently, every time we read from or write to the NegotiateStream instance everything needed to encrypt/decrypt etc will be happening transparently within the implementation of the framework class. Here’s the full output we get after we have wrapped the stream and called Read on it: We see at last the single byte Preamble End record (byte 0xc) sent by the client. The protocol requires our listener to send back the single byte Preamble Ack record, completing the preliminary handshake. The protocol then moves to the stage where the actual messages can be sent and received. Let’s see this happening, when we send the Preamble Ack record (byte 0xb) and read from the pipe again: Well, this looks as though the client may be sending us a message of sorts, but it doesn’t look at first sight like the kind of SOAP message we would expect from a WCF client. There are two reasons: - first, the Framing Protocol itself has a thin envelope which heads the payload message. That is: the first byte (0x06) indicating a Sized Envelope Record; followed by two bytes 0x93 0x02 which specify the size of the payload message. Refer to the Protocol for details of how the size is encoded – it can be up to five bytes in length.
- secondly, remember that Known Encoding record in the PreAmble the client sent… that told us the client was going to use a binary encoding scheme. What we see here, following the framing protocol header, is actually a representation of a SOAP message, but the encoding scheme used makes parts of it pretty hard if not impossible to interpet manually. That’s another layer of complexity which needs to be grappled with before we can work with the messages.
In summary, pulling apart a single service call by a WCF client using the NetNamedPipeBinding, demonstrates that there are at least three layers of protocol implementation which have to be addressed, in addition to the named pipe transport IO itself, if one were to contemplate implementing one side of the interchange without using WCF: - the Framing Protocol, which we have covered a good deal of above. As you can see this is not particularly complex and might not be too daunting a task;
- The .NET Negotiate Stream and SPNEGO protocols by which stream upgrade occurs to secure the conversation.
- The message encoding scheme which the framing protocol calls “Binary encoding with in-band dictionary”.
I will have more to say about these latter two in subsequent posts.
|
-
I’ve mentioned previously the vulnerability to squatting attacks of WCF services using the standard NetNamedPipe binding in .NET version 3.5, and promised to show how it might be done. Let’s review the nature of the vulnerability. First, the attacker needs to know the name of the named pipe object used by the service’s endpoint, and my previous posts have discussed how this can be discovered if the service URL is known. Second, the attacker calls the Windows APIs CreateNamedPipe and ConnectNamedPipe to open a server side handle to the pipe and start listening on it. The CreateNamedPipe call succeeds because the pipe’s ACL grants FILE_CREATE_PIPE_INSTANCE permissions to EVERYONE. The attacker then just waits until a client of the service connects to the attacker’s instance of the pipe rather than one of the instances on which the service itself is listening. Once he has “captured” a client, the attacker may exploit it in a number of ways, such as: - impersonate the client’s windows security identity
- steal data from messages sent by the client
- spoof the behaviour of the service to mislead the client
- insert itself as a man-in-the-middle to spy on traffic between the client and the real service
The classes provided in the System.IO.Pipes namespace make the first of these very easy to do in C#: using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, -1)) // Calls CreateNamedPipe { pipeServer.WaitForConnection(); // Calls ConnectNamedPipe // We have to read at least the first byte sent by the client, before we can identify or impersonate him pipeServer.ReadByte(); // Steal the client's identity pipeServer.RunAsClient(new PipeStreamImpersonationWorker(DoSomethingEvilInTheNameOfTheClient)); } This exploit is not possible in .NET4, provided the WCF service is hosted in a Windows Service, because in the .NET4 NetNamedPipeBinding the FILE_CREATE_PIPE_INSTANCE permission is now restricted by the pipe’s ACL to the Logon ID SID of the process hosting the service. Note, though, that if the service is self-hosted in a console application (or indeed any application) running in an Interactive logon session, any process running in that session will still be able to squat on the pipe using this exploit. This includes any processes started using the secondary logon service (the RunAs service) with a different identity to the interactively-logged on user. It also means that starting a self-hosted WCF service as a console application using RunAs to run the service in the context of a dedicated service account, provides no protection against squatting by other processes in the same Interactive logon session. Exploiting impersonation in the manner shown above, without any further communication with the pipe, will disrupt the communication the client is expecting to see and will surface as an exception in the client process: either a timeout error or a pipe IO error, depending on how long the attacker code takes before closing the pipe instance. Taking the exploit beyond impersonation, and making it less visible to the client, involves understanding both the data stream sent by the client and the data the client expects to be sent back across the pipe. I will go on to look at this in future posts, since it is interesting not just in the context of this vulnerability, but more generally to anyone who has contemplated implementing a non-.NET client or service to communicate with a service/client using the WCF named pipe binding.
|
-
To finish where I left off last time, a quick look at the P/Invoke code we need in order to find the shared memory location where the pipe name is published, and read the pipe name from it. Shared memory is implemented by Windows using the same mechanism used for memory mapped files, but where the file concerned is the the system paging file. Reading from shared memory set up by another process (here, the service) is a two-stage operation: first we obtain a handle to the named kernel File Mapping object using the Windows API OpenFileMapping; then we request a view of the mapped file, from which we can read the data, using the MapViewOfFile API. When working with kernel handles, it is important that we release them after use even in the face of exceptions, so it is a good idea to use the SafeHandle patterns provided by the .NET class library. The Microsoft.Win32.SafeHandles namespace does actually define safe handle classes specifically for FileMapping and ViewOfFile handles, but unfortunately these have been declared internal and so we can’t use them. However, the base abstract class appropriate for both types of handle is public (SafeHandleZeroOrNullIsInvalid), making it quite trivial to declare our own versions of the safe handle types we need, derived from this abstract base. The P/Invoke declarations for the Windows API functions required are also straightforward. Armed with these, and a structure definition for the contents of the shared memory, viz: [StructLayout(LayoutKind.Sequential)] private struct PipeEndpointMetaData { public bool IsInitialised; public Guid PipeGuid; } the core method looks like this: static string GetPipeNameFromSharedMemory(string sharedMemoryName) { const uint FILE_MAP_READ = 0x00000004; const int ERROR_FILE_NOT_FOUND = 2; using (SafeFileMappingHandle fileMappingHandle = OpenFileMapping(FILE_MAP_READ, false, sharedMemoryName)) { if (fileMappingHandle.IsInvalid) { int errorCode = Marshal.GetLastWin32Error(); if (ERROR_FILE_NOT_FOUND == errorCode) return null; //File not found - this isn't the right name variant throw new Win32Exception(errorCode); // The name matched, but something went wrong opening it } // If we get here, we have found and opened the shared memory file mapping // Now we need to map a view and read the pipe metadata from it using (SafeViewOfFileHandle viewBase = MapViewOfFile(fileMappingHandle, FILE_MAP_READ, 0, 0, 20)) { if (viewBase.IsInvalid) { throw new Win32Exception(); } Guid pipeGuid = ((PipeEndpointMetaData)Marshal.PtrToStructure(viewBase.DangerousGetHandle(), typeof(PipeEndpointMetaData))).PipeGuid; return pipeGuid.ToString(); } } } So, locating the pipe name involves calling this method for each of the possible name variants until it returns something other than null. If it throws, the most likely reason is an Access Denied error, which you get if the service has restricted access to the pipe (for instance by using my AclSecuredNamedPipeBinding), or if you try to locate a standard WCF NetNamedPipe endpoint from the security context of a remote user.
|
-
As those who have read my earlier post will be aware, when a WCF client wishes to establish communication with a WCF service over the NetNamedPipe transport binding, the client-side stack has to execute an undocumented algorithm, based on incomplete information, in order to locate the actual named pipe on which the service is listening. This is because the WCF service-side stack dynamically creates a differently named pipe each time it starts up, rather than using a fixed name based on the service endpoint URL. All that the client knows is the service URL with which it has been configured to communicate. This may be identical to the URL on which the service endpoint is configured to listen, but is not necessarily so. This is because the WCF mechanism for dispatching incoming messages to particular service endpoints supports wildcard matching in addition to exact matching of URL. The HostNameComparisonMode property, which most WCF bindings support, determines the URL matching rules which will be applied for a particular service endpoint. A post by Kenny Wolf some time ago explains this mechanism in detail. The WCF client doesn’t know whether its configured service URL is an exact match of the service endpoint’s listening URL, or whether it is matched to the endpoint at a related but not identical URL as a consequence of wildcard matching rules. The client code therefore has to “search” for its rendezvous point with the service, essentially by enumerating the possible endpoint listener URLs implied by the matching rules until it finds one which maps to the rendezvous point – which is a shared memory object with a distinctive name derived from the endpoint listener URL, as explained in my earlier post. This searching algorithm is implemented in the GetPipeName method of the internal WCF type System.ServiceModel.Channels.PipeConnectionInitiator. One way of finding the actual name of the pipe being used by a service is to call this internal method using reflection. But is not too difficult, and much faster, to implement the search oneself. There are three name components which define the scope of the search: - the kernel object namespace, which is either “Local” or “Global”
- the host part of the URL, which is either a proper host name (e.g. localhost) or one of the hostname wildcard specifiers + or *
- the URL path prefix
Suppose a client wants to call a service for which it has been given the URL “net.pipe://localhost/one/two/three”. To locate the named pipe on which the service is actually listening, the client must look for the existence of a named shared memory object with a name based on one of the following URLs, and the search must occur in this order: net.pipe://+/one/two/three/ net.pipe://+/one/two/ net.pipe://+/one/ net.pipe://+/ net.pipe:/localhost/+/one/two/three/ net.pipe:/localhost/+/one/two/ net.pipe:/localhost/+/one/ net.pipe:/localhost/+/ net.pipe://*/one/two/three/ net.pipe://*/one/two/ net.pipe://*/one/ net.pipe://*/
The actual names to be searched for are derived from these URLs by applying an algorithm which is easier to show in code than to describe in words (see below). The full list is searched for in the “Global” kernel namespace first, and then in the “Local” namespace if there is no match in “Global”. Here is a function implementing the name algorithm: private static string DeriveSharedMemoryName(string hostName, string path) { StringBuilder builder = new StringBuilder(); builder.Append(Uri.UriSchemeNetPipe); builder.Append("://"); builder.Append(hostName.ToUpperInvariant()); builder.Append(path); byte[] uriBytes = Encoding.UTF8.GetBytes(builder.ToString()); string encodedNameRoot; if (uriBytes.Length >= 0x80) { using (HashAlgorithm algorithm = new SHA1Managed()) { encodedNameRoot = ":H" + Convert.ToBase64String(algorithm.ComputeHash(uriBytes)); } } else { encodedNameRoot = ":E" + Convert.ToBase64String(uriBytes); } return Uri.UriSchemeNetPipe + encodedNameRoot; }
The result is a name which looks something like this: net.pipe:EbmV0LnBpnGU6Ly9rL1dDRkRFTU9OUF1g6e9cUFNFUlZJQ0Uv Remember, what this algorithm generates is not the name of the pipe itself, but the name of a kernel shared memory FileMapping object which gives access to a shared memory structure containing the name of the pipe currently in use. To complete the search we need to try to open a view on the shared memory mapping and if successful, read the pipe name from it. I’ll save that for next time.
|
-
In my previous post in this series, far longer ago than I intended, I showed how the WCF named pipe binding could be tweaked so as to base service security on Windows OS-level access control for the named pipe object used as the transport. Using my AclSecuredNamedPipeBinding can improve the security of WCF services exposed via named pipes but, as I explained, does not completely remove the vulnerability created by the security defect in the standard WCF binding. I reported this vulnerability to Microsoft, and they have confirmed and accepted it as a defect but have not yet seen fit to patch it in .NET 3.0/3.5 (I haven’t yet looked at .NET 4.0 beta to see whether it may have been addressed there). I discussed this with some Redmond people at the PDC last year, and the view seemed to be that it wasn’t a significant vulnerability because there is in general no way for a user who isn’t an administrator to enumerate named pipe objects in order to discover service endpoints to attack. Personally, I think this is a short-sighted view, because it ignores the fact that anyone who knows the URL of the service’s named pipe endpoint can derive the name of the pipe, in exactly the same way as the WCF client-side channel stack does. If one knows the service endpoint URL, it is remarkably easy to create an exploit to appropriate the credentials of a client accessing the service, for example using the named pipe support provided in the .NET Framework 3.5 System.IO.Pipes namespace. With a bit of extra work, one can make the exploit masquerade as the service, spoofing response messages to the client. In future posts I will explore how this is done, not least because it illuminates a number of the factors which make the NetNamedPipeBinding difficult to interoperate with programs which do not use the .NET Framework. Update: This defect is fixed (well, arguably only partly fixed) in .NET 4. The default DACL created when the service channel listener is opened now gives FILE_CREATE_PIPE_INSTANCE permission only to the logon session SID under which the service is running. The superfluous ACE in respect of the CREATOR OWNER SID has also been removed. It’s arguably only a partial fix, because if the WCF service is hosted in a process running in an interactive logon session (e.g. self-hosted in a console application), any other process running in that logon session can still execute a squatting attack on the service, because it will still have FILE_CREATE_PIPE_INSTANCE permission on the named pipe created by the standard NetNamedPipeBinding. Before that, I want to post a fuller workaround to the vulnerability, which I have added to my AclSecuredNamedPipeBinding class. The strategy for the workaround is: - register an event handler which will fire when the ChannelListener has reached the Opened state. At this point, the listener will have just created the named pipe, complete with its defective DACL.
- in the event handler, amend the DACL to remove the offensive grant of FILE_CREATE_PIPE_INSTANCE permission to all but the service owner.
In principle, this does leave a race condition: there is a scintilla of time between the listener creating the pipe with the insecure DACL, and the DACL being patched by our event handler, which in theory an attacker could exploit. In practice, though, this will be very hard to do, since the publishing of the pipe name (a GUID) to shared memory also only happens fractionally earlier. The first step is very easy to accomplish, with this change to the BuildChannelListener override: public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context) { IChannelListener<TChannel> listener = base.BuildChannelListener<TChannel>(context); PropertyInfo p = namedPipeDuplexChannelListenerType.GetProperty("AllowedUsers", BindingFlags.Instance|BindingFlags.NonPublic); p.SetValue(listener, _allowedUsers, null); listener.Opened += new EventHandler(listener_Opened); return listener; } private void listener_Opened(object sender, EventArgs e) { ChannelListenerBase listener = sender as ChannelListenerBase; if (null != listener) { // code to patch the pipe DACL goes here } } The second step is a bit more of a challenge, but the types provided in the .NET 3.5 System.IO.Pipes namespace can be used to do most of the heavy lifting. A possible implementation is shown below: /// <summary> /// If a WCF NetNamedPipe service endpoint is listening at the specified URL, patches the DACL of the /// named pipe to deny the FILE_CREATE_PIPE_INSTANCE permission to all but the pipe owner i.e. the identity /// of the service process which opened the endpoint listener. /// </summary> /// <param name="serviceUrl">The URL of the WCF service endpoint</param> public static void PatchPipeSecurity(Uri serviceUrl) { string pipeName = ResolveServiceUrlToPipeName(serviceUrl); using (NamedPipeServerStream sstm = new NamedPipeServerStream(pipeName, PipeDirection.InOut, -1, PipeTransmissionMode.Byte, PipeOptions.None, 0, 0, null, System.IO.HandleInheritability.None, PipeAccessRights.ChangePermissions)) { PipeSecurity pipeSecurity = sstm.GetAccessControl(); foreach (PipeAccessRule pipeAccessRule in pipeSecurity.GetAccessRules(true, true, typeof(SecurityIdentifier))) { if (AccessControlType.Allow.Equals(pipeAccessRule.AccessControlType)) { // Remove the CreatorOwner ACE - it is redundant if (new SecurityIdentifier(WellKnownSidType.CreatorOwnerSid, null).Equals(pipeAccessRule.IdentityReference)) { pipeSecurity.RemoveAccessRuleSpecific(pipeAccessRule); continue; } // Remove the FILE_CREATE_PIPE_INSTANCE permission from each ACE except the one // for the service account itself, which will be the identity executing this code // Update: Because of the way the permissions defect has been fixed, this code is not sufficient // in .NET4, where we would need also to grant FILE_CREATE_PIPE_INSTANCE permission to the // service owner account. Without this the service will not work, and will hang in a busy loop. if (!WindowsIdentity.GetCurrent().User.Equals(pipeAccessRule.IdentityReference)) { PipeAccessRights permissions = pipeAccessRule.PipeAccessRights; permissions &= ~PipeAccessRights.CreateNewInstance; PipeAccessRule newAccessRule = new PipeAccessRule(pipeAccessRule.IdentityReference, permissions, AccessControlType.Allow); pipeSecurity.RemoveAccessRuleSpecific(pipeAccessRule); pipeSecurity.SetAccessRule(newAccessRule); } } } sstm.SetAccessControl(pipeSecurity); } } The listing above does not show the implementation of the function ResolveServiceUrlToPipeName. One way of implementing this in just a few lines of code is to use reflection to call the internal static method GetPipeName of the internal WCF type System.ServiceModel.Channels.PipeConnectionInitiator, which is the mechanism used by the WCF client-side channel stack when a WCF client connects to the service. Type pipeConnectionInitiatorType = Type.GetType("System.ServiceModel.Channels.PipeConnectionInitiator, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", true); MethodInfo getPipeNameMethodInfo = pipeConnectionInitiatorType.GetMethod("GetPipeName", BindingFlags.NonPublic | BindingFlags.Static); string pipename = (string)getPipeNameMethodInfo.Invoke(null, new object[] { new Uri("net.pipe://localhost/<service url here>") }); return pipename.Replace(@"\\.\pipe\", String.Empty); In this context, such an implementation based on reflection is a reasonable approach, because it only needs to execute once when the service opens. With an eye to the possibility that I might want an implementation which would be called more frequently, and avoid the overhead of reflection, I have actually created my own code to do this, which I will cover in a separate post.
|
-
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) ---&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.
|
-
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.
|
-
This is the first in a series of posts in which I will aim to explain some details of the named pipe binding provided by Windows Communication Foundation (WCF), discovered during the course of some exploring I have been doing. My motivations for looking into this were: - The standard binding (NetNamedPipeBinding) exposes very few properties relating to configuration of the underlying transport mechanism. Having some awareness of the Windows named pipe APIs from previous work, I was interested to understand how the WCF binding mapped to the underlying transport protocol; which named pipe configuration options were "baked into" the WCF implementation and which might be controlled/tweaked with a bit of work in the channel stack.
- I wanted to understand in more detail the security characteristics of the binding.
- I was just nosey :-)
In this post I will start by looking at the how the named pipe used by a service endpoint with the NetNamedPipe binding is created, and how clients locate it in order to connect. I had expected that if I looked into the service process using a tool like Process Explorer, I would see it holding a handle to a named pipe with a name closely related to the URI of the endpoint. What I see instead is a handle to a pipe named something like... \\.\pipe\197ad019-6e5f-48cb-8f88-02ae11dfd8c0 ... clearly the pipe name has been created using a GUID. I also note that the name of the pipe changes each time I stop and restart my service host, so the GUID is being regenerated each time the endpoint runtime is built by WCF. How then does a client of the service know how to communicate with the endpoint? Somehow it must be able to resolve the well-known URI for the endpoint into whatever is the current name of the pipe it must use to send messages to the service. It turns out that this is accomplished using what amounts to a mini metadata publishing mechanism which is exclusive to the NetNamedPipe binding. This mechanism is based on a named Windows file mapping object backed by the system paging file. It is the name of this object which is invariant, and directly derived from the endpoint URI... though in a far from obvious way. So in order to locate the correct pipe, a client of a WCF NetNamedPipe service endpoint has to: - know that the special metadata mechanism exists
- know how to derive from the endpoint URI the name of the file mapping object through which the metadata is published
- located the file mapping object and use it to open a view on the shared memory
- know how to interpret the metadata stored in the shared memory, and translate it into the name of the pipe currently being used by the endpoint
Some more details for those who are interested: Deriving the file mapping object name from the URI The shared memory file mapping object created by the service endpoint listener (System.ServiceModel.Channels.PipeConnectionListener) has a name which looks something like this: net.pipe:EbmV0LnBpnGU6Ly9rL1dDRkRFTU9OUF1g6e9cUFNFUlZJQ0Uv This is derived from the following components: ["net.pipe"] [:E|:H] [base-64 encoded byte[] X] Where the X is constructed as: - when the second component is :E : UTF8 encoding of ["net.pipe://"] [URI hostname-or-wildcard*] [URI path or parent path] - when the second component is :H : the SHA-1 hash of the above (used when the UTF8 encoding of the above exceeds 127 bytes) *The URI hostname-or-wildcard depends on the HostNameComparisonMode setting for the endpoint's transport binding - this property is set to HostNameComparisonMode.StrongWildcard in the standard NetNamedPipeBinding, and is not exposed as a property of the binding itself. This means that this component of the name will be "+" (the strong wildcard symbol) unless a custom binding has been used to tweak the HostNameComparisonMode property of the transport binding element. Data stored by the service in the shared memory object The service stores 20 bytes of data in the shared memory, representing an instance of the structure System.ServiceModel.Channels.PipeSharedMemory+SharedMemoryContents, which looks like this... [StructLayout(LayoutKind.Sequential)] struct SharedMemoryContents { public bool isInitialized; public Guid pipeGuid; } The client uses the GUID stored in this object to construct the pipe name through which to connect to the service endpoint. Of course, the WCF client stack knows how to jump through these hoops, as it uses the same set of System.ServiceModel types as the service used to set up the mechanism. So you don't really need to know anything about all this if your service client is also a WCF application using the standard binding... which it will be if you are doing things as the WCF designers intended: the named pipe binding was designed solely for WCF-to-WCF scenarios. That's not to say that, in principle, there is any fundamental reason why a named pipe binding to a WCF service should not be able to support any arbitrary client implementation which knows how to write messages to and read messages from a named pipe. Perhaps there are integration scenarios involving legacy unmanaged code or mixed technologies on a single box, where a more open named pipe binding might be useful, not least because the underlying transport mechanism is very fast. But the standard NetNamedPipe binding won't help with this. In practice, it is going to be much easier to use one of the bindings based on standard interoperable protocols, or by providing a COM wrapper around a WCF client implementation.
|
-
The build process for the project I am currently working on just got hosed by Microsoft Security Update for Microsoft .NET Framework 2.0 (KB928365). A hundred-odd projects which had been building correctly for weeks or months suddenly started to error during solution build, with post-build event failures complaining of file paths not found.
The failing post-build events all contained constructs like:
msbuild /v:m "$(ProjectDir)\..\Common\postbuild.proj" ...
the error was happening because these events were now being executed as though it was
msbuild /v:m "<project folder>\Common\postbuild.proj" ... i.e. looking for the Common folder as a child of the project folder rather than its sibling.
The eagle-eyed may have spotted that the expression in our project file generates a superfluous backslash after the project folder, because the $(ProjectDir) macro is expanded by VS to include a trailing backslash.
It turns out that this security patch changes behaviour at the operating system level concerning the interpretation of file system paths containing duplicated backslash characters. Whereas the OS was previously forgiving of duplicates, treating them just as single backslash characters, the new behaviour somewhat bizarrely treats '\\..\' as though it were just '\' . I'm wondering whether this change in behaviour is intentional, and somehow related to a security issue, or whether it is an unintentional side effect of something MS have patched. I'm also wondering how many latent defects there are out there in deployed applications, which are going to be exposed by this.
|
-
Something big happened last week, the birth of a new concept: the Internet Service Bus, or as Microsoft Connected Systems division has christened it, BizTalk Services. Read, for example, Dennis Pilarinos on what this is, and Clemens Vasters on why it's important. Besides being an exteremely interesting initiative because of what it is, I find the choice of name quite intriguing also... is this the first public evidence of a Redmond strategy to morph BizTalk from a product to a brand; from a messaging application platform to a marketing smorgasbord of services and tools for connecting distributed systems? I think it's quite likely: the advent of WCF and WF has always appeared to me as the writing on the wall for BizTalk as a single monolithic product, which the absorption of the BizTalk product group into the CSD seemed to confirm.
|
-
Most of the time, performance tuning of our software is about speeding things up... why would you ever want to slow things down? Well, as it happens, there are scenarios when working with BizTalk Server where slowing things down a bit is a desirable goal. A common one in BizTalk Server 2004 arises when using the SOAP send adapter which comes in the box to make calls to SOAP web services from within orchestrations. The SOAP send adapter uses the standard CLR thread pool to dispatch SOAP requests across the network, and process the responses received from the web service. Unfortunately, if you drive it too hard, at peak throughput you will start to see the send adapter logging exceptions like this and suspending the request messages for retry: Event ID 5740 - The adapter "SOAP" raised an error message. Details "There were not enough free threads in the ThreadPool object to complete the operation. This is actually a symptom of a problem in the ASP.NET HTTP stack rather than in the BizTalk product itself. The issue is explained in this Microsoft Knowledge Base article: briefly, each SOAP request uses a worker thread to make the request, an IO completion thread to service the response, and additional threads to perform authentication handshakes; if the available thread pool threads are tied up on requests and there are no free threads which can be used to service responses, these errors occur. Changes were made in BizTalk Server 2004 SP1 to alleviate this problem, and the throughput limit before the problem manifests can be increased by careful tuning of the CLR thread pool parameters as described in the KB article. The problem doesn't go away completely, however. In a typical BizTalk 2004 installation with optimal tuning of the CLR parameters, spikes in throughput of more than about 150 SOAP requests per second will encounter this issue. (I'm told that the situation is much better in BizTalk Server 2006 - the ASP.NET 2.0 stack was rewritten to considerably reduce the possibility of thread pool starvation - but I do not have first-hand experience or confirmation of this). Unfortunately, even if your normal peak throughput is considerably less than this limit, spikes can still occur due to temporary outages of one sort or another. BizTalk's automatic retry functionality then serves to exacerbate the problem, because the retry interval is fixed so messages which previously failed close together in time are resubmitted in a spike. Also, when the problem starts to occur, thread pool threads affected become tied up for a fairly lengthy timeout period (100 seconds, I think) before being freed up, so the problem tends to escalate. If you need high throughput rates it is highly desirable that you avoid the problem ever occurring. To this end, what we would like to be able to do is smooth out any spikes in the rate at which our orchestrations feed web service requests to the SOAP send adapter. Unfortunately, the knobs provided by BizTalk 2004 for controlling the work rate of orchestration hosts are much too blunt an instrument for doing this effectively - host throttling parameters are global to the BizTalk Group in BizTalk 2004. A different approach which I have used with some success is a fairly direct regulation of the rate at which the orchestrations execute the Send shape to the Web Port. I use a C# helper type, which I have called ExecutionBrake, which is invoked by the orchestration just before it initiates the SOAP request, and which acts as a governor controlling the peak rate at which concurrent instances can execute within that host instance. The idea is to use thread synchronisation primitives within this helper type to identify when the rate of executing requests is approaching the desired limit, and apply as light a touch as possible to delay execution of just enough threads to keep the rate from peaking above the limit. Unless there is a sustained spike, short waits using System.Threading.Thread.Sleep() are sufficient. If a sustained load in excess of the limit is experienced, a fallback method is used, whereby the helper type indicates to the orchestration that it should enter a longer delay loop using a Delay shape. The following sample code should illustrate the idea: using System; using System.Collections; using System.Threading; namespace Charteris.ChrisDicksonBlog.Samples { public class ExecutionBrake { /// <summary> /// Maintains a register of the named instances which are active in the /// current AppDomain. For any unique name, the braking is implemented by /// an internal Singleton object. /// </summary> /// <param name="uniqueName">Unique brake name to register</param> private static void RegisterBrake(string uniqueName) { if (!_brakingImplementations.ContainsKey(uniqueName)) { lock (_sync) { if (!_brakingImplementations.ContainsKey(uniqueName)) { ExecutionBrakeImpl impl = new ExecutionBrakeImpl(); Thread.MemoryBarrier(); _brakingImplementations.Add(uniqueName, impl); } } } } /// <summary> /// Collection of singleton implementation objects, keyed on unique name /// </summary> private static Hashtable _brakingImplementations = new Hashtable(); private static object _sync = new object(); public ExecutionBrake(string uniqueName) { _uniqueName = uniqueName; RegisterBrake(uniqueName); } /// <summary> /// Key method called by the orchestration. If the return value is zero, the orchestration /// continues to make the SOAP request. If non-zero, the orchestration should loop via a /// Delay shape and call this method again before proceeding. The return value can be used /// to seed the Delay shape's configuration, so that retries are spread randomly. /// </summary> public int ThrottleExecution() { return ((ExecutionBrakeImpl)_brakingImplementations[_uniqueName]).ThrottleExecution(); } private string _uniqueName;
private class ExecutionBrakeImpl { public int ThrottleExecution() {
// Maintain a count of threads currently executing this method. The corresponding // decrement is in the finally block int threadsInThisMethod = Interlocked.Increment(ref _threadsUnderControlCount); try { if (threadsInThisMethod >= _deferThreshold) { // We have more than enough threads already so defer this one immediately return _random.Next(_maximumDeferralDurationHint); } int numberOfSleeps = 0; if (_threadsReleasedThisIntervalCount >= _brakingThreshold) { // We have already reached the limit of threads which can be released in // the current reference interval, so this thread must wait ++numberOfSleeps; Thread.Sleep(_random.Next(_maximumThreadSleepDuration)); } // Keep checking for a release window, then sleeping, alternately until this thread has // either been released or has used the maximum number of sleeps while (numberOfSleeps < _maximumThreadSleeps) { lock (_sync) {
// If the reference period has ended, we can start a new one and // be the first thread released in the new period if (0 > DateTime.Compare(_endOfControlInterval, DateTime.Now)) { _endOfControlInterval = DateTime.Now + _thresholdInterval; _threadsReleasedThisIntervalCount = 1; return 0; } // Otherwise we can go if the count for the current interval hasn't been exceeded if (_threadsReleasedThisIntervalCount < _brakingThreshold) { ++_threadsReleasedThisIntervalCount; return 0; } // Otherwise we'll need to sleep and loop again } ++numberOfSleeps; Thread.Sleep(_random.Next(_maximumThreadSleepDuration)); } } finally { Interlocked.Decrement(ref _threadsUnderControlCount); } // We were not able to release the thread, so return a non-zero deferral hint return _random.Next(_maximumDeferralDurationHint); } private DateTime _endOfControlInterval = DateTime.Now; private int _threadsUnderControlCount; private int _threadsReleasedThisIntervalCount; private object _sync = new object(); private Random _random = new Random(); // Configuration parameters. // For the purposes of this sample these are constants, but // in practice they would need some configuration mechanism // to tune the braking for any particular named brake. private const int _brakingThreshold = 100; private const int _deferThreshold = 200; private TimeSpan _thresholdInterval = new TimeSpan(0,0,0,1); private const int _maximumThreadSleepDuration = 150; private const int _maximumThreadSleeps = 3; private const int _maximumDeferralDurationHint = 500; } } } Naturally, if there are multiple host instances executing the orchestration, this braking mechanism smooths the rate of execution of each one independently, and the parameters need to be configured with this is mind. Don't expect such a mechanism to enable very precise regulation of execution rates, particularly with multiple host instances, but it can be used effectively to prevent abnormal spikes in message volumes causing the sort of problems described above with the SOAP adapter.
|
More Posts Next page »
|
|
|