Communicating with the Desktop


A major security problem in the past has been the fact that the console user and services shared console 0. One of the problems is that a service could put windows on the desktop, and the local user could then attack the service with Window messages. Window messages were created for the older 16-bit versions of Windows and, when ported over to Windows NT, did not have any security added because the desktop is deemed a trust boundary: any process running in the same console can communicate with any other process running in the same console using Windows messages. As a consequence, processes running under different user contexts could send messages that exploited flaws in either the application itself or in the way that the windowing subsystem handles messages. The entire class of attacks was publicized as a “shatter” attack in a series of papers by Chris Paget (Paget 2002). And although it has been well known for some time (Microsoft 1994) that there are a number of dangers to running applications with different user contexts on the same desktop, these dangers are not confined to Windows systems, and the exact techniques used do vary. It isn’t advisable for a service to put UI on a user’s desktop; a number of services have used this practice.

More Info  

At the time this book was written, the original source URL for Paget’s paper was not working. For a more comprehensive article, see the Wikipedia reference at http://en.wikipedia.org/wiki/Shatter_attack.)

A second side effect of sharing console 0 between the console user and services is that named objects can have namespace collisions. If you’re familiar with the Saltzer and Schroeder design principles (Saltzer and Schroeder 1975), this is a failure of the principle of least common mechanism, which implies that functionality and services provided to multiple users should have a proper level of separation. Namespace collisions have two consequences: the first is that you can end up with access controls that are much too open than would be best, and the second is that name squatting attacks can be launched. For our sample service, if someone could pre-create the WSC_Vista_Pipe, then instead of getting ridiculous developer excuses, you could have different rude messages delivered, and worse yet, until impersonation became a privilege, the process running the imposter pipe could have impersonated the client.

The first step toward resolving the problems with console 0 was to institute a separation of local versus global namespaces. On Windows XP in fast user switching mode, secondary logons will create objects in local namespaces; on Windows Server 2003 with terminal services in application server mode, all remote logons create named objects in local namespaces, although the console user still shares the global namespace with services. Windows Vista extends this by placing all console logons outside of session 0, even though you could rightly argue that there is still an incomplete enforcement of least common mechanism. Services do run under a number of different user contexts, and even though services can only be installed by administrators, scheduled tasks (created only by administrators by default) also run in session 0. Ideally, we should break namespaces into per-user directories, but since all of the applications with potential collisions are controlled by administrators by default, there’s a reasonable performance-security trade-off.

image from book
Local and Global Namespaces

Just like persistent named objects have containers (directories and registry keys are two familiar examples), other named objects (events, mutexes and shared memory sections, just to name a few) are also created inside a container. The container is \BaseNamed-Objects. When all processes were creating named objects in the same place, there were a number of problems: the first was the possibility of name-squatting attacks. Most of the time, when we use a named object, we need some other piece of code to be able to find our object at run time, and it’s inconvenient to create random names and pass them out of band. Most developers would just hard-code the object name. If someone wanted to cause problems, they could just create the object before your process did, things wouldn’t work correctly, and there could be serious security problems in some cases.

The second problem is that creating an ACL for something meant to be used by all the users, regardless of logon session, is a difficult problem, and you may not be able to do this very well. For example, Office 2003 used shared memory sections to hold toolbar information and reduce memory footprint. If the shared memory gets created in the global namespace, it has to be writable by everyone. This wasn’t overall the best design, and a different approach that avoided shared memory was used in Office 2007.

Depending on the version of the operating system, users may have their own namespace to create named objects in, which avoids the name-squatting problem, and the default ACL is typically just fine, which avoids another issue. You’ll get local namespaces in the following circumstances:

  • Windows XP–not domain joined, if you’re using fast user switching, and you’re not the first user to log on

  • Windows Server 2003–when in Application Server mode, if you’re not the console user

  • Windows Vista and later–will always have a local namespace, unless you’re running as a service

We patched Office 2003 to create a much more restrictive ACL on the shared-memory sections when an available local namespace is found.

image from book

Consequently, we’ve been telling developers for some time not to allow services to interact with the desktop in an unsafe manner, and these services are now broken. If your service depends on having the interactive flag set, placing windows on the desktop, and receiving or sending window messages to and from the console user, then you’re going to need to find a safer way to do these tasks.

Here are some of the options you have–there is no single right answer, and the method you use depends on what you’re trying to do and your performance constraints. A common tradeoff you’ll see as we present the various options is that the higher the performance of the mechanism, the more difficult it is to secure; conversely, the easier it is to provide security, the lower the performance will tend to be.

Simple Message Boxes

The supported way to present simple message boxes to a user is to call WTSSendMessage, which has a set of arguments similar to the familiar MessageBox API. One difference is that you have two arguments for whether you’d like to wait for the user to respond and how long to wait for a response. You’ll also need to call WTSEnumerateSessions to find out which consoles are active and be sure to check the State member of the WTS_SESSION_INFO structure to make sure the session is active–remember that your application could be used on a terminal server as well as a desktop computer. This option gives you both good performance and solid security, but you have a very limited capability to get information from the console user, and you don’t have any real control over how the information is presented.

Shared Memory

You create a shared memory section by using CreateFileMapping with a first argument of INVALID_HANDLE_VALUE, which creates a memory section that isn’t backed by an actual file. You can use shared-memory sections as an extremely high performance way to transfer large amounts of data, and once the section is open, you just read and write memory. The problems you’ll face are that synchronizing access is difficult; there’s no way built into shared memory to ensure atomicity. You can utilize events or mutexes to try to coordinate access, but this depends on well-behaved clients–and that’s rarely a good option for security. A second problem is that you will also have problems setting a correct ACL unless you’re essentially using shared memory as a local broadcast mechanism. Most importantly, there’s nothing that enforces the format of the data, and you’ll need to ensure that any parsers are extremely robust. Although we haven’t tested this, you may also run into the same problem we did with our sample when we set SERVICE_SID_TYPE_RESTRICTED, and you’ll need to create an explicit ACL for the section on creation. Unless you have some unusual performance and application requirements, we’d tend to discourage this approach.

Named Pipes

Pipes are a good choice for service-client communication, especially if your service needs to perform actions on behalf of the user. Impersonating a client is as easy as calling Impersonate-NamedPipeClient and being sure to always check the return. Once a connection is established, named pipes also allow the client and server to have privacy for local connections. If you want to enforce privacy and integrity across the network, your options are to configure IPSec or to use cryptography to protect the data. For reasons we’ll discuss in a moment, named pipes aren’t an ideal mechanism for network communications, so you may want to avoid using named pipes on the network.

Using named pipes is fairly easy: on the server side, you call CreateNamedPipe to create as many instances of the pipe as the number of clients you’d like to support, and then call Con-nectNamedPipe to wait on clients to connect. Once connected, you call ReadFile and/or Write-File to copy data to and from the pipe. The usual set of nonblocking capabilities is present– you can either use overlapped I/O and wait on events, or you can use I/O completion ports. Using named pipes with events can be a little tricky, especially if you don’t want to have one thread per pipe, which is somewhat wasteful. With a service, we’d also need a few events to deal with control messages. Thus, by the time you get around to dealing with unexpected state transitions, the whole thing is fairly messy. With an I/O completion port, we can use one port to deal with all the pipes and control messages at once and serve all of the clients with one thread. Here’s the control handler within the worker thread for our sample service:

 if( GetQueuedCompletionStatus( g_hPort, &dwBytes, &CompletionKey,                                &pOverlapped, INFINITE ) ) {      if( CompletionKey == ControlKey )      {          // Handle control messages here          switch( dwBytes )          {          case SERVICE_CONTROL_STOP:             goto CleanUp;          case SERVICE_CONTROL_PAUSE:             g_CurrentStatus.dwCurrentState = SERVICE_PAUSED;             if( SetServiceStatus( hStatus, &g_CurrentStatus ) )             {                fPaused = true;                continue;             }             goto CleanUp;          case SERVICE_CONTROL_CONTINUE:             g_CurrentStatus.dwCurrentState = SERVICE_RUNNING;             if( SetServiceStatus( hStatus, &g_CurrentStatus ) )             {                fPaused = false;                continue;             }             goto CleanUp;         default:             WriteLogFile( "Unexpected control", dwBytes );             continue;         }     } ...

As you can see, this is a very simple piece of code. One “gotcha” to remember is that if you set the service status to SERVICE_PAUSE_PENDING in the control handler routine, and if you forget to subsequently set the status to SERVICE_PAUSED, the service control manager will time out your service and refuse to pass any further control messages to it. You’ll end up restarting the system or killing the process with Task Manager. Note that when you use Post-QueuedCompletionStatus to a completion port, all three of the arguments passed in are completely arbitrary: dwBytes, the completion key, and the OVERLAPPED pointer could be anything that fits into the size being passed. In this case, we used a completion key that was an index into the arrays used to maintain state for the pipes, and a key of ControlKey (0xffffffff) could be used to mean we had a special event posted to the port. The pipe handling code is nearly as simple; the bulk of it follows:

 switch( g_PipeState[CompletionKey] ) { case PipeStateStart:    assert( false );    break; case PipeStateWaitConnect:    // With a completion port design,    // it's hard to be truly paused    // We could do it with an event, but it's    // more polite to the client to just reject them    if( fPaused )    {       DebugState( g_PipeState[CompletionKey], PipeStateReset );       g_PipeState[CompletionKey] = PipeStateReset;       break;    }    // Let's log the connection info    // This consists of the remote computer name,    // client process ID, and client session ID    // If we wanted to maintain state, we could use these to limit    // the session to one pipe instance.    LogConnectionInfo( g_hServicePipe[CompletionKey] );    if( !WritePipe( g_hServicePipe[CompletionKey],                    g_PipeState[CompletionKey],                    g_Overlapped[CompletionKey] ) )    {       DebugState( g_PipeState[CompletionKey], PipeStateReset );       g_PipeState[CompletionKey] = PipeStateReset;    }    break; case PipeStateWritePending:    // To do - add more error checking here    // On success, write completed    DebugState( g_PipeState[CompletionKey], PipeStateWaitDisconnect );    g_PipeState[CompletionKey] = PipeStateWaitDisconnect;    break; case PipeStateWrite:    DebugState( g_PipeState[CompletionKey], PipeStateWaitDisconnect );    g_PipeState[CompletionKey] = PipeStateWaitDisconnect;    continue; case PipeStateWaitDisconnect:    continue; case PipeStateError:    fDone = true;    break; }

The one feature pipes are missing that could make them difficult to use across a network is that there’s no way to know when the client has read all of the data. Once we call WriteFile to write the data to the pipe, we might assume that when this has returned then the client would have read the data out of the pipe; thus we ought to be able to call DisconnectNamedPipe to disconnect the client from the pipe. Unfortunately, just because we were able to write data into the pipe only means that the pipe buffer had room to hold a few bytes. It says nothing about whether the client has read data and cleared the pipe. What we can do is call ConnectNamed-Pipe, and the return will tell us if the client has closed the connection on its end. If we wanted to make our server a little more complex, we could let clients write an ACK into the pipe once the client was ready for more data, but this still doesn’t help us against a misbehaved client that just opens the pipe and then hangs. If you’re familiar with sockets programming, what we’d like to be able to do is to send the client a shutdown message that tells them to get the data now, or lose it, but that’s not available to named pipes.

What can we do to avoid denial of service attacks against our pipe server? There are some really cool new APIs available that can help with this. We’ve demonstrated these in the following code snippet:

 void LogConnectionInfo( HANDLE hPipe ) {    char tmp[1024];    WCHAR wzComputerName[256];    ULONG ClientProcessId, ClientSessionId;    if( !GetNamedPipeClientComputerName( hPipe,                                         wzComputerName,                                         sizeof( wzComputerName ) ) )     {       wcscpy_s( wzComputerName, _countof( wzComputerName ), L"Unknown" );     }     if( !GetNamedPipeClientProcessId( hPipe, &ClientProcessId ) )        ClientProcessId = 0;     if( !GetNamedPipeClientSessionId( hPipe, &ClientSessionId ) )        ClientSessionId = 0; }

In this case, we just write the information to the debugging log, largely as an example of how to use these new functions. GetNamedPipeClientComputerName will fail if the client is local and will otherwise tell you the name of the remote system. Please remember that all of the naming systems available are prone to corruption and can be unreliable even when you’re not under attack, but it’s better than nothing–and it will reliably distinguish local and remote.

The next two APIs are more intriguing and allow you to do some interesting things. If you call GetNamedPipeClientProcessId, then you could do some checking up on that process. For example, say you wanted your desktop client to be the only code to connect to your server. Most of the current code that we’re aware of to do this depends on unreliable security by obscurity to perform some sort of handshake or, worse yet, uses an “encrypted” stream with a fixed key in the binaries. This is not encryption, nor is it security. Now that we can reliably get the process ID on the other end, we can see the path to the process; check the signature on the file given as the process binary; and, if we wanted to be complete, check the modules loaded in the process. In the case of the sample application, we could have guarded against denial of service attacks by calling GetNamedPipeClientSessionId and enforcing a rule that each session can only have one pipe instance running at any time. Now the user could create a denial of service condition–on only themselves! All three of the previously mentioned APIs also have a server variant that allows a client to find out the same information about the server end of the pipe.

There are two other ways that we could have guarded against denial of service attacks. The first would be to impersonate the pipe client long enough to identify the user and then maintain a per-user pool of pipes. A user could deny itself services, but not other users. The lsass named pipe uses exactly this scheme, which came about after this author demonstrated an annoying attack tool against beta versions of Windows 2000. The second approach that could be used if the pipe accepted anonymous clients would be to maintain a timer on a pipe instance once it had written its final data into the pipe and then just forcefully drop the client once the time out expired. If you wanted to get a little more sophisticated, you could only drop timed-out clients once you ran out of available pipe instances, which might save you from creating problems with clients on very slow or congested networks or with local clients on a very busy system. The SDK recommends using FlushFileBuffers to cause the pipe server to block until the client has read all the data, but we’re not in favor of servers (or even clients) that make blocking calls.

Named pipes have very good performance characteristics and allow for synchronization, as well as a solid separation of the various clients, but it places no restrictions on the format of data passed between the client and the server. You will need to carefully validate any parsers that deal with data being read from the pipe. As an example, our sample client makes the following mistake:

 if( ReadFile( hPipe, wzMsgBuf, 1024, &dwRead, &ol ) && dwRead > 0 ) {     printf( "%S\n", wzMsgBuf ); }

The problem here is that this code makes the assumption that the incoming message is null terminated. If the client connected to an evil server, the printf statement could crash with a read access violation (AV). A more robust piece of code would call wcslen_s to check and see if a null was encountered within wzMsgBuf, or simply check that wzMsgBuf[ dwRead-1] was a null character. Being able to validate the data types passed to your service is one of the primary advantages of RPC over named pipes, although at a cost of some additional development time.

Sockets

Sockets are meant to be used in hostile environments and have a full set of protections against denial of service conditions–networks can do some nasty things, even when there aren’t any attackers causing trouble. We could have easily written the preceding application to work with a socket listening on the localhost address (127.0.0.1 in IPv4 or 0:0:0:0:0:0:0:1 in IPv6), and it would not have been prone to denial of service attacks. Although I/O completion ports and sockets aren’t overly well documented, they’re a great technique for writing high-perfor-mance servers.

What you lose with sockets is the ability to easily authenticate or impersonate clients. This can be done with the Security Support Provider Interface (SSPI) set of API calls, but SSPI isn’t one of the easier API sets to use. Sockets also have no idea of the process or session that originated a connection, although the remote server can be determined. We can also do this with some of the new socket extensions, but it still takes a lot more work than just using a pipe. Sockets, like named pipes, do nothing to validate whether the incoming data stream is well formed, and the same advice about making sure your parsers are well hardened applies.

RPC/COM

Remote procedure calls are an extremely robust form of interprocess communication. With RPC, you get it all–built-in authentication, the ability to identify and impersonate clients, built-in functionality to maintain privacy and integrity of network communications, and a rich way to define the data types being passed. All this and asynchronous capabilities too!

The catch is that RPC programming is difficult, and a specialty area. It’s also complex enough that you can make a lot of mistakes, security and otherwise, when using RPC. We spent 30 pages discussing RPC security issues in the second edition of Writing Secure Code. Not only is it difficult to program, but RPC is also going to have more overhead than any of the techniques mentioned so far. If all you need to do is something simple, RPC is tremendous overkill.

COM (or DCOM) is just a programmer-friendly wrapper over RPC, and you could quite easily create a COM server, perhaps with ATL, and then a COM client to talk to the service. You get all of the features of RPC with much less work on your part. The downside is that you get a lot more overhead, and you get to worry about random applications trying to instantiate your server. If you choose this approach, you might want to go back and review our RPC/DCOM chapter in the second edition of Writing Secure Code. DCOM has a number of security implications and decisions, and you may also need to worry about the client impersonating the server if you choose to use callbacks (also known as sources and sinks). Lastly, be sure you implement all of your interfaces exactly to specifications; if you don’t, an application that can load arbitrary COM objects may get into trouble if your component misbehaves.



Writing Secure Code for Windows Vista
Writing Secure Code for Windows Vista (Best Practices (Microsoft))
ISBN: 0735623937
EAN: 2147483647
Year: 2004
Pages: 122

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net