In my previous column, I explained the various structures that low-level security API calls use to work with reading and setting security attributes. As I discussed last time, several security issues have cropped up from installation routines that don’t set permissions. In this column, I’ll present an application you can use to initialize the discretionary access control list (DACL) on a registry key to give Administrators full control. You can fairly easily extend this application to set different permissions or set file system permissions. I hope that you’ll be able to use this code to make your applications more secure.
Listing 1 presents the application code (I suggest that you view the code in a separate window and follow along while I explain how the application works). First, note that I’m declaring my entry point as wmain, not main. Also, instead of accepting an array of ANSI strings, the entry point for the application takes an array of WCHAR strings. Because Windows 2000 and Windows NT use UNICODE at the lowest levels, it is more efficient to use the UNICODE versions of function calls. If you've ever had to convert user-supplied input from single-byte to UNICODE, you know that it can be troublesome and error-prone; however, if you use wmain, the OS will provide the input as UNICODE for you.
For simplicity, this application assumes that the registry key exists under HKEY_LOCAL_MACHINE. If you need to set permissions on keys contained under other registry hives, you'll have to expand the argument list and parse the input. The first thing the application does (aside from checking arguments) is confirm that the registry key exists and that you have the permissions you need to set the DACL. Alternately, you can have two code paths and call RegCreateKeyEx() where you’d create the key with the permissions you want in one atomic operation.
Now that you've opened the registry key with the proper permissions, you need to start building the structures for your security descriptor. Let’s start with the single access control entry (ACE) that you’re going to apply. Remember that the length of an ACE varies because it contains a SID structure. As a result, you need to calculate the appropriate size. The details of this calculation are in the comments starting at line 91 in Listing 1. When allocating memory for a structure—especially if I might not need to initialize all the members explicitly—I prefer to call GlobalAlloc() with the GPTR flag, which initializes the memory to zero and saves time that you would otherwise spend setting reserved members. In addition, having your memory initialized to zero can help you find bugs in your code and make sure that behavior is consistent across debug and release builds. Uninitialized memory accounts for a large proportion of the bugs that show up in release builds only. Also, note the use of goto. One of the few legitimate uses of goto is to ensure that a function correctly performs any necessary clean up in the same place and gets done right. If an error occurs in the application, the goto function needs to exit in several places, so it is best practice to have one clean up and exit point. If you’re afraid people will snicker at you for using goto, __try, __finally, and __leave accomplish the same thing and provide some real benefits over goto under some circumstances (and inspire awe in your code reviewers). I’ll address use of structured exception handling in a future article.
Next, you need to set the members: The ACE size is the size of the ACE that you just calculated, the ACE type is ACCESS_ALLOWED_ACE_TYPE, and the ACE flags are set so that any new keys that you create under this registry key will inherit the permissions you’re setting. Last, set the access mask to KEY_ALL_ACCESS.
After you set the members, you need to initialize the SID using the LookupAdminSid() function that starts at line 15. The SidStart member of an ACE is unusual, so let’s take a closer look. To declare a variable-length structure, you typically use an array with one element as the last member of the structure, and the structure will change size based on the number of elements in the array. The beginning of a SID isn’t an array, so the typical method doesn’t make sense. The original designers of NT decided to put a 4-byte placeholder at the end of the ACE. As a result, SidStart isn’t a pointer (or a DWORD), but you will still want to cast the address of SidStart to either a PSID or a SID* when you work with it.
Now let’s examine how LookupAdminSid() works. After first checking the arguments associated with LookupAdminSid(), we're now ready to create the SID. If the SID is for a well-known user or group, you can create it by hand, which is the safest method. Creating the SID by hand also means that your code is language-independent (never assume that the administrator account is named "Administrator"—it might be renamed, or your user could be running a different language version of the OS). If the SID is for a unique user, you must use LookupAccountName() to look up the user. When you create a SID by hand, you have to know what to expect. For example, a well-known user or group has two Relative Identifiers (RIDs), except for Everyone and Anonymous, both of which have only one RID. Calling InitializeSid() is the easiest approach, so we’ll make the SID Identifier Authority equal to SECURITY_NT_AUTHORITY and specify that this SID has two RIDs. I suggest that you make yourself a small command-line application to wrap LookupAccountName() and dump the SID in detail so that you can see for yourself what the SID needs to look like. Last, you can cast the PSID pointer to a SID* pointer and set the RIDs directly, but the more proper approach is to use GetSidSubAuthority() to initialize your RIDs.
Now that you have an ACE built, you need to add it to a DACL. Start by allocating memory for your DACL. Next, initialize the DACL with the size and revision. Finally, call AddAce() to add the ACE you built to the DACL. If you create a complex DACL, the ordering of the ACEs can be important, so a good approach is to use the Explorer interface to set the DACL and then dump the DACL in detail. This way, you can make sure your code builds the DACL in exactly the same order as the OS would. The reason I use AddAce() instead of AddAccessAllowedAce() is that AddAccessAllowedAce() won’t let you set the flags in the ACE_HEADER. AddAccessAllowedAceEx() does let you set the flags, but it only works on Windows 2000. AddAce is a little extra work, but it provides complete control and works on every version of the OS.
The next step is to declare a SECURITY_DESCRIPTOR and initialize it. You then set the security descriptor to know about the DACL you created and call RegSetKeySecurity(). If you’d like to set auditing permissions, you go through a very similar process to create a system access control list (SACL), but you also have to enable the SE_SECURITY_NAME privilege for your process using the AdjustTokenPrivileges() function.
Because this process isn’t easy, I strongly suggest creating a set of C++ classes or library functions to make sure that everything is done correctly and in one place. Many of the cautions in this article have come from several classes at the School of Hard Knocks, so I hope this code will save you some aggravation and help you to write applications that install securely.