Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using Access Control Lists to secure access to your objects

0.00/5 (No votes)
30 Nov 2004 1  
How to secure your objects

Introduction

We're, most of us, used to the idea of passing NULL as the pointer to a SECURITY_ATTRIBUTES structure in those WIN32 API calls that expect one. We call CreateFile when we're creating a file and pass it a NULL pointer. Passing a NULL pointer causes Windows to give our object a default security descriptor. I could write a bunch of stuff here describing a default security descriptor but Raymond Chen[^] does a much better job than I could.

However, Windows NT and descendant operating systems does provide a pretty good level of security if we're prepared to use it. The Windows NT security model is based on Access Control Lists (ACLs). Each object in the system has a security descriptor associated with it; each security descriptor either has an ACL specifying a list of users/groups and the permissions granted or denied to each, or it has no ACL at all in which case the object gets the default security descriptor. You get to specify who can or cannot access your objects if you choose to. Other accounts on your system get to specify their own access criteria and some of those accounts may choose to deny even the Sytem Administrator.

Details

There are two kinds of ACLs. System ACLs (SACLs) are used to provide an audit trail of who tried to access a given object and Discretionary ACLs (DACLs) are used to specify who can or cannot access a given object. Each ACL in turn consists of a list of ACEs (Access Control Entries). Each ACE specifies a particular SID (Security Identifier, representing a specific user or group on the system), the access allowed for that SID and whether this is a Deny or an Allow ACE. I'm simplifying a bit; there are other kinds of ACEs but my classes presented here don't cover them so I won't either.

A Deny ACE does just that. It denies access to the object for the SID specified in the ACE according to the set of rights passed when the ACE was added to the ACL. Contrariwise, an Allow ACE allows acess to the object for the SID specified according to the set of...  well you get the idea.

The order you add ACEs to an ACL is important. All Deny ACEs should be added to the ACL before you add Allow ACEs. Larry Osterman describes it well in this article[^].

Once you've created your ACL you add it to a Security Descriptor, embed the Security Descriptor in a Security Attributes structure and pass that structure to the appropriate system call that creates the object you want to control and away you go. Notice I said create? If you're accessing an existing object that access is controlled by the Security Descriptor and ACL set by the creator of that object; your access to an existing object is subject to the criteria specified by the creator and there's no API you can call to override that.

If you've read the MSDN documentation on the subject you're probably thinking I've left something out. There's an important difference between having no DACL on an object and having an empty DACL. If you create an ACL but don't add any ACEs to it you get an empty ACL, which causes the system to deny all attempts to access the object. If you don't even create a DACL but pass NULL instead then the object gets no DACL at all, which means that all access requests will be granted.  This ties back to the original statement at the start of the article; we're all used to the idea of passing NULL as the pointer to a SECURITY_ATTRIBUTES structure in those WIN32 API calls that expect one. All the WIN32 API calls that expect a pointer to a SECURITY_ATTRIBUTES structure interpret NULL as meaning a default security descriptor.

Grubby mechanics

At first glance the set of API's to handle ACLs and ACEs appears confusing. One creates, for example, a ACL but one doesn't create ACEs to be added to the ACL. Instead one calls functions which take a pointer to an ACL and some other information, which add an ACE to the ACL. These functions assume that the ACL was created in a memory buffer you allocate but, as the very name implies, it's a list, so you have to be sure you've allocated a large enough buffer to hold the complete ACL including all the ACEs. If you know, ahead of time, exactly how many ACEs you're going to bung into the ACL you can do a compile time calculation and allocate the right amount of memory; otherwise you have to do it the hard way. How often do you reckon you're going to know at compile time how many ACE's you're going to add?  I thought so...  The hard way can be by allocating enough memory for an ACL and reallocating the memory block (with a copy) each time you want to add an ACE.

Or you can take my approach, which is to defer the actual creation of the ACL until you need it. You create an instance of CAccessControlList and add user or group names to it, specifying the  the access allowed or denied for that name and when you're done and you need the ACL it gets created. The class looks like this.

class CAccessControlList
{
    enum eAceType
    {
        eAllow,
        eDeny,
    };
    class CAccessEntry
    {
    public:
                    CAccessEntry(eAceType type, PSID pSid, UINT uiRights);
                    ~CAccessEntry();

        UINT        m_uiRights;
        PSID        m_pSid;
        eAceType    m_type;
    };
public:
                    CAccessControlList();
    virtual         ~CAccessControlList();

    PACL            operator&();
    bool            Allow(LPCTSTR pszName, 
                          UINT uiRights = STANDARD_RIGHTS_ALL | GENERIC_ALL, 
                          LPCTSTR pszServer = NULL);
    bool            Deny(LPCTSTR pszName, 
                         UINT uiRights = STANDARD_RIGHTS_ALL | GENERIC_ALL, 
                         LPCTSTR pszServer = NULL);

private:
    void            AddAces(eAceType type);

    list<CAccessEntry *> m_lAce;
    PACL            m_pAcl;
    UINT            m_uiAclSize;
};
        

There's a UINT member which is initialised, in the constructor, to sizeof(ACL). As we add account names or groups to the class we add the length of the associated ACE and it's SID. When it comes time to create the ACL we know how much memory to allocate.

You add account names or groups using the Allow() and Deny() calls. You can also specify the access to be allowed or denied as the second parameter; it defaults to STANDARD_RIGHTS_ALL | GENERIC_ALL. The Allow() and Deny() functions retrieve a SID for the specified user or group name then create a CAccessEntry instance with the account or group name, rights and type (allow or deny) and add the class instance to the m_lAce list. We can't create the ACE at this point because there are no API's to create an ACE independently of an ACL and, as noted earlier, at this point, we don't know how many ACEs the ACL will contain so we don't know how much memory to allocate for it. The code to add an entry to the list and calculate the size to be added to the overall ACL size looks like this.

bool CAccessControlList::Allow(LPCTSTR pszName, UINT uiRights, 
                               LPCTSTR pszServer)
{
    assert(pszName);

    PSID pSid = GetSid(pszName, pszServer);

    if (pSid != PSID(NULL) && IsValidSid(pSid))
    {
        m_uiAclSize += sizeof(ACCESS_ALLOWED_ACE) + 
                       GetLengthSid(pSid) - sizeof(DWORD);
        m_lAce.push_back(new CAccessEntry(eAllow, pSid, uiRights));
        return true;
    }

    return false;
}
        

The Deny() function is almost identical. The only really interesting thing in the Allow()/Deny() functions is the call to GetSid() which encapsulates the process of querying the needed buffer size for a SID, allocating the memory and querying for the SID itself. As with quite a few Windows APIs you call the same API twice, once with zero for the buffer length, which will return the needed size, and a second time to do the real work.

So what's a SID?

A SID is a security identifier; it represents a user or a group on the system. If we were dealing only with the local machine the username or groupname would be enough. But you'll have noticed above that the Allow() and Deny() functions take a third parameter, the server name, defaulted to NULL. NULL, of course, means look up this user or group name on the local machine. But it's the very fact that a user or group account may be defined elsewhere than the local machine that makes a SID useful; the SID contains enough information to tell the local security authority that it can trust local system account information if the SID refers to the local machine; else the local security authority must go to the domain controller for final adjudication.  Either way the SID is a blob of data that uniquely identifies an account.

Having a SID doesn't give any special rights. You can fetch a SID that represents the Domain System Administrator and all you have is a SID that represents the Domain System Administrator! You can't use a SID to elevate your privileges; all you can do with it is set the access to your object for that particular SID.

The GetSid() function looks like this:

PSID GetSid(LPCTSTR pszName, LPCTSTR pszServer)
{
    PSID         pSid = PSID(NULL);
    TCHAR        ptszDomainName[256];
    DWORD        dwSIDSize = 0,
                 dwDomainNameSize = sizeof(ptszDomainName);
    SID_NAME_USE snuType = SidTypeUnknown;

    LookupAccountName(pszServer, pszName, NULL, &dwSIDSize, NULL, 
                      &dwDomainNameSize, &snuType);

    if (dwSIDSize)
    {
        //  Success, now allocate our buffers

        pSid = (PSID) new BYTE[dwSIDSize];

        if (!LookupAccountName(NULL, pszName, pSid, &dwSIDSize, 
                               ptszDomainName, &dwDomainNameSize,
                               &snuType))
        {
            // failed, delete the SID buffer

            delete pSid;
            pSid = PSID(NULL);
        }
    }

    return pSid;
}
        
This is a global function rather than a class member for two reasons. The first is that it really doesn't belong in a class; it touches no instance specific data belonging to a class;  The second reason is that I'll be using it in a second class presented a little later.

There's a gotcha or two in there.  The MSDN documentation states that if you call the LookupAccountName() API passing it NULL as the pointer to the SID structure and 0 as the size of that structure the function returns the required size. Which it does indeed, but the way it's worded implies that the function return value is the required size. It's not returned that way. The function returns 0 but sets the DWORD pointed at by dwSIDSize to the required size. This is why I'm not checking the function return value.

You get access to the ACL (and create it) by dereferencing the object. The operator& overload deletes the m_pAcl member and recreates it. The function looks like this:

PACL CAccessControlList::operator&()
{
    delete m_pAcl;
    m_pAcl = (PACL) new BYTE[m_uiAclSize];

    //  Change the ACL_REVISION_DS constant to ACL_REVISION if you want to 

    //  support Windows NT 4.0 or earlier

    InitializeAcl(m_pAcl, m_uiAclSize, ACL_REVISION_DS);
    AddAces(eDeny);
    AddAces(eAllow);
    return m_pAcl;
}
        

Nothing terribly special there except that we do have to iterate our list of accounts twice. The first pass adds the Deny ACEs and the second pass adds the Allow ACEs. Do note that a call to the overloaded & operator on the class is somewhat expensive; the ACL is deleted and recreated each time the operator is called; if you know that the list of allowed and denied users hasn't changed it's probably a good idea to cache the ACL pointer.

Now that I have my ACL what do I do with it?

This is where the Security Attributes we've all seen in relation to such calls as CreateFile() enters the picture. You can create a SECURITY_ATTRIBUTES structure in memory, initialise it, then create a SECURITY_DESCRIPTOR structure, store your ACL in the SECURITY_DESCRIPTOR structure and... well, if you want to pass more than a NULL as that parameter it really makes sense to wrap it all up in a class.

CAccessAttributes

is a class that can be used, at a minimum, to replace that ubiquitous NULL pointer. If you do indeed want to pass a NULL pointer to a SECURITY_ATTRIBUTES structure then just go ahead and use the NULL; but if you want to do something more use a CAccessAtributes instance which will give you a little more control.

The class looks like this:

class CAccessAttributes
{
public:
                    CAccessAttributes(PACL pDacl = NULL);
    virtual         ~CAccessAttributes();

    LPSECURITY_ATTRIBUTES operator&()   { return &m_sa; }
    void            SetDACL(PACL pAcl);
    void            SetSACL(PACL pAcl);
    bool            SetOwner(LPCTSTR pszOwner = NULL, 
                             LPCTSTR pszServer = NULL);
    bool            SetGroup(LPCTSTR pszGroup, LPCTSTR pszServer = NULL);

private:
    SECURITY_ATTRIBUTES m_sa;
    SECURITY_DESCRIPTOR m_sd;
    PSID            m_pOwnerSid,
                    m_pGroupSid;
};
        

The class encapsulates a SECURITY_ATTRIBUTES structure which is initialised to contain an empty DACL. The operator&() member returns the address of the SECURITY_ATTRIBUTES structure. If you do nothing more than instantiate an instance of the CAccessAttributes class and use operator&() to pass it to a WIN32 API then you've just made your object totally inaccessible to everyone on the system. This is because the ACL created in the operator&() call is initialised via the InitializeAcl() API and set to have an empty ACL. If you haven't called Allow() at least once on the object there will be no Allow ACEs and therefore no access will be granted to anyone on the system.  This is rather different from the situation where you pass a NULL as the SECURITY_ATTRIBUTES structure, in which case you get a default security descriptor. Alternately, you can create an instance of CAccessControlList or obtain an ACL from some other source, call SetDACL() or SetSACL() passing the ACL and now you have a SECURITY_ATTRIBUTES structure which sets permissions on any object created using the class instance.

The constructor looks like this:

CAccessAttributes::CAccessAttributes(PACL pAcl)
{
    //  Initialise our security attributes to give access to anyone.

    memset(&m_sa, 0, sizeof(m_sa));
    m_sa.nLength = sizeof(m_sa);
    InitializeSecurityDescriptor(&m_sd, SECURITY_DESCRIPTOR_REVISION);
    m_pGroupSid = m_pOwnerSid = PSID(NULL);
    SetOwner();
    SetDACL(pAcl);
}
        

Nothing special there except for the call to SetOwner(). All that function does is retrieve a SID representing the account under which the current thread is running and sets that as the owner of the SECURITY_DESCRIPTOR. That's true but you'll have noticed that the SetOwner() function defaults the owner to NULL; if it's NULL we fetch the current username and set that as the owner. You can override the constructor by calling SetOwner() with any user/group name after the object has been instantiated.

Using the code

It's very difficult to construct a meaningful sample project to illustrate the use of the classes. I have such projects but they're not the kind of project I want to release into the public domain. I also cannot hope to create a project that will work unambiguously on every system out there in the wild short of stipulating that you must create user such and such and group so and so and make user X a member of group Y. So you're stuck with trusting me. Nonetheless, here's one way of using the classes. Just accept the arbitrary user and group names.
//    Create our DACL

CAccessControlList acl;

acl.Allow(_T("EveryOne"));
acl.Deny(_T("stan"));

// Now create a SECURITY ATTRIBUTES object and

// set the DACL to the ACL we just created.

CAccessAttributes aa(&acl);

::CreateFile(filename, desiredAccess, shareMode, &aa, ...);
    
this creates an ACL allowing acess to "EveryOne" (a group representing everyone who can connect to the system) but denying access to "stan". We then create a CAccessAttributes object passing the ACL which will be set as the DACL for the object. Then we create a file with various access modes and share modes but passing it our SECURITY_ATTRIBUTES object. Assuming the file creation succeeded the file is now accessible by "EveryOne" except "stan" who will be denied access.
  • Initial version - November 30 2004.
  • December 1 2004 - Updated to correct some erroneous statements (I was wrong!). Thanks to Nemanja Trifunovic and the poster calling him/her/self Anonymous and Cowardly :-)

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