Safe Initialization - Adaptive Code via C#. Agile coding with design patterns and SOLID principles (2014)

Adaptive Code via C#. Agile coding with design patterns and SOLID principles (2014)

Chapter 1. Safe Initialization

Robust initialization of a program is important from a security standpoint, because the more parts of the program's environment that can be validated (e.g., input, privileges, system parameters) before any critical code runs, the better you can minimize the risks of many types of exploits. In addition, setting a variety of operating parameters to a known state will help thwart attackers who run a program in a hostile environment, hoping to exploit some assumption in the program regarding an external resource that the program accesses (either directly or indirectly). This chapter outlines some of these potential problems, and suggests solutions that work towards reducing the associated risks.

1.1. Sanitizing the Environment

Problem

Attackers can often control the value of important environment variables, sometimes even remotely—for example, in CGI scripts, where invocation data is passed through environment variables.

You need to make sure that an attacker does not set environment variables to malicious values.

Solution

Many programs and libraries, including the shared library loader on both Unix and Windows systems, depend on environment variable settings. Because environment variables are inherited from the parent process when a program is executed, an attacker can easily sabotage variables, causing your program to behave in an unexpected and even insecure manner.

Typically, Unix systems are considerably more dependent on environment variables than are Windows systems. In fact, the only scenario common to both Unix and Windows is that there is an environment variable defining the path that the system should search to find an executable or shared library (although differently named variables are used on each platform). On Windows, one environment variable controls the search path for finding both executables and shared libraries. On Unix, these are controlled by separate environment variables. Generally, you should not specify a filename and then rely on these variables for determining the full path. Instead, you should always use absolute paths to known locations.[1]

Certain variables expected to be present in the environment can cause insecure program behavior if they are missing or improperly set. Make sure, therefore, that you never fully purge the environment and leave it empty. Instead, variables that should exist should be forced to sane values or, at the very least, treated as highly suspect and examined closely before they're used. Remove any unknown variables from the environment altogether.

Discussion

The standard C runtime library defines a global variable,[2] environ , as a NULL-terminated array of strings, where each string in the array is of the form "name=value".

Most systems do not declare the variable in any standard header file, Linux being the notable exception, providing a declaration in unistd.h. You can gain access to the variable by including the following extern statement in your code:

extern char **environ;

Several functions defined in stdlib.h, such as getenv( ) and putenv( ) , provide access to environment variables, and they all operate on this variable. You can therefore make changes to the contents of the array or even build a new array and assign it to the variable.

This variable also exists in the standard C runtime library on Windows; however, the C runtime on Windows is not as tightly bound to the operating system as it is on Unix. Directly manipulating the environ variable on Windows will not necessarily produce the same effects as it will on Unix; in the majority of Windows programs, the C runtime is never used at all, instead favoring the Win32 API to perform the same functions as those provided by the C runtime. Because of this, and because of Windows' lack of dependence on environment variables, we do not recommend using the code in this recipe on Windows. It simply does not apply. However, we do recommend that you at least skim the textual content of this recipe so that you're aware of potential pitfalls that could affect you on Windows.

On a Unix system, if you invoke the command printenv at a shell prompt, you'll likely see a sizable list of environment variables as a result. Many of the environment variables you will see are set by whichever shell you're using (i.e., bash or tcsh). You should never use nor trust any of the environment variables that are set by the shell. In addition, a malicious user may be able to set other environment variables.

In most cases, the information contained in the environment variables set by the shell can be determined by much more reliable means. For example, most shells set the HOME environment variable, which is intended to be the user's home directory. It's much more reliable to call getuid( ) to determine who the user is, and then call getpwuid( ) to get the user's password file record, which will contain the user's home directory. For example:

#include <sys/types.h>

#include <stdio.h>

#include <string.h>

#include <unistd.h>

#include <pwd.h>

int main(int argc, char *argv[ ]) {

uid_t uid;

struct passwd *pwd;

uid = getuid( );

printf("User's UID is %d.\n", (int)uid);

if (!(pwd = getpwuid(uid))) {

printf("Unable to get user's password file record!\n");

endpwent( );

return 1;

}

printf("User's home directory is %s\n", pwd->pw_dir);

endpwent( );

return 0;

}

WARNING

The code above is not thread-safe. Be sure multiple threads do not try to manipulate the password database at the same time.

In many cases, it is reasonably safe to throw away most of the environment variables that your program will inherit from its parent process, but you should make it a point to be aware of any environment variables that will be used by code you're using, including the operating system's dynamic loader and the standard C runtime library. In particular, dynamic loaders on ELF-based Unix systems (among the Unix variants we're explicitly supporting in this book, Darwin is the major exception here because it does not use ELF (Executable and Linking Format) for its executable format) and most standard implementations of malloc( ) all recognize a wide variety of environment variables that control their behavior.

In most cases, you should never be doing anything in your programs that will make use of the PATH environment variable. Circumstances do exist in which it may be reasonable to do so, but make sure to weigh your options carefully beforehand. Indeed, you should consider carefully whether you should be using any environment variable in your programs. Regardless, if you launch external programs from within your program, you may not have control over what the external programs do, so you should take care to provide any external programs you launch with a sane and secure environment.

In particular, the two environment variables IFS and PATH should always be forced to sane values. The IFS environment variable is somewhat obscure, but it is used by many shells to determine which character separates command-line arguments. Modern Unix shells use a reasonable default value for IFS if it is not already set. Nonetheless, you should defensively assume that the shell does nothing of the sort. Therefore, instead of simply deleting the IFS environment variable, set it to something sane, such as a space, tab, and newline character.

The PATH environment variable is used by the shell and some of the exec*( ) family of standard C functions to locate an executable if a path is not explicitly specified. The search path should never include relative paths, especially the current directory as denoted by a single period. To be safe, you should always force the setting of the PATH environment variable to _PATH_STDPATH, which is defined in paths.h. This value is what the shell normally uses to initialize the variable, but an attacker or naïve user could change it later. The definition of _PATH_STDPATH differs from platform to platform, so you should generally always use that value so that you get the right standard paths for the system your program is running on.

Finally, the TZ environment variable denotes the time zone that the program should use, when relevant. Because users may not be in the same time zone as the machine (which will use a default whenever the variable is not set), it is a good idea to preserve this variable, if present. Note also that this variable is generally used by the OS, not the application. If you're using it at the application level, make sure to do proper input validation to protect against problems such as buffer overflow.

Finally, a special environment variable,, is defined to be the time zone on many systems. All systems will use it if it is defined, but while most systems will get along fine without it, some systems will not function properly without its being set. Therefore, you should preserve it if it is present.

Any other environment variables that are defined should be removed unless you know, for some reason, that you need the variable to be set. For any environment variables you preserve, be sure to treat them as untrusted user input. You may be expecting them to be set to reasonable values—and in most cases, they probably will be—but never assume they are. If for some reason you're writing CGI code in C, the list of environment variables passed from the web server to your program can be somewhat large, but these are largely trustworthy unless an attacker somehow manages to wedge another program between the web server and your program.

Of particular interest among environment variables commonly passed from a web server to CGI scripts are any environment variables whose names begin with HTTP_ and those listed in Table 1-1.

Table 1-1. Environment variables commonly passed from web servers to CGI scripts

Environment variable name

Comments

AUTH_TYPE

If authentication was required to make the request, this contains the authentication type that was used, usually "BASIC".

CONTENT_LENGTH

The number of bytes of content, as specified by the client.

CONTENT_TYPE

The MIME type of the content sent by the client.

GATEWAY_INTERFACE

The version of the CGI specification with which the server complies.

PATH_INFO

Extra path information from the URL.

PATH_TRANSLATED

Extra path information from the URL, translated by the server.

QUERY_STRING

The portion of the URL following the question mark.

REMOTE_ADDR

The IP address of the remote client in dotted decimal form.

REMOTE_HOST

The host name of the remote client.

REMOTE_IDENT

If RFC1413 identification was used, this contains the user name that was retrieved from the remote identification server.

REMOTE_USER

If authentication was required to make the request, this contains the user name that was authenticated.

REQUEST_METHOD

The method used to make the current request, usually either "GET" or "POST".

SCRIPT_NAME

The name of the script that is running, canonicalized to the root of the web site's document tree (e.g., DocumentRoot in Apache).

SERVER_NAME

The host name or IP address of the server.

SERVER_PORT

The port on which the server is running.

SERVER_PROTOCOL

The protocol used to make the request, typically "HTTP/1.0" or "HTTP/1.1".

SERVER_SOFTWARE

The name and version of the server.

The code presented in this section defines a function called spc_sanitize_environment( ) that will build a new environment with the IFS and PATH environment variables set to sane values, and with the TZ environment variable preserved from the original environment if it is present. You can also specify a list of environment variables to preserve from the original in addition to the TZ environment variable.

The first thing that spc_sanitize_environment( ) does is determine how much memory it will need to allocate to build the new environment. If the memory it needs cannot be allocated, the function will call abort( ) to terminate the program immediately. Otherwise, it will then build the new environment and replace the old environ pointer with a pointer to the newly allocated one. Note that the memory is allocated in one chunk rather than in smaller pieces for the individual strings. While this is not strictly necessary (and it does not provide any specific security benefit), it's faster and places less strain on memory allocation. Note, however, that you should be performing this operation early in your program, so heap fragmentation shouldn't be much of an issue.

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <paths.h>

extern char **environ;

/* These arrays are both NULL-terminated. */

static char *spc_restricted_environ[ ] = {

"IFS= \t\n",

"PATH=" _PATH_STDPATH,

0

};

static char *spc_preserve_environ[ ] = {

"TZ",

0

};

void spc_sanitize_environment(int preservec, char **preservev) {

int i;

char **new_environ, *ptr, *value, *var;

size_t arr_size = 1, arr_ptr = 0, len, new_size = 0;

for (i = 0; (var = spc_restricted_environ[i]) != 0; i++) {

new_size += strlen(var) + 1;

arr_size++;

}

for (i = 0; (var = spc_preserve_environ[i]) != 0; i++) {

if (!(value = getenv(var))) continue;

new_size += strlen(var) + strlen(value) + 2; /* include the '=' */

arr_size++;

}

if (preservec && preservev) {

for (i = 0; i < preservec && (var = preservev[i]) != 0; i++) {

if (!(value = getenv(var))) continue;

new_size += strlen(var) + strlen(value) + 2; /* include the '=' */

arr_size++;

}

}

new_size += (arr_size * sizeof(char *));

if (!(new_environ = (char **)malloc(new_size))) abort( );

new_environ[arr_size - 1] = 0;

ptr = (char *)new_environ + (arr_size * sizeof(char *));

for (i = 0; (var = spc_restricted_environ[i]) != 0; i++) {

new_environ[arr_ptr++] = ptr;

len = strlen(var);

memcpy(ptr, var, len + 1);

ptr += len + 1;

}

for (i = 0; (var = spc_preserve_environ[i]) != 0; i++) {

if (!(value = getenv(var))) continue;

new_environ[arr_ptr++] = ptr;

len = strlen(var);

memcpy(ptr, var, len);

*(ptr + len + 1) = '=';

memcpy(ptr + len + 2, value, strlen(value) + 1);

ptr += len + strlen(value) + 2; /* include the '=' */

}

if (preservec && preservev) {

for (i = 0; i < preservec && (var = preservev[i]) != 0; i++) {

if (!(value = getenv(var))) continue;

new_environ[arr_ptr++] = ptr;

len = strlen(var);

memcpy(ptr, var, len);

*(ptr + len + 1) = '=';

memcpy(ptr + len + 2, value, strlen(value) + 1);

ptr += len + strlen(value) + 2; /* include the '=' */

}

}

environ = new_environ;

}

See Also

Recipe 1.7, Recipe 1.8


[1] Note that the shared library environment variable can be relatively benign on modern Unix-based operating systems, because the environment variable will get ignored when a program that can change permissions (i.e., a setuid program) is invoked. Nonetheless, it is better to be safe than sorry!

[2] The use of the term "variable" can quickly become confusing because C defines variables and the environment defines variables. In this recipe, when we are referring to a C variable, we simply say "variable," and when we are referring to an environment variable, we say "environment variable."

1.2. Restricting Privileges on Windows

Problem

Your Windows program runs with elevated privileges, such as Administrator or Local System, but it does not require all the privileges granted to the user account under which it's running. Your program never needs to perform certain actions that may be dangerous if users with elevated privileges run it and an attacker manages to compromise the program.

Solution

When a user logs into the system or the service control manager starts a service, a token is created that contains information about the user logging in or the user under which the service is running. The token contains a list of all of the groups to which the user belongs (the user and each group in the list is represented by a Security ID or SID), as well as a set of privileges that any thread running with the token has. The set of privileges is initialized from the privileges assigned by the system administrator to the user and the groups to which the user belongs.

Beginning with Windows 2000, it is possible to create a restricted token and force threads to run using that token. Once a restricted token has been applied to a running thread, any restrictions imposed by the restricted token cannot be lifted; however, it is possible to revert the thread back to its original unrestricted token. With restricted tokens, it's possible to remove privileges, restrict the SIDs that are used in access checking, and deny SIDs access. The use of restricted tokens is more useful when combined with the CreateProcessAsUser( ) API to create a new process with a restricted token that cannot be reverted to a more permissive token.

Beginning with Windows .NET Server 2003, it is possible to permanently remove privileges from a process's token. Once the privileges have been removed, they cannot be added back. Any new processes created by a process running with a modified token will inherit the modified token; therefore, the same restrictions imposed upon the parent process are also imposed upon the child process. Note that modifying a token is quite different from creating a restricted token. In particular, only privileges can be removed; SIDs can be neither restricted nor denied.

Discussion

Tokens contain a list of SIDs, composed of the user's SID and one SID for each group of which the user is a member. SIDs are assigned by the system when users and groups are created. In addition to the SIDs, tokens also contain a list of restricted SIDs. When access checks are performed and the token contains a list of restricted SIDs, the intersection of the two lists of SIDs contained in the token is used to perform the access check. Finally, tokens also contain a list of privileges. Privileges define specific access rights. For example, for a process to use the Win32 debugging API, the process's token must contain the SeDebugPrivilege privilege.

The primary list of SIDs contained in a token cannot be modified. The token is created for a particular user, and the token must always contain the user's SID along with the SIDs for each group of which the user is a member. However, each SID in the primary list can be marked with a "deny" attribute, which causes access to be denied when an access control list (ACL) contains a SID that is marked as "deny" in the active token.

Creating restricted tokens

Using the CreateRestrictedToken( ) API, a restricted token can be created from an existing token. The resulting token can then be used to create a new process or to set an impersonation token for a thread. In the former case, the restricted token becomes the newly created process's primary token; in the latter case, the thread can revert back to its primary token, effectively making the restrictions imposed by the restricted token useful for little more than helping to prevent accidents.

CreateRestrictedToken( ) requires a large number of arguments, and it may seem an intimidating function to use, but with some explanation and examples, it's not actually all that difficult. The function has the following signature:

BOOL CreateRestrictedToken(HANDLE ExistingTokenHandle, DWORD Flags,

DWORD DisableSidCount, PSID_AND_ATTRIBUTES SidsToDisable,

DWORD DeletePrivilegeCount, PLUID_AND_ATTRIBUTES PrivilegesToDelete,

DWORD RestrictedSidCount, PSID_AND_ATTRIBUTES SidsToRestrict,

PHANDLE NewTokenHandle);

These functions have the following arguments:

ExistingTokenHandle

Handle to an existing token. An existing token handle can be obtained via a call to either OpenProcessToken( ) or OpenThreadToken( ) . The token may be either a primary or a restricted token. In the latter case, the token may be obtained from an earlier call to CreateRestrictedToken( ). The existing token handle must have been opened or created with TOKEN_DUPLICATE access.

Flags

May be specified as 0 or as a combination of DISABLE_MAX_PRIVILEGE or SANDBOX_INERT. If DISABLE_MAX_PRIVILEGE is used, all privileges in the new token are disabled, and the two arguments DeletePrivilegeCount and PrivilegesToDelete are ignored. The SANDBOX_INERT has no special meaning other than it is stored in the token, and can be later queried using GetTokenInformation( ).

DisableSidCount

Number of elements in the list SidsToDisable. May be specified as 0 if there are no SIDs to be disabled. Disabling a SID is the same as enabling the SIDs "deny" attribute.

SidsToDisable

List of SIDs for which the "deny" attribute is to be enabled. May be specified as NULL if no SIDs are to have the "deny" attribute enabled. See below for information on the SID_AND_ATTRIBUTES structure.

DeletePrivilegeCount

Number of elements in the list PrivilegesToDelete. May be specified as 0 if there are no privileges to be deleted.

PrivilegesToDelete

List of privileges to be deleted from the token. May be specified as NULL if no privileges are to be deleted. See below for information on the LUID_AND_ATTRIBUTES structure.

RestrictedSidCount

Number of elements in the list SidsToRestrict. May be specified as 0 if there are no restricted SIDs to be added.

SidsToRestrict

List of SIDs to restrict. If the existing token is a restricted token that already has restricted SIDs, the resulting token will have a list of restricted SIDs that is the intersection of the existing token's list and this list. May be specified as NULL if no restricted SIDs are to be added to the new token.

NewTokenHandle

Pointer to a HANDLE that will receive the handle to the newly created token.

The function OpenProcessToken( ) will obtain a handle to the process's primary token, while OpenThreadToken( ) will obtain a handle to the calling thread's impersonation token. Both functions have a similar signature, though their arguments are treated slightly differently:

BOOL OpenProcessToken(HANDLE hProcess, DWORD dwDesiredAccess, PHANDLE phToken);

BOOL OpenThreadToken(HANDLE hThread, DWORD dwDesiredAccess, BOOL bOpenAsSelf,

PHANDLE phToken);

This function has the following arguments:

hProcess

Handle to the current process, which is normally obtained via a call to GetCurrentProcess( ).

hThread

Handle to the current thread, which is normally obtained via a call to GetCurrentThread( ).

dwDesiredAccess

Bit mask of the types of access desired for the returned token handle. For creating restricted tokens, this must always include TOKEN_DUPLICATE. If the restricted token being created will be used as a primary token for a new process, you must include TOKEN_ASSIGN_PRIMARY; otherwise, if the restricted token that will be created will be used as an impersonation token for the thread, you must include TOKEN_IMPERSONATE.

bOpenAsSelf

Boolean flag that determines how the access check for retrieving the thread's token is performed. If specified as FALSE, the access check uses the calling thread's permissions. If specified as TRUE, the access check uses the calling process's permissions.

phToken

Pointer to a HANDLE that will receive the handle to the process's primary token or the thread's impersonation token, depending on whether you're calling OpenProcessToken( ) or OpenThreadToken( ).

Creating a new process with a restricted token is done by calling CreateProcessAsUser( ) , which works just as CreateProcess( ) does (see Recipe 1.8) except that it requires a token to be used as the new process's primary token. Normally, CreateProcessAsUser( ) requires that the active token have the SeAssignPrimaryTokenPrivilege privilege, but if a restricted token is used, that privilege is not required. The following pseudo-code demonstrates the steps required to create a new process with a restricted primary token:

HANDLE hProcessToken, hRestrictedToken;

/* First get a handle to the current process's primary token */

OpenProcessToken(GetCurrentProcess( ), TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY,

&hProcessToken);

/* Create a restricted token with all privileges removed */

CreateRestrictedToken(hProcessToken, DISABLE_MAX_PRIVILEGE, 0, 0, 0, 0, 0, 0,

&hRestrictedToken);

/* Create a new process using the restricted token */

CreateProcessAsUser(hRestrictedToken, ...);

/* Cleanup */

CloseHandle(hRestrictedToken);

CloseHandle(hProcessToken);

Setting a thread's impersonation token requires a bit more work. Unless the calling thread is impersonating, calling OpenThreadToken( ) will result in an error because the thread does not have an impersonation token and thus is using the process's primary token. Likewise, callingSetThreadToken( ) unless impersonating will also fail because a thread cannot have an impersonation token if it's not impersonating.

If you want to restrict a thread's access rights temporarily, the easiest solution to the problem is to force the thread to impersonate itself. When impersonation begins, the thread is assigned an impersonation token, which can then be obtained via OpenThreadToken( ). A restricted token can be created from the impersonation token, and the thread's impersonation token can then be replaced with the new restricted token by calling SetThreadToken( ).

The following pseudo-code demonstrates the steps required to replace a thread's impersonation token with a restricted one:

HANDLE hRestrictedToken, hThread, hThreadToken;

/* First begin impersonation */

ImpersonateSelf(SecurityImpersonation);

/* Get a handle to the current thread's impersonation token */

hThread = GetCurrentThread( );

OpenThreadToken(hThread, TOKEN_DUPLICATE | TOKEN_IMPERSONATE, TRUE, &hThreadToken);

/* Create a restricted token with all privileges removed */

CreateRestrictedToken(hThreadToken, DISABLE_MAX_PRIVILEGE, 0, 0, 0, 0, 0, 0,

&hRestrictedToken);

/* Set the thread's impersonation token to the new restricted token */

SetThreadToken(&hThread, hRestrictedToken);

/* ... perform work here */

/* Revert the thread's impersonation token back to its original */

SetThreadToken(&hThread, 0);

/* Stop impersonating */

RevertToSelf( );

/* Cleanup */

CloseHandle(hRestrictedToken);

CloseHandle(hThreadToken);

Modifying a process's primary token

Beginning with Windows .NET Server 2003, support for a new flag has been added to the function AdjustTokenPrivileges( ) ; it allows a privilege to be removed from a token, rather than simply disabled. Once the privilege has been removed, it cannot be added back to the token. In older versions of Windows, privileges could only be enabled or disabled using AdjustTokenPrivileges( ), and there was no way to remove privileges from a token without duplicating it. There is no way to substitute another token for a process's primary token—the best you can do in older versions of Windows is to use restricted impersonation tokens.

BOOL AdjustTokenPrivileges(HANDLE TokenHandle, BOOL DisableAllPrivileges,

PTOKEN_PRIVILEGES NewState, DWORD BufferLength,

PTOKEN_PRIVILEGES PreviousState, PDWORD ReturnLength);

This function has the following arguments:

TokenHandle

Handle to the token that is to have its privileges adjusted. The handle must have been opened with TOKEN_ADJUST_PRIVILEGES access; in addition, if PreviousState is to be filled in, it must have TOKEN_QUERY access.

DisableAllPrivileges

Boolean argument that specifies whether all privileges held by the token are to be disabled. If specified as TRUE, all privileges are disabled, and the NewState argument is ignored. If specified as FALSE, privileges are adjusted according to the information in the NewState argument.

NewState

List of privileges that are to be adjusted, along with the adjustment that is to be made for each. Privileges can be enabled, disabled, and removed. The TOKEN_PRIVILEGES structure contains two fields: PrivilegeCount and Privileges. PrivilegeCount is simply a DWORD that indicates how many elements are in the array that is the Privileges field. The Privileges field is an array of LUID_AND_ATTRIBUTES structures, for which the Attributes field of each element indicates how the privilege is to be adjusted. A value of 0 disables the privilege, SE_PRIVILEGE_ENABLED enables it, andSE_PRIVILEGE_REMOVED removes the privilege. See Section 1.2.3.4 later in this section for more information regarding these structures.

BufferLength

Length in bytes of the PreviousState buffer. May be 0 if PreviousState is NULL.

PreviousState

Buffer into which the state of the token's privileges prior to adjustment is stored. It may be specified as NULL if the information is not required. If the buffer is not specified as NULL, the token must have been opened with TOKEN_QUERY access.

ReturnLength

Pointer to an integer into which the number of bytes written into the PreviousState buffer will be placed. May be specified as NULL if PreviousState is also NULL.

The following example code demonstrates how AdjustTokenPrivileges( ) can be used to remove backup and restore privileges from a token:

#include <windows.h>

BOOL RemoveBackupAndRestorePrivileges(VOID) {

BOOL bResult;

HANDLE hProcess, hProcessToken;

PTOKEN_PRIVILEGES pNewState;

/* Allocate a TOKEN_PRIVILEGES buffer to hold the privilege change information.

* Two privileges will be adjusted, so make sure there is room for two

* LUID_AND_ATTRIBUTES elements in the Privileges field of TOKEN_PRIVILEGES.

*/

pNewState = (PTOKEN_PRIVILEGES)LocalAlloc(LMEM_FIXED, sizeof(TOKEN_PRIVILEGES) +

(sizeof(LUID_AND_ATTRIBUTES) * 2));

if (!pNewState) return FALSE;

/* Add the two privileges that will be removed to the allocated buffer */

pNewState->PrivilegeCount = 2;

if (!LookupPrivilegeValue(0, SE_BACKUP_NAME, &pNewState->Privileges[0].Luid) ||

!LookupPrivilegeValue(0, SE_RESTORE_NAME, &pNewState->Privileges[1].Luid)) {

LocalFree(pNewState);

return FALSE;

}

pNewState->Privileges[0].Attributes = SE_PRIVILEGE_REMOVED;

pNewState->Privileges[1].Attributes = SE_PRIVILEGE_REMOVED;

/* Get a handle to the process's primary token. Request TOKEN_ADJUST_PRIVILEGES

* access so that we can adjust the privileges. No other privileges are req'd

* since we'll be removing the privileges and thus do not care about the previous

* state. TOKEN_QUERY access would be required in order to retrieve the previous

* state information.

*/

hProcess = GetCurrentProcess( );

if (!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hProcessToken)) {

LocalFree(pNewState);

return FALSE;

}

/* Adjust the privileges, specifying FALSE for DisableAllPrivileges so that the

* NewState argument will be used instead. Don't request information regarding

* the token's previous state by specifying 0 for the last three arguments.

*/

bResult = AdjustTokenPrivileges(hProcessToken, FALSE, pNewState, 0, 0, 0);

/* Cleanup and return the success or failure of the adjustment */

CloseHandle(hProcessToken);

LocalFree(pNewState);

return bResult;

}

Working with SID_AND_ATTRIBUTES structures

A SID_AND_ATTRIBUTES structure contains two fields: Sid and Attributes. The Sid field is of type PSID, which is a variable-sized object that should never be directly manipulated by application-level code. The meaning of the Attributes field varies depending on the use of the structure. When aSID_AND_ATTRIBUTES structure is being used for disabling SIDs (enabling the "deny" attribute), the Attributes field is ignored. When a SID_AND_ATTRIBUTES structure is being used for restricting SIDs, the Attributes field should always be set to 0. In both cases, it's best to set the Attributes field to 0.

Initializing the Sid field of a SID_AND_ATTRIBUTES structure can be done in a number of ways, but perhaps one of the most useful ways is to use LookupAccountName( ) to obtain the SID for a specific user or group name. The following code demonstrates how to look up the SID for a name:

#include <windows.h>

PSID SpcLookupSidByName(LPCTSTR lpAccountName, PSID_NAME_USE peUse) {

PSID pSid;

DWORD cbSid, cchReferencedDomainName;

LPTSTR ReferencedDomainName;

SID_NAME_USE eUse;

cbSid = cchReferencedDomainName = 0;

if (!LookupAccountName(0, lpAccountName, 0, &cbSid, 0, &cchReferencedDomainName,

&eUse)) return 0;

if (!(pSid = LocalAlloc(LMEM_FIXED, cbSid))) return 0;

ReferencedDomainName = LocalAlloc(LMEM_FIXED,

(cchReferencedDomainName + 1) * sizeof(TCHAR));

if (!ReferencedDomainName) {

LocalFree(pSid);

return 0;

}

if (!LookupAccountName(0, lpAccountName, pSid, &cbSid, ReferencedDomainName,

&cchReferencedDomainName, &eUse)) {

LocalFree(ReferencedDomainName);

LocalFree(pSid);

return 0;

}

LocalFree(ReferencedDomainName);

if (peUse) *peUse = eUse;

return 0;

}

If the requested account name is found, a PSID object allocated via LocalAlloc( ) is returned; otherwise, NULL is returned. If the second argument is specified as non-NULL, it will contain the type of SID that was found. Because Windows uses SIDs for many different things other than simply users and groups, the type could be one of many possibilities. If you're looking for a user, the type should be SidTypeUser. If you're looking for a group, the type should be SidTypeGroup. Other possibilities include SidTypeDomain, SidTypeAlias, SidTypeWellKnownGroup, SidTypeDeletedAccount,SidTypeInvalid, SidTypeUnknown, and SidTypeComputer.

Working with LUID_AND_ATTRIBUTES structures

An LUID_AND_ATTRIBUTES structure contains two fields: Luid and Attributes. The Luid field is of type LUID , which is an object that should never be directly manipulated by application-level code. The meaning of the Attributes field varies depending on the use of the structure. When anLUID_AND_ATTRIBUTES structure is being used for deleting privileges from a restricted token, the Attributes field is ignored and should be set to 0. When an LUID_AND_ATTRIBUTES structure is being used for adjusting privileges in a token, the Attributes field should be set to SE_PRIVILEGE_ENABLED to enable the privilege, SE_PRIVILEGE_REMOVED to remove the privilege, or 0 to disable the privilege. The SE_PRIVILEGE_REMOVED attribute is not valid on Windows NT, Windows 2000, or Windows XP; it is a newly supported flag in Windows .NET Server 2003.

Initializing the Luid field of an LUID_AND_ATTRIBUTES structure is typically done using LookupPrivilegeValue( ) , which has the following signature:

BOOL LookupPrivilegeValue(LPCTSTR lpSystemName, LPCTSTR lpName, PLUID lpLuid);

This function has the following arguments:

lpSystemName

Name of the computer on which the privilege value's name is looked up. This is normally specified as NULL, which indicates that only the local system should be searched.

lpName

Name of the privilege to look up. The Windows platform SDK header file winnt.h defines a sizable number of privilege names as macros that expand to literal strings suitable for use here. Each of these macros begins with SE_, which is followed by the name of the privilege. For example, the SeBackupPrivilege privilege has a corresponding macro named SE_BACKUP_NAME.

lpLuid

Pointer to a caller-allocated LUID object that will receive the LUID information if the lookup is successful. LUID objects are a fixed size, so they may be allocated either dynamically or on the stack.

See Also

Recipe 1.8

1.3. Dropping Privileges in setuid Programs

Problem

Your program runs setuid or setgid (see Section 1.3.3 for definitions), thus providing your program with extra privileges when it is executed. After the work requiring the extra privileges is done, those privileges need to be dropped so that an attacker cannot leverage your program during an attack that results in privilege elevation.

Solution

If your program must run setuid or setgid, make sure to use the privileges properly so that an attacker cannot exploit other possible vulnerabilities in your program and gain these additional privileges. You should perform whatever work requires the additional privileges as early in the program as possible, and you should drop the extra privileges immediately after that work is done.

While many programmers may be aware of the need to drop privileges, many more are not. Worse, those who do know to drop privileges rarely know how to do so properly and securely. Dropping privileges is tricky business because the semantics of the system calls to manipulate IDs for setuid/setgid vary from one Unix variant to another—sometimes only slightly, but often just enough to make the code that works on one system fail on another.

On modern Unix systems, the extra privileges resulting from using the setuid or setgid bits on an executable can be dropped either temporarily or permanently. It is best if your program can do what it needs to with elevated privileges, then drop those privileges permanently, but that's not always possible. If you must be able to restore the extra privileges, you will need to be especially careful in your program to do everything possible to prevent an attacker from being able to take control of those privileges. We strongly advise against dropping privileges only temporarily. You should do everything possible to design your program such that it can drop privileges permanently as quickly as possible. We do recognize that it's not always possible to do—the Unix passwd command is a perfect example: the last thing it does is use its extra privileges to write the new password to the password file, and it cannot do it any sooner.

Discussion

Before we can discuss how to drop privileges either temporarily or permanently, it's useful to have at least a basic understanding of how setuid, setgid, and the privilege model in general work on Unix systems. Because of space constraints and the complexity of it all, we're not able to delve very deeply into the inner workings here. If you are interested in a more detailed discussion, we recommend the paper "Setuid Demystified" by Hao Chen, David Wagner, and Drew Dean, which was presented at the 11th USENIX Security Symposium in 2002 and is available athttp://www.cs.berkeley.edu/~daw/papers/setuid-usenix02.pdf.

On all Unix systems, each process has an effective user ID, a real user ID, an effective group ID, and a real group ID. In addition, each process on most modern Unix systems also has a saved user ID and a saved group ID.[3] All of the Unix variants that we cover in this book have saved user IDs, so our discussion assumes that the sets of user and group IDs each have an effective ID, a real ID, and a saved ID.

Normally when a process is executed, the effective, real, and saved user and group IDs are all set to the real user and group ID of the process's parent, respectively. However, when the setuid bit is set on an executable, the effective and saved user IDs are set to the user ID that owns the file. Likewise, when the setgid bit is set on an executable, the effective and saved group IDs are set to the group ID that owns the file.

For the most part, all privilege checks performed by the operating system are done using the effective user or effective group ID. The primary deviations from this rule are some of the system calls used to manipulate a process's user and group IDs. In general, the effective user or group ID for a process may be changed as long as the new ID is the same as either the real or the saved ID.

Taking all this into account, permanently dropping privileges involves ensuring that the effective, real, and saved IDs are all the same value. Temporarily dropping privileges requires that the effective and real IDs are the same value, and that the saved ID is unchanged so that the effective ID can later be restored to the higher privilege. These rules apply to both group and user IDs.

One more issue needs to be addressed with regard to dropping privileges. In addition to the effective, real, and saved group IDs of a process, a process also has ancillary groups. Ancillary groups are inherited by a process from its parent process, and they can only be altered by a process with superuser privileges. Therefore, if a process with superuser privileges is dropping these privileges, it must also be sure to drop any ancillary groups it may have. This is achieved by calling setgroups( ) with a single group, which is the real group ID for the process. Because the setgroups( )system call is guarded by requiring the effective user ID of the process to be that of the superuser, it must be done prior to dropping root privileges. Ancillary groups should be dropped regardless of whether privileges are being dropped permanently or temporarily. In the case of a temporary privilege drop, the process can restore the ancillary groups if necessary when elevated privileges are restored.

The first of two functions, spc_drop_privileges( ) drops any extra group or user privileges either permanently or temporarily, depending on the value of its only argument. If a nonzero value is passed, privileges will be dropped permanently; otherwise, the privilege drop is temporary. The second function, spc_restore_privileges( ) , restores privileges to what they were at the last call to spc_drop_privileges( ). If either function encounters any problems in attempting to perform its respective task, abort( ) is called, terminating the process immediately. If any manipulation of privileges cannot complete successfully, it's safest to assume that the process is in an unknown state, and you should not allow it to continue.

Recalling our earlier discussion regarding subtle differences in the semantics for changing a process's group and user IDs, you'll notice that spc_drop_privileges( ) is littered with preprocessor conditionals that test for the platform on which the code is being compiled. For the BSD-derived platforms (Darwin, FreeBSD, NetBSD, and OpenBSD), dropping privileges involves a simple call to setegid( ) or seteuid( ), followed by a call to either setgid( ) or setuid( ) if privileges are being permanently dropped. The setgid( ) and setuid( ) system calls adjust the process's saved group and user IDs, respectively, as well as the real group or user ID.

On Linux and Solaris, the setgid( ) and setuid( ) system calls do not alter the process's saved group and user IDs in all cases. (In particular, if the effective ID is not the superuser, the saved ID is not altered; otherwise, it is.). That means that these calls can't reliably be used to permanently drop privileges. Instead, setregid( ) and setreuid( ) are used, which actually simplifies the process except that these two system calls have different semantics on the BSD-derived platforms.

WARNING

As discussed above, always drop group privileges before dropping user privileges; otherwise, group privileges may not be able to be fully dropped.

#include <sys/param.h>

#include <sys/types.h>

#include <stdlib.h>

#include <unistd.h>

static int orig_ngroups = -1;

static gid_t orig_gid = -1;

static uid_t orig_uid = -1;

static gid_t orig_groups[NGROUPS_MAX];

void spc_drop_privileges(int permanent) {

gid_t newgid = getgid( ), oldgid = getegid( );

uid_t newuid = getuid( ), olduid = geteuid( );

if (!permanent) {

/* Save information about the privileges that are being dropped so that they

* can be restored later.

*/

orig_gid = oldgid;

orig_uid = olduid;

orig_ngroups = getgroups(NGROUPS_MAX, orig_groups);

}

/* If root privileges are to be dropped, be sure to pare down the ancillary

* groups for the process before doing anything else because the setgroups( )

* system call requires root privileges. Drop ancillary groups regardless of

* whether privileges are being dropped temporarily or permanently.

*/

if (!olduid) setgroups(1, &newgid);

if (newgid != oldgid) {

#if !defined(linux)

setegid(newgid);

if (permanent && setgid(newgid) = = -1) abort( );

#else

if (setregid((permanent ? newgid : -1), newgid) = = -1) abort( );

#endif

}

if (newuid != olduid) {

#if !defined(linux)

seteuid(newuid);

if (permanent && setuid(newuid) = = -1) abort( );

#else

if (setreuid((permanent ? newuid : -1), newuid) = = -1) abort( );

#endif

}

/* verify that the changes were successful */

if (permanent) {

if (newgid != oldgid && (setegid(oldgid) != -1 || getegid( ) != newgid))

abort( );

if (newuid != olduid && (seteuid(olduid) != -1 || geteuid( ) != newuid))

abort( );

} else {

if (newgid != oldgid && getegid( ) != newgid) abort( );

if (newuid != olduid && geteuid( ) != newuid) abort( );

}

}

void spc_restore_privileges(void) {

if (geteuid( ) != orig_uid)

if (seteuid(orig_uid) = = -1 || geteuid( ) != orig_uid) abort( );

if (getegid( ) != orig_gid)

if (setegid(orig_gid) = = -1 || getegid( ) != orig_gid) abort( );

if (!orig_uid)

setgroups(orig_ngroups, orig_groups);

}

See Also

§ "Setuid Demystified" by Hao Chen, David Wagner, and Drew Dean: http://www.cs.berkeley.edu/~daw/papers/setuid-usenix02.pdf

§ Recipe 2.1


[3] Linux further complicates the already complex privilege model by adding a filesystem user ID and a filesystem group ID, as well as POSIX capabilities. At this time, most systems do not actually make use of POSIX capabilities, and the filesystem IDs are primarily maintained automatically by the kernel. If the filesystem IDs are not explicitly modified by a process, they can be safely ignored, and they will behave properly. We won't discuss them any further here.

1.4. Limiting Risk with Privilege Separation

Problem

Your process runs with extra privileges granted by the setuid or setgid bits on the executable. Because it requires those privileges at various times throughout its lifetime, it can't permanently drop the extra privileges. You would like to limit the risk of those extra privileges being compromised in the event of an attack.

Solution

When your program first initializes, create a Unix domain socket pair using socketpair( ), which will create two endpoints of a connected unnamed socket. Fork the process using fork( ) , drop the extra privileges in the child process, and keep them in the parent process. Establish communication between the parent and child processes. Whenever the child process needs to perform an operation that requires the extra privileges held by the parent process, defer the operation to the parent.

The result is that the child performs the bulk of the program's work. The parent retains the extra privileges and does nothing except communicate with the child and perform privileged operations on its behalf.

If the privileged process opens files on behalf of the unprivileged process, you will need to use a Unix domain socket, as opposed to an anonymous pipe or some other other interprocess communication mechanism. The reason is that only Unix domain sockets provide a means by which file descriptors can be exchanged between the processes after the initial fork( ).

Discussion

In Recipe 1.3, we discussed setuid, setgid, and the importance of permanently dropping the extra privileges resulting from their use as quickly as possible to minimize the window of vulnerability to a privilege escalation attack. In many cases, the extra privileges are necessary for performing some initialization or other small amount of work, such as binding a socket to a privileged port. In other cases, however, the work requiring extra privileges cannot always be restricted to the beginning of the program, thus requiring that the extra privileges be dropped only temporarily so that they can later be restored when they're needed. Unfortunately, this means that an attacker who compromises the program can also restore those privileges.

Privilege separation

One way to solve this problem is to use privilege separation . When privilege separation is employed, one process is solely responsible for performing all privileged operations, and it does absolutely nothing else. A second process is responsible for performing the remainder of the program's work, which does not require any extra privileges. As illustrated in Figure 1-1, a bidirectional communications channel exists between the two processes to allow the unprivileged process to send requests to the privileged process and to receive the results.

Data flow when using privilege separation

Figure 1-1. Data flow when using privilege separation

Normally, the two processes are closely related. Usually they're the same program split during initialization into two separate processes using fork( ). The original process retains its privileges and enters a loop waiting to service requests from the child process. The child process starts by permanently dropping the extra privileges inherited from the parent process and continues normally, sending requests to the parent when it needs privileged operations to be performed.

By separating the process into privileged and unprivileged pieces, the risk of a privilege escalation attack is significantly reduced. The risk is further reduced by the parent process refusing to perform any operations that it knows the child does not need. For example, if the program never needs to delete any files, the privileged process should refuse to service any requests to delete files. Because the unprivileged child process undertakes most of the program's functionality, it stands the greatest risk of compromise by an attacker, but because it has no extra privileges of its own, an attacker does not stand to gain much from the compromise.

A privilege separation library: privman

NAI Labs has released a library that implements privilege separation on Unix with an easy-to-use API. This library, called privman , can be obtained from http://opensource.nailabs.com/privman/. As of this writing, the library is still in an alpha state and the API is subject to change, but it is quite usable, and it provides a good generic framework from which to work.

A program using privman should include the privman.h header file and link to the privman library. As part of the program's initialization, call the privman API function priv_init( ) , which requires a single argument specifying the name of the program. The program's name is used for log entries to syslog (see Recipe 13.11 for a discussion of logging), as well as for the configuration file to use. The priv_init( ) function should be called by the program with root privileges enabled, and it will take care of splitting the program into two processes and adjusting privileges for each half appropriately.

The privman library uses configuration files to determine what operations the privileged half of a program may perform on behalf of the unprivileged half of the same program. In addition, the configuration file determines what user the unprivileged half of the program runs as, and what directory is used in the call to chroot( ) in the unprivileged process (see Recipe 2.12). By default, privman runs the unprivileged process as the user "nobody" and does a chroot( ) to the root directory, but we strongly recommend that your program use a user specifically set up for it instead of "nobody", and that you chroot( ) to a safe directory (see Recipe 2.4).

When the priv_init( ) function returns control to your program, your code will be running in the unprivileged child process. The parent process retains its privileges, and control is never returned to you. Instead, the parent process remains in a loop that responds to requests from the unprivileged process to perform privileged operations.

The privman library provides a number of functions intended to replace standard C runtime functions for performing privileged operations. When these functions are called, a request is sent to the privileged process to perform the operation, the privileged process performs the operation, and the results are returned to the calling process. The privman versions of the standard functions are named with the prefix of priv_, but otherwise they have the same signature as the functions they replace.

For example, a call to fopen( ):

FILE *f = fopen("/etc/shadow", "r");

becomes a call to priv_fopen( ):

FILE *f = priv_fopen("/etc/shadow", "r");

The following code demonstrates calling priv_init( ) to initialize the privman library, which will split the program into privileged and unprivileged halves:

#include <privman.h>

#include <string.h>

int main(int argc, char *argv[ ]) {

char *progname;

/* Get the program name to pass to the priv_init( ) function, and call

* priv_init( ).

*/

if (!(progname = strrchr(argv[0], '/'))) progname = argv[0];

else progname++;

priv_init(progname);

/* Any code executed from here on out is running without any additional

* privileges afforded by the program running setuid root. This process

* is the child process created by the call in priv_init( ) to fork( ).

*/

return 0;

}

See Also

§ privman from NAI Labs: http://opensource.nailabs.com/privman/

§ Recipe 1.3, Recipe 1.7, Recipe 2.4, Recipe 2.12, Recipe 13.11

1.5. Managing File Descriptors Safely

Problem

When your program starts up, you want to make sure that only the standard stdin , stdout, and stderr file descriptors are open, thus avoiding denial of service attacks and avoiding having an attacker place untrusted files on special hardcoded file descriptors.

Solution

On Unix, use the function getdtablesize( ) to obtain the size of the process's file descriptor table. For each file descriptor in the process's table, close the descriptors that are not stdin, stdout, or stderr, which are always 0, 1, and 2, respectively. Test stdin, stdout, and stderr to ensure that they're open using fstat( ) for each descriptor. If any one is not open, open /dev/null and associate with the descriptor. If the program is running setuid, stdin, stdout, and stderr should also be closed if they're not associated with a tty, and reopened using /dev/null.

On Windows, there is no way to determine what file handles are open, but the same issue with open descriptors does not exist on Windows as it does on Unix.

Discussion

Normally, when a process is started, it inherits all open file descriptors from its parent. This can be a problem because the size of the file descriptor table on Unix is typically a fixed size. The parent process could therefore fill the file descriptor table with bogus files to deny your program any file handles for opening its own files. The result is essentially a denial of service for your program.

When a new file is opened, a descriptor is assigned using the first available entry in the process's file descriptor table. If stdin is not open, for example, the first file opened is assigned a file descriptor of 0, which is normally reserved for stdin. Similarly, if stdout is not open, file descriptor 1 is assigned next, followed by stderr's file descriptor of 2 if it is not open.

The only file descriptors that should remain open when your program starts are the stdin, stdout, and stderr descriptors. If the standard descriptors are not open, your program should open them using /dev/null and leave them open. Otherwise, calls to functions like printf( ) can have unexpected and potentially disastrous effects. Worse, the standard C library considers the standard descriptors to be special, and some functions expect stderr to be properly opened for writing error messages to. If your program opens a data file for writing and gets stderr's file descriptor, an error message written to stderr will destroy your data file.

WARNING

Particularly in a chroot( ) environment (see Recipe 2.12), the /dev/null device may not be available (it can be made available if the environment is set up properly). If it is not available, the proper thing for your program to do is to refuse to run.

The potential for security vulnerabilities arising from file descriptors being managed improperly is high in non-setuid programs. For setuid (especially setuid root) programs, the potential for problems increases dramatically. The problem is so serious that some variants of Unix (OpenBSD, in particular) will explicitly open stdin, stdout, and stderr from the execve( ) system call for a setuid process if they're not already open.

The following function, spc_sanitize_files( ) , first closes all open file descriptors that are not one of the standard descriptors. Because there is no easy way to tell whether a descriptor is open, close( ) is called for each one, and any error returned is ignored. Once all of the nonstandard descriptors are closed, stdin, stdout, and stderr are checked to ensure that they are open. If any one of them is not open, an attempt is made to open /dev/null. If /dev/null cannot be opened, the program is terminated immediately.

#include <sys/types.h>

#include <limits.h>

#include <sys/stat.h>

#include <stdio.h>

#include <unistd.h>

#include <errno.h>

#include <paths.h>

#ifndef OPEN_MAX

#define OPEN_MAX 256

#endif

static int open_devnull(int fd) {

FILE *f = 0;

if (!fd) f = freopen(_PATH_DEVNULL, "rb", stdin);

else if (fd = = 1) f = freopen(_PATH_DEVNULL, "wb", stdout);

else if (fd = = 2) f = freopen(_PATH_DEVNULL, "wb", stderr);

return (f && fileno(f) = = fd);

}

void spc_sanitize_files(void) {

int fd, fds;

struct stat st;

/* Make sure all open descriptors other than the standard ones are closed */

if ((fds = getdtablesize( )) = = -1) fds = OPEN_MAX;

for (fd = 3; fd < fds; fd++) close(fd);

/* Verify that the standard descriptors are open. If they're not, attempt to

* open them using /dev/null. If any are unsuccessful, abort.

*/

for (fd = 0; fd < 3; fd++)

if (fstat(fd, &st) = = -1 && (errno != EBADF || !open_devnull(fd))) abort( );

}

1.6. Creating a Child Process Securely

Problem

Your program needs to create a child process either to perform work within the same program or, more frequently, to execute another program.

Solution

On Unix, creating a child process is done by calling fork( ) . When fork( ) completes successfully, a nearly identical copy of the calling process is created as a new process. Most frequently, a new program is immediately executed using one of the exec*( ) family of functions (see Recipe 1.7). However, especially in the days before threading, it was common to use fork( ) to create separate "threads" of execution within a program.[4]

If the newly created process is going to continue running the same program, any pseudo-random number generators (PRNGs) must be reseeded so that the two processes will each yield different random data as they continue to execute. In addition, any inherited file descriptors that are not needed should be closed; they remain open in the other process because the new process only has a copy of them.

Finally, if the original process had extra privileges from being executed as setuid or setgid, those privileges will be inherited by the new process, and they should be dropped immediately if they are not needed. In particular, if the new process is going to be used to execute a new program, privileges should always be dropped so that the new program does not inherit privileges that it should not have.

Discussion

When fork( ) is used to create a new process, the new process is a nearly identical copy of the original process. The only differences in the processes are the process ID, the parent process ID, and the resource utilization counters, which are reset to zero in the new process. Execution in both processes continues immediately after the return from fork( ). Each process can determine whether it is the parent or the child by checking the return value from fork( ). In the parent or original process, fork( ) returns the process ID of the new process, while 0 will be returned in the child process.

It's important to remember that the new process is a copy of the original. The contents of the original process's memory (including stack), file descriptor table, and any other process attributes are the same in both processes, but they're not shared. Any changes to memory contents, file descriptors, and so on are private to the process that is making them. In other words, if the new process changes its file position pointer in an open file, the file position pointer for the same file in the original process remains unchanged.

The fact that the new process is a copy of the original has important security considerations that are often overlooked. For example, if a PRNG is seeded in the original process, it will be seeded identically in the child process. This means that if both the original and new processes were to obtain random data from the PRNG, they would both get the same random data (see Figure 1-2)! The solution to this problem is to reseed the PRNG in one of the processes, or, preferably, both processes. By reseeding the PRNG in both processes, neither process will have any knowledge of the other's PRNG state. Be sure to do this in a thread-safe manner if your program can fork multiple processes.

Consequences of not reseeding PRNGs after calling fork( )

Figure 1-2. Consequences of not reseeding PRNGs after calling fork( )

At the time of the call to fork( ), any open file descriptors in the original process will also be open in the new process. If any of these descriptors are unnecessary, they should be closed; they will remain open in the other process. Closing unnecessary file descriptors is especially important if one of the processes is going to execute another program (see Recipe 1.5).

Finally, the new process also inherits its access rights from the original process. Normally this is not an issue, but if the parent process had extra privileges because it was executed setuid or setgid, the new process will also have the extra privileges. If the new process does not need these privileges, they should be dropped immediately (see Recipe 1.3). Any extra privileges should be dropped especially if one of the two processes is going to execute a new program.

The following function, spc_fork( ) , is a wrapper around fork( ). As presented here, the code is incomplete when using an application-level random number generator; it will require the appropriate code to reseed whatever PRNG you're using. It assumes that the new child process is the process that will be used to perform any work that does not require any extra privileges that the process may have. It is rare that when a process is forked, the original process is used to execute another program or the new process is used to continue primary execution of the program. In other words, the new process is most often the worker process.

#include <sys/types.h>

#include <unistd.h>

pid_t spc_fork(void) {

pid_t childpid;

if ((childpid = fork( )) = = -1) return -1;

/* Reseed PRNGs in both the parent and the child */

/* See Chapter 11 for examples */

/* If this is the parent process, there's nothing more to do */

if (childpid != 0) return childpid;

/* This is the child process */

spc_sanitize_files( ); /* Close all open files. See Recipe 1.1 */

spc_drop_privileges(1); /* Permanently drop privileges. See Recipe 1.3 */

return 0;

}

See Also

Recipe 1.3, Recipe 1.5, Recipe 1.7


[4] Note that we say "program" here rather than "process." When fork( ) completes, the same program is running, but there are now two processes. The newly created process has a nearly identical copy of the original process, but it is a copy; any action performed in one process does not affect the other. In a threaded environment, each thread shares the same process, so all memory, file descriptors, signals, and so on are shared.

1.7. Executing External Programs Securely

Problem

Your Unix program needs to execute another program.

Solution

On Unix, one of the exec*( ) family of functions is used to replace the current program within a process with another program. Typically, when you're executing another program, the original program continues to run while the new program is executed, thus requiring two processes to achieve the desired effect. The exec*( ) functions do not create a new process. Instead, you must first use fork( ) to create a new process, and then use one of the exec*( ) functions in the new process to run the new program. See Recipe 1.6 for a discussion of using fork( ) securely.

Discussion

execve( ) is the system call used to load and begin execution of a new program. The other functions in the exec*( ) family are wrappers around the execve( ) system call, and they are implemented in user space in the standard C runtime library. When a new program is loaded and executed with execve( ), the new program replaces the old program within the same process. As part of the process of loading the new program, the old program's address space is replaced with a new address space. File descriptors that are marked to close on execute are closed; the new program inherits all others. All other system-level properties are tied to the process, so the new program inherits them from the old program. Such properties include the process ID, user IDs, group IDs, working and root directories, and signal mask.

Table 1-2 lists the various exec*( ) wrappers around the execve( ) system call. Note that many of these wrappers should not be used in secure code. In particular, never use the wrappers that are named with a "p" suffix because they will search the environment to locate the file to be executed. When executing external programs, you should always specify the full path to the file that you want to execute. If the PATH environment variable is used to locate the file, the file that is found to execute may not be the expected one.

Table 1-2. The exec*( ) family of functions

Function signature

Comments

int execl(const char *path, char *arg, ...);

The argument list is terminated by a NULL. The calling program's environment is passed on to the new program.

int execle(const char *path, char *arg, ...);

The argument list is terminated by a NULL, and the environment pointer to use follows immediately.

int execlp(const char *file, char *arg, ...);

The argument list is terminated by a NULL. The PATH environment variable is searched to locate the program to execute. The calling program's environment is passed on to the new program.

int exect(const char *path, const char *argv[ ], const char *envp[ ]);

The same as execve( ), except that process tracing is enabled.

int execv(const char *path, const char *argv[ ]);

The PATH environment variable is searched to locate the program to execute.

int execve(const char *path, const char *argv[ ], const char *envp[ ]);

This is the main system call to load and execute a new program.

int execvp(const char *file, const char *argv[ ]);

The PATH environment variable is searched to locate the program to execute. The calling program's environment is passed on to the new program.

The two easiest and safest functions to use are execv( ) and execve( ) ; the only difference between the two is that execv( ) calls execve( ), passing environ for the environment pointer. If you have already sanitized the environment (see Recipe 1.1), it's reasonable to call execv( ) without explicitly specifying an environment to use. Otherwise, a new environment can be built and passed to execve( ).

The argument lists for the functions are built just as they will be received by main( ). The first element of the array is the name of the program that is running, and the last element of the array must be a NULL. The environment is built in the same manner as described in Recipe 1.1. The first argument to the two functions is the full path and filename of the executable file to load and execute.

As a courtesy to the new program, before executing it you should close any file descriptors that are open unless there are descriptors that you intentionally want to pass along to it. Be sure to leave stdin, stdout, and stderr open. (See Recipe 1.5 for a discussion of file descriptors.)

Finally, if your program was executed setuid or setgid and the extra privileges have not yet been dropped, or they have been dropped only temporarily, you should drop them permanently before executing the new program. Otherwise, the new program will inherit the extra privileges when it should not. If you use the spc_fork( ) function from Recipe 1.6, the file descriptors and privileges will be handled for you.

Another function provided by the standard C runtime library for executing programs is system( ) . This function hides the details of calling fork( ) and the appropriate exec*( ) function to execute the program. There are two reasons why you should never use the system( ) function:

§ It uses the shell to launch the program.

§ It passes the command to execute to the shell, leaving the task of breaking up the command's arguments to the shell.

The system( ) function works differently from the exec*( ) functions; instead of replacing the currently executing program, it creates a new process with fork( ). The new process executes the shell with execve( ) while the original process waits for the new process to terminate. The system( )function therefore does not return control to the caller until the specified program has completed.

Yet another function, popen( ) , works somewhat similarly to system( ). It also uses the shell to launch the program, passing the command to execute to the shell and leaving the task of breaking up the command's arguments to the shell. What it does differently is create an anonymous pipe that is attached to either the new program's stdin or its stdout file descriptor. The new program's stderr file descriptor is always inherited from the parent. In addition, it returns control to the caller immediately with a FILE object connected to the created pipe so that the caller can communicate with the new program. When communication with the new program is finished, you should call pclose( ) to clean up the file descriptors and reap the child process created by the call to fork( ).

You should also avoid using popen( ) and its accompanying pclose( ) function, but popen( ) does have utility that is worth duplicating in a secure fashion. The following implementation with a similar API does not make use of the shell.

If you do wish to use either system( ) or popen( ), be extremely careful. First, make sure that the environment is properly set, so that there are no Trojan environment variables. Second, remember that the command you're running will be run in a Unix shell. This means that you must ensure that there is no way an attacker can pass malicious data to the shell command. If possible, pass in a fixed string that the attacker cannot manipulate. If the user must be allowed to manipulate the input, only very careful filtering will accomplish this securely. We recommend that you avoid this scenario at all costs.

The following code implements secure versions of popen( ) and pclose( ) using the spc_fork( ) code from Recipe 1.6. Our versions differ slightly in both interface and function, but not by too much.

The function spc_popen( ) requires the same arguments execve( ) does. In fact, the arguments are passed directly to execve( ) without any modification. If the operation is successful, an SPC_PIPE object is returned; otherwise, NULL is returned. When communication with the new program is complete, call spc_pclose( ), passing the SPC_PIPE object returned by spc_popen( ) as its only argument. If the new program has not yet terminated when spc_pclose( ) is called in the original program, the call will block until the new program does terminate.

If spc_popen( ) is successful, the SPC_PIPE object it returns contains two FILE objects:

§ read_fd can be used to read data written by the new program to its stdout file descriptor.

§ write_fd can be used to write data to the new program for reading from its stdin file descriptor.

Unlike popen( ), which in its most portable form is unidirectional, spc_popen( ) is bidirectional.

#include <stdio.h>

#include <errno.h>

#include <stdlib.h>

#include <sys/types.h>

#include <unistd.h>

#include <sys/wait.h>

typedef struct {

FILE *read_fd;

FILE *write_fd;

pid_t child_pid;

} SPC_PIPE;

SPC_PIPE *spc_popen(const char *path, char *const argv[], char *const envp[]) {

int stdin_pipe[2], stdout_pipe[2];

SPC_PIPE *p;

if (!(p = (SPC_PIPE *)malloc(sizeof(SPC_PIPE)))) return 0;

p->read_fd = p->write_fd = 0;

p->child_pid = -1;

if (pipe(stdin_pipe) = = -1) {

free(p);

return 0;

}

if (pipe(stdout_pipe) = = -1) {

close(stdin_pipe[1]);

close(stdin_pipe[0]);

free(p);

return 0;

}

if (!(p->read_fd = fdopen(stdout_pipe[0], "r"))) {

close(stdout_pipe[1]);

close(stdout_pipe[0]);

close(stdin_pipe[1]);

close(stdin_pipe[0]);

free(p);

return 0;

}

if (!(p->write_fd = fdopen(stdin_pipe[1], "w"))) {

fclose(p->read_fd);

close(stdout_pipe[1]);

close(stdin_pipe[1]);

close(stdin_pipe[0]);

free(p);

return 0;

}

if ((p->child_pid = spc_fork( )) = = -1) {

fclose(p->write_fd);

fclose(p->read_fd);

close(stdout_pipe[1]);

close(stdin_pipe[0]);

free(p);

return 0;

}

if (!p->child_pid) {

/* this is the child process */

close(stdout_pipe[0]);

close(stdin_pipe[1]);

if (stdin_pipe[0] != 0) {

dup2(stdin_pipe[0], 0);

close(stdin_pipe[0]);

}

if (stdout_pipe[1] != 1) {

dup2(stdout_pipe[1], 1);

close(stdout_pipe[1]);

}

execve(path, argv, envp);

exit(127);

}

close(stdout_pipe[1]);

close(stdin_pipe[0]);

return p;

}

int spc_pclose(SPC_PIPE *p) {

int status;

pid_t pid;

if (p->child_pid != -1) {

do {

pid = waitpid(p->child_pid, &status, 0);

} while (pid = = -1 && errno = = EINTR);

}

if (p->read_fd) fclose(p->read_fd);

if (p->write_fd) fclose(p->write_fd);

free(p);

if (pid != -1 && WIFEXITED(status)) return WEXITSTATUS(status);

else return (pid = = -1 ? -1 : 0);

}

See Also

Recipe 1.1, Recipe 1.5, Recipe 1.6

1.8. Executing External Programs Securely

Problem

Your Windows program needs to execute another program.

Solution

On Windows, use the CreateProcess( ) API function to load and execute a new program. Alternatively, use the CreateProcessAsUser( ) API function to load and execute a new program with a primary access token other than the one in use by the current program.

Discussion

The Win32 API provides several functions for executing new programs. In the days of the Win16 API, the proper way to execute a new program was to call WinExec( ) . While this function still exists in the Win32 API as a wrapper around CreateProcess( ) for compatibility reasons, its use is deprecated, and new programs should call CreateProcess( ) directly instead.

A powerful but extremely dangerous API function that is popular among developers is ShellExecute( ) . This function is implemented as a wrapper around CreateProcess( ), and it does exactly what we're about to advise against doing with CreateProcess( )—but we're getting a bit ahead of ourselves.

One of the reasons ShellExecute( ) is so popular is that virtually anything can be executed with the API. If the file to execute as passed to ShellExecute( ) is not actually executable, the API will search the registry looking for the right application to launch the file. For example, if you pass it a filename with a .TXT extension, the filename will probably start Notepad with the specified file loaded. While this can be an incredibly handy feature, it's also a disaster waiting to happen. Users can configure their own file associations, and there is no guarantee that you'll get the expected behavior when you execute a program this way. Another problem is that because users can configure their own file associations, an attacker can do so as well, causing your program to end up doing something completely unexpected and potentially disastrous.

The safest way to execute a new program is to use either CreateProcess( ) or CreateProcessAsUser( ). These two functions share a very similar signature:

BOOL CreateProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine,

LPSECURITY_ATTRIBUTES lpProcessAttributes,

LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles,

DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory,

LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);

BOOL CreateProcessAsUser(HANDLE hToken, LPCTSTR lpApplicationName,

LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes,

LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles,

DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory,

LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);

The two most important arguments for the purposes of proper secure use of CreateProcess( ) or CreateProcessAsUser( ) are lpApplicationName and lpCommandLine. All of the other arguments are well documented in the Microsoft Platform SDK.

lpApplicationName

Name of the program to execute. The program may be specified as an absolute or relative path, but you should never specify the program to execute in any way other than as a fully qualified absolute path and filename. This argument may also be specified as NULL, in which case the program to execute is determined from the lpCommandLine argument.

lpCommandLine

Any command-line arguments to pass to the new program. If there are no arguments to pass, this argument may be specified as NULL, but lpApplicationName and lpCommandLine cannot both be NULL. If lpApplicationName is specified as NULL, the program to execute is taken from this argument. Everything up to the first space is interpreted as part of the filename of the program to execute. If the filename to execute has a space in its name, it must be quoted. If lpApplicationName is not specified as NULL, lpCommandLine should not contain the filename to execute, but instead contain only the arguments to pass to the program on its command line.

By far, the biggest mistake that developers make when using CreateProcess( ) or CreateProcessAsUser( ) is to specify lpApplicationName as NULL and fail to enclose the program name portion of lpCommandLine in quotes. As a rule, you should never specify lpApplicationName as NULL. Always specify the filename of the program to execute in lpApplicationName rather than letting Windows try to figure out what you mean from lpCommandLine.

1.9. Disabling Memory Dumps in the Event of a Crash

Problem

Your application stores potentially sensitive data in memory, and you want to prevent this data from being written to disk if the program crashes, because local attackers might be able to examine a core dump and use that information nefariously.

Solution

On Unix systems, use setrlimit( ) to set the RLIMIT_CORE resource to zero, which will prevent the operating system from leaving behind a core file. On Windows, it is not possible to disable such behavior, but there is equally no guarantee that a memory dump will be performed. A system-wide setting that cannot be altered on a per-application basis controls what action Windows takes when an application crashes.

A Windows feature called Dr. Watson, which is enabled by default, may cause the contents of a process's address space to be written to disk in the event of a crash. If Microsoft Visual Studio is installed, the settings that normally cause Dr. Watson to run are changed to run the Microsoft Visual Studio debugger instead, and no dump will be generated. Other programs do similar things, so from system to system, there's no telling what might happen if an application crashes.

Unfortunately, there is no way to prevent memory dumps on a per-application basis on Windows. The settings for how to handle an application crash are system-wide, stored in the registry under HKEY_LOCAL_MACHINE, and they require Administrator access to change them. Even if you're reasonably certain Dr. Watson will be the handler on systems on which your program will be running, there is no way you can disable its functionality on a per-application basis. On the other hand, any dump that may be created by Dr. Watson is properly protected by ACLs that prevent any other user from accessing them.

Discussion

On most Unix systems, a program that crashes will " dump core." The action of dumping core causes an image of the program's committed memory at the time of the crash to be written out to a file on disk, which can later be used for post-mortem debugging.

The problem with dumping core is that the program may contain potentially sensitive information within its memory at the time the image is written to disk. Imagine a program that has just read in a user's password, and then is forced to dump core before it has a chance to erase or otherwise obfuscate the password in memory.

Because an attacker may be able to manipulate the program's runtime environment in such a way as to cause it to dump core, and thus write any sensitive information to disk, you should try to prevent a program from dumping core if there's any chance the attacker may be able to get read access to the core file.

Generally, core files are written in such a way that the owner is the only person who can read and modify them, but silly things often happen, such as lingering core files accidentally being made world-readable by a recursive permissions change.

It's best to prevent against core dumps as early in the program as possible, because if an attacker is manipulating the program in a way that causes it to crash, you cannot know in advance what state the program will be in when the attacker manages to force it to crash.

Process core dumping can be restricted on a per-application basis by using the resource limit capabilities of most Unix systems. One of the standard limits that can be applied to a process is the maximum core dump file size. This limit serves to protect against large (in terms of memory consumption) programs that dump core and could potentially fill up all available disk space. Without this limit in place, it would even be possible for an attacker who has discovered a way to cause a program to crash from remote and dump core to fill up all available disk space on the server. Setting the value of RLIMIT_CORE to 0 prevents the process from writing any memory dump to disk, instead simply terminating the program when a fatal problem is encountered.

#include <sys/types.h>

#include <sys/time.h>

#include <sys/resource.h>

void spc_limit_core(void) {

struct rlimit rlim;

rlim.rlim_cur = rlim.rlim_max = 0;

setrlimit(RLIMIT_CORE, &rlim);

}

TIP

In addition to the RLIMIT_CORE limit, the setrlimit( ) function also allows other per-process limits to be adjusted. We discuss these other limits in Recipe 13.9.

The advantage of disabling core dumps is that if your program has particularly sensitive information residing in memory unencrypted (even transient data is at risk, because a skilled attacker could potentially time the core dumps so that your program dumps core at precisely the right time), it will not ever write this data to disk in a core dump. The primary disadvantage of this approach is that the lack of a core file makes debugging program crashes very difficult after the fact. How big an issue this is depends on program deployment and how bugs are tracked and fixed. A number of shells provide an interface to the setrlimit( ) function via a built-in command. Users who want to prevent core file generation can set the appropriate limit with the shell command, then run the program.

However, for situations where data in memory is required to be protected, the application should limit the core dumps directly via setrlimit( ) so that it becomes impossible to inadvertently run the program with core dumps enabled. When core dumps are needed for debugging purposes, a safer alternative is to allow core dumps only when the program has been compiled in "debug mode." This is easily done by wrapping the setrlimit( ) call with the appropriate preprocessor conditional to disable the code in debug mode and enable it otherwise.

Some Unix variants (Solaris, for example) allow the system administrator to control how core dumps are handled on a system-wide basis. Some of the capabilities of these systems allow the administrator to specify a directory where all core dumps will be placed. When this capability is employed, the directory configured to hold the core dump files is typically owned by the superuser and made unreadable to any other users. In addition, most systems force the permissions of a core file so that it is only readable by the user the process was running as when it dumped core. However, this is not a very robust solution, as many other exploits could possibly be used to read this file.

See Also

Recipe 13.9