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
struct
s where the access is defined for each SID. These structures will equate to the ACEs in the final ACL. The order of the struct
s 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
#include "stdafx.h"
#include "SecureRegistry.h"
namespace SecureRegistry
{
long KeyManager::CreateRestrictedRegKey(String* account)
{
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;
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;
}
}
Marshal::FreeHGlobal(System::IntPtr((void*)accountName));
if(!IsValidSid(pUserSID))
{
System::Console::WriteLine("Invalid UserSID");
goto Cleanup;
}
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;
}
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;
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;
dwRes = SetEntriesInAcl(2, ea, NULL, &pACL);
if(dwRes != ERROR_SUCCESS)
{
System::Console::WriteLine("Unable to set entries"
" in ACL - {0}", GetLastError().ToString());
goto Cleanup;
}
pSD = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR,
SECURITY_DESCRIPTOR_MIN_LENGTH);
if(!InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION))
{
System::Console::WriteLine("Unable to initialize"
" security descriptor - {0}", GetLastError().ToString());
goto Cleanup;
}
if(!SetSecurityDescriptorDacl(pSD, TRUE, pACL, FALSE))
{
System::Console::WriteLine("Unable to set"
" security descriptor DACL - {0}", GetLastError().ToString());
goto Cleanup;
}
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = pSD;
sa.bInheritHandle = FALSE;
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)
{
lRes = RegCreateKeyEx(companyKey,"MySecuredKey",0,"",0,
KEY_READ | KEY_WRITE, &sa, &securedKey, &dwDisposition);
}
Cleanup:
{
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
#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
#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.