In Search of Robust, Secure IPC for .NET

.NET Remoting Services are the recommended solution for IPC in .NET, but there are many scenarios where .NET Remoting won't work. Shawn shows how to get simple, intrasession IPC messaging in .NET by creating a managed library for good old Dynamic Data Exchange (DDE).


December 01, 2003
URL:http://www.drdobbs.com/in-search-of-robust-secure-ipc-for-net/184416711

In Search of Robust, Secure IPC for .NET

The networking and communications options available on the .NET platform cover a vast space: Sockets, HTTP Web Services, and Remoting Services head the list. But something's missing. In the rush to include a managed library for communication across every possible boundary, employing every popular technology and modern standard, a very basic communication need has been overlooked: IPC, or interprocess communication.

The standard Microsoft party-line is to use .NET Remoting Services for IPC. That's all well and good in theory. But in practice, both of the Remoting channels available to .NET programs out of the box — one based on HTTP, the other raw TCP/IP — are entirely inappropriate for most IPC scenarios. In the first half of this article, I'll explain why. Then we'll take a quick tour of the various IPC mechanisms available to us in the unmanaged Win32 world, select one as a candidate for our IPC needs, and develop a simple managed wrapper for it.

Join me (with apologies to Leonard Nimoy) as we go in search of...robust, secure IPC for the .NET platform.

What is IPC?

Inter-Process Communication (IPC) is used almost everywhere. It's used by the Windows shell to open or print documents in an already-running instance of the associated program. It's used by Word to embed Excel spreadsheets (and by Excel to embed Word content).

IPC is not the name of a specific software technology, as is RPC (remote process communication). Rather, IPC is a generic slang term for any mechanism by which two running programs communicate with each other. IPC stands in contrast to RPC, which allows processes on different computers to communicate across a network — IPC, by definition, refers strictly to same-machine communication.

Before the advent of protected-mode operating systems, IPC was easy because all programs shared the same memory space. Interprocess communication was direct and instantaneous. In fact, IPC was too easy — programs regularly "communicated" to other programs by accidentally overwriting each other's code and data, in memory. One might say the problem of "inadvertent IPC" is what protected memory-mode operating systems were invented to solve in the first place. So, in a way, all modern IPC mechanisms can be said to share the same goal: Allowing two or more processes to safely exchange information across virtual-memory boundaries.

The .NET runtime adds an additional layer of complexity by distinguishing AppDomains from OS processes (a single OS process can serve one or more .NET AppDomains). But this is just an implementation detail — clearly, any mechanism we can use to communicate across process boundaries on the operating-system level can be used intraprocess, across AppDomain boundaries, at the .NET level. Still, process-memory boundaries are not the only boundaries we must consider.

Jumping Over Boundaries, Smashing Through Barriers

On Windows NT-based operating systems, processes run as the users who started them, thus forming security boundaries between running processes. Windows 2000 (and later) up the ante by allowing multiple simultaneous user sessions to log onto a single machine via Terminal Services. As of Windows XP, Terminal Services have been promoted into ubiquitous features of the operating system, such as Fast User Switching, Remote Desktop, and Remote Assistance. Thus, processes are separated by Terminal Services session boundaries, as well.

Clearly, it's no longer sufficient to distinguish IPC from RPC as being "local only" — we must consider IPC mechanisms that are machine-wide (intersession communication) alongside IPC mechanisms that are session-wide (intrasession communication).

The Win32 platform offers numerous technologies for implementing IPC functionality in both categories. Table 1 lists some of the more popular IPC mechanisms, and how they behave under Terminal Services: as inter- or intrasession communication. As you can see, some of the mechanisms are flexible enough to support both.

In the introduction, I hinted that .NET Remoting is no good for IPC. Proper handling of Terminal Services sessions is a big reason why.

Why Can't We Use .NET Remoting?

In Part 1 of "Real World Sample Applications," a series of articles appearing in the MSDN Library (see References), Chris Anderson recommends the use of Remoting for a very simple IPC scenario: forwarding command-line arguments to an already-running instance of an app. In his own words, the experience was "quite an effort."

.NET Remoting, as the name implies, is designed for remote process communication. IPC is really not a special case of RPC — they are, by definition, completely distinct use cases. The features and liabilities brought to bear by the RPC technologies that underpin .NET Remoting are not relevant to, nor welcome in, most IPC applications.

But the question is not simply one of performance, efficiency, or reliability. The RPC technologies that comprise .NET's two standard Remoting channels (Figure 1), introduce real, nonnegligible system requirements: a viable TCP network stack (in the Sockets case), and IIS (in the HTTP case).

Furthermore, neither of the two .NET Remoting channels is at all aware of Terminal Services — that is, their endpoints are scoped by the entire machine, not individual logon sessions.

So even if we're willing to endure the security implications of using TCP for IPC, there is simply no way to do so in a manner that works under Terminal Services. The "Real World Sample Applications" article comes as close as you can get: Ask the OS to allocate an unused TCP port, and publish that port number in the per-use registry hive (HKEY_CURRENT_USER).

That actually works for the Fast User Switching and the Remote Desktop/Assistance features of Windows XP. But Windows Server 2003 allows multiple simultaneous sessions of a single user account! There is no "per-session" registry hive in which to publish a dynamically allocated IPC endpoint.

So, what does all this boil down to? The use of Remoting proposed in "Real World Sample Applications" is broken, with respect to both security and basic functionality.

The security vulnerability is of the "elevation of privilege" variety. That's bad. There are some standard tricks — the TCP socket can be bound to the localhost and set to reject all remote requests — but that won't stop other users logged on at the same machine from sending false command lines to an instance of the app being run by the local Administrator.

The functionality problem comes in Windows Server 2003, where local Administrators find they're unable to run the app in more than one session — command-line arguments intended for, say, the app running in the second session will be mistakenly sent to the app running in the first session (or vice versa). Not good! Programming for multisession Windows compatibility is hard; there are many pitfalls and quite a few modern apps fail the test. But .NET is new and destined, someday, to become the official user-mode API of the Windows platform — one should expect proper support for multisession Windows, out of the box.

What Are the Alternatives?

Remoting is a pluggable architecture — an abstract API for communicating across different mediums — that one needn't necessarily find an alternative to, just an alternative channel. TCP/IP, with its ridiculously limited endpoint space, is the problem, not the abstract interfaces built around it. Alternative Remoting channels have been developed by various parties, including one based on Named Pipes by Microsoft's own Jonathan Hawkins (see References).

Named Pipes can be a reasonabe choice for IPC, if done right. The namespace of endpoints is not as limited as in TCP/IP (hundreds of characters compared to the 16-bit port number of TCP/IP), so applications are free to establish a unique, well-known endpoint name (based on a GUID or URI, perhaps). Further, Named Pipes are Win32 kernel objects, which can be explicitly created in either the local (intrasession) or global (intersession) kernel object namespaces (see References). This is done by simply prefixing the endpoint name with "Local\" or "Global\", as desired. How flexible!

Unfortunately, we must still worry about security because Named Pipes is a network protocol, not an IPC mechanism. The user running in Terminal Services' session zero (where NT's system services and the first logged on user run — session zero is sometimes called the "physical console" or "console session") will have his or her Named Pipe endpoint exposed on the network, like it or not. And the default security descriptor for Named Pipes is far too open (granting read access for Everyone, even unauthenticated users).

It gets worse: Even after handcrafting a Win32 security descriptor to properly secure our pipe for IPC, the Named Pipes protocol is fundamentally susceptible to some well-known denial-of-service attacks. The subtle details of Named Pipes security are beyond the scope of this article; suffice to say that Microsoft has moved away from the use of Named Pipes for RPC (let alone IPC!) in the post-Windows XP era of security. The threat surface is just too vast.

Do As They Do...

So what does Microsoft use for IPC? MSDN sample applications notwithstanding, Microsoft has yet to ship a large, commercial product based on the .NET platform, so it's hard to say. In the unmanaged space, the IPC mechanisms most often used by Microsoft products fall into two categories: user-interface related IPC, and IPC related to system services.

For the former case, Dynamic Data Exchange (DDE) is still heavily used. DDE is an archaic protocol that marshals messages as binary data packed inside Windows messages. We'll see more of DDE later. For the latter case, LRPC is used. LRPC is actually an RPC transport protocol that's confined to a single machine. It's incredibly lightweight and efficient, very secure, and provides the full richness of the RPC request/response programming model. Further, the space of endpoint names is sufficiently large enough (80 characters) to avoid the messiness of having to dynamically allocate and publish endpoints.

Unfortunately, as we saw in Table 1, LRPC is a system-wide IPC mechanism, only. LRPC endpoint names are not kernel object names, and (currently) cannot be scoped to the current user's session, as we saw with Named Pipes earlier.

Most other IPC mechanisms used by Microsoft products (e.g., COM, COM+, MSMQ) are wrappers around RPC at some level — typically RPC over TCP/IP or LRPC — using RPC's endpoint-mapper service to allocate and negotiate endpoints dynamically, wherever needed.

Choosing the Right Path

There are a great many factors to consider when selecting an IPC mechanism for use in your applications. The behavior under a Terminal Services environment is important (and perhaps the most often overlooked), but there are others.

Some IPC mechanisms offer a convenient RPC-style request/response programming model; others are more simple, asynchronous "fire and forget" messaging systems. Some (such as Named Pipes) offer both options. Some (again, Named Pipes) aren't fully implemented on Windows 95/98/ME.

Some IPC mechanisms are based on connections between a single client and server; others offer broadcast-style or one-to-many messaging. Table 2 lists the IPC mechanisms of Table 1, alongside their respective programming models and topologies.

In the next section, we'll write some code — but before we choose an IPC mechanism to wrap, we need to consider a real-world scenario so we can make an intelligent choice. Let's revisit the simple (but very common) use case of Microsoft's "Real World Sample Applications," and design an IPC mechanism suitable for forwarding command-line arguments to an already-running instance of our application. We'll follow Microsoft's lead in the way the Windows shell instructs running applications to open and print files: via DDE.

Resurrecting DDE in .NET

DDE seems like ancient technology next to .NET — the juxtaposition is almost dizzying! But DDE is actually a very good choice for simple, intrasession, interprocess messaging needs. It's lightweight and efficient, and the number of native Win32 functions to P/Invoke is small, so performing a thorough security review is relatively easy. In fact, the security threat surface of DDE is probably less than any other IPC mechanism — DDE is based on Windows messages, which are a purely in-memory, intrasession communication medium. (If an attacker is sending or intercepting Windows messages in your app, you've probably already been hacked!)

DDE is old, but we all still use it everyday — even Visual Studio .NET uses DDE, in conjuction with the Windows shell, to open C# and VB.NET source files (among others) in an already-running instance of devenv.exe. This same functionality is available via newer IPC technologies (like OLE Automation), but you just can't beat the simplicity, interoperability, and low overhead of DDE.

Still, DDE is not a panacea for all our IPC needs, present and future (i.e., DDE can't be used for communication across Terminal Server session boundaries), so we should take the time to define an abstract, reusable architecture for simple IPC messaging needs. Listing 1 contains the base class and interface definitions for a vastly simplified "remoting" architecture.

The IMessageSink interface could not be simpler: With just one method accepting an array of bytes, it serves as the actual pipeline for senders and receivers of messages. IpcEndpoint, IpcClient, and IpcServer are abstract base classes that define the general contract for various IPC technologies' endpoint, client, and server implementations, respectively.

There is a parent/child pattern at play, here: Instances of IpcClient- and IpcServer-derived classes are created by static methods on their corresponding IpcEndpoint-derived classes, and contain back-references to the IPC endpoint that they represent. Figure 2 shows a simplified class diagram for this design, including implementations of IMessageSink for both sending and receiving of messages.

The design is all set — all that's left is the protocol-specific implementation. We derive a set of DDE-specific classes from the abstract base classes: DdeEndpoint, DdeClient, and DdeServer. These are also shown in Figure 2.

DDE is a very simple protocol, consisting of about a dozen Win32 functions, messages, and structures. We only need a handful of those: WM_DDE_INITIATE, to establish a connection; WM_DDE_EXECUTE to transmit a message; and WM_DDE_TERMINATE to close the connection.

Listing 2 shows the P/Invoke declarations for accessing DDE functionality in the Win32 API. The declarations make use of the SuppressUnmanagedCodeSecurity attribute so, in our implementation, we must take care to validate all input and output with due diligence.

Wrapping the DDE code with the abstract framework in Listing 1 is left as an exercise for the reader. (Or, if you prefer, just download the code accompanying this article — DDE is a simple protocol, but the details of handling asynchronous Windows messages are a bit too prosaic to show here.)

Conclusion

In this article, we undertook a comprehensive review of .NET and Win32 IPC technologies, and discussed why some of them, popular though they may be for RPC, simply aren't appropriate for IPC. One common sticking point is Terminal Services support — making the distinction between intrasession and intersession IPC mechanisms. Another is security: consideration of threat surface, and exposure to elevation-of-privilege and denial-of-service attacks. Unfortunately, neither of the two .NET Remoting channels included with the Common Language Runtime is serviceable for intrasession IPC scenarios, for a combination of these reasons.

Finally, we resurrected a very old (but very simple and reliable) technology called Dynamic Data Exchange (DDE) by creating a managed library for simple, intrasession IPC messaging.

References

"Real World Applications Sample, Part 1: Adding SDI, Single Instance, and a Forms Collection to Windows Forms," Chris Anderson, Microsoft Corp. http://msdn.microsoft.com/library/en-us/dnwinforms/ html/reaworapps1.asp.

Named Pipe Remoting Channel, Jonathan Hawkins, Microsoft Corp. http://www.gotdotnet.com/userfiles/jhawk/NamedPipeChannel.zip.

Kernel Object Namespaces, Terminal Services Platform SDK, MSDN Library. http://msdn.microsoft.com/library/en-us/termserv/termserv/ kernel_object_namespaces.asp.
w::d


Shawn A. Van Ness is an independent software consultant specializing in COM, XML, and .NET programming. His many years of experience in the industry include stints at DevelopMentor, Microsoft, and now Leszynski Group, where he leads the design and development of Tablet PC solutions. He can be reached at [email protected].

In Search of Robust, Secure IPC for .NET

Figure 1 .NET Remoting Architecture

In Search of Robust, Secure IPC for .NET

Figure 2 Simple IPC Messaging Architecture

In Search of Robust, Secure IPC for .NET

Listing 1 Base class and interface definitions for IPC architecture


namespace Jitsu.Ipc
{
  public interface IMessageSink
  {
    void AcceptMessage(byte[] payload);
  }

  public abstract class IpcEndpoint
  {
    // Create a client-side connection to the IPC endpoint.
    public abstract IpcClient Connect();

    // Create the server-side of an IPC endpoint, and 
    // begin listening for messages.
    public abstract IpcServer Listen();
  }

  public abstract class IpcClient : 
    IMessageSink, IDisposable
  {
    // Send a message to the IPC endpoint (implements IMessageSink).
    public abstract void SendMessage(byte[] payload);

    // Close the client-side of the connection 
    // (implements IDisposable).
    public abstract void Close();
  }

  public abstract class IpcServer : IDisposable
  {
    // Begin listening for messages (blocks until StopListening 
    // is called).  Incoming messages are routed to the app's 
    // IMessageSink callback.
    public abstract void StartListening(IMessageSink callback);

    // Shuts down the IPC endpoint (implements IDisposable).
    public abstract void StopListening();
  } }

In Search of Robust, Secure IPC for .NET

Listing 2 P/Invoke declarations for accessing DDE


using System;
using System.Runtime.InteropServices;

namespace Jitsu.Dde
{
  internal sealed class Win32
  {
    private Win32()
    {} // suppress default public ctor for static class

    //
    // Managed wrappers for Win32 functions

    public static ushort GlobalAddAtom(string s)
    {
      ushort r = PInvoke.GlobalAddAtom(s);
      if (r == 0) throw new System.ComponentModel.Win32Exception();
      return r;
    }

    public static void UnpackDDElParam(         int msg, IntPtr lParam, out IntPtr loword, out IntPtr hiword)
    {
      bool b = PInvoke.UnpackDDElParam(msg,lParam,out loword, out hiword);
      if (!b) throw new DdeException("UnpackDDElParam failed.");
    }

    public static void FreeDDElParam(int msg, IntPtr lParam)
    {
      bool b = PInvoke.FreeDDElParam(msg,lParam);
      if (!b) throw new DdeException("FreeDDElParam failed.");
    }

    public static void PostMessage(         IntPtr hwnd, Message msg, IntPtr wParam, IntPtr lParam)
    {
      bool b = PInvoke.PostMessage(hwnd,(int)msg,wParam,lParam);
      if (!b) throw new System.ComponentModel.Win32Exception();
    }

    //
    // Nested class for extern Win32 declarations





    [SuppressUnmanagedCodeSecurity]     public sealed class Pinvoke
    {
      private PInvoke()       {} // suppress default public ctor for static class

      [DllImport("User32.dll")]
      public static extern bool IsWindowUnicode(IntPtr hwnd);

      [DllImport("Kernel32.dll", SetLastError=true)]
      public static extern ushort GlobalAddAtom(string s);

      [DllImport("Kernel32.dll")]
      public static extern void GlobalDeleteAtom(ushort atom);

      [DllImport("User32.dll")]
      public static extern IntPtr PackDDElParam(           int msg, IntPtr loword, IntPtr hiword);

      [DllImport("User32.dll", SetLastError=false)]
      public static extern bool UnpackDDElParam(           int msg, IntPtr lParam, out IntPtr pLo, out IntPtr pHi);

      [DllImport("User32.dll", SetLastError=false)]
      public static extern bool FreeDDElParam(int msg, IntPtr lParam);

      [DllImport("User32.dll")]
      public static extern IntPtr SendMessage(           IntPtr hwnd,int msg, IntPtr wParam, IntPtr lParam);

      [DllImport("User32.dll", SetLastError=true)]
      public static extern bool PostMessage(           IntPtr hwnd,int msg, IntPtr wParam, IntPtr lParam);
    }

    //
    // Supporting types

    public enum Message
    {
      DdeInitiate = 0x03E0,
      DdeAck = 0x03E4,
      DdeExecute = 0x03E8,
      DdeTerminate = 0x03E1
    }
  }
}

In Search of Robust, Secure IPC for .NET

Table 1 IPC mechanisms and their Terminal Services support

In Search of Robust, Secure IPC for .NET

Table 2 IPC models and topologies

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.