Previous Up Next

Permissions and Access Control Lists

In this exercise you will write a program that can display a file's list of security permissions, assign a list of permissions to a file, and copy permissions from one file to another.

Reading the security information associated with a file. The system call GetSecurityInfo reads the list of permissions for an object (a file, mutex, event, process, and so on). Under Windows, almost every operating-system object is securable, and can be assigned a list of permissions. This system call returns a structure of type SECURITY_DESCRIPTOR, which contains four fields: the owner of the object, the groups of users to which the object belongs, the list of permissions (the so-called discretionary access-control list, DACL), and the list of access-logging instructions (the system access-control list, SACL). The system call allows you to request all four fields, or just a subset.
#include <aclapi.h>
...
DWORD GetNamedSecurityInfo(
        TCHAR*                name,
        SE_OBJECT_TYPE        object_type,
        SECURITY_INFORMATION  SecurityInfo,
        SID**                 owner,
        SID**                 group,
        ACL**                 dacl,
        ACL**                 sacl,
        SECURITY_DESCRIPTOR** security_descriptor);
The first argument is the name of the object, for example, a file name. The second argument describes the type of object; for files, this argument should be SE_FILE_OBJECT. Another system call, GetSecurityInfo, retrieves the security information of an object referred to by an open handle, not an object specified by name.

The third argument specifies the fields we want to read, and its value should be a bitwise or-ing of a subset of the following constants

The system call stores the output structure in memory, and sets the pointer whose address is passed as the last argument. Upon successful return, this pointer will point to the returned structure. When it is no longer needed, the calling program must release the memory allocated to it using LocalFree. In addition to this pointer, the system call also sets up to four more pointers to areas within the output structure. If the program does not request all four fields, then it can pass NULL in the corresponding arguments 4--7.

To be able to read the DACL, the identity of the owner and the identity of the group, the process issuing the system call must have a READ_CONTROL permission to the object. That is, a process may not have enough permission to even read the security information of an object. To read the SACL, the process must have a special operating-system privelege. In this assignment, we do not read the SACL.

Obtaining human-readable user and group names. A permission entry in the access-control list grants or denies somebody the authorization to perform a certain action on the object. The entity to who permissions are granted or denied is called a principal. Under windows, a principal may be a user, a group of users, a computer, as well as several other entities. A principal may be defined in a single computer, or in a security name space called a domain. A domain usually stores the names of principals in an organization. The system calls that return the security information of an object do not name principals using human-readable strings, but using internal identifiers called security identifiers (SID's). These identifiers must be translated into strings before they are presented to human users.
#include <windows.h>
...
BOOL LookupAccountSid(
       TCHAR*         system_name,
       SID*           sid,
       TCHAR*         name,
       DWORD*         name_length,
       TCHAR*         domain,
       DWORD*         domain_length,
       SID_NAME_USE*  what_kind);
The system call expects, in the second argument, a pointer to a security identifier, and translates it to a string. The translation is performed in the computer whose name is given in the first argument, or, if the first argument is NULL, in the computer on which the program runs.

The SID is translated into the name of the domain in which the SID is defined, and the principal's name. They are returned in arguments 5 and 3, respectively. Arguments 6 and 4 describe to the system call the length of the buffers that the program allocated for these strings. If one or both are too small, the system call fails but sets these variables to the required length. A program can pass 0 as the length of both strings to find out how long they are, allocate them, and issue the system call again with correct lengths.

The last argument returns the kind of principal represented by the given SID. The kind can be one of the following

Processing an Access-Contol List. The DACL consists of a sequence of entries called ACE's (access-control entry). The number of entries in a list is stored in the AceCount field of the ACL data type. The system call GetAce reads an entry of the list, selected by an index (starting at 0), and returns it in a structure of type ACCESS_ACE.
#include <windows.h>
...
BOOL GetAce(ACL*   acl,
            DWORD  index,
            VOID** ace);
The call returns an entry by setting a pointer to it. An access-control list can contain several types of entries, each represented by a different data structure. To allow programs to identify the data type of a specific entry, all entries start with a fixed prefix, which describes, among other things, the type of the entry. The data type of the prefix is ACE_HEADER, and its AceType field describes the entry's type. In this assignment we will process entries of the following types,

but there are many more types. The data types corresponding to these entries are ACCESS_ALLOWED_OBJECT_ACE and ACCESS_DENIED_OBJECT_ACE. As their names suggest, the first type grants access and the second denies access. In the access-allowed and access-denied data types there are two important fields: Mask, which describes the kinds of allowed/denied operations (each operation is represented by one bit in this field), and SidStart, whose address is the address of the SID for the principal that is granted/denied access by the entry. That is, the program must compute the address of this field, and use this address as the address of a SID structure. It seems that the rationale behind this odd interface is that SID is not a fixed-size structure.

The best way to process access-control entries in a program is using a union data type to store the data returned by GetAce. This union should include both the header data type and the access allowed/denied types. The program first uses the header member of the union to determine the kind of entry represented by the data, and then the appropriate specific member.

The allowed or disallowed actions are described by the bits of the Mask field. You can inspect, set, or clear individual bits using the following constants. The first three constants are specific to files, while the rest apply to most object types.

Creating a new access-control list. We shall now learn how to create a new access-contol list. The list must be created in a contiguous area of memory that should be allocated with LocalAlloc. We shall later see how to compute the required size of this area. But first, let's see the system calls that are involved.
#include <windows.h>
...
BOOL InitializeAcl(ACL*  acl,
                   DWORD acl_length,
                   DWORD acl_revision_level);
BOOL AddAccessAllowedAce(
                   ACL*  acl,
                   DWORD acl_revision,
                   DWORD access_mask,
                   SID*  sid);
BOOL AddAccessDeniedAce(/* same interface*/...);
The first system call initializes the list. Its arguments include the address of the memory area allocated for the list, the size of this area, and the version for the required list. There are two possible versions of access-control lists in Windows, a simple one, ACL_REVISION, and a more sophisticated one, ACL_REVISION_DS. The first supports only generic actions, and is sufficient for this assignment. The second version supports object-specific actions, such as separate actions for files, processes, events, and so on. Older releases of Windows support only the simple version.

The next two calls each add a single entry to a list: the first adds an entry that permits access, the second an entry that denies access. In each, the first argument is a pointer to the DACL, the second a revision constant (same as in the initialization system call), the third is a union of the operations that this entry allowes/forbids, and the last is an identifier of a principal.

The entries in the new list are ordered according to the order in which they were added to the list.

The size of the memory area allocated to the list should include the size of an ACL structure and the sum of the sizes of the entries. The size of entries varies, because the size of SID's vary. Therefore, you should compute the size of each entry using a formula such as

sizeof(ACCESS_DENIED_ACE)-sizeof(DWORD)+GetLengthSid(sid);
The system call GetLengthSid, returns, of course, the length of a given SID. The reason that we subtract the size of a DWORD is that the SidStart member of the ACE structure, whose only purpose is to mark the location of the variable-length SID, is a DWORD.

Linking the list that we have created to a specific operating-system object is performed using the following system call:

#include <aclapi.h>
...
DWORD SetNamedSecurityInfo(
        TCHAR*                name,
        SE_OBJECT_TYPE        object_type,
        SECURITY_INFORMATION  SecurityInfo,
        SID*                  owner,
        SID*                  group,
        ACL*                  dacl,
        ACL*                  sacl);
The interface of this system call is similar to that of GetNamedSecurityInfo, except that here we pass pointers to structures rather than pointers to pointers to structures.

The assignment. Write a program called win32-acl that can display the owner and access-control list of a file, and that can assign a new access-control list to a file. When the program is executed with a single argument, a file name, if prints the name of the owner of the file (both owner and group) and the file's DACL. For example,
c:> win32-acl c\:temp
owner: [U] SWIFT\Administrator 
group: [G] SWIFT\None
Allow: [A] BUILTIN\Administrators
generic:---- file:RWE
...
Allow: [W] \CREATOR OWNER
generic:A--- file:---
...
The letter in brackets before each principal describes the kind of principal: U for users, G for an arbitrary group of users, and W for well-known groups (groups that the operating system defines implicitly, not with an explicit list of members). The program should display, as in the example, the type of every entry in the list (allow or deny), the principal, and the generic and file permissions, where each action is represented by a sinle letter.

When the program is executed with two arguments, a file name and an exclamation mark, it should assign a new two-entry DACL to the file. The first entry should allow the owner of the file all the generic permissions, including reading and changing ownership and access-control permissions. The second entry should deny the owner of the file writing to the file.

In addition to implementing the program, answer the following questions.\
  1. Attach to your submission the output of the program on the file c:\temp and on the executable file containing the program itself.
  2. Create a new file using a text editor, and attach the program' output on that file.
  3. Now assign a new DACL to this file using your program, and try to read from it and to write to it. Do you have permissions to read and write? Attach the output of the program on the file with the new DACL.
  4. Now try to inspect these permissions using the operating system's graphical user interface. Right click on the file in Windows Explorer and choose properties, then security. What happens, and why do you think it happens?
  5. Allow the operating system to modify the permissions according to the suggestion in the dialog box. Can you now read and write from the file? Attach the output of your program on the file now.
Copyright Sivan Toledo 2004
Previous Up Next