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
-
DACL_SECURITY_INFORMATION
- OWNER_SECURITY_INFORMATION
- GROUP_SECURITY_INFORMATION
- SACL_SECURITY_INFORMATION
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
-
SidTypeUser
- SidTypeGroup
- SidTypeWellKnownGroup
- SidTypeComputer
- and several more, such as a domain or a computer account that is already
closed.
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,
-
ACCESS_ALLOWED_ACCESS_TYPE
- ACCESS_DENIED_ACCESS_TYPE
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.
-
FILE_GENERIC_READ
- FILE_GENERIC_WRITE
- FILE_GENERIC_EXECUTE
- GENERIC_READ
- GENERIC_WRITE
- GENERIC_EXECUTE
- GENERIC_ALL
- DELETE
- READ_CONTROL
- WRITE_DAC
- WRITE_OWNER
- SYNCHRONIZE
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.\
-
Attach to your submission the output of the program on the file c:\temp
and on the executable file containing the program itself.
- Create a new file using a text editor, and attach the program' output
on that file.
- 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.
- 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?
- 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