Share via

Security Briefs

Limited User Problems and Split Knowledge

Keith Brown

Code download available at:SecurityBriefs2006_11.exe(165 KB)

This month I'll answer more reader questions. I'll discuss Windows Communication Foundation Web services running under normal user accounts, and the use of split knowledge and dual control of keys for protecting credit card data.

Q Why won't my simple Windows® Communication Foundation service start when I run it as a non-administrator?

Q Why won't my simple Windows® Communication Foundation service start when I run it as a non-administrator?

A First, let me say that it's good to hear that you're testing your code under a normal user account! This is an important aspect of testing that developers should not overlook.

A First, let me say that it's good to hear that you're testing your code under a normal user account! This is an important aspect of testing that developers should not overlook.

Now, the first approach I recommend for people trying to track down this type of problem is to fire up Filemon and Regmon, available from Sysinternals (which was recently acquired by Microsoft) at, and look for failed attempts to open files or registry keys. Unfortunately, these tools can't diagnose every issue that could cause this problem. Even the simplest of Web services implemented in Windows Communication Foundation that listen on an HTTP channel will not run as a normal user by default, unless it's hosted in IIS. This is because the Windows Communication Foundation HTTP channel uses the HTTP.SYS driver to set up its listener, and HTTP.SYS does not allow non-administrators to register listeners without an administrator explicitly granting permission.

To demonstrate the problem, I've built a simple Web service that consists of two files: the source for the service and an application configuration file. Figure 1 shows the code for the service. When you launch this service while running as an administrator, it works just fine. But try running it as a normal user, and bang! Figure 2 shows the exception that occurs when using Beta 2 of the Microsoft® .NET Framework 3.0.

Figure 2 Exception When Run as Normal User

System.ServiceModel.Diagnostics.CallbackException: A user callback threw an exception. Check the exception stack and inner exception to determine the callback that failed. ---> System.NullReferenceException: Object reference not set to an instance of an object. at System.ServiceModel.Channels.DatagramChannelDemuxer`2. OnListenerClosed(Object source, EventArgs args) at System.ServiceModel.Channels.CommunicationObject.OnClosed() --- End of inner exception stack trace --- at System.ServiceModel.Channels.CommunicationObject.OnClosed() at System.ServiceModel.Channels.CommunicationObject.Abort() at System.ServiceModel.Channels.ChannelListenerBase.OnAbort() at System.ServiceModel.Channels.SecurityChannelListener`1.OnAbort() at System.ServiceModel.Channels.CommunicationObject.Abort() at System.ServiceModel.Channels.ChannelListenerBase.OnAbort() at System.ServiceModel.Channels.CommunicationObject.Abort() at System.ServiceModel.Dispatcher.ChannelDispatcher.OnAbort() at System.ServiceModel.Channels.CommunicationObject.Abort() at System.ServiceModel.ServiceHostBase.OnAbort() at System.ServiceModel.Channels.CommunicationObject.Abort() at System.ServiceModel.Channels.CommunicationObject.Close( TimeSpan timeout) at System.ServiceModel.Channels.CommunicationObject.Close() at System.ServiceModel.Channels.CommunicationObject.Dispose() at Program.Main(String[] args) in C:\work\HelloService\Program.cs:line 24

Figure 1 Simple Web Service Source Code

//hello.cs - simple WCF Web service that listens on HTTP using System; using System.ServiceModel; [ServiceContract(Namespace = "")] public interface IHello { [OperationContract] void SayHello(); } public class Hello : IHello { public void SayHello() { Console.WriteLine("Hello"); } } class ConsoleHost { static void Main(string[] args) { try { using (ServiceHost host = new ServiceHost(typeof(Hello), new Uri("https://localhost:8080/MyServices/"))) { host.Open(); Console.WriteLine( "Service is listening, press any key to quit."); Console.ReadKey(); } } catch (Exception x) { Console.WriteLine(x); } } } <!--app.config--> <configuration> <system.serviceModel> <services> <service name="Hello"> <endpoint address="Hello" binding="wsHttpBinding" contract="IHello"/> </service> </services> </system.serviceModel> </configuration>

As I mentioned, one way to solve this problem is to host your service in IIS. But for some people, this isn't an option. If, for example, you're using Windows Communication Foundation in a Windows Forms-based application to receive notifications via an HTTP Web service, you'll need to deal with this problem in a different way, following the guidance in this column. If you're hosting in a Windows NT® service, you've got the same issue.

The problem is not difficult to solve—you just need an administrator to grant your application what is called a namespace reservation with HTTP.SYS. In doing so, the administrator is essentially saying, "It's OK for this user to listen over HTTP on a URL prefix that I specify." An administrator can grant these listening permissions to either an individual user or to a group.

Your setup program can grant a namespace reservation programmatically using the HTTP API, which unfortunately doesn't yet have a public .NET wrapper. Figure 3 shows some sample code that you can use to get started if you're working in C#. This particular sample expects you to pass a user or group account by name, and it must be executed by a member of the local Administrators group. This code also uses P/Invoke to call the HTTP API, which means it needs to run fully trusted as far as evidence-based security in the common language runtime (CLR) is concerned. If you're packaging this inside a Microsoft Installer, it should be fine. But if you're trying to do this from a downloaded application that runs with partial trust under a normal user account, it will not work.

Figure 3 Programatically Granting a Namespace Reservation

using System; using System.Xml; using System.Security.Principal; using System.Runtime.InteropServices; public class ReserveHttpNamespace { static void Main(string[] args) { if (args.Length != 2) { Console.WriteLine( "Usage: reserveHttpNamespace " + "prefix account"); return; } try { ModifyReservation(args[0], args[1], false); Console.WriteLine("Success!"); } catch (Exception x) { Console.WriteLine(x.Message); } } static void ModifyReservation( string urlPrefix, string accountName, bool removeReservation) { string sddl = createSddl(accountName); HTTP_SERVICE_CONFIG_URLACL_SET configInfo; configInfo.Key.UrlPrefix = urlPrefix; configInfo.Param.Sddl = sddl; HTTPAPI_VERSION httpApiVersion = new HTTPAPI_VERSION(1, 0); int errorCode = HttpInitialize(httpApiVersion, HTTP_INITIALIZE_CONFIG, IntPtr.Zero); if (0 != errorCode) throw getException("HttpInitialize", errorCode); try { // do our best to delete any existing ACL errorCode = HttpDeleteServiceConfigurationAcl( IntPtr.Zero, HttpServiceConfigUrlAclInfo, ref configInfo, Marshal.SizeOf( typeof(HTTP_SERVICE_CONFIG_URLACL_SET)), IntPtr.Zero); if (removeReservation) { if (0 != errorCode) throw getException( "HttpDeleteServiceConfigurationAcl", errorCode); return; } errorCode = HttpSetServiceConfigurationAcl( IntPtr.Zero, HttpServiceConfigUrlAclInfo, ref configInfo, Marshal.SizeOf( typeof(HTTP_SERVICE_CONFIG_URLACL_SET)), IntPtr.Zero); if (0 != errorCode) throw getException( "HttpSetServiceConfigurationAcl", errorCode); } finally { errorCode = HttpTerminate( HTTP_INITIALIZE_CONFIG, IntPtr.Zero); if (0 != errorCode) throw getException( "HttpTerminate",errorCode); } } static Exception getException(string fcn, int errorCode) { return new Exception( string.Format("{0} failed: {1}", fcn, getWin32ErrorMessage(errorCode))); } static string createSddl(string account) { string sid = new NTAccount(account).Translate( typeof(SecurityIdentifier)).ToString(); // DACL that allows generic execute for the user // specified by account. // See help for HTTP_SERVICE_CONFIG_URLACL_PARAM // for details on what this means. return string.Format("D:(A;;GX;;;{0})", sid); } static string getWin32ErrorMessage(int errorCode) { return Marshal.GetExceptionForHR(HRESULT_FROM_WIN32(errorCode)); } static int HRESULT_FROM_WIN32(int errorCode) { if (errorCode <= 0) return errorCode; return (int)((0x0000FFFFU & ((uint)errorCode)) | (7U << 16) | 0x80000000U); } // P/Invoke stubs from http.h const int HttpServiceConfigUrlAclInfo = 2; const int HTTP_INITIALIZE_CONFIG = 2; [StructLayout(LayoutKind.Sequential)] struct HTTPAPI_VERSION { public HTTPAPI_VERSION(short maj, short min) { Major = maj; Minor = min; } short Major; short Minor; } [StructLayout(LayoutKind.Sequential)] struct HTTP_SERVICE_CONFIG_URLACL_KEY { [MarshalAs(UnmanagedType.LPWStr)] public string UrlPrefix; } [StructLayout(LayoutKind.Sequential)] struct HTTP_SERVICE_CONFIG_URLACL_PARAM { [MarshalAs(UnmanagedType.LPWStr)] public string Sddl; } [StructLayout(LayoutKind.Sequential)] struct HTTP_SERVICE_CONFIG_URLACL_SET { public HTTP_SERVICE_CONFIG_URLACL_KEY Key; public HTTP_SERVICE_CONFIG_URLACL_PARAM Param; } [DllImport("httpapi.dll", ExactSpelling = true, EntryPoint = "HttpSetServiceConfiguration")] static extern int HttpSetServiceConfigurationAcl( IntPtr mustBeZero, int configID, [In] ref HTTP_SERVICE_CONFIG_URLACL_SET configInfo, int configInfoLength, IntPtr mustBeZero2); [DllImport("httpapi.dll", ExactSpelling = true, EntryPoint = "HttpDeleteServiceConfiguration")] static extern int HttpDeleteServiceConfigurationAcl( IntPtr mustBeZero, int configID, [In] ref HTTP_SERVICE_CONFIG_URLACL_SET configInfo, int configInfoLength, IntPtr mustBeZero2); [DllImport("httpapi.dll")] static extern int HttpInitialize( HTTPAPI_VERSION version, int flags, IntPtr mustBeZero); [DllImport("httpapi.dll")] static extern int HttpTerminate(int flags, IntPtr mustBeZero); }

You can also use a tool called HTTPCFG.EXE, which is part of the support tools found in the SUPPORT subdirectory of your operating system installation disk. You can use this tool to list existing namespace reservations, like so:

httpcfg query urlacl

Here's how to create a namespace reservation for a user account using HTTPCFG:

httpcfg set urlacl –u https://+:8080/MyServices/ -a D:(A;;GX;;;S-1-5-21-1681502023-2202157333-1552196959-1028)

The -a argument to HTTPCFG stands for ACL, or Access Control List, which you must specify in a rather arcane language called Security Description Definition Language (SDDL). There isn't enough space here to explain SDDL, so I'll leave that as an exercise you can do on your own. (For more information on SDDL, check out "Security Descriptor Definition Language".) The GX you see refers to GENERIC_EXECUTE permission. HTTP.SYS expects you to grant this permission if your goal is to grant permission to listen on the prefix.

The -u argument is the URL prefix that tells HTTP.SYS the shape of the URLs that you're referring to when you grant permission. The form of the URL is as follows. The scheme must be http or https, in lowercase. The host is case-insensitive and may use either the + or * wildcards (I'll talk more about this shortly). The port is an integer value and is required, even if you're talking about the default port for the scheme. Following this is an optional case-insensitive relative URI (in the previous example, this is /MyServices). And finally, regardless of whether you supply a relative URI, you need to terminate the string with a trailing slash.

Under the covers, the Windows Communication Foundation HTTP channel registers namespaces like the URL shown earlier. It does this using the HTTP API, which has a few rules about routing requests to HTTP listeners. The way you specify the host in the URL determines the priority in which your listener will be considered when a request comes in that matches more than one listener's prefix. For example, one app might register while another app might register Generally the more explicit registration wins. However, there are also wildcards that can be used to control how this prioritization works. An application might register https://*:8080/. Since this listener uses the * wildcard, it picks up the dregs of whatever the other listeners don't want. In other words, * is a low priority or weak wildcard. On the other hand, if the application registered https://+:8080/, it's going to be given top priority and any HTTP request on port 8080 will go to this application without checking for other, more specific registrations. You can read more about how these URL prefixes are formed at "UrlPrefix Strings", but suffice it to say that if you want to successfully reserve a namespace prefix for the Windows Communication Foundation HTTP channel, you'll have to use a form that is general enough to cover the actual prefix that Windows Communication Foundation is going to register.

If you refer back to Figure 1, you can see that the base address I registered with ServiceHost is localhost:8080/MyServices/. Unfortunately, if you simply do the obvious thing and try to register this URL prefix explicitly with HTTPCFG, it will not work. This is because under the covers, Windows Communication Foundation appears to be registering a strong wildcard, which casts a wider net and won't be covered by an explicit host name registration. Instead, you need to use the strong wildcard syntax and register a URL prefix of https://+:8080/MyServices/. This is true regardless of whether you do this programmatically or via the HTTPCFG tool.

You might be asking yourself, "Why do I need to go through all of this hassle? Why can't any old user just register an HTTP listener like they can open a socket?" I believe the answer has to do with the way HTTP.SYS does port sharing. With a socket, once you bind to a port and start listening, that port is in use and no other application can listen on it. But with HTTP.SYS, two applications can listen on the same port because it's actually HTTP.SYS that opens the port and routes each request based on the URL prefix. Say your company allows its users to run a legitimate application that happens to listen on port 8080 by registering an HTTP listener, and the administrator has opened his firewall to allow HTTP requests through to that port. That port is a potential target for malware. Now imagine that one of the users accidentally opened an executable attachment from a bad guy, and he ended up with malware on his machine that listens for instructions from its creator. This malware might hide on port 8080 by registering a listener through the HTTP API, sharing the port with the legitimate application! But since HTTP.SYS requires you to have permission from the administrator before installing a listener, as long as you're running as a normal user, the malware won't be able to register its listener due to your security context.

There are a lot of people out there who write code while running as an administrator. Sadly they aren't going to notice this problem until they deploy their code in a non-privileged environment, which is one reason you should always test your code as a non-admin. Better yet, you can try writing code while running as a non-admin! I won't jump on that soapbox here, but if you're interested in learning more about developing as a non-admin, check out Aaron Margosis's blog, where he's dedicated many posts to the topic.

Q What is the best way to implement split knowledge and dual control of keys?

Q What is the best way to implement split knowledge and dual control of keys?

A Many Web sites accept credit card holder data, including credit card numbers, billing addresses, and so on. In late 2004, a standard called the Payment Card Industry Data Security Standards (PCI-DSS) was published to specify security requirements for companies that store, process, or transmit this type of data. One of the newer requirements is Section 3.6.6, which requires, "Split knowledge and dual control of keys (so that it requires two or three people, each knowing only their part of the key, to reconstruct the whole key)."

A Many Web sites accept credit card holder data, including credit card numbers, billing addresses, and so on. In late 2004, a standard called the Payment Card Industry Data Security Standards (PCI-DSS) was published to specify security requirements for companies that store, process, or transmit this type of data. One of the newer requirements is Section 3.6.6, which requires, "Split knowledge and dual control of keys (so that it requires two or three people, each knowing only their part of the key, to reconstruct the whole key)."

In an online transaction system, it's pretty hard to imagine having two or three people standing around typing in a secret key each time it's needed to process a credit card request. But if you have long-term storage of sensitive data like this, the concept starts to seem a lot more reasonable. So how would you go about building such a system if you wanted to satisfy this requirement?

The simplest way to split a message up among N different parties is to generate N-1 random secrets (each must be exactly as long as the message to be protected) and XOR each byte of those random secrets into each corresponding byte of the message. The blob of data you end up with after all of these operations is treated as the Nth secret. You can then give out each of these secrets to different people and destroy the original message. If one person looks at his own secret, it's going to look like random garbage (after all, it is just random data). The only way to reconstruct the original message is to bring all of the people back together, gather their secrets, and XOR them together. Because of the commutative property of XOR, the order in which this happens doesn't matter. As long as every one of the split secrets is mixed back in, you'll ultimately get back the original message.

I'm using the term "message" to refer to the set of bytes that needs to be protected. This could be a string, like "Attack at dawn!" or binary data, such as a cryptographic key used to protect persistent cardholder data.

The security of this technique is perfect in theory. (I say in theory because implementations are usually less than perfect!) If one person refuses to give up her secret, the other people cannot put their secrets together to get any closer to the original message than they were on their own. But this also leads to the drawback: if anyone loses his secret, the original message can never be recovered. If this is a concern, there are other, considerably more complicated approaches that involve solving equations in a finite field (see Bruce Schneier's book Applied Cryptography, Second Edition). I'll leave implementing these more complicated schemes for a future column. For now, let me present a simple solution to the secret-splitting problem using System.Security.Cryptography.

My proof-of-concept is a layered solution that starts with two core functions in a class called SecretSplitter. These functions split or join a message using the technique I described earlier. The code for these functions is shown in Figure 4. The first function, SplitSecret, takes a byte array that represents the message to be split and an integer that indicates how many secrets you want it to be split into. JoinSecret takes an array of split secrets and joins them together to form the original message. You'll get something different if you don't provide the exact keys that were originally split.

Figure 4 Splitting and Joining a Message

public static List<byte[]> SplitSecret(byte[] secret, int count) { if (null == secret || 0 == secret.Length) throw new ArgumentException("Non-empty value required", "secret"); if (count < 2) throw new ArgumentException("Must be greater than one", "count"); RNGCryptoServiceProvider random = new RNGCryptoServiceProvider(); // Get N-1 new secrets. List<byte[]> newSecrets = new List<byte[]>(count); for (int i = 0; i < count - 1; ++i) { byte[] newSecret = new byte[secret.Length]; random.GetBytes(newSecret); newSecrets.Add(newSecret); } // XOR all secrets into the existing one to get the final secret. byte[] finalSecret = (byte[])secret.Clone(); foreach (byte[] newSecret in newSecrets) { for (int i = 0; i < finalSecret.Length; ++i) { finalSecret[i] ^= newSecret[i]; } } newSecrets.Add(finalSecret); return newSecrets; } public static byte[] JoinSecret(List<byte[]> splitSecrets) { if (null == splitSecrets) throw new ArgumentNullException(); byte[] secret = null; foreach (byte[] splitSecret in splitSecrets) { if (null == splitSecret) throw new ArgumentNullException(); if (null == secret) { secret = (byte[])splitSecret.Clone(); continue; } if (splitSecret.Length != secret.Length) throw new ArgumentException( "All secrets must be of the same length"); // XOR all the split secrets together to get the original secret. for (int i = 0; i < secret.Length; ++i) { secret[i] ^= splitSecret[i]; } } return secret; }

Note the use of the RNGCryptoServiceProvider class to generate the secret keys. This is a much better technique than using something like System.Random. RNGCryptoServiceProvider was specifically designed to generate random data for cryptographic operations and thus isn't nearly as predictable as the stream of data you'll get from something like System.Random. It also seeds itself from many different sources of entropy on your computer.

Layered on top of these two functions are helpers that allow you to work purely with strings by Base64 encoding the data. This code is less relevant, so I won't show it here, but it's useful because we often deal with string-based messages and text files. All of the functionality is packaged in a library assembly, which I called SecretSplittingLibrary.dll.

On top of this library, I built a console application to demonstrate how key splitting might work in a specific application. This application is called ThumbDriveSecretSplitter and has two commands: one to split a message and one to join a message.

To split a message, you can run the following command:

ThumbDriveSecretSplitter split "Attack at dawn!"

This should split the message "Attack at dawn!" onto all the thumb drives that are currently plugged into the machine. Well, that was my original goal at least. But when I first tried this out, I discovered a problem: the app had some difficulty determining which volumes represented removable media. I have a thumb drive that shows up as a fixed drive in Disk Manager and I suspect that it's not the only one with this issue.

So I cheated and gave the program a little hint. In the application's configuration file, I included a list of volume names that the app should look for to determine which drives should be used:

<appSettings> <add key="volumeNames" value="FatDrive;PSThumbDrv"/> </appSettings>

Now when I run the command, the program looks for drives that are included in the configured list and splits the message among those drives by dropping a Base64-encoded split secret into a file called "mysecret.txt" in the root of each drive. This means I can now split the message among all the thumb drives, and then hand out the drives to the individuals responsible for guarding the secret.

When you want to restore the original message, you just need to get those folks to plug their thumb drives into your machine, and then you run the other command:

ThumbDriveSecretSplitter join

This combines the secrets and prints out the protected message. Here's the output of a session using ThumbDriveSecretSplitter:

C:>ThumbDriveSecretSplitter.exe split "Attack at dawn!" Writing E:\mysecret.txt Writing F:\mysecret.txt C:>ThumbDriveSecretSplitter.exe join Reading E:\mysecret.txt Reading F:\mysecret.txt Joining secrets... Attack at dawn!

When using this technique in a real system, you probably don't want to display the secret for all parties to see—that would defeat the purpose of splitting it up in the first place. Instead, you'd have the program collect the keys, compute the secret, and operate on it (most likely using it as a secret key to decrypt a sensitive backup of cardholder data).

Send your questions and comments for Keith to

Keith Brown is a cofounder of Pluralsight, a premier Microsoft .NET training provider. Keith is the author of Pluralsight's Applied .NET Security course, as well as several books, including the .NET Developer's Guide to Windows Security, available in print and on the Web.