Your Windows program runs with elevated privileges, such as Administrator or Local System, but it does not require all the privileges granted to the user account under which it’s running. Your program never needs to perform certain actions that may be dangerous if users with elevated privileges run it and an attacker manages to compromise the program.
When a user logs into the system or the service control manager starts a service, a token is created that contains information about the user logging in or the user under which the service is running. The token contains a list of all of the groups to which the user belongs (the user and each group in the list is represented by a Security ID or SID), as well as a set of privileges that any thread running with the token has. The set of privileges is initialized from the privileges assigned by the system administrator to the user and the groups to which the user belongs.
Beginning with Windows 2000, it is
possible to create a restricted token and force threads to run
using that token. Once a restricted token has been applied to a
running thread, any restrictions imposed by the restricted token
cannot be lifted; however, it is possible to revert the thread back
to its original unrestricted token. With restricted tokens,
it’s possible to remove privileges, restrict the
SIDs that are used in access checking, and deny SIDs access. The use
of restricted tokens is more useful when combined with the
CreateProcessAsUser(
)
API to create a new process with a restricted
token that cannot be reverted to a more permissive token.
Beginning with Windows .NET Server 2003, it is possible to permanently remove privileges from a process’s token. Once the privileges have been removed, they cannot be added back. Any new processes created by a process running with a modified token will inherit the modified token; therefore, the same restrictions imposed upon the parent process are also imposed upon the child process. Note that modifying a token is quite different from creating a restricted token. In particular, only privileges can be removed; SIDs can be neither restricted nor denied.
Tokens contain a list of SIDs, composed
of the user’s SID and one SID for each group of
which the user is a member. SIDs are assigned by the system
when users and groups are created. In addition to the SIDs, tokens
also contain a list of restricted SIDs. When access checks are
performed and the token contains a list of restricted SIDs, the
intersection of the two lists of SIDs contained in the token is used
to perform the access check. Finally, tokens also contain a list of
privileges. Privileges define specific access rights. For example,
for a process to use the Win32 debugging API, the
process’s token must contain the
SeDebugPrivilege
privilege.
The primary list of SIDs contained in a token cannot be modified. The token is created for a particular user, and the token must always contain the user’s SID along with the SIDs for each group of which the user is a member. However, each SID in the primary list can be marked with a “deny” attribute, which causes access to be denied when an access control list (ACL) contains a SID that is marked as “deny” in the active token.
Using the CreateRestrictedToken(
)
API, a restricted token can be created from
an existing token. The resulting token can then be used to create a
new process or to set an impersonation token for a thread. In the
former case, the restricted token becomes the newly created
process’s primary token; in the latter case, the
thread can revert back to its primary token, effectively making the
restrictions imposed by the restricted token useful for little more
than helping to prevent accidents.
CreateRestrictedToken( )
requires a large number
of arguments, and it may seem an intimidating function to use, but
with some explanation and examples, it’s not
actually all that difficult. The function has the following
signature:
BOOL CreateRestrictedToken(HANDLE ExistingTokenHandle, DWORD Flags, DWORD DisableSidCount, PSID_AND_ATTRIBUTES SidsToDisable, DWORD DeletePrivilegeCount, PLUID_AND_ATTRIBUTES PrivilegesToDelete, DWORD RestrictedSidCount, PSID_AND_ATTRIBUTES SidsToRestrict, PHANDLE NewTokenHandle);
These functions have the following arguments:
-
ExistingTokenHandle
Handle to an existing token. An existing token handle can be obtained via a call to either
OpenProcessToken( )
orOpenThreadToken( )
. The token may be either a primary or a restricted token. In the latter case, the token may be obtained from an earlier call toCreateRestrictedToken( )
. The existing token handle must have been opened or created withTOKEN_DUPLICATE
access.-
Flags
May be specified as 0 or as a combination of
DISABLE_MAX_PRIVILEGE
orSANDBOX_INERT
. IfDISABLE_MAX_PRIVILEGE
is used, all privileges in the new token are disabled, and the two argumentsDeletePrivilegeCount
andPrivilegesToDelete
are ignored. TheSANDBOX_INERT
has no special meaning other than it is stored in the token, and can be later queried usingGetTokenInformation( )
.-
DisableSidCount
Number of elements in the list
SidsToDisable
. May be specified as 0 if there are no SIDs to be disabled. Disabling a SID is the same as enabling the SIDs “deny” attribute.-
SidsToDisable
List of SIDs for which the “deny” attribute is to be enabled. May be specified as
NULL
if no SIDs are to have the “deny” attribute enabled. See below for information on theSID_AND_ATTRIBUTES
structure.-
DeletePrivilegeCount
Number of elements in the list
PrivilegesToDelete
. May be specified as 0 if there are no privileges to be deleted.-
PrivilegesToDelete
List of privileges to be deleted from the token. May be specified as
NULL
if no privileges are to be deleted. See below for information on theLUID_AND_ATTRIBUTES
structure.-
RestrictedSidCount
Number of elements in the list
SidsToRestrict
. May be specified as 0 if there are no restricted SIDs to be added.-
SidsToRestrict
List of SIDs to restrict. If the existing token is a restricted token that already has restricted SIDs, the resulting token will have a list of restricted SIDs that is the intersection of the existing token’s list and this list. May be specified as
NULL
if no restricted SIDs are to be added to the new token.-
NewTokenHandle
Pointer to a
HANDLE
that will receive the handle to the newly created token.
The function OpenProcessToken(
)
will obtain a handle to the
process’s primary token, while
OpenThreadToken(
)
will obtain a handle to the calling
thread’s impersonation token. Both functions have a
similar signature, though their arguments are treated slightly
differently:
BOOL OpenProcessToken(HANDLE hProcess, DWORD dwDesiredAccess, PHANDLE phToken); BOOL OpenThreadToken(HANDLE hThread, DWORD dwDesiredAccess, BOOL bOpenAsSelf, PHANDLE phToken);
This function has the following arguments:
-
hProcess
Handle to the current process, which is normally obtained via a call to
GetCurrentProcess( )
.-
hThread
Handle to the current thread, which is normally obtained via a call to
GetCurrentThread( )
.-
dwDesiredAccess
Bit mask of the types of access desired for the returned token handle. For creating restricted tokens, this must always include
TOKEN_DUPLICATE
. If the restricted token being created will be used as a primary token for a new process, you must includeTOKEN_ASSIGN_PRIMARY
; otherwise, if the restricted token that will be created will be used as an impersonation token for the thread, you must includeTOKEN_IMPERSONATE
.-
bOpenAsSelf
Boolean flag that determines how the access check for retrieving the thread’s token is performed. If specified as
FALSE
, the access check uses the calling thread’s permissions. If specified asTRUE
, the access check uses the calling process’s permissions.-
phToken
Pointer to a
HANDLE
that will receive the handle to the process’s primary token or the thread’s impersonation token, depending on whether you’re callingOpenProcessToken( )
orOpenThreadToken( )
.
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);
Beginning with Windows .NET Server 2003, support for a new flag has
been added to the function AdjustTokenPrivileges(
)
; it allows a privilege to be removed from a
token, rather than simply disabled. Once the privilege has been
removed, it cannot be added back to the token. In older versions of
Windows, privileges could only be enabled or disabled using
AdjustTokenPrivileges( )
, and there was no way to
remove privileges from a token without duplicating it. There is no
way to substitute another token for a process’s
primary token—the best you can do in older versions of Windows
is to use restricted impersonation tokens.
BOOL AdjustTokenPrivileges(HANDLE TokenHandle, BOOL DisableAllPrivileges, PTOKEN_PRIVILEGES NewState, DWORD BufferLength, PTOKEN_PRIVILEGES PreviousState, PDWORD ReturnLength);
This function has the following arguments:
-
TokenHandle
Handle to the token that is to have its privileges adjusted. The handle must have been opened with
TOKEN_ADJUST_PRIVILEGES
access; in addition, ifPreviousState
is to be filled in, it must haveTOKEN_QUERY
access.-
DisableAllPrivileges
Boolean argument that specifies whether all privileges held by the token are to be disabled. If specified as
TRUE
, all privileges are disabled, and theNewState
argument is ignored. If specified asFALSE
, privileges are adjusted according to the information in theNewState
argument.-
NewState
List of privileges that are to be adjusted, along with the adjustment that is to be made for each. Privileges can be enabled, disabled, and removed. The
TOKEN_PRIVILEGES
structure contains two fields:PrivilegeCount
andPrivileges
.PrivilegeCount
is simply aDWORD
that indicates how many elements are in the array that is thePrivileges
field. ThePrivileges
field is an array ofLUID_AND_ATTRIBUTES
structures, for which theAttributes
field of each element indicates how the privilege is to be adjusted. A value of 0 disables the privilege,SE_PRIVILEGE_ENABLED
enables it, andSE_PRIVILEGE_REMOVED
removes the privilege. See Section 1.2.3.4 later in this section for more information regarding these structures.-
BufferLength
Length in bytes of the
PreviousState
buffer. May be 0 ifPreviousState
isNULL
.-
PreviousState
Buffer into which the state of the token’s privileges prior to adjustment is stored. It may be specified as
NULL
if the information is not required. If the buffer is not specified asNULL
, the token must have been opened withTOKEN_QUERY
access.-
ReturnLength
Pointer to an integer into which the number of bytes written into the
PreviousState
buffer will be placed. May be specified asNULL
ifPreviousState
is alsoNULL
.
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; }
A SID_AND_ATTRIBUTES
structure contains two fields:
Sid
and Attributes
. The
Sid
field is of type PSID
,
which is a variable-sized object that should never be directly
manipulated by application-level code. The meaning of the
Attributes
field varies depending on the use of
the structure. When 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
.
An
LUID_AND_ATTRIBUTES
structure contains two fields:
Luid
and Attributes
. The
Luid
field is of type
LUID
, which is an object that should never be
directly manipulated by application-level code. The meaning of the
Attributes
field varies depending on the use of
the structure. When 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:
-
lpSystemName
Name of the computer on which the privilege value’s name is looked up. This is normally specified as
NULL
, which indicates that only the local system should be searched.-
lpName
Name of the privilege to look up. The Windows platform SDK header file winnt.h defines a sizable number of privilege names as macros that expand to literal strings suitable for use here. Each of these macros begins with
SE_
, which is followed by the name of the privilege. For example, theSeBackupPrivilege
privilege has a corresponding macro namedSE_BACKUP_NAME
.-
lpLuid
Pointer to a caller-allocated
LUID
object that will receive the LUID information if the lookup is successful.LUID
objects are a fixed size, so they may be allocated either dynamically or on the stack.
Get Secure Programming Cookbook for C and C++ now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.