Click here to Skip to main content
15,867,704 members
Articles / Programming Languages / Visual Basic
Article

The Windows Access Control Model Part 3

Rate me:
Please Sign up or sign in to vote.
4.80/5 (28 votes)
1 Jul 200525 min read 231.9K   5.2K   126   36
In the third part of this series, we will take a tour of the new access control classes coming in .NET v2.0.

Image 1

Figure 28: The ReadSD program that can read and edit a registry security descriptor.

Introduction

This four part series will discuss the Windows Access Control model and its implementation in Windows NT and 2000. In the third part of the series, we will delve into the new security classes introduced for version 2.0 of the .NET framework.

I'm not sure if you have read parts 1 and 2 of this series. If you are new to ACL based security and haven't read part 1[^], I strongly recommend you read it before continuing. (Don't worry. Even though it's in the C++ section, there isn't much C++ code in it.) Part 2 should be safe to skip if you are a .NET programmer. To prepare for part 3, make sure you have a copy of Visual Studio 2005 and the .NET Framework v2.0 (currently in beta 2).

This article is based on a pre-release version of Visual Studio. The framework may change in future releases, meaning the code may not work in the final version.

Table of contents

The table of contents is for the entire series.

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

20. History of .NET security.

Security in .NET was built using the role based security model (which was first seen in NT4 SP3). Role based security was intended as a replacement for the ACL based security model created in 1988. Whilst role based security and code access security have been largely successful for newer applications, software that utilized older protocols (such as Active Directory, COM+, and NTFS), found .NET lacking in fine access control management. To manage security descriptors for these objects, you either had to use the fragile WMI classes, or P/Invoke to some of the most complex functions in the Windows API.

Part of the reason the .NET Framework didn't support ACL-based security was the security descriptor itself. In part 1, the structure of the security descriptor was described, and I think you will agree that the security descriptor is a horrible structure. Microsoft spent most of the 2 years between .NET 1.1 and .NET 2.0 coming up with a class that can handle the security descriptor! However, .NET v2.0 includes a new namespace called System.Security.AccessControl, capable of handling any security descriptor. In the third part of this series, we are going to take a tour on the new security namespace and its related functions.

21. Reading a SID in .NET

The System.Security.Principal namespace (the home of the basic WindowsIdentity/GenericIdentity classes) has two new classes that make viewing SIDs a snap. They are the NTAccount class, and the SecurityIdentifier class. Both inherit from the abstract class IdentityReference, and to translate between the two, you use the virtual method Translate(). The SecurityIdentifier works by utilizing SDDL. Consequentially, the SecurityIdentifier class allows you specify any SDDL user (such as SY) in its constructor. The NTAccount class works by translating the SIDs using the LookupAccountName / LookupAccountSid functions. To print out the SID and username, just call ToString()!

The WindowsIdentity class (which is how you obtained the current username in .NET 1.1) has new properties that return NTAccounts and SecurityIdentifiers for the current user. We can now make a SID lookup program easily:

C#
namespace NetAccessControl
{
  public partial class ViewSids : Form
  {
    public ViewSids()
    {
      InitializeComponent();
    }

    private void chkSidsBtn_Click(object sender, EventArgs e)
    {
      try
      {
        /* disable this button */
        if (this.nameEdit.Text == "")
        {
          /* Convert the SID */
          SecurityIdentifier Sid =
            new SecurityIdentifier(this.sidEdit.Text);
          /* Interestingly, SDDL aliases work here. */
          NTAccount Account =
            (NTAccount)Sid.Translate(typeof(NTAccount));
          this.nameEdit.Text = Account.Value;
        }
        else
        {
          /* Use the Username text box */
          NTAccount Account = new NTAccount(this.nameEdit.Text);
          SecurityIdentifier Sid = (SecurityIdentifier)
            Account.Translate(typeof(SecurityIdentifier));
          this.sidEdit.Text = Sid.Value;
          /** The SID is always returned in textual form, even if it
          *   is well known.
          **/
        }
      }
      catch (System.Exception ex)
      {
        this.sidEdit.Text = "Error occurred: " + ex.Message;
      }
    }
  }
}

Figure 29: Viewing and Translating SIDs in .NET.

If you need the SID of a well known account, just pass in its SDDL representation to the SecurityIdentifier class.

22. Whoami .NET

The WindowsIdentity class has had some improvements that can allow you to obtain some information from an access token (such as the token owner, and the list of groups). Whilst the improvements are definitely welcome, it just doesn't go far enough to be able to port Whoami to .NET. For one thing, it is not possible to list the privileges inside a token, nor can you obtain / set the primary group in a token. WindowsIdentity does not allow you to alter any information from the token.

If you want to obtain Whoami-level information from a token, you'll still have to make some interop calls to the advapi32 functions. That, or find a class that handles the interop for you. I could not find a class that can obtain token information, so I created my own!

AccessToken is a class written in C++/CLI that extends the WindowsIdentity class and adds a few methods of its own to make it as functional as the ATL's CAccessToken class (almost as functional). Although there are some flaws (example, AccessToken maintains a separate token handle that is not equal to the internal WindowsIdentity token—this results in two access token handles) and design issues (class ManagedTokenHandle?!), you can use the class to obtain the token information you can't get with WindowsIdentity. To construct the class you will need an access token. This can be obtained either by interop, or using the provided static method GetAccessToken().

I wrote a program that can obtain a token from any process and extract information from it. It works as long as you have the rights to read a process token (given to all authenticated users by default).

MC++
namespace AccessToken
{

  public class AccessToken : WindowsIdentity
  {

    public AccessToken(IntPtr userToken);
    public AccessToken(IntPtr userToken, String type);

    /// <summary>Constructs the class</summary>
    /// <param name="userToken">The token that 
    /// this class will associate with (obtain using P/Invoke or
    /// static GetAccessToken)</param>
    /// <param name="type">All other parameters are for
    /// WindowsIdentity</param>
    public AccessToken(IntPtr userToken, String type,
      WindowsAccountType acctType);

    /// <summary>Opens an access token, using the specified
    /// WindowsIdentity. By default, the current process token is
    /// opened.
    /// </summary>
    /// <param name="userToken">The token to the user process.
    /// </param>
    /// <param name="type">WindowsAuthentication or custom
    /// authentication</param>
    /// <param name="acctType"></param>
    /// <param name="isAuthenticated"></param>
    public AccessToken(IntPtr userToken, String type,
    WindowsAccountType acctType, bool isAuthenticated);

    /// <summary>frees up resources</summary>
    public ~AccessToken();


    /// <summary>If you need a token, obtain it using this function
    /// </summary>
    /// <param name="processHandle">An optional handle to the
    /// process for which you want an access token.</param>
    /// <param name="dwDesiredAccess">The requested rights to the
    /// handle</param>
    public static ManagedTokenHandle GetAccessToken(
      IntPtr processHandle /* = 0 */,
      TokenAccessLevels dwDesiredAccess);

    /// <summary>We need a setter for the Token function (to override
    /// its functionality).
    /// </summary>
    public property IntPtr Token;


    /* Implemented methods. */
    /// <summary>Duplicates the ATL library function.
    /// BUGBUG: this does not update the Token from WindowsIdentity
    /// </summary>
    public void GetEffectiveToken(TokenAccessLevels dwDesiredAccess);

    public void GetProcessToken(TokenAccessLevels dwDesiredAccess,
      IntPtr hProcess /* = null */);

    public void OpenThreadToken(TokenAccessLevels dwDesiredAccess,
      bool bImpersonate /*= false*/, bool bOpenAsSelf /*= true*/,
      TokenImpersonationLevel Impersonator /* = Impersonation */);

    public void GetThreadToken(TokenAccessLevels dwDesiredAccess,
      IntPtr hThread /* = null */, bool bOpenAsSelf /* = true */);

    /// <summary>Enables or disables the specified privilege.
    /// Unnecessary in .NET (The framework functions do this
    /// automatically). Don't forget to disable the privilege once
    /// you've used it.</summary>
    /// <param name="privilegeName">The privilege to enable</param>
    /// <param name="bEnable">true to enable, false to
    /// disable.</param>
    public void SetPrivilege(String privilegeName, bool bEnable);

    public NameValueCollection GetPrivileges();

    /** Due to marshalling, there is no advantage in implementing
    * the other Privilege setters.
    **/

    /* Properties */
    public property RawAcl DefaultDacl;
    public property SecurityIdentifier PrimaryGroup;
    public property TOKEN_TYPEEnum Type;
    public property Int64 LogonSessionId;
    public property unsigned long TerminalServicesSessionId;

    /// <summary>Implements the Win32 LogonUser API for .NET
    /// </summary>
    public static ManagedTokenHandle LogonUser(String Username,
      String Domain, SecureString Password, int dwLogonType,
      int dwLogonProvider);

    /// <summary>Takes the class's access token, and
    /// creates an impersonation token from it. this.Token needs
    /// to have been opened with the TokenDuplicate right.
    /// </summary>
    /// <param name="sil">the impersonation
    /// level for the new token.</param>
    /// <returns>a handle to the new impersonation token.
    /// Excepts on failure.</returns>
    ManagedTokenHandle
      CreateImpersonationToken(TokenImpersonationLevel sil);
  }
} /* namespace AccessToken */

Figure 30: The AccessToken class to fill in the missing pieces from WindowsIdentity.

Instead of returning status values (like CAccessToken does), AccessToken throws either SecurityExceptions or Win32Exceptions (access denied errors manifest as a Win32Exception(5)).

23. Privilege handling in .NET

If you stick with the .NET Framework, you never need to worry about privileges. The .NET functions automatically enable and disable privileges as needed (they throw PrivilegeNotHeldExceptions if the user is not allowed to use a privilege). However, if you are interacting with low-level / legacy code, then enabling and disabling privileges can be a useful technique. Unfortunately, .NET doesn't yet provide a way to enable privileges in a token (an attempt to keep you clear of legacy code).

The good news is that my AccessToken class can easily enable and disable privileges in a token (it cannot assign privileges to a user...yet). It has the SDK[^] SetPrivilege() function, ported to the .NET Framework. Simply call SetPrivilege with the name of your privilege, and the class will enable it for you. The test program I have made for fig. 31 can enable / disable privileges as long as you have the rights to write a process token (normally given to administrators and owners only). Simply right click a privilege, and select enable.

Note, if you can find a .NET Framework function that can perform your task, use the framework function instead.

C#
private void enableToolStripMenuItem_Click(object sender, EventArgs e)
{
  /* We are required to enable the privilege */
  try
  {
    /* Open up the token with the access token */
    if (MessageBox.Show(this, "Caution, changing access tokens in this" +
      " manner is unsafe and can" +
      " lead to system instability. Do you wish to continue?",
      "Are you sure?", MessageBoxButtons.YesNo, MessageBoxIcon.Warning,
    MessageBoxDefaultButton.Button2) == DialogResult.No)
    {
      return;
    }
    AccessToken.AccessToken ProcToken;
    AccessToken.TokenJobHandle tmpToken =
      AccessToken.AccessToken.GetAccessToken
      (System.Diagnostics.Process.GetProcessById(this.PID).Handle,
      System.Security.Principal.TokenAccessLevels.Read |
      System.Security.Principal.TokenAccessLevels.Write);
    ProcToken = new AccessToken.AccessToken(tmpToken.HandleInternal);
    /* We opened up an access token. Enable the PrivContextmenu */
    ProcToken.SetPrivilege(this.privListView.SelectedItems[0].Text, true);
  }
  catch (System.ComponentModel.Win32Exception)
  {
  }
}

Figure 31: Enabling and disabling privileges in an access token.

24. Running as an unprivileged user in .NET

When I was writing the code for part 2 of this series, I used two methods to run a process as a limited user. The first method used the classic CreateRestrictedToken() API to create the restricted token, and the other was to utilize WinXP's Software Restriction policies to return a predefined restricted token. When I tried to execute notepad using the first method, I found that notepad failed to execute. (GetLastError() mentioned something about an SXS failure.) The workaround was to start an older version of notepad (one from Win98 in this case). The second method never had any of these problems, and GetLastError() didn't return any error. For this part I am going to create a user using the Software Restriction Policies (sorry Win2K users).

AccessToken.Safer is a port of the SaferRaiiWrapper class I wrote in part 2 to the .NET Framework. The SAFER functions are one of the easier functions to P/Invoke, so it was easy writing the class in C#. Fig. 32 shows the Safer wrapper class, ported to a C# class library.

C#
class Safer
{
  [Flags]
  public enum SaferLevelEnum
  {
    Disallowed = 0,
    Untrusted = 0x1000,
    Constrained = 0x10000,
    NormalUser = 0x20000,
    FullyTrusted = 0x40000
  }

  [Flags]
  public enum SaferScopeEnum
  {
    Machine = 1,
    User = 2
  }

  /* From http://www.pinvoke.net */
  [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
  protected struct STARTUPINFO;

  [StructLayout(LayoutKind.Sequential)]
  public struct PROCESS_INFORMATION;

  [StructLayout(LayoutKind.Sequential)]
  public struct SECURITY_ATTRIBUTES;

  /// <summary>
  /// Pinvoke version of CreateProcessAsUser (needed because
  /// Process.Start is not compatible with Safer).
  /// </summary>
  /// <returns>the result from CreateProcessAsUser</returns>
  [DllImport("advapi32.dll", SetLastError = true,
    CharSet = CharSet.Unicode)]
  protected static extern bool CreateProcessAsUser(IntPtr hToken,
    string lpApplicationName,
    string lpCommandLine, IntPtr lpProcessAttributes,
    IntPtr lpThreadAttributes, Boolean bInheritHandles,
    uint dwCreationFlags,
    IntPtr lpEnvironment, string lpCurrentDirectory,
    ref STARTUPINFO lpStartupInfo,
    out PROCESS_INFORMATION lpProcessInformation);


  /// <summary>
  /// PInvoked version of SaferCreateLevel. Try not to use
  /// these functions directly.
  /// </summary>
  [DllImport("advapi32", SetLastError = true,
    CallingConvention = CallingConvention.StdCall)]
  public static extern bool SaferCreateLevel(SaferScopeEnum dwScopeId,
    SaferLevelEnum dwLevelId, int OpenFlags,
    out IntPtr pLevelHandle, IntPtr lpReserved);

  /// <summary>
  /// PInvoked version of SaferComputeTokenLevel. Try not to use these.
  /// </summary>
  [DllImport("advapi32", SetLastError = true,
    CallingConvention = CallingConvention.StdCall)]
  public static extern bool SaferComputeTokenFromLevel
  (IntPtr LevelHandle, IntPtr InAccessToken, out IntPtr OutAccessToken,
    int dwFlags, IntPtr lpReserved);

  /// <summary>
  /// PInvoked version of SaferCloseLevel. Try not to use these.
  /// </summary>
  [DllImport("advapi32", SetLastError = true,
    CallingConvention = CallingConvention.StdCall)]
  public static extern bool SaferCloseLevel(IntPtr hLevelHandle);

  [DllImport("kernel32", SetLastError = true,
    CallingConvention = CallingConvention.StdCall)]
  private static extern bool CloseHandle(IntPtr hObject);


  /// <summary>
  /// Provides a managed frontend for the SAFER CreateProcessAsUser().
  /// Stdio redirection is not supported.
  /// </summary>
  /// <param name="startInfo">The startInfo you'd
  /// normally send to Process.Start</param>
  /// <returns>the status of CreateProcess()</returns>
  public bool StartProcess(System.Diagnostics.ProcessStartInfo startInfo);


  protected IntPtr LevelHandle_;
  protected IntPtr Token_;
  private PROCESS_INFORMATION lpProcInfo_;
  public PROCESS_INFORMATION lpProcInfo;

  /// <summary>
  /// Creates a Safer Level Handle using the current process and user
  /// as a template.
  /// </summary>
  public Safer();

  ~Safer();

  /// <summary>
  /// Creates a Safer Level Handle from the specified access token.
  /// </summary>
  /// <param name="dwScopeIdIn">the safer level you want
  /// to use (defaults to NormalUser)</param>
  /// <param name="hTokenIn">the access token
  /// with which you want to build from (defaults to current token)
  /// </param>
  public Safer(SaferLevelEnum dwScopeIdIn, TokenJobHandle hTokenIn);
}

Figure 32: Using the Software Restriction Policies in .NET.

25. Obtaining and editing a Security Descriptor in .NET, and applying it to an object

All .NET programmers would have at least encountered one of the classes listed in column 1 of fig. 33. They're the base classes that handle file system objects, registry keys, and synchronization objects. If you take a look at the docs[^] for these classes, you may notice a new method called GetAccessControl present in all of these classes. GetAccessControl returns the security descriptor for the object. The return type of GetAccessControl is dependent on which object you are securing. The second column in fig. 33 illustrates which class is returned from GetAccessControl.

GetAccessControl provides a gateway to a whole new namespace: System.Security.AccessControl. This namespace comprises the BCL's implementation of the security descriptor. You'll notice that there are new classes specially designed for manipulating the security descriptor and its parts. Let's take a tour of the AccessControl namespace.

ClassReturn type of GetAccessControl
<N/A>ActiveDirectorySecurity
DirectoryDirectorySecurity
DirectoryInfoDirectorySecurity
EventWaitHandleEventWaitHandleSecurity
FileFileSecurity
FileInfoFileSecurity
FileStreamFileSecurity
MutexMutexSecurity
RegistryKey (The rest of the article will be based on this class.)RegistrySecurity
SemaphoreSemaphoreSecurity
<N/A>DirectoryObjectSecurity
<N/A>CryptoKeySecurity

Figure 33: The list of supported secure objects in the .NET Framework.

The possible return types for GetAccessControl.

As was mentioned, GetAccessControl returns a System.Security.AccessControl class that depends on the object being secured. These classes derive from CommonObjectSecurity (which in turn implements ObjectSecurity), and provides methods to access the five parts of the security descriptor. If you recall from part 1, there is no such thing as a "Common Security Descriptor" (due to differing sets of access masks). Therefore, GetAccessControl has to return differing classes for different objects.

If I was writing the class, I would have made CommonObjectSecurity a generic class, where the template parameter is the enum you are passing into the function. That way, GetAccessControl wouldn't need to return differing access control types.

For the purpose of this article, we will focus solely on the RegistrySecurity. With minor differences, what applies to RegistrySecurity applies to all the other classes. The RegistryKey is a pure container class—securing registry values is not supported. As a container, it also supports auto-inherited ACLs. You can read a key, enumerate its values, create a sub key, create a hard link, get notified of key changes, delete a key, and read and alter the permissions. The complete list of what you can do with a registry key is listed in the RegistryRights enum. RegistrySecurity provides RegistryAuditRule to enumerate its SACL and RegistryAccessRule to enumerate the DACL.

Extracting information from a CommonObjectSecurity.

The CommonObjectSecurity class seems to have been modeled after the CSecurityDesc from ATL. You will notice that the owner, group, DACL and SACL are not referenced by properties in the class. Instead you have to call the pseudo-property functions GetOwner and SetOwner and friends:

  • GetOwner/ SetOwner: retrieves the owner of the security descriptor as a SecurityIdentifier.
  • GetGroup / SetGroup: retrieves the primary group of the security descriptor as a SecurityIdentifier.
  • GetAuditRules: retrieves the SACL of the security descriptor as an AuthorizationRuleCollection.
  • GetAccessRules: retrieves the DACL of the security descriptor as an AuthorizationRuleCollection.
  • GetSecurityDescriptorSddlForm / SetSecurityDescriptorSddlForm: retrieves the SDDL string of the security descriptor.
  • GetSecurityDescriptorBinaryForm / SetSecurityDescriptorBinaryForm: retrieves the PSECURITY_DESCRIPTOR (native) form of the security descriptor.
  • SetAccessRuleProtection: makes the DACL auto-inherit from its parent.
  • SetAuditRuleProtection: makes the SACL auto-inherit from its parent.

However, the control flags are implemented as properties (talk about inconsistency!). You can retrieve the auto inheritance settings from the AreAccessRulesProtected / AreAuditRulesProtected (recall if an ACL is protected, it does not auto-inherit). If you read part 2, you'll know that some of the Windows APIs did not support inheritance, hence could corrupt the inheritance settings of your machine. The good news is that .NET fully supports the inheritance mechanism and will properly preserve the inheritance settings. If you opened a security descriptor that somehow got a disordered ACL (maybe from some rogue Win32 app), an InvalidOperationException will be thrown if you tried to edit it.

If you did want to retrieve the size of the security descriptor (possibly an external function couldn't be bothered with calling GetSecurityDescriptorLength), just get the binary form of the security descriptor as a byte[] array, and return the Length.

If you build the security descriptor from scratch using the generic AccessControl classes (such as CommonSecurityDescriptor or RawSecurityDescriptor), the security descriptor parts are implemented as properties.

This section applies to all objects deriving from common object security, not just RegistrySecurity. The provided ReadSD program is a Windows Forms dialog that illustrates how to obtain a registry security. Supply the location and name of a registry key to view its security descriptor, then press OK. The owner, group, and SDDL of the registry key is then displayed:

C#
private void OKButton_Click(object sender, EventArgs e)
{
  try
  {
    Microsoft.Win32.RegistryHive Hive =
      (Microsoft.Win32.RegistryHive)(this.regHiveSelect.SelectedValue);
    /* Normalize the computer name. */
    string ComputerName = this.computerEdit.Text.Trim(" \\".ToCharArray());
    Microsoft.Win32.RegistryKey regKey =
      Microsoft.Win32.RegistryKey.OpenRemoteBaseKey(Hive, ComputerName);

    regKey = regKey.OpenSubKey(this.regKeyEdit.Text, true);
    this.ppSD = regKey.GetAccessControl();
    this.OutputGroupBox.Enabled = true;

    /* Owner */
    NTAccount OwnerUser =
      (NTAccount)(this.ppSD.GetOwner(typeof(NTAccount)));
    this.ownerEdit.Text = OwnerUser.ToString();
    this.ownerLbl.Enabled = true;
    this.ownerEdit.Enabled = true;

    /* Group */
    OwnerUser =
      (NTAccount)(this.ppSD.GetGroup(typeof(NTAccount)));
    this.groupEdit.Text = OwnerUser.ToString();
    this.groupLbl.Enabled = true;
    this.groupEdit.Enabled = true;

    /* Let user see SACL/DACL */
    this.sACLButton.Enabled = true;
    this.dACLButton.Enabled = true;
    this.sddlMsgButton.Enabled = true;
  }
  catch(System.Exception ex)
  {
    this.statusLabel1.Text = "Error occurred: " + ex.Message;
  }
}

Figure 34: Retrieving the security descriptor from a registry key.

Reading the access control lists from a security descriptor.

By now, you should have already called GetAccessRules / GetAuditRules on the ObjectSecurity object. This returns your access control list as a collection. This can be iterated over with a foreach statement or array indexing. The data type encapsulated in the collection depends on the object we are securing. For registry keys, it is a RegistryAccessRule / RegistryAuditRule.

Note that unlike the Win32 ACE, the ACE flags aren't bunched into a single DWORD. They are split over the properties of the ACE:

  • AccessControlType (AccessRule only): Whether the discretionary ACE is deny or allow (or something else). Corresponds to AceType in ACE_HEADER.
  • AuditFlags (AuditRule only): Whether the system ACE audits successes or failures. Corresponds to AceType in ACE_HEADER.
  • IdentityReference: Who the ACE applies to (either an IdentityReference or NTAccount). Corresponds to SidStart in the ACCESS_ALLOWED_ACE.
  • InheritanceFlags: Which type of children the ACE propagates to. Corresponds to CI and OI in AceFlags.
  • IsInherited: Whether the ACE was an inherited ACL. Corresponds to ID in AceFlags.
  • PropagationFlags: Specifies the type of inheritance for the ACE. Corresponds to NP and IO in AceFlags.
  • AccessMask: the set of access rights for the ACE as an object-dependent enumeration (in our example this is the RegistryRights Enum). Corresponds to AccessMask in ACCESS_ALLOWED_ACE.

Now it is possible to view the access control list for the registry key. The provided ReadSD program reads each access control entry from the list, obtains the list of data, and presents the results in an un-optimized DataGridView. For reasons which will become clear, the DataGridView only shows explicit ACEs and hides the inherited ACEs. ReadSD presents the ACE flags as a separate dialog box (just a bunch of checkboxes databound to the ACE flags). Simply press the View button to see the ACE flags.

When writing to an access control list, you have to consider several issues. First is the preferred ordering of an ACE. The framework does provide the AreAccessRulesCanonical / AreAuditRulesCanonical property to determine if the ACL is well ordered. But what you want is to never have disordered ACEs in the first place. How are you going to deal with this situation?

The second issue is how to proceed if there is already a matching ACE. Suppose you wanted to add read access to an object for yourself, but there was already an ACE that granted your group full access. What do you do about the present ACE? Do you erase the entry, or leave it as it is, or do you completely erase the entire ACL and start from scratch? It depends on why you need to alter the DACL in the first place.

The third issue is how to deal with auto-inheritance. You may note from part 2 that auto-inherited ACEs cannot be edited. (This is the reason why ReadSD's DataGridView doesn't show inherited ACEs). You can only override the inherited settings with deny ACEs, or set the ACL to be protected. If you enable ACL protection you should remember to copy the inherited ACEs onto the current ACL, since a fully auto-inherited ACL has no explicit ACEs.

C#
protected void RefreshDataGrid()
{
  int i = 0;
  /* clear the old view */
  this.daclLView.Rows.Clear();
  foreach (AuthorizationRule Entry in GetAuthzRules(false))
  {
    this.daclLView.Rows.Add();

    /* Username */
    this.daclLView["userLViewCol", i].Value
      = Entry.IdentityReference.ToString();

    /* Allow/deny */
    this.daclLView["typeLViewCol", i].Value
      = GetAccessType(Entry);

    /* Access mask */
    uint accessMask = GetAccessRightsFromEntry(Entry);
    this.daclLView["permsLViewCol", i].Value
      = Convert.ToString(accessMask, 16);

    /* inheritance */
    this.daclLView["inheritLVewCol", i].Tag
      = Entry;
    i++;
  }
}

Figure 35: Reading the contents of an discretionary access control list to a DataGridView with transformations.

Editing the system / discretionary access control list

The good news is that the .NET Framework provides a wealth of functions to resolve all of these issues in whatever way you want.

Starting with the third issue. You can toggle the protection (inheritance) for a security descriptor's DACL using SetAccessRuleProtection, specify true to enable protection (disable auto-inheritance) and false to disable protection (enable auto-inheritance). What's that? You want to copy over the inherited ACEs rather than remove them? No problem—just take a look at the second parameter of SetAccessRuleProtection. For the SACL, the function to use is called SetAuditRuleProtection. Note that an auto-inherited ACL cannot be edited. Therefore, when searching for ACEs to remove, .NET ignores inherited rights. An exception is when you are adding rights to yourself. If .NET finds that an inherited ACE grants you the requested access, then a rewrite of the ACL is not necessary and .NET does not add the inherited ACE.

On the second issue, the .NET Framework provides a plethora of functions to add (or update or whatever) a discretionary ACE:

DACL EditingSACL EditingDescriptionReturns
AddAccessRuleAddAuditRuleAdds an ACE. If one already exists, the new ACE is merged onto the old one.void
ModifyAccessRuleModifyAuditRuleModifies an existing ACE. If none exists, no changes are applied.false if an existing ACE was not found.
RemoveAccessRuleRemoveAuditRuleRemoves rights from an existing compatible ACE. If a more liberal ACE is found, these flags are removed from it (the remaining flags are retained). You are left with a more restrictive ACE.false if a compatible ACE was not found.
RemoveAccessRule<BR>SpecificRemoveAuditRule<BR>SpecificRemoves an existing ACE. The existing ACE must exactly match this ACE. The ACE is then removed.false if the ACE was not found.
RemoveAccessRuleAllRemoveAuditRuleAllRemoves an entire ACE. The ACE only needs to match the user name and Allow / Deny / Audit flag.void. No action is taken if the rule doesn't exist.
PurgeAccessRulesPurgeAuditRulesRemoves all ACEs that match the user. The user no longer appears in the ACL.void.
ResetAccessRule Removes all ACEs that match the user, then applies the specified AccessRule to the object.void. If the user does not yet exist in the ACL, they are added.
SetAccessRuleSetAuditRuleRemoves an entire ACE. The ACE only needs to match the user name and Allow/Deny/Audit flag. Then this ACE is applied.void. If the user does not yet exist in the ACL, they are added.

Figure 36: Available methods for ObjectSecurity

If you want to edit an auto-inherited ACE, you must override them with explicit deny ACEs. The .NET Framework does not take into account group membership issues (e.g. if you wanted to remove a deny ACE on yourself, but the deny ACE is applied to one of your groups, then .NET will not find that group). The functions do take into account the preferred ordering of ACEs however.

If you prefer to work with SDDL, that's completely fine—you can use Get / SetSecurityDescriptorSddlForm. And if you prefer to work with the low level functions (say you needed to interface with a native library), that's fine too. You can use the GenericAcl classes to build an ACL, and GenericSecurityDescriptor to build a security descriptor in binary (these low level classes do have real properties like Control and DiscretionaryAcl, unlike the ObjectSecurity classes). Note however, that with these object-agnostic classes, it becomes once again possible to disorder the ACEs (for a few cases, that may be your intent). To convert a GenericSecurityDescriptor to an ObjectSecurity class, call the GetBinaryForm method, and apply it to the ObjectSecurity object using SetSecurityDescriptorBinaryForm.

When using ReadSD, you may have noticed that ReadSD displays different entries than what Regedit shows. In particular, you may have noticed for each inherited ACE, ReadSD shows each entry twice. What ReadSD is showing you is the complete DACL, as it is read in the registry. You see, Regedit merges similar allow and deny entries together so they appear as one ACE. ReadSD doesn't perform this merge (it shows the unedited security descriptor), hence the duplicated entry. In addition, Regedit remaps generic rights (such as GENERIC_READ) to registry specific rights (in this case KEY_READ or RegistryRights.Read) using the Win32 MapGenericMask(). In order to behave like Regedit, ReadSD provides an option to remap the generic rights into specific rights. To do so we need to make use of the Win32 MapGenericMask API. By selecting the checkbox labeled "remap generic rights", the generic access rights will be remapped to Regedit-style. Unfortunately, ReadSD does not merge ACEs belonging to the same user. The solution to this problem will be discussed in part 4.

ReadSD's DataGridView is editable and allows the user to edit the DACL for the registry key. To facilitate SACL auditing, an inherited form is created from the DACL dialog. Some of the DACL specific functions in the form are overridden in the derived form so that we can reuse the DACL dialog for SACL editing.

Committing the security descriptor.

When you update the RegistrySecurity (or ObjectSecurity) object, changes are not immediately reflected in the registry key. This allows you to preview your changes and possibly cancel them before committing. It also allows you to make more than one change to the security descriptor.

In order to change the owner of a security descriptor, you need to have a SecurityIdentifier / NTAccount / IdentityReference object referencing that user (refer back to fig. 29 for that). ReadSD allows you to type in the username in its dialog, which it will then convert to an NTAccount class. Once you have an IdentityReference, call the SetOwner method on the RegistrySecurity object to write the new owner. In order to set the group, use the corresponding SetGroup.

Now we have come to the point to actually write the security descriptor. Do you remember in fig. 33 there were a number of classes that implemented a GetAccessControl method to retrieve a security descriptor? For all of these classes, there is a corresponding SetAccessControl method that (you guessed it) writes the security descriptor to the resource. Once you have created / edited the security descriptor, [re] open a handle to your resource object and call SetAccessControl on it. With luck (and sufficient rights), the new security descriptor should then be applied onto the object. And that should be enough to secure your objects with the .NET Framework.

I've implemented a number of checks in ReadSD to prevent you from applying a security descriptor once you have edited it (for example, you have to preview the SDDL, and enable an undocumented App.config setting) before ReadSD will write a security descriptor to the registry. I've implemented these checks to stop you from misusing ReadSD to edit the security descriptors. ReadSD shouldn't be used in production machines since it is capable of corrupting your registry. To find out what you need to do to get ReadSD to write security descriptors, you'll have to study the code. A more durable interface will be presented in part 4 of the series.

C#
private void applyButton_Click(object sender, EventArgs e)
{
  /** The final security descriptor is now available. Commit to the
  * registry key
  **/
  if(!this.ppSD.GetOwner(typeof(NTAccount))
    .ToString().Equals(this.ownerEdit.Text))
    this.ppSD.SetOwner(new NTAccount(this.ownerEdit.Text));
  /* Update the Security descriptor if necessary */
  if (!this.ppSD.GetGroup(typeof(NTAccount))
    .ToString().Equals(this.groupEdit.Text))
    this.ppSD.SetGroup(new NTAccount(this.groupEdit.Text));

  /* Reopen the Registry key. */
  Microsoft.Win32.RegistryHive Hive =
    (Microsoft.Win32.RegistryHive)(this.regHiveSelect.SelectedValue);

  string ComputerName = this.computerEdit.Text.Trim(" \\".ToCharArray());
  Microsoft.Win32.RegistryKey regKey =
    Microsoft.Win32.RegistryKey.OpenRemoteBaseKey(Hive, ComputerName);
  using(regKey = regKey.OpenSubKey(this.regKeyEdit.Text, true))
  {
    regKey.SetAccessControl(this.ppSD);
    /* update the status afterward. */
    this.OKButton.PerformClick();
    regKey.Close();
  }
}

Figure 37: Applying a security descriptor to a registry key.

26. Access checks in .NET

In part 2, we went through performing access checks using the Win32 AccessCheck API. Unfortunately, there doesn't seem to be an equivalent managed function that can perform the task. It's not recommended for you perform an access check in .NET. Instead, you should make use of role-based security to perform an access check for you (This is what ReadSD does. Before ReadSD writes a security descriptor, it needs to check if you are allowed to alter the security descriptor. It does this by reading the security descriptor and calling GenericPrincipal.IsInRole to check for group membership). This only works if your objects are designed for role-based security. It does not work with objects secured by security descriptors.

If you need to perform an access check on an object with a security descriptor (Registry key in our case), you wouldn't use AccessCheck to do so (even in Win32). The proper method is to open up the registry key, and if the security descriptor denies access, you will get an "access denied" exception.

In simple access checks, you may be able to perform the access check yourself with the help of an imperative role-based security (fig. 38):

C#
public static bool DoManualAccess(RegistrySecurity regKey,
                                            uint DesiredAccess)
{
  /* returns false if the user is likely to get Access denied. */

  if(regKey.GetOwner(typeof(SecurityIdentifier))
    == WindowsIdentity.GetCurrent().User)
  {/* The owner has ChangePermissions | ReadControl */
    DesiredAccess &=
      ~((uint)RegistryRights.ChangePermissions |
      (uint)RegistryRights.ReadPermissions);
  }
  /** BUGBUG: If there is a deny ACE that is present in our token,
  * it's not checked properly against deny SIDs. As a result, access
  * may be granted rather than denied.
  *
  * For AccCheckFrm and DACLForm, we are saved because we open up the
  * registry key later, and that Will fail as expected.
  **/
  foreach (RegistryAccessRule Entry in
    regKey.GetAccessRules(true, true, typeof(SecurityIdentifier)))
  {
    WindowsPrincipal userSID =
      new WindowsPrincipal(WindowsIdentity.GetCurrent());

    /* Remap generic rights. */
    uint accessMask = (uint)(Entry.RegistryRights);
    DACLForm.GENERIC_MAPPING genericReg = DACLForm.RegGeneric();
    DACLForm.MapGenericMask(ref accessMask, ref genericReg);

    /* IsInRole handles most of the Group stuff for us. */
    if (userSID.IsInRole
      ((SecurityIdentifier)(Entry.IdentityReference)))
    {
      /* This right applies to us. */
      if ((accessMask & DesiredAccess) != 0)
      {
        /** This right holds our desired Registry permission.
        * If its allow, true, otherwise false
        **/
        if (Entry.AccessControlType == AccessControlType.Deny)
        {
          /** Some rights are disallowed. You're gonna get an
          * error 5
          **/
          return false;
        }
        else
        {/* NAND out the Access flags from DesiredAccess. */
          DesiredAccess &= ~(accessMask);
        }
      }
    }
  }
  /* At the end, DesiredAccess should be 0 */
  if (DesiredAccess == 0) return true;
  return false;
}

Figure 38: Manually performing an access check with the help of role based security.

This code suffers from a flaw. To check for group membership, fig. 38 makes use of the IsInRole method. The IsInRole method does not take into account that some SIDs may be deny-only SIDs. If a deny-only SID is encountered, AccessCheck does not consider the SID as valid if it is checking an allow ACE (if an allow ACE allows a deny only user, AccessCheck will not apply the rules). The IsInRole method does not include deny-only groups, meaning our AccessCheck implementation is incomplete. For this reason, I provide a demo of a .NET P/Invoked AccessCheck function (fig. 39). However, in order for it to work, you require the AccessToken class from fig. 30:

C#
/* Wrapper class for the AccessCheck API. */
[DllImport("advapi32", SetLastError=true)]
protected static extern bool AccessCheck(
  [MarshalAs(UnmanagedType.LPArray)]
  byte [] pSecurityDescriptor,
  IntPtr ClientToken,
  uint DesiredAccess,
  [In] ref DACLForm.GENERIC_MAPPING GenericMapping,
  IntPtr PrivilegeSet,
  ref uint PrivilegeSetLength,
  out uint GrantedAccess,
  out bool AccessStatus);

/// <summary>
/// This function provides a managed wrapper over the AccessCheck API,
/// minus the rubbish (And AccessCheck does have a lot of useless
/// parameters).
/// </summary>
/// <param name="regKey">The security descriptor of the
/// registry key.</param>
/// <param name="DesiredAccess">The rights you want to the
/// object.</param>
/// <returns>true if access is allowed, false for access denied.
/// May except with a Win32Exception.</returns>
public static bool DoRegistryAccessCheck(RegistrySecurity regKey,
  uint DesiredAccess)
{
  using (AccessToken.ManagedTokenHandle procHandle
    = AccessToken.AccessToken.
    GetAccessToken(TokenAccessLevels.Duplicate))
  using (AccessToken.AccessToken procToken =
    new AccessToken.AccessToken
    (procHandle.HandleInternal))
  using (AccessToken.ManagedTokenHandle userToken =
    procToken.CreateImpersonationToken
    (TokenImpersonationLevel.Impersonation))
  {
    uint PrivilegeSetLength = 0, GrantedAccess = 0;
    bool AccessStatus = false;
    IntPtr PrivilegeSet = IntPtr.Zero;
    byte [] RawForm = regKey.GetSecurityDescriptorBinaryForm();

    DACLForm.GENERIC_MAPPING genericRegMapping =
      DACLForm.RegGeneric();
    DACLForm.MapGenericMask(ref DesiredAccess, ref genericRegMapping);

    /* The first call to AccessCheck is designed to fail. */
    AccessCheck(RawForm, userToken.HandleInternal, DesiredAccess,
      ref genericRegMapping, PrivilegeSet, ref PrivilegeSetLength,
      out GrantedAccess, out AccessStatus);

    PrivilegeSet =
      Marshal.AllocHGlobal(Convert.ToInt32(PrivilegeSetLength));

    if (!AccessCheck(RawForm, userToken.HandleInternal,
      DesiredAccess, ref genericRegMapping, PrivilegeSet,
      ref PrivilegeSetLength, out GrantedAccess, out AccessStatus))
    {
      throw new System.ComponentModel.Win32Exception();
    }
    /** AccessStatus should be true, and GrantedAccess should
    * be equal to
    * DesiredAccess.
    **/
    if (AccessStatus == true && DesiredAccess == GrantedAccess)
      return true;
    else
      return false;
  }
}

Figure 39: Performing an access check with the help of the Win32 AccessCheck API.

And privately secured objects? Don't even bother. Make use of declarative code access security instead.

27. Summary

In this part, you were shown the new AccessControl classes coming in .NET v2.0. You were shown how to obtain, edit and translate SIDs with the classes (previously not possible in managed code). You were then introduced to a port of the CAccessToken class to .NET, and implemented a Whoami program using the AccessToken .NET class. A similar port of the SaferRaiiWrapper (from part 2) class was created. You used this class to spawn a low-privileged application. Next you were shown how security descriptors were implemented across the .NET Framework. Finally, a demonstration of access checking in .NET was shown (hastily thrown together).

The new .NET access control classes bring the power of the Framework to access control (and vice versa). Although not perfect, they make access control programming in .NET far easier (compare the code in this article to the code inside the GotDotNet security library [^] class). However, there are areas where the AccessControl classes can improve on. Four such areas are with access tokens, privileges, generic right mapping and access checking. Being in Beta, the Framework may change between now and the final release.

Thanks to the power of the .NET Framework classes, the sample programs for part 3 are far more usable [and reusable] than their cousins in part 2. The class library contains the C++/CLI code for class AccessToken, and the C# code for class SAFERWrapper and class AccessCheckWrapper. The demo project is a set of five programs:

  1. "Convert SIDs to User Name" converts between SIDs and user account names.
  2. "Read Token" is a port of the Whoami GUI to C#.NET (with the help of AccessToken).
  3. "Make Unprivileged User" demonstrates how to use the SAFERWrapper class to create a low-privileged program.
  4. "Security Descriptor Factory" is a program that displays the security descriptor for a registry key. Both local and remote registry keys are supported (as long as the necessary services are started).
  5. "Access Check" demonstrates how to perform Access Checks with the .NET classes and platform interop.

With the exception of class AccessToken, all code was written in Visual C# 2005 Express Edition beta 2. The entire code for this article requires version 2.0 of the .NET Framework. If you get a BadImageException whilst running the program, it's because you don't have .NET v2.0. If you get numerous CS1518 errors about "partial", it's because you are using Visual Studio .NET [2003]. Upgrade to Visual Studio 2005.

Coming up

At last you now have prerequisite knowledge to program the Windows ACL Editor. The Windows 2000 ACL editor offers a superior interface to security descriptor editing (compared to my pathetic DataGridView -based editor in ReadSD). Part 4 will cover the structure of the ACL Editor, and provide a programming guide to the ISecurityInformation interface (the core of the ACL UI). The ACL editor was originally going to be the only article I wanted to post on access control [then I got carried away and started this series]. Part 4 will make a return to C++ grounds, and will bring in COM. For you .NET programmers, you may still be able to benefit from part 4, with the help of Keith Brown's CCW class (Note that Keith Brown's class was written before .NET v2.0. It's not aware of System.Security.AccessControl). As a preparation for the final part, make sure you know about implementing COM classes from scratch (see here, here, and here).

History is now maintained in part 4[^].

Next Part...[^]

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


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

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

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

Comments and Discussions

 
GeneralUsing System.Security.AccessControl to secure private objects Pin
Olivier Oswald7-Apr-08 4:37
Olivier Oswald7-Apr-08 4:37 
QuestionIs there a .Net 1.1 alternative? Pin
DuneAgent27-Nov-06 4:21
DuneAgent27-Nov-06 4:21 
AnswerRe: Is there a .Net 1.1 alternative? Pin
oshah27-Nov-06 7:51
oshah27-Nov-06 7:51 
Unfortunately, the accesscontrol classes were only added in .NET 2.0 (access control in 1.1 relied mostly on role-based security).

For .NET 1.1, your best bet is to use the GotDotNet library[^] (otherwise you're going to have to write a lot of pinvoke dllimports).
GeneralSort of along the same lines [modified] Pin
tatron27-Jul-06 14:29
tatron27-Jul-06 14:29 
GeneralRe: Sort of along the same lines Pin
oshah2-Aug-06 9:36
oshah2-Aug-06 9:36 
QuestionSecureString Pin
El Chubb6-May-06 12:40
El Chubb6-May-06 12:40 
AnswerRe: SecureString Pin
oshah6-May-06 14:11
oshah6-May-06 14:11 
QuestionRe: SecureString Pin
El Chubb7-May-06 7:31
El Chubb7-May-06 7:31 
AnswerRe: SecureString Pin
oshah8-May-06 5:49
oshah8-May-06 5:49 
GeneralRe: SecureString Pin
El Chubb8-May-06 9:10
El Chubb8-May-06 9:10 
GeneralRe: SecureString Pin
oshah8-May-06 11:23
oshah8-May-06 11:23 
GeneralRe: SecureString Pin
El Chubb8-May-06 11:33
El Chubb8-May-06 11:33 
GeneralAccessCheck() problem Pin
sean peters12-Apr-06 10:40
sean peters12-Apr-06 10:40 
GeneralRe: AccessCheck() problem Pin
oshah13-Apr-06 0:47
oshah13-Apr-06 0:47 
GeneralRe: AccessCheck() problem Pin
sean peters13-Apr-06 5:28
sean peters13-Apr-06 5:28 
GeneralModifyAccessRule Error Pin
Jaret27-Jan-06 10:57
Jaret27-Jan-06 10:57 
AnswerRe: ModifyAccessRule Error Pin
oshah28-Jan-06 5:52
oshah28-Jan-06 5:52 
GeneralRe: ModifyAccessRule Error Pin
Jaret28-Jan-06 12:49
Jaret28-Jan-06 12:49 
AnswerRe: ModifyAccessRule Error Pin
oshah31-Jan-06 5:13
oshah31-Jan-06 5:13 
GeneralRe: ModifyAccessRule Error Pin
Jaret31-Jan-06 7:51
Jaret31-Jan-06 7:51 
GeneralRe: ModifyAccessRule Error Pin
Jaret31-Jan-06 12:21
Jaret31-Jan-06 12:21 
GeneralIt's tougher than you think Pin
oshah1-Feb-06 5:14
oshah1-Feb-06 5:14 
GeneralRe: It's tougher than you think Pin
Jaret1-Feb-06 11:12
Jaret1-Feb-06 11:12 
GeneralRe: It's tougher than you think Pin
oshah2-Feb-06 12:02
oshah2-Feb-06 12:02 
GeneralGreat article. Have a question Pin
Narb M5-Jul-05 0:56
Narb M5-Jul-05 0:56 

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

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