Ruediger R. Asche
Microsoft Developer Network Technology Group
July 31, 1995
Click to open or copy the files in the CLIAPP/SRVAPP sample applications.
Following up on my article series on Microsoft® Windows NT™ security (see "Windows NT Security in Theory and Practice" and associated articles in the MSDN Library), this article shows how you can incorporate auditing within the CSecureableObject C++ class hierarchy. I discuss the necessary steps for turning on auditing on a Windows NT machine and explain how to programmatically generate audit log entries for system-provided and private securable objects.
CLIAPP/SRVAPP, a sample application suite that consists of a database client and server, illustrates the concepts introduced in this article.
As I mentioned in the article "Windows NT Security in Theory and Practice," the Microsoft® Windows NT™ security application programming interface (API) has two major purposes: to protect objects and to monitor object access. So far in this article series, we have discussed the first aspect, that is, how to protect objects from unauthorized access. This article will deal with the second issue: how to inform a Windows NT server about object access.
When I enhanced the SRVAPP application to add auditing to the CSecureableObject hierarchy, I discovered that a number of strategies in the auditing world are similar to corresponding strategies concerning object protection. However, there are some rather subtle differences in how your machine and your accounts must be set up to deal with auditing. In the first section of this article, I will discuss the analogy between system access control lists (SACLs) and discretionary access control lists (DACLs). The second section will present the code I wrote to manipulate auditing data structures, and the third section will deal with pragmatics, that is, additional information you need about auditing.
Let us quickly recap what we know about security so far. To secure an object against unauthorized access, we associate the object with a security descriptor (SD). The SD has one slot for a discretionary access control list (DACL), which defines the rights individual users or user groups have on the object. Users are identified by unique access tokens, which are consulted whenever an application attempts to access the object. Depending on the object type we are dealing with, either the operating system or the application code matches the DACL in the object's SD against the requesting access token and either grants or refuses the attempted access.
Auditing works similarly. In addition to the slot for the DACL, each SD can accommodate a similar data structure called a system access control list (SACL). Like a DACL, a SACL consists of a list of smaller data structures called access control elements (ACEs). However, while ACEs in DACLs specify user rights (such as "the right to remove eggs from the carton is denied to the user BOZO and everybody in the ELEPHANT group"), ACEs in SACLs specify auditing directives (such as "inform Mom every time somebody from the KIDS group attempts to remove eggs from the carton, regardless of whether the attempt succeeds"). Like ACEs in DACLs, ACEs in SACLs must specify the user or user group and the type of access to be monitored. Thus, building an ACE and linking an ACE to an SACL is almost identical to the process in a DACL. The difference between the ACEs in DACLs and the ACEs in SACLs is that an entry in a DACL may cause a client's attempt to access an object to be refused, whereas an entry in a SACL may cause the server to be alerted when something happens.
Could I be any less specific? What does "cause the server to be alerted when something happens" mean?
First, let's talk about the server to be alerted. When an user does something (here's that "something" again—hang on, I'll tell you what that is in a sec) to an object associated with a SACL that is to be monitored, an event is logged in the security event log on the server machine. This log basically consists of a few informative bits in memory—it is up to a server application to utilize it. One of the things you can do with audit events is to view them interactively with the Event Viewer that is provided with Windows NT (generally located in the Administrative Tools group in Program Manager). Bring up the Event Viewer (notice that it takes special privileges to be able to view events), choose Security from the Log menu, and click one of the entries. The figure below shows a sample entry taken from running the SRVAPP sample.
Sample entry from Event Viewer
Here we see that the user "ruediger" tried to access the object "TestBase" and failed because the access was not granted.
Note that the Event Viewer, like the Clipboard Viewer, is nothing but a convenient front-end for viewing events. Using Windows NT's event log API, you can retrieve the log entries programmatically and use, for example, the messaging API (MAPI) to send e-mail to Mom whenever the security system monitors an attempt by one of the kids to remove the eggs from the carton. Or you could write a service that scans the event log in the background and converts each new entry into a new line in a Microsoft Excel spreadsheet, and show the sheet to Mom at the end of each week, possibly to keep count of the remaining number of eggs in the fridge. This way, she can ask Dad to bring home a new carton of eggs when only a few eggs are left.
Anyway, let's go back to the technical discussion. I mentioned earlier that the code to build and manipulate SACLs is similar to the code to build and maintain DACLs, and, furthermore, that both structures are kept in SDs. Therefore, if you have already written the code that associates an SD with an object, all you need to do to add auditing is to add an SACL to the existing SD.
In theory, that is.
In practice, you need to do a few more things to use both SACLs and DACLs in the same SD. Let's look at some sample code.
If you expect the CSecureableObject class hierarchy to be fortified by new member functions such as AddAuditingForUserAndAccess, you are probably in for a disappointment. I have always felt that a sample application loses a lot of value when it shows more than what is minimally necessary; thus, to demonstrate auditing in the sample application set, I simply added some code to the constructor of CSecureableObject to enable auditing access by everybody. I'm sure you will be able to add more ACEs to the SACL or do whatever your application needs. I will help you with the tough part: linking the SACL to an object.
The code I present in this article can be found in the updated version of the CLIAPP/SRVAPP sample suite that is included with this article series. Note that none of the changes affect the client side. In fact, if you still have an old version of the client application, you can run that application unmodified and not see any change in its behavior.
I wrote the code below to add an SACL to an SD. There is nothing magical about the code—it is mostly an exercise in cut-and-paste. Remember that the security descriptor m_sd has been previously initialized and associated with a DACL. (The code below is from SEC.CPP.)
// At this point, we need to go through the same procedure with an SACL...
if (pcSid) free (pcSid);
pcSid = NULL; // We reuse this data structure.
iTempSidLength = GetSidLengthRequired(1); // This cannot fail.
pcSid = (PSID)malloc(iTempSidLength);
if (!pcSid)
{
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
goto ErrorExit;
};
dwDACLLength = sizeof (ACL) +sizeof (SYSTEM_AUDIT_ACE) - sizeof (DWORD) +
iTempSidLength;
m_pSACL = (PACL) malloc(dwDACLLength);
if (!m_pSACL)
{
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
goto ErrorExit;
};
if (!InitializeAcl(m_pSACL,dwDACLLength,ACL_REVISION)
||!InitializeSid(pcSid,&siaWorld,1))
goto ErrorExit;
*(GetSidSubAuthority(pcSid,0)) = SECURITY_WORLD_RID;
if (!AddAuditAccessAce(m_pSACL,ACL_REVISION,GENERIC_ALL|STANDARD_RIGHTS_ALL|
SPECIFIC_RIGHTS_ALL,pcSid,TRUE,TRUE)) goto ErrorExit;
if (!SetSecurityDescriptorSacl(m_pSD,TRUE,m_pSACL,FALSE)) goto ErrorExit;
SetLastError(ERROR_SUCCESS);
ErrorExit:
m_iSecErrorCode = GetLastError();
if (pcSid) free (pcSid);
Note that the SD is nothing but a data structure in memory at this point and has no association whatsoever with the object that it is supposed to protect. As I discussed in the article "The Guts of Security," we turn the SD into an "official" data structure by calling one of the security functions that associate the SD with an object: SetKernelObjectSecurity, SetUserObjectSecurity, or SetPrivateObjectSecurity.
I also changed the calls to set the system security along with the discretionary security; for example, in the CKernelSecObject::SetTheDescriptor function:
if (!SetKernelObjectSecurity(...,DACL_SECURITY_INFORMATION|SACL_SECURITY_
INFORMATION))...
This line tells the security system to use both the SACL and DACL from the security descriptor for the object's internal security descriptor. Note that you could ask the security system to ignore existing auditing information when building the object's SD by leaving out SACL_SECURITY_INFORMATION from the call above.
The big surprise was that the SetKernelObjectSecurity function returned with the error "access denied," refusing to perform the security change. What happened?
The documentation says that the calling process needs to have SeSecurityPrivilege enabled to access the system security. Ah, a meta-security issue! So although DACL information can be changed without special privileges, a server application must be run by a user who has a certain privilege to perform auditing! Interesting. . . So my next task was to learn everything about the wonderful world of privileges. So here we go.
I discussed the concept of privileges in my earlier article, "Windows NT Security in Theory and Practice." In summary, a privilege constitutes the part of the security model that is not centered around specific objects; instead, a privilege is associated with an access token.
There is a subtle but important distinction between "having a privilege" and "having a privilege enabled." If a user (or, to be more precise, an access token) "has a privilege," all that means is that an entry that represents the privilege exists in the access token. To use the privilege, a server must "enable" it.
To make this a little bit clearer, let us look at the privilege section of a sample access token. The following access token dump was generated by calling the ExamineAccessToken function from EXAMSTFF.CPP:
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 == SeShutdownPrivilege
Token's privilege (01) attributes == 0x00000000
Token's privilege (02) name == SeSecurityPrivilege
Token's privilege (02) attributes == 0x00000000
Token's privilege (03) name == SeBackupPrivilege
Token's privilege (03) attributes == 0x00000000
Token's privilege (04) name == SeRestorePrivilege
Token's privilege (04) attributes == 0x00000000
Token's privilege (05) name == SeSystemtimePrivilege
Token's privilege (05) attributes == 0x00000000
Token's privilege (06) name == SeRemoteShutdownPrivilege
Token's privilege (06) attributes == 0x00000000
Token's privilege (07) name == SeTakeOwnershipPrivilege
Token's privilege (07) attributes == 0x00000000
Token's privilege (08) name == SeDebugPrivilege
Token's privilege (08) attributes == 0x00000002
0x00000002 SE_PRIVILEGE_ENABLED
Token's privilege (09) name == SeSystemEnvironmentPrivilege
Token's privilege (09) attributes == 0x00000000
Token's privilege (10) name == SeSystemProfilePrivilege
Token's privilege (10) attributes == 0x00000000
Token's privilege (11) name == SeProfileSingleProcessPrivilege
Token's privilege (11) attributes == 0x00000000
Token's privilege (12) name == SeIncreaseBasePriorityPrivilege
Token's privilege (12) attributes == 0x00000000
Token's privilege (13) name == SeLoadDriverPrivilege
Token's privilege (13) attributes == 0x00000000
Token's privilege (14) name == SeCreatePagefilePrivilege
Token's privilege (14) attributes == 0x00000000
Token's privilege (15) name == SeIncreaseQuotaPrivilege
Token's privilege (15) attributes == 0x00000000
You will notice that the token has only 16 privileges (only one of which is enabled), but Windows NT currently defines 24 privileges. Where are the other 8 privileges?
It's rather simple: The access token does not have the privilege, therefore trying to enable one of the missing privileges with the AdjustTokenInformation call does not succeed. (To be precise, the call to AdjustTokenPrivileges returns TRUE, but a GetLastError call returns ERROR_PRIVILEGE_NOT_HELD. You figure.)
How do you assign the privilege? Before Windows NT version 3.51, you could assign a privilege only interactively, from the User Rights dialog box in the User Manager Policies dialog box. Using this technique, you must log off from Windows NT after assigning the privilege and then log on again for the updated access token to take effect.
Windows NT 3.51 exposes the calls for assigning privileges in the LSA API; that is, you can now assign privileges to users programmatically, but you will still need to log off and log on again as the user for the changes to take effect.
After the privilege has been assigned but not enabled, you can see the privilege in the access token dump as follows:
Token's privilege (16) name == SeAuditPrivilege
Token's privilege (16) attributes == 0x00000000
To figure out which privilege you need to do something, look in the Win32® Software Development Kit (SDK) documentation. The comments section for a particular function generally includes a remark such as "A process must have the AUDIT_NAME privilege enabled to call this function."
Great. . . The privilege is called AUDIT_NAME, but if you want to enable the privilege using AdjustTokenPrivileges later on, you need to pass the string "SeAuditPrivilege," but neither the privilege name nor the string is visible in the User Manager, User Rights dialog box. How do we know which privilege goes with which description?
Fortunately, you have the MSDN Library CD. You can find a list of all privilege names, along with their corresponding strings and the complete descriptions that appear in the User Manager, in the Knowledge Base article Q101366, "Definition and List of Windows NT Advanced User Rights." The description of AUDIT_NAME in that article states: "The user can generate audit-log entries." This is what you need to select in the User Manager, Policies/User Rights dialog box to assign the AUDIT_NAME privilege to a user. When you make that assignment, and then log off and on again, the server application can successfully enable the privilege using AdjustTokenInformation, passing in the "SeAuditPrivilege" string. Phew!
If you wish to determine the names of the privileges programmatically, you can use the LookupPrivilegeDisplayName and LookupPrivilegeName functions.
After learning all of this about privileges, I recycled a function that I had seen before: SetPrivilegeInAccessToken, which is used in the CHECK_SD sample to enable the SeSecurityPrivilege we need to access system security. Right behind the call to SetPrivilegeInAccessToken, I inserted a call to ExamineAccessToken to double-check that I had done the right thing. The access token dump revealed that the access token indeed had SeSecurityPrivilege enabled:
Token's privilege (16) name == SeAuditPrivilege
Token's privilege (16) attributes == 0x00000002 SE_PRIVILEGE_ENABLED
We see there that the SeSecurityPrivilege is indeed enabled, so now we should be able to call SetKernelObjectSecurity and succeed, right?
Wrong. The documentation is a bit unclear here. What the SeSecurityPrivilege buys us is simply the ability to obtain a new handle to the kernel object that requests ACCESS_SYSTEM_SECURITY rights. It is that NEW handle on which SetKernelObjectSecurity succeeds. Let us look at the modified code for CKernelSecObject::SetObjectSecurity:
BOOL CKernelSecObject::SetObjectSecurity(HANDLE hObject)
{
BOOL bErrorCode=TRUE;
if (!SetKernelObjectSecurity(hObject,DACL_SECURITY_INFORMATION|
SACL_SECURITY_INFORMATION,m_pSD))
{
m_iSecErrorCode = GetLastError();
bErrorCode = FALSE;
};
FreeDataStructures();
return bErrorCode;
};
The only difference between this code and the old version is that now we request SACL_SECURITY_INFORMATION along with the previous DACL_SECURITY_INFORMATION access to the handle. To do that, the object-specific implementation of SetTheDescriptor must duplicate the handle, as we discussed earlier. The example below is for the secured file-mapping object:
BOOL CSecuredFileMapping::SetTheDescriptor(void)
{
HANDLE hNewHandle;
BOOL bReturn=FALSE;
SetPrivilegeInAccessToken(TRUE);
if (!DuplicateHandle(GetCurrentProcess(),m_hMap,GetCurrentProcess(),&hNewHandle,
WRITE_DAC|ACCESS_SYSTEM_SECURITY,NULL,NULL))
{
GetLastError();
goto ErrorExit;
};
bReturn = SetObjectSecurity(hNewHandle);
CloseHandle(hNewHandle);
ErrorExit:
SetPrivilegeInAccessToken(FALSE);
return bReturn;
};
As long as the SeSecurityPrivilege privilege is not enabled, the DuplicateHandle call will fail with the error "access denied." With the privilege enabled, we can obtain the duplicate handle, use SetKernelObjectSecurity on it, and then close the handle. Note that the WRITE_DAC access is necessary to set the DACL from the security descriptor to the object. Note, once more, that no special privilege is necessary to request WRITE_DAC access to the handle, but that the SeSecurityPrivilege privilege must be enabled to request ACCESS_SYSTEM_SECURITY.
The same argument holds for the GetTheDescriptor member function, except that WRITE_DAC is required to set a DACL, whereas READ_CONTROL is required to read a DACL from an object. Thus, in the CSecuredFileMapping::GetTheDescriptor function, WRITE_DAC is replaced by READ_CONTROL.
Now here is something else that may make you wonder: My definition of a kernel-secure object is an object that is initially associated with a DACL that doesn't grant access to anyone. Thus, the very first time an object's security is set, the object's default DACL is replaced by a very strictly secured DACL. Doesn't it now follow that the next time we access an object's DACL, we wouldn't even be able to request READ_CONTROL because the object is now so secure that it is even protected against that access?
You may be surprised to learn that this is not the case: Although the object is now associated with security information that does not grant anyone any rights, we can still duplicate the handle with a request to access WRITE_DAC or READ_CONTROL. Is that a security leak?
No, it is simply a slick design that provides a well-controlled trap door to make sure that we cannot pull the carpet from under our own feet. A security descriptor contains not only a DACL and an SACL, but also an SID that represents the owner. The owner always has special rights, one of which is to be able to access the object's DACL. This is a nice way to keep the owner of an object from locking himself or herself out. Of course, it is possible to either reset the owner to somebody without rights, or to change the rights of the owner. This way, you can write an application that secures an object so well that nobody can possibly do anything with it (like the poor chap who built himself a fallout shelter so secure that after he locked himself in, he couldn't get out, so he starved to death).
In any case, the server application that created the objects in the first place is their owner and can, therefore, change the security regardless of whether the right to access the security is granted.
Note one more thing: When you call DuplicateHandle to obtain a handle that allows you to party on the object's DACL and SACL, at the minimum, you must specify ACCESS_SYSTEM_SECURITY and either READ_CONTROL or WRITE_DAC access at the minimum, depending on whether you want to read from, or write to, the object's security. Why don't we specify anything else, for example, FILE_MAP_ALL_ACCESS for file-mapping objects? This includes READ_CONTROL|WRITE_DAC, so wouldn't it be safest to request all access?
Nope, because now the argument about the stiffly protected objects holds. If you specified ACCESS_SYSTEM_SECURITY|FILE_MAP_ALL_ACCESS as requested rights for DuplicateHandle on a file-mapping object, the doomsday scenario that I pictured earlier would kick in: The first time you set the security, you are fine, because the default DACL for file-mapping objects grants you FILE_MAP_ALL_ACCESS. Then you associate a new DACL with the object. This DACL grants nobody access, so the next time you try to duplicate a handle to the object, you have locked yourself out because you, as the owner, may have READ_CONTROL and WRITE_DAC access, but none of the other rights that FILE_MAP_ALL_ACCESS contains. Thus, minimal is best when it comes to security.
After I figured out all of these little trapdoors and gotchas (some of which I wouldn't have figured out without the help of some of the developers of the security system), I single-stepped through the code and received no more "access denied" errors. I figured that I would now be able to see the event log entries in the Event Viewer, but I was mistaken: I had to solve one more piece of the puzzle—enabling object auditing. (I already took the fun out by revealing this to you in the preceding section on privileges.)
At this point, I had extended the original client-server application set to incorporate auditing on kernel objects. That is, whenever the client tried to access either the secured mutex, the secured shared file, or the secured named pipe, a new event log entry was generated.
One last piece was missing, and that was extending the auditing capabilities to privately secured objects. You will recall that the server application manages four secured object types: mutexes, named pipes, shared files, and a homegrown database object. Let us see how we can add auditing to privately secured objects.
When it comes to DACLs, the effort it takes to protect objects against unauthorized access is considerably higher for private objects than for kernel or user objects, as we saw in the article "The Guts of Security." There are two reasons for this oddity:
That said, I had a slight suspicion that auditing private objects would provide a number of new and interesting challenges over kernel objects. Guess what? I was right.
My first thought was, hey, maybe the AccessCheck function (which performs the security check in CPrivateSecObject::MatchAccessRequest) is smart enough to see if the SD it works on has an SACL associated with it, and will, therefore, automatically generate an audit if the SACL says so. AccessCheck is called. Nice try, but wrong answer. Next candidate, please.
It turns out that a second-degree cousin of AccessCheck called AccessCheckAndAuditAlarm does exactly what AccessCheck does, but also generates audit logs according to the SACL information in its security descriptor. (In other words, AccessCheckAndAuditAlarm does exactly what I wanted it to do.) Unfortunately, you cannot simply replace AccessCheck with AccessCheckAndAuditAlarm, because the two functions are, well, second-degree cousins.
Some of the parameters of AccessCheck and AccessCheckAndAuditAlarm overlap, but AccessCheckAndAuditAlarm has some new parameters. Also, the user that runs the server application must have the SeAuditPrivilege privilege enabled to call AccessCheckAndAuditAlarm. Recall the distinction between "having a privilege" and "having a privilege enabled," which I discussed earlier. By default, the SeAuditPrivilege is not even in your access token, so you must first assign the privilege to the token, and then enable it programmatically.
The first three parameters of AccessCheckAndAuditAlarm are strings that will be copied into the log entry. The first parameter is the identifier of the "object server," which is basically a process or service that maintains a custom securable object. Our server application is an object server because it maintains the database object type. I chose the name "DBServer" to identify the server application. The name of the object server will show up in the security log entry to inform the user which process generated the audit.
The second string identifies the object type, and the third parameter specifies the name of the object instance. If your application supports multiple instances of object types, it is a good idea to assign unique names to each generated object to make it easier for the user of the security log to determine which instance of the object type caused the audit. Likewise, the object type name provides a nice way for you to distinguish between multiple object types.
With the exception of the second parameter (object handle) and last parameter (bAuditGenerated), all remaining parameters of AccessCheckAndAuditAlarm are identical to AccessCheck parameters, so I will not discuss them here (please refer to "The Guts of Security" for more information). The last parameter (bAuditGenerated) is for subsequent use with the ObjectCloseAuditAlarm function, and the object handle parameter identifies the security log entry. It is up to you to determine what the value should be. There is no requirement that the handle must be unique, but using a unique handle can help you determine which object access has generated a log entry. For the CPrivateSecObject class, I devised a hack to make the handles unique: Each instance of a CPrivateSecObject object has a private member variable called m_dwUniqueHandleId. This member variable is initialized to the this pointer, converted to a DWORD, and with every successful audit, the handle is incremented. This is useful for debugging purposes, because you can always determine the this pointer from a debugger. This, along with the current value of the m_dwUniqueHandleId member, can help you track down the object access generated by the security log entry.
Debugging secure servers is not at all like debugging anything else. The problems you most frequently encounter in most applications are data overwrites, access violations, synchronization problems, and the like. The problem you generally encounter in a secure server is that one function call that is supposed to succeed returns the error "access denied," or a call that should fail with "access denied" returns successfully. Even worse, you can't really poke around in the system to figure out what's going on (for security reasons).
The best thing to do to figure out why a client has access when he/she shouldn't or vice versa is to manually examine the SD associated with the object in question and the access token of the requesting client at the same time. There are two ways to accomplish this:
Given the access token and SD information, you should be able to determine why your object access calls and audit requests behave the way they do.
To make auditing work in your server application, you need to do a number of things. I have put together a checklist to make sure that everything works smoothly: