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.
- Part 1 - Background and Core Concepts. The Access Control Structures
- The Security Identifier (SID)
- The Security Descriptor (SD)
- The Access Control List (ACL)
- Choosing a good discretionary access control list
- Windows 2000 Inheritance Model
- The Token
- A Note on the Security Descriptor Definition Language
- Coming Up
- Part 2 - Basic Access Control programming
- Choosing your language
- Fun with SIDs
- Which Groups are you a member of?
- Enabling Token Privileges
- Dissecting the Security Descriptor
- Walking an Access Control List
- Do I Have Access?
- Creating an Access Control List
- Creating and Editing a Secure Object
- Making Your Own Classes Secure
- Toy Programs Download
- Part 3 - Access Control programming with .NET v2.0
- A history of .NET security.
- Reading a SID in .NET.
- Whoami .NET.
- Privilege handling in .NET.
- Running as an unprivileged user in .NET.
- Obtaining and editing a security descriptor in .NET, and applying it to an object.
- Access checks in .NET.
- NetAccessControl program and AccessToken class library.
- Part 4 - The Windows 2000-style Access Control editor
- Features of the ACL editor (ACLUI).
- Getting Started.
- Implementing the
ISecurityInformation
Interface.
ISecurityInformation::SetSecurity
- Optional Interfaces.
- Presenting the interface.
- Filepermsbox - A program to display the security descriptor on an NTFS file or folder.
- 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:
- The low level method.
- The Windows 2000 method.
- The ATL method.
- The .NET method. (If you chose this, skip this part and go straight to part 3.)
A detailed look at each method
- 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 bool
s, errnos, pointers, or void
s. 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.
- 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).
- 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.
- (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.
- 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.
- 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
.
- 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)
{
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.
- 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
).
- 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).
- 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 );
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()
.
- 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.
- 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.
- 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"?
- 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.
- Use method 1.
- 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).
- 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.
- See method 1.
- To get the groups from the token, call the
CAccessToken::GetGroups()
method, to return a CSidArray
and a CAtlArray
of DWORD
s (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);
ProcToken.GetUser(&SidUser);
std::wcout << _T("Owner: ") <<
SidUser.AccountName() << _T("\r\n");
ATL::CTokenGroups pGroups;
ProcToken.GetGroups(&pGroups);
ATL::CSid::CSidArray pSids;
ATL::CAtlArray<DWORD> pAttributes;
pGroups.GetSidsAndAttributes(&pSids, &pAttributes);
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");
ATL::CTokenPrivileges pPrivileges;
ProcToken.GetPrivileges(&pPrivileges);
ATL::CTokenPrivileges::CNames pNames;
ATL::CTokenPrivileges::CAttributes pGroupAttributes;
pPrivileges.GetNamesAndAttributes(&pNames, &pGroupAttributes);
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");
}
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.
- 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.
- 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.
- 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:
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];
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).
- 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).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
.
- 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.
- 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.
- 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 struct
s (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;
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.
- To check a security descriptor for access, first create a
GENERIC_MAPPING
structure.
- Gather up this structure, your security descriptor, the desired access, and your thread token.
- Make the first call to
AccessCheck()
. We expect this to fail.
- If the call failed with an
ERROR_INSUFFICIENT_BUFFER
, allocate a buffer for the PRIVILEGE_SET
structure.
- Call
AccessCheck()
again with the new buffer.
- Check the result in the
AccessStatus
parameter. If true
, check if the GrantedAccess
member is equal to the desired access.
- 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:
- Open your token (thread or process) with
OpenThreadToken()
.
- Call
GetTokenInformation(TokenGroups)
to retrieve the list of groups (as obtained in fig. 11).
- From your supplied security descriptor, access the DACL. (See fig. 15.) If it is null, you should use the DACL: Everyone (Full Control).
- Get the nth ACE (as shown in fig. 18).
- Get the SID associated with this ACE (as shown in fig. 18).
- Lookup this SID in the list of
TOKEN_GROUPS
array you obtained in step 2.
- Go back to the ACE and look up its type and access mask (see fig. 18).
- Map out any generic access rights to the supplied
GENERIC_MAPPING
structure using MapGenericMask()
.
- Compare the present access mask with the desired access mask.
To compare two ACCESS_MASK
s, simply NOT
one of the ACCESS_MASK
s, 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.)
- If the desired access mask is covered by the ACE, you are granted access.
- If the ACE does not completely allow access, clear out the granted accesses and continue the search.
- 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
- 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.
- 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).
- Allocate a buffer for the
ACL_HEADER
, which will most likely have to be LocalAlloc()
ed.
- 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.
- Build up an array of your access denied ACEs.
- Reallocate your ACL so it can hold this ACE array after it.
- (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).
- Add the Denied ACEs to the ACL by calling
AddAccessDeniedAce()
.
- Repeat steps 3-7 for the Allowed ACEs (call
AddAccessAllowedAce()
instead of AddAccessDeniedAce()
).
- 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.
- 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.
- 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.
- To add ACEs in low level instead of calling
AddAce()
, you call the AddAuditAccessAce()
functions.
- 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".
- 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:
- 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.
- 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.
- 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.
- 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.
- 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
.
- 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);
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()));
}
}
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
.
- 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.
- 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.
- 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;
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_READACCESS_MODIFYACCESS_DELETE ACCESS_ALL
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;
public:
enum Rights {
ReadLen = 1,
WriteLen = 2,
SetClassSecurity = 4,
CopyClass = 8
};
~SecureClass();
SecureClass(int FullRights, const CHandle &ThreadHandle);
SecureClass(const SecureClass &OldClass);
double get_len(void) const ;
void set_len(double Newlen) ;
void set_SecDesc(SECURITY_INFORMATION psi,
PSECURITY_DESCRIPTOR pNewSD);
};
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:
- Steps:
- If you need to enable any privileges, do that first.
- Find out which object you want to secure.
- Obtain a handle to your object with
READ_CONTROL
/WRITE_DAC
access (alternatively get the name of the object).
- Call the relevant
Get*Security()
API, dependent on the type of object. Call InitializeSecurityDescriptor()
if the object doesn't exist yet.
- Convert the returned self relative security descriptor to an absolute security descriptor.
- Split the security descriptor into its parts by calling
GetSecurityDescriptor*()
and friends.
- If necessary set the owner SID, then reapply it to the security descriptor using
SetSecurityDescriptorOwner()
.
- Repeat step 7 for the group SID.
- If you are editing a DACL, get the current DACL and its size.
- From the current size and the size of your new entries, calculate the required size of your resultant DACL.
- Allocate a buffer of this size.
- Unless you are starting from scratch, copy the old DACL onto this buffer (
GetAce()
and AddAce()
).
- Add your new entries to the ACL with
FindFirstFreeAce()
and AddAce()
, reallocating if necessary.
- Apply the edited DACL onto your security descriptor using
SetSecurityDescriptorDacl()
.
- Repeat steps 9-12 for the SACL.
- With the security descriptor built, call the corresponding
Set*Security()
API dependent on the type of object.
- If the object doesn't exist yet, create a
SECURITY_ATTRIBUTES
structure that holds the security descriptor.
- Create the object using the relevant API. Set the
SECURITY_ATTRIBUTES
parameter to the one you created in step 17.
- 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.
- 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.
- 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... [^]