Share via


Security Bits and Pieces

 

Ruediger R. Asche
Microsoft Developer Network Technology Group

May 9, 1995

Abstract

This article is third in a series of technical articles that describe the implementation and application of a C++ class hierarchy that encapsulates the Windows NT® security application programming interface (API). The series consists of the following articles:

"Windows NT Security in Theory and Practice" (introduction)

"The Guts of Security" (implementation of the security class hierarchy)

"Security Bits and Pieces" (architecture of the sample application suite)

"A Homegrown RPC Mechanism" (description of the remote communication implemented in the sample application suite)

CLIAPP/SRVAPP, a sample application suite that consists of a database client and server, illustrates the concepts introduced in this article series.

Introduction

One of the things I really like about C++ is that it lends itself naturally to modularization. The CLIAPP and SRVAPP sample applications (included with this article series) are very complex, but C++ makes it very easy to dissect the applications into little, independent, digestible pieces. Thus, this article describes the "glue" between the numerous components of the sample application suite, all of which are described elsewhere. The figure below illustrates the architecture of the sample applications.

CLIAPP/SRVAPP client/server architecture

The client and the server both have access to an area of shared memory in which a database resides. Both the client and the server can access the database through a common interface that supports the Insert, Remove, and Retrieve operations. In the current version of the applications, the database is homegrown; that is, no existing database engine is used. (There is nothing to prevent the application suite from working with an existing database interface; I might add this feature in a later version of this article.)

The client can access the database in two ways: locally (via shared memory) or remotely (via a network). In a real-life scenario, it is fairly unlikely for a database to reside in shared memory—it normally resides on the server and is accessed from the server through a network connection. The sample application suite provides this option through a very primitive remote procedure call (RPC) mechanism that is built upon a named pipe communication; the user can access this option using the Remote Access menu. The client can also access the database through the shared memory object; this option is available from the Local Access menu. The client and the server share a named mutex, which protects the database from corruption when a client in local mode accesses the database concurrently with the server.

Now, where does security come into the equation? If you look at the figure above, you will see that the database, the shared memory section, the named pipe, and the mutex are drawn in grey. The grey color indicates that these objects can be secured; that is, they are objects derived from CSecureableObject and can therefore be associated with security information.

Basically everything in the code is implemented as C++ classes, but we will not discuss all of the classes in this article. Check with the following articles or article series for additional information about the components:

  • The RPC mechanism is designed on top of the CNamedPipe and CProtocol classes, which are described in the articles "Communication with Class" and "Garden Hoses at Work" in the MSDN Library. The RPC mechanism itself is described in the article "A Homegrown RPC Mechanism."
  • The security base class hierarchy CSecureableObject and its derivatives are described in the articles "Windows NT Security in Theory and Practice" and "The Guts of Security."

In this article, we will discuss the remaining components of the application suite:

  • The CEasyOutputView class, which displays system output in a view window. I know, I already used this class in the YAHU sample application, but this time I made the class a universal base class.
  • The ChainedQueue database class, which I use to demonstrate privately secured objects.
  • Small C++ wrappers for the Win32® kernel objects file mappings and mutexes.
  • The program structure that links the RPC mechanism to the user interface.

The CEasyOutputView Class

One of the most frequent gripes I hear about the Microsoft® Foundation classes (MFC) from Microsoft developers is: "Actually, I prefer console applications because my application doesn't need an elaborate user interface. I just need some very simple, command-based interaction between the software and the user, and scanf and printf do a much better job for me than dialog boxes and all the hassle I have to go through to output a single line of text."

I agree. Input is not so much of a problem: If the command set that your application supports is small, you can restrict your input to menus and dialog boxes, which can be coded easily with MFC. However, outputting information involves more work, especially if you simply want to display some text (such as diagnostic messages).

The CLIAPP and SRVAPP applications only need to display diagnostic messages indicating success or failure of system calls. There is no need to waste any more code than absolutely necessary for this rudimentary output.

Thus, I used the intermediate class CEasyOutputView, which is roughly a list box equivalent of the CEditView class. A CEasyOutputView object is a derivative of CView, which has a control embedded in its client area. (In the CEditView class, this control is an edit control; in CEasyOutputView, it is a list box.)

To use the class, simply derive a view class from CEasyOutputView, and call the member functions AddStringandAdjust (to add a string to the list box) and ClearWindow (to reset the list box). The CEasyOutputView class takes care of the rest (for example, it resizes the control when the view window is resized, and keeps track of the scroll logic).

The ChainedQueue Database Class

Actually, we don't have much to discuss in this section, because ChainedQueue is merely a demonstration class that will eventually be replaced by a "real" database engine. The interesting code in this class pertains to security. Let us first look at the class declaration for ChainedQueue (from DBCODE.H):

class ChainedQueue
{ 
 private: 
   COMPLETECHAINLIST *cpBase;
   CMutex *m_hMutex;
 public:
   int m_iErrorCode;
 public:
   ChainedQueue(void *pSharedMemory,int iLength,CMutex *hM);
   ChainedQueue(); // This is the "anonymous" constructor.
   ~ChainedQueue();
   BOOL Retrieve(int iIndex,CHAINLIST *cpResult);
   BOOL Remove(int iIndex);
   BOOL Insert(int *iIndex,CHAINLIST *cpElement);
   int inline GetEntries() { return cpBase[0].cl.iInsecuredElement; };
};

#ifdef SERVER

class ServerChainedQueue : public ChainedQueue, public CPrivateSecObject
{
 private:
   CSecuredNamedPipe *m_cpPipe;
 public:
   ServerChainedQueue(void *pSharedMemory,int iLength,CMutex *hM,CSecuredNamedPipe *hP);
   BOOL SafeRetrieve(int iIndex,CHAINLIST *cpResult);
   BOOL SafeRemove(int iIndex);
   BOOL SafeInsert(int *iIndex,CHAINLIST *cpElement);
   BOOL SetTheDescriptor(void);
};

The server side of the object is derived from CPrivateSecObject, so we can use the AddRightsTo, RevokePreviouslyGrantedAccess, AddSecurity, and MatchAccessRequest functions to control security (see "The Guts of Security" for information on these member functions). Being a derivative of CSecureableObject, the ServerChainedQueue class also needs to implement the SetTheDescriptor and GetTheDescriptor member functions.

The interesting members of ChainedQueue are the member functions that work on the database. Database manipulations from the server side call the Retrieve, Remove, and Insert functions, and so do manipulations from the client side when the shared database (local access) is used. In this article, I will not discuss how Retrieve, Remove, and Insert work; let's just assume that they do the "right thing." It doesn't really make a difference how the database engine is implemented as long as we can rely on the correct behavior of database operations. The only thing you should know about Retrieve, Remove, and Insert is that they return TRUE when they succeed and FALSE when they fail, leaving an error code in the m_iErrorCode member variable for the caller to inspect.

When a client uses remote access to manipulate a server's database, the server's RPC layer (which is discussed in "A Homegrown RPC Mechanism") translates a request to retrieve, remove, or add a record into one of the member functions SafeRetrieve, SafeRemove, or SafeInsert. These functions do the security-related work. Let us first look at SafeRetrieve:

BOOL ChainedQueue::SafeRetrieve(int iIndex,CHAINLIST *cpResult)
{
 return Retrieve(iIndex,cpResult);
};

Hmmm, this does not look too complicated, and it certainly isn't more sophisticated than Retrieve. How exactly does security come into play here?

Not at all, because I decided not to protect the retrieve operation at this point. The application suite protects write access to the database—that is, removing and adding records. Let us look at SafeInsert to see what happens. (SafeRemove works exactly the same way, except that it calls Remove instead of Insert.)

BOOL ChainedQueue::SafeInsert(int *iIndex, CHAINLIST *cpParam)
{
 HANDLE hThread;
 m_cpPipe->ImpersonateClient(TRUE);
 BOOL bReturn;
 hThread = GetCurrentThread();
 bReturn = MatchAccessRequest(DBASE_WRITE,hThread);
 if (!bReturn)
  m_iErrorCode = ERROR_ACCESS_DENIED;
 else 
  bReturn=Insert(iIndex,cpParam);
 m_cpPipe->ImpersonateClient(FALSE);
 return bReturn;
};

First, the current thread assumes the identity of the client via the ImpersonateClient member function of the named pipe. This call temporarily associates the current thread with the access token of the named pipe client so that a later call to AccessCheck can be made against the token of the client.

This particular technique for obtaining a client's access token seems somewhat awkward. Why can't the server simply retrieve the client's access token without assuming the client's identity?

Remember that you need an access token only to check security on privately secured objects. Whenever you make an implicit security check (for example, when your application tries an operation on a secured kernel object), the system service that is called (for example, CreateFile) obtains the access token. Application code does not have any control over the implicit security code, so a call to obtain a client's access token without impersonating the client would not be of any help for kernel objects.

In other words, a server application that assumes the identity of the client can use the same code to try to access privately secured and system-level secured objects, whereas a server application that simply retrieves the client's access token for privately secured objects would still have to assume the client's identity when it tries to access kernel objects on the client's behalf.

Once the client is impersonated, we can call CPrivateSecObject::MatchAccessRequest (as discussed in "The Guts of Security") to verify that the requested operation (in this case, DBASE_WRITE) is allowed. If not, the code sets the error code to ACCESS_DENIED and returns FALSE. This is the strategy for enforcing private security.

If the access request is granted, the code simply dispatches to the "real" code that does the work (in this case, Insert). In either case, the previous client impersonation is revoked after the completion of the code discussed above.

Thus, SafeInsert in effect validates the request against the object's security information before it satisfies the request.

Miscellaneous Classes

I should introduce two more classes—CFileMapping and CMutex—before we dig into the program structure. I wrote these classes as thin wrappers for the system-provided file mapping and mutex data structures. The sample application suite needs these structures only to implement local database access (that is, sharing the physical memory in which the database resides between the server and the client).

The only reason I provide these data structures as C++ objects is that both can be derived from CKernelSecObject and thus receive optional protection. There is no mystery whatsoever in CFileMapping and CMutex; their respective member function sets are basically equivalent to the system-provided operations on the corresponding kernel objects.

Program Structure

Both the client and the server are plain-vanilla MFC single-document interface (SDI) applications generated by ClassWizard. Because all the relevant code resides in the respective view classes, you can easily extend the server to service several clients at the same time. For example, you can convert the server into a multiple-document interface (MDI) application that services a client in each window.

Client Side

Let us look at the client first. As I mentioned earlier, all the relevant code for the client resides in the view class, so that is the only class (files CLIVIEW.CPP and CLIVIEW.H) we need to look at.

ClassWizard generates almost all of the custom member functions in the view class. These functions implement command handlers and menu item update handlers for menu items. The only two member functions of CSecCliView that ClassWizard did not generate are ObtainErrorFromCode and DisplayTextErrorMessage. ObtainErrorFromCode is basically a wrapper for the Win32 FormatMessage function. Given an error code returned from a Win32 function, ObtainErrorFromCode returns a string that contains the description of the error. DisplayTextErrorMessage accepts an error code and a string that contains the format element %s. The error code is converted into a string using ObtainErrorFromCode, and the returned string replaces the format element %s, which is then displayed in the view's window using AddStringandAdjust. Because the view's primary function is to display error codes to the user, DisplayTextErrorMessage comes in handy for presenting an error condition to the user in a readable form.

The interesting member functions of the CSecCliView class are the ones that ClassWizard generated for us to process user input. The three command handlers that provide access to the shared database—OnLocalaccessAddarecord, OnLocalaccessRemovearecord, and OnLocalaccessViewcontents—very simply query the user for more information on the . operation and dispatch the request to the m_cqQueue database object, which was created when OnLocalaccessOpenshareddatabase was processed.

When the user selects Connect from the Remote Access menu, the OnConnect member function asks the user for the machine name and tries to open the client end of a named pipe. If that operation is successful, the code creates a new database protocol object that is assigned to the member variable m_cpDB. At this point, the commands in the Remote Access menu become available, and the OnRemoteaccessAddarecord, OnRemoteaccessRemovearecord, and OnRemoteaccessViewcontents member functions are called to implement the remote operations. When you look at these functions, you will notice that they all dispatch the request to the database protocol object and simply display the respective error codes to the user.

I have not yet revealed what a database protocol object is. For the time being, let us simply say that it is a magic black box that knows how to communicate requests for database operations over a network. The object basically defines an interface for the server and the client to submit requests (this applies to the client) or to accept and process requests (this applies to the server) to manipulate the database. Note that the database protocol object does not define or contain the database itself. The database is contained in an object of type ChainedQueue; the database protocol object only provides a communication channel between the client and the server.

The technical article "A Homegrown RPC Mechanism" in the MSDN Library discusses this subject in more detail.

Server Side

The server is slightly more complicated than the client, because we are now dealing with multithreading. The code that communicates with the client resides in a secondary thread so that the user interface can keep accepting commands while a communication executes in the background. The multithreaded approach can make it rather easy to serve multiple communications in individual threads. Let us follow what happens when the user of the server application clicks Wait to Connect (code from SRVVIEW.CPP):

void CSecSrvView::OnWaittoconnect() 
{ DWORD id;
  AddStringandAdjust("waiting to connect...");
  m_hThread = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)PipeThreadFunction,this,
             CREATE_SUSPENDED,&id);
  SetThreadPriority(m_hThread,THREAD_PRIORITY_BELOW_NORMAL);
  ResumeThread(m_hThread);
              
}

This is fairly straightforward: A secondary thread is created in the suspended state, the priority of the thread is slightly reduced, and the thread is resumed. I hope you will ask me why I lower the priority of the thread. I do this because there are three types of named pipes: synchronous blocking pipes, asynchronous non-blocking pipes, and overlapped asynchronous pipes (see the technical article "Garden Hoses at Work" in the MSDN Library for details on these variations). As we will see in a second, the thread function calls the CNamedPipe member function AwaitCommunicationAttempt, which uses polling to establish a connection in one variation of named pipes (namely, the non-blocking asynchronous pipe). To prevent this polling from eating up too many CPU cycles, I lower the base priority of the thread a little bit.

Note   From the three types of named pipes listed above, note that I chose non-blocking pipes for this application. Overlapped pipes are the most sophisticated type of pipe. However, because the client side of the application suite must run on all Win32 platforms, overlapped pipes (which are available only on Windows NT) are too restrictive. Synchronous blocking pipes, on the other hand, pose an interesting problem: The AwaitCommunicationAttempt member function is implemented using a "fake" connection from the server to itself (see the technical article "Garden Hoses at Work"). However, this "fake" connection may be refused for security reasons if the owner of the named pipe does not grant itself rights on the named pipe. To avoid confusion here, I chose non-blocking types, which do not pose this problem.

What happens in the thread function? Let's see (again from SRVVIEW.CPP):

long WINAPI PipeThreadFunction(CSecSrvView *cvTarget) 
{ char szDiagnosticMessage[255];
  CDatabaseProtocol *cpProt;
  BOOL bFinished;
  ChainedQueue *cqTheQueue = cvTarget->m_cqQueue;

First, we wait until a client connects. The experienced programmer of multithreaded applications will probably raise an eyebrow, because this code contains an implied call to the AddStringandAdjust member function of the view from a secondary thread. What if the primary thread also attempts to output something to the view at the same time? Wouldn't we run into synchronization problems here? Shouldn't there be a critical section that is claimed before, and released after, the AddStringandAdjust call?

Yes, but this needs to happen for a different reason than what you thought. Concurrent drawing to the list box is a safe operation because AddStringandAdjust calls the AddString member of the CListBox class, which is an MFC inline function that translates into a SendMessage call. The USER component of the Win32 subsystem implicitly serializes SendMessage between critical sections, so no synchronization is necessary for the text output.

However, AddStringandAdjust also recomputes the new vertical scroll range using the inline member function CEasyOutputView::ComputeNewHorizontalExtent, and we may run into trouble if two threads update the m_iLongestStringSizeInPixels member variable incorrectly. That is why CEasyOutputView::ComputeNewHorizontalExtent wraps what it does in a critical section. Note that when we use the CEasyOutputView class in a single-threaded application (such as the client application), keeping the multithread-aware code in the base class doesn't do anything but introduce a slight inefficiency into the code. So should we assume multithreading and make the base class code safe, or let the derived classes or the application add the safeguarding code?

Design decisions. . . I don't have a one-size-fits-all answer, and the decision probably depends on the application. In this case, I figured that the overhead introduced by the critical section calls in a single-threaded application would be bearable, and would simplify the code overall.

Note that if the user of the server application chooses to cancel the wait attempt, the AwaitCommunicationAttempt function will fail, causing the thread to terminate:

  if (!cvTarget->m_cpPipe->AwaitCommunicationAttempt())
  { 
  cvTarget->DisplayTextErrorMessage("Open named pipe failed -- %s",
                                    cvTarget->m_cpPipe->m_iErrorCode);
  goto ErrorExit;
  }

Now we have a working communication with a client. The next thing we will do is create and open a protocol object (see the article "A Homegrown RPC Mechanism" for details).

  else
  {
  sprintf(szDiagnosticMessage,"Open named pipe succeeded");
  cvTarget->AddStringandAdjust(szDiagnosticMessage);
  }
  cpProt = new CDatabaseProtocol(cvTarget->m_cpPipe);
  if (!cpProt->Open("",CFile::modeRead|CFile::modeWrite)) // We are server...
  // Log an error here.
    goto ErrorExit;
  int iCommand,iIndex;
  CHAINLIST cpElement;
  bFinished=FALSE;

Here we enter a loop in which a remote command is read, interpreted, and translated into a local database call. As I describe in the article "A Homegrown RPC Mechanism," the AcceptCommand member of the CDatabaseProtocol class returns a command identifier which can be CMD_ADDRECORD, CMD_DELETERECORD, CMD_RETRIEVERECORD, CMD_GETENTRIES, or CMD_EXIT. The first four commands are dispatched to the database, whereas the last command lets the code break out of the loop.

In this code, we use the SafeRemove, SafeRetrieve, and SafeInsert member functions, which we discussed earlier. This is where the security checks take place.

  while (!bFinished)  // We'll break out of this loop later...
  {
   if (!cpProt->AcceptCommand(&iCommand,&cpElement,&iIndex))
   { 
     cvTarget->DisplayTextErrorMessage("Accepting command from named pipe
                                       failed -- %s",cpProt->m_iErrorCode);
     bFinished = TRUE;
     continue;
   };
   switch (iCommand)
   { case CMD_EXIT:
          cvTarget->AddStringandAdjust("Client terminated connection!");
          bFinished=TRUE;
        break;
    case CMD_GETENTRIES:
         cpProt->Acknowledge(cqTheQueue->GetEntries());
         break;
    case CMD_ADDRECORD:
         if (!cqTheQueue->SafeInsert(&iIndex,&cpElement))
         {
            cvTarget->DisplayTextErrorMessage("Remote insert failed; propagating 
                                              error code -- %s",
                                              cqTheQueue->m_iErrorCode);
            cpProt->Fail(cqTheQueue->m_iErrorCode);
         }
         else
         {
          cpProt->Acknowledge(iIndex);
         cvTarget->AddStringandAdjust("Remote insert succeeded!");
         };
         break;
    case CMD_DELETERECORD:
        if (!cqTheQueue->SafeRemove(iIndex))
         { 
            cvTarget->DisplayTextErrorMessage("Remote remove failed; propagating 
                                              error code -- %s",
                                              cqTheQueue->m_iErrorCode);
            cpProt->Fail(cqTheQueue->m_iErrorCode);
         }
         else
         {
          cpProt->Acknowledge(0);
          cvTarget->AddStringandAdjust("Remote remove succeeded!");
         };
         break;
    case CMD_RETRIEVERECORD:
        if (!cqTheQueue->SafeRetrieve(iIndex,&cpElement))
         {
             cvTarget->DisplayTextErrorMessage("Remote retrieve failed; 
                                               propagating error code -- %s",
                                               cqTheQueue->m_iErrorCode);
             cpProt->Fail(cqTheQueue->m_iErrorCode);
         }
         else
         { 
           cpProt->Acknowledge(cpElement.iInsecuredElement);
          cpProt->Acknowledge(cpElement.iSecuredElement);
          cvTarget->AddStringandAdjust("Remote retrieve succeeded!");
            };
            break;
      }; // switch
    };   // while

The code breaking out of the while loop indicates that the connection has terminated. Now we can clean up and return.

 cpProt->Close();
 delete (cpProt);
 cvTarget->m_cpPipe->CloseInstance();
 ErrorExit:
 CloseHandle(cvTarget->m_hThread);
 cvTarget->m_bThreadIsActive = FALSE;
 return 0;
};   // Thread fn

And that is all I have to say about the server view class.

Summary

Discussing application code in MFC is generally an easy task, because a well-coded application is only a shell that glues other classes together and provides a user interface for the user to interact with the other classes. In the case of the sample client and server application suite associated with this article series, the interesting work is done in C++ classes that implement an RPC mechanism, a small database, and security.

Although the sample application suite is mainly intended as a starting point for your own secure servers, you could modify it in various ways to make it more generic and sophisticated. For example, you could replace the homegrown RPC classes with the built-in RPC mechanism in Windows NT, or you could replace the database interface with an OLE-based database interface. Another way to extend the scope of the application would be to rewrite the server so that multiple clients can work on the database concurrently.