Chapter 7. Running with Least Privilege

There exists in the field of security the notion of always performing tasks with the least set of privileges required to perform those tasks. To cut a piece of plastic pipe, you could use a hacksaw or a chainsaw. Both will do the job, but the chainsaw is overkill. If you get things wrong, the chainsaw is probably going to destroy the pipe. The hacksaw will do the job perfectly well. The same applies to executable processes—they should run with no more privilege than is required to perform the task.

Running with least privilege also means using the elevated privileges for the shortest possible time. This reduces the window of exploit period. In Windows, you can enable privileges just prior to using them, perform the task requiring the privileges, and then disable the privileges. In the example above, you would not keep the elevated privilege, the chainsaw, running all the time in the kitchen! It’s dangerous!

Any serious software flaw, such as a buffer overrun, that can lead to security issues will do less damage if the compromised software is running with few privileges. Problems occur when users accidentally or unintentionally execute malicious code (for example, Trojans in e-mail attachments or code injection through a buffer overrun) that runs with the user’s elevated capabilities. For example, the process created when a Trojan is launched inherits all the capabilities of the caller. In addition, if the user is a member of the local Administrators group, the executed code can potentially have full system privileges and object access. The potential for damage is immense.

All too often, I review products that execute in the security context of an administrator account or, worse, as a service running as SYSTEM (the local system account). With a little thought and correct design, the product would not require such a privileged account. This chapter describes the reasons why development teams think they need to run their code under such privileged accounts and, more important, how to determine what privileges are required to execute code correctly and securely.

Important

Some applications do require administrative privilege to execute, including administration tools and tools that affect the operation of operating systems.

Before I discuss some of the technical aspects of least privilege, let’s look at what happens in the real world when you force your users to run your application as administrators or, worse, SYSTEM!

Least Privilege in the Real World

You can bury your head in the sand, but the Internet is full of bad guys out to get your users as your users employ applications created by you, and many of the attacks in the past would have failed if the programs were not running as elevated accounts. Presently, two of the more popular kinds of attacks on the Internet are viruses/Trojans and Web server defacements. I want to spend some time on each of these categories and explain how some common attacks could have been mitigated if the users had run their applications as plain users.

Viruses and Trojans

Viruses and Trojans both include malicious code unintentionally executed by users. Let’s look at some well-known malicious code; we’ll see how the code would have been foiled if the user executing the code were not an administrator.

Back Orifice

Back Orifice is a tool that, when installed on a computer, allows a remote attacker to, among other things, restart the computer, execute applications, and view file contents on the infected computer, all unbeknownst to the user. On installation, Back Orifice attempts to write to the Windows system directory and to a number of registry keys, including HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionRun. Only administrators can perform either of these tasks. If the user were not an administrator on the computer, Back Orifice would fail to install.

SubSeven

Similar to Back Orifice, SubSeven enables unauthorized attackers to access your computer over the Internet without your knowledge. To run, SubSeven creates a copy of itself in the Windows system directory, updates Win.ini and System.ini, and modifies registry service keys located in HKEY_LOCAL_MACHINE and HKEY_CLASSES_ROOT. Only administrators can perform these tasks. Once again, if the user were not an administrator, SubSeven would fail.

FunLove Virus

The FunLove virus, also called W32.FunLove.4099 by Symantec, uses a technique that was first used in the W32.Bolzano virus. When the virus is executed, it grants users access to all files by modifying the kernel access checking code on the infected computer. It does so by writing a file to the system directory and patching the Windows NT kernel, Ntoskrnl.exe. Unless the user is an administrator, FunLove cannot write to these files and fails.

ILoveYou Virus

Possibly the most famous of the viruses and Trojans, ILoveYou, also called VBS.Loveletter or The Love Bug, propagates itself using Microsoft Outlook. It operates by writing itself to the system directory and then attempts to update portions of HKEY_LOCAL_MACHINE in the registry. Once again, this malware will fail unless the user is an administrator.

Web Server Defacements

Web server defacing is a common pastime for script kiddies, especially defacing high-profile Web sites. A buffer overrun in the Internet Printing Protocol (IPP) functionality included in Microsoft Windows 2000 and exposed through Internet Information Services (IIS) allowed such delinquents to attack many IIS servers.

The real danger is the IPP handler, which is implemented as an Internet Server Application Programming Interface (ISAPI) extension, running as the SYSTEM account. The following text from the security bulletin issued by Microsoft, available at http://www.microsoft.com/technet/security/bulletin/MS01-023.asp, outlines the gravity of the vulnerability:

A security vulnerability results because the ISAPI extension contains an unchecked buffer in a section of code that handles input parameters. This could enable a remote attacker to conduct a buffer overrun attack and cause code of her choice to run on the server. Such code would run in the local system security context. This would give the attacker complete control of the server and would enable her to take virtually any action she chose.

If IPP were not running as the local system account, fewer Web sites would have been defaced. The local system account has full control of the computer, including the ability to write new Web pages.

Important

Running applications with elevated privileges and forcing your users to require such privileges is potentially dangerous at best and catastrophic at worst. Don’t force your application to run with dangerous privileges unless doing so is absolutely required.

With this history in mind, let’s take some time to look at access control and privileges in Windows before finally moving on to how to reduce the privileges your application requires.

Brief Overview of Access Control

Microsoft Windows NT, Windows 2000, Windows XP, and Windows .NET Server 2003 protect securable resources from unauthorized access by employing discretionary access control, which is implemented through discretionary access control lists (DACLs). DACLs, often abbreviated to ACLs, are a series of access control entries (ACEs). Each ACE lists a Security ID (SID)—which represents a user, a group, or a computer, often referred to as principals—and contains information about the principal and the operations that the principal can perform on the resource. Some principals might be granted read access, and others might have full control of the object protected by the ACL. Chapter 6, offers a more complete explanation of ACLs.

Brief Overview of Privileges

Windows user accounts have privileges, or rights, that allow or disallow certain privileged operations affecting an entire computer rather than specific objects. Examples of such privileges include the ability to log on to a computer, to debug programs belonging to other users, to change the system time, and so on. Some privileges are extremely potent; the most potent are defined in Table 7-1.

Keep in mind that privileges are local to a computer but can be distributed to many computers in a domain through Group Policy. It is possible that a user might have one set of privileges on one computer and a different set of privileges on another. Setting privileges for user accounts on your own computer by using the Local Policy option has no effect on the privilege policy of any other computer on the network.

Table 7-1. Some Potent Windows Privileges 

Display Name

Internal Name (Decimal)

#define (Winnt.h)

Backup Files And Directories

SeBackupPrivilege (16)

SE_BACKUP_NAME

Restore Files And Directories

SeRestorePrivilege (17)

SE_RESTORE_NAME

Act As Part Of The Operating System

SeTcbPrivilege (6)

SE_TCB_NAME

Debug Programs

SeDebugPrivilege (19)

SE_DEBUG_NAME

Replace A Process Level Token

SeAssignPrimaryToken­Privilege (2)

SE_ASSIGNPRIMARYTOKEN _NAME

Load And Unload Device Drivers

SeLoadDriverPrivilege (9)

SE_LOAD_DRIVER_NAME

Take Ownership Of Files Or Other Objects

SeTakeOwnershipPrivilege (8)

SE_TAKE_OWNERSHIP_NAME

Let’s look at the security ramifications of these privileges.

SeBackupPrivilege Issues

An account having the Backup Files And Directories privilege can read files the account would normally not have access to. For example, if a user named Blake wants to back up a file and the ACL on the file would normally deny Blake access, the fact that he has this privilege will allow him to read the file. A backup program reads files by setting the FILE_FLAG_BACKUP_SEMANTICS flag when calling CreateFile. Try for yourself by performing these steps:

  1. Log on as an account that has the backup privilege—for example, a local administrator or a backup operator.

  2. Create a small text file, named Test.txt, that contains some junk text.

  3. Using the ACL editor tool, add a deny ACE to the file to deny yourself access. For example, if your account name is Blake, add a Blake (Deny All) ACE.

  4. Compile and run the code that follows this list. Refer to MSDN at http://msdn.microsoft.com or the Platform SDK for details about the security-related functions.

    /*
      WOWAccess.cpp
    */
    #include <stdio.h>
    #include <windows.h>
    
    int EnablePriv (char *szPriv) {
        HANDLE hToken = 0;
    
        if (!OpenProcessToken(GetCurrentProcess(),
                              TOKEN_ADJUST_PRIVILEGES,
                              &hToken)) {
            printf("OpenProcessToken() failed -> %d", GetLastError());
            return -1;
        }
    
        TOKEN_PRIVILEGES newPrivs;
        if (!LookupPrivilegeValue (NULL, szPriv,
                                   &newPrivs.Privileges[0].Luid)) {
            printf("LookupPrivilegeValue() failed->%d", 
               GetLastError());
            CloseHandle (hToken);
            return -1;
        }
    
        newPrivs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
        newPrivs.PrivilegeCount = 1;
        
        if (!AdjustTokenPrivileges(hToken, FALSE, &newPrivs , 0, 
                NULL, NULL)) {
            printf("AdjustTokenPrivileges() failed->%d", 
               GetLastError());
            CloseHandle (hToken);
            return -1;
        }
    
        if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) 
            printf
    ("AdjustTokenPrivileges() succeeded, but not all privs set
    ");
    
        CloseHandle (hToken);
        return 0;
    }
    
    void DoIt(char *szFileName, DWORD dwFlags) {
    
        printf("
    
    Attempting to read %s, with 0x%x flags n",
               szFileName, dwFlags);
    
        HANDLE hFile = CreateFile(szFileName,
                                  GENERIC_READ, FILE_SHARE_READ,
                                  NULL, OPEN_EXISTING,
                                  dwFlags,
                                  NULL);
    
        if (hFile == INVALID_HANDLE_VALUE) {
            printf("CreateFile() failed->%d", 
               GetLastError());
            return;
        }
    
        char buff[128];
        DWORD cbRead=0, cbBuff = sizeof buff;
        ZeroMemory(buff, sizeof buff);
    
        if (ReadFile(hFile, buff, cbBuff, &cbRead, NULL)) {
            printf("Success, read %d bytes
    
    Text is: %s",
                   cbRead, buff);
        } else {
            printf("ReadFile() failed - > %d", GetLastError());
        }
        CloseHandle(hFile);
    }
    
    void main(int argc, char* argv[]) {
        if (argc < 2) {
           printf("Usage: %s <filename>", argv[0]);
           return;
        }
    
        //Need to enable backup priv first.
        if (EnablePriv(SE_BACKUP_NAME) == -1) 
            return;
    
        //Try with no backup flag -  should get access denied.
        DoIt(argv[1], FILE_ATTRIBUTE_NORMAL);
    
        //Try with backup flag - should work!
        DoIt(argv[1], FILE_ATTRIBUTE_NORMAL | 
                      FILE_FLAG_BACKUP_SEMANTICS);
    }

This sample code is also available with the book’s sample files in the folder Secureco2Chapter07. You should see output that looks like this:

Attempting to read Test.txt, with 0x80 flags
CreateFile() failed -> 5

Attempting to read Test.txt, with 0x2000080 flags
Success, read 15 bytes
Text is: Hello, Blake!

As you can see, the first call to CreateFile failed with an access denied error (error #5), and the second call succeeded because backup privilege was enabled and the backup flag was used.

In exploiting SeBackupPrivilege, I showed some custom code. However, if a user has both SeBackupPrivilege and SeRestorePrivilege, no custom code is needed. A user with these privileges can read any file on the system by launching NTBackup.exe, back up any file regardless of the file ACL, and then restore the file to an arbitrary location.

Assigning this user right can be a security risk. Since there is no way to be sure whether a user is backing up data legitimately or stealing data, assign this user right to trusted users only.

SeRestorePrivilege Issues

Obviously, this privilege is the inverse of the backup privilege. With this privilege, an attacker could overwrite files, including DLLs and EXEs, he would normally not have access to! The attacker could also change object ownership with this privilege, and the owner has full control of the object.

SeDebugPrivilege Issues

An account having the Debug Programs privilege can attach to any process and view and adjust its memory. Hence, if an application has some secret to protect, any user having this privilege and enough know-how can access the secret data by attaching a debugger to the process. You can find a good example of the risk this privilege poses in Chapter 9. A tool from nCipher (http://www.ncipher.com) can read the private key used for SSL/TLS communications by groveling through a process’s memory, but only if the attacker has this privilege.

The Debug Programs privilege also allows the caller to terminate any process on the computer through use of the TerminateProcess function call. In essence, a nonadministrator with this privilege can shut down a computer by terminating a critical system process, such as the Local Security Authority (LSA), Lsass.exe.

But wait, there’s more!

The most insidious possibility: an attacker with debug privileges can execute code in any running process by using the CreateRemoteThread function. This is how the LSADUMP2 tool, available at http://razor.bindview.com/tools, works. LSADUMP2 allows the user having this privilege to view secret data stored in the LSA by injecting a new thread into Lsass.exe to run code that reads private data after it has been decrypted by the LSA. Refer to Chapter 9 for more information about LSA secrets.

The best source of information about thread injection is Programming Applications for Microsoft Windows, by Jeffrey Richter (Microsoft Press).

Note

Contrary to popular belief, an account needs the Debug Programs privilege to attach to processes and debug them if the process is owned by another account. You do not require the privilege to debug processes owned by you. For example, Blake does not require the debug privilege to debug any application he owns, but he does need it to debug processes that belong to Cheryl.

SeTcbPrivilege Issues

An account having the Act As Part Of The Operating System privilege essentially behaves as a highly trusted system component. The privilege is also referred to as the Trusted Computing Base (TCB) privilege. TCB is the most trusted and hence most dangerous privilege in Windows. Because of this, the only account that has this privilege by default is SYSTEM.

Important

You should not grant an account the TCB privilege unless you have a really good reason. Hopefully, after you’ve read this chapter, you’ll realize that you do not need the privilege often.

Note

The most common reason developers claim they require the TCB privilege is so that they can call functions that require this privilege, such as LogonUser. Starting with Windows XP, LogonUser no longer requires this privilege if your application is calling to log on a Windows user account. This privilege is required, however, if you plan to use LogonUser to log on Passport account or if the GroupsSid parameter is not NULL.

SeAssignPrimaryTokenPrivilege and SeIncreaseQuotaPrivilege Issues

An account having the Replace A Process Level Token and Increase Quotas privileges can access a process token and then create a new process on behalf of the user of the other process. This can potentially lead to spoofing or privilege elevation attacks.

SeLoadDriverPrivilege Issues

Executable code that runs in the kernel is highly trusted and can perform just about any task possible. To load code into the kernel requires the SeLoadDriverPrivilege privilege because the code can perform so many potentially dangerous tasks. Therefore, assigning this privilege to untrusted users is not a great idea, and that’s why only administrators have this privilege by default.

Note that this privilege is not required to load Plug and Play drivers because the code is loaded by the Plug and Play service that runs as SYSTEM.

SeRemoteShutdownPrivilege Issues

I think it’s obvious what this privilege allows—the ability to shut down a remote computer. Note that, like all privileges, the user account in question must have this privilege enabled on the target computer. Imagine the fun an attacker could have if you gave the Everyone group this privilege on all computers in your network! Talk about distributed denial of service!

SeTakeOwnershipPrivilege Issues

The concept of object owners exists in Windows NT and later, and the owner always has full control of any object the account owns. An account that has this privilege can potentially take object ownership away from the original owner. The upshot of this is that an account with this privilege can potentially have total control of any object in the system.

More Information

Note that in versions of Windows earlier than Windows XP, an object created by a local administrator is owned by the local administrators group. In Windows XP and later versions, including Windows .NET Server 2003, this is configurable; the owner can be either the local Administrators group or the user account that created the object.

Note

The only privilege required by all user accounts is the Bypass Traverse Checking privilege, also referred to as SeChangeNotifyPrivilege. This privilege is required for a user to receive notifications of changes to files and directories. However, the main reason it’s required by default is that it also causes the system to bypass directory traversal access checks and is used as an NT File System (NTFS) optimization.

Brief Overview of Tokens

When a user logs on to a computer running Windows NT, Windows 2000, or Windows XP and the account is authenticated, a data structure called a token is created for the user by the operating system, and this token is applied to every process and thread within each process that the user starts up. The token contains, among other things, the user’s SID, one SID for each group the user belongs to, and a list of privileges held by the user. Essentially, it is the token that determines what capabilities a user has on the computer. A token is created only when a user is authenticated, either by logging on at a console, or over the network. Any adjustments made to an account, such as changing group membership or changing privileges, take effect only at the next logon.

Starting with Windows 2000, the token can also contain information about which SIDs and privileges are explicitly removed or disabled. Such a token is called a restricted token. I’ll explain how you can use restricted tokens in your applications later in this chapter.

How Tokens, Privileges, SIDs, ACLs, and Processes Relate

All processes in Windows NT, Windows 2000, and Windows XP run with some identity; in other words, a token is associated with the process. Normally, the process runs as the identity of the user who started the application. However, applications can be started as other user accounts through use of the CreateProcessAsUser function by a user who has the appropriate privileges. Typically, the process that calls the CreateProcessAsUser function must have the SeAssignPrimaryTokenPrivilege and SeIncreaseQuotaPrivilege privileges. However, if the token passed as the first argument is a restricted version of the caller’s primary token, the SeAssignPrimaryTokenPrivilege privilege is not required.

Another type of process, a service, runs with the identity defined in the Service Control Manager (SCM). By default, many services run as the local system account, but this can be configured to run as another account by entering the name and password for the account into the SCM, as shown in Figure 7-1.

Setting a service to run as a specified account in SCM.

Figure 7-1. Setting a service to run as a specified account in SCM.

More Information

Passwords used to start services are stored as LSA secrets. Refer to Chapter 9 for more information about LSA secrets.

Because the process has an account’s token associated with it and therefore has all the user’s group memberships and privileges, it can be thought of as a proxy for the account—anything the account can do, the process can do. This is true unless the token is neutered in some way on Windows 2000 and later by using the restricted token capability.

SIDs and Access Checks, Privileges and Privilege Checks

A token contains SIDs and privileges. The SIDs in a token are used to perform access checks against ACLs on resources, and the privileges in the token are used to perform specific machine-wide tasks. When I ask developers why they need to run their processes with elevated privileges, they usually comment, "We need to read and write to a portion of the registry." Little do they realize that this is actually an access check—it’s not a use of privileges! So why run with all those dangerous privileges enabled? Sometimes I hear, "Well, you have to run as administrator to run our backup tool." Backup is a privilege—it is not an ACL check.

If this section of the chapter hasn’t sunk in, please reread it. It’s vitally important that you understand the relationship between SIDs and privileges and how they differ.

Three Reasons Applications Require Elevated Privileges

Over the last couple of years, I have devoted many hours to working out why applications require administrative access to use, given that they are not administrative tools. And I think it’s safe to say there are only three reasons:

  • ACL issues

  • Privilege issue

  • Using LSA secrets

Let’s take a closer look at each in detail, and then I will outline some remedies.

ACL Issues

Imagine that a folder exists on an NTFS partition with the following ACL:

  • SYSTEM (Full Control)

  • Administrators (Full Control)

  • Everyone (Read)

Unless you are a privileged account, such as an administrator or the SYSTEM account (remember, many services run as system), the only operation you can perform in this folder is read files. You cannot write, you cannot delete, and you cannot do anything else. If your application tries to perform any file I/O other than read, it will receive an access denied error. Get used to it—access denied is error #5!

This is a very common issue. Applications that write data to protected areas of the file system or to other portions of the operating system such as the registry must be executed under an administrative account to operate correctly. How many games do you know that write high-score information to the C:Program Files directory? Let me answer that for you. Lots. And that’s a problem because it means the user playing the game must be an administrator. In other words, many games allow users to play one another over the Internet, which means they must open sockets; if there’s a buffer overrun or similar vulnerability in the game socket-handling code, an attacker could potentially run code using the vulnerability and the code would run as an admin. Game Over!

Opening Resources for GENERIC_ALL

There’s a subtle variation of the ACL issue—opening resources with more permission than is required. For example, imagine that the same ACL defined above exists on a file, and the code opens the file for GENERIC_ALL. Which account must the user be running in order for the code to not fail? Administrator or SYSTEM. GENERIC_ALL is the same as Full Control. In other words, you want to open the file and want to be able to do anything to the file. However, imagine your code only wants to read the file. Does it need to open the file for GENERIC_ALL? No, of course not. It can open the file for GENERIC_READ and any user running this application can successfully open the file because there is an Everyone (Read) ACE on the file. This is usability and security in harmony—usability in that the application works and performs its read-only operation, and security in that the application is only reading the file and can do no more, because of the read-only ACE.

Remember, in Windows NT and later an application is either granted the permissions it requests, or it gets an access denied error. If the application requests for all access, and the ACL on the resource only allows read access, the application will not be granted read access. It’ll get an access denied error instead.

You can attempt to open objects for the maximum allowed access by setting dwDesiredAccess to MAXIMUM_ALLOWED. However, you don’t know ahead of time what the result will be, so you will still have to handle errors.

Privilege Issue

If your account needs a specific privilege to get a job such as backing up files done, it is a simple fact that you need the privilege. However, be wary of having an administrator adding too many potentially dangerous privileges to user accounts, or requiring your users to have too many unneeded privileges. I have already explained the reasons why in detail earlier in this chapter.

Using LSA Secrets

The Local Security Authority (LSA) can store secret data on behalf of an application. The APIs for manipulating LSA secrets include LsaStorePrivateData and LsaRetrievePrivateData. Now here is the issue—to use LSA secrets, the process performing these tasks must be a member of the local administrators group. Note that the Platform SDK says about LsaStorePrivateData, "the data is encrypted before being stored, and the key has a DACL that allows only the creator and administrators to read the data." For all intents, only administrators can use these LSA functions, which is a problem if your application adopts the least privilege goal, and all you want to do is store some secret data for the user.

Solving the Elevated Privileges Issue

Now let’s look at some solutions to the three issues that require users to run their applications as elevated accounts.

Solving ACL Issues

There are three main solutions to getting out of the ACL doldrums:

  • Open resources for appropriate access.

  • Save user data to areas the user can write to.

  • Loosen ACLs.

The first is to open resources with the permissions you require and no more. If you want to read a key in the registry, request read-only access and no more. This is a simple thing to do and the chance of it causing regression errors in your application is slim.

The second solution is not to write user data to protected portions of the operating system. These portions include but are not limited to the HKEY_LOCAL_MACHINE hive, C:Program Files (or whatever directory the %PROGRAMFILES% environment variable points to on the computer),and the C:Windows directory (%SYSTEMROOT%). Instead, you should store user information in HKEY_CURRENT_USER and store user files in the user’s profile directory. You can determine the user’s profile directory with the following code snippet:

#include "shlobj.h"
...
TCHAR szPath[MAX_PATH];
...
if (SUCCEEDED(SHGetFolderPath(NULL, CSIDL_PERSONAL NULL, 0, szPath))  {
   HANDLE hFile = CreateFile(szPath, ...);
   ⋮
}

If the current version of your application stores user data in a part of the operating system accessible only by administrators, and you decide to move the data to an area where the user can safely store his or her own data without being an admin, you’ll need to provide a migration tool to migrate existing data. If you do not, you will have backward compatibility issues because users won’t be able to access their existing data.

Finally, you could loosen the ACLs a little, because downgrading an ACL may be less of a risk than requiring all users to be administrators. Obviously, you should do this with caution, as an insecure ACL could make the resource being protected open to attack. So don’t solve the least privilege issue and simply create an authorization issue.

Solving Privilege Issues

As I mentioned, if you need a privilege to get the job done, that’s just the way it has to be; there is no simple way around it. That said, do not go handing out privileges to all user accounts like candy, simply to get the job done! Frankly, there is no easy way to solve privilege issues.

Solving LSA Issues

There is a solution available to you in Windows 2000 and later, and it’s called the data protection API, or DPAPI. There are many good reasons for using DPAPI, but the most important one for solving our issues is that the application does not require the user to be an admin to access the secret data, and the data is protected using a key tied to the user, such that the owner of the data has access.

More Information

You can learn more about DPAPI and how to use it in Chapter 9.

A Process for Determining Appropriate Privilege

In Chapter 6, I commented that you must be able to account for each ACE in an ACL; the same applies to SIDs and privileges in a token. If your application requires that you run as an administrator, you need to vouch for each SID and privilege in the administrator’s token. If you cannot, you should consider removing some of the token entries.

Here’s a process you can use to help determine, based on the requirements of your application, whether each SID and privilege should be in a token:

  1. Find out each resource the application uses.

  2. Find out each privileged API the application calls.

  3. Evaluate the account under which the application is required to run.

  4. Ascertain the SIDs and privileges in the token.

  5. Determine which SIDs and privileges are required to perform the application tasks.

  6. Adjust the token to meet the requirements in the previous step.

Step 1: Find Resources Used by the Application

The first step is to draw up a list of all the resources used by the application: files, registry keys, Active Directory data, named pipes, sockets, and so on. You also need to establish what kind of access is required for each of these resources. For example, a sample Windows application that I’ll use to illustrate the privilege-determining process utilizes the resources described in Table 7-2.

Table 7-2. Resources Used by a Fictitious Application

Resource

Access Required

Configuration data

Administrators need full control, as they must configure the application. All other users can only read the data.

Incoming data on a named pipe

Everyone must use the pipe to read and write data.

The data directory that the application writes files to

Everyone can create files and do anything to their own data. Everyone can read other users’ files.

The program directory

Everyone can read and execute the application. Administrators can install updates.

Step 2: Find Privileged APIs Used by the Application

Analyze which, if any, privileged APIs are used by the application. Examples include those in Table 7-3.

Table 7-3. Windows Functions and Privileges Required

Function Name

Privilege or Group Membership Required

CreateFile with FILE_FLAG_BACKUP_SEMANTICS

SeBackupPrivilege

LogonUser

SeTcbPrivilege (Windows XP and Windows .NET Server 2003 no longer require this)

SetTokenInformation

SeTcbPrivilege

ExitWindowsEx

SeShutdownPrivilege

OpenEventLog using the security event log

SeSecurityPrivilege

BroadcastSystemMessage[Ex] to all desktops (BSM_ALLDESKTOPS)

SeTcbPrivilege

SendMessage and PostMessage across desktops

SeTcbPrivilege

RegisterLogonProcess

SeTcbPrivilege

InitiateSystemShutdown[Ex]

SeShutdownPrivilege or SeRemoteShutdownPrivilege

SetSystemPowerState

SeShutdownPrivilege

GetFileSecurity

SeSecurityPrivilege

Debug functions, when debugging a process running as a different account than the caller, including DebugActiveProcess and ReadProcessMemory

SeDebugPrivilege

CreateProcessAsUser

SeIncreaseQuotaPrivilege and usually ­SeAssignPrimaryTokenPrivilege

CreatePrivateObjectSecurityEx

SeSecurityPrivilege

SetSystemTime

SeSystemtimePrivilege

VirtualLock and AllocateUser­PhysicalPages

SeLockMemoryPrivilege

Net APIs such as NetUserAdd and NetLocalGroupDel

For many calls, caller must be a member of certain groups, such as Administrators or Account Operators.

NetJoinDomain

SeMachineAccountPrivilege

Note

Your application might call Windows functions indirectly through wrapper functions or COM interfaces. Make sure you take this into account.

In our sample Windows-based application, no privileged APIs are used. For most Windows-based applications, this is the case.

Step 3: Which Account Is Required?

Write down the account under which you require the application to run. For example, determine whether your application requires an administrator account to run or whether your service requires the local system account to run.

For our sample Windows application, development was lazy and determined that the application would work only if the user were an administrator. The testers were equally lazy and never tested the application under anything but an administrator account. The designers were equally to blame—they listened to development and the testers!

Step 4: Get the Token Contents

Next ascertain the SIDs and privileges in the token of the account determined above. You can do this either by logging on as the account you want to test or by using the RunAs command to start a new command shell. For example, if you require your application to run as an administrator, you could enter the following at the command line:

RunAs /user:MyMachineAdministrator cmd.exe

This would start a command shell as the administrator—assuming you know the administrator password—and any application started in that shell would also run as an administrator.

If you are an administrator and you want to run a shell as SYSTEM, you can use the task scheduler service command to schedule a task one minute in the future. For example, assuming the current time is 5:01 P.M. (17:01 using the 24-hour clock), the following will start a command shell no more than one minute in the future:

At 17:02 /INTERACTIVE "cmd.exe"

The newly created command shell runs in the local system account context.

Now that you are running as the account you are interested in, run the following test code, named MyToken.cpp, from within the context of the account you want to interrogate. This code will display various important information in the user’s token.

/*
  MyToken.cpp
*/
#define SECURITY_WIN32
#include "windows.h"
#include "security.h"
#include "strsafe.h"

#define MAX_NAME 256

// This function determines memory required
//  and allocates it. The memory must be freed by caller.
LPVOID AllocateTokenInfoBuffer(
    HANDLE hToken,
    TOKEN_INFORMATION_CLASS InfoClass,
    DWORD *dwSize) {

    *dwSize=0;
    GetTokenInformation(
        hToken,
        InfoClass,
        NULL,
        *dwSize, dwSize);

    return new BYTE[*dwSize];
}

// Get user name(s)
void GetUserNames() {
    EXTENDED_NAME_FORMAT enf[] = {NameDisplay,
                                  NameSamCompatible,NameUserPrincipal};
    for (int i=0; i < sizeof(enf) / sizeof(enf[0]); i++) {
        char szName[128];
        DWORD cbName = sizeof(szName);
        if (GetUserNameEx(enf[i],szName,&cbName))
            printf("Name (format %d): %s
",enf[i],szName);
    }
}

// Display SIDs and Restricting SIDs.
void GetAllSIDs(HANDLE hToken, TOKEN_INFORMATION_CLASS tic) {
    DWORD dwSize = 0;
    TOKEN_GROUPS *pSIDInfo = (PTOKEN_GROUPS)
        AllocateTokenInfoBuffer(
            hToken,
            tic,
            &dwSize);

    if (!pSIDInfo) return;

    if (!GetTokenInformation(hToken, tic, pSIDInfo, dwSize, &dwSize))
        printf("GetTokenInformation Error %u
", GetLastError());

    if (!pSIDInfo->GroupCount)
        printf("	None!
");

    for (DWORD i=0; i < pSIDInfo->GroupCount; i++) {
        SID_NAME_USE SidType;
        char lpName[MAX_NAME];
        char lpDomain[MAX_NAME];
        DWORD dwNameSize = MAX_NAME;
        DWORD dwDomainSize = MAX_NAME;
        DWORD dwAttr = 0;

        if (!LookupAccountSid( 
            NULL,                      
            pSIDInfo->Groups[i].Sid,
            lpName, &dwNameSize,
            lpDomain, &dwDomainSize,
            &SidType)) {

            if (GetLastError() == ERROR_NONE_MAPPED)
                StringCbCopy(lpName, sizeof(lpName), "NONE_MAPPED");
            else
                printf("LookupAccountSid Error %u
", GetLastError());
        } else 
            dwAttr = pSIDInfo->Groups[i].Attributes;

        printf("%12s\%-20s	%s
", 
               lpDomain, lpName, 
               (dwAttr & SE_GROUP_USE_FOR_DENY_ONLY) ? "[DENY]" : "");
    }

    delete [] (LPBYTE) pSIDInfo;
}

// Display privileges.
void GetPrivs(HANDLE hToken) {
    DWORD dwSize = 0;
    TOKEN_PRIVILEGES *pPrivileges = (PTOKEN_PRIVILEGES)
        AllocateTokenInfoBuffer(hToken,
        TokenPrivileges, &dwSize);

    if (!pPrivileges) return;

    BOOL bRes = GetTokenInformation(
               hToken,
               TokenPrivileges,
               pPrivileges,
               dwSize, &dwSize);

    if (FALSE == bRes)
        printf("GetTokenInformation failed
");

    for (DWORD i=0; i < pPrivileges- >PrivilegeCount; i++) {
        char szPrivilegeName[128];
        DWORD dwPrivilegeNameLength=sizeof(szPrivilegeName);
 
        if (LookupPrivilegeName(NULL,
            &pPrivileges->Privileges[i].Luid,
            szPrivilegeName,
            &dwPrivilegeNameLength))
            printf("	%s (%lu)
",
                   szPrivilegeName, 
                   pPrivileges->Privileges[i].Attributes);
        else
            printf("LookupPrivilegeName failed - %lu
", 
                   GetLastError());

    }

    delete [] (LPBYTE) pPrivileges;
}

int wmain( ) {
    if (!ImpersonateSelf(SecurityImpersonation)) {
        printf("ImpersonateSelf Error %u
", GetLastError());
        return -1;
    } 

    HANDLE hToken = NULL;
    if (!OpenProcessToken(GetCurrentProcess(),TOKEN_QUERY,&hToken)) {
        printf( "OpenThreadToken Error %u
", GetLastError());
        return -1;
    }

    printf("
User Name
");
    GetUserNames();

    printf("
SIDS
");
    GetAllSIDs(hToken,TokenGroups);

    printf("
Restricting SIDS
");
    GetAllSIDs(hToken,TokenRestrictedSids);
    
    printf("
Privileges
");    
    GetPrivs(hToken);

    RevertToSelf();

    CloseHandle(hToken);

    return 0;
}

You can also find this sample code with the book’s sample files in the folder Secureco2Chapter07. The code opens the current thread token and queries that token for the user’s name and the SIDs, restricting SIDs, and privileges in the thread. The GetUser, GetAllSIDs, and GetPrivs functions perform the main work. There are two versions of GetAllSIDs, one to get SIDs and the other to get restricting SIDs. Restricting SIDs are those SIDs in an optional list of SIDs added to an access token to limit a process’s or thread’s access to a level lower than that to which the user is allowed. I’ll discuss restricted tokens later in this chapter. A SID marked for deny, which I’ll discuss later, has the word [DENY] after the SID name.

Note

You need to impersonate the user before opening a thread token for interrogation. You do not need to perform this step if you call OpenProcessToken, however.

If you don’t want to go through the exercise of writing code to investigate token contents, you can use the Token Master tool, originally included with Programming Server-Side Applications for Microsoft Windows 2000 (Microsoft Press, 2000), by Jeff Richter and Jason Clark, and included on the CD accompanying this book. This tool allows you to log on to an account on the computer and investigate the token created by the operating system. It also lets you access a running process and explore its token contents. Figure 7-2 shows the tool in operation.

Spelunking the token of a copy of Cmd.exe running as SYSTEM.

Figure 7-2. Spelunking the token of a copy of Cmd.exe running as SYSTEM.

Scrolling through the Token Information field will give you a list of all SIDs and privileges in the token, as well as the user SID. For our sample application, the application is required to run as an administrator. The default contents of an administrator’s token include the following, as determined by MyToken.cpp:

User   NORTHWINDTRADERSlake
SIDS   NORTHWINDTRADERSDomain Users
                       Everyone
       BUILTINAdministrators
       BUILTINUsers
       NT AUTHORITYINTERACTIVE
       NT AUTHORITYAuthenticated Users

Restricting SIDS
    None

Privileges
    SeChangeNotifyPrivilege (3)
    SeSecurityPrivilege (0)
    SeBackupPrivilege (0)
    SeRestorePrivilege (0)
    SeSystemtimePrivilege (0)
    SeShutdownPrivilege (0)
    SeRemoteShutdownPrivilege (0)
    SeTakeOwnershipPrivilege (0)
    SeDebugPrivilege (0)
    SeSystemEnvironmentPrivilege (0)
    SeSystemProfilePrivilege (0)
    SeProfileSingleProcessPrivilege (0)
    SeIncreaseBasePriorityPrivilege (0)
    SeLoadDriverPrivilege (2)
    SeCreatePagefilePrivilege (0)
    SeIncreaseQuotaPrivilege (0)
    SeUndockPrivilege (2)
    SeManageVolumePrivilege (0) 

Note the numbers after the privilege names. This is a bitmap of the possible values described in Table 7-4.

Table 7-4. Privilege Attributes

Attribute

Value

Comments

SE_PRIVILEGE_USED_FOR_ACCESS

0x80000000

The privilege was used to gain access to an object.

SE_PRIVILEGE_ENABLED_BY_DEFAULT

0x00000001

The privilege is enabled by default.

SE_PRIVILEGE_ENABLED

0x00000002

The privilege is enabled.

Step 5: Are All the SIDs and Privileges Required?

Here’s the fun part: have members from the design, development, and test teams analyze each SID and privilege in the token and determine whether each is required. This task is performed by comparing the list of resources and used APIs found in steps 1 and 2 against the contents of the token from step 4. If SIDs or privileges in the token do not have corresponding requirements, you should consider removing them.

Note

Some SIDs are quite benign, such as Users and Everyone. You shouldn’t need to remove these from the token.

In our sample application, we find that the application is performing ACL checks only, not privilege checks, but the list of unused privileges is huge! If your application has a vulnerability that allows an attacker’s code to execute, it will do so with all these privileges. Of the privileges listed, the debug privilege is probably the most dangerous, for all the reasons listed earlier in this chapter.

Step 6: Adjust the Token

The final step is to reduce the token capabilities, which you can do in three ways:

  • Allow less-privileged accounts to run your application.

  • Use restricted tokens.

  • Permanently remove unneeded privileges.

Let’s look at each in detail.

Allow Less-Privileged Accounts to Run Your Application

You can allow less-privileged accounts to run your application but not allow them to perform certain features. For example, your application might allow users to perform 95 percent of the tasks in the product but not allow them to, say, perform backups.

Note

You can check whether the account using your application holds a required privilege at run time by calling the PrivilegeCheck function in Windows. If you perform privileged tasks, such as backup, you can then disable the backup option to prevent the user who does not hold the privilege from performing these tasks.

Important

If your application requires elevated privileges to run, you might have corporate adoption problems for your application. Large companies don’t like their users to run with anything but basic user capabilities. This is both a function of security and total cost of ownership. If a user can change parts of his systems because he has privilege to do so, he might get into trouble and require a call to the help desk. In short, elevated privilege requirements might be a deployment blocker for you.

One more aspect of running with least privilege exists: sometimes applications are poorly designed and require elevated privileges when they are not really needed. Often, the only way to rectify this sad state of affairs is to rearchitect the application.

I once reviewed a Web-based product that mandated that it run as SYSTEM. The product’s team claimed this was necessary because part of their tool allowed the administrator of the application to add new user accounts. The application was monolithic, which required the entire process to run as SYSTEM, not just the administration portion. As it turned out, the user account feature was rarely used. After a lengthy discussion, the team agreed to change the functionality in the next release. The team achieved this in the following ways:

  • By running the application as a predefined lesser-privileged account instead of as the local system account.

  • By making the application require that administrators authenticate themselves by using Windows authentication.

  • By making the application impersonate the user account and attempt to perform user account database operations. If the operating system denied access, the account was not an administrator!

The new application is simpler in design and leverages the operating system security, and the entire process runs with fewer privileges, thereby reducing the chance of damage in the event of a security compromise.

From a security perspective, there is no substitute for an application running as a low-privilege account. If a process runs as SYSTEM or some other high-privilege account and the process impersonates the user to "dumb down" the thread’s capabilities, an attacker might still be able to gain SYSTEM rights by injecting code, say through a buffer overrun, that calls RevertToSelf, at which point the thread stops impersonating and reverts to the process identity, SYSTEM. If an application always runs in a low-level account, RevertToSelf is less effective. A great example of this is in IIS 5. You should always run Web applications out of process (Medium and High isolation settings), which runs the application as the low-privilege IWAM_machinename account, rather than run the application in process with the Web server process (Low isolation setting), which runs as SYSTEM. In the first scenario, the potential damage caused by a buffer overrun is reduced because the process is a guest account, which can perform few privileged operations on the computer. Note also that in IIS 6 no user code runs as SYSTEM; therefore, your application will fail to run successfully if it relies on the Web server process using the SYSTEM identity.

Use Restricted Tokens

A new feature added to Windows 2000 and later is the ability to take a user token and "dumb it down," or restrict its capabilities. A restricted token is a primary or impersonation token that the CreateRestrictedToken function has modified. A process or thread running in the security context of a restricted token is restricted in its ability to access securable objects or perform privileged operations, and the thread can access only local resources. You can perform three operations on a token with this function to restrict the token:

  • Removing privileges from the token

  • Specifying a list of restricting SIDs

  • Applying the deny-only attribute to SIDs

Removing privileges

Removing privileges is straightforward; it simply removes any privileges you don’t want from the token, and they cannot be added back. To get the privileges back, the thread must be destroyed and re-created.

Specifying restricting SIDs

By adding restricting SIDs to the access token, you can decide which SIDs you will allow in the token. When a restricted process or thread attempts to access a securable object, the system performs access checks on both sets of SIDs: the enabled SIDs and the list of restricting SIDs. Both checks must succeed to allow access to the object.

Let’s look at an example of using restricting SIDs. An ACL on a file allows Everyone to read the file and Administrators to read, write, and delete the file. Your application does not delete files; in fact, it should not delete files. Deleting files is left to special administration tools also provided by your company. The user, Brian, is an administrator and a marketing manager. The token representing Brian has the following SIDs:

  • Everyone

  • Authenticated Users

  • Administrators

  • Marketing

Because your application does not perform any form of administrative function, you choose to incorporate a restricting SID made up of only the Everyone SID. When a user uses the application to manipulate the file, the application creates a restricted token. Brian attempts to delete the file by using the administration tool, so the operating system performs an access check by determining whether Brian has delete access based on the first set of SIDs. He does because he’s a member of the Administrators group and administrators have delete access to the file. However, the operating system then looks at the next set of SIDs, the restricting SIDs, and finds only the Everyone SID there. Because Everyone has only read access to the file, Brian is denied delete access to the file.

Note

The simplest way to think about a restricted SID is to think of ANDing the two SID lists and performing an access check on the result. Another way of thinking about it is to consider the access check being performed on the intersection of the two SID lists.

Applying a deny-only attribute to SIDs

Deny-only SIDs change a SID in the token such that it can be used only to deny the account access to a secured resource. It can never be used to allow access to an object. For example, a resource might have a Marketing (Deny All Access) ACE associated with it, and if the Marketing SID is in the token, the user is denied access. However, if another resource contains a Marketing (Allow Read) ACE and if the Marketing SID in the users’ token is marked for deny access, only the user will not be allowed to read the object.

I know it sounds horribly complex. Hopefully, Table 7-5 will clarify matters.

Table 7-5. Deny-Only SIDs and ACLs Demystified

 

Object ACL Contains Marketing (Allow Read) ACE

Object ACL Contains Marketing (Deny All Access) ACE

Object ACL Does Not Contain a Marketing ACE

User’s token includes Marketing SID

Allow access

Deny access

Access depends on the other ACEs on the object

User’s token includes the deny-only Marketing SID

Deny access

Deny access

Access depends on the other ACEs on the object

Note that simply removing a SID from a token can lead to a security issue, and that’s why the SIDs can be marked for deny-only. Imagine that an ACL on a resource denies Marketing access to the resource. If your code removes the Marketing SID from a user’s token, the user can magically access the resource! Therefore, the SIDs ought to be marked for deny-only, rather than having the SID removed.

When to Use Restricted Tokens

When deciding when to use a restricted token, consider these issues:

  • If you know a certain level of access is never needed by your application, you can mark those SIDs for deny-only. For example, screen savers should never need administrator access. So mark those SIDs for deny-only. In fact, this is what the screen savers in Windows 2000 and later do.

  • If you know the set of users and groups that are minimally necessary for access to resources used by your application, use restricted SIDs. For example, if Authenticated Users is sufficient for accessing the resources in question, use Authenticated Users for the restricted SID. This would prohibit rogue code running under this restricted token from accessing someone’s private profile data (such as cryptographic keys) because Authenticated Users is not on the ACL.

  • If your application loads arbitrary code, you should consider using a restricted token. Examples of this include e-mail programs (attachments) and instant messaging and chat programs (file transfer). If your application calls ShellExecute or CreateProcess on arbitrary files, you might want to consider using a restricted token.

Restricted Token Sample Code

Restricted tokens can be passed to CreateProcessAsUser to create a process that has restricted rights and privileges. These tokens can also be used in calls to ImpersonateLoggedOnUser or SetThreadToken, which lets the calling thread impersonate the security context of a logged-on user represented by a handle to the restricted token.

The following sample code outlines how to create a new restricted token based on the current process token. The token then has every privilege removed, with the exception of SeChangeNotifyPrivilege, which is required by all accounts in the system. The DISABLE_MAX_PRIVILEGE flag performs this step; however, you can create a list of privileges to delete if you want to remove specific privileges. Also, the local administrator’s SID is changed to a deny-only SID.

/*
  Restrict.cpp
*/
// Create a SID for the BUILTINAdministrators group.
BYTE sidBuffer[256];
PSID pAdminSID = (PSID)sidBuffer;
SID_IDENTIFIER_AUTHORITY SIDAuth = SECURITY_NT_AUTHORITY;

If (!AllocateAndInitializeSid( &SIDAuth, 2,
                            SECURITY_BUILTIN_DOMAIN_RID ,
                            DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0,
                            &pAdminSID) ) {
    printf( "AllocateAndInitializeSid Error %u
", GetLastError() );
    return -1;   
}

// Change the local administrator’s SID to a deny-only SID.
SID_AND_ATTRIBUTES SidToDisable[1];
SidToDisable[0].Sid = pAdminSID;
SidToDisable[0].Attributes = 0;

// Get the current process token.
HANDLE hOldToken = NULL;
if (!OpenProcessToken(
    GetCurrentProcess(),                   
    TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | 
    TOKEN_QUERY | TOKEN_ADJUST_DEFAULT, 
    &hOldToken)) { 
    printf("OpenProcessToken failed (%lu)
", GetLastError() );
    return -1; 
}

// Create restricted token from the process token.
HANDLE hNewToken = NULL;
if (!CreateRestrictedToken(hOldToken,
    DISABLE_MAX_PRIVILEGE, 
    1, SidToDisable, 
    0, NULL, 
    0, NULL, 
    &hNewToken)) {
    printf("CreateRestrictedToken failed (%lu)
", GetLastError() );
    return -1;
}

if (pAdminSID)
    FreeSid(pAdminSID);

// The following code creates a new process
// with the restricted token.
PROCESS_INFORMATION pi;
STARTUPINFO si;
ZeroMemory(&si, sizeof(STARTUPINFO) );
si.cb = sizeof(STARTUPINFO);
si.lpDesktop = NULL;

// Build the path to Cmd.exe to make sure
// we’re not running a Trojaned Cmd.exe.
char szSysDir[MAX_PATH+1];
if (GetSystemDirectory(szSysDir,MAX_PATH)) {
   char szCmd[MAX_PATH+1];
   if (StringCchCopy(szCmd,MAX_PATH,szSysDir) == S_OK &&
       StringCchCat(szCmd,MAX_PATH,"\") == S_OK &&
       StringCchCat(szCmd,MAX_PATH,"cmd.exe") == S_OK) {

          if(!CreateProcessAsUser( 
                  hNewToken,
                  szCmd, NULL,
                  NULL,NULL,
                  FALSE, CREATE_NEW_CONSOLE,
                  NULL, NULL,  
                  &si,&pi)) 
              printf("CreateProcessAsUser failed (%lu)
", 
                     GetLastError() );
     }
}

CloseHandle(hOldToken);
CloseHandle(hNewToken);
return 0;

Note

If a token contains a list of restricted SIDs, it is prevented from authenticating across the network as the user. You can use the IsTokenRestricted function to determine whether a token is restricted.

Important

Do not force STARTUPINFO.lpDesktop—NULL in Restrict.cpp—to winsta0\default. If you do and the user is using Terminal Server, the application will run on the physical console, not in the Terminal Server session that it ran from.

The complete code listing is available with the book’s sample files in the folder Secureco2Chapter07. The sample code creates a new instance of the command shell so that you can run other applications from within the shell to see the impact on other applications when they run in a reduced security context.

If you run this sample application and then view the process token by using the MyToken.cpp code that you can find on the companion CD, you get the following output. As you can see, the Administrators group SID has become a deny-only SID, and all privileges except SeChangeNotifyPrivilege have been removed.

User   NORTHWINDTRADERSlake
SIDS   NORTHWINDTRADERSDomain Users
       Everyone
       BUILTINAdministrators      [DENY]
       BUILTINUsers
       NT AUTHORITYINTERACTIVE
       NT AUTHORITYAuthenticated Users

Restricting SIDS
    None

Privileges
    SeChangeNotifyPrivilege (3) 

The following code starts a new process using a restricted token. You can do the same for an individual thread. The following code shows how to use a restricted token in a multithreaded application. The thread start function, ThreadFunc, removes all the privileges from the thread token, other than bypass traverse checking, and then calls DoThreadWork.

#include <windows.h>
DWORD WINAPI ThreadFunc(LPVOID lpParam) {
    DWORD dwErr = 0;

    try {
        if (!ImpersonateSelf(SecurityImpersonation))
            throw GetLastError();

        HANDLE hToken = NULL;
        HANDLE hThread = GetCurrentThread();
        if (!OpenThreadToken(hThread,
            TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | 
            TOKEN_QUERY | TOKEN_IMPERSONATE,
            TRUE,
            &hToken))
            throw GetLastError();

        HANDLE hNewToken = NULL;
        if (!CreateRestrictedToken(hToken,
            DISABLE_MAX_PRIVILEGE, 
            0, NULL, 
            0, NULL, 
            0, NULL, 
            &hNewToken))
            throw GetLastError();

        if (!SetThreadToken(&hThread, hNewToken))
            throw GetLastError();

        // DoThreadWork operates in restricted context.
        DoThreadWork(hNewToken);
      
    } catch(DWORD d) {
        dwErr = d;
    }

    if (dwErr == 0)
        RevertToSelf();

    return dwErr;
}

void main() {
    HANDLE h = CreateThread(NULL, 0,
                            (LPTHREAD_START_ROUTINE)ThreadFunc,
                            NULL, CREATE_SUSPENDED, NULL);
    if (h) 
        ResumeThread(h);
}

Software Restriction Policies and Windows XP

Windows XP includes new functionality, named Software Restriction Policies—also known as SAFER—to make restricted tokens easier to use and to deploy in applications. I want to focus on the programmatic aspects of SAFER rather than on its administrative features. You can learn more about SAFER administration in the Windows XP online Help by searching for Software Restriction Policies.

SAFER also includes some functions, declared in Winsafer.h, to make working with reduced privilege tokens easier. One such function is SaferComputeTokenFromLevel. This function is passed a token and can change the token to match predefined reduced levels of functionality.

The following sample code shows how you can create a new process to run as NormalUser, which runs as a nonadministrative, non-power-user account. This code is also available with the book’s sample files in the folder Secureco2Chapter07. After you run this code, run MyToken.cpp to see which SIDs and privileges are adjusted.

/*
  SAFER.cpp
*/
#include <windows.h>
#include <WinSafer.h>
#include <winnt.h>
#include <stdio.h>
#include <strsafe.h>

void main() {
    SAFER_LEVEL_HANDLE hAuthzLevel;

    // Valid programmatic SAFER levels:
    //  SAFER_LEVELID_FULLYTRUSTED
    //  SAFER_LEVELID_NORMALUSER
    //  SAFER_LEVELID_CONSTRAINED
    //  SAFER_LEVELID_UNTRUSTED
    //  SAFER_LEVELID_DISALLOWED

    // Create a normal user level.
    if (SaferCreateLevel(SAFER_SCOPEID_USER,
                         SAFER_LEVELID_NORMALUSER,
                         0, &hAuthzLevel, NULL)) {

        //  Generate the restricted token that we will use.
        HANDLE hToken = NULL;
        if (SaferComputeTokenFromLevel(
            hAuthzLevel,    // Safer Level handle
            NULL,           //  NULL is current thread token.
            &hToken,        // Target token
            0,              // No flags
            NULL)) {        // Reserved

            // Build the path to Cmd.exe to make sure
            // we’re not running a Trojaned Cmd.exe.
            char szPath[MAX_PATH+1], szSysDir[MAX_PATH+1];
            if (GetSystemDirectory(szSysDir, sizeof (szSysDir))) {
                StringCbPrintf(szPath,
                          sizeof (szPath),
                          "%s\cmd.exe",
                          szSysDir);
            
                STARTUPINFO si;
                ZeroMemory(&si, sizeof(STARTUPINFO));
                si.cb = sizeof(STARTUPINFO);
                si.lpDesktop = NULL;
            
                PROCESS_INFORMATION pi;
                if (!CreateProcessAsUser( 
                    hToken,
                    szPath, NULL,
                    NULL, NULL,
                    FALSE, CREATE_NEW_CONSOLE,
                    NULL, NULL,  
                    &si, &pi))
                    printf("CreateProcessAsUser failed (%lu)
",
                           GetLastError() );
            }

        }
        SaferCloseLevel(hAuthzLevel);
    }
}

Note

SAFER does much more than make it easier to create predefined tokens and run processes in a reduced context. Explaining the policy and deployment aspects of SAFER is beyond the scope of this book, a book about building secure applications, after all. However, even a well-written application can be subject to attack if it’s poorly deployed or administered. It is therefore imperative that the people deploying your application understand how to install and manage technologies, such as SAFER, in a robust and usable manner.

Permanently Removing Unneeded Privileges

During the Windows Security Push, we added new functionality to Windows .NET Server 2003 to remove privileges from a running application. This is a little different from the Software Restriction Policies, in that the new functionality removes privileges from the process’s primary token, not a duplicated thread. The advantage is that the privileges can never be used by the application, regardless of whether the code is used normally or is under attack.

Generally, the code to remove privileges is called early when the application starts up, and the following code is an example that removes two privileges from the process token.

// RemPriv
#ifndef SE_PRIVILEGE_REMOVED
#define SE_PRIVILEGE_REMOVED (0x00000004)
#endif

DWORD RemovePrivs(LPCTSTR szPrivs[], DWORD cPrivs) {
    HANDLE hProcessToken = NULL;

    if (!OpenProcessToken(GetCurrentProcess(),
                    TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
                    &hProcessToken))
        return GetLastError();
        
    DWORD cbBuff = sizeof TOKEN_PRIVILEGES + 
                (sizeof LUID_AND_ATTRIBUTES * cPrivs);
    char *pbBuff = new char[cbBuff];    
    PTOKEN_PRIVILEGES pTokPrivs = (PTOKEN_PRIVILEGES)pbBuff;
    
    // remove two privileges
    pTokPrivs->PrivilegeCount = cPrivs;

    for (DWORD i=0; i < cPrivs; i++) {
        LookupPrivilegeValue(NULL,szPrivs[i],
                &(pTokPrivs->Privileges[i].Luid));
        pTokPrivs->Privileges[i].Attributes = SE_PRIVILEGE_REMOVED;
    }
    
    // Remove the privileges
    BOOL fRet = AdjustTokenPrivileges(hProcessToken,
                                    FALSE,
                                    pTokPrivs,
                                    0,
                                    NULL,
                                    NULL);
    DWORD dwErr = GetLastError();

#ifdef _DEBUG                  
    printf("AdjustTokenPrivileges() -> %d
GetLastError() -> %d
",
                fRet,
                dwErr);          
#endif

    if (pbBuff) delete [] pbBuff;
    
    CloseHandle(hProcessToken);
    
    return dwErr;
}

int main(int argc, CHAR* argv[]) {
    LPCTSTR szPrivs[] = {SE_TAKE_OWNERSHIP_NAME, SE_DEBUG_NAME};
    if (RemovePrivs(szPrivs, 
        sizeof(szPrivs)/sizeof(szPrivs[0])) == 0) {
        //Cool! It worked
    }
}

If you are familiar with AdjustTokenPrivileges, you’ll realize that the only change is the addition of a new flag, SE_PRIVILEGE_REMOVED. The good news is that’s all there is to it! Remember, this is different from simply disabling a privilege, because the privilege is permanently removed from the instance of the token when the new option is used. Removing privileges from your process token will only affect your process, and not other processes running under the same account.

If you have created a service designed to work with Windows .NET Server 2003, and you know that the code never uses certain privileges, you should use code like this to remove the unneeded privileges. You should wrap the code in call to GetVersionEx to determine the operating system, since this code runs on Windows .NET Server 2003 and later.

For example, in Windows .NET Server 2003, the LSA process (LSASS.EXE) removes the following privileges because they are not required by the process when performing its operating system tasks:

  • SeTakeOwnershipPrivilege

  • SeCreatePagefilePrivilege

  • SeLockMemoryPrivilege

  • SeAssignPrimaryTokenPrivilege

  • SeIncreaseQuotaPrivilege

  • SeIncreaseBasePriorityPrivilege

  • SeCreatePermanentPrivilege

  • SeSystemEnvironmentPrivilege

  • SeUndockPrivilege

  • SeLoadDriverPrivilege

  • SeProfileSingleProcessPrivilege

  • SeManageVolumePrivilege

The Smartcard service also disables the following unnecessary privileges:

  • SeSecurityPrivilege

  • SeSystemtimePrivilege

  • SeDebugPrivilege

  • SeShutdownPrivilege

  • SeUndockPrivilege

Some components have gone so far as to simply remove all privileges but SeChangeNotifyPrivilege, which is required by NTFS. The following code will achieve this goal:

/*
    JettisonPrivs.cpp
*/

#ifndef SE_PRIVILEGE_REMOVED
#    define SE_PRIVILEGE_REMOVED (0x00000004)
#endif

#define SAME_LUID(luid1,luid2) 
    (luid1.LowPart == luid2.LowPart && 
    luid1.HighPart == luid2.HighPart)

DWORD JettisonPrivs() {
    DWORD  dwError = 0;
    VOID*  TokenInfo = NULL;

    try {
        HANDLE hToken = NULL;
        if (!OpenProcessToken(
            GetCurrentProcess(),
            TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES,
            &hToken)) 
                throw GetLastError();

        DWORD dwSize=0;
        if (!GetTokenInformation(
            hToken,
            TokenPrivileges,
            NULL, 0,
            &dwSize)) {

            dwError = GetLastError();
            if (dwError != ERROR_INSUFFICIENT_BUFFER)
                throw dwError;
        }

        TokenInfo = new char[dwSize];

        if (NULL == TokenInfo)
            throw ERROR_NOT_ENOUGH_MEMORY;

        if (!GetTokenInformation(
            hToken,
            TokenPrivileges,
            TokenInfo, dwSize,
            &dwSize))
                throw GetLastError();

        TOKEN_PRIVILEGES* pTokenPrivs = (TOKEN_PRIVILEGES*) TokenInfo;

        // don’t remove this priv
        LUID luidChangeNotify;
        LookupPrivilegeValue(NULL,SE_CHANGE_NOTIFY_NAME,
                             &luidChangeNotify);

        for (DWORD dwIndex = 0; 
                   dwIndex < pTokenPrivs->PrivilegeCount; 
                   dwIndex++)
            if (!SAME_LUID (pTokenPrivs->Privileges[dwIndex].Luid,
                         luidChangeNotify)) 
                pTokenPrivs->Privileges[dwIndex].Attributes = 
                      SE_PRIVILEGE_REMOVED;

        if (!AdjustTokenPrivileges(
            hToken,
            FALSE,
            pTokenPrivs, dwSize,
            NULL, NULL))
                throw GetLastError();
    } catch (DWORD err) {
        dwError = err;
    }

    if (TokenInfo) 
        delete [] TokenInfo;

    return dwError;
}

Low-Privilege Service Accounts in Windows XP and Windows .NET Server 2003

Traditionally, Windows services have had the choice of running under either the local system security context or under some arbitrary user account. Creating user accounts for each service is unwieldy at best. Because of this, nearly all local services are configured to run as SYSTEM. The problem with this is that the local system account is highly privileged—it has SeTcbPrivilege, the SYSTEM SID, and Local Administrators SID, among others—and breaking into the service is often an easy way to achieve a privilege elevation attack.

Many services don’t need an elevated privilege level; hence the need for a lower privilege–level security context available on all systems. Windows XP introduces two new service accounts:

  • The local service account (NT AUTHORITYLocalService)

  • The network service account (NT AUTHORITYNetworkService)

The local service account has minimal privileges on the computer and acts as the anonymous user account when accessing network-based resources. The network service account also has minimal privileges on the computer; however, it acts as the computer account when accessing network-based resources.

For example, if your service runs on a computer named BlakeLaptop as the local Service account and accesses, say, a file on a remote computer, you’ll see the anonymous user account (not to be confused with the guest account) attempt to access the resource. In many cases, unauthenticated access (that is, anonymous access) is disallowed, and the request for the network-based file will fail. If your service runs as the network service account on BlakeLaptop and accesses the same file on the same remote computer, you’ll see an account named BLAKELAPTOP$ attempt to access the file.

Note

Remember that in Windows 2000 and later a computer in a domain is an authenticated entity, and its name is the machine name with a $ appended. You can use ACLs to allow and disallow computers access to your resources just as you can allow and disallow normal users access.

Table 7-6 shows which privileges are associated with each service account in Windows .NET Server 2003.

Table 7-6. Well-Known Service Accounts and Their Default Privileges

Privilege

Local System

Local Service

Network Service

SeCreateTokenPrivilege

X

  

SeAssignPrimaryTokenPrivilege

X

X

X

SeLockMemoryPrivilege

X

  

SeIncreaseQuotaPrivilege

X

  

SeMachineAccountPrivilege

   

SeTcbPrivilege

X

  

SeSecurityPrivilege

X

X

X

SeTakeOwnershipPrivilege

X

  

SeLoadDriverPrivilege

X

  

SeSystemProfilePrivilege

   

SeSystemtimePrivilege

X

X

X

SeProfileSingleProcessPrivilege

X

  

SeIncreaseBasePriorityPrivilege

X

  

SeCreatePagefilePrivilege

X

  

SeCreatePermanentPrivilege

X

  

SeBackupPrivilege

X

  

SeRestorePrivilege

X

  

SeShutdownPrivilege

X

  

SeDebugPrivilege

X

  

SeAuditPrivilege

X

X

X

SeSystemEnvironmentPrivilege

X

  

SeChangeNotifyPrivilege

X

X

X

SeRemoteShutdownPrivilege

   

SeUndockPrivilege

X

X

X

SeSyncAgentPrivilege

   

SeEnableDelegationPrivilege

   

As you can see, the local system account is bristling with privileges, some of which you will not need for your service to run. So why use this account? Remember that the big difference between the two new service accounts is that the network service account can access networked resources as the computer identity. The local service account can access networked resources as the anonymous user account, which, in secure environments where anonymous access is disallowed, will fail.

Important

If your service currently runs as the local system account, perform the analysis outlined in "A Process for Determining Appropriate Privilege" earlier in this chapter and consider moving the service account to the less-privileged network service or local service accounts.

The Impersonate Privilege and Windows .NET Server 2003

The impersonation model works really well with the trusted subsystem model—the server is all-powerful and controls access to all resources it owns. However, what we are seeing now is a factored model, where the server is not all-powerful and does not own the resources—they belong to the next server in the chain. Because it is possible for a not-so-trusted server to impersonate a highly privileged account and potentially become that account, we added a new privilege to Windows .NET Server 2003—SeImpersonatePrivilege. The details of the new impersonate privilege are shown in Table 7-7.

Table 7-7. The Impersonate Privilege

#define

Name

Value

SE_IMPERSONATE_NAME

SeImpersonatePrivilege

29L

By default, a process with the following SIDs in the token has this privilege:

  • SYSTEM

  • Administrators

  • Service

The Everyone account does not have this privilege, while the Service account has this privilege because it is very common for services to impersonate users. Installing a new service requires the user be a trusted account, such as an administrator.

You should test your application thoroughly if it uses impersonation.

Note that this privilege only applies when quality of security is set to impersonate or delegate (for example, RPC_C_IMP_LEVEL_IMPERSONATE and RPC_C_IMP_LEVEL_DELEGATE). It is not enforced for anonymous or identify (for example, RPC_C_IMP_LEVEL_ANONYMOUS and RPC_C_IMP_LEVEL_IDENTIFY). In addition, your code can always impersonate the process identity whether the account has this privilege or not. In other words, you can always impersonate yourself.

Debugging Least-Privilege Issues

You might be wondering why I’m adding a debugging section to a book about good security design and coding practices. Developers and testers often balk at running their applications with least privilege because working out why an application fails can be difficult. This section covers some of the best ways to debug applications that fail to operate correctly when running as a lower-privilege account, such as a general user and not as an administrator.

People run applications with elevated privileges for two reasons:

  • The code runs fine on Windows 95, Windows 98, and Windows Me but fails mysteriously on Windows NT and later unless the user is an administrator.

  • Designing, writing, testing, and debugging applications can be difficult and time-consuming.

Let me give you some background. Before Microsoft released Windows XP, I spent some time with the application compatibility team helping them determine why applications failed when they were not run as an administrator. The problem was that many applications were designed to run on Windows 95, Windows 98, and Windows Me. Because these operating systems do not support security capabilities such as ACLs and privileges, applications did not need to take security failures into account. It’s not uncommon to see an application simply fail in a mysterious way when it runs as a user and not as an administrator because the application never accounts for access denied errors.

Why Applications Fail as a Normal User

Many applications designed for Windows 95, Windows 98 and Windows Me do not take into consideration that they might run in a more secure environment such as Windows NT, Windows 2000, or Windows XP. As I have already discussed, these applications fail because of privilege failures and ACL failures. The primary ACL failure culprit is the file system, followed by the registry. In addition, applications might fail in various ways and give no indication that the failure stems from a security error, because they were never tested on a secure platform in the first place.

For example, a popular word processor we tested yielded an Unable To Load error when the application ran as a normal user but worked flawlessly as an administrator. Further investigation showed that the application failed because it was denied access to write to a registry key. Another example: a popular shoot-’em-up game ran perfectly on Windows Me but failed in Windows XP unless the user was logged on as a local administrator. Most disconcerting was the Out Of Memory error we saw. This led us to spend hours debugging the wrong stuff until finally we contacted the vendor, who informed us that if all error-causing possibilities are exhausted, the problem must be a lack of memory! This was not the case—the error was an access denied error while attempting to write to the c:Program Files directory. Many other applications simply failed with somewhat misleading errors or access violations.

Important

Make sure your application handles security failures gracefully by using good, useful error messages. Your efforts will make your users happy.

How to Determine Why Applications Fail

Three tools are useful in determining why applications fail for security reasons:

The Windows Event Viewer

The Windows Event Viewer will display security errors if the developer or tester elects to audit for specific security categories. It is recommended that you audit for failed and successful use of privileges. This will help determine whether the application has attempted to use a privilege available only to higher-privileged accounts. For example, it is not unreasonable to expect a backup program to require backup privileges, which are not available to most users. You can set audit policy by performing the following steps in Windows XP. (You can follow similar steps in Windows 2000.)

  1. Open Mmc.exe.

  2. In the Console1 dialog box, select File and then select Add/Remove Snap-In.

  3. In the Add/Remove Snap-In dialog box, click Add to display the Add Standalone Snap-In dialog box.

  4. Select the Group Policy snap-in, and click Add.

  5. In the Select Group Policy Object dialog box, click Finish. (The Group Policy object should default to Local Computer.)

  6. Close the Add Standalone Snap-In dialog box.

  7. Click OK to close the Add/Remove snap-in.

  8. Navigate to Local Computer Policy, Computer Configuration, Windows Settings, Security Settings, Local Policies, Audit Policy.

  9. Double-click Audit Privilege Use to open the Audit Privilege Use Properties dialog box.

  10. Select the Success and Failure check boxes, and click OK.

  11. Exit the tool. (Note that it might take a few seconds for the new audit policy to take effect.)

When you run the application and it fails, take a look at the security section of the Windows event log to look for events that look like this:

Event Type:      Failure Audit
Event Source:      Security
Event Category:   Privilege Use 
Event ID:      578
Date:         5/21/2002
Time:         10:15:00 AM
User:         NORTHWINDTRADERSlake
Computer:      CHERYL-LAP
Description:
Privileged object operation:
    Object Server:   Security
    Object Handle:   0
    Process ID:   444
    Primary User Name:BLAKE-LAP$
    Primary Domain:   NORTHWINDTRADERS
    Primary Logon ID:   (0x0,0x3E7)
    Client User Name:   blake
    Client Domain:   NORTHWINDTRADERS
    Client Logon ID:   (0x0,0x485A5)
    Privileges:      SeShutdownPrivilege

In this example, Blake is attempting to do some task that uses shutdown privilege. Perhaps this is why the application is failing.

RegMon and FileMon

Many failures occur because of ACL checks failing in the registry or the file system. These failures can be determined by using RegMon and FileMon, two superb tools from http://www.sysinternals.com. Both these tools display ACCDENIED errors when the process attempts to use the registry or the file system in an inappropriate manner for that user account—for example, a user account attempting to write to a registry key when the key is updatable only by administrators.

No security file access issues exist when the hard drive is using FAT or FAT32. If the application fails on NTFS but works on FAT, the chances are good that the failure stems from an ACL conflict, and FileMon can pinpoint the failure. But you’re not using FAT, right? Because you care about security! GetFileSecurity and SetFileSecurity succeed on FAT, but they are essentially no-ops. Depending on your application, you might want to warn the user if she chooses to install onto a FAT partition.

Note

Both RegMon and FileMon allow you to filter the tool’s output based on the name of the application being assessed. You should use this option because the tools can generate volumes of data!

The flowcharts in Figure 7-3 through Figure 7-5 illustrate how to evaluate failures caused by running with reduced privileges.

Important

From a security perspective, there is no substitute for an application operating at least privilege. This includes not requiring that applications run as an administrator or SYSTEM account when performing day-to-day tasks. Ignore this advice at your peril.

Investigating a potential privilege failure.

Figure 7-3. Investigating a potential privilege failure.

Investigating a potential registry access failure.

Figure 7-4. Investigating a potential registry access failure.

Investigating a potential file access failure.

Figure 7-5. Investigating a potential file access failure.

Summary

In my opinion, the principle of least privilege is the most powerful security tenet because an application that runs with minimal privileges can do very little more than it is ordinarily tasked to do. Remember that a secure application is one that does what it is supposed to do and no more. However, overcoming the hurdles of building a least-privilege application can be complex—I often call it the "Challenge of Least Privilege" because of the effort required.

Don’t fall into the bad habit of simply running services as SYSTEM and requiring that users be admins to use your application. If you do, not only are you leaving your clients open to serious consequences if they are compromised, but also as time passes by and you add more code to the system, it will become harder to run the application with reduced, and safer, privileges. And when you do take the plunge and run with reduced privileges, chances are good that you will break some older capability that will prevent users from getting their jobs done.

So get it right from the start: design, build, and test for least privilege, and document the privilege requirements for your applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.9.147