Programming for Access Control

[Previous] [Next]

In this section, we will discuss how to programmatically manipulate security in Windows. We'll begin with an overview of the steps involved.

Basic Steps for Security Tasks

Modifying the security of a securable object is largely the same no matter what type of object you are dealing with. You usually perform one of two tasks: create a new object with security, or change the security of an existing object.

Here are the basic steps that you will take to create an object with security:

  1. Compile a list of SIDs for which you will be creating denied and allowed ACEs.
  2. Create and initialize a security descriptor.
  3. Create and initialize a DACL large enough to hold the required ACEs.
  4. Add ACEs to the DACL.
  5. Add the DACL to the security descriptor.
  6. Create the object using the new security descriptor.
  7. Clean up after yourself.

Only step 6 is different from one securable object type to the next.

To modify the security of an existing object, follow these steps:

  1. Compile a list of SIDs for which you will be adding denied and allowed ACEs to the object's DACL.
  2. Retrieve the DACL of the object.
  3. Check the existing DACL for ACEs that you wish to remove, and remove them.
  4. Check the existing DACL for ACEs that you are adding so that you can avoid adding them again and creating unnecessary bulk.
  5. Create a new DACL large enough to accommodate the modified "old" DACL in addition to the new ACEs.
  6. Copy the old ACEs and add new ACEs to the "new" DACL.
  7. Set the DACL to the object.
  8. Clean up after yourself.

In this process, only step 2 (in which you retrieve a DACL) and step 7 (in which you apply the DACL) differ from one securable object type to the next.

Don't let yourself be overwhelmed by this process. I will discuss each step in detail and describe alternatives for a few of these steps. The purpose of mentioning the process here is to show the commonality of approach for any securable object in Windows.

As you can see, once you are comfortable modifying the security of one object, you have the skills (and perhaps even the code) that you need to modify the security of any object. The differences in your approach to modification are mainly in the getting and the setting of security. If you know which function to use for the type of object you are concerned with, you are all set. Take a look at Table 10-7.

Table 10-7. Specific security functions for securable objects

Securable Object Creation Function Getting and Setting
Security Descriptor
Access tokens

Desktops

DuplicateTokenEx

CreateDesktop

GetSecurityInfo, SetSecurityInfo
Directories
(on an NTFS file system)

Event objects

Files (on an NTFS file system)

File-mapping objects

Job objects

Mutex objects

Network share objects

CreateDirectory
CreateDirectoryEx

CreateEvent

CreateFile

CreateFileMapping

CreateJobObject

CreateMutex

NetShareAdd

GetNamedSecurityInfo,
GetSecurityInfo,
SetNamedSecurityInfo,
SetSecurityInfo
Pipes, anonymous

Pipes, named

CreatePipe

CreateNamedPipe

GetSecurityInfo, SetSecurityInfo
Printers AddPrinter GetNamedSecurityInfo,
GetSecurityInfo,
SetNamedSecurityInfo,
SetSecurityInfo
Process objects CreateProcess,
CreateProcessAsUser
GetSecurityInfo, SetSecurityInfo
Registry keys

Semaphore objects

RegCreateKeyEx

CreateSemaphore

GetNamedSecurityInfo,
GetSecurityInfo,
SetNamedSecurityInfo,
SetSecurityInfo
Service Control Manager [cannot create the Service Control Manager] QueryServiceObjectSecurity,
SetServiceObjectSecurity
Services [cannot specify security when creating services] GetNamedSecurityInfo,
GetSecurityInfo,
SetNamedSecurityInfo,
SetSecurityInfo
Thread objects CreateThread,
CreateRemoteThread
GetSecurityInfo,
SetSecurityInfo
Waitable timer objects CreateWaitableTimer GetNamedSecurityInfo,
GetSecurityInfo,
SetNamedSecurityInfo,
SetSecurityInfo
Window stations CreateWindowStation GetSecurityInfo,
SetSecurityInfo
Private objects GetPrivateObjectSecurity,
SetPrivateObjectSecurity,
SetPrivateObjectSecurityEx

Notice in Table 10-7 that although the functions for creating an object differ for each object, only a handful of functions are required for setting and retrieving security information for all securable object types in the system.

NOTE
There are other functions for getting and setting security for specific object types, such as GetFileSecurity and SetKernelObjectSecurity. However, object-specific functions are no longer the preferred method for retrieving and setting security for objects, because functions such as GetSecurityInfo and SetNamedSecurityInfo are easier to use and offer a more complete implementation of the inheritance model in Windows 2000. You should always use these functions when possible.

Reading Security Information for an Object

Only reading an object's security information is not a common task, and typically you do it so you can modify the security in some way. Understanding how to read an object's security, however, will greatly simplify the more common task of modifying object security, and so I'll cover the topic here.

As you might imagine, not everyone can read the security of an object. Like any other task performed on a securable object, the ability to read the security information itself is securable. To read the security information, one or both of the following conditions must be true:

  1. You are the owner of the object.
  2. An ACE in the object's DACL grants you, or a group to which you are a member, the standard right READ_CONTROL. (See Table 10-13.)

If neither of these conditions is true, you do not have the right to read an object's security. If either is true, when you obtain a handle to the object, you can ask for READ_CONTROL access in the access required parameter of the function used to acquire a handle, and the system will give you a handle. The following code fragment shows an example of obtaining a handle for reading the security information of a file:

 HANDLE hFile = CreateFile(TEXT("C:\\Test\\Test.txt"), READ_CONTROL, 0,     NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if ((hFile == INVALID_HANDLE_VALUE) &&    GetLastError() == ERROR_ACCESS_DENIED){    // You do not have READ_CONTROL access to the file } 

If the file C:\Test\Test.txt exists and resides on an NTFS drive, this code fragment will work. If you meet one of the two conditions, this code will give you a valid file handle that you can use to read the security of your object.

Now that you have a handle to a securable object such as a file, let's use that handle to retrieve security information about the object by making a call to the GetSecurityInfo function:

 DWORD GetSecurityInfo(    HANDLE               hHandle,    SE_OBJECT_TYPE       objType,    SECURITY_INFORMATION secInfo,    PSID                 *ppsidOwner,    PSID                 *ppsidGroup,    PACL                 *ppDACL,    PACL                 *ppSACL,    PSECURITY_DESCRIPTOR *ppSecurityDescriptor); 

NOTE
As you might recall from Table 10-7, the GetSecurityInfo function is used to retrieve security information for the majority of securable objects in Windows. It is a very flexible and useful function indeed.

When using GetSecurityInfo, you pass a handle to an object, which you have opened with READ_CONTROL access as the hHandle parameter. The objType parameter is an enumerated type that indicates the type of securable object that the handle represents. Table 10-1 shows different enumerated values that can be used with this function. For example, if you passed the handle of a file as the hHandle parameter of GetSecurityInfo, you would have to pass the enumerated value SE_FILE_OBJECT as the objType parameter.

The secInfo parameter indicates what information in the object's security descriptor you would like the system to return. Remember that an object's security descriptor maintains an owner, a group, a DACL, and a SACL, and you can use GetSecurityInfo to retrieve any or all of these by passing any combination of the values in Table 10-8.

Table 10-8. SECURITY_INFORMATION values that can be passed for GetSecurityInfo's secInfo parameter

Value Description
DACL_SECURITY_INFORMATION Indicates that you wish to retrieve DACL information for a securable object
SACL_SECURITY_INFORMATION Indicates that you wish to retrieve SACL information for a securable object
OWNER_SECURITY_INFORMATION Indicates that you wish to retrieve owner information for a securable object
GROUP_SECURITY_INFORMATION Indicates that you wish to retrieve group information for a securable object

The ppsidOwner, ppsidGroup, ppDACL, and ppSACL parameters are optional. If you want the system to return a pointer to the owner's SID, group SID, DACL, or SACL of the object, you can pass the address of a PSID or PACL variable for any or all of these parameters. However, you must pass NULL for any information that you do not request by passing the proper flag as the secInfo parameter. Regardless, you can always pass NULL for any or all of these parameters.

The final parameter is the address of a pointer to a SECURITY_DESCRIPTOR structure and is mandatory. The system allocates a buffer large enough to hold the requested security information for the object using LocalAlloc and places a pointer to the security descriptor in the variable whose address you supply in ppSecurityDescriptor. (It is necessary to pass the value returned in the ppSecurityDescriptor parameter to LocalFree when you are finished with the security descriptor for the object.)

NOTE
If you should pass the address of a PSID or PACL variable as the ppsidOwner, ppsidGroup, ppDACL, or ppSACL parameter, the system will return an address that points to a portion of the security descriptor returned in ppSecurityDescriptor. This is why these parameters are optional.

You don't have to call GetLastError after calling GetSecurityInfo, because GetSecurityInfo returns an error code directly. If GetSecurityInfo succeeds, it returns ERROR_SUCCESS. Common error values are ERROR_ ACCESS_DENIED, which indicates that your handle was not opened with READ_CONTROL access, and ERROR_INVALID_HANDLE, which usually indicates that you passed mismatched handle and object type values for the hHandle and objType parameters.

The following code could be used with the preceding code fragment (in the beginning of "Reading Security Information for an Object"), which opens a handle to a file using CreateFile, to retrieve DACL and object owner information for the file at C:\Test\Test.txt:

 PSECURITY_DESCRIPTOR pSD; PSID  pSID; PACL  pDACL;  ULONG lErr = GetSecurityInfo(hFile, SE_FILE_OBJECT,     DACL_SECURITY_INFORMATION, &pSID, NULL, &pDACL, NULL, &pSD); if (lErr != ERROR_SUCCESS){    // Error case } // Perform work here // Clean up LocalFree(pSD); CloseHandle(hFile); 

In this example, after successfully calling GetSecurityInfo, the pSID and pDACL variables point to a SID structure and a DACL structure contained in the buffer returned in the pSD variable. It is neither necessary nor correct to pass the pointers returned in pSD or pDACL to LocalFree, because the entire security descriptor is freed when the pointer returned in the pSD variable is passed to LocalFree.

You are familiar with SIDs and pointers to SIDs from Chapter 9, and you can use the owner SID returned from GetSecurityInfo with functions such as LookupAccountSid and CopySid, which are discussed in that chapter.

Before dissecting the DACL of a secured object, I would like to introduce GetSecurityInfo's sister function, GetNamedSecurityInfo. Whereas GetSecurityInfo requires a handle to a securable object, GetNamedSecurityInfo requires the textual name of a secure object in the system, and is defined as follows:

 DWORD GetNamedSecurityInfo(    LPTSTR               pObjectName,    SE_OBJECT_TYPE       objType,    SECURITY_INFORMATION secInfo,    PSID                 *ppsidOwner,    PSID                 *ppsidGroup,    PACL                 *ppDACL,    PACL                 *ppSACL,    PSECURITY_DESCRIPTOR *ppSecurityDescriptor); 

Notice that this function differs from GetSecurityInfo only in the first parameter, which takes a pointer to a string containing the name of an object in the system. Fewer secured objects in the system are named. However, for those that are, this function can sometimes be more convenient than GetSecurityInfo, because it does not require you to first obtain a handle to an object. For a list of the objects whose security can be retrieved using GetNamedSecurityInfo, see Table 10-7.

Our previous two code fragments could be consolidated to create this smaller code fragment by using GetNamedSecurityInfo:

 PSECURITY_DESCRIPTOR pSD; PSID pSID; PACL pDACL;  ULONG lErr = GetNamedSecurityInfo(TEXT("C:\\Test\\Test.txt"),     SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, &pSID, NULL,     &pDACL, NULL, &pSD); if (lErr != ERROR_SUCCESS){    // Error case } // Clean up LocalFree(pSD); 

NOTE
In the examples I have shown so far, I have used the file at C:\Test\Test.txt to demonstrate how to retrieve an object's security information. However, both GetNamedSecurityInfo and GetSecurityInfo can be used to retrieve security for other securable objects including registry keys, kernel objects, and user objects. See Table 10-7 for specific security functions for securable objects.

GetSecurityInfo and GetNamedSecurityInfo allow you to pass the address of pointer variables to retrieve the address of the owner SID, group SID, DACL, or SACL in the returned security descriptor. However, you might be wondering how you would retrieve this information if you had only retrieved the security descriptor of an object. You can use the GetSecurityDescriptorOwner, GetSecurityDescriptorGroup, GetSecurityDescriptorDacl, and GetSecurityDescriptorSacl functions to retrieve pointers to these data structures within a security descriptor. I will discuss these functions further in the section titled "Securing Private Objects" later in this chapter.

Dissecting the DACL

Now that we have discussed how to retrieve a pointer to the DACL of a secured object, it is time to begin uncovering how the DACL is read. Remember that the DACL is an access control list (ACL), and an ACL is little more than a list of access control entries (ACEs) that indicate access that is denied or allowed to a trustee.

The DACL for a security descriptor can be set to NULL. This indicates that no DACL is present for a particular object, and is referred to as a "null DACL." If an object has a null DACL, all access is implicitly granted to all trustees of the system.

NOTE
When a DACL is present for an object, all access to the object is implicitly denied unless explicitly allowed. Sometimes an object will have an empty DACL, which should not be confused with a null DACL. An empty DACL signifies no access for anyone (but the owner) for an object. A null DACL, on the other hand, grants all access to all trustees.

If a DACL is present, retrieving information about the ACEs in the DACL might be necessary. First you must find out how many ACEs the DACL contains by calling GetAclInformation:

 BOOL GetAclInformation(    PACL                  pACL,    PVOID                 pACLInformation,    DWORD                 dwACLInformationLength,    ACL_INFORMATION_CLASS aclInformationClass);  

This function allows you to retrieve information about a DACL (or a SACL), which you pass to the function via the pACL parameter. You should pass the address of a structure to retrieve ACL information as the pACLInformation parameter, and the length of the structure as the dwACLInformationLength parameter. The aclInformationClass parameter is an enumerated type that indicates what type of information is being returned and also defines which type of structure you should pass as the pACLInformation parameter. Possible values for the aclInformationClass parameter are listed in Table 10-9.

Table 10-9. The ACL_INFORMATION_CLASS enumeration

Enumerated Value Structure Definition
AclRevisionInformation ACL_REVISION_INFORMATION Returns ACL revision information
AclSizeInformation ACL_SIZE_INFORMATION Returns ACL size information including ACE count

The ACL_SIZE_INFORMATION structure is most commonly used with GetAclInformation and is defined as follows:

 typedef struct _ACL_SIZE_INFORMATION {     DWORD AceCount;     DWORD AclBytesInUse;     DWORD AclBytesFree;  } ACL_SIZE_INFORMATION; 

The following code fragment builds on the last fragment to retrieve the number of ACEs in the DACL for the file at C:\Test\Text.txt:

 PSECURITY_DESCRIPTOR pSD; PACL pDACL;  ULONG lErr = GetNamedSecurityInfo(TEXT("C:\\Test\\Test.txt"),     SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL,     &pDACL, NULL, &pSD); if (lErr != ERROR_SUCCESS){    // Error case } ACL_SIZE_INFORMATION aclSize = {0}; if(pDACL != NULL){       if(!GetAclInformation(pDACL, &aclSize, sizeof(aclSize),       AclSizeInformation)){       // Error case    } } ULONG nAceCount = aclSize.AceCount; 

NOTE
If the file at C:\Test\Test.txt is on a FAT drive, or if it does not have a DACL, the system returns NULL in the pDACL variable. This is why it is important to test for NULL before calling GetAclInformation. Passing a NULL pointer value to GetAclInformation causes an access violation.

After you have the number of ACEs in a DACL, you can use this information to retrieve individual ACEs. Calling GetAce does this.

 BOOL GetAce(    PACL  pACL,    DWORD dwACEIndex,    PVOID *pACE); 

This function simply takes the pointer to an ACL, the zero-based index of the ACE you want to retrieve, and the address of a pointer variable in which to store the pointer to the ACE in the DACL.

Notice that the pACE parameter is of type PVOID. This is because a number of different ACE types can reside in a DACL, and each of these types is represented by a different structure. GetAce is used to retrieve each type. However, ACLs can contain any number of ACEs and any combination of ACE types, so there is no way to know which ACEs will be of which type until after you have retrieved the ACE with GetAce.

Although retrieving the ACE count and the ACE itself is simple, the real work in reading a DACL comes in understanding the different types of ACEs.

Understanding ACEs

There are a number of different ACE types, which can be divided into two broad types: standard ACEs and object ACEs. In each group you will find ACE types for allowing and denying access, as well as for auditing access. Each ACE type is guaranteed to share one attribute in common—the first member of its structure will be of type ACE_HEADER, defined as follows:

 typedef struct _ACE_HEADER {     BYTE   AceType;     BYTE   AceFlags;     USHORT AceSize;  } ACE_HEADER; 

The ACE header is key to reading the DACL, because when you retrieve a pointer to an ACE from GetAce, you do not know what type of ACE it is or what type of structure makes up the ACE until you have read the ACE's header.

As you might have guessed, the AceType member of the ACE header indicates what type of ACE the header represents. The possible values for AceType are listed in Table 10-10. These six ACE types are currently available in Windows 2000.

Table 10-10. ACE types

ACE Type Description
ACCESS_ALLOWED_ACE_TYPE Used in a DACL. Indicates a set of access rights that are explicitly granted to a trustee. Uses the ACCESS_ ALLOWED_ACE structure.
ACCESS_DENIED_ACE_TYPE Used in a DACL. Indicates a set of access rights that are explicitly denied to a trustee. Uses the ACCESS_DENIED_ACE structure.
SYSTEM_AUDIT_ACE_TYPE Used in a SACL (see "Auditing and the SACL"). Indicates a set of access rights that should cause an audit event when requested by a trustee. Uses the SYSTEM_AUDIT_ACE structure.
ACCESS_ALLOWED_OBJECT_ACE_TYPE Used in a DACL of a directory services object. Indicates a set of access rights that are explicitly granted to a trustee for an object in Active Directory. Uses the ACCESS_ALLOWED_OBJECT_ACE structure.
ACCESS_DENIED_OBJECT_ACE_TYPE Used in a DACL of a directory services object. Indicates a set of access rights that are explicitly denied to a trustee for an object in Active Directory. Uses the ACCESS_DENIED_OBJECT_ACE structure.
SYSTEM_AUDIT_OBJECT_ACE_TYPE Used in a SACL (see "Auditing and the SACL") of a directory services object. Indicates a set of access rights that should cause an audit event when requested by a trustee. Uses the SYSTEM_AUDIT_OBJECT_ACE structure.

The AceFlags member of the ACE_HEADER structure contains inheritance information about the ACE, as well as auditing information. The AceFlags member will be some combination of the values in Table 10-11.

Table 10-11. Values for the AceFlags member of the ACE_HEADER structure

Value Description
Inheritance Flags
INHERITED_ACE This flag is set in the ACE if the flag was inherited from a parent object. This allows the system to distinguish between ACEs that were directly applied and ACEs that were automatically applied due to inheritance.
CONTAINER_INHERIT_ACE Container objects that are children of the object owning this ACE will inherit this ACE as an effective ACE.

This inheritance continues indefinitely by default; however, inheritance can be configured to stop with direct children if the NO_PROPAGATE_INHERIT_ACE flag is also set. If this bit is not set, but the OBJECT_INHERIT_ACE bit is set, the ACE will inherit to container objects, but the INHERIT_ONLY_ACE flag will be set for the container's ACE.

OBJECT_INHERIT_ACE This ACE is inherited to and effective on non-container and child objects of the owning object. For child objects that are containers, the ACE is inherited as an inherit-only ACE unless the CONTAINER_INHERIT_ACE flag is also set.

If the NO_PROPAGATE_INHERIT_ACE flag is set, and the CONTAINER_INHERIT_ACE flag is not set, an ACE with the OBJECT_INHERIT_ACE bit set will not inherit to container objects.

INHERIT_ONLY_ACE This ACE is not effective on the object owning the ACE. However, it might be effective on child container or child non-container objects. It is invalid for an ACE to have the INHERIT_ONLY_ACE flag set unless one or both of the CONTAINER_INHERIT_ACE and OBJECT_INHERIT_ACE flags are set.
NO_PROPAGATE_INHERIT_ACE Setting this flag in an ACE causes it to be inherited only once. The inherited ACEs will have their OBJECT_INHERIT_ACE and CONTAINER_INHERIT_ACE flags cleared so that the new ACEs will not also be inherited.
Auditing Flags
FAILED_ACCESS_ACE_FLAG In a SACL, an ACE with this bit set will cause an audit event if access is requested and the result is ACCESS_DENIED.
SUCCESSFUL_ACCESS_ACE_FLAG Used with system-audit ACEs in a SACL to generate audit messages for successful access attempts.

In addition to the ACE type and flags, the AceSize member of the ACE_HEADER structure indicates the size of the ACE in question. As you can see, the ACE_HEADER contains a fair amount of information about the ACE. In fact, if you wanted to retrieve only inheritance and type information about an ACE, when calling GetAce, you could look no farther than the ACE_HEADER structure. The following code fragment shows how to call GetAce to retrieve the ACE's type:

 ACE_HEADER* pACEHeader ; GetAce(pDACL, 0, (PVOID*)&pACEHeader); switch( pACEHeader->AceType ){    // Do work depending on the ACE's type } 

Since you will typically require more information from an ACE than just its type and inheritance style, you will have to go beyond the ACE_HEADER. There are two main types of ACEs, standard and object. Standard ACEs are ACEs that are not object ACEs. Object ACEs are used only with Active Directory. They are known as object ACEs because they contain GUIDs that make it possible for them to work with the many types of objects available with directory services. Because standard ACEs are far and away the most common, we will cover them first.

Standard ACEs Most securable objects in the system deal exclusively with standard ACEs. The exceptions are objects that exist in Active Directory, which are referred to as directory services objects.

Like all ACEs, the standard ACEs are represented by three structures, one each for allowing access, denying access, and auditing. Here is the definition of each of these structures:

 typedef struct _ACCESS_DENIED_ACE {     ACE_HEADER  Header;     ACCESS_MASK Mask;     DWORD       SidStart;  } ACCESS_DENIED_ACE; typedef struct _ACCESS_ALLOWED_ACE {    ACE_HEADER  Header;     ACCESS_MASK Mask;     DWORD       SidStart;  } ACCESS_ALLOWED_ACE; typedef struct _SYSTEM_AUDIT_ACE {    ACE_HEADER  Header;     ACCESS_MASK Mask;     DWORD       SidStart;  } SYSTEM_AUDIT_ACE; 

The definition of each of these structures is identical, and each includes the promised Header member, which is the now-familiar type ACE_HEADER.

The remaining two members are relatively easy to understand. The first is Mask, which is a 32-bit mask indicating the access rights that are being denied, allowed, or set for auditing.

I will discuss access rights in great detail shortly, but for now remember that the access rights are broken up into generic, standard, and specific rights for a given object type, and are combined to form a single 32-bit value. This 32-bit value is the Mask member of an ACE. See Figure 10-2 for the access mask format.

The last member of the ACE structure is SidStart, which indicates the beginning of the SID of the trustee account for which the ACE is denying, allowing, or auditing access for the object. This member requires some discussion.

Remember that a SID (discussed in Chapter 9) is a variable-length binary structure indicating a trustee account of the system. Because each ACE contains a SID, the ACE structure is also a variable-length structure. The SidStart member is a placeholder for the beginning of the SID contained within the ACE. The value (and the type) of SidStart is irrelevant and should never be accessed, because it is actually the first couple of bytes of a SID structure.

You might find that a macro such as the following is useful in extracting a SID from an ACE structure:

 #define PSIDFromPACE(pACE) ((PSID)(&((pACE)->SidStart))) 

This particular macro takes a pointer to any ACE structure—object or standard—and returns a pointer to the SID structure for the ACE.

The standard ACE type is broken into three separate structures, which are used primarily as logical placeholders for you when you create ACEs for an ACL. However, because the structure of each ACE is identical, when reading ACEs from a DACL, it is very common to write code that uses only one of the ACE types but uses the AceType member of the ACE_HEADER structure to maintain a type for the ACE. The following function uses this technique and prints information about each ACE in a DACL:

 void DumpACL( PACL pACL ){    __try{       if (pACL == NULL){          _tprintf(TEXT("NULL DACL\n"));          __leave;       }       ACL_SIZE_INFORMATION aclSize = {0};       if (!GetAclInformation(pACL, &aclSize, sizeof(aclSize),          AclSizeInformation))          __leave;       _tprintf(TEXT("ACL ACE count: %d\n"), aclSize.AceCount);              struct{          BYTE  lACEType;          PTSTR pszTypeName;       }aceTypes[6] = {          {ACCESS_ALLOWED_ACE_TYPE, TEXT("ACCESS_ALLOWED_ACE_TYPE")},          {ACCESS_DENIED_ACE_TYPE, TEXT("ACCESS_DENIED_ACE_TYPE")},          {SYSTEM_AUDIT_ACE_TYPE, TEXT("SYSTEM_AUDIT_ACE_TYPE")},          {ACCESS_ALLOWED_OBJECT_ACE_TYPE,             TEXT("ACCESS_ALLOWED_OBJECT_ACE_TYPE")},          {ACCESS_DENIED_OBJECT_ACE_TYPE,             TEXT("ACCESS_DENIED_OBJECT_ACE_TYPE")},          {SYSTEM_AUDIT_OBJECT_ACE_TYPE,             TEXT("SYSTEM_AUDIT_OBJECT_ACE_TYPE")}};       struct{          ULONG lACEFlag;          PTSTR pszFlagName;       }aceFlags[7] = {          {INHERITED_ACE, TEXT("INHERITED_ACE")},          {CONTAINER_INHERIT_ACE, TEXT("CONTAINER_INHERIT_ACE")},          {OBJECT_INHERIT_ACE, TEXT("OBJECT_INHERIT_ACE")},          {INHERIT_ONLY_ACE, TEXT("INHERIT_ONLY_ACE")},          {NO_PROPAGATE_INHERIT_ACE, TEXT("NO_PROPAGATE_INHERIT_ACE")},          {FAILED_ACCESS_ACE_FLAG, TEXT("FAILED_ACCESS_ACE_FLAG")},          {SUCCESSFUL_ACCESS_ACE_FLAG,              TEXT("SUCCESSFUL_ACCESS_ACE_FLAG")}};       for (ULONG lIndex = 0;lIndex < aclSize.AceCount;lIndex++){          ACCESS_ALLOWED_ACE* pACE;          if (!GetAce(pACL, lIndex, (PVOID*)&pACE))             __leave;          _tprintf(TEXT("\nACE #%d\n"), lIndex);                    ULONG lIndex2 = 6;          PTSTR pszString = TEXT("Unknown ACE Type");          while (lIndex2--){             if(pACE->Header.AceType == aceTypes[lIndex2].lACEType){                pszString = aceTypes[lIndex2].pszTypeName;             }          }          _tprintf(TEXT("  ACE Type =\n  \t%s\n"), pszString);          _tprintf(TEXT("  ACE Flags = \n"));          lIndex2 = 7;          while (lIndex2--){             if ((pACE->Header.AceFlags & aceFlags[lIndex2].lACEFlag)                 != 0)                _tprintf(TEXT("  \t%s\n"),                    aceFlags[lIndex2].pszFlagName);          }          _tprintf(TEXT("  ACE Mask (32->0) =\n  \t"));          lIndex2 = (ULONG)1<<31;          while (lIndex2){             _tprintf(((pACE->Mask & lIndex2) != 0)?TEXT("1"):TEXT("0"));             lIndex2>>=1;          }          TCHAR szName[1024];          TCHAR szDom[1024];          PSID pSID = PSIDFromPACE(pACE);          SID_NAME_USE sidUse;                   ULONG lLen1 = 1024, lLen2 = 1024;          if (!LookupAccountSid(NULL, pSID,              szName, &lLen1, szDom, &lLen2, &sidUse))             lstrcpy(szName, TEXT("Unknown"));          PTSTR pszSID;          if (!ConvertSidToStringSid(pSID, &pszSID))             __leave;          _tprintf(TEXT("\n  ACE SID =\n  \t%s (%s)\n"), pszSID, szName);          LocalFree(pszSID);       }    }__finally{} } 

Notice for all ACE types of the ACEs that are dumped the use of the ACCESS_ALLOWED_ACE structure. This structure will work for any standard ACE type (although this function will fail for object ACEs) because all standard ACE structures are the same.

The following code fragment could be used with the DumpACL function to show the ACEs in a directory on an NTFS share:

 PSECURITY_DESCRIPTOR pSD; PACL pDACL;  ULONG lErr = GetNamedSecurityInfo(TEXT("C:\\Test\\Test"),     SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL,     &pDACL, NULL, &pSD); if (lErr == ERROR_SUCCESS){    DumpACL(pDACL); } 

NOTE
The DumpACL sample function demonstrates how to read ACE information in a DACL. You might find code like this useful in your own production code. You might also find the DumpACL function useful as a debugging tool, particularly when you are beginning to write code that modifies the DACL of securable objects. Use it to dump out the DACL of the object before and after you make your modifications to see whether your code is producing the desired results.

Techniques such as this one have advantages over viewing security by using the Security Properties page in Explorer, because the system's user interface does not display the ACE information of an object verbatim. In fact, the user interface will not display a DACL that is not properly ordered without first ordering the ACEs in the DACL.

Because of the nature of the ACE structures, you might find it helpful to define a union that represents each ACE type. Then you can deal with ACEs returned from GetAce in terms of a pointer to the union type. The following code shows an example of such a technique:

 typedef union _ACE_UNION{    ACE_HEADER         aceHeader;    ACCESS_ALLOWED_ACE aceAllowed;    ACCESS_DENIED_ACE  aceDenied;    SYSTEM_AUDIT_ACE   aceAudit; }*PACE_UNION; PACE_UNION pACE ; GetAce(pDACL, 0, (PVOID*)&pACE); switch (pACE->aceHeader.AceType){  

Object ACEs Unless you are writing code to secure and modify the security of objects in Active Directory, you are not likely to use object ACEs in your own applications. However, Active Directory is an important component of Windows 2000, and there is no harm in understanding how it secures objects. We won't spend much time on the topic, though, to avoid clouding your understanding of standard ACEs.

Like standard ACEs, object ACEs come in three flavors: one to deny, one to allow, and one to audit access rights for a trustee. Here are the structures for the object ACEs:

 typedef struct _ACCESS_ALLOWED_OBJECT_ACE {    ACE_HEADER  Header;    ACCESS_MASK Mask;    DWORD       Flags;    GUID        ObjectType;    GUID        InheritedObjectType;    DWORD       SidStart; } ACCESS_ALLOWED_OBJECT_ACE, *PACCESS_ALLOWED_OBJECT_ACE; typedef struct _ACCESS_DENIED_OBJECT_ACE {    ACE_HEADER  Header;    ACCESS_MASK Mask;    DWORD       Flags;    GUID        ObjectType;    GUID        InheritedObjectType;    DWORD       SidStart; } ACCESS_DENIED_OBJECT_ACE, *PACCESS_DENIED_OBJECT_ACE; typedef struct _SYSTEM_AUDIT_OBJECT_ACE {    ACE_HEADER  Header;    ACCESS_MASK Mask;    DWORD       Flags;    GUID        ObjectType;    GUID        InheritedObjectType;    DWORD       SidStart; } SYSTEM_AUDIT_OBJECT_ACE, *PSYSTEM_AUDIT_OBJECT_ACE; 

Notice that like standard ACEs, each structure type is the same and the members of each structure are the same except for the addition of the Flags, ObjectType, and InheritedObjectType members.

Take care not to confuse the Flags member of the object ACE structures with the AceFlags member of the ACE_HEADER structure. The Flags member can be 0 or any combination of the values in Table 10-12.

NOTE
Unlike similar structures in the Win32 SDK, the flags in Table 10-12 do not indicate whether the ObjectType and InheritedObjectType members are used in the structure, but rather whether these members exist in the structure at all!

Structures that do not have a fixed member list are referred to as amorphous. Each of the object ACE structures is amorphous, which is very uncommon for a structure in the Win32 SDK. This is by far the most difficult aspect of dealing with object ACEs.

Table 10-12. Object ACE Flags member values

Value Description
ACE_OBJECT_TYPE_PRESENT Indicates that the ObjectType member is present in the object ACE structure
ACE_INHERITED_OBJECT_TYPE_PRESENT Indicates that the InheritedObjectType member is present in the object ACE structure

Both ObjectType and InheritedObjectType are GUIDs. If you are unfamiliar with GUIDs, you can find a complete discussion of the topic in the Platform SDK documentation or Inside COM (Dale Rogerson, Microsoft Press, 1997). For our purposes, you need to understand only that a GUID is a 128-bit value that can be used to uniquely identify just about anything.

Fortunately, the Active Directory Service Interfaces (ADSI) provide interfaces for manipulating security on Active Directory objects that remove from your code the responsibility of dealing with object ACEs. For this reason, it is highly unlikely that you will have to write code that directly manipulates object ACEs in a DACL. For more information on this topic, see the Platform SDK discussion on the IADsSecurityDescriptor, IADsAccessControlList, and IADsAccessControlEntry interfaces.

NOTE
Object ACEs are never found in the ACLs of securable objects outside of Active Directory, and they significantly complicate ACL manipulation code. For this reason the remainder of this chapter (and all the code in this chapter) assumes ACLs do not contain object ACEs. ACLs without object ACEs ease my job as a writer and simplify your job of understanding a complicated topic.

Access Rights

Access rights might at first seem like a simple concept to grasp, but some key points might not be obvious. The best way to understand the nuances of access rights is to discuss where they are used and to examine each group. First let's review what we already know:

  • A trustee must hold a combination of access rights before the system will perform any securable task on a securable object on behalf of that trustee.
  • All access rights are packed into a 32-bit access mask. (See Figure 10-2.)
  • Access rights are divided into three groups: generic rights (bits 31-28), standard rights (bits 22-16), and specific rights (bits 15-0).
  • Two "odd" bits in an access mask do not fit into an access right group: the auditing bit and the maximum allowed bit.

Up to this point, I have discussed access rights in terms of the access mask stored in an ACE, which is only one side of the transaction. I said that rights are necessary to perform tasks, but I have not specifically stated how you ask the system for a right. Asking for a right is the other side of the transaction.

You typically ask for a right when you ask the system for a handle to an object. Most functions that return handles to securable objects have a required access parameter to which you pass a value indicating how you intend to use the handle. Look at the following example:

 HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, TEXT("MyEvent")); 

You are probably so familiar with code like this that it never occurred to you that you were asking the system for a specific access right to the event object, but you are. The flag bits that you pass in the desired access parameter of OpenEvent (and other similar functions) are the same bits that are stored in the ACEs that secure the object. In fact, the system performs an access check before returning the handle to the event.

NOTE
Many secured objects in Windows are accessed through handles. With these objects, security is checked when the handle is requested, and your rights are stored with the handle. This means that once you have received a handle to an object, the access rights on the object itself might change, but the access provided to you by your handle remains the same until the handle is closed.

Some secure actions are performed on objects for which you never see a handle. A perfect example of this is every call to GetNamedSecurityInfo. The convenience of this function is that you do not have to first retrieve a handle to an object. In this case, the system internally performs an access check before performing any securable activity.

Now that you are familiar with both the ACE side and the access request side of access rights, let's discuss the types of access rights in more detail. First, we need to get the "odd" bits in the access mask out of the way.

The auditing bit represents the access right ACCESS_SYSTEM_SECURITY, which must be held by a trustee before that user can modify the SACL on an object. (I will discuss auditing in more detail later in this chapter.)

The maximum allowed bit represents the "non-access right" MAXIMUM_ALLOWED. I call it a non-access right because it will never be found in an ACE stored in a DACL. It is used only in calls to functions requesting access. For example, the following code fragment uses the MAXIMUM_ALLOWED flag in retrieving a handle to a named event object:

 HANDLE hEvent = OpenEvent(MAXIMUM_ALLOWED, FALSE, TEXT("MyEvent")); 

In this case, the system performs an access check on the object, and rather than allowing or denying the access check, it returns an access mask that is the maximum rights awarded to you for the object. This might seem like a very convenient feature (and in certain situations it can be), as it allows you to always pass MAXIMUM_ALLOWED when requesting a handle rather than forcing you to figure out exactly which rights you need for an object. However, there is a very good reason not to do this.

When you obtain a handle to an object, typically you are being told by the system that you are allowed to use the object in a certain way. If you are denied a request for a handle, this typically means that you are not allowed to do whatever you have requested, and your software can take the appropriate measures.

If you pass MAXIMUM_ALLOWED each time you need access to an object, your code might work well for a long time. But the first time your code encounters an object for which you are not awarded sufficient access, your software might not catch the error until long after it has retrieved a handle to the object. The failing function might not even return ERROR_ACCESS_DENIED. It is entirely possible that a function will return another error such as ERROR_INVALID_PARAMETER, because you passed a handle to it with insufficient access to an object. Code with this type of problem can be drastically harder to debug.

Standard access rights Five standard rights are defined by the system. In addition to these, another five "composite" access rights are defined by the system that map to some combination of the five standard rights.

Not all standard rights are relevant for all securable objects in the system, but each standard right is special in that its meaning does not change from object to object. For example, not all objects can be deleted, so the standard right DELETE has no meaning for certain objects. However, for objects that can be deleted, the standard right DELETE carries the same meaning from one object type to the next: the holder of this right can delete the object. Table 10-13 lists all the standard and composite rights.

Table 10-13. Standard and composite rights

Right Description
Standard Rights
DELETE
(bit 16)
Delete access.
READ_CONTROL
(bit 17)
Read access to the security information of a securable object, including the owner SID, the group SID, the DACL, and the security descriptor revision and control. It does not include the access required to read the SACL of a secured object.
WRITE_DAC
(bit 18)
Write access to the DACL and group SID of a secured object. WRITE_DAC access is implicitly granted to the owner of an object.
WRITE_OWNER
(bit 19)
Write access to the owner SID of a secured object. Unless you also hold the SE_BACKUP_NAME privilege, you can write a SID that only denotes your user SID or one of your group SIDs to the owner SID of an object.
SYNCHRONIZE
(bit 20)
Synchronize access can be thought of as the right to wait on an object. Such an object can be a synchronization object (for example, an event or mutex), or it can be one of the other waitable kernel objects (for example, a file or a process handle).
Standard Right Composites
STANDARD_RIGHTS_READ
STANDARD_RIGHTS_WRITE
STANDARD_RIGHTS_EXECUTE
Maps to READ_CONTROL access.
STANDARD_RIGHTS_REQUIRED Maps to bits 16-19; or to DELETE, READ_ CONTROL, WRITE_DAC, and WRITE_OWNER. This access right is typically included with the "all access" defines for specific objects such as EVENT_ALL_ACCESS or FILE_ALL_ACCESS.
STANDARD_RIGHTS_ALL Defined as 0x001F0000, this access right includes all the standard rights.

As Table 10-13 shows, the standard rights include some powerful rights. For example, if you hold WRITE_OWNER access to an object, but are denied all other access, you might not be able to do much, as is. But if you choose to use this access to set the owner SID of the object to your SID, you have implicit WRITE_DAC access. You can then use this access to modify the DACL of the object, allowing yourself all the access you want to this object.

Specific rights Specific rights inhabit bits 15-0 of the access mask, and therefore each securable object in the system can have 16 different access rights defined for it.

Because the specific rights differ from object to object, the greatest challenge in working with specific rights is finding a comprehensive list of all specific rights available for a given object. You should look for specific rights in two places: the Platform SDK documentation and the Platform SDK header files. You should check the function that gives you a handle to an existing object of the type you are interested in. For example, if you were interested in finding the specific rights available for a registry key, you might check the Platform SDK documentation for the RegOpenKeyEx function, where you would find a description of rights such as KEY_READ, KEY_SET_VALUE, and KEY_ALL_ACCESS. After you have the names of a couple of access rights for an object, you can search the Platform SDK's include directory for a header file that defines one or more of the specific rights that you know, and you will find the remaining access rights for that object.

Table 10-14 lists the specific rights, as of this writing, for the most common securable objects in Windows.

SPECIFIC_RIGHTS_ALL is defined as 0x0000FFFF, which includes all 16 of the bits, reserved for specific rights in an access mask. Object Type Specific Right

Table 10-14. Specific rights for common securable objects

Object Type Specific Right
File
(WinNT.h)
FILE_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SYNCHRONIZE |
    0x1FF)
    (0x1FF includes all currently defined standard rights for files.)
FILE_READ_DATA
FILE_WRITE_DATA
FILE_APPEND_DATA
FILE_READ_EA
FILE_WRITE_EA
FILE_EXECUTE
FILE_READ_ATTRIBUTES
FILE_WRITE_ATTRIBUTES
Directory
(WinNT.h)
FILE_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SYNCHRONIZE |
    0x1FF)
    (0x1FF includes all currently defined standard rights for files.)
FILE_LIST_DIRECTORY
FILE_ADD_FILE
FILE_ADD_SUBDIRECTORY
FILE_READ_EA
FILE_WRITE_EA
FILE_TRAVERSE
FILE_DELETE_CHILD
FILE_READ_ATTRIBUTES
FILE_WRITE_ATTRIBUTES
Service
(WinSvc.h)
SERVICE_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SERVICE_QUERY_CONFIG |
    SERVICE_CHANGE_CONFIG |
    SERVICE_QUERY_STATUS |
    SERVICE_ENUMERATE_DEPENDENTS |
    SERVICE_START |
    SERVICE_STOP |
    SERVICE_PAUSE_CONTINUE |
    SERVICE_INTERROGATE |
    SERVICE_USER_DEFINED_CONTROL)
SERVICE_CHANGE_CONFIG
SERVICE_ENUMERATE_DEPENDENTS
SERVICE_INTERROGATE
SERVICE_PAUSE_CONTINUE
SERVICE_QUERY_CONFIG
SERVICE_QUERY_STATUS
SERVICE_START
SERVICE_STOP
SERVICE_USER_DEFINED_CONTROL
Printer
(WinSpool.h)
SERVER_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SERVER_ACCESS_ADMINISTER |
    SERVER_ACCESS_ENUMERATE)
PRINTER_ALL_ACCESS
     (STANDARD_RIGHTS_REQUIRED |
    PRINTER_ACCESS_ADMINISTER |
    PRINTER_ACCESS_USE)
JOB_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    JOB_ACCESS_ADMINISTER)
SERVER_ACCESS_ADMINISTER
SERVER_ACCESS_ENUMERATE
PRINTER_ACCESS_ADMINISTER
PRINTER_ACCESS_USE
JOB_ACCESS_ADMINISTER
Registry key
(WinNT.h)
KEY_ALL_ACCESS
    (STANDARD_RIGHTS_ALL |
    KEY_QUERY_VALUE |
    KEY_SET_VALUE |
    KEY_CREATE_SUB_KEY |
    KEY_ENUMERATE_SUB_KEYS |
    KEY_NOTIFY |
    KEY_CREATE_LINK)
KEY_QUERY_VALUE
KEY_SET_VALUE
KEY_CREATE_SUB_KEY
KEY_ENUMERATE_SUB_KEYS
KEY_NOTIFY
KEY_CREATE_LINK
Share object
(LMShare.h)
PERM_FILE_READ
PERM_FILE_WRITE
PERM_FILE_CREATE
Process
(WinNT.h)
PROCESS_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SYNCHRONIZE |
    0xFFF)
    (0xFFF includes all currently defined standard rights for processes.)
PROCESS_TERMINATE
PROCESS_CREATE_THREAD
PROCESS_SET_SESSIONID
PROCESS_VM_OPERATION
PROCESS_VM_READ
PROCESS_VM_WRITE
PROCESS_DUP_HANDLE
PROCESS_CREATE_PROCESS
PROCESS_SET_QUOTA
PROCESS_SET_INFORMATION
PROCESS_QUERY_INFORMATION
Thread
(WinNT.h)
THREAD_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SYNCHRONIZE |
    0x3FF)
    (0x3FF includes all currently defined standard rights for threads.)
THREAD_TERMINATE
THREAD_SUSPEND_RESUME
THREAD_GET_CONTEXT
THREAD_SET_CONTEXT
THREAD_SET_INFORMATION
THREAD_QUERY_INFORMATION
THREAD_SET_THREAD_TOKEN
THREAD_IMPERSONATE
THREAD_DIRECT_IMPERSONATION
Job
(WinNT.h)
JOB_OBJECT_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SYNCHRONIZE |
    0x1F)
    (0x1F includes all currently defined standard rights for job objects.)
JOB_OBJECT_ASSIGN_PROCESS
JOB_OBJECT_SET_ATTRIBUTES
JOB_OBJECT_QUERY
JOB_OBJECT_TERMINATE
JOB_OBJECT_SET_SECURITY_ATTRIBUTES
Semaphore
(WinNT.h)
SEMAPHORE_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SYNCHRONIZE |
    0x3)
    (0x3 includes all currently defined standard rights for semaphores.)
SEMAPHORE_MODIFY_STATE
MUTANT_QUERY_STATE
Event
(WinNT.h)
EVENT_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SYNCHRONIZE |
    0x3)
    (0x3 includes all currently defined standard rights for events.)
EVENT_MODIFY_STATE
MUTANT_QUERY_STATE
Mutex
(WinBase.h)
MUTEX_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SYNCHRONIZE |
    0x3)
    (0x3 includes all currently defined standard rights for mutexes.)
MUTEX_MODIFY_STATE
MUTANT_QUERY_STATE
File map object
(WinBase.h)
FILE_MAP_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    FILE_MAP_COPY |
    FILE_MAP_WRITE |
     FILE_MAP_READ |
    SECTION_MAP_EXECUTE
    SECTION_EXTEND_SIZE)
FILE_MAP_WRITE
FILE_MAP_READ
FILE_MAP_COPY
SECTION_EXTEND_SIZE
Waitable timer
(WinNT.h)
TIMER_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SYNCHRONIZE |
    TIMER_QUERY_STATE |
    TIMER_MODIFY_STATE)
TIMER_QUERY_STATE
TIMER_MODIFY_STATE
Token
(WinNT.h)
TOKEN_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    TOKEN_ASSIGN_PRIMARY |
    TOKEN_DUPLICATE |
    TOKEN_IMPERSONATE |
    TOKEN_QUERY |
    TOKEN_QUERY_SOURCE |
    TOKEN_ADJUST_PRIVILEGES |
    TOKEN_ADJUST_GROUPS |
    TOKEN_ADJUST_SESSIONID |
    TOKEN_ADJUST_DEFAULT)
TOKEN_ASSIGN_PRIMARY
TOKEN_DUPLICATE
TOKEN_IMPERSONATE
TOKEN_QUERY
TOKEN_QUERY_SOURCE
TOKEN_ADJUST_PRIVILEGES
TOKEN_ADJUST_GROUPS
TOKEN_ADJUST_DEFAULT
TOKEN_ADJUST_SESSIONID
Pipe
(WinNT.h)
FILE_ALL_ACCESS
    (STANDARD_RIGHTS_REQUIRED |
    SYNCHRONIZE |
     0x1FF)
    (0x1FF includes all currently defined standard rights for files.)
FILE_READ_DATA
FILE_WRITE_DATA
FILE_CREATE_PIPE_INSTANCE
FILE_READ_ATTRIBUTES
FILE_WRITE_ATTRIBUTES
Window station
(WinUser.h)
WINSTA_ACCESSCLIPBOARD
WINSTA_ACCESSGLOBALATOMS
WINSTA_CREATEDESKTOP
WINSTA_ENUMDESKTOPS
WINSTA_ENUMERATE
WINSTA_EXITWINDOWS
WINSTA_READATTRIBUTES
WINSTA_READSCREEN
WINSTA_WRITEATTRIBUTES
Desktop
(WinUser.h)
DESKTOP_CREATEMENU
DESKTOP_CREATEWINDOW
DESKTOP_ENUMERATE
DESKTOP_HOOKCONTROL
DESKTOP_JOURNALPLAYBACK
DESKTOP_JOURNALRECORD
DESKTOP_READOBJECTS
DESKTOP_SWITCHDESKTOP
DESKTOP_WRITEOBJECTS

With these specific rights and the standard rights discussed earlier, you have all the rights required to secure an object. However, there is still the issue of generic rights.

Generic rights and default security Previously in this chapter I mentioned that default security is assigned to securable objects when the software creating the object does not explicitly set the security for the object. This assignment is typically characterized by passing NULL for the security attributes parameter of a function that creates a securable object such as a file or an event. Before we can discuss generic rights, you must understand how default security is implemented.

Remember that your executing code is associated with an internal structure known as a token. Up to this point, I have said that the token contains your identifying SID and group SIDs, as well as a list of privileges assigned to you. In addition to this information, every token also stores a DACL that is used for creating objects with default security, known as the default DACL, and can be set by your code. (This and many other topics regarding tokens are covered in detail in the next chapter.)

For the moment, you need to be aware that a DACL exists (which you can modify and set) and that it can be applied to objects created with default security. The important issue to note is that the DACL can be applied to any type of object. So you can see that this creates a problem for the system.

NOTE
Neither the DACL nor the standard ACE structures maintain information about the object type that they are protecting. This complicates the creation of ACEs for a default DACL, since specific rights for one type of object might not be appropriate if applied to another type of object. The creators of Windows solved this problem by creating generic rights.

Generic rights are intended for use in the default DACL for a token. The system defines the generic rights GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE, and GENERIC_ALL. When a default DACL is applied to an object, the system maps any generic rights found in the DACL's ACEs to a combination of standard and specific rights appropriate for that object. In this way, an ACE that will appropriately affect the security on any object that uses that ACE can be added to the default DACL.

NOTE
You cannot directly assign a DACL to an object that contains ACEs whose access masks include generic rights. The system automatically clears these rights from the ACE before setting the security on the object.

NOTE
You can specify generic rights when requesting access to an object, but you should never do this. It is more appropriate to request the standard and specific rights that will meet the needs of your code.

Knowing what you know about generic rights, you might be wondering how you can find out what generic rights map to for a specific object in the system. This is a good question, and the only good answer is to create a default DACL with an ACE that includes a single generic right (such as GENERIC_READ), and then create an object and inspect the resulting ACE. (See Chapter 11.) I did this to produce Table 10-15.

Table 10-15. Generic mappings for common securable objects

Object Type Generic Right Standard and Specific Mapped Rights
File
GENERIC_READ
GENERIC_WRITE
GENERIC_EXECUTE
GENERIC_ALL
FILE_GENERIC_READ
FILE_GENERIC_WRITE
FILE_GENERIC_EXECUTE
FILE_ALL_ACCESS
Directory
GENERIC_READ STANDARD_RIGHTS_READ |
FILE_LIST_DIRECTORY |
FILE_ADD_FILE
GENERIC_WRITE STANDARD_RIGHTS_WRITE |
FILE_ADD_SUBDIRECTORY |
FILE_READ_EA
GENERIC_EXECUTE STANDARD_RIGHTS_EXECUTE |
FILE_LIST_DIRECTORY |
FILE_ADD_FILE
GENERIC_ALL STANDARD_RIGHTS_REQUIRED |
FILE_LIST_DIRECTORY |
FILE_ADD_FILE |
FILE_ADD_SUBDIRECTORY |
FILE_READ_EA
Service
GENERIC_READ STANDARD_RIGHTS_READ |
SERVICE_QUERY_CONFIG |
SERVICE_QUERY_STATUS |
SERVICE_ENUMERATE_DEPENDENTS |
SERVICE_INTERROGATE |
SERVICE_USER_DEFINED_CONTROL
GENERIC_WRITE STANDARD_RIGHTS_WRITE |
SERVICE_CHANGE_CONFIG
GENERIC_EXECUTE STANDARD_RIGHTS_EXECUTE |
SERVICE_START |
SERVICE_STOP |
SERVICE_PAUSE_CONTINUE |
SERVICE_INTERROGATE |
SERVICE_USER_DEFINED_CONTROL
GENERIC_ALL SERVICE_ALL_ACCESS
Printer
GENERIC_READ
GENERIC_WRITE
GENERIC_EXECUTE
GENERIC_ALL
PRINTER_READ
PRINTER_WRITE
PRINTER_EXECUTE
PRINTER_ALL_ACCESS
Registry key
GENERIC_READ
GENERIC_WRITE
GENERIC_EXECUTE
GENERIC_ALL
KEY_READ
KEY_WRITE
KEY_EXECUTE
KEY_ALL_ACCESS
Process
GENERIC_READ STANDARD_RIGHTS_READ |
PROCESS_VM_READ |
PROCESS_QUERY_INFORMATION
GENERIC_WRITE STANDARD_RIGHTS_WRITE |
PROCESS_CREATE_PROCESS |
PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE |
PROCESS_DUP_HANDLE |
PROCESS_TERMINATE |
PROCESS_SET_QUOTA |
PROCESS_SET_INFORMATION
GENERIC_EXECUTE STANDARD_RIGHTS_EXECUTE |
SYNCHRONIZE
GENERIC_ALL PROCESS_ALL_ACCESS
Thread
GENERIC_READ STANDARD_RIGHTS_READ |
THREAD_GET_CONTEXT |
THREAD_QUERY_INFORMATION
GENERIC_WRITE STANDARD_RIGHTS_WRITE |
THREAD_TERMINATE |
THREAD_SUSPEND_RESUME |
THREAD_SET_INFORMATION |
THREAD_SET_CONTEXT
GENERIC_EXECUTE STANDARD_RIGHTS_EXECUTE |
SYNCHRONIZE
GENERIC_ALL THREAD_ALL_ACCESS
Job
GENERIC_READ STANDARD_RIGHTS_READ |
JOB_OBJECT_QUERY
GENERIC_WRITE STANDARD_RIGHTS_WRITE |
JOB_OBJECT_ASSIGN_PROCESS |
JOB_OBJECT_SET_ATTRIBUTES |
JOB_OBJECT_TERMINATE
GENERIC_EXECUTE STANDARD_RIGHTS_EXECUTE |
SYNCHRONIZE
GENERIC_ALL JOB_OBJECT_ALL_ACCESS
Semaphore
GENERIC_READ STANDARD_RIGHTS_READ |
MUTANT_QUERY_STATE
GENERIC_WRITE STANDARD_RIGHTS_WRITE |
SEMAPHORE_MODIFY_STATE
GENERIC_EXECUTE STANDARD_RIGHTS_EXECUTE |
SYNCHRONIZE
GENERIC_ALL SEMAPHORE_ALL_ACCESS
Event
GENERIC_READ STANDARD_RIGHTS_READ |
MUTANT_QUERY_STATE
GENERIC_WRITE STANDARD_RIGHTS_WRITE |
EVENT_MODIFY_STATE
GENERIC_EXECUTE STANDARD_RIGHTS_EXECUTE |
SYNCHRONIZE
GENERIC_ALL EVENT_ALL_ACCESS
Mutex
GENERIC_READ STANDARD_RIGHTS_READ |
MUTANT_QUERY_STATE
GENERIC_WRITE STANDARD_RIGHTS_WRITE
GENERIC_EXECUTE STANDARD_RIGHTS_EXECUTE |
SYNCHRONIZE
GENERIC_ALL MUTEX_ALL_ACCESS
File map object
GENERIC_READ STANDARD_RIGHTS_READ |
FILE_MAP_COPY |
FILE_MAP_READ
GENERIC_WRITE STANDARD_RIGHTS_WRITE |
FILE_MAP_WRITE
GENERIC_EXECUTE STANDARD_RIGHTS_EXECUTE |
FILE_MAP_EXECUTE
GENERIC_ALL FILE_MAP_ALL_ACCESS
Waitable timer
GENERIC_READ STANDARD_RIGHTS_READ |
TIMER_QUERY_STATE
GENERIC_WRITE STANDARD_RIGHTS_WRITE |
TIMER_MODIFY_STATE
GENERIC_EXECUTE STANDARD_RIGHTS_EXECUTE |
SYNCHRONIZE
GENERIC_ALL TIMER_ALL_ACCESS
Token
GENERIC_READ TOKEN_READ
GENERIC_WRITE TOKEN_WRITE
GENERIC_EXECUTE TOKEN_EXECUTE
GENERIC_ALL TOKEN_ALL_ACCESS
Window station
GENERIC_READ WINSTA_ENUMDESKTOPS |
WINSTA_READATTRIBUTES |
WINSTA_ENUMERATE |
WNSTA_READSCREEN |
STANDARD_RIGHTS_READ
GENERIC_WRITE WINSTA_ACCESSCLIPBOARD |
WINSTA_CREATEDESKTOP |
WINSTA_WRITEATTRIBUTES |
STANDARD_RIGHTS_WRITE
GENERIC_EXECUTE WINSTA_ACCESSGLOBALATOMS |
WINSTA_EXITWINDOWS |
STANDARD_RIGHTS_EXECUTE
GENERIC_ALL WINSTA_ENUMDESKTOPS |
WINSTA_READATTRIBUTES |
WINSTA_ENUMERATE |
WINSTA_READSCREEN |
WINSTA_ACCESSCLIPBOARD |
WINSTA_CREATEDESKTOP |
WINSTA_WRITEATTRIBUTES |
WINSTA_ACCESSGLOBALATOMS |
WINSTA_EXITWINDOWS |
STANDARD_RIGHTS_REQUIRED
Desktop
GENERIC_READ DESKTOP_READOBJECTS |
DESKTOP_ENUMERATE |
STANDARD_RIGHTS_READ
GENERIC_WRITE DESKTOP_WRITEOBJECTS |
DESKTOP_CREATEWINDOW |
DESKTOP_CREATEMENU |
DESKTOP_HOOKCONTROL |
DESKTOP_JOURNALRECORD |
DESKTOP_JOURNALPLAYBACK |
STANDARD_RIGHTS_WRITE
GENERIC_EXECUTE DESKTOP_SWITCHDESKTOP |
STANDARD_RIGHTS_EXECUTE
GENERIC_ALL DESKTOP_READOBJECTS |
DESKTOP_WRITEOBJECTS |
DESKTOP_ENUMERATE |
DESKTOP_CREATEWINDOW |
DESKTOP_CREATEMENU |
DESKTOP_HOOKCONTROL |
DESKTOP_JOURNALRECORD |
DESKTOP_JOURNALPLAYBACK |
DESKTOP_SWITCHDESKTOP |
STANDARD_RIGHTS_REQUIRED

Before we proceed, I must make one more point about default security. If you are dealing with objects for which security is inheritable (such as registry keys or files), the default security is applied only to objects for which no ACEs are inherited from parent objects. In both registry and file system hierarchies, ACEs are inherited by default, so default security is not applied in the common case.

Now that you are familiar with all types of access rights, as well as the ACE and ACL types that contain these rights, you have all the tools necessary to read and interpret the security of any object in the system.

The AccessMaster Sample Application

The AccessMaster sample application ("10 AccessMaster.exe") demonstrates the usage of the GetNamedSecurityInfo, SetNamedSecurityInfo, GetSecurityInfo, and SetSecurityInfo functions as well as the ISecurityInformation interface for the EditSecurity common dialog. The source code and resource files for the application are in the AccessMaster directory on the companion CD. Figure 10-7 shows AccessMaster being used to set the security for a file.

click to view at full size.

Figure 10-7. AccessMaster being used to set the security for a file

In addition to showing how to programmatically get and set the security for common objects in the system, the AccessMaster sample application can be a very useful tool for understanding access control for securable objects. You can access objects by name or by process ID and numeric handle value (use decimal values). You can also view ACEs with raw binary access masks rather than map them to standard and specific rights for the objects.

The AccessMaster tool also lets you set the security on securable objects in the system. Remember that because you might not have rights to all objects in the system, you have to take ownership of an object before you can read or write its other security information. To accomplish this, you can use the TrusteeMan sample application from Chapter 9 to assign the SE_TAKE_ OWNERSHIP_NAME privilege to your user account.

If you are unfamiliar with the ramifications of access control on objects in the system, including objects created by your software, you can use the AccessMaster tool to view and modify the access rights assigned to these objects.

Setting Security Information for an Object

The next step in programming for access control is setting the security of a securable object. For most securable objects in the system, you do this by calling SetSecurityInfo or SetNamedSecurityInfo. (For a list of securable objects and the functions used to get and set their security, see Table 10-7.)

Here is the definition of the SetSecurityInfo function:

 DWORD SetSecurityInfo(    HANDLE               handle,    SE_OBJECT_TYPE       objType,    SECURITY_INFORMATION secInfo,    PSID                 psidOwner,    PSID                 psidGroup,    PACL                 pDACL,    PACL                 pSACLl); 

And here is the definition of the SetNamedSecurityInfo function:

 DWORD SetNamedSecurityInfo(    LPTSTR               pObjectName,    SE_OBJECT_TYPE       objType,    SECURITY_INFORMATION secInfo,    PSID                 psidOwner,    PSID                 psidGroup,    PACL                 pDACL,    PACL                 pSACL); 

As you can see, these functions are very similar, except that SetSecurityInfo sets the security of an object for which you have a handle, and SetNamedSecurityInfo sets the security of a named object in the system and does not require a handle.

These functions also have a strong resemblance to their cousins, GetSecurityInfo and GetNamedSecurityInfo. Notice that the objType parameter of the "SetSecurity" functions defines the type of object for which you are setting security information, and uses the same object types as the "GetSecurity" functions. (See Table 10-7.)

The secInfo parameter indicates which components of the object's security descriptor you wish to set. These components can be any combination of the object's owner SID, group SID, DACL, and SACL. Additionally, this parameter is used to set a security descriptor as protected from inherited ACEs in parent DACLs and SACLs. Table 10-16 shows all values that can be passed as the secInfo parameter to SetNamedSecurityInfo and SetNamedSecurity. You can combine any of these values to indicate exactly what and how to apply the security to your object.

Table 10-16. Values that can be passed as the secInfo parameter to SetNamedSecurityInfo and SetNamedSecurity

Value Description
DACL_SECURITY_INFORMATION Indicates that you wish to set DACL information for a securable object.
SACL_SECURITY_INFORMATION Indicates that you wish to set SACL information for a securable object.
OWNER_SECURITY_INFORMATION Indicates that you wish to set the owner SID for a securable object.
GROUP_SECURITY_INFORMATION Indicates that you wish to set the group SID for a securable object.
UNPROTECTED_DACL_SECURITY_INFORMATION Indicates that you want inherited ACEs in parent objects to propagate to this object's DACL. This flag must be used with DACL_SECURITY_INFORMATION and cannot be used with PROTECTED_DACL_ SECURITY_INFORMATION.
PROTECTED_DACL_SECURITY_INFORMATION Indicates that you do not want inherited ACEs in parent objects to propagate to this object's DACL. This flag must be used with DACL_SECURITY_INFORMATION and cannot be used with UNPROTECTED_DACL_ SECURITY_INFORMATION.
UNPROTECTED_SACL_SECURITY_INFORMATION Indicates that you want inherited ACEs in parent objects to propagate to this object's SACL. This flag must be used with SACL_SECURITY_INFORMATION and cannot be used with PROTECTED_SACL_SECURITY_INFORMATION.
PROTECTED_SACL_SECURITY_INFORMATION Indicates that you do not want inherited ACEs in parent objects to propagate to this object's SACL. This flag must be used with SACL_SECURITY_INFORMATION and cannot be used with UNPROTECTED_SACL_SECURITY_INFORMATION.

Depending on which flags you pass as the secInfo parameter when calling SetSecurityInfo or SetNamedSecurityInfo, the values you pass for the psidOwner, psidGroup, pDACL, and pSACL parameters are ignored or indicate the object's new owner SID, group SID, DACL, and SACL, respectively.

You should pass NULL for any parameters to indicate portions of the security descriptor for which you are not setting information.

NOTE
It can be appropriate to indicate DACL_SECURITY_INFORMATION or SACL_SECURITY_INFORMATION in the secInfo parameter and still pass NULL for the pDACL or pSACL parameter. This indicates a NULL DACL or a NULL SACL.

The following code fragment shows the use of SetNamedSecurityInfo to assign a NULL DACL to a registry key and protect its DACL from inherited ACEs found in parent keys' DACLs.

 ULONG lErr = SetNamedSecurityInfo(    TEXT("Machine\\Software\\Jason'sKey"), SE_REGISTRY_KEY,     DACL_SECURITY_INFORMATION|PROTECTED_DACL_SECURITY_INFORMATION,     NULL, NULL, NULL, NULL);    if (lErr != ERROR_SUCCESS){       // Error case    } 

NOTE
Remember that a NULL DACL indicates that everyone has all access to the object (even the ability to write the security of the object). In the rare cases where it is appropriate to set NULL security to an object, you must protect the object's DACL (or lack of DACL) from inherited ACEs. Otherwise, the system is forced to create a DACL that will include the ACEs inherited from parent objects, which immediately undermines your goal of NULL security for the object. On the other hand, this side effect of inherited security can be a convenient way to set an object's DACL to include no ACEs except those inherited from parent objects.

As you can see, setting an object's security can be simple. However, it can become significantly more difficult when including a DACL with meaningful ACEs. Whether you are setting the security of a new object (one you create) or an object that already exists, you can take two basic approaches to create the DACL for the object: create a new DACL for the object, or modify an existing DACL.

You will typically create a DACL for new objects, but not always. You can also use an existing DACL from an object and make modifications to it, and then use the modified DACL to create a new object of the same type as the original securable object.

Similarly for existing objects, it is very common to make modifications to the existing DACL and far less common to completely overwrite the security. However, this does not mean that you cannot completely replace the DACL of an existing object.

Because you can be flexible about the approaches depending on your needs, I will cover both, starting with the simpler task of creating a brand new DACL.

Creating a DACL

You will typically follow these steps when creating a DACL:

  1. Gather the SIDs for the trustees for which you will be creating ACEs for the DACL.
  2. Calculate the size of the new DACL by using the size of the SIDs and the size of the ACE structure that you are using.
  3. Allocate memory for the DACL.
  4. Initialize the DACL.
  5. Use the SIDs to add ACEs to the DACL, taking care to add your access-denied ACEs before adding your access-allowed ACEs.

For existing objects, you will use one of the "SetSecurity" functions to apply the new DACL to the objects. For most new objects, you are required to also create and initialize a security descriptor and a security attributes structure to pass when calling a create function such as CreateEvent or CreateFile. I will show you how to do this in a moment.

The most complicated part of creating a DACL is calculating its size. The CalculateACLSize sample function shows how to do this. To use this function, you pass an array of pointers to SIDs, which are used to calculate the necessary size of the new DACL. I also included an optional pointer to an existing DACL, which, if NULL, is ignored, and if not NULL, adds the size of the ACEs in the existing DACL and gives you the size of a new DACL created from an existing DACL. More on this in a moment, but for now, see the CalculateACLSize function here.

 ULONG CalculateACLSize( PACL pACLOld, PSID* ppSidArray, int nNumSids,     PACE_UNION* ppACEs, int nNumACEs ){    ULONG lACLSize = 0;        try{       // If we are including an existing ACL, then find its size       if (pACLOld != NULL){          ACL_SIZE_INFORMATION aclSize;          if(!GetAclInformation(pACLOld, &aclSize, sizeof(aclSize),              AclSizeInformation)){                         goto leave;          }          lACLSize = aclSize.AclBytesInUse;       }       if (ppSidArray != NULL){          // Step through each SID          while (nNumSids--){             // If a SID isn't valid, then we bail             if (!IsValidSid(ppSidArray[nNumSids])){                lACLSize = 0;                goto leave;             }             // Get the SID's length             lACLSize += GetLengthSid(ppSidArray[nNumSids]);             // Add the ACE structure size, minus the              // size of the SidStart member             lACLSize += sizeof(ACCESS_ALLOWED_ACE) -                 sizeof(((ACCESS_ALLOWED_ACE*)0)->SidStart);                   }             }       if (ppACEs != NULL){          // Step through each ACE          while (nNumACEs--){                         // Get the SIDs length             lACLSize += ppACEs[nNumACEs]->aceHeader.AceSize;          }             }                // Add in the ACL structure itself       lACLSize += sizeof(ACL);    leave:;    }catch(...){       // An exception means we fail the function       lACLSize = 0;          }    return (lACLSize); } 

NOTE
The CalculateACLSize function uses the size of the ACCESS_ ALLOWED_ACE structure, which will work for all standard ACE types. This function will not calculate an ACL size properly if it includes object ACE types. To properly calculate the size, replace the reference to ACCESS_ALLOWED_ACE in this function with an object ACE structure such as ACCESS_ALLOWED_OBJECT_ACE. For a discussion on object ACEs, see the section earlier in this chapter titled "Object ACEs."

When you use a function such as CalculateACLSize, finding the amount of memory necessary to create a new ACL is a simple task. You should use the value returned from the function to allocate memory using an allocator such as new, HeapAlloc, or malloc.

After you have memory for your new ACL, you should initialize the ACL using InitializeAcl:

 BOOL InitializeAcl(    PACL  pACL,    DWORD dwAclLength,    DWORD dwAclRevision); 

InitializeAcl is a very simple function that sets the length of the buffer in the structure of the ACL and the ACL's revision. You should pass ACL_REVISION for the dwAclRevision parameter. An ACL can also have the revision of ACL_REVISION_DS, which indicates that the ACL contains object ACEs. However, you do not need to explicitly set this revision, because when you add object ACEs to an ACL, the system updates the revision of the ACL.

After calling InitializeAcl, your memory buffer contains an empty ACL containing no ACEs. If you wish to remove all ACEs from an existing ACL, you can also use InitializeAcl to do this.

Now you are ready to begin adding ACEs to your empty ACL. Assuming you have properly calculated the size of your finished ACL, adding ACEs should be as simple as making repeated calls to AddAccessDeniedAceEx for all your access-denied ACEs, followed by calls to AddAccessAllowedAceEx for all of your access-allowed ACEs.

AddAccessDeniedAceEx and AddAccessAllowedAceEx build ACEs of the appropriate type and add them to the end of a DACL (assuming that there is room in the DACL to accompany the ACE). AddAccessDeniedAceEx is defined as follows:

 BOOL AddAccessDeniedAceEx(    PACL  pDACL,    DWORD dwACERevision,    DWORD dwACEFlags,    DWORD dwAccessMask,    PSID  psidTrustee); 

Here is AddAccessAllowedAceEx:

 BOOL AddAccessAllowedAceEx(    PACL  pDACL,    DWORD dwACERevision,    DWORD dwACEFlags,    DWORD dwAccessMask,    PSID  psidTrustee); 

The declarations for these functions are identical, which is nice. The pDACL parameter indicates the DACL to which you are adding an ACE. The dwACERevision parameter should be set to ACL_REVISION. The dwACEFlags parameter indicates the value that will be set in the AceFlags member of the new ACE's ACE_HEADER. This is used to indicate the ACE's inheritance properties. See Table 10-11 for an explanation of the different ACE flags. You should not pass the flags indicating audit type for the dwACEFlags parameter.

The dwAccessMask parameter indicates the access mask of the new ACE, which describes which rights you are denying or allowing to the trustee. And finally, you should pass a pointer to a SID structure for the psidTrustee parameter to indicate the trustee for which access is being denied or allowed.

If there is insufficient room in the DACL, the AddAccessDeniedAceEx and AddAccessAllowedAceEx functions return FALSE, and GetLastError returns ERROR_ALLOTTED_SPACE_EXCEEDED.

NOTE
For objects that do not support inheritance, you can use the simpler non-"Ex" versions of AddAccessDeniedAce and AddAccessAllowedAce. The only difference with the non-"Ex" functions is that they do not allow you to set the flags for the new ACE.

When you understand AddAccessDeniedAceEx and AddAccessAllowedAceEx, you can see that calling them repeatedly to create a new DACL is no big deal. After you create the new DACL, you can pass it to SetSecurityInfo or SetNamedSecurityInfo to apply it to an existing object. (Don't forget to free the memory for your new DACL after you have called one of these functions.)

You usually create a new DACL for an object that you are about to create. Creating an object typically requires you to assign the new DACL to a security descriptor, and then assign the security descriptor to a security attributes structure before calling the "create" function. The following code shows how to create a new DACL for a new event object. The code creates and initializes a security descriptor with a DACL, and then uses it to create a named event. (Note that this code uses the previous CalculateACLSize function to calculate the size of the new DACL.) The two challenges in this process are calculating the DACL's size and adding your ACEs in the proper order.

 PSID psidEveryone ; // Create a SID for the built-in "Everyone" group SID_IDENTIFIER_AUTHORITY sidAuth = SECURITY_WORLD_SID_AUTHORITY; if (!AllocateAndInitializeSid( &sidAuth, 1, SECURITY_WORLD_RID,     0, 0, 0, 0, 0, 0, 0, &psidEveryone )){    // Error } // We are creating two ACEs, both using the "Everyone" group PSID psidArray[2]; psidArray[0] = psidEveryone; psidArray[1] = psidEveryone; // Get the size of the new ACL ULONG lACLSize = CalculateACLSize( NULL, psidArray, 2, NULL, 0); if (lACLSize == 0){    // Error } // Allocate memory for the ACL PACL pDACL = (PACL)HeapAlloc(GetProcessHeap(), 0, lACLSize); if (pDACL == NULL){    // Error } // Initialize the ACL if (!InitializeAcl(pDACL, lACLSize, ACL_REVISION)){    // Error } // Make sure to add our denied ACE first if (!AddAccessDeniedAce(pDACL, ACL_REVISION,     WRITE_OWNER|WRITE_DAC, psidArray[0])){    // Error } // Then add our allowed ACE if (!AddAccessAllowedAce(pDACL, ACL_REVISION,     STANDARD_RIGHTS_ALL|SPECIFIC_RIGHTS_ALL, psidArray[1])){    GetLastError();//Error "winerror.h" } // Allocate space for a security descriptor PSECURITY_DESCRIPTOR pSD = HeapAlloc(GetProcessHeap(), 0,     SECURITY_DESCRIPTOR_MIN_LENGTH); if (pSD == NULL){    // Error } // We now have an empty security descriptor if (!InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION)){    // Error } // To which we assign our DACL if (!SetSecurityDescriptorDacl(pSD, TRUE, pDACL, FALSE)){    // Error } // Then we point to our SD from a SECURITY_ATTRIBUTES structure SECURITY_ATTRIBUTES sa = {0}; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = pSD; // Which we pass to CreateEvent HANDLE hEvent = CreateEvent(&sa, TRUE, FALSE, TEXT("SecureEvent")); // Clean up  HeapFree(GetProcessHeap(), 0, pSD); HeapFree(GetProcessHeap(), 0, pDACL); FreeSid(psidEveryone); 

NOTE
An alternative to adding the ACEs in the proper order would be to write a function to order the ACL for you so that you could add ACEs without concern for order. You would then order the entire ACL before applying the ACL to an object. I implement such a function later in this chapter.

This sample not only shows how to build a DACL and security descriptor for use in object creation, but it also illustrates an important technique for sharing objects with services.

The default security for a service in the LocalSystem user context will cause objects such as named events and named pipes to be secured against use by processes running in the security context of a logged-on user. But sometimes this is not the desired behavior. At times you might be tempted to create an object with a NULL DACL, which allows everyone all access to the object, but doing so creates a security hole. All access includes the ability to adjust the object's security, and would allow anyone to change the security of the object. Having the security changed would almost definitely cause the object to stop working as expected.

A better solution to controlling access is to deny everyone access to the security of the object (which allows only the owner of the object to modify the object's security). After adding the denied ACE, you can comfortably allow any trustees access to the object, knowing that the deny ACE will take precedence. In this case, I allow everyone all access.

Modifying a DACL

Creating secure objects involves as much access control as many services ever need to implement. It is surprising how many services you can implement without ever having to modify the security of an existing object. Of course, some programs have to deal with existing objects that already have security applied to them. In such a case, your software must be able to read and modify a DACL for an object. Here are the steps you might take to do this:

  1. Retrieve the DACL of the object that you wish to modify.
  2. If you are removing ACEs, search the DACL for these ACEs and delete them.
  3. Gather the SIDs of the trustees for which you will be adding ACEs.
  4. Create the ACEs that you will be adding.
  5. Search the DACL for ACEs that are the same as ACEs you will be adding. If any exist, remove them from the group of ACEs you will be adding.
  6. Calculate the new DACL size.
  7. Allocate memory for and initialize the new DACL.
  8. Copy the old DACL to the new DACL.
  9. Insert new ACEs in the proper position in the new DACL.
  10. Assign the DACL back to the object.

As you can see, the process for reading and modifying an object's DACL is not so simple, but I should tell you that I've offered the worst-case scenario. Often you are not removing ACEs at all, so step 2 can be ignored. And commonly you add only a single ACE; if that ACE is already in the DACL, you can abort the whole procedure, simplifying step 5.

NOTE
It is important to search ACLs for ACEs that already exist, even though duplicate ACEs do not affect the security of an object. The reason is that ACEs take memory in the system, and some system objects (such as a window station) can hold surprisingly few ACEs. Any software that adds an ACE every time it is run, without checking the necessity of the task, will eventually exhaust system resources.

Now I will tell you about the tools required to tackle the task of reading and modifying a DACL. Let's start with a simple function. (You already know how to read information in a DACL, and those skills will come into play here.)

 BOOL DeleteAce(    PACL  pACL,    DWORD dwACEIndex); 

The DeleteAce function removes an ACE from a DACL. You must pass it the ACL and the index of the ACE that you wish to remove.

NOTE
If the only modification that you are making to an object is to remove ACEs, you do not have to allocate memory for a new DACL. You can simply remove the ACEs in the DACL returned by GetSecurityInfo, and then set the modified DACL back to the object.

The next steps in the process require that you have an ACE handy because you will use the AddAce function (discussed shortly), which requires an ACE that is already built. Also, because you might search the ACL for the existence of the ACE, it is convenient to have an ACE structure with which to compare ACEs in the DACL.

Unfortunately the system does not provide a function that helps you build ACEs in a simple manner. The task of building ACEs is complicated because each ACE includes a SID structure, and SID structures are variable in length. It is best to use your SID's length and the length of the ACE structure to allocate a buffer for the ACE, copy the SID into the ACE, and then set the ACE fields. The following function shows how to do this:

 PACE_UNION AllocateACE( ULONG bACEType, ULONG bACEFlags,     ULONG lAccessMask, PSID pSID ){    PACE_UNION pReturnACE = NULL;    PBYTE pbBuffer = NULL;    try{       // Get the offset of the SID in the ACE       ULONG lSIDOffset = (ULONG)(&((ACCESS_ALLOWED_ACE*)0)->SidStart);       // Get the size of the ACE without the SID       ULONG lACEStructSize = sizeof(ACCESS_ALLOWED_ACE) -           sizeof(((ACCESS_ALLOWED_ACE*)0)->SidStart);       // Get the length of the SID       ULONG lSIDSize = GetLengthSid(pSID);             // Allocate a buffer for the ACE       pbBuffer = (PBYTE)LocalAlloc(LPTR, lACEStructSize + lSIDSize);       if (pbBuffer == NULL)          goto leave;       // Copy the SID into the ACE       if(!CopySid(lSIDSize, (PSID)(pbBuffer+lSIDOffset), pSID)){          goto leave;       }             pReturnACE = (PACE_UNION) pbBuffer;       pReturnACE->aceHeader.AceSize = (USHORT)(lACEStructSize + lSIDSize);       pReturnACE->aceHeader.AceType = (BYTE)bACEType;       pReturnACE->aceHeader.AceFlags = (BYTE)bACEFlags;       pReturnACE->aceAllowed.Mask = lAccessMask;    leave:;    }catch(...){}    // Free the buffer in an error case    if (pbBuffer != (PBYTE)pReturnACE){       LocalFree(pbBuffer);    }    return (pReturnACE);     } 

When using AllocateAce, you simply pass the returned ACE to LocalFree when finished with it.

NOTE
AllocateAce returns a PACE_UNION pointer, which is a type I defined earlier in this chapter. I chose to use this technique to represent ACEs in a generic way. It is also very common to simply choose one of the specific ACE types, such as ACCESS_ALLOWED_ACE, to use generically for your ACE manipulation. Either approach works; however, for a number of the sample functions in this chapter, I use the PACE_UNION.

Now that you have an ACE built, you must search your object's existing DACL for ACEs that match. You can do this by using the ACL reading techniques discussed earlier in this chapter. I wrote code to perform this task using two functions: one to compare ACEs and one to search an ACL for a matching ACE.

 BOOL IsEqualACE( PACE_UNION pACE1, PACE_UNION pACE2 ){    BOOL fReturn = FALSE;        try{{       if(pACE1->aceHeader.AceType != pACE2->aceHeader.AceType)          goto leave;       // Get the offset of the SID in the ACE       ULONG lSIDOffset = (ULONG)(&((ACCESS_ALLOWED_ACE*)0)->SidStart);       // Get the size of the ACE without the SID       ULONG lACEStructSize = sizeof(ACCESS_ALLOWED_ACE) -           sizeof(((ACCESS_ALLOWED_ACE*)0)->SidStart);       PBYTE pbACE1 = (PBYTE)pACE1;       PBYTE pbACE2 = (PBYTE)pACE2;       fReturn = TRUE;       while(lACEStructSize--)          fReturn = (fReturn && ((pbACE1[lACEStructSize] ==             pbACE2[lACEStructSize])));       // Check SIDs       fReturn = fReturn && EqualSid((PSID)(pbACE1+lSIDOffset),          (PSID)(pbACE2+lSIDOffset));          }leave:;    }catch(...){    }    return (fReturn); } 

Here is the second function:

 int FindACEInACL( PACL pACL, PACE_UNION pACE ){    int nACEIndex = -1;    try{{       ACL_SIZE_INFORMATION aclSize;       if (!GetAclInformation(pACL, &aclSize, sizeof(aclSize),           AclSizeInformation)){          goto leave;       }       while (aclSize.AceCount--){          PACE_UNION pACETemp;          if(!GetAce(pACL, aclSize.AceCount, (PVOID *)&pACETemp))             goto leave;          if(IsEqualACE(pACETemp, pACE)){             nACEIndex = (int)aclSize.AceCount;             break;          }       }    }leave:;    }catch(...){    }    return (nACEIndex); } 

The FindAceInACL function returns the index of a matching ACE in the DACL, or -1 if no match is found. You should use this function or one like it to find out whether the ACEs you intend to add to your object's DACL already exist in the DACL. (The FindACEInACL function can also be useful for finding ACEs that you wish to delete using DeleteAce).

Now that you know which ACEs you must add your DACL and you have built the ACEs, it is time to calculate the size of your new DACL. You can use the CalculateACLSize function, shown earlier in this chapter. It allows you to calculate an ACL size using an existing ACL, an array of SIDs, and an array of ACEs. You can pass NULL for any of these parameters. In our example, you are likely to pass an existing ACL and an array of ACEs to calculate the new size of the ACL after your ACEs have been added.

After you allocate memory for your new ACL, you initialize the new ACL using InitializeAcl (discussed earlier in this chapter). You have to copy the ACEs from your old ACL to your new ACL. The following function shows how to do this:

 BOOL CopyACL( PACL pACLDestination, PACL pACLSource ){    BOOL fReturn = FALSE;    try{{       // Get the number of ACEs in the source ACL       ACL_SIZE_INFORMATION aclSize;       if (!GetAclInformation(pACLSource, &aclSize,           sizeof(aclSize), AclSizeInformation)){          goto leave;       }       // Use GetAce and AddAce to copy the ACEs       for(ULONG lIndex=0;lIndex < aclSize.AceCount;lIndex++){          ACE_HEADER* pACE;          if(!GetAce(pACLSource, lIndex, (PVOID*)&pACE))             goto leave;          if(!AddAce(pACLDestination, ACL_REVISION, MAXDWORD,              (PVOID*)pACE, pACE->AceSize))             goto leave;       }             fReturn = TRUE;    }leave:;    }catch(...){    }    return (fReturn); } 

This CopyACL function is fairly simple. It simply iterates through the ACEs in the existing DACL and copies each one to the new DACL. CopyACL uses the AddAce system function, which is defined as follows:

 BOOL AddAce(    PACL  pACL,    DWORD dwACERevision,    DWORD dwStartingACEIndex,    PVOID pACEList,     DWORD dwACEListLength); 

Notice that AddAce is different from AddAccessAllowedAce and AddAccessDeniedAce. First you have to supply the ACE; the system does not compile the ACE into the DACL for you. This means that you define the type of ACE as well, so AddAce can add any type of ACE to the DACL. Second, AddAce allows you to decide where in the DACL you wish to insert the new ACE by using an index starting with zero. Third, AddAce allows you to add more than one ACE by passing a sequential list of ACEs as the pACEList parameter and the size of the list as the dwACEListLength parameter (not to be confused with the number of ACEs in the list).

NOTE
The CopyACL sample function could have been implemented more efficiently by allowing AddAce to copy all of the source ACEs over in a single function call. However, doing this assumes information about the ACL structure, which you should treat as opaque.

After you copy your old DACL into your new, more spacious DACL, you can begin adding ACEs to it using AddAce. Take care to insert your ACEs in the proper index for your new DACL so that proper ACE ordering is preserved. (See Table 10-6 regarding ACE order.) You can use the following function to determine the appropriate index for a new ACE in an ACL:

 ULONG GetACEInsertionIndex( PACL pDACL, PACE_UNION pACENew){    ULONG lIndex = (ULONG) -1;    try{{       // ACE types by ACL order       ULONG lFilterType[] = { ACCESS_DENIED_ACE_TYPE,                                ACCESS_DENIED_OBJECT_ACE_TYPE,                                ACCESS_ALLOWED_ACE_TYPE,                                ACCESS_ALLOWED_OBJECT_ACE_TYPE};       // Determine which group the new ACE should belong to       ULONG lNewAceGroup;       for(lNewAceGroup = 0; lNewAceGroup<4 ; lNewAceGroup++){          if(pACENew->aceHeader.AceType == lFilterType[lNewAceGroup])             break;       }       // If group == 4, the ACE type is no good       if(lNewAceGroup==4)          goto leave;       // If new ACE is an inherited ACE, then it goes after other ACEs       if((pACENew->aceHeader.AceFlags & INHERITED_ACE) != 0)          lNewAceGroup+=4;       // Get ACE count       ACL_SIZE_INFORMATION aclSize;       if (!GetAclInformation(pDACL, &aclSize,           sizeof(aclSize), AclSizeInformation)){          goto leave;       }             // Iterate through ACEs       lIndex = 0;       for(lIndex = 0;lIndex < aclSize.AceCount;lIndex++){          ACE_HEADER* pACE;          if(!GetAce(pDACL, lIndex, (PVOID*)&pACE))             goto leave;          // Get the group of the ACL ACE          ULONG lAceGroup;          for(lAceGroup = 0; lAceGroup<4 ; lAceGroup++){             if(pACE->AceType == lFilterType[lAceGroup])                break;          }          // Test for bad ACE          if(lAceGroup==4){             lIndex = (ULONG) -1;             goto leave;          }          // Inherited adjustment          if((pACE->AceFlags & INHERITED_ACE) != 0)             lAceGroup+=4;          // If this is the same group, then insertion point found          if(lAceGroup>=lNewAceGroup)             break;       }           }leave:;    }catch(...){    }       return (lIndex); } 

The GetACEInsertionIndex function finds the index in which to insert your new ACE, and in the process considers object ACEs and ACE inheritance. After you know the proper index, you can call AddAce to add the new ACE into the ACL.

Do this for each new ACE, and then use the appropriate function to set the new DACL to the securable object. Don't forget to clean up after yourself and free any memory that you have allocated.

Earlier in this chapter, I promised to include a sample function that orders the ACEs in a DACL. This function might be useful if you don't want to concern yourself with ACE ordering at all until after you completely finish adding ACEs to your DACL. The following OrderDACL function is fairly simple, and it builds on the GetACEInsertionIndex sample function:

 BOOL OrderDACL( PACL pDACL ){    BOOL fReturn = FALSE;    try{{       // Get ACL size and ACE count       ACL_SIZE_INFORMATION aclSize;       if (!GetAclInformation(pDACL, &aclSize,           sizeof(aclSize), AclSizeInformation)){          goto leave;       }       // Get memory for temporary ACL       PACL pTempDACL = (PACL) _alloca(aclSize.AclBytesInUse);       if (pTempDACL==NULL)          goto leave;       // Initialize temporary ACL       if (!InitializeAcl(pTempDACL, aclSize.AclBytesInUse,           ACL_REVISION))          goto leave;              // Iterate through ACEs       for (ULONG lAceIndex = 0;           lAceIndex < aclSize.AceCount ; lAceIndex++){          // Get ACE          PACE_UNION pACE;          if (!GetAce(pDACL, lAceIndex, (PVOID*)&pACE))             goto leave;          // Find location, and add ACE to temp DACL          ULONG lWhere = GetACEInsertionIndex(pTempDACL, pACE);          if (!AddAce(pTempDACL, ACL_REVISION,              lWhere, pACE, pACE->AceSize))             goto leave;       }       // Copy temp DACL to original       CopyMemory(pDACL, pTempDACL, aclSize.AclBytesInUse);       fReturn = TRUE;    }    leave:;    }catch(...){    }    return (fReturn); } 

Let's take a look at a real-world example that implements the techniques we have been discussing.

Modifying a DACL Example

Our example shows how to modify an existing DACL while providing a useful set of functions for the service developer. I used the window station and the desktop-securable objects in Windows.

A window station is a secure object in Windows that contains a clipboard, a set of global atoms, and a collection of desktop objects. A window station can be interactive, meaning its "desktops" can be seen by a user. An interactive window station also maintains keyboard and mouse information. A process can have a single window station associated with it.

A desktop is a secure object contained within a window station. A desktop maintains a logical display surface and contains menus, windows, and other objects visible on screen.

Services that do not interact with the user are not associated with the interactive desktop. When a user logs on to a system, the DACL for the interactive window station (named WinSta0) and its default desktop (named Default) are reset, and the user is given access to the objects. Ultimately, only the logged-on user and the system are granted access to the objects.

Problem description Sometimes a service has to create another process under an arbitrary trustee account (using CreateProcessAsUser discussed in the next chapter) to create a process under a user context that is not currently interacting with the system. If this process needs to interact with a user, and the user account does not have access to the interactive window station and its default desktop, the system fails the call to CreateProcessAsUser.

Herein lies the problem. Before creating the process, you have to check the DACLs of these objects for sufficient access rights for the user. If these rights aren't found, they must be added. It is important to check for the rights first, because adding an ACE blindly to the user objects can eventually exhaust resources in the system. (Typically you can add only about 80 ACEs to a window station.)

Solution Now let's tackle the solution. I used the tools and concepts (and some of the sample functions) discussed throughout this section to implement two functions: one that allows a trustee access to a window station and one that allows a trustee access to a desktop. These functions are pretty straightforward. Although the process of modifying an object's DACL can seem somewhat daunting, in the real world the process tends to be less complex than we imagine. The following code allows a trustee access to a window station:

 BOOL AllowAccessToWinSta( PSID psidTrustee, HWINSTA hWinSta ){    BOOL fReturn = FALSE;         PSECURITY_DESCRIPTOR psdWinSta = NULL;    PACE_UNION pACENew = NULL;    try{{           // Get the DACL for the window station       PACL pDACLWinSta;       if(GetSecurityInfo(hWinSta, SE_WINDOW_OBJECT,           DACL_SECURITY_INFORMATION, NULL, NULL, &pDACLWinSta,           NULL, &psdWinSta) != ERROR_SUCCESS)          goto leave;              // Allocate our new ACE       // This is the access awarded to a user who logged on interactively       PACE_UNION pACENew = AllocateACE(ACCESS_ALLOWED_ACE_TYPE, 0,           DELETE|WRITE_OWNER|WRITE_DAC|READ_CONTROL|          WINSTA_ENUMDESKTOPS|WINSTA_READATTRIBUTES|          WINSTA_ACCESSCLIPBOARD|WINSTA_CREATEDESKTOP|          WINSTA_WRITEATTRIBUTES|WINSTA_ACCESSGLOBALATOMS|          WINSTA_EXITWINDOWS|WINSTA_ENUMERATE|WINSTA_READSCREEN,           psidTrustee);                    // Is the ACE already in the DACL?       if (FindACEInACL(pDACLWinSta, pACENew) == -1){          // If not, calculate new DACL size          ULONG lNewACL = CalculateACLSize( pDACLWinSta, NULL, 0,              &pACENew, 1 );          // Allocate memory for the new DACL          PACL pNewDACL = (PACL)_alloca(lNewACL);          if (pNewDACL == NULL)             goto leave;          // Initialize the ACL          if (!InitializeAcl(pNewDACL, lNewACL, ACL_REVISION))             goto leave;          // Copy the ACL          if (!CopyACL(pNewDACL, pDACLWinSta))             goto leave;          // Get location for new ACE          ULONG lIndex = GetACEInsertionIndex(pNewDACL, pACENew);          // Add the new ACE          if (!AddAce(pNewDACL, ACL_REVISION, lIndex,              pACENew, pACENew->aceHeader.AceSize))             goto leave;          // Set the DACL back to the window station          if (SetSecurityInfo(hWinSta, SE_WINDOW_OBJECT,              DACL_SECURITY_INFORMATION, NULL, NULL,              pNewDACL, NULL)!=ERROR_SUCCESS)             goto leave;       }       fReturn = TRUE;            }leave:;    }catch(...){    }    // Clean up    if(pACENew != NULL)       LocalFree(pACENew);    if(psdWinSta != NULL)       LocalFree(psdWinSta);       return (fReturn);    } 

The next sample function allows a trustee access to a desktop:

 BOOL AllowAccessToDesktop( PSID psidTrustee, HDESK hDesk ){    BOOL fReturn = FALSE;         PSECURITY_DESCRIPTOR psdDesk = NULL;    PACE_UNION pACENew = NULL;    try{{           // Get the DACL for the desktop       PACL pDACLDesk;       if(GetSecurityInfo(hDesk, SE_WINDOW_OBJECT,           DACL_SECURITY_INFORMATION, NULL, NULL, &pDACLDesk,           NULL, &psdDesk) != ERROR_SUCCESS)          goto leave;              // Allocate our new ACE       // This is the access awarded to a user who logged on interactively       PACE_UNION pACENew = AllocateACE(ACCESS_ALLOWED_ACE_TYPE, 0,           DELETE|WRITE_OWNER|WRITE_DAC|READ_CONTROL|          DESKTOP_READOBJECTS|DESKTOP_CREATEWINDOW|          DESKTOP_CREATEMENU|DESKTOP_HOOKCONTROL|          DESKTOP_JOURNALRECORD|DESKTOP_JOURNALPLAYBACK|          DESKTOP_ENUMERATE|DESKTOP_WRITEOBJECTS|DESKTOP_SWITCHDESKTOP,          psidTrustee);                    // Is the ACE already in the DACL?       if (FindACEInACL(pDACLDesk, pACENew) == -1){          // If not, calculate new DACL size          ULONG lNewACL = CalculateACLSize( pDACLDesk, NULL, 0,              &pACENew, 1 );          // Allocate memory for the new DACL          PACL pNewDACL = (PACL)_alloca(lNewACL);          if (pNewDACL == NULL)             goto leave;          // Initialize the ACL          if (!InitializeAcl(pNewDACL, lNewACL, ACL_REVISION))             goto leave;          // Copy the ACL          if (!CopyACL(pNewDACL, pDACLDesk))             goto leave;          // Get location for new ACE          ULONG lIndex = GetACEInsertionIndex(pNewDACL, pACENew);          // Add the new ACE          if (!AddAce(pNewDACL, ACL_REVISION, lIndex,              pACENew, pACENew->aceHeader.AceSize))             goto leave;          // Set the DACL back to the window station          if (SetSecurityInfo(hDesk, SE_WINDOW_OBJECT,              DACL_SECURITY_INFORMATION, NULL, NULL,              pNewDACL, NULL)!=ERROR_SUCCESS)             goto leave;       }       fReturn = TRUE;            }leave:;    }catch(...){    }    // Clean up    if(pACENew != NULL)       LocalFree(pACENew);    if(psdDesk != NULL)       LocalFree(psdDesk);       return (fReturn);    } 

The following code fragment shows an example of how these functions are used. The code creates a SID for the built-in Everyone group and passes it to the AllowAccessToWinSta and AllowAccessToDesktop functions:

 PSID psidEveryone;  // Create a SID for the built-in "Everyone" group SID_IDENTIFIER_AUTHORITY sidAuth = SECURITY_WORLD_SID_AUTHORITY; if (!AllocateAndInitializeSid( &sidAuth, 1, SECURITY_WORLD_RID,     0, 0, 0, 0, 0, 0, 0, &psidEveryone )){    // Error } HWINSTA hWinSta = GetProcessWindowStation(); if (hWinSta == NULL){    // Error } AllowAccessToWinSta(psidEveryone, hWinSta); HDESK hDesk = GetThreadDesktop(GetCurrentThreadId()); if (hDesk == NULL){    // Error } AllowAccessToDesktop(psidEveryone, hDesk); 

NOTE
Here's a tip. The AllowAccessToWinSta and AllowAccessToDesktop functions use a function called _alloca defined by the C run-time library in the header file Malloc.h. The _alloca function allocates a block of memory on the thread's stack. The beauty of this function is that it is very fast, requires no internal thread synchronization, and returns memory that does not need to be freed by your application. The system frees the memory when you exit the function in which it was called.

For a security programmer who must make repeated small allocations, a function like this one can be a lifesaver. It will speed up your code and help you avoid memory leaks.

I strongly suggest that you take the time to walk through the AllowAccessToWinSta and AllowAccessToDesktop functions until you understand how they work and feel confident using the techniques that they use. These sample functions perform tasks that are about as complex as you will see in access control programming, and if you are comfortable with them, you probably won't have trouble implementing access control that meets your needs.

Options for Implementing Access Control

Microsoft and other parties have attempted to ease the burden on the access control programmer by creating higher-level functions that wrap the low-level functions we have been covering. Third-party vendors have also created solutions to ease this task of access control. Awareness of the low-level functionality of access control in Windows, as well as of some of the pitfalls of these higher-level functions, should enable you to make decisions that will address the needs of your code.

The implementers of the higher-level packages have been faced with these challenges:

  • Simplifying the extremely flexible access control system, without restricting flexibility or the features you are likely to need in your project
  • Creating robust and usable code

The first challenge—maintaining flexibility—is the most difficult to overcome. It is safe to say that the great majority of your access control needs can be implemented directly using Windows access control. However, with this flexibility comes complexity, and the moment you remove the complexity, you remove features that some other developer is undoubtedly expecting.

The second challenge might appear to be more achievable, but this has not proven to be the case. Microsoft has implemented a set of "high-level" security functions that have been added to the Win32 API. These functions drastically simplify certain aspects of security programming, but some have a history of bugs, and some have intrinsic flaws. Here is an example:

 DWORD GetEffectiveRightsFromAcl(    PACL         pACL,    PTRUSTEE     pTrustee,    PACCESS_MASK pAccessRights); 

The GetEffectiveRightsFromAcl function is intended to search an ACL and return an access mask that indicates access allowed to a trustee by a DACL. Sounds very convenient! Such a function could potentially remove our need to search a DACL for ACEs before adding our own ACEs. GetEffectiveRightsFromAcl, however, attempts to do too much, and thus ends up doing almost nothing of use.

GetEffectiveRightsFromAcl figures out the access allowed based on a composite of the matching access-allowed ACEs, and then subtracts a composite of the matching access-denied ACEs. What this means is that GetEffectiveRightsFromAcl can return a set of access rights indicating that the ACL does not award the access I want for my trustee, and I'm left still not knowing how to fix the problem. Is the access not granted because of the absence of access-allowed ACEs, or is it not granted because of the presence of access-denied ACEs? I would hate to add an access-allowed ACE, just to find out that I still don't have access because of an overriding access-denied ACE.

GetEffectiveRightsFromAcl not only searches for ACEs that match the trustee you supply, but also searches for any ACEs for group accounts of which your trustee is a member. But the function excludes built-in groups such as Everyone and Authenticated Users. And it fails if it finds ACEs for groups whose member trustees your code does not have rights to enumerate. Finally, there is no way to limit the search to finding only the access explicitly assigned to your trustee.

GetEffectiveRightsFromAcl was intended to enable your code to find out whether an access check on an object would succeed or fail for a trustee. However, access checks require a token, not a trustee SID. Tokens contain privileges; tokens can also be adjusted or restricted. (See Chapter 11.) GetEffectiveRightsFromAcl does not take privileges into account when checking access. Access checks can succeed based on object ownership, but GetEffectiveRightsFromAcl has no knowledge of object ownership. In these ways, GetEffectiveRightsFromAcl has limited use.

The more useful functions SetEntriesInAcl and GetExplicitEntriesFromAcl are intended to relieve you of the responsibility of allocating memory for ACEs and ACLs while still allowing you to deal with the ACL directly. The goal of these functions is great, but the functions have a sordid history of bugs and performance issues. Some of these problems have been cleared up, but if you choose to use these functions in your projects, it is important that you thoroughly test the code that uses them.

The BuildExplicitAccessWithName function, which should be used with SetEntriesInAcl, does not return a value. Instead it potentially defers an error scenario (that you would have caught when using low-level functions to call LookupAccountName) to the SetEntriesInAcl function. Because SetEntriesInAcl has no way of reporting back which entry failed, you are left with a failure case from which it is difficult to recover.

You also have other options. The developers of the Active Template Library designed a C++ class called CSecurityDescriptor defined in the header file AtlCom.h. The bonus of CSecurityDescriptor is that the entire source code for the class is provided. Although this class provides a wealth of functionality, it comes with its own pitfalls. For example, the functions used to add ACEs do not follow the ACE ordering guidelines set forth for Windows 2000, although they do follow the guidelines for Windows NT 4.0. Additionally, the AttachObject function for retrieving the security from a kernel object uses GetKernelObjectSecurity instead of GetSecurityInfo, which is the suggested function for use with Windows 2000. (The class also suffers from an unlikely but potential race condition.)

As I mentioned, the great thing about the CSecurityDescriptor class is that you have the source code. If it works for your needs, great! If it doesn't, you have the option of modifying the class. And if you find any bugs, you have the option of fixing them yourself using the knowledge you now have about low-level access control in Windows.

Securing Private Objects

I have mentioned several times that you can secure private objects created by your software by using Windows access control. This feature is a powerful one indeed, and it is particularly likely to be of use to service developers. The DACL building techniques you have learned so far apply to securing private objects as well as securing system objects, so there is little to learn about implementing access control on your own objects.

The tasks involved in securing your objects are shared between your software and the system. Your software must perform the following tasks to implement private security for objects:

  • Your software must define specific rights for your objects (using the low 16 bits of the access mask).
  • Your software must decide which of the standard rights are relevant for your objects.
  • Your software must decide to which standard and specific rights the generic rights map.
  • Your software must associate security descriptors (created by the system) with objects.
  • Your software must store security descriptors with objects in persistent storage if appropriate for your objects.
  • Your software must perform access checks before performing securable actions on secured objects.

The system provides the following features:

  • Functions that create and destroy security descriptors for your objects
  • Functions that get and set specific parts of these security descriptors
  • A function that performs an access check in a manner consistent with other secured objects in Windows

Before beginning our discussion of specific functions, I need to clarify two points:

  • Private object security should be used for service software that is serving client software running in a different security context.
  • Private object security requires your software to use tokens to indicate client security context. Remember that tokens contain a trustee's SID as well as its group SIDs, privileges, and default DACL. (I will discuss the token in greater detail in Chapter 11. You might find it helpful to refer to that chapter while continuing your reading in this chapter.)

When creating an object that is to be secured privately, you should create the object's security descriptor. Call CreatePrivateObjectSecurity to do this:

 BOOL CreatePrivateObjectSecurity(    PSECURITY_DESCRIPTOR psdParentDescriptor,    PSECURITY_DESCRIPTOR psdCreatorDescriptor,    PSECURITY_DESCRIPTOR *ppsdNewDescriptor,    BOOL                 fIsDirectoryObject,    HANDLE               hToken,    PGENERIC_MAPPING     gmGenericMapping); 

The CreatePrivateObjectSecurity function takes an optional parent security descriptor, as well as an optional creator descriptor. Both parameters can be NULL. If neither are provided, CreatePrivateObjectSecurity creates a security descriptor by using the default DACL found in the provided token. The generic rights found in the default DACL are mapped to the object's specific and standard rights using the information found in the passed GENERIC_MAPPING structure. The CreatePrivateObjectSecurity function returns the new security descriptor by assigning its address to the PSECURITY_DESCRIPTOR variable, whose address you passed as the ppsdNewDescriptor parameter. This is a new security descriptor that you should now associate with your privately secured object. When you need to free the memory allocated by this function, you should pass the address of a variable that points to the security descriptor you want to free to DestroyPrivateObjectSecurity.

 BOOL DestroyPrivateObjectSecurity(    PSECURITY_DESCRIPTOR *ppsdObjectDescriptor); 

The GENERIC_MAPPING structure that you initialize and pass to CreatePrivateObjectSecurity is defined as follows:

 typedef struct _GENERIC_MAPPING {     ACCESS_MASK GenericRead;     ACCESS_MASK GenericWrite;     ACCESS_MASK GenericExecute;     ACCESS_MASK GenericAll;  } GENERIC_MAPPING; 

You can fill in this simple structure by setting each member to the appropriate combination of standard and specific rights for each generic right. The system uses this information when building the DACL from the token's default DACL.

NOTE
You should never directly modify the security descriptor returned from CreatePrivateObjectSecurity. Although the system provides functions for you to request specific security information from this descriptor, your software should treat the returned pointer as a black box, as if the security descriptor were stored in system memory.

When the client attempts to act on a secure object in your software, you must first subject the client to an access check. To do this, you pass the rights required to perform the securable task, as well as the security descriptor and the client's token, to the AccessCheck function.

 BOOL AccessCheck(    PSECURITY_DESCRIPTOR pSecurityDescriptor,     HANDLE               hClientToken,    DWORD                dwDesiredAccess,    PGENERIC_MAPPING     gmGenericMapping,    PPRIVILEGE_SET       pPrivilegeSet,    PDWORD               pdwPrivilegeSetLength,    PDWORD               pdwGrantedAccess,    PBOOL                pfAccessStatus); 

You must also include an address of a GENERIC_MAPPING structure for the object, as well as the address of an array of PRIVILEGE_SET structures. The system uses the PRIVILEGE_SET structure to report the privileges that were used to grant the access. The system uses privileges to grant access in several cases, and you should provide a sufficiently large buffer to receive several returned privileges. The PRIVILEGE_SET structure is defined as follows:

 typedef struct _PRIVILEGE_SET {     DWORD               PrivilegeCount;     DWORD               Control;     LUID_AND_ATTRIBUTES Privilege[ANYSIZE_ARRAY];  } PRIVILEGE_SET; 

The ANYSIZE_ARRAY value is defined as 1, and you should create a buffer large enough to receive the relevant privileges. You can also call AccessCheck once to receive the required size of the privilege buffer, and then allocate a buffer of sufficient length.

NOTE
Privileges can be used to grant access when your client requests WRITE_OWNER, READ_CONTROL, WRITE_DAC, or ACCESS_SYSTEM_SECURITY access for an object. If these access rights are not explicitly assigned to your client's trustee accounts, the system checks the client's token for privileges that override the security of the object. An example of such a privilege is SE_TAKE_OWNERSHIP_NAME, which allows the client to set the ownership of any object in the system.

The access mask that your client was granted is returned via the pdwGrantedAccess parameter, and a Boolean value is returned via the pfAccessStatus parameter indicating whether the AccessCheck function succeeded.

The AccessCheck function is the centerpiece when implementing private object security. If your software is secure, and it religiously calls AccessCheck before performing a secure task on an object, Windows takes care of remaining details of deciding when your software's clients are allowed access.

NOTE
You can also create audit events with private security objects by using the AccessCheckAndAuditAlarm function. Auditing will be covered later in this chapter.

At this point, you know how to create and destroy private security, and you also know how and when to call AccessCheck. The only remaining piece of the private object puzzle is how to modify the components of a security descriptor.

To modify the security of a private object, you should first get the component of the security descriptor that you wish to adjust. Typically this is the DACL, but it could be the object's owner or SACL. Use GetPrivateObjectSecurity to do this.

 BOOL GetPrivateObjectSecurity(    PSECURITY_DESCRIPTOR psdObjectDescriptor,    SECURITY_INFORMATION secInfo,    PSECURITY_DESCRIPTOR psdResultantDescriptor,    DWORD                dwDescriptorLength,    PDWORD               pdwReturnLength); 

The GetPrivateObjectSecurity function takes a pointer to the private security object's security descriptor, and returns the requested security descriptor. You use the familiar secInfo parameter to indicate which portions of the object's security descriptor you want to retrieve. You must supply a buffer large enough to contain a security descriptor that will hold the requested information. You can call GetPrivateObjectSecurity once to find the size of the required buffer, and again to retrieve the information.

After you have the security descriptor for the object, you must modify it, and then reset the modified security descriptor to the private object using SetPrivateObjectSecurity.

 BOOL SetPrivateObjectSecurity(    SECURITY_INFORMATION secInfo,    PSECURITY_DESCRIPTOR psdModificationDescriptor,    PSECURITY_DESCRIPTOR *ppsdObjectsSecurityDescriptor,    PGENERIC_MAPPING     gmGenericMapping,     HANDLE               hClientToken); 

The secInfo parameter indicates which information in the "modification security descriptor" is to be set in the object's private security descriptor. You pass the address of a PSECURITY_DESCRIPTOR variable, which contains a pointer to the object's private security descriptor. SetPrivateObjectSecurity frees the security descriptor and replaces it with the new security descriptor, whose address is returned in the provided pointer variable. You must also pass hClientToken so that the system can confirm object owner settings, as well as the generic mapping structure.

The modification of a security descriptor is typically performed using these steps:

  1. Retrieve the object's security descriptor.
  2. Create and initialize a new security descriptor (shown earlier in this chapter).
  3. Copy security information from the original security descriptor to the new security descriptor.
  4. Modify the new security descriptor (using techniques described throughout this chapter).
  5. Set the new security descriptor back to the private object.

To get the individual components of a security descriptor, you should use the following functions: GetSecurityDescriptorOwner, GetSecurityDescriptorDacl, GetSecurityDescriptorSacl, and GetSecurityDescriptorGroup. To set the components in a new security descriptor, you can use SetSecurityDescriptorOwner, SetSecurityDescriptorDacl, SetSecurityDescriptorSacl, and SetSecurityDescriptorGroup. These functions are similar, so I will show and discuss the most common and complex two, GetSecurityDescriptorDacl and SetSecurityDescriptorDacl. Here is GetSecurityDescriptorDacl:

 BOOL GetSecurityDescriptorDacl(    PSECURITY_DESCRIPTOR pSecurityDescriptor,     PBOOL                pfDACLPresent,    PACL                 *pDACL,    PBOOL                pfDACLDefaulted); 

This function retrieves a pointer to the DACL in a security descriptor. It also indicates whether a DACL is present, and whether it was originally created via default security. You supply the security descriptor as well as the address of a Boolean value indicating whether a DACL is present, the address of a PACL variable to retrieve the DACL, and the address of another Boolean value, which the system uses to indicate whether the DACL was defaulted.

To set the DACL back to a security descriptor, you can use SetSecurityDescriptorDacl:

 BOOL SetSecurityDescriptorDacl(    PSECURITY_DESCRIPTOR pSecurityDescriptor,     BOOL                 bDaclPresent,    PACL                 pDacl,    BOOL                 bDaclDefaulted); 

This function simply takes the pointer to a security descriptor object that you wish to modify, and Boolean values indicating whether the DACL is present and whether it is the result of default security (for which you will typically pass FALSE). The SetSecurityDescriptorDacl function requires a pointer to a DACL that becomes the new DACL for the security descriptor.

NOTE
You should never attempt to set the DACL or any other component of a security descriptor that is returned for any object in the system. You should always create a new security descriptor, to which you can set components such as the DACL and SACL. The reason for this is security descriptors returned from the system are packaged in self-relative format. This means that the data comes in a single contiguous block of memory, which leaves no room for modification.

When you allocate memory and initialize a new security descriptor by using the InitializeSecurityDescriptor function, the system initializes a security descriptor that is in absolute format. A security descriptor in absolute format uses pointers to reference its components, allowing the components to be set and reset.

The RoboService Sample Service

The RoboService sample service ("10 RoboService.exe") demonstrates how private security objects and client/server access control are employed using named pipes. The source code and resource files for the application are in the 10-RoboService directory on the companion CD.

When you launch RoboService with the "/install" switch, the application installs itself as a service on the system. (See Figure 10-8.) You can then use any service control program to start and stop the service. Also, if you are executing the service from a debugger, you can pass the "/debug" switch, which causes the service to execute in debug mode, bypassing service functionality.

click to view at full size.

Figure 10-8. The command-line options for RoboService

After the service is running, you can run the RoboClient sample application, shown in Figure 10-9, and enter the computer name of the system running the service. If you don't enter a computer name, the client attempts to connect to the service on the local machine.

Once connected, you will see a list of "virtual robots" created by the service. These robots are secured using private security functionality on the service side. You may add and remove robots. You can perform several defined actions with the robots, including editing robot security.

click to view at full size.

Figure 10-9. User interface for the RoboClient sample application

I suggest you run the service and then launch the client from multiple user contexts, perhaps by using the RunAs.exe utility packaged with Windows. This will allow you to experiment with object ownership and access control.

The client is very thin, and defers nearly all functionality to the service. All security code (except for ACL editing) is implemented on the service side.

The service uses impersonation to obtain a token for each connecting client. (Impersonation is discussed in detail in Chapter 11.) The service then stores the token for use in future securable requests from the client and in robot creation. The service uses CreatePrivateObjectSecurity, DestroyPrivateObjectSecurity, and AccessCheck, as well as many other security-related functions, to implement the service.

In addition to security functionality, the service uses I/O completion ports, discussed in Chapter 2, to communicate efficiently with the client, implementing a model for scalable communication.

Auditing and the SACL

Finally, we're ready to discuss auditing and the SACL. Unlike the DACL, the SACL of an object has no effect on who can access an object. However, when access is requested of an object, the SACL can cause an event to be added to the event log. Specifically, you can add ACEs to an object's SACL, which adds events to the event log under two conditions:

  • When an access check succeeds for any of a set of access rights
  • When an access check fails for any of a set of access rights

For example, you could create a SACL for a file on an NTFS partition that contains a single SYSTEM_AUDIT_ACE. This would indicate that each time a certain trustee failed to write to the file because of denied access, an event should be added to the event log by the system.

Auditing is disabled by default in Windows 2000, so you should enable auditing if you wish to begin writing software that audits. Follow these steps to enable auditing for your system:

  1. Run the Microsoft Management Console (MMC) with the /a switch, mmc /a.
  2. On the Console menu, select Add/Remove Snap-in, and then click the Add button.
  3. In the Add Standalone Snap-in dialog box, select Group Policy, and then click Add.
  4. In the Select Group Policy Object dialog box, the Local Computer object should be selected, so click Finish.
  5. Click the Close button, and then click OK. The Local Computer Policy object should be displayed in the MMC.
  6. Expand the Local Computer Policy object as follows: Local Computer Policy\Computer Configuration\Windows Settings\Security Settings\Local Policies\Audit Policy.
  7. In the right pane, right-click Audit Object Access, and then select Security.
  8. In the Local Security Policy Setting dialog box, check the Success and Failure check boxes as shown in Figure 10-10 and click OK.
  9. click to view at full size.

    Figure 10-10. Enabling success and failure auditing for object access

After you have followed these steps, the system begins adding audit events to the event log under the Security log.

NOTE
A domain policy can override your local policy, preventing auditing from being set. You must have domain administrative rights to adjust domain policy.

Believe it or not, you've already finished the hard work for implementing auditing. You know how to create ACEs and modify ACLs from the work we did with DACLs. Just about everything you have learned so far applies to creating ACEs and SACLs.

Here is the ACE structure used for the SACL:

 typedef struct _SYSTEM_AUDIT_ACE {    ACE_HEADER  Header;     ACCESS_MASK Mask;     DWORD       SidStart;  } SYSTEM_AUDIT_ACE; 

This structure should look familiar, because it is exactly the same as the ACCESS_ALLOWED_ACE and ACCESS_DENIED_ACE structures that we have been discussing. The AceFlags member of the ACE_HEADER structure typically contains inheritance information, which is also true for audit ACEs. But you should also include one or both of the SUCCESSFUL_ACCESS_ACE_FLAG and FAILED_ACCESS_ACE_FLAG audit flags to indicate auditing for successful and failed access, respectively.

To modify SACLs for objects, use the functions that you are already familiar with, such as InitializeAcl and AddAce. Getting and setting the SACL for a system object is still handled using GetSecurityInfo and SetSecurityInfo, in much the same manner you would use them for the DACL. Creating a SACL is somewhat simpler than creating a DACL, because the order of ACEs in a SACL has no effect on the object. You can add ACEs in any order that is convenient for your software.

NOTE
One difference you'll notice in dealing with DACLs is that a trustee cannot set the SACL of an object unless it has the SE_AUDIT_NAME privilege.

Access Control Programming Considerations

You now understand what is possible with access control and how to implement access control in your own software, but you've still got more to learn to become a successful security programmer—it has been argued that the flexibility of Windows access control gives the security developer more than enough rope to hang himself. In this section, I discuss some issues you should consider in your security development. My overall advice to you is to plan the access control of your service. With a little forward planning, you will save yourself a great deal of headache.

Memory Management

Unlike 90 percent of the programming you do at the application level, security programming requires a fair amount of buffer management, including the allocating and moving of structures within memory. Although this is what many developers focused on in school, it is not a terribly common task in Windows development. When dealing with security, you should come up with ways to simplify your memory management because so many functions require the allocation and reallocation of temporary buffers. Here are some tips:

  • Consider using functions such as _alloca that do not require memory to be freed.
  • Consider writing a class such as the CAutoBuf class, which is discussed in Appendix B. (You will also find the sample applications in Appendix B.) This class automatically adjusts its buffer and frees itself when it goes out of scope.
  • Avoid static buffers; they are a lazy programmer's technique and will eventually break your application.
  • Think about the task at hand, and design an approach that requires as few buffer allocations as possible. Place as much of the buffer allocation at the beginning of the algorithm as possible.
  • Use structured exception handling to clean up after yourself.

Code Reuse

The more code you write, the more bugs you write. With access control programming, this is doubly true. Plan ahead and write code that can be used generally throughout your service. Try to design the security of different aspects of your service to be consistent with one another so that you can depend on certain patterns and reuse functions as much as possible. This reuse will greatly ease the task of debugging your security code.

Finally, I can't express enough how advantageous it is to implement your security code in a set of thin C++ classes. Doing so buys you code reuse and encapsulation that can greatly simplify a task such as access control.

Keep It Simple

Simple security is often the best security. If an object has dozens of ACEs, your software and users are probably going to have trouble keeping tabs on who really has access to the object. If an object has only a few simple ACEs, both the software and the users will be clear about who has access. An object that is owned by the system and has only an empty DACL (not a NULL DACL) is as simple and secure as an object gets in Windows. Add a single ACE to allow one trustee access and the object is still secure, and it is clear who has access to the object.

Try to implement as much of your access control as possible using only access-allowed ACEs. Remember that access is denied implicitly unless it is explicitly allowed. If you must use both access-allowed ACEs and access-denied ACEs, allow access to groups and individuals, and deny access to individuals and smaller groups. This way you won't find yourself needing to override a group access-denied ACE for a single user in the group.

Keep it simple to avoid holes in your access control.

Use Default Security and Inherited Security

If at all possible, use default security and inherited security. If your service can avoid manipulating ACLs on individual objects, great! Many services can get by with creating only a single DACL and setting it as the default DACL for the service (discussed in more detail in Chapter 12). An object created by the service can use the default security, avoiding the need to create a DACL for each object created. This approach can greatly simplify a service that must create many secure objects.

NOTE
Remember that an object that inherits security will not use ACEs from the default DACL. You might find that you'll need to protect a parent directory or registry key's security descriptor if you want to use default security on child objects.

Inherited security is another way to get a DACL for free. You can set the DACL on a single parent object in a file structure (or registry tree), and each object you create under that node can inherit a whole DACL's worth of ACEs. This is a very powerful technique, and it can free your code from creating dozens of individual DACLs.

NOTE
Remember that an object that is protected from inheritable ACEs can itself have inheritable ACEs. So you can create a root directory or registry key that is protected from inherited ACEs, and the object's ACEs can define the inherited security for every object below it. This is a common and powerful tactic for implementing inherited security on files and registry keys.



Programming Server-Side Applications for Microsoft Windows 2000
Programming Server-Side Applications for Microsoft Windows 2000 (Microsoft Programming)
ISBN: 0735607532
EAN: 2147483647
Year: 2000
Pages: 126

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