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.
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:
This string can be split in this way:
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;;;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:
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
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.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:
fileReadSD fileReadSD.cs
This will give you a result looking something like this:
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:
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:
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:
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:
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.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:
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:
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 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:
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.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:
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:
{
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:
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.