This article is part of a continuing series on Windows NT Security. In the previous article, we covered terminology...below, we continue with a brief segment on basic construction.
Now that we have reviewed the basic terms, we will discuss how to construct these various data structures within your own kernel mode code. Many of these routines are based upon the NT Native API, and several of these functions are present only in the IFS Kit.
Security Identifier
Because the Security Descriptor is used to identify security entities (users, groups, computers, etc.) it is essential that we start by describing how to build security descriptors. In the remainder of this section we will discuss how to obtain an SID for use in your driver.
Using Standard SID Values
One possibility here is to use the set of pre-defined SIDs exported by the operating system. This is part of the SE_EXPORTS structure (from ntifs.h) as shown in Figure 1.
typedef struct _SE_EXPORTS {
// Privilege values
//
LUID SeCreateTokenPrivilege;
LUID SeAssignPrimaryTokenPrivilege;
LUID SeLockMemoryPrivilege;
LUID SeIncreaseQuotaPrivilege;
LUID SeUnsolicitedInputPrivilege;
LUID SeTcbPrivilege;
LUID SeSecurityPrivilege;
LUID SeTakeOwnershipPrivilege;
LUID SeLoadDriverPrivilege;
LUID SeCreatePagefilePrivilege;
LUID SeIncreaseBasePriorityPrivilege;
LUID SeSystemProfilePrivilege;
LUID SeSystemtimePrivilege;
LUID SeProfileSingleProcessPrivilege;
LUID SeCreatePermanentPrivilege;
LUID SeBackupPrivilege;
LUID SeRestorePrivilege;
LUID SeShutdownPrivilege;
LUID SeDebugPrivilege;
LUID SeAuditPrivilege;
LUID SeSystemEnvironmentPrivilege;
LUID SeChangeNotifyPrivilege;
LUID SeRemoteShutdownPrivilege;
// Universally defined Sids
//
PSID SeNullSid;
PSID SeWorldSid;
PSID SeLocalSid;
PSID SeCreatorOwnerSid;
PSID SeCreatorGroupSid;
// Nt defined Sids
//
PSID SeNtAuthoritySid;
PSID SeDialupSid;
PSID SeNetworkSid;
PSID SeBatchSid;
PSID SeInteractiveSid;
PSID SeLocalSystemSid;
PSID SeAliasAdminsSid;
PSID SeAliasUsersSid;
PSID SeAliasGuestsSid;
PSID SeAliasPowerUsersSid;
PSID SeAliasAccountOpsSid;
PSID SeAliasSystemOpsSid;
PSID SeAliasPrintOpsSid;
PSID SeAliasBackupOpsSid;
} SE_EXPORTS, *PSE_EXPORTS;
Figure 1 — Pre-defined SIDs from SE_EXPORTS
Access to these must be “activated” by ensuring that your driver calls the SeEnableAccessToExports macro because a failure to call this function will yield incorrect results. Once enabled, you can use the SeExports variable to access these well-known values.
Constructing an SID
An alternative to using the standard SID values is to construct an SID directly. Typically, this would only be done for the pre-defined values as an alternative to using the exported types. For example, the code snippet in Figure 2 demonstrates how to construct an SID for “local system”.
{
//
// Temporary stack based storage for an SID.
//
UCHAR sidBuffer[64];
PISID localSid = (PISID) sidBuffer;
SID_IDENTIFIER_AUTHORITY localSidAuthority = SECURITY_NT_AUTHORITY;
//
// Build the local system SID
//
RtlZeroMemory(sidBuffer, sizeof(sidBuffer));
localSid->Revision = SID_REVISION;
localSid->SubAuthorityCount = 1;
localSid->IdentifierAuthority = localSidAuthority;
localSid->SubAuthority[0] = SECURITY_LOCAL_SYSTEM_RID;
//
// Make sure it is valid
//
if (!RtlValidSid(localSid)) {
DbgPrint("no dice - SID is invalid\n");
return(1);
}
}
Figure 2 — Constructing a SID for a local system
In this code sample, we used the well-known “local system” authority value (SECURITY_NT_AUTHORITY) and then associated with it a single “Relative Identifier” or RID. This RID value (SECURITY_LOCAL_SYSTEM_RID) is used to indicate that the calling process is part of the operating system.
Lastly, we used RtlValidSid to confirm that we built the SID correctly. While we might not use this in production code, such crosschecks are valuable in debugging new security code.
Extracting the SID from the Token
Another technique, and one that can prove to be quite valuable, is to extract the SID from the token of the current thread or process (See Figure 3). This is often used to determine the caller performing a particular I/O operation. For example, when the CIFS File Server (srv.sys) is performing an operation that requires authentication it impersonates the client. A file system (or filter) can determine the SID of the remote client and act appropriately based upon that information, rather than using the local system SID, which is normally used by SRV because it runs as a kernel mode driver.
NTSTATUS GetCallerSid(PUCHAR SidBuffer, PULONG SidBufferLength)
{
UCHAR buffer[256];
PISID sid = (PISID)&buffer[sizeof(TOKEN_USER)];
NTSTATUS status;
HANDLE handle;
ULONG tokenInfoLength;
LONG length;
// sanity check
//
ASSERT(SidBuffer);
ASSERT(SidBufferLength);
if (*SidBufferLength == 0) {
return STATUS_INSUFFICIENT_RESOURCES;
}
// open the thread token
//
status = ZwOpenThreadToken(NtCurrentThread(), TOKEN_READ, TRUE, &handle);
if (status == STATUS_NO_TOKEN) {
// No thread level token, so use the process
// level token. This is the common case since the only
// time a thread has a token is when it is impersonating.
//
status = ZwOpenProcessToken(NtCurrentProcess(), TOKEN_READ, &handle);
}
//
// This should have succeeded. In this example, we
// crash if it didn't work.
//
if (!NT_SUCCESS(status)) {
return status;
}
// Retrieve the user information from the token.
//
status = ZwQueryInformationToken( handle,
TokenUser,
buffer,
sizeof(buffer),
&tokenInfoLength);
// This call should always work.
//
if (!NT_SUCCESS(status)) {
DbgPrint("ZwQueryInformationToken failure - status %x\n",status);
return status;
}
length = tokenInfoLength - sizeof(TOKEN_USER);
ASSERT(length > 0);
if ((ULONG)length > *SidBufferLength) {
DbgPrint("SidBufferLength too small - expected %d. got %d.\n", length,
*SidBufferLength);
return STATUS_INSUFFICIENT_RESOURCES;
}
// copy the sid
//
*SidBufferLength = (ULONG)length;
RtlCopyMemory(SidBuffer, sid, *SidBufferLength);
//
// for debug, let's see it
//
DbgPrint("SID (Revision %u, SubAuthorityCount %u):\n",
sid->Revision,
sid->SubAuthorityCount);
DbgPrint("PsclUtilsGetSid:\tIdentifierAuthority = %u-%u-%u-%u-%u-%u\n",
sid->IdentifierAuthority.Value[0],
sid->IdentifierAuthority.Value[1],
sid->IdentifierAuthority.Value[2],
sid->IdentifierAuthority.Value[3],
sid->IdentifierAuthority.Value[4],
sid->IdentifierAuthority.Value[5]);
if (sid->SubAuthorityCount) {
ULONG index;
for (index = 0; index < sid->SubAuthorityCount;index++) {
DbgPrint("PsclUtilsGetSid:\tSubAuthority = index %d value %u\n",
index,
sid->SubAuthority[index]);
}
}
return STATUS_SUCCESS;
}
Figure 3 — Technique For Extracting SID From Token
Note the technique used here was specifically designed to work, even in the face of impersonation. This is accomplished by first attempting to open the token for the calling thread. If that fails, the process token is used. This works because normally threads rely upon their process credentials, rather than specific thread credentials. In the case where the thread has credentials, it is probably because the thread is impersonating. A process must have a token.
The routine ZwOpenThreadToken() is not presently included in ntddk.h or ntifs.h but has been discussed and published in a number of support forums. The prototype for this function is shown in Figure 4.
NTSYSAPI
NTSTATUS
NTAPI
ZwOpenThreadToken(HANDLE ThreadHandle,
ACCESS_MASK AccessMask,
BOOLEAN OpenAsSelf,
PHANDLE TokenHandle);
Figure 4 — ZwOpenThreadToken()
The prototypes for NtProcessToken() is included in ntifs.h. Note that ZwProcessToken() has the same prototype, although it uses the normal system call mechanism to invoke NtProcessToken().