Windows 2000 file security: ACLs
NTFS file security is handled with access control lists, which are lists of access control entries. Each ACE is either an ALLOW or a DENY rule, with a mask of permissions which it affects, and applies to a specified user or group. By combining all the ACEs in an object's ACL, the system calculates the effective permissions on the object for a given user.
Actually that part about ACEs being either allow or deny is not true because there are auditing ACEs as well...
Sample ACLs
The simplest non-trivial ACL is the one granting "full control" to Everyone.
Access | Account | Permissions |
---|---|---|
Allow | Everyone | Full control |
Perhaps we want to allow everyone to read a file and let members of the Administrators group have full control.
Access | Account | Permissions |
---|---|---|
Allow | Administrators | Full control |
Allow | Everyone | Read |
We can even allow full control to everyone except for one user, by adding a deny ACE for that user and an allow ACE for Everyone.
Access | Account | Permissions |
---|---|---|
Deny | eviluser | Write |
Allow | Everyone | Full control |
API representation
That's what ACLs look like in the abstract. To deal with them programatically, we need to peer inside winnt.h.
As you might expect (or as you should expect, if you've been following this article from the beginning), the user to which an ACE applies is stored as a SID.
The permissions are stored as a 32-bit bitmask of the following values:
FILE_READ_DATA: Read from a file or pipe, or list the contents of a directory.
FILE_WRITE_DATA: Write to a file or pipe, or create a new file inside a directory.
FILE_APPEND_DATA: Append data to a file or pipe, create a new subdirectory inside a directory, or create a pipe instance.
FILE_READ_EA: Read extended attributes.
FILE_WRITE_EA: Write extended attributes.
FILE_READ_ATTRIBUTES: Read attributes.
FILE_WRITE_ATTRIBUTES: Write attributes.
FILE_EXECUTE: Execute a file or access a directory.
FILE_DELETE_CHILD: Delete a file from a directory.
DELETE: Delete a file, pipe or directory.
READ_CONTROL: Read permissions.
WRITE_DAC: Write permissions.
WRITE_OWNER: Take ownership.
SYNCHRONIZE: Use object for synchronisation.
Clearly, the exact effect of each value in the bitmask depends on the object to which the ACE applies. For example, FILE_READ_DATA when applied to a file gives permission to read the data from that file, whereas when applied to a directory it gives permission to list the contents of that directory.
With this in mind, we observe that aliases exist for several of these permission names:
FILE_LIST_DIRECTORY: Equivalent to FILE_READ_DATA
FILE_ADD_FILE: Equivalent to FILE_WRITE_DATA
FILE_ADD_SUBDIRECTORY: Equivalent to FILE_APPEND_DATA
FILE_CREATE_PIPE_INSTANCE: Equivalent to FILE_APPEND_DATA
FILE_TRAVERSE: Equivalent to FILE_EXECUTE
Structures
Let's see the data structures used in ACEs and ACLs.
All ACEs are essentially the same. An ACCESS_ALLOWED_ACE is defined in exactly the same way as an ACCESS_DENIED_ACE and the type is stored in the header. For that reason, only the ACCESS_ALLOWED_ACE structure is shown here.
typedef struct _ACE_HEADER { char AceType; char AceFlags; short AceSize; } ACE_HEADER;
winnt.h lists all the types and flags that can be stored in an ACE header. ACCESS_ALLOWED_ACE_TYPE and ACCESS_DENIED_ACE_TYPE are particularly interesting types. The flags determine whether the ACE is inherited or if it can itself be inherited by subobjects (ie if an ACE applied to a directory will propagate to files inside that directory).
typedef struct _ACCESS_ALLOWED_ACE { ACE_HEADER Header; unsigned long Mask; unsigned long SidStart; } ACCESS_ALLOWED_ACE;
The ACE structure itself is easy to understand. Only SidStart needs to be explained. It is a pointer to the SID to which the ACE applies, only declared as DWORD. To get the SID, you can do something like
SID *sid = (SID *) &ace->Header->SidStart;
I will skip presentation of the ACL structure, as we don't need to know about it at all... To get ACEs from an ACL, we use GetAclInformation() to find the number of ACEs and then loop through them with GetAce().
int GetAclInformation(ACL *acl, void *info, unsigned long size, ACL_INFORMATION_CLASS class); int GetAce(ACL *acl, unsigned long ace_id, void **ace);
The second argument to GetAclInformation() is a pointer to either an ACL_REVISION_INFORMATION or ACL_SIZE_INFORMATION structure. You set class to either AclRevisionInformation or AclSizeInformation in order to tell the function what information you want.
typedef struct _ACL_REVISION_INFORMATION { unsigned long AclRevision; } ACL_REVISION_INFORMATION
typedef struct _ACL_SIZE_INFORMATION { unsigned long AceCount; unsigned long AclBytesInUse; unsigned long AclBytesFree; } ACL_SIZE_INFORMATION;
Sample code
Getting the size and number of ACEs in an ACL
#include <stdio.h> #include <windows.h> ACL_SIZE_INFORMATION size_info; ACL *acl; /* Obtained from somewhere */ if (! GetAclInformation(acl, &size_info, sizeof(size_info), AclSizeInformation)) /* Error */ printf("Number of ACEs: %d\n", size_info.AceCount);
Inspecting all ACEs
#include <stdio.h> #include <windows.h> ACL *acl; /* Obtained from somewhere */ ACL_SIZE_INFORMATION size_info; /* Filled in as per code above */ unsigned long i, mask; void *ace; SID *sid; char *stringsid; for (i = 0; i < size_info.AceCount; i++) { if (! GetAce(acl, i, &ace)) /* Error */ /* Check type of ACE */ if (((ACCESS_ALLOWED_ACE *) ace)->Header.AceType == ACCESS_ALLOWED_ACE_TYPE) { sid = (SID *) &((ACCESS_ALLOWED_ACE *) ace)->SidStart; mask = ((ACCESS_ALLOWED_ACE *) ace)->Mask; printf("Allow ACE\n"); } else if (((ACCESS_DENIED_ACE *) ace)->Header.AceType == ACCESS_DENIED_ACE_TYPE) { sid = (SID *) &((ACCESS_DENIED_ACE *) ace)->SidStart; mask = ((ACCESS_DENIED_ACE *) ace)->Mask; printf("Deny ACE\n"); } else printf("Other ACE\n"); /* Check permissions */ if (mask & FILE_READ_DATA) printf("Read access\n"); if (mask & FILE_WRITE_DATA) printf("Write access\n"); /* ... */ /* Get SID */ if (! ConvertSidToStringSid(sid, &stringsid)) /* Error */ printf("Account affected: %s\n", stringsid); HeapFree(GetProcessHeap(), 0, stringsid); }