[ Team LiB ] |
1.2 Restricting Privileges on Windows1.2.1 ProblemYour 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. 1.2.2 SolutionWhen 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. 1.2.3 DiscussionTokens 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. 1.2.3.1 Creating restricted tokensUsing 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:
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:
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, calling SetThreadToken( ) 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); 1.2.3.2 Modifying a process's primary tokenBeginning 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:
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; } 1.2.3.3 Working with SID_AND_ATTRIBUTES structuresA 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 a SID_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. 1.2.3.4 Working with LUID_AND_ATTRIBUTES structuresAn 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 an LUID_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:
1.2.4 See Also |
[ Team LiB ] |