Process Tokens and Default DACLs

I ran up on something the other day that isn't very well documented in one place. When you're dealing with restricted tokens, and in a few other limited scenarios, the default DACL on the process token becomes important. We can look at the default DACL by calling GetTokenInformation with a level of TokenDefaultDacl. What this is used for is when you create something, we almost always have the opportunity to atomically set the ACL on the object at creation time. This avoids race conditions, and is something I really prefer about how the Windows security subsystem works over how UNIX(ish) APIs work. If you don't supply an ACL at creation time, then what happens depends on what sort of object you're creating – if it is a file or registry key, the ACL will get inherited from the parent, which is typically what you want. If it isn't a persistent object, then the token's default DACL gets used.

So far, so good. Now let's take a look at how an ACCESS_MASK is actually put together:

// typedef struct _ACCESS_MASK {

// WORD SpecificRights;

// BYTE StandardRights;

// BYTE AccessSystemAcl : 1;

// BYTE Reserved : 3;

// BYTE GenericAll : 1;

// BYTE GenericExecute : 1;

// BYTE GenericWrite : 1;

// BYTE GenericRead : 1;

// } ACCESS_MASK;

// typedef ACCESS_MASK *PACCESS_MASK;

 

We normally treat this as a DWORD, but the various parts of the bit fields have very specific meanings. SpecificRights are things that only apply to whatever type of widget you're dealing with. There's no guarantee at all that 0x40 means even remotely the same thing between say an event and a window station. An example of a gotcha along these lines is that the bits for pipes and the file system mostly mean the same thing, but FILE_APPEND_DATA happens to have the same bit as FILE_CREATE_PIPE_INSTANCE, which have very different implications, and opening a pipe for FILE_GENERIC_WRITE asks for create instance permission (which ought to get denied), and asking for FILE_WRITE_DATA does what you want – lets you write to the pipe.

The next 8 bits are standard rights – only 6 of them have been used, and many of them regulate things that apply to any securable object like WRITE_DAC or WRITE_OWNER. After that, the next byte is the generic rights, 3 unused bits, and something I don't know about – AccessSystemAcl. You might wonder how the OS maps the generic bits to specific and standard bits. This is done by supplying a GENERIC_MAPPING struct:

typedef
struct _GENERIC_MAPPING {

ACCESS_MASK GenericRead;

ACCESS_MASK GenericWrite;

ACCESS_MASK GenericExecute;

ACCESS_MASK GenericAll;

} GENERIC_MAPPING;

In this, we can define what specific and standard rights each of the generic bits work out to be. For example, GENERIC_ALL on a file works out to:

FILE_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF)

How this applies to you is that if you've just made a restricted token, you have to adjust the token DACL, or you'll create a process that doesn't have access to itself, you'll get a completely strange failure out of the app, and generally have a bad day. Now the question becomes just what to adjust the DACL to. What we'll normally get as our default is this:

System:F
Administrators:F
Creator-Owner:F

An interesting problem with the Creator-Owner part is that you can't count on it to be anything. If the user isn't an admin, Creator-Owner will be the user. If they are an admin, Creator_owner depends on an OS setting that regulates whether things created by admins are owned by that user or administrators. There are good and bad points to both settings. For a restricted token, one reasonable approach it to set the ACE to the logon ID SID (I'll get into how to sort out which one this is later). Now the question is what access mask would be appropriate.

I started out by making the access mask GENERIC_ALL, which is probably the right answer for most apps. Being on a never-ending quest to reduce privileges and attack surface, I experimented, and found that I could set just 2-3 specific bits, which would then give the process enough rights to itself to run. The problem that I then got into was that subsequent objects, like memory mapped sections, were getting created with really weird ACLs that didn't do what I wanted at all, resulting in very strange symptoms like not being able to run an ATL app in debug(!). Once I thought through the problem, I realized that setting specific bits in a default token DACL was just a mistake, and that the only bits that really make sense are the generic bits, since you have no idea how specific bits actually map to any given thing. Unfortunately, this nuance isn't documented in either the TOKEN_DEFAULT_DACL struct info, nor SetTokenInformation.

Overall, I'm never quite sure whether our security subsystem is hideously overcomplicated and byzantine, since I've been dinking around with it for 14 years now and still learning things, or whether it's just incredibly versatile. Maybe both. Maybe it's just an endless playground for this security geek.

Expect more articles on restricted tokens and the general topic of Windows sandboxing to follow – I have a turbo talk at Blackhat coming up next week, but only have 20 minutes to talk about it. As you can imagine, I can't get into much detail in that amount of time, so I'll document it here in installments.