Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Windows 2000 Security Descriptors


Nov00: Windows 2000 Security Descriptors

Marcelo is a developer for MSN Search team at Microsoft. He can be contacted at [email protected].


Absolute versus Self-Relative


If you write applications for Windows (or for any other platform, for that matter), it is critical that you protect all of your objects. If you don't, it's just a matter of time until someone stumbles and creates havoc with your applications and systems. Central to Windows security are Security Descriptors -- structures and associated data that contain the security information for securable objects. Security Descriptors (SD) identify an object's owner and primary group. They also contain a Discretionary Access Control List that controls access to objects, and a System Access Control List that controls the logging of attempts to access objects. In this article, I'll examine how Security Descriptors work on Windows. I'll also present a set of C++ classes (available electronically; see "Resource Center," page 5) that make Security Descriptor programming a straightforward process.

Security Descriptor Backgrounder

Under Windows, Security Identifiers (SIDs) uniquely identify a security principal, which can be a user, group (of other security principals), or computer. The familiar logon name format (MyDomain\ MyUser, for example) is a representation of a SID in a human-readable format.

A Well-Known SID identifies built-in accounts on Windows -- administrators, backup operators, authenticated users, and the like. They are called "well known" because they are hard-coded and identical on all computers. Each Windows computer (server or member server) has its own SID that represents the local system. Consequently, a service running on a computer might have some rights over some objects.

The Windows 2000 Security Descriptor (SD) consists of an Owner, Group, Discretionary Access Control List (DACL), and System Access Control List (SACL). Other bytes on the SD structure save control information (such as the version). The SD can also be in absolute or relative format. (See the accompanying text box "Absolute versus Self-Relative" for more information.)

The most powerful security principal is the Owner. By default, the Owner has the right to change permissions. Even if the Owner doesn't have read permissions on the object the SD is protecting, Owners can still change that and give themselves the permission they want. Owners are represented as a SID on the SD.

The Group (or Primary Group Owner) is the equivalent of an Owner, but for a SID of a Group. Originally introduced for the Windows POSIX subsystem, it has lost importance and Windows now ignores it.

The DACL is the list of rights that identifies the type of an object and who does or doesn't have access to it. The DACL is a table that contains several entries that pair a SID with a right. Multiple rights might be represented on each of these entries, but only one SID. Each of these entries is known as an Access Control Entry (ACE).

The SACL is used for auditing. Each entry doesn't represent a right, but shows what should be logged if somebody tries to do that operation. For example, you might want to log unsuccessful attempts to delete a file, or successful attempts to access a web page so you can build a list of everybody that passed by the site. You need special privileges to change the SACL of an object, and these privileges are usually not granted by default.

The Access Control List structure is straightforward. It contains version information, the size of the entire structure, and the number of ACEs on it. The ACEs on the Access Control List follow immediately after a contiguous block of memory.

Each structure of an ACE might change based on what it is representing. An ACE can be of many types: Allowed, Denied, or Audit (see Listing One for one of the several ACE structures). It stores information about the type, size, access mask, flags, SID, Object-Type, and Inherited-Object-Type. The last two are optional and only exist on Windows 2000. If they are present, the ACE Type is different and is called the "Object-Type ACE." A DACL can only contain Allowed and Denied ACEs, while a SACL can only contain Audit ACEs.

The Access Mask is a set of bits, a DWORD, which represents the access rights. If the Access Mask is zero, nothing is represented and the ACE is useless. The meaning of each bit on the Access Mask might change depending on the object that the SD is protecting. It also might change if it is an Object-Type ACE.

If the ACE is not an Object-Type ACE, the bits between 0 and 15 change their meaning depending on the object that the SD is associated with. Bits 16 to 31 are reserved and represent common actions on objects -- delete, read, write, change the DACL, change the Owner, and so on.

If the ACE is an Object-Type ACE, the meaning of the Access Mask depends on the application that reads it. For the Active Directory (AD), if the Object-Type of the ACE represents a property or a property set, the Access Mask contains flags for read and write rights. If the Object-Type is an extended right, it represents Control over the extended right.

The ACE flags decide if the ACE should be inherited to child objects. For example, you might want to set an ACE in the root of your hard disk and inherit it to all files and directories. It can also be used to block inheritance from the parent object to the current object.

The Object-Type is a GUID and is only used if you are securing objects on the AD. It might also represent other application-specific rights. On the AD, it might be the objectGUID of an object class, a property or a property-set, or the rightsGuid of an extended right. (The forthcoming organizationalUnit example describes the use of an extended right.) You can create your own extended right that only has meaning in your application. For example, assume you make an application that creates records on a database. You can create an extended right called "create-record" and apply it to the database. Windows ignores it because the OS doesn't know what that right represents. When you receive a request from somebody to add a new record, however, you do your own security check and grant or deny the request.

The Inherited-Object-Type of an ACE represents which objectClass inherits this ACE; the AD mostly uses this. For example, instead of applying a right directly to a printer object, you might apply it to the root of your domain and make it inheritable with the ACE flags, and set the Inherited-Object-Type to Printer. This way, any printer under that domain will inherit this ACE.

Here are three examples of different types of ACEs:

  • A file might have an ACE that represents that somebody has the right to read, write, and delete it. Here, you use an Allowed ACE and set the 3 bits on the mask. The ACE flags are zero, and the Object-Type and Inherited-Object-Type are not present.
  • An entry on the AD can have an ACE that represents that somebody has the right to read/write the displayName of a user on the AD. This ACE will have the Object-Type set to the objectGUID in the displayName property, and its flags will be set to Read/Write. These are different read/write flags from the previous example because the ACE has an Object-Type.

  • Finally, an organizationalUnit on the AD might have an ACE that represents people from a group called "Support" having the right to force-change-password for all users under that container, meaning that the next time the user logs on, it has to change his password. To accomplish that, you set the Object-Type to be the rightsGuid of that extended right, and set the Access Mask with the Control bit turned on. You also want to set the ACE flag to inherit to all child objects.

The SD has two special conditions based on the presence or absence of the DACL. If you have an SD that doesn't have a DACL, it means that everybody has full control of the object; it is the infamous "Everybody -- Full Control." On the other hand, if it does have a DACL but it has no ACE, it means nobody has access to the object. You'll need to be the Owner to change the permissions.

Using Security Descriptors

How do you apply an SD to an object? Say that you're creating a file and want to protect this file in the following manner:

  • The Owner and Group are defaulted.

  • The group of users MyAppAdmins and the built-in group Administrators have full control rights.

  • The group of users Accountants has the right to read/write.

  • Users from the group Managers have the right to read.

  • All other users don't have access to that file.

To accomplish these protections, you would use the APIs from Win32 to create the DACL with ACEs to specify the requirements. Because you are using the default Owner and Group, you don't have to specify them or the SACL (empty here). Then you use some functions to apply these objects to a newly created SD. When you do things this way, your SD is in Absolute format. Finally, you apply this SD to the file using SetFileSecurity(); see Listing Two. Listing Two appears to be a lot of code just to apply such a simple security scheme. That's the reason why some application programmers avoid going deeper into security.

There is practically no such thing as an invalid SD. If you add an ACE that does not have an Access Mask, it will work and no error will be issued, but it won't have any effect. Several other schemes are also accepted without error, but there are usually no consequences.

Canonical SD

The Windows security subsystem evaluates ACEs on the DACL from the first to the last and stops when it finds the first SID/right it is looking for. Imagine this scenario:

ACE 1: Everybody allowed to read.

ACE 2: Ross denied to write.

ACE 3: Group 1 (Ross and Rachel) allowed to write.

If Ross attempts to open the file to read it, access is granted at ACE 1 and Windows stops evaluating. If Rachel requests the file for read and write, Windows will evaluate the first ACE and grant read access. But the request was for two rights, so it continues until it finds the third ACE granting write permissions to Rachel. If Ross tries to open the file to write, it fails even if he belongs to Group 1. For instance, if Monica, who is not on the list, tries to open the file to write, it fails because Windows couldn't find any ACE that matches the request.

Now, imagine that you have the aforementioned first and third ACEs already on your DACL, and you add the second ACE at the end:

ACE 1: Everybody allowed to read.

ACE 2: Group 1 (Ross and Rachel) allowed to write.

ACE 3: Ross denied to write.

For this configuration, Ross is granted write access to the file because the second ACE grants it and Windows won't go further. For this reason, Microsoft recommends you sort your DACL in a canonical format. The canonical format has the following order:

1. Noninherited ACEs come before inherited ACEs.

2. Denied ACEs come before Allowed ACEs.

3. ACEs that apply to the entire object come before those that apply to part of the object (just a property for example).

You are not required to create your SD in a canonical format, but the previous example shows why it is important. Also, you cannot edit a noncanonical SD using the Windows interface. It asks you to reset or reorder in a canonical format.

The Classes

When dealing with SD, the first thing you notice is how many pointers, allocations, and deallocations there are to control. It ends up being a full plate for memory leaks and memory corruption bugs. Consequently, nothing is more logical than to encapsulate all this in an object-oriented programming language such as C++.

To handle SD in an easier manner, I created the classes: CSid, CAce, CAcl, and CSD. CAcl uses an STL vector to store the list of ACEs, and CSid, if compiled with _DEBUG defined, uses a wstring to store the logon name of a user. This helps when you need to debug some security problems. Also, a SID and an SD can be in string format, so I used the STL wstring to set and get those objects.

For all classes, I defined copy constructors, assignment operators, and equal/not-equal operators. You can also construct or set each instance with the raw structure of Win32. For example, you can construct (or set) a CAcl with an Access Control List, and call GetAcl() to get a reference of the Access Control List structure, or GetAclCopy() to get a copy of the structure; in this case, you need to use DeleteAcl() to delete the returning pointer.

The CSid class contains most of the functions you'll ever need to handle SIDs. You can populate a CSid five different ways: from another CSid, from a SID structure, from a string format SID, from an Account Name, and from a Well-Known SID; see Example 1. Example 1(e) uses an enumerator type as a parameter. I didn't define all Well-Known SIDs, but the most common are: Administrators, Authenticated Users, Self, Creator, Owner, Everyone, and so on. Also, there are two types that are not Well-Known SIDs, but they could be useful -- WKS_CurrentThreadSid and WKS_CurrentProcessSid. They get the SID from the current thread or process context that the application is running.

The CAce class contains the type of ACE and all the properties as in an ACE structure. The back-pointer to a CAcl structure is used if the CAce is in the list of a CAcl. This way, you can change the CAce structure and it will propagate the modifications to the CAcl. When you change something on CAce, it calls the SetModified function, which verifies whether this CAce belongs to a CAcl. If it does, it will call the CAcl::SetModified.

The CAcl class has two important member variables. One variable is a flag that represents whether an Access Control List should be made present in an SD. If the Access Control List is present, then the second member variable lists all ACEs. This is the STL vector of CAce. You also have a cache to an Access Control List structure that is returned when you call GetAcl(). If you want a copy of the structure, you should use GetAclCopy().

With the CAcl class, you have the CanonicalSort() function that puts an Access Control List into a canonical format. Only the DACL needs to be in canonical format. The CanonicalSort functions calls the sort method of the STL <algorithm> library and it uses the overloaded CCanonicalSort::operator() to specify the sort order.

Also, CAcl contains a back-pointer to the CSD that owns this CAcl. This isn't used for standalone CAcls, but for those that are CSD members. This is because you can change the DACL or SACL on the SD, then they will notify the CSD that something changed and the CSD will be needed to recalculate the raw SD if requested.

With CAcl, I also defined an iterator, const_iterator, begin(), and end() function for each. This way, you can go through the list of CAce, find what you're looking for, or edit the CAce without having the overhead of a function call. This makes the code faster if you constantly need to find a specific CAce; for example, if you're writing your own security check mechanism.

Finally, the CSD class has four important members: two CSids (for the Owner and the Group) and two CAcls (for the DACL and the SACL). It also contains the control flags for the SD, and cache information for SD in Absolute and Relative formats. The functions that retrieve a copy of the SD structure also accept the minimum size that will be allocated for the DACL and the SACL.

The big advantage of CSD is that you can manage your DACL and SACL directly through it without having to create a CAcl structure. I do this by writing functions to add CAce directly through CSD, and it figures out by itself whether the CAce goes to the DACL or the SACL, based on its type.

If you feel an urge to edit the DACL of a CSD, you can call the function Dacl(), which returns a reference. This is not a const, so you can edit the DACL the way you want.

You have to be careful with the pointers returned by CSid, CAcl, and CSD. The pointers have a life equal to the instances that generate them, and they are valid until you call the next nonconst function. If you need to get a permanent pointer, you should call Get*Copy() and use the respective Delete*() function to free it.

For error conditions, I added a per-thread global variable that can be accessed through GetSDLastError() in sderror.h. To set the error code, I call SetSDLastError() (you might want to rewrite this function to log error messages when you are in debug mode). There are plenty of error codes, but you'll likely never need them if you use the classes without doing any hack. Many errors can be a result of Win32 errors, so I named them with _W32 at the end. In such cases, you can call the Windows API GetLastError() to find out why the API failed. It's a good idea to check for errors after you create a new instance of a class passing some pointers, like a PSECURITY_DESCRIPTOR, a PACL, or a PSID.

Recall that Listing Two adds an SD to a newly created file. It takes about 80 lines of code to implement this. Listing Three, however, shows my implementation, which does the same thing in about a dozen lines and in a much more readable format. Be aware that the code presented here is not thread safe because the Microsoft STL implementation is not thread safe. Plus, I'm assuming that, for error conditions, you're going to get the error messages on the thread they were generated upon. It is not even safe to copy a CSD to another CSD and pass it to another thread, because the STL's wstring implementation on Visual C uses reference counting without any locking mechanism. If you need to pass any of these classes to another thread, the safest (and only) way to do it is to get a copy of a raw object, pass this pointer to the other thread, and then reconstruct it.

Conclusion

Security is a complex topic and, to fully understand it on Windows, you should examine Privileges and Access Tokens in addition to Security Descriptors. Privileges are the reason that you sometimes end up getting an "access denied" error, even if your SD says that you can change the permission on an object.

DDJ

Listing One

typedef struct _ACL { 
  BYTE AclRevision; 
  BYTE Sbz1; 
  WORD AclSize; 
  WORD AceCount; 
  WORD Sbz2; 
} ACL;
typedef struct _ACCESS_ALLOWED_OBJECT_ACE {
  ACE_HEADER Header;
  ACCESS_MASK Mask;
  DWORD Flags;
  GUID ObjectType;
  GUID InheritedObjectType;
  DWORD SidStart;
} ACCESS_ALLOWED_OBJECT_ACE;    
typedef struct _ACE_HEADER { 
  BYTE AceType; 
  BYTE AceFlags; 
  WORD AceSize; 
} ACE_HEADER;
typedef DWORD ACCESS_MASK;

Back to Article

Listing Two

BOOL SetMySecurityW32(LPCTSTR lpszFileName)
{
    // Create the SD structure
    SECURITY_DESCRIPTOR sd;
    if(!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION))
        return FALSE;
    SECURITY_DESCRIPTOR_CONTROL ControlBitsOfInterest = 
                                              ALL_CONTROL_BITS_OF_INTEREST;
    SECURITY_DESCRIPTOR_CONTROL Control = 
                          SE_DACL_AUTO_INHERITED | SE_DACL_AUTO_INHERIT_REQ;
    if(!SetSecurityDescriptorControl(&sd, ControlBitsOfInterest, Control)) {
        return FALSE;
    }
    // Create the DACL
    PACL pAcl = NULL;
    SID_NAME_USE snu;
    wchar_t szBuffDomain[256];
    UCHAR   BuffSid[256];
    PSID pSid = (PSID)BuffSid;
    DWORD dwSidSize, dwBuffDomainSize;

    UCHAR   BuffSid1[64];
    UCHAR   BuffSid2[64];
    PSID pCreatorOwnerSid = (PSID)BuffSid1;
    PSID pAdministratorsSid = (PSID)BuffSid2;

    try {
        pAcl = (PACL)new UCHAR[2048]; // should be enough
        if(!pAcl)
            return FALSE;
        if(!InitializeAcl(pAcl, 2048, ACL_REVISION_DS)) {
            goto fail;
        }
        // Add the ACE for "MyAppAdmins" full control
        dwSidSize = dwBuffDomainSize = 256;
        if(LookupAccountName(NULL, L"MyAppAdmins", pSid, &dwSidSize, 
                               szBuffDomain, &dwBuffDomainSize, &snu)) {
            AddAccessAllowedAce(pAcl, ACL_REVISION_DS, FILE_ALL_ACCESS, pSid);
        }       
        // Create the Well-Known SID for "Builtin\Administrators"
        SID_IDENTIFIER_AUTHORITY siaNTAuth = SECURITY_NT_AUTHORITY;
        InitializeSid(pAdministratorsSid, &siaNTAuth, 2);
        *(GetSidSubAuthority(pAdministratorsSid, 0)) = 
                                           SECURITY_BUILTIN_DOMAIN_RID;
        *(GetSidSubAuthority(pAdministratorsSid, 1)) = 
                                           DOMAIN_ALIAS_RID_ADMINS;
        // Add the ACE for "BUILTIN\Administrators"
        AddAccessAllowedAce(pAcl, ACL_REVISION_DS, 
                                     FILE_ALL_ACCESS, pAdministratorsSid);
        // Add the ACE for "Accountants" Read/Write
        dwSidSize = dwBuffDomainSize = 256;
        if(LookupAccountName(NULL, L"Accountants", pSid, &dwSidSize, 
                                 szBuffDomain, &dwBuffDomainSize, &snu)) {
            AddAccessAllowedAce(pAcl, ACL_REVISION_DS, GENERIC_READ | 
                                                     GENERIC_WRITE, pSid);
        }       
        // Add the ACE for "Managers" Read-only
        dwSidSize = dwBuffDomainSize = 256;
        if(LookupAccountName(NULL, L"Managers", pSid, &dwSidSize, 
                                   szBuffDomain, &dwBuffDomainSize, &snu)) {
            AddAccessAllowedAce(pAcl, ACL_REVISION_DS, GENERIC_READ, pSid);
        }
        // Apply the DACL to the SD
        if(!SetSecurityDescriptorDacl(&sd, TRUE, pAcl, FALSE))
            goto fail;
        if(!SetFileSecurity(lpszFileName, DACL_SECURITY_INFORMATION, &sd))
            goto fail;
        delete pAcl;
    } catch(...) {
        delete pAcl;
        throw;
    }
    return TRUE;
fail:
    delete pAcl;
    return FALSE;
}

Back to Article

Listing Three

CSid Sid;
CSD sd;
sd.SetControl(SE_DACL_AUTO_INHERITED | SE_DACL_AUTO_INHERIT_REQ);
sd.AddAccessAllowed(CSid(WKS_Administrators), FILE_ALL_ACCESS);

Sid.SetSidFromName(L"MyAppAdmins");
sd.AddAccessAllowed(Sid, FILE_ALL_ACCESS);

Sid.SetSidFromName(L"Accountants");
sd.AddAccessAllowed(Sid, GENERIC_READ | GENERIC_WRITE);

Sid.SetSidFromName(L"Managers");
sd.AddAccessAllowed(Sid, GENERIC_READ);

if(sd.SetFileSecurity(lpszFileName, DACL_SECURITY_INFORMATION))
    return TRUE;

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.