Access Checks, part 2

Yesterday I discussed the format of an ACL.  For todays post, I want to talk about how the system uses ACLs to perform access checks.  Once again, the post on security terms is likely to be helpful.

There are two forms of access check accessable from the same API – an access check can either be for a specific set of access rights, or it can be for the well known right “MAXIMUM_ALLOWED” – a MAXIMUM_ALLOWED access check basically grants the user as many rights as they can have, and no more.  In general, asking for MAXIMUM_ALLOWED is not advised, instead you should ask for the rights you need and no more.

As I mentioned yesterday, Access Check takes three inputs: The user’s token, a desired access mask, and a security descriptor, and performs bitwise manipulation to determine a Boolean result: granted or not (the actual check is more complicated than that, but…).

In a nutshell, the AccessCheck logic is as follows:

Iterate through the ACEs in the ACL.
            If the SID in the ACE is active in the users token, then if the ACE is a grant ACE, turn off the bits in the desired access mask that correspond to the bits in the AccessMask field in the ACE.
            If the current desired access mask is 0, grant access.
            If the ACE is a deny ACE, then if the bits in the AccessMask are on in the desired access mask, then deny access.

One feature of this algorithm that merits calling out: The user is denied access by default – if you aren’t granted access to the resource by virtue of the SIDs active in your token, then you don’t have access to the resource.  Period, end of discussion.

The addition of restricted SIDs makes the access check process a check a smidge more complicated.  There are actually two checks performed, the first against the ACL using the normal SIDs in the users token.  If that succeeds, a second check is done on the restricted SIDs in the users token.  If either fails, access is denied.  In addition, there are two types of SIDs that can appear in the token – “normal” SIDs and “deny-only” SIDs – the deny-only SIDs will never grant access, but WILL deny access (in other words, the deny-only SIDs only apply for deny ACEs, not grant ACEs).  You can find the list of SIDs used in the AccessCheck process by calling GetTokenInformation asking for the TokenGroupsAndPrivileges information level.  The TOKEN_GROUPS_AND_PRIVILEGES structure contains the both lists of SIDs checked in AccessCheck.  The list of SIDs used for the first check is contained in the Sids structure field.  The list of restricted SIDs used for the second check is contained in the RestrictedSids structure field.

The following is a rough pseudo-code version for the AccessCheck API:

AccessCheck (desiredAccess, Token, SecurityDescriptor)

{

      //

      // Handle the implicit WRITE_DAC and READ_CONTROL access rights.

      //

      if (<SecurityDescriptor->Owner is active in Token>)

      {

            desiredAccess &= ~(WRITE_DAC | READ_CONTROL);

            grantedAccess |= (WRITE_DAC | READ_CONTROL);

      }

      //

      // Handle the ACCESS_SYSTEM_SECURITY meta access right.

      //

      if (desiredAccess & ACCESS_SYSTEM_SECURITY)

      {

            if (SE_SYSTEM_SECURITY_PRIVILEGE enabled in Token)

            {

                  desiredAccess &= ~ACCESS_SYSTEM_SECURITY;

                  grantedAccess |= ACCESS_SYSTEM_SECURITY;

            }

            else if (desiredAccess != MAXIMUM_ALLOWED)

                  return failure;

            }

      }

      //

      // Handle NULL DACL case.

      //

      if (SecurityDescriptor->Control & SE_DACL_PRESENT == 0)

            return success, desiredAccess;
//
// Empty DACL means no access.
//

      if (SecurityDescriptor->Dacl->AceCount == 0)

            return failure, grantedAccess;

      //

      // If we’ve granted all the desired accesses, access is allowed.

      //

      if (desiredAccess == 0)

            return success, grantedAccess;

 

      //

      // Handle MAXIMUM_ALLOWED meta-right

      //

      if (desiredAccess == MAXIMUM_ALLOWED)

      {

            result = <MAXIMUM_ALLOWED Access Check, with normal token SIDs>

            If (result == success && Token is restricted)

            {

                  result = <MAXIMUM_ALLOWED Access Check, with restricted token SIDs>

            }

return result;
}

      else // Handle “normal” access rights.

      {

            result = <Simple Access Check with normal token SIDs>

            If (result == success && Token is restricted)

            {

            result = <Simple Access Check with restricted token SIDs>

      }

      }

}

 The MAXIMUM_ALLOWED access check is (roughly):

for (i = 0; i< SecurityDescriptor->Dacl->AceCount ; i+=1)

{

      Ace = SecurityDescriptor->Dacl->Ace[i];

      if (<Ace->Sid is active in Token>)

      {

            if (Ace->AceType==ACCESS_ALLOWED_ACE)

            {

                  grantedAccess |= Ace->AccessMask;

            }

            else if (Ace->AceType==ACCESS_DENIED_ACE|| Ace->AceType==ACCESS_DENIED_OBJECT_ACE)

            {

                  deniedAccess |= Ace->AccessMask;

            }

      }

}

returnedAccess = grantedAccess | ~deniedAccess;

if (returnedAccess != 0)

      return success, returnedAccess;

else

      return failure, returnedAccess;

The “Normal” access check is (roughly):

for (i = 0; i< SecurityDescriptor->Dacl->AceCount ; i+=1)

{

      Ace = SecurityDescriptor->Dacl->Ace[i];

      if (<Ace->Sid is active in Token>)

      {

            if (Ace->AceType==ACCESS_ALLOWED_ACE)

            {

                  desiredAccess &= ~Ace->AccessMask;

                  grantedAccess |= (Ace->AccessMask & ~deniedAccess);

            }

            else if (Ace->AceType==ACCESS_DENIED_ACE||

                  Ace->AceType==ACCESS_DENIED_OBJECT_ACE)

            {

                  deniedAccess |= (Ace->AceMask & ~grantedAccess);

                  if (desiredAccess & Ace->AceMask)

                  {

                        return failure, desiredAccess;

                  }

            }

      }

      if (desiredAccess==0)

      {

            return success, grantedAccess|~deniedAccess;

      }

}

if (desiredAccess != 0)

{

      return failure, grantedAccess|~deniedAccess;

}

The big difference between the “normal” and the “maximum allowed” access check is that the normal access check has an early-out when all the desired accesses are granted, while the maximum allowed access check needs to iterate over all the ACEs to determine the full set of rights granted to the user.

Edit: Added recommendation against using MAXIMUM_ALLOWED.