Ruediger R. Asche
Microsoft Developer Network Technology Group
May 9, 1995
Click to open or copy the files in the CLIAPP/SRVAPP sample applications.
This article is second in a series of technical articles that describe the implementation and application of a C++ class hierarchy that encapsulates the Windows NT® security application programming interface (API). The series consists of the following articles:
"Windows NT Security in Theory and Practice" (introduction)
"The Guts of Security" (implementation of the security class hierarchy)
"Security Bits and Pieces" (architecture of the sample application suite)
"A Homegrown RPC Mechanism" (description of the remote communication implemented in the sample application suite)
CLIAPP/SRVAPP, a sample application suite that consists of a database client and server, illustrates the concepts introduced in this article series.
If you read "Windows NT Security in Theory and Practice," chances are that you have already played with the CLIAPP/SRVAPP client-server application suite that I provide with this article series. In that application suite, the server can secure a number of objects: mutexes, file mappings, named pipes, and a database object. In both "Windows NT Security in Theory and Practice" and "Security Bits and Pieces," I left you in the dark as to how the server actually secures these objects. In this article, I will dissect the underlying C++ classes that implement security. Please note that the sample C++ classes are not part of the Microsoft® Foundation Class Library (MFC) and are provided solely for demonstration purposes.
A very rough count of the security-related functions listed in the Security Overview section of the Win32® application programming interface (API) Help file yields about 75 functions. This does not include the data structures. A lot of stuff to deal with, right?
The C++ class hierarchy I provide encapsulates the security API (providing possibly 80 percent of what you will need in a real-life, security-aware server application). However, some issues, such as auditing, are currently not implemented.
It helps to think of the security API as consisting of subgroups of functions that provide certain functionalities. This section gives a short breakdown of functional groups to help you digest the API a little bit better.
Four data structures are crucially important to security programming: security descriptors (SDs), access control lists (ACLs), access tokens, and security identifiers (SIDs). The functions in the security API can be categorized into five groups that closely relate to these data structures:
An important subgroup consists of functions that build and maintain ACLs within SDs. ACLs consist of collections of access control elements (ACEs). Thus, a number of functions that manipulate ACLs actually work on ACEs. This subgroup includes functions such as AddAccessAllowedAce, FindFirstFreeAce, and GetAce.
We will discuss some of the functions in this group in the "Building and Maintaining ACLs" section later in this article.
Figure 1 shows the class hierarchy that I designed for securable objects.
Figure 1. Class hierarchy for securable objects
The CSecureableObject base class contains most of the code that pertains to security. CSecureableObject is an abstract base class—that is, you cannot create any instances from this class. You need the derived "intermediate" classes CPrivateSecObject, CUserSecObject, and CKernelSecObject because of the way Windows NT® defines security and assigns SDs to objects. (We will look at this issue in the "Associating SDs with Objects" section later in this article.)
We will eventually create real instances from classes that are derivatives of CPrivateSecObject, CUserSecObject, and CKernelSecObject. In the sample application suite, these derived classes are CSecuredNamedPipe, CMutex, CFileMapping, and ChainedQueue. Coincidentally, these are also the object types that are exposed to the user of the client application through the Permissions dialog box. In your application, you will probably derive your own custom objects from CPrivateSecObject, CUserSecObject, or CKernelSecObject. (However, note that CUserSecObject and CKernelSecObject can serve as base classes only for objects that Windows NT defines as "securable user" or "kernel" objects; thus, most of your custom objects will probably be derived from CPrivateSecObject.)
Note that being securable is generally only one property of these object types. For example, a named pipe is first of all a communication object (please refer to "Communication with Class" and related articles in the MSDN Library for a description of the CCommunication class hierarchy), and the property of "being securable" is more of a side-effect of a named pipe. Furthermore, not all instances of a securable object need to be securable; for example, the client end of a named pipe does not have anything to do with security.
To accommodate this "optional" property of securability, the class hierarchy uses the powerful C++ multiple inheritance feature: A CSecuredNamedPipe object is derived from both CServerNamedPipe and CKernelSecObject. If securability is not required, we can simply derive this object from CServerNamedPipe. The code below is from the NPIPE.H file:
class CSecuredNamedPipe: public CServerNamedPipe, public CKernelSecObject
{
<class definition>
}
This implementation allows the server application to use the class members that are related to security and the class members that relate to the "real" functionality of the object without additional coding. Note that in a "dynamic" object-oriented system such as OLE, it would make perfect sense to place all the functions that pertain to security in one interface, and all other functions in another interface; such an implementation would make the coding much more elegant.
The three files that are related to security are in the SRV\SECSTUFF subdirectory:
I replaced all of the printfs with OutputDebugString calls so that applications that use the security class library can call the ExamineSD or ExamineAccessToken and monitor the appropriate data structures at run time.
Let's look at the prototypes for the class definition of CSecureableObject, the mother of all security classes (from SEC.H):
class CSecureableObject
{
protected:
PSECURITY_DESCRIPTOR m_pSD;
PACL m_pDACL;
PACL m_pSACL;
PSID m_pOwner;
PSID m_pPrimaryGroup;
public:
int m_iSecErrorCode;
protected:
virtual BOOL SetTheDescriptor()=0;
virtual BOOL GetTheDescriptor()=0;
private:
BOOL GetSIDFromName(PSTR pDomainName,PSTR pAccountName,BYTE **pcSid,
char **pcDomainName);
protected:
BOOL BuildSD(PSECURITY_DESCRIPTOR pSelfRelativeReturnSD);
void inline FreeDataStructures()
{
if (m_pSD) free (m_pSD);
if (m_pDACL) free(m_pDACL);
if (m_pSACL) free(m_pSACL);
if (m_pOwner) free(m_pOwner);
if (m_pPrimaryGroup) free(m_pPrimaryGroup);
ZeroOut();
};
void inline ZeroOut()
{
m_pSD = NULL;
m_pDACL = NULL;
m_pSACL = NULL;
m_pOwner = NULL;
m_pPrimaryGroup = NULL;
};
public:
CSecureableObject(BOOL bProtected);
~CSecureableObject();
BOOL AddRightsTo(PSTR pDomainName,PSTR pAccountName,DWORD dwAccessMask,
BOOL bGranted);
BOOL RevokePreviouslyGrantedAccess(PSTR pAccountName,PSTR pDomainName);
BOOL AddSecurity(DWORD dwAccessMask, LPSTR lpTitleString);
};
If you wish to plug the classes into your code without understanding the internal workings, you'll find the following members interesting:
BOOL AddRightsTo(PSTR pDomainName,PSTR pAccountName,DWORD dwAccessMask,
BOOL bGranted);
BOOL RevokePreviouslyGrantedAccess(PSTR pAccountName,PSTR pDomainName);
These two member functions change the security of the object. Both functions accept the user name and a domain on which the user has an account as parameters. You must also pass a DWORD (dwAccessMask) and a Boolean (bGranted) to AddRightsTo. dwAccessMask specifies the rights to be granted or denied (we will look at this parameter later on). If bGranted is FALSE, the rights specified in dwAccessMask will be denied to the user, if it is TRUE, access will be granted. RevokePreviouslyGrantedAccess will remove a previously granted right (granted or denied) from the list of rights associated with the object.
Both functions will return TRUE if the operation completed successfully. If an error occurred, they will return FALSE, and the m_iSecErrorCode member variable will contain a Win32 error code that explains why the function failed.
Your application probably does not need to call AddRightsTo or RevokePreviouslyGrantedAccess directly; instead, it can simply call the AddSecurity member function. AddSecurity accepts the same dwAccessMask parameter as AddRightsTo or RevokePreviouslyGrantedRights (which specifies a set of rights) and a string. This function displays the dialog box that is illustrated in Figure 2.
Figure 2. Dialog box displayed by the AddSecurity function
The title of the dialog box reflects the string that you passed to AddSecurity. The user fills in the dialog box, and the C++ class does the rest (that is, it calls AddRightsTo or RevokePreviouslyGrantedAccess, depending on the user's selections).
We will examine the class definitions for the derived classes CUserSecObject, CKernelSecObject, and CPrivateSecObject in the "Associating SDs with Objects" section later in this article.
As I mentioned earlier, security-aware code must know how to manipulate four basic data structures: access tokens, SDs, ACLs, and SIDs. Very roughly, an access token represents a user who wishes to access something, and an SD is a data structure that associates users (or user groups) with rights. Typically, an SD is associated with an object (thus, the member variable m_pSD in the declaration of CSecureableObject), but SDs can also be used in different ways (for example, to associate protection with a group of objects instead of only one object).
Embedded in SDs are ACLs, which are collections of ACEs. An ACE is a unit of security information that consists of three parts: a user identification, a list of rights, and a designation of whether the right is granted or denied. (There are also other types of ACEs that I will discuss in a later article.)
Robert Reichel's article series on Windows NT security, which I mentioned earlier, discusses the data structures very nicely, so I will not elaborate on what SIDs, SACLs, DACLs, ACEs, and SDs are made of. However, I would like to show you an access token to make this abstract discussion a little more meaningful. I obtained the access token from a test run of the client-server application suite on one of my machines; it is shown in Appendix A (I did not want to clog up the article more than absolutely necessary).
Note that an access token is created on behalf of a user, but obtained from a process or a thread. The reason for that, strictly speaking, is that it is never a "user" who wants to access a secured object, but a piece of code (which has to reside in a process and execute within a thread). Thus, any process that is executed "inherits" the security context (that is, the access token) of the user who is logged onto the machine on which the process is executed.
That is the easy answer. What happens in the case of remote access, though? Through the process of impersonation, a server application can temporarily assume the access token of a remote client, so that access to objects on the server can be attempted within the context of the server. Please see the other articles in this series for a discussion of how the sample application suite uses impersonation to enforce protection.
The central piece of information that belongs to an access token is a SID. A SID is a worldwide unique value that very roughly describes one user in a specific domain. The SID is built when the user registers with the domain; that is, when the user account in the domain is created. As soon as the user account is deleted and recreated, a new SID is created. As Robert Reichel describes in his article series on Windows NT security ("Inside Windows NT Security," which appeared in the April 1993 and May 1993 issues of the Windows/DOS Developer's Journal), the components of a SID follow a hierarchical convention; that is, a SID contains parts that identify the network, the user, and the authority that assigned the SID.
In the sample token, we see that the user who is identified by the token is Ruediger in the domain RUEDIGERDOM. Note that this user is associated with a number of other SIDs—that is because Ruediger was assigned membership in user groups (in this case, Administrators and Users) when his user account was created. Ruediger also belongs to the "wildcard" group Everyone. Note that the SID associated with Everyone is what is called a well-known SID—that is, a SID that has a constant numeric value everywhere. A number of well-known SIDs represented by constant identifiers (authorities) are available: SECURITY_NULL_SID_AUTHORITY, SECURITY_WORLD_SID_AUTHORITY, SECURITY_LOCAL_SID_AUTHORITY, SECURITY_CREATOR_SID_AUTHORITY, and SECURITY_NT_AUTHORITY.
Later in this article, we will see how it can be useful to work with well-known SIDs and how an authority can be converted to a SID.
In this section, we will describe the sequence of API calls we need to build ACLs and to add ACEs to them. Let us first look at the constructor for CSecureableObject to see how things start to take shape. For readability reasons, I've removed all the code that catches error handling (the omissions are indicated by ellipses); please refer to SEC.CPP for the complete code.
CSecureableObject::CSecureableObject(BOOL bProtected)
{
DWORD dwDACLLength;
PSID pcSid = NULL;
ZeroOut(); // This sets a few member variables to 0.
SID_IDENTIFIER_AUTHORITY siaWorld = SECURITY_WORLD_SID_AUTHORITY;
int iTempSidLength;
m_pSD = malloc(sizeof(SECURITY_DESCRIPTOR));
if (!m_pSD) ...
if (!InitializeSecurityDescriptor(m_pSD,SECURITY_DESCRIPTOR_REVISION)) ...
iTempSidLength = GetSidLengthRequired(1); // This cannot fail
pcSid = (PSID)malloc(iTempSidLength);
if (!pcSid) ...
dwDACLLength = sizeof (ACL) +sizeof (ACCESS_ALLOWED_ACE) - sizeof (DWORD) + iTempSidLength;
m_pDACL = (PACL) malloc(dwDACLLength);
if (!m_pDACL) ...
if (!InitializeAcl(m_pDACL,dwDACLLength,ACL_REVISION) ||!InitializeSid(pcSid,&siaWorld,1)) ...
*(GetSidSubAuthority(pcSid,0)) = SECURITY_WORLD_RID;
if (bProtected) // This works like an empty DACL - fully protected.
{
if (!AddAccessAllowedAce(m_pDACL,ACL_REVISION,NULL,pcSid)) ...
}
else // This works like a NULL DACL - unprotected.
{
if (!AddAccessAllowedAce(m_pDACL,ACL_REVISION,GENERIC_ALL|STANDARD_RIGHTS_ALL|
SPECIFIC_RIGHTS_ALL,pcSid)) ...
};
if (!SetSecurityDescriptorDacl(m_pSD,TRUE,m_pDACL,FALSE)) ...
if (pcSid) free (pcSid);
};
SDs consist of a number of data structures, as illustrated in Figure 3.
Figure 3. Embedded data structures in SDs
The handling of SDs and ACLs is a little awkward because the structures come in two flavors: self-relative and absolute.
Self-relative structures are "flat"; that is, the SD contains all of its information in one string of contiguous data—it contains no indirect pointer references. For this reason, whenever the information in an SD needs to be changed, we must reallocate a new SD that accommodates the space for all the modified data, rebuild the new SD, and free the old one. The same holds true for ACEs and ACLs.
Absolute data structures, on the other hand, contain indirect references. In Figure 3 above, we see that the m_pSD security descriptor contains a pointer to the m_pDACL access control list. This makes the SD absolute instead of self-relative (a self-relative SD would have the entire ACL embedded within it).
Why are there two flavors of SDs, and when do we need each? Well, you must use self-relative SDs when security information must be communicated over a network or stored on disk (for example, for files on an NTFS partition), because indirect references cannot easily be maintained on disk or over a communication line.
The Windows NT security system handles SDs as follows: Each SD that is returned from a security function has a self-relative format. Most SDs that are passed as parameters to security functions must be in absolute format. You can use the MakeSelfRelativeSD and MakeAbsoluteSD functions to convert SDs from one format to the other.
When an SD is assigned to an object (we will discuss this process in more detail in the section "Associating SDs with Objects"), Windows NT copies the absolute SD that the server application built to a private location in self-relative format. Thus, technically speaking, after an SD is used (that is, assigned to an object), all the memory that is associated with the SD (the SD itself as well as the ACLs) can be freed. If later on the SD must be manipulated, the code for AddRightsTo and RevokePreviouslyGrantedAccess retrieves the descriptor from the object using the GetTheDescriptor member function.
Note that in order to manipulate SDs and ACLs, you never manipulate the memory directly; instead, you call Win32 API functions such as InitializeSecurityDescriptor, InitializeAcl, or AddAccessAllowedAce to set up and manipulate the data structures.
The code of the CSecureableObject constructor builds an SD that is either fully protected (if the Boolean parameter to the constructor is TRUE) or not protected at all (a FALSE parameter). The semantics of ACLs leave wide room for variation, so this behavior can be implemented in several ways. By convention, an SD that has a NULL DACL is unprotected (that is, every attempt to access the object that is associated with the SD will succeed), whereas an SD with a DACL that is empty (it has no ACEs) is fully protected (that is, access to the object associated with the SD will fail). Note that a NULL SD is still something different: An object without an explicit SD will normally be protected according to the default DACL that is specified in the access token of the user who created the object.
Thus, it would have been easy to design the constructor so that it translates a FALSE parameter into a NULL DACL, and a TRUE parameter into an empty DACL. However, my inborn laziness and sense for neat code took over, and I decided to find a more elegant way to code this initial SD. The problem with a NULL DACL versus an empty DACL approach is that you have to revoke previously granted rights completely differently, depending on the initial DACL. Let's assume that an ACE is added to a DACL, and the rights encoded in the ACE are removed later. If no other ACE is left, depending on whether the DACL was initially created empty or is nonexistent, you must take a totally different action to restore the initial state.
Note that the drawback of always allocating one ACE in every DACL is that an ACE does take up memory and (in the case of SDs stored on an NTFS partition) disk space. Thus, the NULL DACL and empty DACL approach can be somewhat more efficient.
In my code, each SD always has a DACL with at least one ACE. This ACE grants rights to the special, well-known SID that specifies Everybody. For an object that is initially protected, the access mask in that ACE is 0 (no rights granted); for an object that is initially unprotected, the access mask is GENERIC_ALL|STANDARD_RIGHTS_ALL|SPECIFIC_RIGHTS_ALL (that is, all rights). Note that I could have designed the code so that an initially protected object denies all rights to Everybody. However, in that case, the ordering of ACEs within the ACL (which we will discuss in the next section) would require some special treatment.
Let us look at an SD created by the procedure we just described (this dump was obtained by calling the ExamineSD function on the newly created SD):
SD is valid. SD is 48 bytes long. SD revision is 1 ==
SECURITY_DESCRIPTOR_REVISION1
SD's Owner is NULL, so SE_OWNER_DEFAULTED is ignored
SD's Group is NULL, so SE_GROUP_DEFAULTED is ignored
SD's Group being NULL is typical, GROUP in SD(s) is mainly for POSIX compliance
SD's DACL is Present
SD's DACL-Defaulted flag is FALSE
ACL has 1 ACE(s), 28 bytes used, 0 bytes free
ACL revision is 2 == ACL_REVISION2
ACE 0 size 20
ACE 0 flags 0x00
ACE 0 is an ACCESS_ALLOWED_ACE_TYPE
ACE 0 mask == 0x00000000
Standard Rights == 0x00000000
Specific Rights == 0x00000000
Access System Security == 0x00000000
Generic Rights == 0x00000000
SID domain == , Name == Everyone S-1-1-0
SID is the World SID
SID type is SidTypeWellKnownGroup
SD's SACL is Not Present, so SE_SACL_DEFAULTED is ignored
SD has no SACL at all (or we did not request to see it)
This SD illustrates what we have discussed so far: The DACL has been set up to contain one ACE, which grants no access to the world; that is, the object that is associated with this SD will be fully protected until rights are specifically granted.
One question remains: How do we identify the owner "Everybody"? Let's look at the code that retrieves the SID (abbreviated for readability from the CSecureableObject constructor we reviewed earlier):
SID_IDENTIFIER_AUTHORITY siaWorld = SECURITY_WORLD_SID_AUTHORITY;
pcSid = (PSID)malloc(iTempSidLength);
InitializeSid(pcSid,&siaWorld,1);
*(GetSidSubAuthority(pcSid,0)) = SECURITY_WORLD_RID;
Now we can use pcSid to build ACEs for SDs; for example, using the function call:
AddAccessAllowedAce(pACL,ACL_REVISION,GENERIC_ALL|STANDARD_RIGHTS_ALL|
SPECIFIC_RIGHTS_ALL,pcSid);
So how do we specifically grant rights? Moe says, "That's easy: Simply run the server application that comes with this article, fill out the Permissions dialog box, and that'll take care of it."
Ha ha. Very funny. But since a stupid answer is at times better than no answer at all, remember that the dialog box Moe mentions is displayed in response to a CSecureableObject::AddSecurity call, which in turn calls either CSecureableObject::AddRightsTo or CSecureableObject::RevokePreviouslyGrantedAccess. So why don't we look at the implementation of those two members? Let us first review AddRightsTo (from SEC.CPP). Again, this function contains a lot of code, but we can actually split up the code into different "phases," which makes it much easier to understand what's going on. Once more, I omit error handling code (replaced by ellipses) for clarity.
BOOL CSecureableObject::AddRightsTo(PSTR pDomainName,PSTR pAccountName,
DWORD dwAccessMask,BOOL bGranted)
{
DWORD dwDACLLength;
char *pcDomainName;
BYTE *pcSid;
BOOL bHasDacl,bHasDefaulted;
PACL pCurrentAcl=NULL;
PACL pNewACL=NULL;
PSECURITY_DESCRIPTOR pNewSD=NULL;
First, the user and domain name passed to the function are translated into a SID using the CSecureableObject::GetSidFromName private member function, which calls the ::LookupAccountName security function. Remember that when we built the initial DACL for the object, we obtained the security ID for the ACE from the InitializeSid function, because the Everybody user group is "well-known"; that is, it is hardcoded. However, we must retrieve the security ID for a user or group account dynamically. This is what CSecureableObject::GetSidFromName does:
if (!GetSIDFromName(pDomainName,pAccountName,&pcSid,&pcDomainName))...
You will wonder why CSecureableObject::GetSidFromName accepts so many parameters, some of which seem to be redundant. This is because GetSidFromName is a wrapper for LookupAccountName, which also accepts these parameters. Why? It's simple: You can pass NULL as pDomainName, and if the function returns successfully, pcDomainName will contain a pointer to the name of the domain in which the name was found.
Next, we retrieve the SD and the current DACL associated with the SD, and then we compute how large the new SD must be to accommodate the new ACE.
Note the call to GetTheDescriptor, which is a virtual member function of the CSecureableObject class. GetTheDescriptor calls "up" the hierarchy into the securable object and back "down" into one of the Win32 functions—GetKernelObjectSecurity, GetUserObjectSecurity, or GetPrivateObjectSecurity—which retrieve the SD from a secured object. (We will examine the control flow in detail in the "Associating SDs with Objects" section.)
UINT dwNumberOfAces;
DWORD dwAceSize;
if (!GetTheDescriptor()) goto ErrorExit; // This will implicitly set m_pSD.
if (!GetSecurityDescriptorDacl(m_sd,&bHasDacl, (PACL *)&pCurrentAcl,
&bHasDefaulted)) ...
dwNumberOfAces = pCurrentAcl->AceCount;
dwAceSize = pCurrentAcl->AclSize;
DWORD dwCurrentSecurityDescriptorLength;
dwDACLLength = dwAceSize + sizeof (ACCESS_ALLOWED_ACE) -
sizeof (DWORD) + GetLengthSid(pcSid);
pNewACL = (PNewACL) malloc(dwDACLLength);
if (!pNewACL) ...
dwCurrentSecurityDescriptorLength=GetSecurityDescriptorLength(m_pSD);
pNewSD = malloc(dwDACLLength+dwCurrentSecurityDescriptorLength);
if (!pNewSD) ...
Now we have successfully allocated a new SD and a new ACL. With the following two calls, we ask Windows NT to prepare those data structures for manipulation:
if (!InitializeSecurityDescriptor(pNewSD,SECURITY_DESCRIPTOR_REVISION)) ...
if (!InitializeAcl(pNewACL,dwDACLLength,ACL_REVISION)) ...
UINT iLoop;
void *pTempACL;
What happens next depends on whether the right is granted or denied. In both cases, we basically copy the old DACL, ACE by ACE, to the new DACL using the GetAce and AddAce functions, and add an ACE that grants or denies the desired rights to the user whose SID was retrieved earlier. The location where we add the ACE to the ACL differs: For granted rights, we add the new ACE at the very end of the ACL; for denied rights, we simply attach the ACE to the beginning of the ACL. ACCESS_DENIED_ACEs should always precede all ACCESS_ALLOWED_ACEs for the following reason: When the system validates an access request against a DACL, it always traverses the DACL from beginning to end. If it does not find the appropriate right by the end of the list, it denies the request. Thus, explicitly denying rights should be a refinement of granting rights, which overrides the default case. (See "Windows NT Security in Theory and Practice" for more information on this process.)
It seems strange that we can get away with tucking a granted right at the end of the ACL. After all, if the object is fully protected initially, m_sd contains an ACE that reads "grant no access to Everybody," so wouldn't this ACE end the search, forcing all subsequent ACEs to be ignored? Shouldn't we rather "splice" the new ACE in front of the "grant no access to Everybody" ACE?
Surprisingly enough, both techniques work, but it is easier to code the "tuck the new ACE at the end" strategy. Both techniques do the same thing because of the way the right verification algorithm works: When AccessCheck traverses an ACL from beginning to end, it proceeds until it finds all the requested rights explicitly denied or granted or until it gets to the end of the list, whichever comes first. Thus, the "grant no access to everybody" ACE does not terminate an access check (because it contains NO rights), it acts more as a "dummy" ACE. In this respect, an ACE that reads "deny all access to Everybody" is semantically different, because such an ACE does terminate an access check.
if (!bGranted)
if (!AddAccessDeniedAce(pNewACL,ACL_REVISION,dwAccessMask,(PSID)pcSid)) ...
for (iLoop = 0; iLoop < dwNumberOfAces; iLoop++)
{
if (! GetAce(pCurrentAcl, iLoop, &pTempACL)) ...
if (!AddAce(pNewACL,ACL_REVISION,MAXDWORD,pTempACL,
((PACE_HEADER)pTempACL)->AceSize)) ...
};
if (bGranted)
if (!AddAccessAllowedAce(pACL,ACL_REVISION,dwAccessMask,(PSID)pcSid)) ...
The rest of the code cleans up the memory we don't need anymore, verifies the validity of the new data structures, and returns. Note the call into the SetTheDescriptor virtual function. This call associates the object with the SD; we will discuss the coding in the next section. I'd like to mention here that the SetTheDescriptor code, as a side effect, frees all the memory that was previously allocated to build the SD.
Also note that the code rebuilds only those parts of the SD that were changed. The GetTheDescriptor function "dissects" the previous SD into its four components (DACL, SACL, owner, and group). Each component is assigned to its respective member variable (m_pDACL, m_pSACL, m_pOwner, or m_pPrimaryGroup). The following code copies all of those members except m_pDACL without modification to the new SD. m_pDACL is rebuilt according to the security change that was requested. A member function that would modify the SACL (that is, change auditing) would leave the DACL untouched and rebuild the SACL instead.
// Now we may nuke the old SD because we don't need it anymore.
// Note that we keep it until we know that security was set correctly. This way,
// if anything fails, we'll still have the old one around...
if (!IsValidAcl(pNewACL)) ...
if (!IsValidSecurityDescriptor(pNewSD)) ...
if (!SetSecurityDescriptorDacl(pNewSD,TRUE,pNewACL,FALSE)) ...
// Now copy all of the other relevant stuff to the new SD.
if (m_pOwner &&
!SetSecurityDescriptorOwner(pNewSD,m_pOwner,FALSE)) goto ErrorExit;
if (m_pSACL &&
!SetSecurityDescriptorSacl(pNewSD,TRUE,m_pSACL,FALSE)) goto ErrorExit;
if (m_pPrimaryGroup &&
!SetSecurityDescriptorGroup(pNewSD,m_pPrimaryGroup,FALSE)) goto ErrorExit;
free(pCurrentAcl);
free(m_pSD);
m_pSD = pNewSD;
free(m_pDACL);
m_pDACL=pNewACL;
if (!SetTheDescriptor()) goto ErrorExit;
return TRUE;
...
Let's look at the code that revokes rights. This code resides in CSecureableObject::RevokePreviouslyGrantedRights (in SEC.CPP). Notice that the prelude (the code that looks up the user's SID) is identical to the code in AddRightsTo:
BOOL CSecureableObject::RevokePreviouslyGrantedAccess(PSTR pAccountName,
PSTR pDomainName)
{
PACL pNewACL = NULL;
PACL pCurrentAcl=NULL;
DWORD dwDACLLength;
char *pcDomainName;
BYTE *pcSid;
PSECURITY_DESCRIPTOR pNewSD = NULL;
UINT iOffendingIndex;
if (!GetSIDFromName(pDomainName,pAccountName,&pcSid,&pcDomainName)) ...
The following code provides bookkeeping: It retrieves the ACL and the current SD's length. My comments for AddRightsTo apply here as well.
BOOL bHasDacl;
BOOL bHasDefaulted;
UINT dwNumberOfAces;
DWORD dwAceSize;
if (!GetTheDescriptor()) ...
if (!GetSecurityDescriptorDacl(m_pSD,&bHasDacl, (PACL *)
&pCurrentAcl,&bHasDefaulted)) ...
dwNumberOfAces = pCurrentAcl->AceCount;
dwAceSize = pCurrentAcl->AclSize;
DWORD dwCurrentSecurityDescriptorLength;
dwCurrentSecurityDescriptorLength=GetSecurityDescriptorLength(m_sd);
Next, we traverse the DACL and memorize the index of the ACE that references the same SID as the one that belongs to the user (using the EqualSid system service to perform the comparison). If the SID is not found in the ACL, we return ERROR_PRIVILEGE_NOT_HELD. (OK, this is not exactly the return value we want, but I didn't want to invent my own error code.)
// Here we figure out if an ACE with the requested SID is already in the ACL...
UINT iLoop;
void *pTempACL;
for (iLoop = 0; iLoop < dwNumberOfAces; iLoop++)
{
if (! GetAce(pCurrentAcl, iLoop, &pTempACL)) goto ErrorExit;
if (EqualSid ((PSID) &(((PACCESS_ALLOWED_ACE)pTempACL)->SidStart),pcSid))
break;
};
if (iLoop >= dwNumberOfAces) ...
iOffendingIndex = iLoop;
Now we allocate and initialize a new ACL and a new SD that contain the old ACL minus the matching ACE:
dwDACLLength = dwAceSize -
((PACE_HEADER)pTempACL)->AceSize;
pNewACL = (PACL) malloc(dwDACLLength);
if (!pNewACL) ...
pNewSD = malloc(dwDACLLength+dwCurrentSecurityDescriptorLength);
if (!pNewSD) ...
if (!InitializeSecurityDescriptor(pNewSD,SECURITY_DESCRIPTOR_REVISION)) ...
if (!InitializeAcl(pACL,dwDACLLength,ACL_REVISION)) ...
Let's copy the old ACE to the new one, leaving out the ACE with the offending SID:
for (iLoop = 0; iLoop < dwNumberOfAces; iLoop++)
{if (iLoop == iOffendingIndex) continue;
if (! GetAce(pCurrentAcl, iLoop, &pTempACL)) ...
if (!AddAce(pNewACL,ACL_REVISION,MAXDWORD,pTempACL,
((PACE_HEADER)pTempACL)->AceSize)) ...
};
The postlude and cleanup code is exactly the same as in AddRightsTo:
// Now we may nuke the old SD because we don't need it anymore.
// Note that we keep it until we know that security was set correctly. This way,
// if anything fails, we'll still have the old one around...
if (!IsValidAcl(pNewACL)) ...
if (!IsValidSecurityDescriptor(pNewSD)) ...
if (!SetSecurityDescriptorDacl(pNewSD,TRUE,pNewACL,FALSE)) ...
// Now copy all of the other relevant stuff to the new SD.
if (m_pOwner &&
!SetSecurityDescriptorOwner(pNewSD,m_pOwner,FALSE)) goto ErrorExit;
if (m_pSACL &&
!SetSecurityDescriptorSacl(pNewSD,TRUE,m_pSACL,FALSE)) goto ErrorExit;
if (m_pPrimaryGroup &&
!SetSecurityDescriptorGroup(pNewSD,m_pPrimaryGroup,FALSE)) goto ErrorExit;
free(pCurrentAcl);
free(m_pSD);
m_pSD = pNewSD;
free(m_pDACL);
m_pDACL=pNewACL;
if (!SetTheDescriptor()) goto ErrorExit;
return TRUE;
...
You can also implement the RevokePreviouslyGrantedRights function a different way, using the Win32 DeleteAce function to remove an ACE from an ACL. Thus, instead of copying the old ACE to the new ACE without the offending element, we would simply call DeleteAce and leave the ACE untouched. However, this process makes it more difficult to keep track of the SD size—after a DeleteAce call, the ACL has a "hole," and the next time we wish to add an ACE, we first need to check whether the "hole" is large enough before allocating new memory for the SD. The security API contains functions that help you find the "hole" and fill it in appropriately (check out the FindFirstFreeAce function for details).
So far, we have discussed how SDs are built and manipulated. However, one important piece is still missing from the security puzzle: We mentioned earlier that an SD must be associated with an object in order to protect that object. Our current SD is nothing but a structure in memory—it is not linked to anything. To make use of the SD, we need to establish some kind of link between the SD and an object. How does that work?
You can enforce a security check using two methods: built-in, implicit access checking or application-coded, explicit access checking. Let's look at these methods in turn.
For object types that have built-in security support (almost all Windows NT kernel objects and certain user objects), object instances can be associated with SDs. The match between the object's SD and the accessing user token is made implicitly when you call a function that works on the system object. For example, you can associate a named pipe with an SD, and a client's attempt to open the named pipe using CreateFile may fail with error ACCESS_DENIED (which implies that AccessCheck is called implicitly within the CreateFile call).
For this scheme to work, the system object must be permanently associated with an SD. This association can be accomplished in two ways:
The object hierarchy in my sample application suite employs the second approach: For example, in the CNamedPipe constructor, the code that creates the server end of the pipe calls SetKernelObjectSecurity (wrapped in a member function call to be discussed later) right after creating the pipe. Alternatively, we could have wrapped the public SD member m_sd into a SECURITY_ATTRIBUTES structure, which we could have passed to the CreateNamedPipe function. (This would have corresponded to the first approach above.)
In the CSecureableObject class hierarchy, the SetObjectSecurity function sets an object's security. There is a little programming twist here that I need to explain: Once the SD has been successfully built, the AddRightsTo and RevokePreviouslyGrantedAccess base class functions will attempt to associate the SD with the secured object. To do that, however, we need a handle to an object, which the base class does not provide, because the base class code doesn't have any idea what to secure.
To solve this problem, the base class includes one pure virtual member function, SetTheDescriptor, which is implemented in the derived classes. AddRightsTo and RevokePreviouslyGrantedAccess call SetTheDescriptor after the SD has been manipulated successfully.
SetTheDescriptor, as I said before, must be implemented in the derived classes (for example, in CNamedPipe), because only those objects know about their handles. However, it would be a waste of code if we didn't acknowledge the fact that functions such as SetKernelObjectSecurity can be applied to a multitude of objects, such as mutexes, file mappings, and semaphores. Thus the three-level class hierarchy:
This approach looks a little bouncy; the base class functions call "up" the class hierarchy into the SetTheDescriptor calls, which call back "down" into the SetObjectSecurity members of the intermediate classes.
The CSecureableObject class hierarchy also defines the counterpart for SetTheDescriptor, which is GetTheDescriptor. This function takes an existing SD from a secured object and stores the components in the object's member variables m_pSD, m_pDACL, m_pSACL, m_pOwner, and m_pPrimaryGroup. For the same reason as we discussed for SetTheDescriptor, a GetTheDescriptor call "bounces" back through the class hierarchy.
Note that most of the code that eventually does the SD retrieval (that is, the code that resides in CKernelSecObject::GetObjectSecurity, CUserSecObject::GetObjectSecurity, and CPrivateSecObject::GetObjectSecurity) focuses on allocating memory chunks for the SD's components. One of the nice things about the Win32 API (that is, the parts that were not carried over from the Win16 API) is that the functions are designed very intelligently. Many security functions accept memory buffers whose sizes cannot be determined immediately as parameters, so these functions always expect both the buffers and the buffer sizes as parameters.
Let's look at the functions (for example, LookupAccountName and GetKernelObjectSecurity) that do this type of thing. Typically, these functions are called twice: once with empty buffers and a buffer size of 0, and then with the correct buffer sizes. The first function call will return FALSE, and GetLastError will return ERROR_INSUFFICIENT_BUFFER. As a result, the variables that contain the buffer sizes will be set to the requested sizes, so the code can allocate the buffers and retry the operation. This design allows applications to dynamically allocate memory as needed.
Very roughly, the difference between system-provided security and private security is that the Win32 API does not have any clue (and does not care, for that matter) which operations on private objects should be protected. The operations on system-defined objects, on the other hand, are well-defined. In other words, the objects that you define in your application have semantics and operations totally unknown to the system; thus, it is up to you to enforce security for your own objects.
You enforce private security in your server application by using an SD and the Win32 AccessCheck function. The CPrivateSecObject class and its derivatives support the MatchAccessRequest public member function, which encapsulates AccessCheck. MatchAccessRequest takes as parameters an access mask and the handle of a thread on whose behalf the security check is made. Here is what MatchAccessRequest does:
BOOL CPrivateSecObject::MatchAccessRequest(DWORD dwMask, HANDLE hThread)
{
HANDLE hClientToken;
BOOL bReturn;
PRIVILEGE_SET ps;
DWORD dwStatus;
DWORD dwStructureSize = sizeof(PRIVILEGE_SET);
if (!OpenThreadToken(hThread,TOKEN_ALL_ACCESS,FALSE,&hClientToken))
{
m_iSecErrorCode = GetLastError();
return FALSE;
};
if (!AccessCheck(m_ObjectSD,hClientToken,dwMask,
&m_gmPrivateMapping,&ps,&dwStructureSize,&dwStatus,&bReturn))
{
m_iSecErrorCode = GetLastError();
return FALSE;
};
m_iSecErrorCode = ERROR_SUCCESS;
return bReturn;
};
Note that the error handling in this code is somewhat sloppy because FALSE is somewhat overloaded as a return value. FALSE may mean either that a system call failed or that AccessCheck succeeded and refused the access. A calling application should check the m_iSecErrorCode member variable to determine the cause of the return value. Note that returning FALSE from this function upon an error implies a stringent security check: If, for some reason, you cannot determine whether access is granted or denied, assume that the access is denied. Thus, a server application that does not check m_iSecErrorCode upon returning from MatchAccessRequest may behave unexpectedly if a system call in MatchAccessRequest fails, but there will never be a security leak.
The code first retrieves an access token against which it will match the SD. This token represents the user who wants to access our object. For typical client-server applications, this token will be an impersonation token; that is, the server will temporarily assume the identity of the client. See the "Security Bits and Pieces," technical article in the MSDN Library for details on how the sample server achieves the impersonation.
If you have any doubts as to whether the correct access token was obtained, it is a good idea to include a call to ExamineAccessToken right after the OpenThreadToken call for debugging purposes.
The next function that MatchAccessRequest calls is the famous end-of-all-security-calls, AccessCheck. AccessCheck takes four input parameters (the client token, the SD to match the access request against, an access mask, and a generic mapping structure) and four pointer parameters for storing the output (a PRIVILEGE_SET structure and its size, a Boolean return parameter, and a pointer to a DWORD that will contain the granted rights). Let's talk about the input parameters first. We know what the client token, the SD, and the access mask do, but what is a generic mapping structure, and why do we need it?
As I discussed in the "Windows NT Security in Theory and Practice" article, a generic mapping is a useful way to hide private access rights from a server application. Let us make this a little clearer within the context of the sample application suite. The database-specific DBASE_READ and DBASE_WRITE rights are defined in the SEC.H file. You will notice that these two rights are located in the lower 16 bits of the access mask, which are defined as specific to the object (that is, Windows NT doesn't really care what those bits mean; the interpretation is up to the server application).
When a client attempts to either add a record to, or remove a record from, the database, the communication layer in the server translates the request into a GENERIC_WRITE access mask. This makes intuitive sense: GENERIC_WRITE basically means "anything that modifies the object." The AccessCheck function takes the GENERIC_WRITE access mask and translates it to DBASE_WRITE. This way, any code that "sees" the object from the outside sees only generic (never specific) rights, and whenever the server deals with rights on the object, it can define the rights in generic (abstract) rather than object-specific terms. The mapping is accomplished through a process that you can also invoke directly from the server using the MapGenericMask function.
How do AccessCheck and MapGenericMask perform this translation? Simple—through a data structure that is filled in when the object is created. (The following code is from the constructor for the CPrivateSecObject class.)
CPrivateSecObject::CPrivateSecObject() : CSecureableObject(TRUE)
{
HANDLE hProcess;
hProcess = GetCurrentProcess();
OpenProcessToken(hProcess,TOKEN_ALL_ACCESS,&m_hAccessToken); // Error
m_gmPrivateMapping.GenericRead = DBASE_READ;
m_gmPrivateMapping.GenericWrite = DBASE_WRITE;
m_gmPrivateMapping.GenericExecute = STANDARD_RIGHTS_EXECUTE;
m_gmPrivateMapping.GenericAll = DBASE_READ|DBASE_WRITE;
CreatePrivateObjectSecurity(NULL,NULL,&m_ObjectSD,FALSE,m_hAccessToken,&m_gmPrivateMapping); // Error
SetObjectSecurity();
};
The m_gmPrivateMapping structure tells us what generic rights are mapped to. Note that generic mappings are most powerful when generic rights are mapped to combinations of rights. For example, for most built-in objects, the generic read right is mapped to the object-specific right plus the STANDARD_RIGHTS_READ right, which includes a predefined right to "retrieve information from an object's DACL." Make sure that this is what you intend when you map a generic right to STANDARD_RIGHTS_READ.
You must also define GENERIC_ALL as a superset of the mappings for GENERIC_WRITE and GENERIC_READ in the GENERIC_MAPPING structure; otherwise, generic rights will be mapped to no rights.
Note that the implementation of SetObjectSecurity differs somewhat between the CUserSecObject and CKernelSecObject classes on one hand, and the CPrivateSecObject class on the other, because a call to SetPrivateObjectSecurity is more complex than a call to SetUserObjectSecurity or SetKernelObjectSecurity: The latter two calls accept an object handle and an SD as parameters, whereas SetPrivateObjectSecurity does not accept an object handle, but two SDs (yes!), an access token, and a GENERIC_MAPPING structure.
Yikes! Why is this? Isn't the whole thing complicated enough as is?
Quite possibly, but this isn't that difficult to understand. SetPrivateObjectSecurity does not accept an object handle because a private SD is not associated with any object per se—the security, as we discussed before, is enforced through explicit calls to AccessCheck. The two SDs and the generic mapping structure make sense because, as we noticed before, the logic that builds and maintains the SDs (as well as the code that requests access later on) deals with generic rather than specific rights, whereas the "internal" representation of the SD deals with standard and specific rights. Using the GENERIC_MAPPING structure, SetPrivateObjectSecurity converts the SD that is passed as the second parameter to an SD that is returned.
The access token that SetPrivateObjectSecurity expects as the last parameter is part of a "meta-security" issue. The target SD that SetPrivateObjectSecurity expects should have been created by a CreatePrivateObjectSecurity call. To ensure that a malicious user does not manipulate a private SD, the user assigned to the SD is derived from the access token passed into CreatePrivateObjectSecurity. Any attempt to change the SD using SetPrivateObjectSecurity is validated against the access token of the user who tries to manipulate the security.
Note It is possible to use "free-flowing" private security, that is, to call AccessCheck against an SD that is not maintained by the CreatePrivateObjectSecurity and SetPrivateObjectSecurity functions. Remember that SetPrivateObjectSecurity converts one SD into another, so you could use the "input" SD to do your access check. However, using the private security API is a safeguard against unauthorized attempts to change SDs.
After reading this article, you should have a solid understanding of security programming under Windows NT. In the next article in this series, "Security Bits and Pieces," you will see the CSecureableObject class hierarchy put into effect.
Token's User SID
This is a SID that is used to compare to SIDs in DACL(s) and SACL(s)
SID domain == RUEDIGERDOM, Name == ruediger S-1-5-21-52177637-1330004027-2027761767-1001
SID type is SidTypeUser
Token's User SID Attributes == 0x00000000
These should always be 0 - see \mstools\h\winnt.h right after
the defines such as SE_GROUP_LOGON_ID - there are no user
attributes yet defined
Token groups (5)
These SID(s) also are used to compare to SIDs in DACL(s) and SACL(s)
Token group (0)
SID domain == RUEDIGERDOM, Name == Domain Users S-1-5-21-52177637-1330004027-2027761767-513
SID type is SidTypeGroup
Token's group (0) attributes == 0x00000007
0x00000001 SE_GROUP_MANDATORY
0x00000002 SE_GROUP_ENABLED_BY_DEFAULT
0x00000004 SE_GROUP_ENABLED
Token group (1)
SID domain == , Name == Everyone S-1-1-0
SID is the World SID
SID type is SidTypeWellKnownGroup
Token's group (1) attributes == 0x00000007
0x00000001 SE_GROUP_MANDATORY
0x00000002 SE_GROUP_ENABLED_BY_DEFAULT
0x00000004 SE_GROUP_ENABLED
Token group (2)
SID domain == BUILTIN, Name == Administrators S-1-5-32-544
SID type is SidTypeAlias
Token's group (2) attributes == 0x0000000f
0x00000001 SE_GROUP_MANDATORY
0x00000002 SE_GROUP_ENABLED_BY_DEFAULT
0x00000004 SE_GROUP_ENABLED
0x00000008 SE_GROUP_OWNER
Token group (3)
SID domain == BUILTIN, Name == Users S-1-5-32-545
SID type is SidTypeAlias
Token's group (3) attributes == 0x00000007
0x00000001 SE_GROUP_MANDATORY
0x00000002 SE_GROUP_ENABLED_BY_DEFAULT
0x00000004 SE_GROUP_ENABLED
Token group (4)
SID domain == NT AUTHORITY, Name == NETWORK S-1-5-2
SID is the Network SID
SID type is SidTypeWellKnownGroup
Token's group (4) attributes == 0x00000007
0x00000001 SE_GROUP_MANDATORY
0x00000002 SE_GROUP_ENABLED_BY_DEFAULT
0x00000004 SE_GROUP_ENABLED
Token privileges (16)
NOTE: Most token privileges are not enabled by default.
For example the privilege to reboot or logoff is not.
0x00000000 for attributes implies the privilege is not enabled.
Use care when enabling privileges. Enable only those needed,
and leave them enabled only for as long as they are needed.
Token's privilege (00) name == SeChangeNotifyPrivilege
Token's privilege (00) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (01) name == SeSecurityPrivilege
Token's privilege (01) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (02) name == SeBackupPrivilege
Token's privilege (02) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (03) name == SeRestorePrivilege
Token's privilege (03) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (04) name == SeSystemtimePrivilege
Token's privilege (04) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (05) name == SeShutdownPrivilege
Token's privilege (05) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (06) name == SeRemoteShutdownPrivilege
Token's privilege (06) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (07) name == SeTakeOwnershipPrivilege
Token's privilege (07) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (08) name == SeDebugPrivilege
Token's privilege (08) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (09) name == SeSystemEnvironmentPrivilege
Token's privilege (09) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (10) name == SeSystemProfilePrivilege
Token's privilege (10) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (11) name == SeProfileSingleProcessPrivilege
Token's privilege (11) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (12) name == SeIncreaseBasePriorityPrivilege
Token's privilege (12) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (13) name == SeLoadDriverPrivilege
Token's privilege (13) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (14) name == SeCreatePagefilePrivilege
Token's privilege (14) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (15) name == SeIncreaseQuotaPrivilege
Token's privilege (15) attributes == 0x00000003
0x00000001
SE_PRIVILEGE_ENABLED_BY_DEFAULT
0x00000002 SE_PRIVILEGE_ENABLED
Token's default-owner-SID for created objects
This is NOT a SID that is used to compare to SIDs in DACL(s) and SACL(s)
SID domain == BUILTIN, Name == Administrators S-1-5-32-544
SID type is SidTypeAlias
Token's Primary Group SID
(Current uses are Posix and Macintosh client support)
SID domain == RUEDIGERDOM, Name == Domain Users S-1-5-21-52177637-1330004027-2027761767-513
SID type is SidTypeGroup
Token's default-DACL for created objects
ACL has 2 ACE(s), 52 bytes used, 120 bytes free
ACL revision is 2 == ACL_REVISION2
ACE 0 size 24
ACE 0 flags 0x00
ACE 0 is an ACCESS_ALLOWED_ACE_TYPE
ACE 0 mask == 0x10000000
Standard Rights == 0x00000000
Specific Rights == 0x00000000
Access System Security == 0x00000000
Generic Rights == 0x10000000
0x10000000 GENERIC_ALL
SID domain == BUILTIN, Name == Administrators S-1-5-32-544
SID type is SidTypeAlias
ACE 1 size 20
ACE 1 flags 0x00
ACE 1 is an ACCESS_ALLOWED_ACE_TYPE
ACE 1 mask == 0x10000000
Standard Rights == 0x00000000
Specific Rights == 0x00000000
Access System Security == 0x00000000
Generic Rights == 0x10000000
0x10000000 GENERIC_ALL
SID domain == NT AUTHORITY, Name == SYSTEM S-1-5-18
SID is the LocalSystem SID
SID type is SidTypeWellKnownGroup
Token's Source
Source Name == NTLanMan
Source Identifier == 0x0000000000000000
Token's Type is TokenImpersonation
Hence the token's TokenImpersonationLevel can be examined
Token is a SecurityImpersonation impersonation token
Token's Statistics
TokenId == 0x0000000000005687
AuthenticationId == 0x0000000000005643
ExpirationTime == (not supported in this release of Windows NT)
TokenType == See token type above
ImpersonationLevel == See impersonation level above (only if TokenType is not TokenPrimary)
DynamicCharged == 500
DynamicAvailable == 300
GroupCount == 5
PrivilegeCount == 16
ModifiedId == 0x0000000000005649