CSDN博客

img waterboy

Defend Your Apps and Critical User Info with Defensive Coding Techniques

发表于2004/11/3 8:46:00  2940人阅读

分类: WINFORM

n today's connected world in which every application is a potential target, you must extend your defensive programming efforts to cover security. Everything you have learned about defensive programming helps you write more secure code, but this is not enough. You need to go much further to build explicit defenses into your software.

In this article, I'll focus on protecting users, securing their credentials and private information, and defending servers. I will cover a wide range of common programming scenarios and explore practical ways in which you can write code that is more resistant to attack.


Protecting Credentials

Let's start with how you can protect your users' credentials. For best credentials security, you should avoid managing user credentials explicitly because passwords need to be kept secret and secrets are hard to secure. Therefore, you should rely on logon sessions and single sign-on. If you need to run an application as a different user, use the shell's Run As option or type runas at the command prompt. However, when your back is to the wall and you must manage credentials yourself, here's what you should do.

First, you will need to install the Platform SDK from February 2003 or later to use the functions described here. Once installed, add the Platform SDK directories named Include and Lib to your Visual C++® search paths. If you prefer to use P/Invoke from C# or Visual Basic® .NET, you do not need to install the Platform SDK.

The next step is to prompt the user for credentials. To provide a consistent user experience and write less security-related code, you should use the CredUIPromptForCredentials function for GUI applications, and CredUICmdLinePromptForCredentials for console applications. Both provide essentially the same functionality, with the GUI version providing a few more options specific to a graphical interface such as allowing you to change the default banner bitmap, the caption, and the dialog box message. These functions can be used to prompt the user for Windows® domain credentials or generic credentials, as illustrated in Figure 1.

Credentials are stored in the user profile using the target name as the key, so be sure to use meaningful and unique target names. The CREDUI_INFO structure allows you to specify the handle to the parent window for the modal dialog box. It also lets you override the default caption, message text, and banner bitmap that will be displayed to the user.

Then there is the matter of integer overflow. Did you notice the two static_cast operations? These are used to convert the result of the std::vector<wchar_t>::size method to ULONG, which is what the CredUIPromptForCredentials function expects. Although ULONG will always be a 32-bit unsigned integer, size_t is big enough to span the full range of a pointer. In other words, size_t is a 32-bit unsigned integer on 32-bit Windows and a 64-bit unsigned integer on 64-bit Windows. The static_cast is used to suppress the compiler warning informing you that the cast may not be safe on 64-bit versions of Windows.

By suppressing the compiler warning, you are taking full responsibility to ensure that the cast will always be safe, even if run on 64-bit platforms. In the previous example, you can see that the size of the vector is less than std::numeric_limits<ULONG>::max(), so it is safe in this case. Needless to say there is a lot of room for error and that's why I wrote the PromptForCredentials class to make it easier and safer to manage credentials. The class is available in the code download for this article. The following example shows the PromptForCredentials class in use:

PromptForCredentials prompt;
prompt.Target(L"server");
prompt.ParentWindow(0); // set to the parent window
prompt.Flags(CREDUI_FLAGS_GENERIC_CREDENTIALS);

if (prompt.ShowDialog()) {
    // TODO: use prompt.UserName() and prompt.Password()
    prompt.ScrubPassword();
}

As you can see, the code is simple and takes care of error handling. The PromptForCredentials destructor will automatically call the ScrubPassword method, but it is a good idea to zero out any secrets in memory as soon as possible. Internally, the ScrubMemory method calls SecureZeroMemory (which is just a substitute for the RtlSecureZeroMemory function) in order to zero out the password buffer.

Figure 2 Prompt for Credentials Sample
Figure 2 Prompt for Credentials Sample

To allow you to experiment more easily with the wide range of options and flags provided by the CredUIPromptForCredentials function, I wrote the Prompt for Credentials sample application displayed in Figure 2. It allows you to combine the different options and flags to find the functionality you need.


Protecting Data on the Client

Protecting sensitive data involves encrypting the data while it is held in memory and before it's written to disk. If the data is persisted, it must also be protected by a strong access control list (ACL). This section will show you how to perform these tasks correctly.

With good cryptography libraries such as the CryptoAPI, CAPICOM, and System.Security.Cryptography freely available, writing encryption code is pretty easy, but managing encryption keys and passwords remains a challenge. Windows XP and Windows Server 2003 include data protection functions that allow you to encrypt and decrypt data without having to manage encryption keys. You can use the CryptProtectData function to encrypt data that can then only be decrypted using the CryptUnprotectData function when called from a logon session based on the same user account. Figure 3 illustrates an example.

CheckError is a helper function that checks the return value of a function. If the return value indicates failure, an appropriate exception is thrown. CheckError, along with many other useful functions and classes, can be found in the download for this article. As you can see, there is no need to manage an encryption key. First, you need to get the secret from the user—for example, use the GetWindowTextLength and GetWindowText functions to read some text from a control into the buffer. Next, the plaintext DATA_BLOB structure is populated to describe the location and size of the data to the CryptProtectData function. Internally, CryptProtectData allocates enough memory to hold the encrypted data using the LocalAlloc function, and returns both a pointer to this memory and the size in the ciphertext DATA_BLOB structure. At this point, you can write the secret to disk if necessary and free the memory using the LocalFree function (you should also remember to scrub the plaintext secret as soon as possible using the SecureZeroMemory function). Decryption is performed using the CryptUnprotectData function, which is virtually identical to CryptProtectData. The first parameter points to a DATA_BLOB structure referring to the ciphertext, and the last parameter points to a DATA_BLOB structure for which the function will allocate memory using LocalFree to hold the plaintext secret.

Managing DATA_BLOB structures is a cumbersome task that can result in errors if you forget to free and scrub the memory where it is appropriate to do so. It is also difficult to use in the face of exceptions. The DataBlob class shown in Figure 4 can be used to simplify these issues.

The DataBlob destructor will automatically scrub and free the memory where appropriate. This works great for storing data securely in the file system, but if all you need to do is protect some data in memory while the application is running, this approach can consume too much memory. You should encrypt memory in your process's address space because it is possible for the memory to be paged out to the swap file or to the hibernation file. If an attacker can compromise some other aspect of your computer's security, such as gaining physical access to it, he may be able to read your secret data.

Windows Server 2003 introduced the CryptProtectMemory and CryptUnprotectMemory functions for precisely this reason. On Windows 2000 and Windows XP you can use the RtlEncryptMemory and RtlDecryptMemory functions for the same purpose, but use caution as they may not be available on newer platforms. CryptProtectMemory and CryptUnprotectMemory are designed to efficiently encrypt a block of memory in-place. This is useful if you want to hold onto some sensitive information, such as a set of credentials, for an extended period or use them at different times. What is interesting about these functions is that the data to be encrypted must be a multiple of given block size. Encryption algorithms generally encrypt streams of data one block at a time. Data that is shorter than the block size must be padded to fill an entire block. Therefore, if you want to encrypt arbitrary data, you need to ensure that the buffer it is stored in is a multiple of the block size. The following example shows a simple function to calculate the block size for a buffer:

DWORD ToBlockSize(DWORD original) {
    DWORD result = CRYPTPROTECTMEMORY_BLOCK_SIZE;
    if (0 != original) {
        DWORD remainder = original % CRYPTPROTECTMEMORY_BLOCK_SIZE;
        result = original + (0 != remainder ? 
            CRYPTPROTECTMEMORY_BLOCK_SIZE - remainder : 0);
    }
    return result;
}

Given this function, it becomes easy to use the CryptProtectMemory function. The following code shows how to read text from a control in an MFC application and encrypt it:

CWnd* pControl = GetDlgItem(IDC_SECRET);
DWORD charCount = pControl->GetWindowTextLength() + 1;

std::vector<BYTE> buffer(ToBlockSize(charCount * sizeof (WCHAR)));
pControl->GetWindowText(reinterpret_cast<PWSTR>(&buffer[0]), charCount);

if (!::CryptProtectMemory(&buffer[0], static_cast<DWORD>(buffer.size()),
           CRYPTPROTECTMEMORY_SAME_PROCESS)) {
    AtlThrowLastWin32();
}

Decrypting the data is a simple matter of calling CryptUnprotectMemory with the exact same set of parameters. The buffer will again contain the plaintext data.

So far, I have talked about encrypting data for long- and short-term storage. What I have yet to discuss is how to protect the data when it is stored in the file system or the registry. The user profile, which includes the user's My Documents folder, provides a secure location in which applications can store user-specific application data as well as documents. The user profile includes a portion of the registry for the current user as well as a branch of the file system, both of which are secured by ACLs that effectively limit access permissions to user information to the specific user and administrators. The two most useful folder locations in the user profile are the application data folder and the personal folder, also known as My Documents. The application data folder is for application data files that are specific to each user but that the user does not normally have to interact with. For example, you might want to store user preferences in a configuration file in this location. The personal folder is where the application should default to when saving documents created for the user.

To use these folders from C++, employ the SHGetFolderPathAndSubDir function. This function, introduced in Windows XP, does the work of calling SHGetFolderPath followed by PathAppend in a single function. It can also create the folder if it does not exist. Here is an example:

std::vector<wchar_t> path(MAX_PATH);
CheckError(::SHGetFolderPathAndSubDir(0 /*no parent window*/,
    CSIDL_FLAG_CREATE | CSIDL_APPDATA, 0 /*no token*/,
    SHGFP_TYPE_CURRENT, L"Kerr//SecuritySample", &path[0]));
::PathAppend(&path[0], L"settings.xml");
CHandle file(::CreateFile(&path[0], ...));

Keep in mind that many of these Windows Shell functions do not take a buffer size argument so you need to take special care not to introduce a buffer overrun error. To create the file in the user's personal folder, simply replace the CSIDL_APPDATA flag with CSIDL_PERSONAL. The equivalent C# code is slightly less concise, but it's simpler and less error prone:

string path = 
  Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
path = Path.Combine(path, @"Kerr/SecuritySample");
Directory.CreateDirectory(path);
path = Path.Combine(path, "settings.xml");
FileStream file = new FileStream(path, ...);


Protecting Named Pipe Clients

Named pipes are one of the simplest and most efficient forms of interprocess communication (IPC) on the Windows platform. A named pipe provides a simple one-way or duplex pipe that a client and server can use to communicate messages or byte streams. It also provides an abstraction over sockets and the Security Service Provider Interface (SSPI) to make network communications programming easier. There are, however, some nuances in how named pipes deal with authentication that can be troublesome.

In this section, I will explore named pipes from the perspective of the named pipe client. Even if you do not use named pipes directly, you may still be relying on them for some other API that you are using. For example, the Service Control Manager (SCM) functions, used to manage local and remote Windows services, use named pipes for communication. Access to a remote registry when using the RegConnectRegistry function also relies on named pipes. Having a good understanding of named pipe security will help you understand how many common Windows services work, if not your own applications.

A client connects to a named pipe using the CreateFile function. Internally, it delegates the management of the communication with the named pipe server to the Workstation service. Consider the following example:

CHandle handle(::CreateFile(L"////server//pipe//Kerr.SecuritySample",
    FILE_READ_DATA | FILE_WRITE_DATA, 0 /*no sharing*/,
    0 /*default security attributes*/, OPEN_EXISTING,
    SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, 0 /*no template*/));

By requesting read and write access to the pipe, the client is assuming that the server created a duplex, or bi-directional, pipe. If this is not the case, CreateFile will fail and the GetLastError function will return ERROR_ACCESS_DENIED.

The other interesting thing to note is the Security Quality of Service (SQOS) information. When communicating with named pipes, you will typically want to combine the SECURITY_SQOS_PRESENT flag with either SECURITY_IDENTIFICATION or SECURITY_IMPERSONATION. SECURITY_IMPERSONATION is the default if you do not provide any information for SQOS. As a developer of a client application, one of your main goals should be to protect the identity of the client. This means limiting your trust in the servers to which you connect.

The SECURITY_IMPERSONATION flag indicates that the server will be able to access local resources on the client's behalf. For example, the server may want to impersonate the client and call the CreateFile function to allow the file system to perform an access check against the client's identity. If you do not want to let the server masquerade as the client, you can specify the SECURITY_IDENTIFICATION flag instead. This flag indicates that the server will be able to impersonate the client for the purposes of getting authorization information about the client, but will not be able to impersonate the client when accessing other resources.

The way the client is authenticated is also important. The CreateFile function has no provision for accepting credentials, so there must be some other way to authenticate with the named pipe server, considering it is common to require alternate credentials. This is easier to understand if you consider that named pipes are really hosted by the Server service, which, coincidentally, also manages file and print sharing. The Workstation service mentioned earlier is then simply the service that manages sessions to various Server services on different computers. This infrastructure is loosely referred to as the Windows file server.

What makes using the file server a little tricky is that for a given logon session there can be only one client session connected to any single server. If you attempt to establish a second connection with a different set of credentials, it will fail. So managing sessions becomes an important part of writing a named pipe client app.

Sessions are usually created automatically on your behalf when you use Explorer to connect to a share on a remote computer, call CreateFile to open a file residing on a different computer, or open the client end of a named pipe. These sessions are generally short lived and are closed automatically by the Workstation service some time after the client's file handle is closed. They use the identity of the logon session that initiated the connection. Sometimes, however, there is a need to use a different set of credentials.

A set of credentials can be associated with a remote machine to be used in place of any implicit sessions that may or may not be creatable. This is commonly referred to as a use record, which is helpful in creating a session to a file server with an explicit set of credentials, where the user's logon session either does not have network credentials or does not have permission to access the remote server.

To create a use record, call the NetUseAdd function. Figure 5 shows an example of using the PromptForCredentials class introduced earlier. When the use record is no longer needed, call the NetUseDel function to remove it:

CheckError(::NetUseDel(0 /*reserved*/, L"////server//IPC$", USE_FORCE));


Protecting COM Clients

Detailed coverage of COM architecture and security would fill a book or two, so for this section I will focus on what you should understand about COM security to better protect your COM client applications. COM is still a big part of a developer's life, being at the heart of many Windows services. Some are hidden behind managed wrappers and exposed through elegant interfaces in the .NET Framework, but COM is still there. The point is that COM is going to be around for a long time.

To simplify the terminology, I will refer to code that makes use of COM objects as COM clients, and I'll refer to COM objects as COM servers, regardless of how they are activated. This is consistent with the COM specification.

The first interesting function called by a typical COM client, after selecting the apartment model, is CoCreateInstance—as shown in the following code snippet:

CheckError(::CoCreateInstance(__uuidof(CoLibraryObject),
             0 /*no outer unknown*/, CLSCTX_INPROC_SERVER,
             __uuidof(ILibraryObject),
             reinterpret_cast<PVOID*>(&spLibraryObject)));

This COM server will be created in the client process, indicated by the CLSCTX_INPROC_SERVER flag. The CLSCTX, or class context value, does not determine where the object will be activated (that is controlled by the server). Rather, this value is used to restrict where the object can be created for a given instantiation. If the class context does not match the packaging defined by the server, the call to CoCreateInstance will fail. Because the server in this example is running within the client's logon session, it can do almost anything on the client's behalf. Therefore, for this discussion, I will focus on out-of-process activation, shown here:

CheckError(::CoCreateInstance(..., 
             CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER, ...));

CLSCTX_LOCAL_SERVER indicates that the COM client directs the COM server to run out of process. You can also specify CLSCTX_ALL, which effectively indicates that the COM client does not care where the COM server will run. This is the default for the CComPtr<T>::CoCreateInstance method. In order to avoid any surprises you should always be explicit about where you expect the COM server to run.

In the previous example, the COM client provided the client's process identity to the COM server for the purposes of authentication and authorization. In this case, the COM server will get a token for the client's logon session. If the COM server were activated on a remote computer, a network logon session would have been created to represent the client.

If the client is impersonating—implying that there is a thread token—and you want to provide the client's thread identity to the COM server, you must enable dynamic cloaking. You can enable it for the entire process using the CoInitializeSecurity function, but this is often undesirable as it affects all the COM clients in the process. A better option is to enable dynamic cloaking for a given COM interface proxy. You can do this with the CoSetProxyBlanket function:

CheckError(::CoSetProxyBlanket(spServerObject, RPC_C_AUTHN_DEFAULT,
               RPC_C_AUTHZ_NONE, COLE_DEFAULT_PRINCIPAL,
               RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_DEFAULT,
               COLE_DEFAULT_AUTHINFO, EOAC_DYNAMIC_CLOAKING));

Most of the parameters just tell COM to pick an appropriate default. The parameters of interest are the first parameter, which specifies the proxy to configure, and the last parameter, which specifies the EOAC_DYNAMIC_CLOAKING capability. For the lifetime of the proxy, before every subsequent method call, the proxy will determine the effective token and use it when connecting to the COM server. This might sound inefficient at first, but it isn't. Internally the proxy's channel object holds onto a remote procedure call (RPC) binding handle, since distributed COM uses RPC under the covers. The binding handle manages the authentication information used to connect to a particular server. As long as the effective token does not change, the same binding handle will be reused on subsequent method calls.

Another option is to use a different set of credentials for connecting to a remote computer. This may be useful if the client cannot establish a network logon session on that computer, for example if the computer is not part of a trusted domain. Explicit credentials are provided with a COAUTHIDENTITY structure. This structure can be used during the initial activation request to connect to the remote computer using the CoCreateInstanceEx function. It can also be used when setting the authentication information for a proxy using the CoSetProxyBlanket function. You can provide credentials independently for either or both. If you use activation credentials, it will have no effect on proxies. The proxies will still use the process token under normal conditions or the effective token if you enable dynamic cloaking. Looking at this a different way, regardless of whether you use different credentials for activation and for the proxy, two distinct logon sessions will be created on your behalf.

To protect myself from common buffer-related errors, I wrote the CoAuthIdentity class shown in Figure 6 for populating COAUTHIDENTITY structures. This class takes care of all the error handling when dealing with string buffers and especially passwords. Calling CoCreateInstanceEx is now relatively simple, as shown in Figure 7.

Keep in mind that you should never hard-code credentials, and especially passwords, in your code as I've done here. I use literal string passwords in the samples for this article only because it makes the samples simpler and more to the point.

A COSERVERINFO structure is passed to CoCreateInstanceEx, providing the name of the remote computer as well as the authentication information in the form of a COAUTHINFO structure. The COAUTHINFO structure in the example is configured to use Windows NT LAN Manager (NTLM) authentication. This is a safe bet, as you should expect NTLM to always be available. One reason that you may not want to use NTLM, however, is that it does not verify the identity of the server, which I will discuss later.

Using RPC_C_IMP_LEVEL_IMPERSONATE as the impersonation level is necessary because CoCreateInstanceEx defers to the local COM SCM to communicate with the remote SCM for the purposes of remote activation. You need to provide the SCM with this level of trust so that it can establish a remote network logon session on your behalf.

As I mentioned, the credentials used for the activation request do not affect the proxy that will be returned by CoCreateInstanceEx. To have the proxy's underlying binding handle authenticate with the same credentials, simply pass the COAUTHIDENTITY structure or CoAuthIdentity object to the CoSetProxyBlanket method, as shown in the following:

CheckError(::CoSetProxyBlanket(..., &authIdentity, EOAC_DEFAULT));

Keep in mind that the proxy may refer to the COAUTHIDENTITY structure at any time until it is released, so you need to ensure that the memory does not change or become freed before all proxies are released.

So far, I've focused on how COM clients manage authentication with COM servers. The next important aspect of COM security is the level of trust you place in the server. By connecting to the COM server, the COM client allows the server to impersonate the client. What the server is allowed to do with the resulting thread token is limited by what the client allows. Although this can be specified at the machine or process level, I recommend always specifying your preferences for a given proxy. Again, let's turn to the CoSetProxyBlanket function, as shown here:

CheckError(::CoSetProxyBlanket(..., RPC_C_IMP_LEVEL_IMPERSONATE, ...));

The relevant options for the dwImpLevel parameter are RPC_C_IMP_LEVEL_IDENTIFY, RPC_C_IMP_LEVEL_IMPERSONATE, and RPC_C_IMP_LEVEL_DELEGATE.

Let's first tackle the RPC_C_IMP_LEVEL_DELEGATE option. Delegation refers to the ability of a server to take a client's network credentials and use them to establish another network logon session on a different machine. Clearly, this will only work if the network logon session actually caches the client's credentials. NTLM does not support delegation, but Kerberos does. Interestingly, if a client uses the RPC_C_IMP_LEVEL_DELEGATE flag to access a local COM server, delegation will be supported despite the fact that NTLM is used for local authentication. This is possible because when the COM client connects to a local COM server, a network logon session is not created (as is the case for remote COM servers), but rather the client's logon session is used. Because the client's logon session will typically have network credentials, delegation effectively works. Considering the power that a server can wield with a client's delegated credentials, you need to carefully consider its use.

The RPC_C_IMP_LEVEL_IDENTIFY flag has the same effect as the SECURITY_IDENTIFICATION flag used by named pipe clients. The server will be able to query the client's token for information that can be used to identify the client and make authorization decisions. The server will not, however, be able to impersonate the client in order to access other secure resources such as the file system.

RPC_C_IMP_LEVEL_IMPERSONATE has the same effect as the SECURITY_IMPERSONATION flag used by named pipe clients and is a superset of the RPC_C_IMP_LEVEL_ IDENTIFY flag. In addition to being able to query the client's token, the server can impersonate the client while accessing local resources.

Once the client has a proxy and has configured suitable authentication, the client can finally get to work and call the methods on the COM object. The security service provider (SSP) can also provide protection for the data communicated between the client and the server. Again, using only the defaults will not help you get a good night's sleep.

The authentication level controls data protection on the wire and optionally provides two services: integrity and privacy. Although there are a number of authentication levels, the only two that are useful for the defensive security programmer are RPC_C_AUTHN_LEVEL_PKT_INTEGRITY and RPC_C_AUTHN_LEVEL_PKT_PRIVACY. The latter provides complete protection that covers tamper detection and encryption of all data, including protocol headers. RPC_C_AUTHN_LEVEL_PKT_INTEGRITY provides the same degree of tamper detection by signing the data with the session key, but the data is visible to eavesdroppers. You should favor packet privacy, unless the performance overhead of sealing the entire communication is prohibitive. You may find that it does not have a noticeable effect on performance, and you will make it that much harder for hackers to attack your software.

In the discussion about COM authentication, I hinted that NTLM does not authenticate the server. Windows 2000 introduced the Kerberos network authentication protocol, and compared to NTLM, Kerberos is quite modern and sophisticated. One of its many distinguishing characteristics is that Kerberos supports mutual authentication and can ensure server identity. To use Kerberos, not only do you need to use Windows 2000 or later, but the computers involved need to be part of a Kerberos domain, sometimes referred to as a Windows 2000 domain to distinguish it from the vastly different Windows NT® domain model. Assuming these requirements are met and all the planets are aligned correctly, COM makes it relatively simple to request Kerberos authentication with the COAUTHINFO structure. One thing to keep in mind is that if you are trying to get Kerberos to work, make sure the COM server's identity has network credentials. If you launch your COM server under the Local Service account, for example, the server identity will not have any network credentials to authenticate.


Protecting Data on the Server

Frequently, a server application needs to store sensitive configuration information such as database connection strings or, in an extreme case, some cached user credentials. This data needs to be stored securely, and a best practice involves the use of a strong ACL to protect the data on disk or in the registry. An ACL is associated with a file or registry key by means of a security descriptor. Security descriptors are used to store per-object security information to support the object-centric security model that is at the heart of Windows security programming.

A security descriptor stores two things of interest: the first is the owner's security identifier (SID) that identifies the owner of the object, and the second is a discretionary access control list (DACL) that lists principals and groups that are granted or denied access to the object and the permissions assigned to each.

Creating a security descriptor from scratch has traditionally involved writing a lot of code. To create the DACL you need to call the InitializeAcl function to create an initially empty ACL, and then call AddAccessAllowedAceEx and AddAccessDeniedAceEx functions on each SID for which you want to specify permissions. Of course, before you can do this you need to actually retrieve or create the SIDs representing the various principals and groups. Then you need to create the actual security descriptor by calling InitializeSecurityDescriptor followed by SetSecurityDescriptorDacl to set the previously created ACL as the DACL for the security descriptor. This can be an error-prone task. One solution is to create some helpful wrapper functions and classes to hide some of this complexity. An even better solution is to use a helper function introduced with Windows 2000 to create security descriptors from a string. The ConvertStringSecurityDescriptorToSecurityDescriptor function takes a specially formatted string and constructs a complete security descriptor for you. Here is an example:

LocalMemory<PSECURITY_DESCRIPTOR> pSecurityDescriptor;

const std::wstring definition = L"O:BAD:(A;;FA;;;BA)(A;;FR;;;NS)";

CheckError(::ConvertStringSecurityDescriptorToSecurityDescriptor(
    definition.c_str(),SDDL_REVISION_1, &pSecurityDescriptor.m_ptr, 0));

The LocalMemory template class automatically frees the owned memory in its destructor using the LocalFree function. I often use the LocalMemory class when doing security programming because many security functions allocate memory with LocalAlloc. The LocalMemory class is available along with the sample code for this article. The O: and D: tokens in the string denote the owner and DACL sections in the security descriptor string definition. The owner is classified as BA, which represents the built-in Administrators group. The DACL is described by the (A;;FA;;;BA)(A;;FR;;;NS) segment, which represents two access control entries (ACEs). The A in both indicates that they are used to allow access. Denying access is represented by D. FA and FR indicate the permissions granted, where FA represents FILE_ALL_ACCESS and FR represents FILE_GENERIC_READ. BA and NS indicate the SIDs being granted the access. As I mentioned previously, BA indicates the Administrators group and NS represents the Network Service account.

For a detailed description of the string format, consult the Security Descriptor String Format documentation in the MSDN® Library.

Now that you have a basic understanding of how to construct a security descriptor, let's proceed to secure a newly created file. When you call the CreateFile function to create a new file, you can specify a security descriptor as well. The security descriptor is specified in the SECURITY_ATTRIBUTES structure passed to CreateFile. The catch is that if the file already exists, the security descriptor will be ignored. This is a serious risk because an attacker could have created a file with a weak ACL before your application had a chance to create the file. If you do not write code to defend against this sort of attack, you may leave sensitive data unprotected and vulnerable to unauthorized access. To avoid this threat, check the result of the GetLastError function after a successful call to CreateFile. If it returns the ERROR_ALREADY_EXISTS error code, you know that the file already existed and you can then attempt to apply the security descriptor once again. This is illustrated by the code in Figure 8.

The SetSecurityInfo function is used to update the object and DACL for the file. You may have noticed that I had to include a few more access rights to allow me to update the security descriptor through the file handle returned by CreateFile. These permissions are only used if the file's security descriptor needs to be updated. You should always limit the permissions you request to the absolute minimum. A better solution that avoids requesting all these permissions is to use the SetNamedSecurityInfo function instead of SetSecurityInfo. Instead of passing a handle to the file as the first parameter, you pass the full path to the file as a string. The function will internally open the file with the appropriate access rights, update the security descriptor, and then close its file handle.


Defending Named Pipe Servers

The file protection techniques I explored in the previous section can also be used to protect named pipe servers as well as other types of servers. A named pipe server is typically implemented as a Windows service. The service's main listening thread calls CreateNamedPipe to create an instance of a named pipe. It then calls the ConnectNamedPipe function to wait for a client process to connect to it. Once a client has connected to the pipe, the server can queue the connection to a worker thread and create a new instance of the named pipe to listen for the next client. Here is a basic use of the CreateNamedPipe function:

CHandle handle(::CreateNamedPipe(L"////.//pipe//Kerr.SecuritySample",
                 PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, 
                 PIPE_TYPE_BYTE, PIPE_UNLIMITED_INSTANCES, 
                 0 /*default output buffer size*/,
                 0 /*default input buffer size*/, 0 /*default time-out*/,
                 0 /*no security attributes*/));

This code will create a new instance of the Kerr.SecuritySample pipe for bidirectional communication and asynchronous input and output on the server end. This is simple enough, but who is allowed to access this pipe? If you call the GetSecurityInfo function on the returned handle to get the pipe's security descriptor, you can then pass it to the ConvertSecurityDescriptorToStringSecurityDescriptor function to get the security descriptor in string format. Using the OWNER_SECURITY_INFORMATION and DACL_SECURITY_INFORMATION flags, you should get something like the following line of code:

O:NSD:(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;NS)(A;;FR;;;WD)(A;;FR;;;AN)

Figure 9 gives you a breakdown of the string. Clearly, this is not a very strong ACL—you should never include the Anonymous and Everyone aliases in an ACL. A better approach is to provide a security descriptor explicitly. The security descriptor is specified in the SECURITY_ATTRIBUTES structure passed as the last parameter to the CreateNamedPipe function.

Another important step in improving your server defenses is to ensure that you are indeed the creator of the named pipe. The first call to CreateNamedPipe with a unique pipe name is said to be the creator and thus the owner of the pipe. Subsequent calls result in new instances of the existing pipe object. A hacker could determine your pipe name and attempt to create the named pipe object before the application does. Then, being the creator, he or she will have sufficient permissions to access any pipe instances freely.

In order to avoid this type of attack, you can use the FILE_FLAG_FIRST_PIPE_INSTANCE access mode flag. If CreateNamedPipe determines that the named pipe already exists, it will fail.

Depending on your requirements, you may need to create additional named pipes for individual clients. For these pipes, you can create a more restrictive security descriptor that only allows individual clients access to the pipe. For this and other reasons, it is often useful to get the token representing the client's logon session. The ImpersonateNamedPipeClient function places the client's token on the current thread. Assuming the client used the SECURITY_IMPERSONATION flag (discussed under Protecting Named Pipe Clients) when connecting to the client end of the pipe, the server will now be able to access local resources on behalf of the client. The following code snippet is a simple class that you can use to place the client's token on the current thread:

class SafeImpersonateNamedPipeClient {
public:
    explicit SafeImpersonateNamedPipeClient(HANDLE pipe) {
        CheckError(::ImpersonateNamedPipeClient(pipe));
    }
    ~SafeImpersonateNamedPipeClient() {
        BOOL success = ::RevertToSelf();
        ATLASSERT(success);
    }
};

If the client uses the SECURITY_IDENTIFICATION flag instead, the server will not be able to use the thread token for accessing resources, but it will, however, be able to query the token for the client's group membership, privileges, and so on, which can be used for authorization. To get the token, call the OpenThreadToken function. The SafeImpersonateNamedPipeClient destructor will automatically take care of restoring the server's security context. With the help of this class, it becomes simple to obtain the client's token, as shown here:

CHandle token;
{
    SafeImpersonateNamedPipeClient impersonate(handle);
    CheckError(::OpenThreadToken(::GetCurrentThread(),
                                 TOKEN_QUERY, TRUE, &token.m_h));
}

When execution leaves the inner scope, the RevertToSelf function will be called and the token variable will hold a copy of the client's token. A common misconception is that impersonation means that the server can perform tasks acting as the client. This is untrue. In Windows security parlance, impersonation simply refers to the act of placing a token for the client's logon session on the current thread. Whether or not the server can access resources on behalf of the client is dependent on authentication settings defined by the client. If you attempt to access the file system using the CreateFile function while impersonating a client for example, it will fail unless the client has granted you the rights to do so.

Now that you have a token, you can interrogate it using the GetTokenInformation function to perform any authorization of your own. Alternatively, you can manage your own private security descriptors for your application and use the AccessCheck function to take care of implementing a consistent authorization model.

Although named pipes support signing, thus providing a level of data integrity, they do not support privacy. Signing data is not even enabled by default and is controlled by a machine-wide setting. For these reasons, do not use pipes for communicating sensitive information. If you must use a pipe, first establish a shared key using some other form of interprocess communication that does provide data integrity and privacy, and then use a symmetric algorithm to encrypt the data communicated over the pipe.


Defending COM Servers

For COM clients, I suggested being explicit about your security demands and ignoring machine and process defaults. Server applications, on the other hand, are generally better off letting administrators influence their security configuration. It is still important to be conscious of the level of security in use, but rather than simply overriding the declarative security settings, you should start by installing your server with secure defaults. An administrator can then modify those security defaults as needed. In this section, I'm going to focus on those aspects of security that relate to classic COM servers as well as COM+ server applications.

There are two distinct points of access control for a COM server. The first is controlled by launch or activation permissions and the second is controlled by access permissions. Classic COM servers are typically caller activated, meaning that the process that hosts the COM server is started when the client first activates the COM object—for example, using the CoCreateInstanceEx function. The ability for a client to activate a COM server is thus controlled by activation permissions, which are controlled at the COM application level and expressed in the form of a security descriptor stored in the COM application's registry key.

Administrators can manage launch permissions through the Component Services Microsoft® Management Console (MMC) snap-in. If launch permissions are not set at the application level, machine-wide defaults are used. These, too, can be edited using the Component Services snap-in.

The second point at which access control occurs is when a client makes a call to the COM server through a proxy. The proxy's authentication settings are communicated to the stub on the server, which can then determine whether the client has permissions to access the object. This is called access control, which is also managed at the COM application level through the DCOM Config node of the Component Services snap-in. A server can also override access permissions by calling the CoInitializeSecurity function when it first starts. The first parameter to this function takes a security descriptor which, if present, provides the effective ACL that all RPC calls into the server are then measured against.

Although the client specifies the level of authentication information provided to the server, the server can demand a minimum quality of service from the client. This is controlled by the authentication level. In the Protecting COM Clients section, I discussed the authentication level that the client will use since it is a choice the client makes. The server can, however, demand a minimum level of authentication. This is typically used to ensure data integrity and privacy if needed. You should employ the packet integrity and packet privacy levels, favoring packet privacy, as it provides tamper detection as well as encryption of all communication. The authentication level can also be controlled for a given COM application using the Component Services snap-in or using the CoInitializeSecurity function.

Once the client has successfully navigated past launch and access permissions, the server can use the CoImpersonateClient function to place the client's token on the current thread. When you have finished impersonating, call the CoRevertToSelf function to restore the server's security context. Both of these functions are simply wrappers around the IServerSecurity interface, which you can retrieve directly using the CoGetCallContext function. What the server can do with the user token is subject to the impersonation level controlled by the client. Using the client's token is the same as for named pipe servers, using the OpenThreadToken function to get the token from the thread, and so on.

COM+ applications break from the traditional Windows object-centric security model of access control lists to a model where access is granted to objects and methods based on role membership. These roles are distinct from Windows groups and are managed declaratively using the Component Services snap-in. An application developer defines the roles for a given application, and pre-populates them with conservative defaults during installation. The server administrator can then modify these default role assignments as needed. The nice thing about this model is that security is managed declaratively—you do not need to do anything special in the code to take advantage of role-based security. If a client attempts to call a method that it does not have access to, based on its role assignment, the call will be rejected before entering the COM object.

COM introduced the concept of a call context for representing the current call into an object and specifically the direct caller. You can access the call context through the IServerSecurity interface returned by CoGetCallContext. COM+ extended the notion of call context to provide more information about the call context including information about intermediate security contexts in the call chain. It also provides the ability to programmatically check role membership for fine-grained control. This functionality is exposed through the ISecurityCallContext interface, which can also be retrieved using the CoGetCallContext function. The following code snippet illustrates how to determine if the direct caller is in a given role:

bool IsCallerInRole(const CComBSTR& role) {
    CComPtr<ISecurityCallContext> spCallContext;
    CheckError(::CoGetCallContext(IID_ISecurityCallContext,
               reinterpret_cast<PVOID*>(&spCallContext)));

    VARIANT_BOOL inRole = VARIANT_FALSE;
    CheckError(spCallContext->IsCallerInRole(role, &inRole));
    return VARIANT_FALSE != inRole;
}

COM+ services are also exposed through the .NET Framework in the System.EnterpriseServices namespace, though more and more of its features and services are being provided natively by the common language runtime, ASP.NET, and Web services.


Conclusion

The Windows programming landscape covers many technologies that can be combined to provide new and exciting applications. All of these technologies and applications must be secured to provide reliable protection for users and organizations providing services. For every type of client and server technology you employ, you need to understand the security model that is used to authenticate, authorize, and protect users and their data.


阅读全文
0 0

相关文章推荐

img
取 消
img