Click here to Skip to main content
15,861,125 members
Articles / Programming Languages / C

The Windows Access Control Model: Part 2

Rate me:
Please Sign up or sign in to vote.
4.80/5 (28 votes)
27 Jun 2005CPOL43 min read 243.5K   7.2K   113   31
This second part of the Access Control series will program with the basic Access Control structures.

MyWhoami output

Figure 8: Sample output from the Whoami clone program

About the series

This four part series will discuss the Windows Access Control model and its implementation in Windows NT and 2000.

In this second article we will start programming with security identifiers, access control lists and security descriptors. We will solve trivial problems using the SID, obtain information from an access token, enable a privilege, fill up an access control list, and finally we will check if we have access to a resource. The demo project provided is a Whoami clone written in Windows 2000 style. The source code includes equivalent programs of the article's code written with the low level APIs, the Windows 2000 APIs, and the Active Template Library.

Table Of contents

The table of contents is for the entire series.

  1. Part 1 - Background and Core Concepts. The Access Control Structures
    1. The Security Identifier (SID)
    2. The Security Descriptor (SD)
    3. The Access Control List (ACL)
    4. Choosing a good discretionary access control list
    5. Windows 2000 Inheritance Model
    6. The Token
    7. A Note on the Security Descriptor Definition Language
    8. Coming Up
  2. Part 2 - Basic Access Control programming
    1. Choosing your language
    2. Fun with SIDs
    3. Which Groups are you a member of?
    4. Enabling Token Privileges
    5. Dissecting the Security Descriptor
    6. Walking an Access Control List
    7. Do I Have Access?
    8. Creating an Access Control List
    9. Creating and Editing a Secure Object
    10. Making Your Own Classes Secure
    11. Toy Programs Download
  3. Part 3 - Access Control programming with .NET v2.0
    1. A history of .NET security.
    2. Reading a SID in .NET.
    3. Whoami .NET.
    4. Privilege handling in .NET.
    5. Running as an unprivileged user in .NET.
    6. Obtaining and editing a security descriptor in .NET, and applying it to an object.
    7. Access checks in .NET.
    8. NetAccessControl program and AccessToken class library.
  4. Part 4 - The Windows 2000-style Access Control editor
    1. Features of the ACL editor (ACLUI).
    2. Getting Started.
    3. Implementing the ISecurityInformation Interface.
    4. ISecurityInformation::SetSecurity
    5. Optional Interfaces.
    6. Presenting the interface.
    7. Filepermsbox - A program to display the security descriptor on an NTFS file or folder.
    8. History

9. Pick your poison (choose your language)

At the end of part 1, I asked you to choose which language to program in. I presented four choices for you:

  1. The low level method.
  2. The Windows 2000 method.
  3. The ATL method.
  4. The .NET method. (If you chose this, skip this part and go straight to part 3.)

A detailed look at each method

  1. The low level method involves using the original security APIs to create and read the security objects (dating back to day one of Windows NT in 1988). This method has the advantage of working in Windows NT3.x and NT4.x. It also has the advantage of not being reliant on any external DLLs, or require you to buy Visual Studio .NET (this method doesn't work on Win9x, so you'll have to rely on dynamic loading if you care about backward compatibility).

    In order to get this method to work, you have to call some of the most confusing APIs in the Windows SDK! These low level APIs know nothing about the inheritance model of Windows 2000 (using these APIs will lead to a security bug on Windows 2000). There's no consistency as to whether the functions return bools, errnos, pointers, or voids. And one API expects you to manage five memory buffers, all from the LocalAlloc() heap! It's very easy to make a mistake developing with this method.

    The only reason you'd want to use this technique is if your target market still uses Windows NT3.x or 4.x (and you enjoy being on the security mailing lists). New programs must not choose this technique at all. If you need to support just one Win9x or Win2000 client, you must not develop using this method. I feel sorry for you if you're forced to develop with this method of Access Control. Since your target environment is crippled, I will be assuming that your development environment is crippled (i.e. you don't have a reliable C++ compiler). This method will be programmed in C.

  2. New programs should not be considering anything below this method. This method bears the advantage that you don't need Visual Studio .NET to develop with security, and it is the most comfortable language to program with for people who don't like ATL. However, this method bears the disadvantage that it only works on Windows 2000 and later. Coding with security descriptors becomes a little tough with this method too (unless you've coded textual parsers before).

    During Windows NT 4 and 2000, Microsoft added a new set of APIs to make security programming easier. Perhaps the most significant result was the addition of the Security Descriptor Definition Language (SDDL).

    The Security Descriptor Definition Language (SDDL) presents security descriptors as a data-driven structure rather than as a programmatic structure, so both developers and administrators can now write security descriptors. At first sight SDDL seems to be just as non-descript as the low level structures, but if you look at enough SDDL strings, you will see it is far simpler to make SDDL strings than raw structures. The ConvertSecurityDescriptorToStringSecurityDescriptor() and ConvertStringSecurityDescriptorToSecurityDescriptor() functions make it easy to convert SDDL to security descriptors.

    The other significant addition to Windows 2000 was the automatic inheritance of DACLs (described in part 1). The support of automatic inheritance required the addition of new APIs to manipulate them, and you can find this new functionality in the GetSecurityInfo() functions (or the GetPrivateObjectSecurityEx() functions if you are using a custom class).

  3. During the Trustworthy Computing Initiative, Microsoft added a set of classes to ATL that allowed ATL projects to manipulate security descriptors and Access Control lists as easily as calling a COM server. This method bears the advantage of providing a fully object oriented framework for Access Control editing (the way security should have been implemented from day one). There is no longer a need to manage buffers, or check the type of the return code, or create text parsers.

    If you work carefully enough, you can make this technique run on Windows NT! This article will not tell you how to do this (you'll just have to read the documentation carefully) but it is possible.

    There is a requirement that you have to redistribute the ATL DLLs (or expect for a bloated application), and if you don't own Visual Studio .NET, this method will be unavailable. Finally, if you are one of those that loath ATL, you are unlikely to choose this method.

  4. (This will be discussed in the next part).

You most likely based your decision either on your previous experience of Access Control, or your programming background. Now unfortunately, for this article I have already made the decision for you: "All the code in this article will use the ATL way". But don't worry! The demo project includes equivalent programs for all the four methods. And I will discuss all the solutions to the problem, one for each method (it's just the code that will be presented in ATL).

10. Fun with SIDs

Q. Retrieve the SID for the LocalSystem account. Print the SID in textual form, and dump it into a TRUSTEE structure.

The LocalSystem account is a special NT user that represents the username for the kernel and system services. In English Windows, it has the name "NT AUTHORITY\SYSTEM" (English Windows only), and generally has unrestricted access to your local workstation.

  1. If you were doing this the low level way, you would need to perform some hacks. You'll note that NT names the LocalSystem account "NT AUTHORITY\SYSTEM". By passing this name to LookupAccountName(), you can retrieve the SID for the LocalSystem account. You can then make use of this function [^] to print the SID. A TRUSTEE only works with APIs not available in NT3.x, and since the only reason you'd want to choose this method is to support NT3.x a TRUSTEE is rather useless to you.
  2. In Windows 2000, you can cheat a little by utilizing SDDL. The SDDL form of LocalSystem is "SY". Passing this string to ConvertStringSidToSid() gives you the required SID. Then you just have to use BuildTrusteeWithSid() to make the TRUSTEE.
  3. You can retrieve the SID of the system from the SID's namespace. Now, that you have the SID, you'll probably want to wrap it in ATL's CSid class. Printing out the SID is really easy with ATL, just print the output of the CSid::Sid() method, and that's it. The last thing to do, is fill out a TRUSTEE, that can be accomplished with BuildTrusteeWithSid().
int WellKnownSid2Trustee(void)
{
  /* Wrap up the object in a CSid */
  ATL::CSid SidUser(ATL::Sids::System());

  std::wcout << SidUser.Sid();

  TRUSTEE TrusteeSid = {0};
  ::BuildTrusteeWithSid(&TrusteeSid, 
       const_cast<SID *>(SidUser.GetPSID()));

  return 0;
}

Figure 9: Converting a well known SID into a TRUSTEE

Q. You have obtained your username from your thread token. Unfortunately, it's in SID form. It's needed in a user friendly form. Convert the SID to a user name.

  1. If you had no trouble with fig. 8, this exercise should be of little trouble. It's basically the reverse of the above exercise. You are supplied a SID, so all you need to do is call LookupAccountSid(). The returned user name can be formatted in SAM form, alias form, or domain form (dependent on the value of SidType).
  2. To show there is more than one way to skin a cat, WMI will be used to obtain the user. After you have connected to the "root\cimv2" namespace, you would want to execute the following query: SELECT * FROM Win32_UserAccount WHERE Sid = "<SidUser>" (replace <SidUser> with the supplied SID). This query should return one result, which you can open to get the Win32_UserAccount associated with this user. The user name is located in the Name property, so retrieve it and finally convert the BSTR into a normal string. Phew! You don't have to do all that. If you don't want to, you can simply duplicate method 1. (I included WMI as a solution to get you considering WMI as an alternative API for programming Windows security).
  3. It's highly recommended you wrap the SID in a CSid class. That way you can simply use the methods of the CSid class to obtain your username (in this case CSid::AccountName()).
void Sid2UserName(const SID *UserSid)
{
  ATL::CSid UserCSid(UserSid /*, Domain */);
  /** This line would be unnecessary if we passed in a
  * CSid rather than an unwrapped SID
  **/
  std::wcout << UserCSid.AccountName();
}

Figure 10: Getting the Username from a SID.

11. What Groups are you a member of?

Q. You need to determine if the current user is an administrator. There's an action you need to perform that doesn't work if your program is not run as an administrator.

First, why do you need to know if you are an administrator? If there's a directory or registry key that you are denied access to, why not check the security descriptor of that folder? If there's an API that doesn't work, it may be because you don't have the right privilege enabled. Second, what are you doing that requires administrative privileges? Are you trying to access Program Files (write to "Application data" or "My documents" instead)? Or are you trying install some kind of malware (which requires admin privileges to do its damage)? Third, why do you have to be an Administrator specifically? Why can't it be someone like Power User or Domain Account Operator? Fourth, note that you can accomplish this by using the Net*Info functions, or by calling WMI, or even using the Shell provided function: IsUserAnAdmin().

  1. You can get your entire group membership from the thread token (or process token). Open up your thread token (or process token if that fails), then call GetTokenInformation(TokenGroups) on the returned token. With the list of groups returned, look up the group SID, then see if that's the SID for the Administrators group.
  2. This is easier in Windows 2000 now that you have CheckTokenMembership() instead of having to read the entire list of groups. This is the method that IsUserAnAdmin() uses to check if you are an admin.
  3. ATL wraps CheckTokenMembership(), the Access Token and the Groups into its CAccessToken class. It's just a matter of supplying the correct SID for the Administrators group. Getting your token is easier in ATL, thanks to the GetEffectiveToken() method.
int IsAdminRunning(void)
{
  bool IsMember = FALSE;
  ATL::CAccessToken ProcToken;
  ATL::CAccessToken ImpersonationToken;
  ATL::CSid UserSid(Sids::Admins());

  ProcToken.GetEffectiveToken(TOKEN_READ | TOKEN_DUPLICATE);
  ProcToken.CreateImpersonationToken(&ImpersonationToken);
  ImpersonationToken.CheckTokenMembership(UserSid, &IsMember);

  return IsMember;
}

Figure 11: Determining if the user is an admin.

If you're dealing with a domain situation, you also need to lookup the domain administrators SID ("DA" in SDDL). A sample in the Platform SDK contains another sample solution.

12. Enabling Token Privileges

Q. You need to read and edit the SACL of an object, but you can only read SACLs by having the "Manage auditing and security log" policy enabled. How do you enable this "SeSecurityPrivilege"?

  1. Inside the Platform SDK is a function called SetPrivilege(). This sample code will allow us to enable the "SeSecurityPrivilege" (provided the administrator has allowed us), and therefore read SACLs. This function simplifies the task of enabling the privilege into just one line. Before you copy it though, you'll want to add error handling to the function, and make the function call OpenThreadToken() itself.
  2. Use method 1.
  3. The ATL team found the SetPrivilege() function so useful, they created a method called CAccessToken::EnablePrivilege(), which is just SetPrivilege() ATL style.
void SetPrivilege(
     const ATL::CStringT<TCHAR,
     ATL::StrTraitATL<TCHAR> > &lpszPrivilege,
     bool bEnablePrivilege)
{
  ATL::CAccessToken ProcToken;
  ProcToken.GetEffectiveToken(TOKEN_QUERY |
    TOKEN_ADJUST_PRIVILEGES);

  if(bEnablePrivilege)
  {
    ProcToken.EnablePrivilege(lpszPrivilege);
  }
  else
  {
    ProcToken.DisablePrivilege(lpszPrivilege);
  }
}

Figure 12: Enabling a group policy privilege.

Group policy privileges are turned off by default, even when enabled in group policy. You have to turn them on by changing your access token. You should be prepared to handle the case when the privilege is disabled in group policy (and you can't turn it on, no matter how hard you try). When you've finished with the privilege, don't forget to turn it back off.

Q. Print out the current user, the list of available privileges, the list of restricted SIDs and the list of groups from a token.

This is a Whoami clone. To make life easier, we won't decrypt the attributes from a number to text (unlike what Whoami does).

  1. You can get the needed information by calling GetTokenInformation() with the TokenInformation parameter set to TokenGroupsAndPrivileges. Then it is simply a matter of printing out the contents of the returned structure.
  2. See method 1.
  3. To get the groups from the token, call the CAccessToken::GetGroups() method, to return a CSidArray and a CAtlArray of DWORDs (the SIDS_AND_ATTRIBUTES map). This list contains a merged view of mandatory SIDs and restricted SIDs. You can use CAccessToken::GetPrivileges() to get similar results for the privileges.
void DoWhoAmI(void)
{
  size_t i = 0;
  ATL::CAccessToken ProcToken;
  ATL::CSid SidUser;
  ProcToken.GetEffectiveToken(TOKEN_QUERY);

  /* First print off the user. */
  ProcToken.GetUser(&SidUser);
  std::wcout << _T("Owner: ") << 
        SidUser.AccountName() << _T("\r\n");

  /* Now print the groups */
  ATL::CTokenGroups pGroups;
  ProcToken.GetGroups(&pGroups);
  ATL::CSid::CSidArray pSids;
  ATL::CAtlArray<DWORD> pAttributes;
  pGroups.GetSidsAndAttributes(&pSids, &pAttributes);

  /* Iterate both pSids and pAttributes simultaneously */
  std::wcout << _T("\r\nGroups\r\n");
  for(i = 0; i < pGroups.GetCount() ; i++)
    std::wcout << pSids[i].AccountName() << _T(": ") <<
        pAttributes.GetAt(i) << _T("\r\n");

  /* Get the list of Privileges */
  ATL::CTokenPrivileges pPrivileges;
  ProcToken.GetPrivileges(&pPrivileges);
  ATL::CTokenPrivileges::CNames pNames;
  ATL::CTokenPrivileges::CAttributes pGroupAttributes;
  pPrivileges.GetNamesAndAttributes(&pNames, &pGroupAttributes);

  /* Printing Privileges is very similar to */
  std::wcout << _T("\r\nPrivileges\r\n");
  for(i = 0; i < pGroups.GetCount() ; i++)
  std::wcout << static_cast<LPCTSTR>(pNames.GetAt(i))
  << _T(": ") << pGroupAttributes.GetAt(i) << _T("\r\n");

  /** TODO: the DWORDs are printed out as numbers. Convert these
  * DWORDs into text, the same text that whoami displays.
  **/
}

Figure 13: Regenerating the information from Whoami.

Q. How do you run IE with low rights in Windows XP / Server 2003?

This technique only applies to just Windows XP and Server 2003. The next version of Windows will change this technique.

  1. An example of using this method is not available. You'd have to resort to method 2 or 3 if you want to implement this.
  2. You can either use the CreateRestrictedToken() function to handle the necessary tasks, or for XP and above, you can utilize the Software Restriction Policies (SAFER for short). The SAFER functions are basically a set of predefined restricted tokens you can use to lower the rights of a process token.
  3. ATL has encapsulated the list of privileges into a CAtlArray, which makes it quite easy to iterate and disable the privileges. It's just as easy to create restricted tokens. However, these tokens can be a little too restrictive (restrictive enough to prevent the application initializing). Therefore, you may want to consider using the Software Restriction Policies as an alternative.

For method 2, I wrapped the SAFER routines into a class (to abstract object management from the caller).

class SaferRaiiWrapper {
public:
  /** Error handling has been added in the
  *downloadable version of this class
  **/
  explicit SaferRaiiWrapper(
    const DWORD dwScopeIdIn = SAFER_LEVELID_NORMALUSER,
    const HANDLE hTokenIn = NULL) : hToken(hTokenIn),
    LevelHandle(NULL), dwScopeId(dwScopeIdIn)
  {
    ::SaferCreateLevel(SAFER_SCOPEID_USER, this->dwScopeId,
                       SAFER_LEVEL_OPEN, &LevelHandle, NULL);
    ::SaferComputeTokenFromLevel(this->get_LevelHandle(),
                       NULL, &hToken, NULL, NULL);
  } ;


  virtual PROCESS_INFORMATION CreateProcessAsUser(const
    const std::basic_string<TCHAR> &lpCommandLine,
    STARTUPINFO *lpStartupInfoIn = NULL,
    DWORD dwCreationFlags = CREATE_NEW_CONSOLE,
    const std::basic_string<TCHAR> &lpApplicationName = _T(""),
    const std::basic_string<TCHAR> &lpCurrentDirectory = _T(""),
    LPVOID lpEnvironment = NULL, BOOL bInheritHandles = FALSE,
    SECURITY_ATTRIBUTES *lpProcessAttributes = NULL,
    SECURITY_ATTRIBUTES *lpThreadAttributes = NULL)
  {
    STARTUPINFO StartupInfoAlt = {0};
    LPSTARTUPINFO lpStartupInfoActual = (lpStartupInfoIn != NULL) ?
    lpStartupInfoIn : &StartupInfoAlt;
    PROCESS_INFORMATION Result = {0};

    TCHAR *lpCmdLineWritable = new TCHAR[sCmdLine.capacity() + 1];
    /** The command line needs to be writable.
    * So make a writable copy of our command line.
    **/
    sCmdLine.copy(lpCmdLineWritable, sCmdLine.size());
    lpCmdLineWritable[sCmdLine.size()] = _T('\0');

    lpStartupInfoActual->cb = sizeof(STARTUPINFO);
    lpStartupInfoActual->lpDesktop = NULL;
    ::CreateProcessAsUser(this->hToken,
      (sAppName.empty() ? NULL : sAppName.c_str()),
      lpCmdLineWritable, lpProcessAttributes,
      lpThreadAttributes, bInheritHandles,
      dwCreationFlags, lpEnvironment,
      (sCurDir.empty() ? NULL : sCurDir.c_str()),
      lpStartupInfoActual, &Result);

    delete [] lpCmdLineWritable;

    return Result;
  } ;

  HANDLE get_hToken(void) const
  {
    return hToken;
  } ;

  virtual ~SaferRaiiWrapper()
  {
    ::CloseHandle(this->hToken);
    ::SaferCloseLevel(this->LevelHandle);
  } ;

protected:

  const SAFER_LEVEL_HANDLE &get_LevelHandle(void) const
  {
    return LevelHandle;
  } ;
  void set_LevelHandle(const SAFER_LEVEL_HANDLE &LevelHandleIn)
  {
    this->LevelHandle = LevelHandleIn;
  } ;

  void set_hToken(const HANDLE hToken)
  {
    this->hToken = hToken;
  } ;

private:
  HANDLE hToken;
  SAFER_LEVEL_HANDLE LevelHandle;
  const DWORD dwScopeId;
};

Figure 14: Creating a restricted token using the Software Restriction Policies.

13. Dissecting the Security Descriptor

Q. How do you obtain the security descriptor for a folder?

The question doesn't specify what specific information it wants to be returned in the security descriptor, so we will assume it wants the whole lot returned in the security descriptor (Control, SACL, DACL, Group, and Owner).

In order to read the SACL, you must first have the SeSecurityPrivilege enabled in your token (use the handy SetPrivilege() function from fig. 10 for this).

  1. Do not attempt to use this method if you are on Windows 2000 or later (detect the Windows version, and branch out to separate code instead), otherwise you will trash the security on the operating system. To obtain the security descriptor for a file, call the GetFileSecurity() API (or GetKernelObjectSecurity() if you already have a handle).
  2. If you have the file open, call GetSecurityInfo() on the opened file handle. Otherwise call GetNamedSecurityInfo() on the filename. Since we're only getting the security descriptor, we can set the other parameters to NULL. Don't forget to LocalFree() the security descriptor when you're done.
  3. The GetNamedSecurityInfo() API has been encapsulated into the global ATL function: AtlGetSecurityDescriptor(). It returns the information in a CSecurityDesc type for us. By default, AtlGetSecurityDescriptor() automatically enables the SeSecurityPrivilege for us, so there is no need to use SetPrivilege() here.
int GetFolderSecDesc(const CStringT<TCHAR, 
       ATL::StrTraitATL<TCHAR> > &FileName)
{
  ATL::CSecurityDesc OutSecDesc;
  ATL::AtlGetSecurityDescriptor(FileName, SE_FILE_OBJECT, &OutSecDesc);
  return 0;
}

Figure 15: Obtaining the security descriptor for a folder.

GetNamedSecurityInfo() can also be used to read security descriptors from registry keys, kernel objects, window stations, and other objects. For a complete list of objects supported by GetNamedSecurityInfo(), see section 17 or your help documentation for SE_OBJECT_TYPE [^]. If your object is not supported by GetNamedSecurityInfo(), then open a handle yourself (with READ_CONTROL access), and pass it to the GetSecurityInfo() function.

The returned security descriptor will be in self-relative form. If you are going to enumerate the security descriptor, it will be easier if the security descriptor was absolute.

Q. Convert a self relative security descriptor to an absolute one.

  1. You will have to call the MakeAbsoluteSD() API to make the security descriptor absolute. The MakeAbsoluteSD() function does not allocate the buffers for you, you must allocate them yourselves. There are five buffers you have to manage, just for one security descriptor! And if you have to pass the security descriptor back to the operating system (as will happen when we reach part 4 [^]), the chances of leaking memory become very likely. You could maintain five global variables to keep track of the buffers, or you can allocate one large block of memory, and with some creative pointer fix ups, set the other buffers to point inside this big buffer (this technique is described in the C FAQ [^]). Now with the buffer allocated, and your pointers pointing to big enough memory locations, the next call to MakeAbsoluteSD() should work.
  2. If your self-relative security descriptor is going to stay in scope throughout this task, then you can build the absolute security descriptor yourself. Using functions like GetSecurityDescriptorDacl(), GetSecurityDescriptorOwner() and friends will give you the pointers you need.
  3. ATL contains the CSecurityDesc::MakeAbsolute() method that makes converting security descriptors far easier. What's more, you no longer need to worry about managing buffers; ATL handles the buffers for you. Note that most of the reasons to convert a security descriptor aren't necessary with ATL. (The ATL security classes can handle absolute security descriptors as well as self relative security descriptors.)
...
OutSecDesc.MakeAbsolute();
...

Figure 16: Converting a self relative security descriptor to an absolute security descriptor.

It's much easier to do the reverse (i.e. convert an absolute security descriptor to a self relative one). The reason is because an absolute security descriptor has to maintain five buffers to work (or in our case, a heap of five pointers), whereas a self relative security descriptor only needs to maintain one buffer. The good news is that unless you need to work with method 1, converting security descriptors is rarely required.

You may have been asking why not allocate a buffer of the same size as the self-relative security descriptor, reinterpret_cast it to an absolute security descriptor, then convert the offset index into physical pointers. The problem is that you are assuming indexes that are of the same size as the pointers. This is not true on Win64, and attempting to do so will lead to errors (yes, Microsoft should have made the DWORD indexes in the self relative security descriptor size agnostic, but now we're stuck with that 17+ year old mistake).

Q. You have been supplied with a security descriptor. You now need to print out the contents of the security descriptor.

Although it's not mentioned, this question wants the security descriptor in either debugger form, or SDDL form.

  1. First of all, make sure the security descriptor is in absolute form (it will be easier to read that way). There are a set of functions that you can use to obtain the security descriptor parts. They are GetSecurityDescriptorLength(), GetSecurityDescriptorControl(), GetSecurityDescriptorOwner(), GetSecurityDescriptorGroup(), GetSecurityDescriptorDacl(), and GetSecurityDescriptorSacl(). These functions return the length, control bits, owner, group, DACL and SACL respectively (whether the security descriptor is self relative or absolute). With the group and owner obtained, you should now print out the SID in textual form. Printing out the Access Control lists will be covered later.
  2. You will have the benefit of SDDL in your case. Once you have the security descriptor, you can call ConvertSecurityDescriptorToStringSecurityDescriptor(). This will convert the security descriptor (absolute or self relative) into an SDDL string, which you can print out.
  3. ATL can convert a CSecurityDesc into an SDDL string using the CSecurityDesc::ToString().
...
ATL::CString pstr = _T("");
OutSecDesc.ToString(&pstr);
std::wcout << static_cast<LPCTSTR>(pstr);
...

Figure 17: Printing out the contents of the security descriptor.

Now that you have the security descriptor presented in a uniform way (SDDL), you have reduced the task of parsing a security descriptor into a text processing task.

14. Walking an access control list.

Q. You have been supplied with an access control list. Turn this ACL into an array of access control entries.

The result should be a table with three columns: SID, (deny | allow | audit | alarm) inheritance, and ACCESS_MASK.

  1. To walk a list of access entries in NT3.x, you first have to read off the ACL headers to get the count of ACEs (do not continue if you find out there are zero entries). To retrieve a pointer to the nth Access Control entry, you'll need to call GetAce() on the ACL. This functions returns a void*. The first byte at the pointer identifies the exact type of structure (this reminds me of a primitive version of RTTI). Once you have cast the void* into the correct structure, you can now obtain the required details from this structure. The SID is located at the SidStart member (cast this member to a SID). To check for a deny or allow, check the name of your structure (is it an ACCESS_ALLOWED_ACE struct, or an ACCESS_DENIED_ACE?). The exact type of inheritance can be obtained from the AceFlags. The last item (the access mask) can be obtained from the Mask member. Repeat this process for all ACEs.
  2. It will be easiest if you convert the security descriptor to SDDL form. Then you can perform text processing on the returned security descriptor and print out the required contents from the SDDL.
  3. The CDacl and CSacl classes are derived from CAcl. You can then either obtain the columns of ACLs by calling the GetAclEntries() method (returns four arrays: SID, Access mask, type, and inheritance). Alternatively, you can loop through the ACL by row, by calling GetAclEntry(). Personally, I'd rather decrypt the ACL to SDDL form and print it there.

If the ACL is a system access control list, you would not get allow / deny entries. Instead you will audit / alarm entries in the SACL. To make your walker function read from SACLs as well as DACLs, extend your walker to handle the audit and alarm ACE structs (the walker function given in the sample code can handle SACLs equally as well as DACLs).

void ReadDacl(const ATL::CDacl &pDacl)
{
  UINT i = 0;
  for(i = 0; i < pDacl.GetAceCount(); i++)
  {
    ATL::CSid pSid;
    ACCESS_MASK pMask = 0;
    BYTE pType = 0, pFlags = 0;
    const_cast<ATL::CDacl &>(pDacl).GetAclEntry
        (i, &pSid, &pMask, &pType, &pFlags);
    std::wcout << pSid.AccountName() << _T(": ");
    switch (pType)
    {
      case ACCESS_ALLOWED_ACE_TYPE:
        std::wcout << _T("allow");
        break;
      case ACCESS_DENIED_ACE_TYPE:
        std::wcout << _T("deny");
        break;
      case SYSTEM_AUDIT_ACE_TYPE:
        std::wcout << _T("audit");
        break;
      case SYSTEM_ALARM_ACE_TYPE:
        std::wcout << _T("alarm");
        break;
      /* ... TODO: Repeat for the other structures */
      default:
        std::wcout << _T("Unknown");
        break;
    }
    std::wcout << _T(": ");
    if(pFlags & INHERITED_ACE)
      std::wcout << _T("Inherited: ");
    std::wcout << std::hex << 
         pMask << std::dec << std::endl;
  }
  std::wcout << std::endl;
}

Figure 18: Reading and printing a discretionary access control list.

15. Do I Have Access?

Q. You need to determine if a specific security descriptor will allow you to access an object without getting the dreaded error 5 (ERROR_ACCESS_DENIED). How do you do that?

A naive implementation of this would be to look up your username in the security descriptor and directly check which accesses are granted and which are denied (GetEffectiveRightsFromAcl() can help). There are two problems using this technique.

  • Your username may not actually appear in the security descriptor—rather, your group appears in it instead.
  • One single entry may not grant you the desired access. It could be two access control entries, one that grants you some of the desired access, and one that grants the rest of the access.

The only reliable way of checking this is to actually perform the action (i.e. open the file and read it). If you succeed, you are granted access. If you fail with an error 5, you are denied access. However, if you really must...

To check if a security descriptor grants you access, you require a call to the AccessCheck() API. The AccessCheck() API is a simplified form of the AccessCheckByTypeResultListAndAuditAlarmByHandle() API. The AccessCheckByTypeResultListAndAuditAlarmByHandle() forms the heart of the entire Windows Access Control Model. All the security APIs and objects are just a way to configure the behaviour of this little API (okay, maybe not so little!). But for our purposes, AccessCheck() should suffice. AccessCheck() may at first sight seem intimidating, but if you look at it closely, it just takes in three parameters, the security descriptor, You (your thread token), and what action you desire (the wanted access mask). The rest of AccessCheck() are just Out parameters.

You'd think that Windows can make it easier for you by making these three parameters optional. Why can't AccessCheck() just get the current thread token as default, then you pass in the filename, and AccessCheck() will look up its security descriptor itself. That's just one In parameter. Oh wait, that's just CreateFile()! That leads us back to what we first said.

Actually, AccessCheck() requires you to supply a fourth parameter, the GENERIC_MAPPING structure. This structure maps the object specific ACLs (like GENERIC_READ) into object specific rights (like FILE_GENERIC_READ). The reason why AccessCheck() needs a GENERIC_MAPPING is because it makes a call to the AreAllAccessesGranted() function, and this requires you to supply a GENERIC_MAPPING structure. Larry Osterman [^] offers a more complete reason why the GENERIC_MAPPING is required.

  1. To check a security descriptor for access, first create a GENERIC_MAPPING structure.
  2. Gather up this structure, your security descriptor, the desired access, and your thread token.
  3. Make the first call to AccessCheck(). We expect this to fail.
  4. If the call failed with an ERROR_INSUFFICIENT_BUFFER, allocate a buffer for the PRIVILEGE_SET structure.
  5. Call AccessCheck() again with the new buffer.
  6. Check the result in the AccessStatus parameter. If true, check if the GrantedAccess member is equal to the desired access.
  7. If anything goes wrong, access is denied.

As was discussed in part 1, it is possible to make the access check yourself. The ten steps involved were:

  1. Open your token (thread or process) with OpenThreadToken().
  2. Call GetTokenInformation(TokenGroups) to retrieve the list of groups (as obtained in fig. 11).
  3. From your supplied security descriptor, access the DACL. (See fig. 15.) If it is null, you should use the DACL: Everyone (Full Control).
  4. Get the nth ACE (as shown in fig. 18).
  5. Get the SID associated with this ACE (as shown in fig. 18).
  6. Lookup this SID in the list of TOKEN_GROUPS array you obtained in step 2.
  7. Go back to the ACE and look up its type and access mask (see fig. 18).
  8. Map out any generic access rights to the supplied GENERIC_MAPPING structure using MapGenericMask().
  9. Compare the present access mask with the desired access mask.

    To compare two ACCESS_MASKs, simply NOT one of the ACCESS_MASKs, then AND the two variables together. The result should be zero if you are granted access, otherwise you should be denied. Or you can make a call to AreAllAccessesGranted() to help you. (This API has the advantage of helping you fix-up generic access rights.)

  10. If the desired access mask is covered by the ACE, you are granted access.
  11. If the ACE does not completely allow access, clear out the granted accesses and continue the search.
  12. If you are at the end, deny access.

You could perform the 11 above steps, or you can use the AccessCheck() provided function. There isn't anything special that Windows 2000 or ATL provides to make this task easier; this technique is the same for all operating systems.

{
  ATL::CAccessToken ProcToken, ImpersonationToken;
  ProcToken.GetEffectiveToken(TOKEN_QUERY |
    TOKEN_DUPLICATE | TOKEN_IMPERSONATE);
  ProcToken.CreateImpersonationToken(&ImpersonationToken);

  {
    BOOL AccessStatus = FALSE;
    DWORD GrantedAccess = 0, PrivilegeSetLength = 0,
    DesiredAccess = FILE_GENERIC_WRITE;
    GENERIC_MAPPING GenericMapping =
      {
        READ_CONTROL | FILE_READ_DATA |
        FILE_READ_ATTRIBUTES | FILE_READ_EA,
        FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA |
        FILE_WRITE_DATA | FILE_APPEND_DATA,
        READ_CONTROL | FILE_READ_ATTRIBUTES |
        FILE_EXECUTE,
        FILE_ALL_ACCESS
      } ;

    ::AccessCheck(const_cast<SECURITY_DESCRIPTOR *>
    (OutSecDesc.GetPSECURITY_DESCRIPTOR()),
      ImpersonationToken.GetHandle(), DesiredAccess, &GenericMapping,
      NULL, &PrivilegeSetLength,
      &GrantedAccess, &AccessStatus);

    ATL::CAutoVectorPtr<BYTE> PrivilegeSet
      (new BYTE[PrivilegeSetLength]);

    ::AccessCheck(const_cast<SECURITY_DESCRIPTOR *>
    (OutSecDesc.GetPSECURITY_DESCRIPTOR()),
      ImpersonationToken.GetHandle(), DesiredAccess,
      &GenericMapping, reinterpret_cast<PPRIVILEGE_SET>
    (static_cast<BYTE *>(PrivilegeSet)), 
      &PrivilegeSetLength, &GrantedAccess,
      &AccessStatus);
    if(AccessStatus == TRUE)
    {
      std::wcout << std::hex <<
      GrantedAccess==DesiredAccess << std::dec;
    }
  }
}

Figure 19: Verifying if a security descriptor grants you access to an object

16. Creating a discretionary access control list.

This part assumes you already know what your Access Control list contains (refer back to part 1 [^] for advice on choosing a good DACL). Please note, that each security descriptor has a tightly-coupled relationship with the object it is securing. The reason is that each ACE bears an ACCESS_MASK member, an object dependent value.

Q. You now know the contents of your discretionary Access Control List. You are now required to build it.

Here is the example ACL we will build from. This is a typical DACL for a file under the user profile:

Allow LocalSystem: Full Control (FILE_ALL_ACCESS), and propagate to all children. 
Allow Admins: Full Control (FILE_ALL_ACCESS), and propagate to all children.
Allow CurrentUser: Read Write & Execute (FILE_GENERIC_READ | 
FILE_GENERIC_EXECUTE | FILE_GENERIC_WRITE), and propagate to all children.

Figure 20a: Build this example DACL.

In SDDL that is:

"(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;GRGX;;;<CurrentUserSid>)"

Figure 20b: The example DACL in SDDL

  1. This is perhaps the toughest part of Access Control. If you are editing a security descriptor rather than creating one from scratch, you need to get the old security descriptor first. Then when you are adding the entries for the DACL, make sure you add the ACEs in the preferred order of ACEs.
    1. Calculate the total size for the ACL (the ACL needs to be a contiguous block that can hold an ACL structure, the size of all the simple ACEs minus the SidStart member, the size of all the object ACEs minus the SidStart member, and the size of all the SIDs).
    2. Allocate a buffer for the ACL_HEADER, which will most likely have to be LocalAlloc()ed.
    3. If you are building a security descriptor from scratch, call InitializeAcl() to initialize the ACL headers. Otherwise, you can copy the information from an existing ACL.
    4. Build up an array of your access denied ACEs.
    5. Reallocate your ACL so it can hold this ACE array after it.
    6. (may not require this step) Get a pointer to the free space after your ACL (either by calling FindFirstFreeAce(), or by moving the pointer yourself).
    7. Add the Denied ACEs to the ACL by calling AddAccessDeniedAce().
    8. Repeat steps 3-7 for the Allowed ACEs (call AddAccessAllowedAce() instead of AddAccessDeniedAce()).
    9. With the ACL now built, set the Dacl member of the absolute security descriptor to this member by calling SetSecurityDescriptorDacl().

    It is interesting to note that this is the only method of the three that can make unordered DACLs, and NULL DACLs.

  2. In Windows 2000, editing a DACL is as simple as appending text to a string. Build up your access control list from an SDDL string. Once you have built up your SDDL, call the ConvertStringSecurityDescriptorToSecurityDescriptor() function to build a security descriptor. This will give you a security descriptor. Then just extract the DACL using GetSecurityDescriptorDacl(). If you are editing an existing security descriptor, you can either start from scratch, building an all new DACL, or you can take the existing SDDL, and build from there.
  3. You can build a security descriptor in ATL either by supplying your SDDL to CSecurityDesc::FromString(), or you can build it up using the CDacl class. If you are editing a security descriptor, you should obtain the security descriptor first and call its GetDacl() method. You can obtain a Dacl directly from an object by calling AtlGetDacl(). Otherwise instantiate a new Dacl object yourself. Regardless of the way it was created, you call AddDeniedAce() to add an access denied entry, then you call AddAllowedAce() to add an access allowed entry.
...
pDacl.AddAllowedAce(ATL::Sids::LocalSystem(), FILE_ALL_ACCESS,
  CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE);
pDacl.AddAllowedAce(ATL::Sids::Admins(), FILE_ALL_ACCESS,
  CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE);
pDacl.AddAllowedAce(ATL::CSid(CurrentUser), FILE_GENERIC_READ |
  FILE_GENERIC_WRITE | FILE_GENERIC_WRITE, CONTAINER_INHERIT_ACE |
  OBJECT_INHERIT_ACE);

ATL::AtlSetDacl(FileName, SE_FILE_OBJECT, pDacl);
...

Figure 20c: Creating the access control list to apply to a file.

17. Creating a Secure object

Q. This is all very well and good for a discretionary access control list, but what about a system access control list?

Apart from administrative and/or troubleshooting purposes, you never need a system Access Control list (I have not yet encountered an SACL other than for test purposes). Anyway, you can only set an SACL if you have the SeSecurityPrivilege enabled. If your object supports inheritance, just get the SACL from the parent. Otherwise, your SACL should be NULL or empty. Remember, if you generate an audit every time you access an object (particularly if you frequently access the object), your Security Event Log will fill up with information overload.

To read and write an SACL, you will need to enable the SeSecurityPrivilege. Adding entries to an SACL is similar to creating a DACL.

  1. To add ACEs in low level instead of calling AddAce(), you call the AddAuditAccessAce() functions.
  2. In SDDL, a SACL is just a DACL with an S in front of it! Audit ACE strings starting with an "AU" rather than an "A" and alarms start with "AL".
  3. The only difference between a CSacl and a CDacl is the way they add ACEs. Otherwise, the rest of the methods are the same.
...
pSacl.AddAuditAce(ATL::Sids::Users(), WRITE_DAC, true, false);
...

Figure 21: Adding a system access control entry to an SACL.

Q. You have now created both a DACL and an SACL. Now how do you deal with the other members?

This section will cover securing a new object only. For a new object, the object by default has no owner. You can specify an owner by filling the Owner member of the security descriptor. A suggested new owner is you (you from your thread token) or your group. However, any user that has WRITE_OWNER access to the object can take ownership of it (and thus get full control to it).

Few apps read the Group part of the security descriptor, but just in case there are, this should be set to the primary group of your token—obtained from GetTokenInformation(TokenPrimaryGroup).

The Control member is a collection of flags dumped into a 32 bit integer. Windows will use this parameter to determine which members of the security descriptor are valid. If you are securing an object that supports inheritance, there are extra flags you need to set in the Control member of the security descriptor. If you get one of the flags wrong in this member (e.g.. you say that the group is valid when it in fact isn't), then you may crash.

To set these members, follow your chosen method:

  1. In Fig. 17, we discussed a class of functions that could obtain the pieces of a security descriptor. Those functions have corresponding Set functions that, yes you guessed it, set the parts of the security descriptor. These functions work only if the security descriptor is being built absolute (see how useful it was to create an absolute security descriptor from the start?). The SetSecurityDescriptorControl() API was made deliberately tricky so as to prevent you from accidentally turning an absolute security descriptor to a self relative security descriptor. You must supply both the replacement value, and the control bits you intend to change.
  2. If you converted the owner and group SIDs to usernames, you'll have to convert them back. Then to build the owner, append the characters "O:" and the string SID to your SDDL. For the group, append "G:" instead of "O:" first. The control bits are set directly in the "D:" and "S:" tokens.
  3. Once your members have been built up, you can set the security descriptor parts by calling the following methods: CSecurityDesc::SetControl(), CSecurityDesc::SetGroup(), CSecurityDesc::SetOwner(), CSecurityDesc::SetDacl(), CSecurityDesc::SetSacl().
...
OutSecDesc.SetOwner(ATL::Sids::Admins(), false);
OutSecDesc.SetGroup(ATL::Sids::Admins(), false);
...

Figure 22: Finalizing the security descriptor for a new object.

Q. How does Inheritance come into this?

The inheritance of a security descriptor appears in two places. Each ACE has an inheritance flag that specifies how inheritance is applied to child objects / containers. These flags (the IO, OI, CI, etc.) were described in part 1. This same flag also determines if an ACE came from a parent ACL (which you can detect with the INHERITED_ACE flag).

The Control member determines if the DACL and SACL auto-inherit ACEs from their parent. By setting the SE_DACL_AUTO_INHERITED | SE_DACL_AUTO_INHERIT_REQ control flags in a security descriptor, Windows will get the parent's DACL, attach it to the end of your DACL, and write this merged DACL to the object. These inherited ACLs cannot be edited—if you want to change something in the inherited ACL, you must either add in a deny ACE to override the inherited ACE, or stop inheriting.

To prevent inheritance, you must set your DACL to be protected (SE_DACL_PROTECTED). If you set this flag, only the explicit entries remain, meaning you may have to copy the parent's DACL into the object to get the old DACL.

If you have a protected DACL, and you want to stop it from being protected, you should empty the DACL, then set the SE_DACL_AUTO_INHERIT_REQ Control bit. This will disable DACL protection and enable auto-inheritance.

  1. Inheritance is not supported by the low level methods. This should be reason enough to consider one of the other methods. At the very least, you'll have to branch out to separate code paths for different Windows versions, essentially creating two versions of your program.
  2. You can obtain inheritance information from an SDDL ACE string. In the second token, of an ACE string, there are flags that determine the inheritance of that ACE (which can be "IO", "OI", "CI", "ID" and "NP"). For the control bits, you can set the auto inheritance by post fixing the DACL delimiter with "AI", for example: D:AI(A;ID;FA;;;SY).

    The "AI" after the "D:" tells SDDL to set SE_DACL_AUTO_INHERITED in the Control bits. If instead of "AI", you had "P", then SE_DACL_PROTECTED will be set instead. The last flag, "AR", corresponds to SE_DACL_AUTO_INHERIT_REQ.

  3. You can edit the security descriptor control flags directly with CSecurityDesc::SetControl(). This function requires you to supply two parameters. One is the new value of the Control member, and the other is the flags you wish to set. This is to prevent you from accidentally changing a security descriptor from self-relative to absolute.
...
OutSecDesc.SetControl(SE_DACL_AUTO_INHERITED |
SE_DACL_PROTECTED, SE_DACL_AUTO_INHERITED);
...

Figure 23: Supporting inheritance for security descriptors.

The inheritance rules for SACLs are the same as DACLs.

Q. Create a Windows NT object that bears your security descriptor.

When you created a Windows object, you encountered a parameter asking for a SECURITY_ATTRIBUTES structure (unless you encountered a wrapper class / function for the resource). This is where you will supply the security descriptor for the new object. Note that some objects do not support all the features of security descriptors (particularly inheritance)--in this case, these extra features will be ignored (which could lead to inaccessible objects if you're not careful). Once you pass this parameter in, that should be it. How the security descriptor gets stored is the object's problem, not yours. However, to prevent surprises, you should ensure that the object is created (not merely opened). If the object already exists, then the security descriptor is not applied, and the security attributes are ignored.

Generally, in order to access the SECURITY_ATTRIBUTES, you'll probably want to use the Win32 APIs directly. This is because most wrapper classes neglect the SECURITY_ATTRIBUTES parameter, and pass in NULL for this class. (CAtlFile is an exception, but by default, it passes in NULL too.)

if(::GetFileAttributes(FileName) == INVALID_FILE_ATTRIBUTES)
{
  SECURITY_ATTRIBUTES lpSecurityAttributes =
    {sizeof(SECURITY_ATTRIBUTES),
    const_cast<SECURITY_DESCRIPTOR *>
  (OutSecDesc.GetPSECURITY_DESCRIPTOR()), FALSE};

  ::SetLastError(ERROR_SUCCESS);
  /** We're going to use the low level Win32 APIs
  * instead of ATL::CAtlFile
  **/
  ATL::CHandle FileHandle (::CreateFile(FileName, GENERIC_ALL |
     READ_CONTROL | WRITE_DAC, 
     FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
     &lpSecurityAttributes,
     CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL));
  if( static_cast<HANDLE>(FileHandle) == NULL ||
      static_cast<HANDLE>(FileHandle) == INVALID_HANDLE_VALUE)
  {
    throw ATL::CAtlException(HRESULT_FROM_WIN32(::GetLastError()));
  }
}
/* The else part will be handled in fig. 24. */

Figure 24: Creating an object that has a custom security descriptor.

This technique doesn't work if the object already exists. If you open an existing object, the security descriptor is ignored.

Q. The object already exists. The current security descriptor needs to be edited, instead of created. How do we do that?

This question assumes you either know the name and type of the object you are securing, or you have a handle to that object. If you are securing an existing object, you can get away with creating an incomplete security descriptor (e.g.. a security descriptor without an owner or group). You tell Windows which parts of the security descriptor are valid by passing in a SECURITY_INFORMATION variable. For example, if you are just setting the DACL of an object, you can specify DACL_SECURITY_INFORMATION.

  1. In this method, the solution becomes specific to the object you are trying to set. If you take a deep look at the security documentation, you may be able to find the correct function to use. For example, to set the security descriptor for files, you need to call SetFileSecurity(). For registry keys it's RegSetKeySecurity(). None of the older APIs work with inheritance, so if you use these APIs on a Windows 2000 system, you can severely damage the inheritance.
  2. The preferred API for setting security descriptors is SetSecurityInfo(). Plus it supports inheritance (and multiple inheritance). Although this function first appeared in Windows NT 3.51, the early versions had some bugs meaning it could only be applied in limited circumstances. With Windows NT4 SP6, all these bugs have been fixed.

    If you cannot obtain a handle to the file (say, you don't have READ_CONTROL or WRITE_DAC access yet), you can call the similar SetNamedSecurityInfo() function to set the security for the file. However, the SetSecurityInfo() function can secure more objects than SetNamedSecurityInfo().

    SetNamedSecurityInfo() can set the security on the following objects:

    • SE_FILE_OBJECT: a file system object (file or directory).
    • SE_SERVICE: an NT service.
    • SE_PRINTER: a local or remote printer.
    • SE_REGISTRY_KEY: a registry key.
    • SE_LMSHARE: a NetBIOS share.
    • SE_KERNEL_OBJECT: a semaphore, event, mutex, waitable timer, or file mapping.
    • SE_WINDOW_OBJECT: a window station.
    • SE_DS_OBJECT: a specific property of a Directory Services object.
    • SE_DS_OBJECT_ALL: all properties of a Directory Services object.
    • SE_PROVIDER_DEFINED_OBJECT: a provider defined object.
    • SE_WMIGUID_OBJECT: a WMI object.
    • SE_REGISTRY_WOW64_32_KEY: a WOW32 registry key (works only inside a 64 bit application).

    For SetNamedSecurityInfo() to work, you have to split your security descriptor back into separate pieces. You can call GetSecurityDescriptorDacl() and friends to get these members (portably). One thing to note is that SetSecurityInfo() apparently doesn't allow you to set the control bits directly. Instead, you provide the control bits in the SECURITY_INFORMATION parameter. The Control bits to SECURITY_INFORMATION mapping is as follows:

    • SE_DACL_PRESENT --> DACL_SECURITY_INFORMATION.
    • SE_SACL_PRESENT --> SACL_SECURITY_INFORMATION (make sure you have the SeSecurityPrivilege).
    • SE_DACL_AUTO_INHERITED --> UNPROTECTED_DACL_SECURITY_INFORMATION.
    • SE_SACL_AUTO_INHERITED --> UNPROTECTED_SACL_SECURITY_INFORMATION (make sure you have the SeSecurityPrivilege).
    • SE_DACL_PROTECTED --> PROTECTED_DACL_SECURITY_INFORMATION.
    • SE_SACL_PROTECTED --> PROTECTED_SACL_SECURITY_INFORMATION (make sure you have the SeSecurityPrivilege).
    • SE_DACL_DEFAULTED --> [None]. SetSecurityInfo() doesn't care if the security descriptor is a default..
    • SE_SACL_DEFAULTED --> [None]. SetSecurityInfo() doesn't care if the security descriptor is a default..
    • SE_GROUP_DEFAULTED --> GROUP_SECURITY_INFORMATION.
    • SE_OWNER_DEFAULTED --> OWNER_SECURITY_INFORMATION.
    • SE_SELF_RELATIVE --> [None]. SetSecurityInfo() doesn't care if the security descriptor is self relative or absolute.

    Use this map to translate the control bits into SECURITY_INFORMATION flags. This new security descriptor will replace the old security descriptor. If this is not what you want (i.e. you want to add entries to the DACL rather than replace it), you have to merge the old entries with the new ones yourself, or you could apply auto-inheritance (these will be automatically applied onto the child).

    If you have the rights, the security descriptor should get applied to the object with the correct inheritance settings and sorted entries. The Set*SecurityInfo() does not support NULL DACLs, and will fail if you attempt to set one.

  3. Instead of calling SetSecurityInfo(), you are required to set the security descriptor parts one by one. To set the owner, you call AtlSetOwnerSid(). Similarly, you have AtlSetGroupSid(), AtlSetDacl() and AtlSetSacl() to set the group DACL and SACL. Internally, these functions call SetSecurityInfo(), so the rules for method 2 are the same for ATL. If you are in a debug build, ATL will flag a warning if you try to set a NULL DACL to the object.
...
ATL::AtlSetDacl(FileName, SE_FILE_OBJECT, pDacl);
ATL::CSacl pSacl;
/* We've already set the Dacl. Now set the SACL. */
OutSecDesc.GetSacl(&pSacl, &pbPresent);
if(pbPresent)
{
  ATL::AtlSetSacl(FileName, SE_FILE_OBJECT, pSacl);
}
ATL::CSid pOwner, pGroup;
if(OutSecDesc.GetOwner(&pOwner))
{
  ATL::AtlSetOwnerSid(FileName, SE_FILE_OBJECT, pOwner);
}
if(OutSecDesc.GetGroup(&pGroup))
{
  ATL::AtlSetGroupSid(FileName, SE_FILE_OBJECT, pGroup);
}

...

Figure 25: Applying the security descriptor to an existing object

In order for SetSecurityInfo() to succeed, you must have the WRITE_DAC right to set the DACL and WRITE_OWNER rights to set the owner. If you were denied the WRITE_OWNER | WRITE_DAC right, you can acquire these by taking ownership of the file. Enabling the SeTakeOwnershipPrivilege will allow you to take ownership of the file (even if WRITE_OWNER was disabled).

Although the Set*SecurityInfo() functions work with almost all of the Windows built-in objects (and even some special objects), you may want to implement security descriptors for your own classes.

18. Making Your Own Classes Secure

Q. This security descriptor model seems cool. Is there anyway I can use this to secure my own objects?

It is possible to have a class member that has type PSECURITY_DESCRIPTOR to secure access to your object. If you want to make your security descriptor writable, you'll have to add special logic to your property functions. In particular when updating, you are expected to merge the new security descriptor into the current security descriptor.

You can use your own logic to manage the security descriptors (it will be easier to manage in SDDL form). Or you can make use of a special API, CreatePrivateObjectSecurity(), to do it for you. First, you must decide which methods of your class you want to restrict (you can restrict up to 16 methods per security descriptor). These methods must include generic actions specified in the GENERIC_MAPPING structure (even if you don't support them—just provide empty methods in this case).

The class given in the Platform SDK sample defines the actions in fig. 26 as part of its private object. Not only should the class map its own methods to these actions, it should map a GENERIC_MAPPING structure onto this structure (without this GENERIC_MAPPING structure, the AccessCheck() function will not work).

ACCESS_READ// ==1 <-- GENERIC_MAPPING::GenericRead
ACCESS_MODIFY// ==2 <-- GENERIC_MAPPING::GenericWrite
ACCESS_DELETE // ==4
ACCESS_ALL// ==7 <-- GENERIC_MAPPING::GenericAll

Figure 26: An example of the set of actions performable by a custom class.

In the constructor for your class, make a call to CreatePrivateObjectSecurity() and store the returned security descriptor in one of your class members. You have now associated the security descriptor to your class. Should you need to update the security descriptor (e.g. during a configuration change), make a call to SetPrivateObjectSecurity(). This function will merge the old security descriptor with your own security descriptor.

When you are about to perform an action (i.e. one of your methods are called), you should now make a call to AccessCheckAndAuditAlarm(). You make a call here because if there are any audit entries in the security descriptor, you'll want an audit event to be fired. AccessCheckAndAuditAlarm() requires you to supply information for the audit event log. Once the call has been made, make a call to ObjectCloseAuditAlarm().

If your class bears a parent-child relationship model (like a folder), you'll want to support inheritance in your security descriptors. In this case, you'd use the Ex variants of the *PrivateSecurity functions. These functions bear two extra parameters: a GUID (in case your class multiple inherits), and the AutoInheritFlags. The AutoInheritFlags control how inheritance is applied from the parent object, and can also reduce the overhead of enabling privileges.

When you have finished using the class, call DestroyPrivateObjectSecurity() in the destructor to release the resources. ATL provides no special classes to handle privately secure objects. Therefore, just call the private security APIs directly. And for method 1, privately secure objects are only viable for classes (which is not available in C).

class SecureClass
{
private:
  PSECURITY_DESCRIPTOR ppSD;
  double len;
  CAccessToken ProcToken;


  bool CheckClassAccess(DWORD RightsToCheck) const;
  /* This helper is where we do the access check */

public:
  enum Rights { /* The set of rights */
    ReadLen = 1,
    WriteLen = 2,
    SetClassSecurity = 4,
    CopyClass = 8
  };

  ~SecureClass();
  SecureClass(int FullRights, const CHandle &ThreadHandle);

  SecureClass(const SecureClass &OldClass);
  /* Copy constructor needs CopyClass access */

  double get_len(void) const ;
  /* You must have ReadLen rights to access this property */

  void set_len(double Newlen) ;
  /* You must have WriteLen rights to write this property */

  void set_SecDesc(SECURITY_INFORMATION psi,
    PSECURITY_DESCRIPTOR pNewSD);
  /** You need SetClassSecurity rights to access
  * this method
  **/
};

Figure 27: The outline of a class that secures access through security descriptors.

19. Summary

Q. The Cramsheet

That's a lot of information just to build a security descriptor (especially if you worked using method 1)! Most of the time you just want to retrieve and edit the security descriptor for an object. I've summarized the required steps, so you won't have to remember all of the above:

  1. Steps:
    1. If you need to enable any privileges, do that first.
    2. Find out which object you want to secure.
    3. Obtain a handle to your object with READ_CONTROL/WRITE_DAC access (alternatively get the name of the object).
    4. Call the relevant Get*Security() API, dependent on the type of object. Call InitializeSecurityDescriptor() if the object doesn't exist yet.
    5. Convert the returned self relative security descriptor to an absolute security descriptor.
    6. Split the security descriptor into its parts by calling GetSecurityDescriptor*() and friends.
    7. If necessary set the owner SID, then reapply it to the security descriptor using SetSecurityDescriptorOwner().
    8. Repeat step 7 for the group SID.
    9. If you are editing a DACL, get the current DACL and its size.
    10. From the current size and the size of your new entries, calculate the required size of your resultant DACL.
    11. Allocate a buffer of this size.
    12. Unless you are starting from scratch, copy the old DACL onto this buffer (GetAce() and AddAce()).
    13. Add your new entries to the ACL with FindFirstFreeAce() and AddAce(), reallocating if necessary.
    14. Apply the edited DACL onto your security descriptor using SetSecurityDescriptorDacl().
    15. Repeat steps 9-12 for the SACL.
    16. With the security descriptor built, call the corresponding Set*Security() API dependent on the type of object.
    17. If the object doesn't exist yet, create a SECURITY_ATTRIBUTES structure that holds the security descriptor.
    18. Create the object using the relevant API. Set the SECURITY_ATTRIBUTES parameter to the one you created in step 17.
    19. Pray that you can one day, redo this using method 2 or 3 because this only works on Windows NT 3.x and 4.0.
  2. First enable any required privileges. Next build an SDDL string that represents your required security descriptor. Once built, convert the SDDL back into a security descriptor by calling ConvertStringSecurityDescriptorToSecurityDescriptor(). Break the security descriptor into its parts and apply it to the object with Set*SecurityInfo(). If you need to build from an existing security descriptor, you can call Get*SecurityInfo() to retrieve it. Far simpler than method 1, no need to convert it to absolute form plus it supports auto-inheritance.
  3. Obtain a security descriptor by calling AtlGetSecurityDescriptor(), then convert it to SDDL form using the ToString() method. Then proceed as in method 2.

In this part you were shown how to obtain a SID, print it, convert it into a TRUSTEE, and convert it to a user name. You extracted information from your access token, such as who you are, which groups you are a member of, the list of restricted groups, the list of privileges and their state. You enabled and disabled a privilege, and created a restricted token to run a low privilege application.

Next, you obtained a security descriptor from a file, converted it to absolute form, then extracted the five parts of the security descriptor. You converted the security descriptor to SDDL form, and printed its contents. For the DACL, you were shown how to create and edit an Access Control List, and print out its contents. You then edited a security descriptor, applied inheritance rules to it, and finally secured an object with it (predefined and custom objects).

You were shown how to do all of this in NT3.x-style, 2000-style, and ATL style. This all culminates in the AccessCheck() function, which checks if you are allowed access to a certain resource.

I have left out domain issues and multiple inherited objects since they are out of scope for this series. To learn about these, I recommend checking out the Windows resource kits. The demo project contains all the code for this part, written in methods 1, 2 and 3. Currently the samples are not designed to be reusable, but given sufficient interest, I may change that (I'm saving the real functionality for part 4). In order to compile method 2, you need the boost::regex library and the WMI SDK installed and enabled. To compile method 3, you need the ATL libraries.

Coming Up

Part 3 is a repeat of part 2. However, the next part will be written using C# and .NET 2.0. If you are interested in programming Windows Access Control using .NET, go ahead and read part 3. If you're not, read it anyway—it may tempt you into the .NET framework (well probably not!).

History is now maintained in part 4 [^].

Next Part... [^]

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
Mr. Shah is a reclusive C++/C# developer lurking somewhere in the depths of the city of London. He learnt physics at Kings' College London and obtained a Master in Science there. Having earned an MCAD, he teeters on the brink of transitioning from C++ to C#, unsure of which language to jump to. Fortunately, he also knows how to use .NET interop to merge code between the two languages (which means he won't have to make the choice anytime soon).

His interests (apart from programming) are walking, football (the real one!), philosophy, history, retro-gaming, strategy gaming, and any good game in general.

He maintains a website / blog / FAQ / junk at shexec32.serveftp.net, where he places the best answers he's written to the questions you've asked. If you can find him, maybe you can hire Mr. Shah to help you with anything C++[/CLI]/C#/.NET related Smile | :) .

Comments and Discussions

 
QuestionDACL converting Pin
AndrMan20-Sep-13 4:06
AndrMan20-Sep-13 4:06 
GeneralMy vote of 5 Pin
ledtech328-Aug-13 11:34
ledtech328-Aug-13 11:34 
QuestionATL::AtlGetSecurityDescriptor method failed when calling GetNamedSecurityInfo API function Pin
fd012900226-Feb-11 3:05
fd012900226-Feb-11 3:05 
QuestionPassing folder access right, is it possible? Pin
namphvn17-May-10 16:00
namphvn17-May-10 16:00 
QuestionImplement accesschk Sysinternals ,not work properly [modified] Pin
jinshiyi1111-Jun-09 16:47
jinshiyi1111-Jun-09 16:47 
Questionfile based users Pin
general_era15-Nov-08 22:15
general_era15-Nov-08 22:15 
RantMSDN glitch with GetNamedSecurityInfo Pin
Vitaly Tomilov16-Jul-08 8:57
Vitaly Tomilov16-Jul-08 8:57 
General[Message Deleted] Pin
Danny Rodriguez27-Jan-08 9:18
Danny Rodriguez27-Jan-08 9:18 
Questionvc++ Pin
hmt soft30-Aug-07 1:37
hmt soft30-Aug-07 1:37 
AnswerRe: vc++ Pin
oshah31-Aug-07 10:49
oshah31-Aug-07 10:49 
QuestionLookupSids is never called Pin
Decipator28-Aug-07 14:08
Decipator28-Aug-07 14:08 
AnswerRe: LookupSids is never called Pin
oshah29-Aug-07 9:27
oshah29-Aug-07 9:27 
GeneralRe: LookupSids is never called Pin
Decipator30-Aug-07 0:13
Decipator30-Aug-07 0:13 
GeneralRe: LookupSids is never called Pin
oshah31-Aug-07 11:08
oshah31-Aug-07 11:08 
GeneralRe: LookupSids is never called Pin
Decipator10-Sep-07 9:18
Decipator10-Sep-07 9:18 
QuestionQuestion regarding the users in the Windows 2000 Pin
PavanD8-May-07 20:13
PavanD8-May-07 20:13 
QuestionConvertStringSecurityDescriptorToSecurityDescriptor in VS6 Pin
Pappu5star5-Apr-07 2:25
Pappu5star5-Apr-07 2:25 
Questiontoken owner / logonsession associated Pin
ThePM20-Oct-06 11:37
ThePM20-Oct-06 11:37 
hi
i've got a windows service that is running under the localsystem account
(i need to run it as localsystem)
this computer is in a domain
i want to be able to logoff a domain user with my service
i haven't got a token from the logged on user
i need my "logoff-program" to be associated with the logged on users
session, but how do i do that?!? (because the exitwindowsex function logs off the user associate with the caller of this function)
is it possible to duplicate the token of my service an to set some
information in the token so that it is associated with an other logonsession?

my current (very poor) solution is to scan processes for an explorer.exe
and use the token of that process to create a new process with the
createprocessasuser function


as i said, this is very poor
i tried to read through msdn/Authorization Reference but i found
nothing useful

i know that it is possible to enumerate the current logonsessions an to
scan for an interactive logon
that would give me a handle to the logonsession of the domain user,
but how do i "link" a token who a logonsession?
does SetTokenInformation and TOKEN_USER deal with that in some way?

i don't know, please help!

(sorry for my bad english)

ThePM
GeneralRe: token owner / logonsession associated Pin
oshah20-Oct-06 23:23
oshah20-Oct-06 23:23 
GeneralExcellent series! Pin
aroeckelein27-Apr-06 7:35
aroeckelein27-Apr-06 7:35 
Generalaccess token online/offline Pin
WSAver19-Mar-06 23:42
WSAver19-Mar-06 23:42 
GeneralRe: access token online/offline Pin
oshah20-Mar-06 4:08
oshah20-Mar-06 4:08 
GeneralRe: access token online/offline Pin
WSAver20-Mar-06 23:09
WSAver20-Mar-06 23:09 
GeneralRe: access token online/offline Pin
oshah21-Mar-06 9:35
oshah21-Mar-06 9:35 
GeneralRe: access token online/offline Pin
WSAver21-Mar-06 19:22
WSAver21-Mar-06 19:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.