A NotQuiteNullDacl Class

Environment: VC6 -- WinNT4, Win2000, WinXP

Default security is not always good enough

Many of the Win32 API functions, specifically those that create objects such as files, mutexes, and pipes, require a pointer to a SECURITY_ATTRIBUTES structure. Most of us program for a long time before we ever have to code anything other than a NULL for that pointer. The NULL pointer makes the object uninheritable and indicates the created object should be assigned a default security descriptor. That default security is just the security associated with the creating process and that is sufficient for most purposes.

However, if you try to share an object created this way between, say, a Service and a regular App, or, now that we have XP, between two logon session on the same workstation where the logged on users have different access rights, the objects created with default security begin to fail you right and left.

Take the case of a mutex that you are trying to use to let a Service and an App be aware of each other's existence. Say the App is used to configure the Service. The App needs to work a little differently if the Service is running than if the Service is not. And the Service shouldn't start at all if the App is running, on the theory that its configuration parameters may be in an inconsistent state.

Traditionally each module uses a CreateMutex call to open a handle to a named mutex. If that call succeeds, each module then calls GetLastError to check for ERROR_ALREADY_EXISTS. If the App sees that it is not the original creator of the mutex, it knows the Service is running and modifies its behavior accordingly. If the Service sees that it is not the original creator of the mutex, it just terminates.

The usual code to do this is pretty straightforward. Each module has a section that looks something like:

HANDLE hMutex = CreateMutex( NULL, FALSE, "MyMutex" );
DWORD dwErr = GetLastError();

if ( hMutex != NULL )
{
   // the call succeeded
   // but is it a new mutex or an existing one?
   if ( dwErr != ERROR_ALREADY_EXISTS )
   {
      // we actually created the mutex
      // free to run in normal mode
   }
   else
   {
      // we got a handle to an existing mutex,
      // take appropriate action
   }
}
else
{
   // call to create the mutex failed
   // use the dwErr value to see why
}

This is entirely ordinary code and always works fine between two copies of an App or two different Apps running in the same login session. However, if you test it using a Service and an App, you see that, if the Service is already running, the App's call to CreateMutex fails with a GetLastError result of 5, access denied. A little digging in books or MSDN reveals the problem. The Service is running under the LocalSystem account, so when it creates the mutex with default security, it implicitly denies the App permission to open a handle to the mutex, since the App is running under a user logon.

The Null Dacl solution

This forces you to finally confront the documentation on the SECURITY_ATTRIBUTES structure and the all-important Security Descriptor it points to. After reading a bit and giving your eyes time to stop spinning, the most likely solution you'll hit upon is to build a SecurityDescriptor with a Null Discretionary Access Control List (Dacl). Using a Null Dacl allows anyone any access to the object created and certainly solves the problem at hand. And it is SIMPLE TO CODE; you can read about it, code it up, and test it in a mortal amount of time.

The code now looks like:

// declare and initialize a security attributes structure
SECURITY_ATTRIBUTES MutexAttributes;
ZeroMemory( &MutexAttributes, sizeof(MutexAttributes) );
MutexAttributes.nLength = sizeof( MutexAttributes );
MutexAttributes.bInheritHandle = FALSE; // object uninheritable

// declare and initialize a security descriptor
SECURITY_DESCRIPTOR SD;
BOOL bInitOk = InitializeSecurityDescriptor( &SD,
                         SECURITY_DESCRIPTOR_REVISION );
if ( bInitOk )
{
   // give the security descriptor a Null Dacl
   // done using the  "TRUE, (PACL)NULL" here
   BOOL bSetOk = SetSecurityDescriptorDacl( &SD,
                                            TRUE,
                                            (PACL)NULL,
                                            FALSE );
   if ( bSetOk )
   {
      // Make the security attributes point 
      // to the security descriptor
      MutexAttributes.lpSecurityDescriptor = &SD;

      // use the attributes with the Null Dacl to
      // create the mutex
      hMutex = CreateMutex( &MutexAttributes,
                            FALSE,
                            MutexName );
      dwMutexErr = GetLastError();

      ... same logic as before

   }
   else
      
}
else
   // Initialize failed, use GetLastError to find out why

The problem is that using the Null Dacl is bad practice. By allowing anyone any access, it conceivably allows some other process to hijack your object, change its accesses out from underneath you, and disable your application(s) or some part of the file system.

Sound far-fetched? Well, it probably is. And, despite Microsoft's dire warnings, which have gotten louder and more strident with its recent emphasis on secure programming, lots of good programmers have been using the Null Dacl approach for along time and the sky has not yet fallen.

Still, the danger of just coding up a Null Dacl for your little app-that-no-one-would-bother-to-hijack is that we all borrow code all the time, usually when we are in a hurry. The next time you need to solve this problem, perhaps for a more critical app, or the day the junior programmer down the hall needs an example of how to get around it, the code you write today will get propagated. And generations of code hence, it could just be the centerpiece of an embarrassing incident.

NotQuiteNull Dacl to the rescue

So, what we need is an equally easy to use but more secure Dacl, the NotQuiteNull Dacl. This Dacl explicitly grants all accesses to the Everyone group (which is essentially what the Null Dacl does) but filters that by first denying Everyone access to the WRITE_DAC and WRITE_OWNER permissions. This means that all users of the object except the original creator get all of the permissions they need to use the object and none of the permissions needed to hijack it.

There are examples of coding for this Dacl lying around, but they are not particularly easy to find. The 'Ask Dr. Gui #49' MSDN article has a pretty straightforward discussion and coding. The Richter/Clark Server-Side Applications book (see references) has a much more general discussion and coding in Chapter 10, pp 458-460, but the NotQuiteNullDacl code is pretty well buried in the example.

I've created a wrapper class, called NotQuiteNullDacl, to package the coding and make the better Dacl almost as easy to use as the Null Dacl. The full class is included in the sample code files. The code jumps through some hoops to create a SID for the Everyone group (m_pEveryoneSid) and to initialize the dacl (m_pDacl). But the critical code is:

BOOL bAddAce;
   
// Add an ace that denies WRITE_OWNER and WRITE_DAC to 
// 'everyone' which denies everyone but the owner of the
// associated object access to the object's security.

bAddAce = 
  AddAccessDeniedAce( m_pDACL,  // the acl to add the ace to
              ACL_REVISION,     // must be ACL_REVISION
              WRITE_OWNER | WRITE_DAC, // accesses to deny
              m_pEveryoneSid ); // SID to be denied (everyone)
if ( bAddAce )
{
   // Add an ace that gives 'everyone' all accesses 
   // to the object. By itself, the bit of code below
   // would be the moral equivalent of a NULL DACL -- 
   // it gives all rights to everyone.  But, because
   // accesses are evaluating in order of their placement
   // in the DACL the DeniedACE above acts as a filter, 
   // before this can be evaluated.
   bAddAce = 
     AddAccessAllowedAce( m_pDACL, // the acl to add the ace to
                          ACL_REVISION, // must be ACL_REVISION
                          GENERIC_ALL,  // accesses to allow
                          m_pEveryoneSid ); // SID to be allowed
   if ( bAddAce )
   {
      bAddedOk = true;
   }
}

if ( !bAddedOk )
{
   // use GetLastError() to find out why the add failed
}

Using the NotQuiteNullDacl class

Once you've included the class header, using the NotQuiteNullDacl is pretty straightforward. The setup for the CreateMutex call becomes:

// create the not-quite-null-dacl
NotQuiteNullDacl Dacl;
bool bDaclOk = Dacl.Create();

if ( bDaclOk)
{
   bool bSetupOk = false;

   // declare and initialize a security attributes structure
   SECURITY_ATTRIBUTES MutexAttributes;
   ZeroMemory( &MutexAttributes, sizeof(MutexAttributes) );
   MutexAttributes.nLength = sizeof( MutexAttributes );
   MutexAttributes.bInheritHandle = FALSE; // object uninheritable

   // declare and initialize a security descriptor
   SECURITY_DESCRIPTOR SD;
   BOOL bInitOk = InitializeSecurityDescriptor( &SD,
                           SECURITY_DESCRIPTOR_REVISION );
   if ( bInitOk )
   {
      // assign the dacl to it
      BOOL bSetOk = SetSecurityDescriptorDacl( &SD,
                                               TRUE,
                                               Dacl.GetPDACL(),
                                               FALSE );
      if ( bSetOk )
      {
         // Make the security attributes point to 
         //the security descriptor
         MutexAttributes.lpSecurityDescriptor = &SD;
         bSetupOk = true;
      }
   }

   if ( bSetupOk )
   {
      // use the security attributes to call CreateMutex
   }
   else
   {
      // the setup failed somehow, use GetLastError() 
      // to find out why
   }
}
else
{
   // nqnDacl create failed, 
   // use Dacl.GetErr() to find out how
}

Side effects? There are a few

Using the NotQuiteNullDacl is not exactly the same as using a Null Dacl. For example, we're used to using CreateMutex either to create a new mutex or to open a handle to an existing mutex. But CreateMutex implicitly asks for MUTEX_ALL_ACCESS on the handle to be opened. If the Mutex was created in one logon session, say by a Service, with the NotQuiteNullDacl, another process won't be able to get MUTEX_ALL_ACCESS since WRITE_DAC and WRITE_OWNER have been explicitly denied. The second process CAN use OpenMutex, asking for SYNCHRONIZE permission, to determine the existence of the mutex.

So now, rather than issuing a CreateMutex call and then having to check the error status when it returns a valid handle, which has always been a little jarring to me, you first try to open the mutex. If you can open it, you know it already exists and can act accordingly. If you can't open it, you can create it. The result is rather more readable code and a much higher level of security in the object created.

References:

  1. Ask Dr. Gui #49
  2. Programming Server-Side Applications for Microsoft Windows 2000, Richter/Clark, Microsoft Press 2000

Downloads

Download source - 3 Kb