Freigeben über


Security descriptors, part 1: what they are, in simple words

Part 2 >>

I've been dealing with the security descriptors recently. I've figured out for myself, what different concept around them mean, and wrote a few helper functions to deal with them in PowerShell. Now I want to do a brain-dump of it all, starting with the concepts, and the PowerShell examples will be forthcoming later.

Overview

The security descriptor is the way to control the access to an object. The object may be whatever you want, and the access may be whatever you want as long as someone defines the meaning of various bits in the mask for this object type (more on this later). You can even make the ACLs for your own objects, as long as you also provide the code that checks this access. But there also are a few object types (files, registry entries, synchronization primitives, ETW log sessions), for which the Windows OS checks the access.

A security descriptor consists of the information about the owner and group of the object, the system ACL (access control list) and discretionary ACL. There also are other small bits and pieces like control flags that carry the extra information but they're not particularly important for the general understanding.

The meaning of the owner and group is entirely up to the particular object type. For the files in the filesystem, I think the owner is the one allowed to change the security descriptor for this file, and is used to compute the quotas, and I'm not sure what can the group do. Maybe the group is also used for some quotas.

The System ACL can be used for auditing: recording when someone accessed something, or even killing the process that touched something that it should not have. I haven't looked at it much, so I won't talk any more about it.

The Discretionary ACL is what controls the access. In the future, when I talk about the ACLs, I'll mean the discretionary ACLs. The system ACLs are similar, just they're used for a different purpose.In general, the ACL in Windows is the ACL-as-usual, nothing particularly special, if you've seen the ACLs before, you know the idea. Basically, it contains a number of Access Control Entries (ACE), that are examined sequentially in the order they're placed in the ACL (so the order matters, more on that later). Each ACE contains the identity of a user or group (principal, in Windows terms), the type of the entry (allowing or denying the access) and the bitmask describing what exact access it allows or denies (I'm used to calling them "permissions" in the Unix way, I like this word, and I'll be using it). The ACL gets read sequentially, each entry is checked whether the principal in it matches the identity of the current user or one of its groups, and if the match is found, the bitmask of permissions in the entry is applied to the bitmap of the permissions the user is asking. If anything asked is found in a Deny entry, the access error is returned. If anything asked is found in an Allow entry, that permission is considered satisfied. If all the asked permissions are satisfied, the access gets granted and the walk through the ACL stops. Otherwise the walk continues.

The ACLs can be inherited. For example, instead of specifying an ACL on each file, its possible to specify an ACL on the directory and have it inherited by all the files in it.

Command-line

The two main commands to know for manipulation of security descriptors on the files are takeown.exe and icacls.exe.

Principals

A principal is a user or group, they're in a common namespace, which is very convenient. A principal generally has a name and a binary identifier. The binary identifier is called SID (Security IDentifier).  Naturally, the identifier gets used in all the internal representations.

In Unix, the user (or group) namespace is flat and the identifier is just a number. Which is historically simple but creates many problems when combining the users from different sources, such as the local users and the users from the network, requiring the rules like "the users with the id less than N are the local users, and greater than that are the network users".

In Windows, the identifiers are more flexible. They're organized into domains (such as the network domains, the local computer domain, and a disturbing number of domains for the pre-defined "Well Known" users), and the SID contains the parts for the domain and the principal as such within the domain. The format is also extensible, and in result the binary format having the variable length.

There is also a human-readable numeric form of SID. It consists of a bunch of numbers separated by a dash and starting with "S-1-". I suppose, "S" stands for "SID". "1" means that this is the version 1 of the SID format, and the rest are the bits and pieces from that variable-length numeric format.

The names of the principals also include the domain part, they're generally written as "domain\principal". The domain named the same as the current computer contains the principals defined locally on the current computer (or "." can be used for the same purpose). For all I can tell, there is no concept of "user on THAT computer", there are no domains denoting the other computers, but only the domains describing the shared networked users. It's also possible to give the name simply as "principal", and when it gets resolved to a SID, the resolved is smart enough to look for it in the available domains and figure out the domain part by itself (of course, if the same principal name is used in multiple domains, the result might be different from what you've meant).

It's also possible to have a SID for which there isn't any translation to a name. It's really an undefined principal but the code that deals only with SIDs doesn't care: it will process it just like any other SID.

There is also the third kind of names for some principals. Some principals are pre-defined in the windows OS to have a hardcoded meaning. They are the "well-known" principals. Many of these well-known principals also have a special two-letter representation of the SID. They do have the normal SID starting with "S-1-" as well, and the normal binary representation, but also a short name that can be used in any place where a "S-1-" string form of the SID can be used, and the SID parser knows how to parse these strings. There are places that require a text form of a SID, not the user name. The idea here is that it's easier to remember and write say "BA" than "S-1-5-32-544" for the group "BUILTIN\Administrators".

Descriptor Formats

The  descriptors can be represented in multiple formats, depending on the needs.

There are two native binary formats, one for the in-memory use, one serialized and usable for the off-line storage. Since the descriptor contains multiple elements (all these principals and ACLs, that further contain ACEs in them that also contain principals), the natural and convenient in-memory format for them consists of a tree connected by pointers. The serialized format is essentially the same but all these bits and pieces are collected in a contiguous chunk of memory, and the pointers are replaced by the relative offsets in that chunk.

If you see the type SECURITY_DESCRIPTOR, it's the header for the in-memory format with pointers, the type SECURITY_DESCRIPTOR_RELATIVE is the serialized version with the relative offsets. The first 4 bytes of these structures are actually common, and contain the version of the format and the control flags. The control flag SE_SELF_RELATIVE means that the data is in the serialized format, so the code that handles it can determine the format automatically.

There is also a compact human-readable format named SDDL (Security Descriptor Definition Language, or some such). It's basically the same information in a string, using the string SID format for the principals. Looks like this:

O:BAG:BAD:(A;;0x201;;;SY)(A;;0x200;;;BA)(A;;0x200;;;BO)(A;;0x200;;;SU)(A;;0x200;;;WR)(A;;0xffff;;;SY)(A;;0xff7f;;;BA)(A;;0xffff;;;S-1-5-80-880578595-1860270145-482643319-2788375705-1540778122)

"O:BA" and "G:BA" mean that the owner and group are both the "BUILTIN\Administrators". "D:" is the start of the Discretionary ACL followed by ACES in parentheses. Each ACE shows its type ("A" for "Allow"), the bitmask of permissions, and the principal's SID in the text form. There are more fields in the ACE for such things as the inheritance flags, and the permissions can also be written symbolically with the two-letter combinations, like:

(A;CI;GRGX;;;BU)

I won't go into the details, they can be found in the SDDL definition. It's somewhat readable but not hugely so, and I'll show the PowerShell functions to print it in the format closer to the one used by icacls.exe.

The .NET provides the classes that encapsulate the descriptors, which can also be used from PowerShell. Now, I've been searching for examples, and people use all kinds of strange ways of dealing with permissions in PowerShell, including the WMI objects. There is no need to go that strange, the .NET classes provide a more straightforward way. Unfortunately, it's not very straightforward, so I'll show in the future installments a few PowerShell functions I wrote to make things easier.

The namespace for all the security stuff is System.Security.AccessControl, and the main two classes are RawSecurityDescriptor and CommonSecurityDescriptor (both inheriting from GenericSecurityDescriptor). The difference between them is that the Common version provides a more high-level interface (though still not very easy to use) but expects the ACLs to be in a canonical form (se e more later); while the Raw class just gives the access to a descriptor and lets you deal with it in any way you want, including the creation or repair of the non-canonical forms.

The canonical form

The canonical form means that the entries in the ACLs are ordered in a certain way: all the Deny entries must go before all the Allow entries.

But why? Because in this order the further ordering of the entries for different principals doesn't matter. Remember, in general the ACEs are processed sequentially, and the order matters. There might be multiple entries applicable for the current user that contain the overlapping permissions masks. If there are two entries, one of them Allow and another one Deny, with the same permission bit set, and the Allow goes before Deny, the access will be granted; if Deny goes before Allow, the access will be denied. These entries won't necessarily be for the same principal, they might be for the different groups to which the current user belongs. The creative ordering of the entries allows to create some complicated ACLs but figuring out what these complicated ACLs do is a pain. the canonical order makes sure that the ACLs are easy to understand.

Things can get even more complicated when the inheritance of the security descriptors is taken into account. In the canonical form, it's simple: just take all the inherited ACEs and all the explicit ACEs, and put them into one ACL, and order them to make all the Deny entries go first. The meaning of that ACL will be the same as of the original set of ACLs. The ordering within the Deny and Allow sections of the ACL won't matter.

Part 2 >>