Working with the SACL


The SACL helps you perform application auditing. You can tell Windows to monitor an application or a file and make Event Log entries each time a caller does something with it. Auditing is a feature that the .NET Framework doesn’t provide—you need to use the Win32 API when you want to perform auditing. Using auditing can help you track the activities of crackers and make it less likely that someone can do anything on your system without notice.

Unfortunately, auditing is one of the least used Windows security features because most network administrators don’t want to wade through the security entries that auditing creates. However, it’s an important tool for your security arsenal and you need to know how to use it.

Writing the Auditing Code

Listing 15.2 shows a typical example of adding a SACL entry to a managed application. This listing is incomplete—it doesn’t include the Win32 API function declarations and some of the error handling found in the complete program. (You can find the complete source code for this example in the \Chapter 15\C#\AuditFile and \Chapter 15\VB\AuditFile folders of the source code located on the Sybex Web site.)

Listing 15.2 Adding a SACL Entry to an Application

start example
private void btnTest_Click(object sender, System.EventArgs e) {    IntPtr            SecurityDescriptor;  // File security information.    Int32             LastError;           // Last Win32 API error.    IntPtr            UserSID;             // User security identifier.    Int32             UserSIDSize;         // Size of the user SID.    StringBuilder     DomainName;          // User’s domain.    Int32             DomainNameSize;      // User’s domain name length.    SID_NAME_USE      AccountUse;          // The type of account.    IntPtr            SACL;                // Audit information.    Int32             SACLSize;            // Size of the SACL needed.    ACL               SACLStruct;          // Used for size calculation.    SYSTEM_AUDIT_ACE  AuditACE;            // Holds the audit ACE.    IntPtr            ProcessHandle;       // The current process.    IntPtr            ProcessTokenHandle;  // Process access token.    LUID              SecurePriv;          // Security privilege LUID.    LUID              AuditPriv;           // Audit privilege LUID.    TOKEN_PRIVILEGES  NewPriv;             // New process privileges.    Int32             NewPrivLen;          // Size of NewPriv buffer.      // Determine the variable sizes for the audited user.    UserSID = new IntPtr(0);    UserSIDSize = 0;    DomainName = new StringBuilder();    DomainNameSize = 0;    LookupAccountName(null,                      @txtUserName.Text,                      UserSID,                      ref UserSIDSize,                      DomainName,                      ref DomainNameSize,                      out AccountUse);    // Get the audited user SID.    UserSID = Marshal.AllocHGlobal(UserSIDSize);    if (!LookupAccountName(null,                           @txtUserName.Text,                           UserSID,                           ref UserSIDSize,                           DomainName,                           ref DomainNameSize,                           out AccountUse))    {       // Get the last error.       LastError = Marshal.GetLastWin32Error();       // Display an error message and exit if not successful.       MessageBox.Show("Error getting User SID." +          "\r\nLast Error: " + LastError.ToString(),          "Application Error",          MessageBoxButtons.OK,          MessageBoxIcon.Error);       // Release the memory and exit.       Marshal.FreeHGlobal(UserSID);       return;    }                       // Calculate the size of the SACL.    SACLStruct = new ACL();    AuditACE = new SYSTEM_AUDIT_ACE();    SACLSize = Marshal.SizeOf(SACLStruct) + Marshal.SizeOf(AuditACE) -       Marshal.SizeOf(UserSIDSize) + UserSIDSize;    // Initialize the SACL.    SACL = Marshal.AllocHGlobal(SACLSize);    if (!InitializeAcl(SACL, SACLSize, ACL_REVISION_DS))    {       Handle error...    }    // Add the ACE to the SACL    if (!AddAuditAccessAceEx(       SACL,       ACL_REVISION_DS,       AceFlags.SUCCESSFUL_ACCESS_ACE_FLAG |       AceFlags.FAILED_ACCESS_ACE_FLAG,       AccessMaskFlags.GENERIC_ALL |       AccessMaskFlags.SPECIFIC_RIGHTS_ALL |       AccessMaskFlags.STANDARD_RIGHTS_ALL,       UserSID,       true,       true))    {       Handle error...    }    // Initialize the security descriptor.    SecurityDescriptor =       Marshal.AllocHGlobal(SECURITY_DESCRIPTOR_MIN_LENGTH);    if (!InitializeSecurityDescriptor(SecurityDescriptor,                                      SECURITY_DESCRIPTOR_REVISION))    {       Handle error...    }    // Set the SACL into the security descriptor.    if (!SetSecurityDescriptorSacl(SecurityDescriptor,                                   true,                                   SACL,                                   false))    {       Handle error...    }    // Get the LUID for the security privilege.    if (!LookupPrivilegeValue(null,                              "SeSecurityPrivilege",                              out SecurePriv))    {       Handle error...    }    // Get the LUID for the audit privilege.    if (!LookupPrivilegeValue(null, "SeAuditPrivilege", out AuditPriv))    {       Handle error...    }    // Open the process token.    ProcessHandle = GetCurrentProcess();    if (!OpenProcessToken(ProcessHandle,       TokenAccessFlags.TOKEN_ADJUST_PRIVILEGES |       TokenAccessFlags.TOKEN_QUERY,       out ProcessTokenHandle))    {       Handle error...    }    // Create the token privileges data structure.    NewPriv = new TOKEN_PRIVILEGES();    NewPriv.PrivilegeCount = 2;    NewPriv.Privileges = new LUID_AND_ATTRIBUTES[2];    NewPriv.Privileges[0].Luid = AuditPriv;    NewPriv.Privileges[0].Attributes =       PrivilegeFlags.SE_PRIVILEGE_ENABLED;    NewPriv.Privileges[1].Luid = SecurePriv;    NewPriv.Privileges[1].Attributes =       PrivilegeFlags.SE_PRIVILEGE_ENABLED;    NewPrivLen = Marshal.SizeOf(NewPriv);    // Assign the new privilege to the process.    if (!AdjustTokenPrivileges(ProcessTokenHandle,                               false,                               ref NewPriv,                               NewPrivLen,                               IntPtr.Zero,                               IntPtr.Zero))    {       Handle error...    }    // Verify that the AdjustTokenPrivileges() call actually adjusted    // the privileges.    LastError = Marshal.GetLastWin32Error();    if (LastError != ERROR_SUCCESS)       MessageBox.Show("Error in setting privileges, the application " +                       "may fail.\r\nError is: " + LastError.ToString(),                       "Error Setting Privilege",                       MessageBoxButtons.OK,                       MessageBoxIcon.Warning);    // Close the handles.    CloseHandle(ProcessHandle);    CloseHandle(ProcessTokenHandle);    // Save the resulting security information.    if (!SetFileSecurity(txtFile.Text,                         SECURITY_INFORMATION.SACL_SECURITY_INFORMATION,                         SecurityDescriptor))    {       Handle error...    }    // Free the memory we allocated.    Marshal.FreeHGlobal(UserSID);    Marshal.FreeHGlobal(SACL);    Marshal.FreeHGlobal(SecurityDescriptor); }
end example

This application demonstrates one of the reasons that many developers appreciate the .NET form of security management. Even cutting out the error handling and function declarations, this program runs several pages. Some security exercises for the Win32 API are lengthy and there aren’t any good methods for getting around the problem.

When you assign auditing to a particular object, you have to associate the audit entry with an object and a caller. The caller can be a group, a person, another server—any entity that could request access to the object, resource, or service. Consequently, the code begins by using the LookupAccountName() function to obtain a SID for the account that you provide. This function also returns the domain name for the caller. It’s a handy feature for validation purposes in your code to ensure the caller doesn’t belong to another domain. This code also shows a simple form of the error handling the example uses. Notice that you must free memory before the application exits or the application will develop a memory leak.

At this point, the code knows which caller to associate with an auditing entry, so it begins to build the SACL. This SACL only exists in memory for the moment, so the code needs to know how much memory to allocate for it. Unfortunately, Windows can’t tell the code how much memory to provide, so the code has to calculate the value. The example shows a typical calculation. The SACL must include space for the SACL structure, the audit ACE, and the caller’s SID. However, due to an oddity in the way Windows stores the data, you must subtract the size of a DWORD (Int32) from the calculation. Once the code knows how much memory to allocate, it uses the Marshal.AllocHGlobal() method to perform the task and initializes the SACL using the InitializeAcl() function. You must perform the initialization or the code will report odd errors when you attempt to use the SACL. The most common error is that the SACL version number is wrong. Never attempt to modify the SACL directly.

An empty SACL isn’t very useful, so the code adds an ACE to it next using the AddAuditAccessAceEx() function. The flags for this function are exceptionally important because they determine how the ACE affects the SACL and consequently, the security settings. The example provides the empty SACL as the first entry, the ACE revision number as the second entry, the kind of ACE entries to create as the third entry, the rights that these ACE entries will affect as the fourth entry, and the caller’s SID as the fifth entry. These five entries define how auditing occurs on the system. The example will monitor all access to the target file for the selected user. See the source code and the AddAuditAccessAceEx help topic for a complete listing of flag values.

A SACL appears as part of a security descriptor that also includes owner, group, and DACL information. As with the SACL, never modify the security descriptor directly. The code creates a minimum length security descriptor and then initializes it using the InitializeSecurityDescriptor() function. Notice that this action adds a security descriptor revision number to the structure. The next step is to add the SACL to the security descriptor using the SetSecurityDescriptorSacl() function. At this point, the security descriptor is ready to add to the target file.

There’s only one problem. The application doesn’t have permission to add an auditing entry to anything. Unlike security settings, Windows doesn’t assume that programs should have rights to make SACL entries. Consequently, the next set of steps gives the example permission to perform this step. (Make sure your security policy allows such an action.)

The code begins by obtaining the locally unique identifiers (LUIDs) for the two rights needed to add an auditing entry, SeSecurityPrivilege and SeAuditPrivilege using the LookupPrivilegeValue() function. A LUID is about the same as a GUID, except that Windows doesn’t retain a LUID after the current session.

The code not only needs the kind of permission in the form of a LUID, it also needs to apply that permission to a specific object. For example, you could give the caller permission to perform the act, but that wouldn’t do much good, in this case, because the process is performing the change, not the caller. The code obtains the process token using a combination of the GetCurrentProcess() and OpenProcessToken() functions. The code builds a TOKEN_PRIVILEGES that contains the two LUIDs obtained earlier and the associated permission. It then uses the AdjustTokenPrivileges() function to give the process permission to add the audit entry to the selected file.

You need to be aware of an odd problem that can occur when you call the AdjustTokenPrivileges() function. The function can succeed, without making any change to the process’s privileges. The call did succeed—it didn’t run into any memory or other errors, but the security change can fail. To overcome this problem, Windows records any security errors. However, to obtain these errors, you must call the Marshal.GetLastWin32Error() method after the call. A 0 return value means that the call succeeded and Windows changed the privileges. Unlike an actual call failure, your program could still work if the security update fails, but you still need to consider the fact that it won’t—that’s why the example displays a warning message.

The code has created the security descriptor that includes the auditing changes to the file and the process has permission to apply it. At this point, the code can finally call the SetFileSecurity() function to make the change permanent. Notice that the code ends by clearing up all of the memory items created to perform this task.

Running the Application

Running this example could still present a few surprises. It’s easy to run into a 1314 error, “A required privilege is not held by the client.” when working with this program. The program normally indicates that it couldn’t set the security policy by first displaying a 1300 error, “Not all privileges referenced are assigned to the caller.” When you see this combination of errors, check the local or domain security policy to ensure you have the correct privileges. Verify the Replace a Process Level Token right found in the User Rights Assignment folder of the appropriate MMC console (such as the Local Security Settings console) lists you as one of the entities that can perform this task.

After the program runs, you’ll need to verify that it actually added the entry by checking the security settings for the file. To check the security settings, right-click the file and choose Properties from the context menu to display the file’s Properties dialog box. Select the Security tab and you’ll see a list of common security settings. Click Advanced, select the Auditing tab, and you’ll see a new entry for the user you selected similar to the one shown in Figure 15.2.

click to expand
Figure 15.2: Verify the program worked by checking the advanced security settings for the selected file.

You also need to perform a second check. The fact that you have an audit in place doesn’t mean that the system is set up to allow entry of the events in the Event Log. Open and close the file several times to generate audit events. Open the Event Log and select the Security log. You should see a series of entries for the selected file. If you don’t see these entries, make sure you have the local or domain security policy set to provide an audit policy.

Considering a Security Setting Alternative

Not every object is a file, so you can’t always use the SetFileSecurity() function to set security. Fortunately, the Win32 API supplies a generic alternative that works about the same as the SetFileSecurity() function. Listing 15.3 shows how to use the SetNamedSecurityInfo() function. The example application in the \Chapter 15\C#\AuditFile and \Chapter 15\VB\AuditFile folders of the source code located on the Sybex Web site also contain this code so you can see the program in action.

Listing 15.3 An Alternative Method for Setting Security

start example
LastError = SetNamedSecurityInfo(@txtFile.Text,    SE_OBJECT_TYPE.SE_FILE_OBJECT,    SECURITY_INFORMATION.SACL_SECURITY_INFORMATION,    IntPtr.Zero,    IntPtr.Zero,    IntPtr.Zero,    SACL); if (LastError != ERROR_SUCCESS)    MessageBox.Show("Error setting the security descriptor." +                    "\r\nLast Error: " + LastError.ToString(),                    "Application Error",                    MessageBoxButtons.OK,                    MessageBoxIcon.Error);
end example

Notice that the SetNamedSecurityInfo() function accepts each of the four security descriptor entries separately, so you don’t have to create a security descriptor to hold them. You do have to tell the function what types of entries you want to make using the SECURITY_INFORMATION enumeration. Unlike the SetFileSecurity() function, the SetNamedSecurityInfo() function returns error values directly, so you need to use a slightly different technique for handling errors, as shown in the listing.




.Net Development Security Solutions
.NET Development Security Solutions
ISBN: 0782142664
EAN: 2147483647
Year: 2003
Pages: 168

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net