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:

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:


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;

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;

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.

  unsigned long AclRevision;

typedef struct _ACL_SIZE_INFORMATION {
  unsigned long AceCount;
  unsigned long AclBytesInUse;
  unsigned long AclBytesFree;

Sample code

Getting the size and number of ACEs in an ACL

#include <stdio.h>
#include <windows.h>

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);