Click here to Skip to main content
15,868,141 members
Articles / Programming Languages / C++
Article

Securing a Registry Key ACL using .NET

Rate me:
Please Sign up or sign in to vote.
4.09/5 (7 votes)
19 May 20054 min read 67.5K   20   5
Managing ACLs and securing registry keys using .NET.

Introduction

The .NET API provided by Microsoft allows you to create, modify, and delete keys in the Windows registry easily, all from managed code. The problem comes in when you want to control access to the registry keys, which, until the release of .NET 2.0, must be performed by Win32 API calls. For example, you may store secure information for your application in the registry and you want to be sure that information is only available to the users of that application and not read or modified by other users. To do so, your application must modify the Access Control List (ACL) for that registry key.

This article demonstrates the process of looking up a Security Identifier (SID) for either a user account or a built-in group (like Administrators), creating Access Control Entities (ACE) to define the access rights for each SID, and adding those ACEs to an ACL which is then used to create a registry key with restricted access.

Background

My first shot at this task attempted to perform the entire process in C#, using P/Invoke to make calls to the Win32 APIs. I gave up and moved to Managed C++ after unsuccessfully troubleshooting a NullPointerException in the call to SetEntriesInAcl(). The majority of this code will work in unmanaged C++ or C just as well; the only .NET specific code deals with marshalling the string parameter from .NET to an LPCTSTR for the Win32 calls and a few lines of console output.

Using the code

This code is intended to demonstrate a number of functions, including looking up a SID based on username or built in group, creating an ACL, and applying that ACL to a registry key. You may only want to lookup a SID, or you may want to apply your ACL to a file. The process is generally the same, but you will need to deviate from the code here to meet your application needs. To apply an ACL to a file rather than a registry key, instead of passing the ACL to a SECURITY_ATTRIBUTE that is passed to RegCreateKeyEx, you will pass the ACL to a SECURITY_DESCRIPTOR to pass to SetFileSecurity.

The code initially takes in a .NET string and creates an LPCTSTR which is necessary for the Win32 API calls. Next, a call to LookupAccountName is used to get the SID for that user. To retrieve the SID for a well known group (Administrators, in this case), a call to AllocateAndInitializeSid is used.

Next, the code creates an array of EXPLICIT_ACCESS structs where the access is defined for each SID. These structures will equate to the ACEs in the final ACL. The order of the structs in the array is important, since an ACL is evaluated from top to bottom. That is, if a user is explicitly given read only access in the first structure, and a group to which that user belongs is given full control in a later struct, the first structure will take precedence and the user will have read only access.

Once the EXPLICIT_ACCESS array is created, a call is made to SetEntriesInAcl to actually create the ACL. This method also accepts a pointer to another ACL, if your intention is to modify a current ACL rather than creating a new one. After the ACL is created, it's just a few more API calls to InitializeSecurityDescriptor to create a SECURITY_DESCRIPTOR struct and SetSecurityDescriptorDacl to set the ACL to the SECURITY_DESCRIPTOR. This SECURITY_DESCRIPTOR is the one you would pass in a call to SetFileSecurity if you wanted to set the ACL on a file.

Once the SECURITY_DESCRIPTOR is ready, it can be set to a SECURITY_ATTRIBUTES struct. This SECURITY_ATTRIBUTES structure is passed by reference to the RegCreateKeyEx method. Note that you will need to open any higher level registry keys such as the the SOFTWARE key to access the location where the new registry key will be created. Finally, there is a labeled section of code called Cleanup: that releases any resources that were allocated during the method's execution.

Managed C++ source file

C++
// This is the main DLL file.

#include "stdafx.h"

#include "SecureRegistry.h"

namespace SecureRegistry
{
    long KeyManager::CreateRestrictedRegKey(String* account)
    {
        // Convert .NET string type to LPCTSTR (remember to free LPCTSTR after use)
        LPCTSTR accountName = (const char *)
               (Marshal::StringToHGlobalAnsi(account)).ToPointer();

        DWORD dwRes, dwDisposition;
        PSID pAdminSID = NULL;
        PSID pUserSID = NULL;
        PACL pACL = NULL;
        PSECURITY_DESCRIPTOR pSD = NULL;
        EXPLICIT_ACCESS ea[2];
        SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY;
        SECURITY_ATTRIBUTES sa;
        DWORD cbSid = 0;
        DWORD dwRefDomain = NULL;
        SID_NAME_USE peUse;
        HKEY softwareKey = NULL;
        HKEY companyKey = NULL;
        HKEY securedKey = NULL;
        LONG lRes = 0;

        // Lookup SID for specific user account that is passed in to this method
        if(!LookupAccountName(NULL,accountName,NULL, 
            &cbSid,NULL,&dwRefDomain,&peUse))
        {
            pUserSID = LocalAlloc(LPTR,cbSid);
            TCHAR refDomain[128];
            if(!LookupAccountName(NULL,accountName,pUserSID, 
               &cbSid,refDomain,&dwRefDomain,&peUse))
            {
                System::Console::WriteLine("Unable to initialize" 
                       " User SID - {0}", GetLastError().ToString());
                Marshal::FreeHGlobal(System::IntPtr((void*)accountName));
                return 0;
            }
        }

        // Frees LPCTSTR
        Marshal::FreeHGlobal(System::IntPtr((void*)accountName));

        // Validate SID before continuing
        if(!IsValidSid(pUserSID))
        {
            System::Console::WriteLine("Invalid UserSID");
            goto Cleanup;
        }

        // Lookup a well known group, Administrators
        if(!AllocateAndInitializeSid(&SIDAuthNT, 2, 
            SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 
            0,0,0,0,0,0,&pAdminSID))
        {
            System::Console::WriteLine("Unable to initialize" 
                    " Admin SID - {0}", GetLastError().ToString());
            goto Cleanup;
        }

        // Create two EXPLICIT_ATTRIBUTES to be used in the ACL
        // Administrators will have full access in this ACL
        ea[0].grfAccessPermissions = KEY_ALL_ACCESS;
        ea[0].grfAccessMode = SET_ACCESS;
        ea[0].grfInheritance = NO_INHERITANCE;
        ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
        ea[0].Trustee.TrusteeType = TRUSTEE_IS_GROUP;
        ea[0].Trustee.ptstrName = (LPTSTR) pAdminSID;

        // The specified user will have read access in the ACL
        ea[1].grfAccessPermissions = KEY_READ;
        ea[1].grfAccessMode = SET_ACCESS;
        ea[1].grfInheritance = NO_INHERITANCE;
        ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
        ea[1].Trustee.TrusteeType = TRUSTEE_IS_USER;
        ea[1].Trustee.ptstrName = (LPTSTR) pUserSID;

        // Create the new ACL containing the EXPLICIT_ATTRIBUTES
        dwRes = SetEntriesInAcl(2, ea, NULL, &pACL);
        if(dwRes != ERROR_SUCCESS)
        {
            System::Console::WriteLine("Unable to set entries" 
                    " in ACL - {0}", GetLastError().ToString());
            goto Cleanup;
        }

        // Allocate memory for a SECURITY_DESCRIPTOR
        pSD = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR, 
                           SECURITY_DESCRIPTOR_MIN_LENGTH);

        // Initialize a new SECURITY_DESCRIPTOR
        if(!InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION))
        {
            System::Console::WriteLine("Unable to initialize" 
                 " security descriptor - {0}", GetLastError().ToString());
            goto Cleanup;
        }

        // Set the ACL to the SECURITY_DESCRIPTOR
        if(!SetSecurityDescriptorDacl(pSD, TRUE, pACL, FALSE))
        {
            System::Console::WriteLine("Unable to set" 
               " security descriptor DACL - {0}", GetLastError().ToString());
            goto Cleanup;
        }

        // Apply the SECURITY_DESCRIPTOR to the SECURITY_ATTRIBUTES struct
        sa.nLength = sizeof(SECURITY_ATTRIBUTES);
        sa.lpSecurityDescriptor = pSD;
        sa.bInheritHandle = FALSE;

        // Lookup registry key where security should be applied
        lRes = RegOpenKeyEx(HKEY_LOCAL_MACHINE, 
                 "SOFTWARE",0,KEY_ALL_ACCESS,&softwareKey);
        if(lRes == ERROR_SUCCESS)
        {
            lRes = RegOpenKeyEx(softwareKey,"MyCompany", 0, 
                                     KEY_ALL_ACCESS,&companyKey);
        }
        if(lRes == ERROR_SUCCESS)
        {
            // Create the new registry key, applying the SECURITY_ATTRIBUTES
            lRes = RegCreateKeyEx(companyKey,"MySecuredKey",0,"",0,
                KEY_READ | KEY_WRITE, &sa, &securedKey, &dwDisposition);
        }

Cleanup:  // Free any resources allocated during execution of this method
        {
            if(pAdminSID)
                FreeSid(pAdminSID);
            if(pUserSID)
                FreeSid(pUserSID);
            if(pACL)
                LocalFree(pACL);
            if(pSD)
                LocalFree(pSD);
            if(securedKey)
                LocalFree(securedKey);
            if(companyKey)
                LocalFree(companyKey);
            if(softwareKey)
                LocalFree(softwareKey);
        }

        return lRes;
    }
}

Header file

C++
// SecureRegKeyNET.h

#pragma once

using namespace System;
using namespace System::Runtime::InteropServices;

namespace SecureRegistry
{
    public __gc class KeyManager
    {
    public:
        static long CreateRestrictedRegKey(String* account);
    };
}

stadfx.h header file

C++
#pragma once

#include <windows.h>
#include <aclapi.h>

I look forward to the day when .NET 2.0 is released and I don't have to use Win32 API calls to manage ACLs on registry keys. Until then, I am happy to use C++ to perform the dirty work and provide a simple static method call to use in other .NET applications. If you have any questions, suggestions, or improvements to this code, or if you managed to successfully tackle this using P/Invoke, please let me know.

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
Systems Analyst specializing in Java and C# development.

Comments and Discussions

 
GeneralRegCreateKeyEx Pin
Member 68943213-Jun-05 3:34
Member 68943213-Jun-05 3:34 
QuestionHard isn't it? Pin
oshah22-May-05 10:35
oshah22-May-05 10:35 
AnswerRe: Hard isn't it? Pin
Dave Curylo, MCAD23-May-05 5:13
Dave Curylo, MCAD23-May-05 5:13 
GeneralRe: Hard isn't it? Pin
oshah24-May-05 11:50
oshah24-May-05 11:50 
Dave Curylo, MCAD wrote:

ConvertStringSecurityDescriptorToSecurityDescriptor is part of the same library as SetEntriesInAcl, so neither of them work in Win9x or NT 4.0.


Correct, but there is a difference between the two. A very important difference.

Using SDDL is easier than using SetEntriesInAcl, ConvertStringSecurityDescriptorToSecurityDescriptor only requires you to manipulate strings. Manipulating strings is far easier to do in .NET than manipulating structures, particularly a complex structure like EXPLICIT_ACCESS. Had you chosen to use SDDL, you may not have given up doing this in C#:


C#
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool LookupAccountName(
  string lpSystemName,
  string lpAccountName,
  [MarshalAs(UnmanagedType.LPArray)] byte[] Sid,
  ref uint cbSid,
  StringBuilder ReferencedDomainName,
  ref uint cchReferencedDomainName,
  out SID_NAME_USE peUse);

[DllImport("kernel32.dll")]
static extern IntPtr LocalFree(IntPtr hMem);

[DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool ConvertSidToStringSid(
  [MarshalAs(UnmanagedType.LPArray)] byte[] pSID,
  out IntPtr ptrSid);

[DllImport("advapi32", SetLastError=true)]
public static extern bool ConvertStringSecurityDescriptorToSecurityDescriptor(
  string StringSecurityDescriptor,
  uint SDRevision,
  ref IntPtr SecurityDescriptor,
  ref uint SecurityDescriptorSize);

[DllImport("advapi32.dll", SetLastError = true)]
static extern int RegCreateKeyEx(
  int hKey,
  string lpSubKey,
  int Reserved,
  string lpClass,
  int dwOptions,
  int samDesired,
  ref SECURITY_ATTRIBUTES lpSecurityAttributes,
  ref int phkResult,
  ref int lpdwDisposition);

const int NO_ERROR = 0;
const int ERROR_INSUFFICIENT_BUFFER = 122;
const int SDDL_REVISION_1 = 1;
const int KEY_WRITE = 131078;
const int KEY_READ = 131097;

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
  public int nLength;
  public IntPtr lpSecurityDescriptor;
  public bool bInheritHandle;
}

enum SID_NAME_USE
{
  SidTypeUser = 1,
  SidTypeGroup,
  SidTypeDomain,
  SidTypeAlias,
  SidTypeWellKnownGroup,
  SidTypeDeletedAccount,
  SidTypeInvalid,
  SidTypeUnknown,
  SidTypeComputer
}

[DllImport("advapi32.dll", SetLastError=true)]
static extern int RegCloseKey(IntPtr hKey);

static int Main(string[] args)
{
  string sddlStr = "D:PAI(A;;KA;;;BA)(A;;KR;;;" +
    GetUserSid(WindowsIdentity.GetCurrent().Name) + ")";
  IntPtr pSDNative = IntPtr.Zero;
  uint pSDLength = 0;
  int phkResult = 0, lpdwDisposition = 0;

  if (!ConvertStringSecurityDescriptorToSecurityDescriptor(sddlStr, SDDL_REVISION_1,
    ref pSDNative, ref pSDLength))
  {
    throw new System.ComponentModel.Win32Exception();
  }

  try
  {
    SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
    sa.nLength = Marshal.SizeOf(sa);
    sa.lpSecurityDescriptor = pSDNative;
    sa.bInheritHandle = false;

    RegCreateKeyEx((int)RegistryHive.LocalMachine, @"SOFTWARE\MyCompany\MySecuredKey", 0, null,
      0, KEY_READ | KEY_WRITE, ref sa, ref phkResult, ref lpdwDisposition);
    RegCloseKey(phkResult);
  }
  finally
  {
    LocalFree(pSDNative);
  }

  return 0;
}

private static string GetUserSid(string currentUser)
{
  StringBuilder referencedDomainName = new StringBuilder();
  byte[] Sid = null;
  uint cchReferencedDomainName = (uint)referencedDomainName.Capacity;
  uint cbSid = 0;
  SID_NAME_USE sidUse = SID_NAME_USE.SidTypeUser;
  int err = NO_ERROR;

  if (!LookupAccountName(null, currentUser, Sid, ref cbSid, referencedDomainName,
    ref cchReferencedDomainName, out sidUse))
  {
    err = Marshal.GetLastWin32Error();
    if (err == ERROR_INSUFFICIENT_BUFFER)
    {
      Sid = new byte[cbSid];
      referencedDomainName.EnsureCapacity((int)cchReferencedDomainName);
      err = NO_ERROR;
      if (!LookupAccountName(null, currentUser, Sid, ref cbSid,
        referencedDomainName, ref cchReferencedDomainName, out sidUse))
      {
        throw new System.ComponentModel.Win32Exception();
      }
    }
  }

  IntPtr ptrSid;
  if (!ConvertSidToStringSid(Sid, out ptrSid))
  {
    throw new System.ComponentModel.Win32Exception();
  }
  string sidString = Marshal.PtrToStringAuto(ptrSid);
  LocalFree(ptrSid);
  return sidString;
}


Given the choice of manipulating a string (plenty of built in string/text classes to help you) or creating a nonblittable array of structures that need to be marshalled as an LPArray, I'd choose the strings.

Not so big of an advantage in your case, but you can also set the owner, group, SACL, and inheritance settings all in one command. No need to call InitializeSecurityDescriptor, or SetSecurityDescriptorDacl, or BuildTrusteeWithName or even SetSecurityDescriptorControl.

There is another way to "skin this cat" (not including .NET 2). You can resort to the low level APIs (such as AddAce, AddAccessDeniedAce, AddAllowedObjectAce). However, if you use these APIs, you have to worry about conflicting ACE entries, ordering of ACEs and buffer size issues. You chose SetEntriesInAcl over the low level APIs because it was easier to program... why not make it that bit easier for yourself by using SDDL?

Enjoy the GotDotNet security library.
GeneralRe: Hard isn't it? Pin
Ian_Rintoul4-Aug-09 8:54
Ian_Rintoul4-Aug-09 8:54 

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.