.NET Security Workshop
Home About Workshops Articles Writing Talks Books Contact

9. Access Control

NT Security is based on Access Control Lists (ACL). You saw how this worked earlier in the workshop. NT defines many types of securable objects: files and directories, named and anonymous pipes, processes, threads, file mapping objects, window stations and desktops, registry keys, Windows services, printers, network shares, job objects and directory service objects. In addition all inter-process synchronization objects (events, mutexes, semaphores, waitable timers) are also securable objects. A securable object has a security descriptor, which contain information about the security applied to the object.

Let's look at what happens when you access a secure object, starting at the beginning. When you log on to your machine your account is authenticated to determine if the password corresponds to the account and therefore you are who you say you are. The system gives you an access token and every process that you execute (including Windows Explorer that provides the Windows desktop) will have access to this token as the primary token. This token is a bit like a badge that you'll get to access a building: it has security information on it that can be checked by the security API. Amongst this information are: a binary value that is your account's security identifier (SID), the SIDs for the groups that you are a member of, and a list of privileges that your account has.

When a thread accesses a secure object, or performs some administrative task that requires privileges, the system checks the process's primary token. If your account has sufficient privileges a thread in your process can impersonate another user and that thread will get an impersonation token which will be used for the access check when the thread accesses a secured object.

Each secure object has a security descriptor. This contains information supplied by the creator of the object, or by the system. If the creator of the object does not provide a security descriptor (a null SD) then the system will provide a default SD. The security descriptor has information about the owner of the object and two lists: the discretionary access control list (DACL) and the system access control list (SACL). The DACL determines who is allowed to access the object and who is not allowed access. The SACL has a confusing name, because it is used to determine how auditing is carried out on accesses to the object. Each ACL holds a list of access control entries (ACEs) Each ACE contains a set of access rights and contains the SID of a trustee for whom the rights are allowed, denied or audited. A trustee is a user account, a group of user accounts or a logon session.

When a thread accesses a secured object it will do so requesting specific rights. An access check will be performed to determine if the SID in the access token attached to the process (or the impersonation token on the thread) matches an ACE with the specified rights. More than one ACE in the DACL could match the access token, because ACEs can allow access and deny access, and ACEs can match a user and the groups that the user is a member of. The access check will involve checking all ACEs sequentially until one matches the access token and the requested rights; at this point if the ACE allows access then the thread will have access to the object, but if the ACE denies access then the thread will not have access. Deny access ACEs are important if you have ACEs that grant access to groups because it means that you can selectively remove the right for a particular user. To do this, the deny ACE for the user must be checked before the allow access ACE for the group, thus the order of ACEs in the DACL is important.

ACEs will contain the rights that are allowed or denied (or will be audited). There are three types of rights: specific rights which apply to a certain type of object, like reading from a file or being able to terminate a process; standard rights are things that can apply to all objects like the ability to read the DACL or change the object's owner; finally, generic rights are combinations of standard and specific rights. Note that to read the security descriptor (and hence the DACL and ACEs) your account must have the READ_CONTROL right. Generic rights may mean different things depending on the object they are applied to. For example, GENERIC_READ for a file is mapped to the standard rights READ_CONTROL and SYNCHRONIZE and the specific rights to read file data, directories and file and directory attributes.

Access rights are represented by a 32-bit value: 16 bits for object specific rights, eight bits for standard rights and five bits for the generic rights; this means that a right in an ACE may contain all three types of rights, even though some of the bits may be redundant. In addition, DACL and SACL rights are different bits so it is possible to put SACL rights on an ACE in a DACL even though it makes no sense and is invalid. Handling SACLs is a privileged action: a thread can only read or write the SACL if its corresponding access token has the SE_SECURITY_NAME privilege.

Some objects can be contained by another secure object, for example, both files and folders are secure objects and a file is contained in a folder. A contained object will inherit the ACEs from the container that have been marked as inheritable by that object. Inheritance is complicated by two behaviours: how inheritance is propagated and what it applies to. If the secured object is a container it means that it can contain other containers or non-container objects. An ACE can be marked as being inherited by child containers, by child objects, by both or not inherited at all. Further, if the ACE can be inherited it can be marked as to how the inheritance is propagated: the ACE can apply to itself and be propagated to children and the rest of the inheritance chain; or it can be propagated to children and the rest of the inheritance chain but not to the object itself; or it can be applied to the object and its children but not to grandchildren.

When you use the Windows API to create an SD for a secured object, then Windows will obtain and merge the inheritable ACEs from the container into the DACL you create. The important thing is that if you use the Windows API to build ACLs the APIs will ensure that the ACEs are ordered. (There are some low level API in the Windows API that allow you to determine the order yourself, but you should avoid them. If you use the .NET access control classes then you will get ACE ordering.) The first ACEs will be the direct ACE from the object and then these will be followed by the ACEs that have been inherited from the parent, and then by the ones inherited by the grandparent and so on. Within these groups the denied ACEs will be listed before the access allowed ACEs. In this way a denied ACE will occur before the allowed ACE for a group. Note that this means that a direct ACE will take precedence over an inherited ACE.

An entire ACL (the DACL or the SACL) can be marked as protected. This acts as a 'block' because the DACL will not inherit from its parent, nor will it allow inheritance to propagate through it.

As mentioned previously, if you create a secured object with a null-SD then the system will create a SD for the object. The DACL in this SD will contain ACEs inherited from the object's container, and if there are no inheritable ACEs the system will use the default DACL in the primary or the impersonation token. If there is no default DACL then a null DACL is created. This is unlikely to happen because the primary or impersonation token will have a default DACL that will give access to the creator of the object and to the system.

A null DACL is not a DACL at all, it is the absence of a DACL, and it gives everyone access to the object. If there is a DACL then checks will be performed against ACEs in the DACL and access will only be given if an ACE in the DACL gives access (and there are no ACEs that deny the access). This implies that if the DACL has no ACEs then no one can have access (this is called an empty DACL). Thus although they have similar names these two types of DACL have completely opposite actions: a null DACL allows everyone all access to the object; an empty DACL prevents access to everyone.

The v1.1 version of the .NET framework provides wrappers for many secured objects (files, threads, mutexes and events are just four). Some of these are intended only for intra-process use (events, threads) but others are created for inter-process use (files, mutexes). In v1.1 these objects are created with null-SD (you can confirm this by perusing FileStream's constructors with Reflector or searching through SSCLI for the AutoResetEventNative, ManualResetEventNative and MutexNative classes). This means that these objects are created wit the default DACL. v1.1 has no mechanism to allow you to specify a different DACL.

version 3.0/2.0 of the framework is ACL aware, not only are there classes to allow you to manipulate ACLs, but many of the existing classes have been extended to have methods that allow you to inspect an existing object's DACL and to specify a DACL for a new object. This page describes the new access control API in .NET version 3.0/2.0, if you are using an earlier version of the framework then you should ignore this page and move to the next page.

9.1 Overview of .NET Access Control

The System.Security.AccessControl and System.Security.Principal namespaces contain the classes for accessing NT access control. To be honest these classes are a bit of a mess and appear more like the first attempt of someone designing an object hierarchy. Class inheritance can be extremely powerful, however, it must be used sparingly, and it is a pity that the designer of the classes in System.Security.AccessControl wasn't told this. This namespace has overly complicated class hierarchies with intermediate base classes that serve no purpose. If these intermediary classes were removed then the other classes would continue to work as before: this is a sure sign that the hierarchy has been over-engineered.

Let's start at security descriptors. There are two hierarchies of classes to consider. If you want to access the security on an object type that has a wrapper in the framework, then you will consider the hierarchy of classes with ObjectSecurity as its head; if you want to access the security descriptor on an object where there is no .NET wrapper then you will consider the hierarchy with GenericSecurityDescriptor at its head; There is no direct public relationship between GenericSecurityDescriptor and ObjectSecurity even though they both represent the same physical feature, a security descriptor. Confused yet? Well, let's look at the implementation of these classes and the classes derived from them.

The abstract class GenericSecurityDescriptor has properties to give read/write access to the main members of the security descriptor: the owner and group. However, it has methods to return the binary data or the security string of the entire security descriptor. (Windows has a language called the security descriptor definition language - SDDL - that represents a security descriptor as a sequence of characters, this is covered in the next section). The two sealed classes derived from GenericSecurityDescriptor are CommonSecurityDescriptor and RawSecurityDescriptor. Both classes have properties that hold the discretionary and system ACLs, but the ACLs for CommonSecurityDescriptor are of the type DiscretionaryAcl class and SystemAcl class whereas both ACLs in the RawSecurityDescriptor class are RawlAcl. In addition, the CommonSecurityDescriptor class has methods that allow you to remove all ACEs in the DACL or the SACL, and to apply protection to the DACL or SACL. This design makes sense: the properties allows you to access the constituent parts of the security descriptor, the methods allow you to act upon those parts as a whole. Incidentally, there is an abstract GenericAcl class that gives access to an enumerator which returns the ACEs in the ACL as GenericAce objects. The DiscretionaryAcl class has methods that allow you to add or remove ACEs from the ACL by providing detailed information about the ACE. In contrast RawAcl provides methods to add ACEs wrapped as GenericAce objects or remove them by index. Its getting complicated, isn't it? Well, it gets more complicated.

If you obtain the security descriptor for an existing secure object then you'll get an object of a class derived from ObjectSecurity. This class has methods to give access to the members of the security descriptor. The abstract class ObjectSecurity allows you to persist a security descriptor, give access to its binary form or the SDDL string. It also allows you to add, remove or modify access and audit ACEs through generic methods. For example ModifyAccess will allow you to provide an AccessRule object and a AccessControlModification enumeration to specify what the method should do with the object: add the ACE to the DACL, remove the ACE that matches the AccessRule, remove ACEs that matches the SID and access mask of the AccessRule or removes the ACEs that just match the SID. The one thing that ModifyAccess will not allow you to do is modify an ACE. To do this you must remove the ACE that you want to change and then insert the modified copy. You can also use this method to remove all ACEs from the ACL and add the specified AccessRule or remove all ACEs that matches its SID and then add the AccessRule. If you want to create an AccessRule object (or an AuditRule object) then you can use the AccessRuleFactory (AuditRuleFactory) method with the specific details of the SID, access mask, inheritance and propagation information.

ObjectSecurity, of course, is abstract. There are two derived classes, both abstract, CommonObjectSecurity and DirectoryObjectSecurity. CommonObjectSecurity adds methods to add, modify and remove ACEs individually, again, using AccessRule and AuditRule objects. DirectoryObjectSecurity adds similar methods, but they take objects of the abstract type ObjectAccessRule (ObjectAuditRule) and an AccessRuleFactory (AuditRuleFactory) to create the ACE rule objects. ActiveDirectorySecurity is the concrete class derived from DirectoryObjectSecurity, unlike the other security descriptor classes, this class is in the System.DirectoryServices namespace. It adds overloads of the methods to add, modify and remove ACEs but this time using the specific ActiveDirectoryAccessRule (ActiveDirectoryAuditRule) objects. Luckily, that is as far as that branch of the class hierarchy goes, but the other branch, through CommonObjectSecurity, goes much further.

The derived class is the abstract NativeObjectSecurity which implements the Persist method. This then acts as the base class for the concrete classes that are security descriptors associated with cryptographic service providers, events, mutexes, semaphores and registry keys. Each provides implementations of the methods to add, remove or modify ACEs using objects of classes derived from AccessRule or AuditRule (specific to the security descriptor type) and an AccessRuleFactory and an AuditRuleFactory to create the ACE object. The class hierarchy goes one level further, there is an abstract child class of NativeObjectSecurity called FileSecurity with two concrete child classes: DirectorySecurity and FileSecurity.

The following table lists the classes that are wrappers around Win32 secured objects. These classes all have a GetAccessControl method that returns information about the security descriptor attached to the object, a SetAccessControl method to change the security descriptor on an existing object and constructors (or other creation methods) that allow you to specify the security descriptor that will be used for a new object. The table lists the security descriptor class (derived from NativeObjectSecurity) for the secured object.

Class Description Security Descriptor Class
File, FileInfo, FileStream Wrappers around Win32 files FileSecurity
Directory, DirectoryInfo Wrappers around Win32 folders DirectorySecurity
EventWaitHandle Wrapper around Win32 event synchronization objects EventWaitHandleSecurity
Mutex Wrapper aroundWin32 mutex synchronization objects MutexSecurity
Semaphore Wrapper around Win32 semaphore synchronization objects SemaphoreSecurity
RegistryKey Wrapper around Win32 registry key RegistrySecurity
CspParameters Provides information for a Crypto Service Provider CryptoKeySecurity
DirectoryEntry A node in the active directory ActiveDirectorySecurity

Frankly this design is not particularly OO, while I accept that different secured objects will have different ACEs the rest of the Security Descriptor is the same for every secured object. Thus, a generic security descriptor class could be used (for example CommonObjectSecurity) and polymorphic ACE objects (each implementing a generic interface) could be added to a generic SD object. The more that you look into the design of the access control classes the more apparent it appears that they are heavily over-engineered.

So do the GenericSecurityDescriptor classes have anything that the ObjectSecurity do not have? Well, not really. Both class hierarchies allow you to access and change the owner and group and edit and modify the ACLs in the security descriptor. Both class hierarchies have methods to initialise from binary data or SDDL string, or be exported to binary data or SDDL string. You can check to see if the ACEs in the DACL are ordered using ObjectSecurity.AreAccessRulesCanonical or CommonSecurityDescriptor.IsDiscretionaryAclCanonical, and corresponding methods for the SACL. In addition, both classes can indicate that the DACL (or SACL) is protected, that is, it does not inherit ACEs. To set protection on a DACL you use ObjectSecurity.SetAccessRuleProtection or CommonSecurityDescriptor.SetDiscretionaryAclProtection and to determine if an ACL is protected you call ObjectSecurity.AreAccessRulesProtected or test the value of the CommonSecurityDescriptor.ControlFlags property.

As a further example of the over-engineering of the hierarchy, take a look at how access control entries are represented. GenericSecurityDescriptor and its children hold their ACLs in objects of GenericAcl and its children. These classes are containers of ACEs with instances of GenericAce and its children. Compare this with ObjectSecurity and its children represent which holds ACEs in objects of AuthorizationRule and its children (AccessRule and AuditRule). Clearly there are two conflicting object hierarchies in the same namespace. Are there any advantages with the GenericAce classes over the AuthorizationRule classes? No, the two hierarchies are equivalent and can be considered as being mere alternatives. This just shows that Microsoft is losing its grip over the design of the .NET framework.

The AuthorizationRule hierarchy of classes is far flatter than the GenericAce hierarchy of classes. First, the hierarchy for the rules classes:

To me, this makes sense, the AuthorizationRule class has properties common to all rules (including common rights), the abstract AccessRule and abstract AuditRule differentiate between access and audit ACEs and the children of these have rights specific to the secured object. The GenericAce hierarchy makes little sense to me:

Such a complicated class hierarchy would be useful if objects of these classes were actually used, but they are not. The .NET access control classes use either GenericAce or the two most derived classes, CommonAce and ObjectAce.

Finally, let's take a look at the classes that represents SIDs. These were introduced on the last page. IdentityReference is an abstract class and the two most important abstract members are the property Value, which has a string representation of the SID, and Translate, which will convert one child class to another sibling class, to specify the type to convert to you pass the Type object of the class as the parameter of Translate. There are just two concrete classes derived from IdentityReference: NTAccount and SecurityIdentifier. As far as I can tell this is the only place where a pattern like this occurs. It is a very odd design because it means that NTAccount must know about SecurityIdentifier and vice versa. In my opinion, the information about a SID should be held in a protected generic form in IdentityReference and hence would be accessible by both NTAccount and SecurityIdentifier. These two classes should have had a constructor that took a IdentityReference (or an explicit conversion operator) so that they could be constructed from the base class object. It is an OO principal that classes are dependent upon their base class and not dependent upon their sibling classes.

Let's just think about the difference between NTAccount and SecurityIdentifier. NTAccount returns a human readable string for the account from the Value property in the form authority\account. SecurityIdentifier returns a string version of the SID in a numeric form from the Value property. Converting between the two forms in Win32 is carried out using the three APIs LookupAccountName (to get the NT account name from a binary SID), LookupAccountSid (to get a binary SID from an NT account name), and ConvertSidToStringSid (to convert a binary SID into a numeric string representation). There is no rocket science involved. It would have been quite simple for IdentityReference to have two methods GetAccountName and GetSidString that provided these conversions. Instead, the classes in System.Security.Principal make what should be a simple action into something overly complicated.

9.2 Security Descriptor Definition Language

Security Descriptors contain information about the owner and group, it has an ACL for access checks and an ACL for audits. In addition, the ACLs contain ACEs that associates a collection of rights to a user. ACEs can be inherited, but the security descriptor will give propagation rules as to how the ACEs are inherited. Finally a security descriptor can be protected so that it does not inherit ACEs from its parent. Clearly this is a complicated structure containing many flags and a variable number of items (ACEs in particular). If a secured object is persisted then its security descriptors must be persisted too, however, this is complicated by the fact that some of the Win32 APIs take absolute security descriptors (which cannot be persisted) whereas others take self-relative security descriptors (which can be persisted). Windows 2000 introduced the security descriptor definition language (SDDL). This is a text based language that represents the various components of the security descriptor using human readable characters. Indeed, the intention is that administrators should be able to read and edit SDDL strings. Although this is possible, it would take a strange individual to actually want to read SDDL strings. The .NET classes for security descriptors all support SDDL strings, so it is important to understand their structure.

A security descriptor has an owner, a group,  the DACL and the SACL; in SDDL these are represented by substrings that are prefixed with O:, G:, D: and S: respectively. The owner and group are represent wither by a SID in numeric form, or a two letter string for a well-known SID. After the owner and group have been defined there will be letters that specify the flags for the security descriptor. This is then followed by the DACL and the SACL, and each will be a ACL made up of ACE substrings. An ACE is identified by a pair of parenthesis (round brackets) and six items separated by semicolons separated. These items are the ACE type, inheritance flags, the access mask, a GUID if the ACE is for an object, an inherited GUID, and the user SID or well-known SID identifier. If an item is missing then its semicolon must be given so that all ACE strings must contain five semicolons.

For example, on my machine I created a file and printed out the SDDL for its security descriptor:

O:S-1-5-21-436374069-1993962763-1060284298-1003G:S-1-5-21-436374069-1993962763-1060284298-513D:AI(A;ID;FA;;;BA)(A;ID;FA;;;SY)(A;ID;FA;;;S-1-5-21-436374069-1993962763-1060284298-1003)(A;ID;0x1200a9;;;BU)

This string can be split in this way:

Owner: S-1-5-21-436374069-1993962763-1060284298-1003
Group: S-1-5-21-436374069-1993962763-1060284298-513
DACL: AI(A;ID;FA;;;BA)(A;ID;FA;;;SY)(A;ID;FA;;;S-1-5-21-436374069-1993962763-1060284298-1003)(A;ID;0x1200a9;;;BU)

The owner is the numeric string for the SID of my account (note that the last part, the RID, is 1003) and the group is MARS\None (where MARS is my machine). The DACL has AI which means that its rights are automatically inherited. There is no SACL. The DACL is:

(A;ID;FA;;;BA)
(A;ID;FA;;;SY)
(A;ID;FA;;;S-1-5-21-436374069-1993962763-1060284298-1003)
(A;ID;0x1200a9;;;BU)

In all of these the ACE type is A which means an access ACE, and specifies who will have the specified access. Similarly all ACEs here have ID for the inheritance flags which means they are automatically inherited. Three of the ACEs have FA for access (file all access), but the last ACE has 0x1200a9. This illustrates that if there is not an SDDL string for the access then the numeric value of the access will be given. These ACEs do not have an object GUID nor an inherited GUID. Three of the ACEs have well-known SIDs and the fourth is the numeric string for my account. BA is BUILTIN\Administrators, SY is NT AUTHORITY\SYSTEM and BU is BUILTIN\Users. Thus, the DACL is:

BUILTIN\Administrators; FullControl
NT AUTHORITY\SYSTEM; FullControl
MARS\RichardGrimes; FullControl
BUILTIN\Users mask; ReadAndExecute, Synchronize

As you can see, it is fairly easy to decypher an SDDL string, but you really would not want to do this all the time. If you really do enjoy reading such strings then I heartily recommend that you look at the inf files in

%windir%\security\templates

which contains the information files used during the initial setup of your machine.

9.3 Reading Security Information

The .NET framework provides wrappers around Win32 secured objects, these were listed in the table given earlier. These classes all have a GetAccessControl method that returns information about the security descriptor attached to the object, a SetAccessControl method to change the security descriptor on an existing object and constructors that allow you to specify the security descriptor that will be used for a new object.

Reading the security information of an existing object is straightforward, but bear in mind that the account that you use must have the appropriate rights to be able to access the various parts of the security descriptor.

Create the source file for a process fileReadSD.cs which has the following:

using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;

class App
{
   static void Main(string[] args)
   {
      if (args.Length < 1) return;
      Console.WriteLine("for {0}:", args[0]);
      FileInfo fi = new FileInfo(args[0]);
      FileSecurity sd = fi.GetAccessControl();
      NTAccount owner = (NTAccount)sd.GetOwner(typeof(NTAccount));
      Console.WriteLine("Owner is: {0}", owner.ToString());
      NTAccount group = (NTAccount)sd.GetGroup(typeof(NTAccount));
      Console.WriteLine("Primary group is: {0}", group.ToString());
   }
}

There are two overloads for GetAccessControl. This version requests access to the entire security descriptor. You may find that you won't have the right to access some of the parts of the security descriptor (in particular the SACL). If this is the case then you can call the other overload which takes an AccessControlSections value. (This enumerator has the [Flags] attribute and so you can combine various values.) You can use this parameter to determine the parts of the security descriptor that you want to access, omitting the parts that you do not have access to.

Compile this and run it on itself:

csc fileReadSD.cs
fileReadSD fileReadSD.cs

This will give you a result looking something like this:

for fileReadSD.cs:
Owner is: MARS\RichardGrimes
Primary group is: MARS\None

The GetAccessControl method on the FileInfo object will return a FileSecurity object, notice that this is specifically for Win32 file objects because the class used to describe an ACE is a FileSystemAccessRule and this uses the FileSystemRights enumeration to specify the rights allowed or denied by the ACE (and similarly for the audit ACEs). FileSecurity cannot be used for an event or a registry key.

To get the ACEs for the DACL you call GetAccessRules. This is a curious method. It takes three parameters, the first two are bool and indicate whether to include the ACEs that were declared explicitly on the object and whether to include the ACEs inherited. The third parameter is a Type object and indicates the type of IdentityReference that will be used to identify the account that the rights apply to. (Note that the documentation says that the class should be derived from SecurityIdentifier, which is not correct.) This method will return an AuthorizationRuleCollection object from which you can get an enumerator. As the name suggests, this enumerator will return AuthorizationRule items which are specific to the secured object (in this case FileSystemAccessRule), so you can implicitly cast to this. To test this out, add the following to the code:

Console.WriteLine("Access Rules:");
foreach (FileSystemAccessRule rule in sd.GetAccessRules(true, true, typeof(NTAccount)))
{
   Console.WriteLine("\t{0} user: {1} rights: {2}",
      rule.AccessControlType,
      rule.IdentityReference,
      rule.FileSystemRights);
   Console.WriteLine("\t\t{0}, inheritance: {1} propagation: {2}",
      rule.IsInherited ? "inherited right" : "direct right",
      rule.InheritanceFlags, rule.PropagationFlags);
}

Here the code specifies that the FileSystemAccessRule should identify the user through an NTAccount object. Each ACE is printed on the console, giving the access type (Allow or Deny), the user and the rights. On a second line is information about the inheritance: is the ACE inherited (IsInherited), is it marked as being inherited by container or child objects (InheritanceFlags) and if it can be inherited, how will this be propagated to children (PropagationFlags). Compile and run this code. You will now be able to see a more detailed display of the file's security.

I mentioned earlier that there are two types of security descriptor classes, but that the .NET wrapper classes around Win32 secure objects will only return objects from the ObjectSecurity hierarchy. However, it is possible to convert between the two types. ObjectSecurity provides two classes to serialize the security descriptor: GetSecurityDescriptorBinaryForm and GetSecurityDescriptorSddlForm and RawSecurityDescriptor (from the GenericSecurityDescriptor hierarchy) has constructors that will take either of these.

Add the following code:

Console.WriteLine("\n{0}\n",
   sd.GetSecurityDescriptorSddlForm(AccessControlSections.All));

byte[] sdBytes = sd.GetSecurityDescriptorBinaryForm();
CommonSecurityDescriptor csd = new CommonSecurityDescriptor(false, false, sdBytes, 0);
Console.WriteLine("Owner is: {0}", csd.Owner.Translate(typeof(NTAccount)));
Console.WriteLine("Primary group is: {0}", csd.Group.Translate(typeof(NTAccount)));
Console.WriteLine("Access Rules:");
foreach (CommonAce ace in csd.DiscretionaryAcl)
{
   Console.WriteLine("\t{0} user: {1} rights: {2:x8}",
      ace.AceType,
      ace.SecurityIdentifier.Translate(typeof(NTAccount)),
      ace.AccessMask);
   Console.WriteLine("\t\t{0}, inheritance: {1} propagation: {2}",
      ace.IsInherited ? "inherited right" : "direct right",
      ace.InheritanceFlags, ace.PropagationFlags);
}

By default, the SIDs will be held as SecurityIdentifer and so to get a human readable account this code calls Translate(typeof(NTAccount)). Compile and run this. You'll see that the same information is displayed for the two security descriptor classes although the format will appear in a slightly different form:

for fileReadSD.cs:
Owner is: MARS\RichardGrimes
Primary group is: MARS\None
Access Rules:
   Allow user: BUILTIN\Administrators rights: FullControl
      inherited right, inheritance: None propagation: None
   Allow user: NT AUTHORITY\SYSTEM rights: FullControl
      inherited right, inheritance: None propagation: None
   Allow user: MARS\RichardGrimes rights: FullControl
      inherited right, inheritance: None propagation: None
   Allow user: BUILTIN\Users rights: ReadAndExecute, Synchronize
      inherited right, inheritance: None propagation: None

O:S-1-5-21-436374069-1993962763-1060284298-1003G:S-1-5-21-436374069-1993962763-1060284298-513D:AI(A;ID;FA;;;BA)(A;ID;FA;;;SY)(A;ID;FA;;;S-1-5-21-436374069-1993962763-1060284298-1003)(A;ID;0x1200a9;;;BU)

Owner is: MARS\RichardGrimes
Primary group is: MARS\None
Access Rules:
   AccessAllowed user: BUILTIN\Administrators rights: 001f01ff
      inherited right, inheritance: None propagation: None
   AccessAllowed user: NT AUTHORITY\SYSTEM rights: 001f01ff
      inherited right, inheritance: None propagation: None
   AccessAllowed user: MARS\RichardGrimes rights: 001f01ff
      inherited right, inheritance: None propagation: None
   AccessAllowed user: BUILTIN\Users rights: 001200a9
      inherited right, inheritance: None propagation: None

The main differences are that the rights are returned as an integer, but this is understandable because just one class (CommonAce) is used to represent the ACE for all of the main secured object types.

Microsoft's response about the duality of ObjectSecurity and GenericSecurityDescriptor is that ObjectSecurity and the Rule classes give a friendlier access to an object's security while GenericSecurityDescriptor and the Ace classes are for the hardcore security developer who wants to have advanced access to the security descriptor. However, as you have seen, there is very little difference between the two. However, it is worth pointing out that ObjectSecurity class is implemented using CommonSecurityDescriptor using containment not inheritance.

So that we can use this process later, copy the code to a new file (copy fileReadSD.cs getSecurity.cs), then in the new code remove the code that you added last. Add some code that detects if the parameter is for a file or a folder (highlighted below), the code should look like this:

using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;

class App
{
   static void Main(string[] args)
   {
      if (args.Length < 1) return;
      Console.WriteLine("for {0}:", args[0]);
      FileSystemSecurity sd = null;
      if (File.Exists(args[0]))
      {
         FileInfo fi = new FileInfo(args[0]);
         sd = fi.GetAccessControl();
      }
      else if (Directory.Exists(args[0]))
      {
         DirectoryInfo di = new DirectoryInfo(args[0]);
         sd = di.GetAccessControl();
      }
      else
      {
         Console.WriteLine("file or folder {0} does not exist", args[0]);
         return;
      }
      NTAccount owner = (NTAccount)sd.GetOwner(typeof(NTAccount));
      Console.WriteLine("Owner is: {0}", owner.ToString());
      NTAccount group = (NTAccount)sd.GetGroup(typeof(NTAccount));
      Console.WriteLine("Primary group is: {0}", group.ToString());
      Console.WriteLine("Access Rules:");
      foreach (FileSystemAccessRule rule
         in sd.GetAccessRules(true, true, typeof(NTAccount)))
      {
         Console.WriteLine("\t{0} user: {1} rights: {2} [{2:x}]",
            rule.AccessControlType.ToString(),
            rule.IdentityReference.ToString(),
            rule.FileSystemRights);
         Console.WriteLine("\t\t{0}, inheritance: {1} propagation: {2}",
            rule.IsInherited ? "inherited right" : "direct right",
            rule.InheritanceFlags, rule.PropagationFlags);
      }
   }
}

Compile this code (csc getSecurity.cs) and try it out on a few files and folders. For example, the root of my drive gives this:

For c:\:
Owner is: BUILTIN\Administrators
Primary group is: BUILTIN\Administrators
Access Rules:
   Allow user: Everyone rights: ReadAndExecute, Synchronize [001200A9]
      direct right, inheritance: None propagation: None
   Allow user: CREATOR OWNER rights: 268435456 [10000000]
      direct right, inheritance: ContainerInherit, ObjectInherit propagation: InheritOnly
   Allow user: NT AUTHORITY\SYSTEM rights: FullControl [001F01FF]
      direct right, inheritance: None propagation: None
   Allow user: NT AUTHORITY\SYSTEM rights: 268435456 [10000000]
      direct right, inheritance: ContainerInherit, ObjectInherit propagation: InheritOnly
   Allow user: BUILTIN\Administrators rights: 268435456 [10000000]
      direct right, inheritance: ContainerInherit, ObjectInherit propagation: InheritOnly
   Allow user: BUILTIN\Administrators rights: FullControl [001F01FF]
      direct right, inheritance: None propagation: None
   Allow user: BUILTIN\Users rights: -1610612736 [A0000000]
      direct right, inheritance: ContainerInherit, ObjectInherit propagation: InheritOnly
   Allow user: BUILTIN\Users rights: AppendData [00000004]
      direct right, inheritance: ContainerInherit propagation: None
   Allow user: BUILTIN\Users rights: CreateFiles [00000002]
      direct right, inheritance: ContainerInherit propagation: InheritOnly
   Allow user: BUILTIN\Users rights: ReadAndExecute, Synchronize [001200A9]
      direct right, inheritance: None propagation: None

0x10000000 is GENERIC_ALL, 0xA0000000 is GENERIC_READ | GENERIC_WRITE. Since these are applied to files, the system will treat the GENERIC_READ constant as all read operations on files (that is, FILE_READ_DATA | FILE_READ_EA | FILE_READ_ATTRIBUTES), GENERIC_WRITE will be treated as all write operations on files and GENERIC_ALL will be treated as all read, all write and all execute operations on files. Also note that FullControl is file specific and the value is STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE bitwise OR'd with all of the file and folder permissions. SYNCHRONIZE is the right to use the object in thread synchronization. STANDARD_RIGHTS_REQUIRED is the standard access rights DELETE | READ_CONTROL | WRITE_DAC | WRITE_OWNER, which is the right to delete the object, the right to read the security descriptor except for the SACL, the right to modify the DACL and the right to change the owner in the security descriptor.

This means that everyone can read, execute and synchronize files in the root. The CREATOR OWNER (my account) has all access to all folders and files below the root (but not to the root itself, because of the InheritOnly) and can read, write, execute and delete files as well as reading and altering the security descriptor. SYSTEM has full access to the root and all access to files and folders below the root. Administrators have the same access as SYSTEM. Users have read and write access to files and folders below the root, append data rights to folders, can create files in folders below the root and can read, execute and synchronize files.

9.4 Altering Security Information

Altering the security for an existing object is also simple, but again, your account must have the rights to do so. The first action is to obtain the security descriptor for the object as shown in the previous section and then make the change to the ACEs. Note that you can only affect direct ACEs. If an inherited ACE gives your object more access than you want it to have then you have two options: you can either deny access to some accounts so that you restrict the access given by the inherited ACE, or you can edit the security descriptor of the parent container. Clearly if you take the second approach you will affect all of the container's child objects, but if you find that a container is giving too much access then in most cases it will be a good thing to edit the parent. If the parent container denies a right that you want then this is not a problem because if you provide the access right it will be acted upon before the inherited denied  right. As mentioned in previous sections, the .NET methods for ACEs allow you to add or remove them, they do not allow you to modify an existing ACE in place.

In this section you'll change the security descriptor on folders and files, so before continuing I should introduce you to the Security tab and Advanced Security Settings dialog in Windows Explorer. If you use XP Professional you should not use simple file sharing, even though (apparently) Microsoft recommends this setting. Switching off this option is amongst the first things that I do when I use a clean copy of XP. This option was introduced earlier in this workshop, but I will repeat the details here.

Open an instance of Windows Explorer, open the Tools menu and select Folder Options. Now select the View tab and ensure that the Use simple file sharing option is not checked.

With simple file sharing disabled you can access the properties of a file or folder through Windows Explorer and get the security through the Security tab.

Use Windows Explorer to open the folder where you are running these tests. Right click and through the context menu select New and then Folder; call this folder Test. Right click on this new folder and from the context menu select Properties. Select the Security tab.

You will see something like the screen shot above. If your account does not have the rights to view security descriptors then you should log off, log on as Administrator and make your account a member of the Administrators group and then log on again under your account. After running these tests you should use the Administrator to remove your membership of the Administrators group.

The top box lists the accounts that have rights on this folder, in this case, the Administrators group, the SYSTEM account, the Users group and my account (RichardGrimes). There is also an account called CREATOR OWNER which is the account that created and owns the folder. When you select an account you'll see the rights of that account in the bottom list view. Incidentally, the list view gives Full Control as well as various other permissions that are presumably part of Full Control, if you look at the advanced settings (which you'll do in a moment) you'll see a more detailed list of permissions and yet you'll still see Full Control. The reason is that Full Control has additional permissions not shown in the detailed list of permissions, for example if you have Full Control then you will have the WRITE_OWNER permission that will allow you to change the owner of the object (actually, it allows you to take over ownership). Be careful about this permission, if you give someone Full Control of an object then that person will be allowed to take ownership of the object away from you and can then change the DACL to deny you all rights! It is rarely a good idea to give someone else Full Control.

Click on the Advanced button, this will give more detailed information for all accounts:

Notice that this lists all the accounts and the rights that are allowed, it also indicates if the rights were inherited and if so, where they came from. The final column lists how inheritance is applied and propagated.

At the bottom of the dialog are two check boxes. The top check box determines if the folder is protected. If it is checked (as in this picture) the folder is not protected and hence it inherits rights from its container. If it is not checked then the folder will not inherit rights from its parent and so you would have to make sure that you add direct rights to folder so that accounts will have access.

Normally, when you change rights you will specify how the right will be propagated. However, you may decide that you want to apply the rights recursively to all child objects. This is a rather blunt tool because, as you have seen above, the propagation options on inheritance gives you control over whether the rights are inherited by containers or objects, and whether they apply to a child or all descendant objects. The second check box specifies that you want to use this blunt tool, but it is rarely a good idea.

Select your account (noting that the permissions have been inherited from the permissions on C:\) and then click on the Edit button.

Notice that all the items here are greyed out. This highlights the fact that you cannot edit an inherited ACE. As you can see you are given full access to the folder and the rights are applied to the current folder only, that is, the ACE is not inherited by child objects. The reason for this is because you created the folder, so as the creator you get full rights. Close down this dialog, the Advanced Security Settings dialog and the Properties dialog.

Create a new file (addRight.cs) with this code:

using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;

class App
{
   static void Main()
   {
      DirectoryInfo di = new DirectoryInfo("Test");
      DirectorySecurity ds = di.GetAccessControl();
      FileSystemAccessRule fsar = new FileSystemAccessRule(
         new NTAccount(@"BUILTIN\Users"), FileSystemRights.Write, AccessControlType.Allow);
      ds.AddAccessRule(fsar);
      di.SetAccessControl(ds);
   }
}

This code gets access to the security descriptor for the folder called Test in the current folder, it then creates a new ACE giving the Users group write access. Compile this code (csc addRight.cs) and then run it. Now use Windows Explorer and look at the Permissions tab on the Advanced Security Settings page.

The ACE is the first in the list. This is understandable because it is a direct ACE and direct ACEs are ordered before inherited ACEs. The page indicates that the ACE is <not inherited> and that it is not inheritable. Select this ACE and click on Edit. Now you'll see that all the items on the page are enabled and so you can change them. Open the drop down list box Apply onto.

This lists all the combinations of the InheritanceFlags and PropagationFlags as applied to folders (containers) and files (objects). Again, this value (This folder only) is understandable because we did not apply any value for the inheritable and propagation.

Now change the code so that we access this right and change it so that it applies to this folder, subfolders and files (InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit). Add these lines to the code:

FileSystemAccessRule fsar = new FileSystemAccessRule(
   new NTAccount(@"BUILTIN\Users"), FileSystemRights.Write, AccessControlType.Allow);
ds.RemoveAccessRule(fsar);
fsar = new FileSystemAccessRule(
   new NTAccount(@"BUILTIN\Users"), FileSystemRights.Write,
   InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
   PropagationFlags.None, AccessControlType.Allow);
ds.AddAccessRule(fsar);

There is no way to programmatically change a particular ACE, there is no 'get-this-ACE-to-edit' method and anyway the properties on the FileSystemAccessRule are all read-only. Instead, you have to create an ACE that matches the ACE that you want to remove, then call RemoveAccessRule, create the new ACE and add it to the DACL. Compile this code, run it and confirm that the User group has write access to this folder, its subfolders and files.

The code so far has accessed the folder's security descriptor through a DirectorySecurity object from the ObjectSecurity hierarchy. Let's see how you can write the same code using the classes from the GenericSecurityDescriptor hierarchy. First, run the getSecurity tool you wrote in the last section to get the integer value of the right you just added (it will show a value of 0x00100116). You will edit the code so that you will remove the right you just added using CommonSecurityDescriptor. To do this delete all the code after you call GetAccessControl and then add the following:

DirectoryInfo di = new DirectoryInfo("Test");
DirectorySecurity ds = di.GetAccessControl();

byte[] buf = ds.GetSecurityDescriptorBinaryForm();
CommonSecurityDescriptor sd = new CommonSecurityDescriptor(true, false, buf, 0);
DiscretionaryAcl dacl = sd.DiscretionaryAcl;
SecurityIdentifier users = new SecurityIdentifier("BU");
dacl.RemoveAccess(AccessControlType.Allow, users, 0x00100116,
   InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
   PropagationFlags.None);
buf = new byte[sd.BinaryLength];
sd.GetBinaryForm(buf, 0);

ds = new DirectorySecurity();
ds.SetSecurityDescriptorBinaryForm(buf);
di.SetAccessControl(ds);

The CommonSecurityDescriptor is created by exporting the binary form of the security descriptor and then using this buffer in the constructor. The first parameter is true because the security descriptor is for a container object. Next, access is obtained to the DACL through the DiscretionaryAcl property; this accesses the actual DACL within the security descriptor and does not return a copy. To remove the ACE you need to provide the component parts of the ACE. The account must be a SecurityIdentifier, which generally means the numeric form of the account. Such numbers are difficult to remember, so there is a SecurityIdentifier constructor that takes a string in SDDL form, in this case BU is BUILTIN\Users. The access mask has to be an integer, which is why you had to get this from getSecurity tool. After you have called RemoveAccess you must apply the new security descriptor to the object and here it gets messy. The SetAccessControl method takes a DirectorySecurity object which you have to create from the binary data (or SDDL) from the CommonSecurityDescriptor you've been modifying. Now compile and run the code and use Windows Explorer to confirm that the folder no longer gives write access to the Users group.

Finally, clean up your work: use Windows Explorer to delete the folder that you created.

9.5 Creating An Object With A Security Descriptor

This is the type of code that is typically used to create files:

FileStream fs = new FileStream("test.txt", FileMode.CreateNew);

FileStream will create the file with a call to the Win32 function CreateFile passing NULL to the LPSECURITY_ATTRIBUTES parameter. This indicates that the file will get the ACL from its parent. If the parent ACL does not suit your needs then you can call SetAccessControl to change the ACL to something more suitable. However, this presents a security hole such that someone else could access the file before the call to SetAccessControl. For this reason, the classes that have a SetAccessControl method will also have a constructor that you can use to pass security information.

For example, the FileStream class has this constructor:

public FileStream(string path, FileMode mode, FileSystemRights rights,
   FileShare share, int bufferSize, FileOptions options, FileSecurity fileSecurity);

There are three new parameters: FileSystemRights, FileOptions and FileSecurity. The FileOptions enumeration provides additional information about how you wish to create the file. For example, you can create the file as encrypted (using the automatic encryption provided by the operating system) or you can indicate that the file should be deleted when the file handle is closed (through FileSystem.Close). This is essentially the dwFlagsAndAttributes parameter of the CreateFile function. The FileSecurity security descriptor allows you to determine who can access the file and what access they will get. For file creation, these rights are subsequent rights, that is, they do not affect how your code can open the file. The FileSystemRights parameter is essentially a more detailed version of FileAccess and it indicates the type of access that you require once you get the file handle, irrespective of the rights you give others through the security descriptor.

Since FileSystemRights is a more granular way to request access through the file handle, there is a version of the constructor that takes this parameter but without a security descriptor.

Create a new file and add this code (file.cs):

using System;
using System.IO;
using System.Text;
using System.Security.AccessControl;

class App
{
   const string file = "test.txt";
   public static void Main(string[] args)
   {
      if (args.Length == 0)
      {
         if (File.Exists(file))
            File.Delete(file);
         FileSecurity fs = new FileSecurity();
         fs.AddAccessRule(new FileSystemAccessRule(
            @"BUILTIN\Users",
            FileSystemRights.ReadData | FileSystemRights.ReadExtendedAttributes,
            AccessControlType.Allow));

         using (FileStream fWrite = new FileStream(
            file, FileMode.Create,
            FileSystemRights.WriteData,
            FileShare.None, 256, FileOptions.None, fs))
         {
            byte[] buf = Encoding.ASCII.GetBytes("this is data");
            fWrite.Write(buf, 0, buf.Length);
         }
      }
      else
      {
         using (FileStream fRead = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None))
         {
            StreamReader sr = new StreamReader(fRead);
            Console.WriteLine(sr.ReadToEnd());
         }
      }
   }
}

The idea here is that if you run the code with no parameters a file will be created called test.txt that will allow read access to everyone in the Users group. If the code is run with one parameter (irrespective of the actual value) then the file is open for read access and the data in the file is printed on the console.

The combination of ReadData and ReadExtendedAttributes is required to allow subsequent users (through the code at the end) to read the file. Another option is to use Read, and although this value contains the rights you need, it also gives you ReadAttributes, ChangePermissions, and Synchronize. The principle of least privilege says that you don't need these extra permissions, so that is why this code does not use Read. However, note that the rights you specify do not allow users to write to the file. This is not an issue because as creator you have full control and since you request WriteData access when creating the file you are able to call the Write method.

Compile this code (csc file.cs) and run it without a parameter to create a file. Use the getSecurity tool to get the security descriptor:

for test.txt:
Owner is: MARS\RichardGrimes
Primary group is: MARS\None
Access Rules:
   Allow user: BUILTIN\Users rights: ReadData, ReadExtendedAttributes, Synchronize [00100009]
      direct right, inheritance: None propagation: None

This indicates that I am the owner and that Users are allowed ReadData access to the file. To test this out run file again, but this time with a parameter so that the code attempts to read data from the file. You'll find that the call succeeds. Now change the code so that when you pass a parameter to the application it will attempt to get read/write access:

using (FileStream fRead = new FileStream(file, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
   StreamReader sr = new StreamReader(fRead);
   Console.WriteLine(sr.ReadToEnd());
}

Compile this code and run it again with a parameter. This time you will get an UnauthorizedAccessException exception because your code attempts to open the file with more rights than you are allowed in the security descriptor. Return the code back to what it was before (replace the FileAccess.ReadWrite with FileAccess.Read).

Note that the ACE in the ACL that gives read access is a direct ACE, there are no inherited ACEs from the parent because you have created the security descriptor from scratch. This is not always what you want. The whole point of having a hierarchy of ACEs and the ability to inherit ACEs is that it simplifies security administration: once it is set up the default security should only require small adjustments.

To inherit ACEs from the container you need to get access to the container's DACL. Add the following code:

if (File.Exists(file))
   File.Delete(file);
DirectoryInfo dir = new DirectoryInfo(".");
DirectorySecurity ds = dir.GetAccessControl(AccessControlSections.Access);

FileSecurity fs = new FileSecurity();
fs.SetSecurityDescriptorBinaryForm(ds.GetSecurityDescriptorBinaryForm());

Compile this and run it without a parameter, then use getSecurity to list the ACL to confirm that you have got ACEs inherited from the container.

9.6 Custom Security Descriptors

The security descriptors described so far are for Win32 secured objects. Win32 does give the developer the ability to create her own object type and secure it with a security descriptor. However, to do this the object has to be able to perform an access check on the security token of the process (or the impersonation token of the thread). Unfortunately, .NET does not allow you to perform access checks, so you cannot provide your own secured objects.

However, this is not a great issue because you can use .NET identity checks to perform a similar action to Win32 access checks.

I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (ho use this tutorial ((contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Ten

This page is (c) 2007 Richard Grimes, all rights reserved